@odoo/o-spreadsheet 16.3.29 → 16.3.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/o-spreadsheet.cjs.js +87 -42
- package/dist/o-spreadsheet.esm.js +87 -42
- package/dist/o-spreadsheet.iife.js +87 -42
- package/dist/o-spreadsheet.iife.min.js +260 -260
- package/dist/o_spreadsheet.xml +4 -4
- package/package.json +2 -2
|
@@ -1020,7 +1020,7 @@ function getItemId(item, itemsDic) {
|
|
|
1020
1020
|
}
|
|
1021
1021
|
// Generate new Id if the item didn't exist in the dictionary
|
|
1022
1022
|
const ids = Object.keys(itemsDic);
|
|
1023
|
-
const maxId = ids.length === 0 ? 0 :
|
|
1023
|
+
const maxId = ids.length === 0 ? 0 : largeMax(ids.map((id) => parseInt(id, 10)));
|
|
1024
1024
|
itemsDic[maxId + 1] = item;
|
|
1025
1025
|
return maxId + 1;
|
|
1026
1026
|
}
|
|
@@ -1212,6 +1212,34 @@ function isConsecutive(iterable) {
|
|
|
1212
1212
|
}
|
|
1213
1213
|
}
|
|
1214
1214
|
return true;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Alternative to Math.max that works with large arrays.
|
|
1218
|
+
* Typically useful for arrays bigger than 100k elements.
|
|
1219
|
+
*/
|
|
1220
|
+
function largeMax(array) {
|
|
1221
|
+
let len = array.length;
|
|
1222
|
+
if (len < 100000)
|
|
1223
|
+
return Math.max(...array);
|
|
1224
|
+
let max = -Infinity;
|
|
1225
|
+
while (len--) {
|
|
1226
|
+
max = array[len] > max ? array[len] : max;
|
|
1227
|
+
}
|
|
1228
|
+
return max;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Alternative to Math.min that works with large arrays.
|
|
1232
|
+
* Typically useful for arrays bigger than 100k elements.
|
|
1233
|
+
*/
|
|
1234
|
+
function largeMin(array) {
|
|
1235
|
+
let len = array.length;
|
|
1236
|
+
if (len < 100000)
|
|
1237
|
+
return Math.min(...array);
|
|
1238
|
+
let min = +Infinity;
|
|
1239
|
+
while (len--) {
|
|
1240
|
+
min = array[len] < min ? array[len] : min;
|
|
1241
|
+
}
|
|
1242
|
+
return min;
|
|
1215
1243
|
}
|
|
1216
1244
|
|
|
1217
1245
|
const RBA_REGEX = /rgba?\(|\s+|\)/gi;
|
|
@@ -6823,10 +6851,10 @@ function aggregateDataForLabels(labels, datasets) {
|
|
|
6823
6851
|
}
|
|
6824
6852
|
}
|
|
6825
6853
|
return {
|
|
6826
|
-
labels:
|
|
6854
|
+
labels: Array.from(labelSet),
|
|
6827
6855
|
dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
|
|
6828
6856
|
...dataset,
|
|
6829
|
-
data:
|
|
6857
|
+
data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),
|
|
6830
6858
|
})),
|
|
6831
6859
|
};
|
|
6832
6860
|
}
|
|
@@ -7543,7 +7571,7 @@ function getBestTimeUnitForScale(labels, format) {
|
|
|
7543
7571
|
return undefined;
|
|
7544
7572
|
}
|
|
7545
7573
|
const labelsTimestamps = labelDates.map((date) => date.getTime());
|
|
7546
|
-
const period =
|
|
7574
|
+
const period = largeMax(labelsTimestamps) - largeMin(labelsTimestamps);
|
|
7547
7575
|
const minUnit = getFormatMinDisplayUnit(format);
|
|
7548
7576
|
if (UNIT_LENGTH.second >= UNIT_LENGTH[minUnit] && Milliseconds.inSeconds(period) < 180) {
|
|
7549
7577
|
return "second";
|
|
@@ -7983,7 +8011,7 @@ function getPieConfiguration(chart, labels, dataSetFormat) {
|
|
|
7983
8011
|
}
|
|
7984
8012
|
function getPieColors(colors, dataSetsValues) {
|
|
7985
8013
|
const pieColors = [];
|
|
7986
|
-
const maxLength =
|
|
8014
|
+
const maxLength = largeMax(dataSetsValues.map((ds) => ds.data.length));
|
|
7987
8015
|
for (let i = 0; i <= maxLength; i++) {
|
|
7988
8016
|
pieColors.push(colors.next());
|
|
7989
8017
|
}
|
|
@@ -8946,8 +8974,8 @@ const DELETE_CONTENT_ROWS_NAME = (env) => {
|
|
|
8946
8974
|
let last;
|
|
8947
8975
|
const activesRows = env.model.getters.getActiveRows();
|
|
8948
8976
|
if (activesRows.size !== 0) {
|
|
8949
|
-
first =
|
|
8950
|
-
last =
|
|
8977
|
+
first = largeMin([...activesRows]);
|
|
8978
|
+
last = largeMax([...activesRows]);
|
|
8951
8979
|
}
|
|
8952
8980
|
else {
|
|
8953
8981
|
const zone = env.model.getters.getSelectedZones()[0];
|
|
@@ -8975,8 +9003,8 @@ const DELETE_CONTENT_COLUMNS_NAME = (env) => {
|
|
|
8975
9003
|
let last;
|
|
8976
9004
|
const activeCols = env.model.getters.getActiveCols();
|
|
8977
9005
|
if (activeCols.size !== 0) {
|
|
8978
|
-
first =
|
|
8979
|
-
last =
|
|
9006
|
+
first = largeMin([...activeCols]);
|
|
9007
|
+
last = largeMax([...activeCols]);
|
|
8980
9008
|
}
|
|
8981
9009
|
else {
|
|
8982
9010
|
const zone = env.model.getters.getSelectedZones()[0];
|
|
@@ -9004,8 +9032,8 @@ const REMOVE_ROWS_NAME = (env) => {
|
|
|
9004
9032
|
let last;
|
|
9005
9033
|
const activesRows = env.model.getters.getActiveRows();
|
|
9006
9034
|
if (activesRows.size !== 0) {
|
|
9007
|
-
first =
|
|
9008
|
-
last =
|
|
9035
|
+
first = largeMin([...activesRows]);
|
|
9036
|
+
last = largeMax([...activesRows]);
|
|
9009
9037
|
}
|
|
9010
9038
|
else {
|
|
9011
9039
|
const zone = env.model.getters.getSelectedZones()[0];
|
|
@@ -9046,8 +9074,8 @@ const REMOVE_COLUMNS_NAME = (env) => {
|
|
|
9046
9074
|
let last;
|
|
9047
9075
|
const activeCols = env.model.getters.getActiveCols();
|
|
9048
9076
|
if (activeCols.size !== 0) {
|
|
9049
|
-
first =
|
|
9050
|
-
last =
|
|
9077
|
+
first = largeMin([...activeCols]);
|
|
9078
|
+
last = largeMax([...activeCols]);
|
|
9051
9079
|
}
|
|
9052
9080
|
else {
|
|
9053
9081
|
const zone = env.model.getters.getSelectedZones()[0];
|
|
@@ -9130,7 +9158,7 @@ const INSERT_ROWS_BEFORE_ACTION = (env) => {
|
|
|
9130
9158
|
let row;
|
|
9131
9159
|
let quantity;
|
|
9132
9160
|
if (activeRows.size) {
|
|
9133
|
-
row =
|
|
9161
|
+
row = largeMin([...activeRows]);
|
|
9134
9162
|
quantity = activeRows.size;
|
|
9135
9163
|
}
|
|
9136
9164
|
else {
|
|
@@ -9162,7 +9190,7 @@ const INSERT_ROWS_AFTER_ACTION = (env) => {
|
|
|
9162
9190
|
let row;
|
|
9163
9191
|
let quantity;
|
|
9164
9192
|
if (activeRows.size) {
|
|
9165
|
-
row =
|
|
9193
|
+
row = largeMax([...activeRows]);
|
|
9166
9194
|
quantity = activeRows.size;
|
|
9167
9195
|
}
|
|
9168
9196
|
else {
|
|
@@ -9207,7 +9235,7 @@ const INSERT_COLUMNS_BEFORE_ACTION = (env) => {
|
|
|
9207
9235
|
let column;
|
|
9208
9236
|
let quantity;
|
|
9209
9237
|
if (activeCols.size) {
|
|
9210
|
-
column =
|
|
9238
|
+
column = largeMin([...activeCols]);
|
|
9211
9239
|
quantity = activeCols.size;
|
|
9212
9240
|
}
|
|
9213
9241
|
else {
|
|
@@ -9241,7 +9269,7 @@ const INSERT_COLUMNS_AFTER_ACTION = (env) => {
|
|
|
9241
9269
|
let column;
|
|
9242
9270
|
let quantity;
|
|
9243
9271
|
if (activeCols.size) {
|
|
9244
|
-
column =
|
|
9272
|
+
column = largeMax([...activeCols]);
|
|
9245
9273
|
quantity = activeCols.size;
|
|
9246
9274
|
}
|
|
9247
9275
|
else {
|
|
@@ -16549,14 +16577,19 @@ const categorieFunctionAll = {
|
|
|
16549
16577
|
children: [allFunctionListMenuBuilder],
|
|
16550
16578
|
};
|
|
16551
16579
|
function allFunctionListMenuBuilder() {
|
|
16552
|
-
const fnNames = functionRegistry.getKeys();
|
|
16580
|
+
const fnNames = functionRegistry.getKeys().filter((key) => !functionRegistry.get(key).hidden);
|
|
16553
16581
|
return createFormulaFunctions(fnNames);
|
|
16554
16582
|
}
|
|
16555
16583
|
const categoriesFunctionListMenuBuilder = () => {
|
|
16556
16584
|
const functions = functionRegistry.content;
|
|
16557
|
-
const categories = [
|
|
16585
|
+
const categories = [
|
|
16586
|
+
...new Set(functionRegistry
|
|
16587
|
+
.getAll()
|
|
16588
|
+
.filter((fn) => !fn.hidden)
|
|
16589
|
+
.map((fn) => fn.category)),
|
|
16590
|
+
].filter(isDefined$1);
|
|
16558
16591
|
return categories.sort().map((category, i) => {
|
|
16559
|
-
const functionsInCategory = Object.keys(functions).filter((key) => functions[key].category === category);
|
|
16592
|
+
const functionsInCategory = Object.keys(functions).filter((key) => functions[key].category === category && !functions[key].hidden);
|
|
16560
16593
|
return {
|
|
16561
16594
|
name: category,
|
|
16562
16595
|
children: createFormulaFunctions(functionsInCategory),
|
|
@@ -20216,6 +20249,9 @@ class FigureComponent extends owl.Component {
|
|
|
20216
20249
|
el?.focus({ preventScroll: true });
|
|
20217
20250
|
}
|
|
20218
20251
|
}, () => [this.env.model.getters.getSelectedFigureId(), this.props.figure.id, this.figureRef.el]);
|
|
20252
|
+
owl.onWillUnmount(() => {
|
|
20253
|
+
this.props.onFigureDeleted();
|
|
20254
|
+
});
|
|
20219
20255
|
}
|
|
20220
20256
|
clickAnchor(dirX, dirY, ev) {
|
|
20221
20257
|
this.props.onClickAnchor(dirX, dirY, ev);
|
|
@@ -20234,6 +20270,7 @@ class FigureComponent extends owl.Component {
|
|
|
20234
20270
|
this.props.onFigureDeleted();
|
|
20235
20271
|
ev.stopPropagation();
|
|
20236
20272
|
ev.preventDefault();
|
|
20273
|
+
ev.stopPropagation();
|
|
20237
20274
|
break;
|
|
20238
20275
|
case "ArrowDown":
|
|
20239
20276
|
case "ArrowLeft":
|
|
@@ -20254,6 +20291,7 @@ class FigureComponent extends owl.Component {
|
|
|
20254
20291
|
});
|
|
20255
20292
|
ev.stopPropagation();
|
|
20256
20293
|
ev.preventDefault();
|
|
20294
|
+
ev.stopPropagation();
|
|
20257
20295
|
break;
|
|
20258
20296
|
}
|
|
20259
20297
|
}
|
|
@@ -21062,6 +21100,10 @@ class Composer extends owl.Component {
|
|
|
21062
21100
|
if (!this.isKeyStillDown) {
|
|
21063
21101
|
this.processContent();
|
|
21064
21102
|
}
|
|
21103
|
+
// Required because typing '=SUM' and double-clicking another cell leaves ShowProvider and ShowDescription true
|
|
21104
|
+
if (this.env.model.getters.getEditionMode() === "inactive") {
|
|
21105
|
+
this.processTokenAtCursor();
|
|
21106
|
+
}
|
|
21065
21107
|
});
|
|
21066
21108
|
}
|
|
21067
21109
|
// ---------------------------------------------------------------------------
|
|
@@ -26915,7 +26957,7 @@ function convertHyperlink(link, cellValue, warningManager) {
|
|
|
26915
26957
|
function getSheetDims(sheet) {
|
|
26916
26958
|
const dims = [0, 0];
|
|
26917
26959
|
for (let row of sheet.rows) {
|
|
26918
|
-
dims[0] = Math.max(dims[0],
|
|
26960
|
+
dims[0] = Math.max(dims[0], largeMax(row.cells.map((cell) => toCartesian(cell.xc).col)));
|
|
26919
26961
|
dims[1] = Math.max(dims[1], row.index);
|
|
26920
26962
|
}
|
|
26921
26963
|
dims[0] = Math.max(dims[0], EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS);
|
|
@@ -31202,7 +31244,7 @@ class HeaderSizePlugin extends CorePlugin {
|
|
|
31202
31244
|
for (let headerIndex of [...cmd.elements].sort((a, b) => b - a)) {
|
|
31203
31245
|
sizes.splice(headerIndex, 1);
|
|
31204
31246
|
}
|
|
31205
|
-
const min =
|
|
31247
|
+
const min = largeMin(cmd.elements);
|
|
31206
31248
|
sizes = sizes.map((size, row) => {
|
|
31207
31249
|
if (cmd.dimension === "ROW" && row >= min) {
|
|
31208
31250
|
// invalidate sizes
|
|
@@ -31429,7 +31471,7 @@ class HeaderVisibilityPlugin extends CorePlugin {
|
|
|
31429
31471
|
if (hiddenElements.size >= elements) {
|
|
31430
31472
|
return 67 /* CommandResult.TooManyHiddenElements */;
|
|
31431
31473
|
}
|
|
31432
|
-
else if (
|
|
31474
|
+
else if (largeMin(cmd.elements) < 0 || largeMax(cmd.elements) > elements) {
|
|
31433
31475
|
return 87 /* CommandResult.InvalidHeaderIndex */;
|
|
31434
31476
|
}
|
|
31435
31477
|
else {
|
|
@@ -32202,8 +32244,8 @@ class RangeAdapter {
|
|
|
32202
32244
|
let newRange = range;
|
|
32203
32245
|
let changeType = "NONE";
|
|
32204
32246
|
for (let group of groups) {
|
|
32205
|
-
const min =
|
|
32206
|
-
const max =
|
|
32247
|
+
const min = largeMin(group);
|
|
32248
|
+
const max = largeMax(group);
|
|
32207
32249
|
if (range.zone[start] <= min && min <= range.zone[end]) {
|
|
32208
32250
|
const toRemove = Math.min(range.zone[end], max) - min + 1;
|
|
32209
32251
|
changeType = "RESIZE";
|
|
@@ -32634,7 +32676,7 @@ class SheetPlugin extends CorePlugin {
|
|
|
32634
32676
|
const elements = cmd.dimension === "COL"
|
|
32635
32677
|
? this.getNumberCols(cmd.sheetId)
|
|
32636
32678
|
: this.getNumberRows(cmd.sheetId);
|
|
32637
|
-
if (
|
|
32679
|
+
if (largeMin(cmd.elements) < 0 || largeMax(cmd.elements) > elements) {
|
|
32638
32680
|
return 87 /* CommandResult.InvalidHeaderIndex */;
|
|
32639
32681
|
}
|
|
32640
32682
|
else if (this.checkElementsIncludeAllNonFrozenHeaders(cmd.sheetId, cmd.dimension, cmd.elements)) {
|
|
@@ -34136,13 +34178,13 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
|
|
|
34136
34178
|
.map((cell) => cell.value);
|
|
34137
34179
|
switch (threshold.type) {
|
|
34138
34180
|
case "value":
|
|
34139
|
-
const result = functionName === "max" ?
|
|
34181
|
+
const result = functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
|
|
34140
34182
|
return result;
|
|
34141
34183
|
case "number":
|
|
34142
34184
|
return Number(threshold.value);
|
|
34143
34185
|
case "percentage":
|
|
34144
|
-
const min =
|
|
34145
|
-
const max =
|
|
34186
|
+
const min = largeMin(rangeValues);
|
|
34187
|
+
const max = largeMax(rangeValues);
|
|
34146
34188
|
const delta = max - min;
|
|
34147
34189
|
return min + (delta * Number(threshold.value)) / 100;
|
|
34148
34190
|
case "percentile":
|
|
@@ -36092,13 +36134,13 @@ class AutomaticSumPlugin extends UIPlugin {
|
|
|
36092
36134
|
const cells = this.getters.getEvaluatedCellsInZone(sheet.id, zone);
|
|
36093
36135
|
const cellPositions = range(end, -1, -1);
|
|
36094
36136
|
const invalidCells = cellPositions.filter((position) => cells[position] && !cells[position].isAutoSummable);
|
|
36095
|
-
const maxValidPosition =
|
|
36137
|
+
const maxValidPosition = largeMax(invalidCells);
|
|
36096
36138
|
const numberSequences = groupConsecutive(cellPositions.filter((position) => this.isNumber(cells[position])));
|
|
36097
36139
|
const firstSequence = numberSequences[0] || [];
|
|
36098
|
-
if (
|
|
36140
|
+
if (largeMax(firstSequence) < maxValidPosition) {
|
|
36099
36141
|
return Infinity;
|
|
36100
36142
|
}
|
|
36101
|
-
return
|
|
36143
|
+
return largeMin(firstSequence);
|
|
36102
36144
|
}
|
|
36103
36145
|
shouldFindData(sheetId, zone) {
|
|
36104
36146
|
return this.getters.isEmpty(sheetId, zone) || this.getters.isSingleCellOrMerge(sheetId, zone);
|
|
@@ -37683,7 +37725,7 @@ class RendererPlugin extends UIPlugin {
|
|
|
37683
37725
|
* column of the current viewport
|
|
37684
37726
|
*/
|
|
37685
37727
|
getColDimensionsInViewport(sheetId, col) {
|
|
37686
|
-
const left =
|
|
37728
|
+
const left = largeMin(this.getters.getSheetViewVisibleCols());
|
|
37687
37729
|
const start = this.getters.getColRowOffsetInViewport("COL", left, col);
|
|
37688
37730
|
const size = this.getters.getColSize(sheetId, col);
|
|
37689
37731
|
const isColHidden = this.getters.isColHidden(sheetId, col);
|
|
@@ -37698,7 +37740,7 @@ class RendererPlugin extends UIPlugin {
|
|
|
37698
37740
|
* of the current viewport
|
|
37699
37741
|
*/
|
|
37700
37742
|
getRowDimensionsInViewport(sheetId, row) {
|
|
37701
|
-
const top =
|
|
37743
|
+
const top = largeMin(this.getters.getSheetViewVisibleRows());
|
|
37702
37744
|
const start = this.getters.getColRowOffsetInViewport("ROW", top, row);
|
|
37703
37745
|
const size = this.getters.getRowSize(sheetId, row);
|
|
37704
37746
|
const isRowHidden = this.getters.isRowHidden(sheetId, row);
|
|
@@ -39226,7 +39268,7 @@ class SheetUIPlugin extends UIPlugin {
|
|
|
39226
39268
|
getColMaxWidth(sheetId, index) {
|
|
39227
39269
|
const cellsPositions = positions(this.getters.getColsZone(sheetId, index, index));
|
|
39228
39270
|
const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));
|
|
39229
|
-
return Math.max(0,
|
|
39271
|
+
return Math.max(0, largeMax(sizes));
|
|
39230
39272
|
}
|
|
39231
39273
|
splitWordToSpecificWidth(ctx, word, width, style) {
|
|
39232
39274
|
const wordWidth = computeTextWidth(ctx, word, style);
|
|
@@ -41683,6 +41725,7 @@ class GridSelectionPlugin extends UIPlugin {
|
|
|
41683
41725
|
this.gridSelection.zones = this.gridSelection.zones.map((z) => this.getters.expandZone(sheetId, z));
|
|
41684
41726
|
this.gridSelection.anchor.zone = this.getters.expandZone(sheetId, this.gridSelection.anchor.zone);
|
|
41685
41727
|
this.setSelectionMixin(this.gridSelection.anchor, this.gridSelection.zones);
|
|
41728
|
+
this.selectedFigureId = null;
|
|
41686
41729
|
break;
|
|
41687
41730
|
}
|
|
41688
41731
|
/** Any change to the selection has to be reflected in the selection processor. */
|
|
@@ -42488,12 +42531,14 @@ class BottomBarSheet extends owl.Component {
|
|
|
42488
42531
|
this.editionState = "initializing";
|
|
42489
42532
|
}
|
|
42490
42533
|
stopEdition() {
|
|
42491
|
-
|
|
42534
|
+
const input = this.sheetNameRef.el;
|
|
42535
|
+
if (!this.state.isEditing || !input)
|
|
42492
42536
|
return;
|
|
42493
42537
|
this.state.isEditing = false;
|
|
42494
42538
|
this.editionState = "initializing";
|
|
42495
|
-
|
|
42539
|
+
input.blur();
|
|
42496
42540
|
const inputValue = this.getInputContent() || "";
|
|
42541
|
+
input.innerText = inputValue;
|
|
42497
42542
|
interactiveRenameSheet(this.env, this.props.sheetId, inputValue, () => this.startEdition());
|
|
42498
42543
|
}
|
|
42499
42544
|
cancelEdition() {
|
|
@@ -42954,7 +42999,7 @@ class BottomBar extends owl.Component {
|
|
|
42954
42999
|
this.sheetListRef.el.scrollTo({ top: 0, left: scroll, behavior: "smooth" });
|
|
42955
43000
|
}
|
|
42956
43001
|
onSheetMouseDown(sheetId, event) {
|
|
42957
|
-
if (event.button !== 0)
|
|
43002
|
+
if (event.button !== 0 || this.env.model.getters.isReadonly())
|
|
42958
43003
|
return;
|
|
42959
43004
|
this.closeMenu();
|
|
42960
43005
|
const mouseX = event.clientX;
|
|
@@ -46128,7 +46173,7 @@ function addLineChart(chart) {
|
|
|
46128
46173
|
}
|
|
46129
46174
|
function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {
|
|
46130
46175
|
const colors = new ChartColors();
|
|
46131
|
-
const maxLength =
|
|
46176
|
+
const maxLength = largeMax(chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
|
|
46132
46177
|
const doughnutColors = range(0, maxLength).map(() => toXlsxHexColor(colors.next()));
|
|
46133
46178
|
const dataSetsNodes = [];
|
|
46134
46179
|
for (const [dsIndex, dataset] of Object.entries(chart.dataSets).reverse()) {
|
|
@@ -48049,6 +48094,6 @@ exports.setTranslationMethod = setTranslationMethod;
|
|
|
48049
48094
|
exports.tokenize = tokenize;
|
|
48050
48095
|
|
|
48051
48096
|
|
|
48052
|
-
__info__.version = '16.3.
|
|
48053
|
-
__info__.date = '2024-
|
|
48054
|
-
__info__.hash = '
|
|
48097
|
+
__info__.version = '16.3.31';
|
|
48098
|
+
__info__.date = '2024-04-05T13:57:05.657Z';
|
|
48099
|
+
__info__.hash = '052efd4';
|