@odoo/o-spreadsheet 17.0.70 → 17.0.71

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.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 17.0.70
6
- * @date 2025-07-28T13:26:37.626Z
7
- * @hash 2ec8d82
5
+ * @version 17.0.71
6
+ * @date 2025-08-21T06:37:34.192Z
7
+ * @hash 1592964
8
8
  */
9
9
 
10
10
  import { Component, useRef, onMounted, useEffect, onWillPatch, useState, onWillUpdateProps, onPatched, useComponent, useExternalListener, onWillUnmount, onWillStart, xml, useChildSubEnv, useSubEnv, markRaw } from '@odoo/owl';
@@ -30693,6 +30693,897 @@ const NON_RETROCOMPATIBLE_FUNCTIONS = [
30693
30693
  ];
30694
30694
  const CONTENT_TYPES_FILE = "[Content_Types].xml";
30695
30695
 
30696
+ class FunctionCodeBuilder {
30697
+ scope;
30698
+ code = "";
30699
+ constructor(scope = new Scope()) {
30700
+ this.scope = scope;
30701
+ }
30702
+ append(...lines) {
30703
+ this.code += lines.map((line) => line.toString()).join("\n") + "\n";
30704
+ }
30705
+ return(expression) {
30706
+ return new FunctionCodeImpl(this.scope, this.code, expression);
30707
+ }
30708
+ toString() {
30709
+ return indentCode(this.code);
30710
+ }
30711
+ }
30712
+ class FunctionCodeImpl {
30713
+ scope;
30714
+ returnExpression;
30715
+ code;
30716
+ constructor(scope, code, returnExpression) {
30717
+ this.scope = scope;
30718
+ this.returnExpression = returnExpression;
30719
+ this.code = indentCode(code);
30720
+ }
30721
+ toString() {
30722
+ return this.code;
30723
+ }
30724
+ wrapInClosure() {
30725
+ const closureName = this.scope.nextVariableName();
30726
+ const code = new FunctionCodeBuilder(this.scope);
30727
+ code.append(`const ${closureName} = () => {`);
30728
+ code.append(this.code);
30729
+ code.append(`return ${this.returnExpression};`);
30730
+ code.append(`}`);
30731
+ return code.return(closureName);
30732
+ }
30733
+ assignResultToVariable() {
30734
+ if (this.scope.isAlreadyDeclared(this.returnExpression)) {
30735
+ return this;
30736
+ }
30737
+ const variableName = this.scope.nextVariableName();
30738
+ const code = new FunctionCodeBuilder(this.scope);
30739
+ code.append(this.code);
30740
+ code.append(`const ${variableName} = ${this.returnExpression};`);
30741
+ return code.return(variableName);
30742
+ }
30743
+ }
30744
+ class Scope {
30745
+ nextId = 1;
30746
+ declaredVariables = new Set();
30747
+ nextVariableName() {
30748
+ const name = `_${this.nextId++}`;
30749
+ this.declaredVariables.add(name);
30750
+ return name;
30751
+ }
30752
+ isAlreadyDeclared(name) {
30753
+ return this.declaredVariables.has(name);
30754
+ }
30755
+ }
30756
+ /**
30757
+ * Takes a list of strings that might be single or multiline
30758
+ * and maps them in a list of single line strings.
30759
+ */
30760
+ function splitLines(str) {
30761
+ return str
30762
+ .split("\n")
30763
+ .map((line) => line.trim())
30764
+ .filter((line) => line !== "");
30765
+ }
30766
+ function indentCode(code) {
30767
+ let result = "";
30768
+ let indentLevel = 0;
30769
+ const lines = splitLines(code);
30770
+ for (const line of lines) {
30771
+ if (line.startsWith("}")) {
30772
+ indentLevel--;
30773
+ }
30774
+ result += "\t".repeat(indentLevel) + line + "\n";
30775
+ if (line.endsWith("{")) {
30776
+ indentLevel++;
30777
+ }
30778
+ }
30779
+ return result.trim();
30780
+ }
30781
+
30782
+ var State;
30783
+ (function (State) {
30784
+ /**
30785
+ * Initial state.
30786
+ * Expecting any reference for the left part of a range
30787
+ * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A"
30788
+ */
30789
+ State[State["LeftRef"] = 0] = "LeftRef";
30790
+ /**
30791
+ * Expecting any reference for the right part of a range
30792
+ * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A"
30793
+ */
30794
+ State[State["RightRef"] = 1] = "RightRef";
30795
+ /**
30796
+ * Expecting the separator without any constraint on the right part
30797
+ */
30798
+ State[State["Separator"] = 2] = "Separator";
30799
+ /**
30800
+ * Expecting the separator for a full column range
30801
+ */
30802
+ State[State["FullColumnSeparator"] = 3] = "FullColumnSeparator";
30803
+ /**
30804
+ * Expecting the separator for a full row range
30805
+ */
30806
+ State[State["FullRowSeparator"] = 4] = "FullRowSeparator";
30807
+ /**
30808
+ * Expecting the right part of a full column range
30809
+ * e.g. "1", "A1"
30810
+ */
30811
+ State[State["RightColumnRef"] = 5] = "RightColumnRef";
30812
+ /**
30813
+ * Expecting the right part of a full row range
30814
+ * e.g. "A", "A1"
30815
+ */
30816
+ State[State["RightRowRef"] = 6] = "RightRowRef";
30817
+ /**
30818
+ * Final state. A range has been matched
30819
+ */
30820
+ State[State["Found"] = 7] = "Found";
30821
+ })(State || (State = {}));
30822
+ const goTo = (state, guard = () => true) => [
30823
+ {
30824
+ goTo: state,
30825
+ guard,
30826
+ },
30827
+ ];
30828
+ const goToMulti = (state, guard = () => true) => ({
30829
+ goTo: state,
30830
+ guard,
30831
+ });
30832
+ const machine = {
30833
+ [State.LeftRef]: {
30834
+ REFERENCE: goTo(State.Separator),
30835
+ NUMBER: goTo(State.FullRowSeparator),
30836
+ SYMBOL: [
30837
+ goToMulti(State.FullColumnSeparator, (token) => isColReference(token.value)),
30838
+ goToMulti(State.FullRowSeparator, (token) => isRowReference(token.value)),
30839
+ ],
30840
+ },
30841
+ [State.FullColumnSeparator]: {
30842
+ SPACE: goTo(State.FullColumnSeparator),
30843
+ OPERATOR: goTo(State.RightColumnRef, (token) => token.value === ":"),
30844
+ },
30845
+ [State.FullRowSeparator]: {
30846
+ SPACE: goTo(State.FullRowSeparator),
30847
+ OPERATOR: goTo(State.RightRowRef, (token) => token.value === ":"),
30848
+ },
30849
+ [State.Separator]: {
30850
+ SPACE: goTo(State.Separator),
30851
+ OPERATOR: goTo(State.RightRef, (token) => token.value === ":"),
30852
+ },
30853
+ [State.RightRef]: {
30854
+ SPACE: goTo(State.RightRef),
30855
+ NUMBER: goTo(State.Found),
30856
+ REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
30857
+ SYMBOL: goTo(State.Found, (token) => isColHeader(token.value) || isRowHeader(token.value)),
30858
+ },
30859
+ [State.RightColumnRef]: {
30860
+ SPACE: goTo(State.RightColumnRef),
30861
+ SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)),
30862
+ REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
30863
+ },
30864
+ [State.RightRowRef]: {
30865
+ SPACE: goTo(State.RightRowRef),
30866
+ NUMBER: goTo(State.Found),
30867
+ REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
30868
+ SYMBOL: goTo(State.Found, (token) => isRowHeader(token.value)),
30869
+ },
30870
+ [State.Found]: {},
30871
+ };
30872
+ /**
30873
+ * Check if the list of tokens starts with a sequence of tokens representing
30874
+ * a range.
30875
+ * If a range is found, the sequence is removed from the list and is returned
30876
+ * as a single token.
30877
+ */
30878
+ function matchReference(tokens) {
30879
+ let head = 0;
30880
+ let transitions = machine[State.LeftRef];
30881
+ let matchedTokens = "";
30882
+ while (transitions !== undefined) {
30883
+ const token = tokens[head++];
30884
+ if (!token) {
30885
+ return null;
30886
+ }
30887
+ const transition = transitions[token.type]?.find((transition) => transition.guard(token));
30888
+ const nextState = transition ? transition.goTo : undefined;
30889
+ switch (nextState) {
30890
+ case undefined:
30891
+ return null;
30892
+ case State.Found:
30893
+ matchedTokens += token.value;
30894
+ tokens.splice(0, head);
30895
+ return {
30896
+ type: "REFERENCE",
30897
+ value: matchedTokens,
30898
+ };
30899
+ default:
30900
+ transitions = machine[nextState];
30901
+ matchedTokens += token.value;
30902
+ break;
30903
+ }
30904
+ }
30905
+ return null;
30906
+ }
30907
+ /**
30908
+ * Take the result of the tokenizer and transform it to be usable in the
30909
+ * manipulations of range
30910
+ *
30911
+ * @param formula
30912
+ */
30913
+ function rangeTokenize(formula, locale = DEFAULT_LOCALE) {
30914
+ const tokens = tokenize(formula, locale);
30915
+ const result = [];
30916
+ while (tokens.length) {
30917
+ result.push(matchReference(tokens) || tokens.shift());
30918
+ }
30919
+ return result;
30920
+ }
30921
+
30922
+ const functionRegex = /[a-zA-Z0-9\_]+(\.[a-zA-Z0-9\_]+)*/;
30923
+ const UNARY_OPERATORS_PREFIX = ["-", "+"];
30924
+ const UNARY_OPERATORS_POSTFIX = ["%"];
30925
+ const ASSOCIATIVE_OPERATORS = ["*", "+", "&"];
30926
+ const OP_PRIORITY = {
30927
+ "^": 30,
30928
+ "%": 30,
30929
+ "*": 20,
30930
+ "/": 20,
30931
+ "+": 15,
30932
+ "-": 15,
30933
+ "&": 13,
30934
+ ">": 10,
30935
+ "<>": 10,
30936
+ ">=": 10,
30937
+ "<": 10,
30938
+ "<=": 10,
30939
+ "=": 10,
30940
+ };
30941
+ /**
30942
+ * Parse the next operand in an arithmetic expression.
30943
+ * e.g.
30944
+ * for 1+2*3, the next operand is 1
30945
+ * for (1+2)*3, the next operand is (1+2)
30946
+ * for SUM(1,2)+3, the next operand is SUM(1,2)
30947
+ */
30948
+ function parseOperand(tokens) {
30949
+ const current = tokens.shift();
30950
+ if (!current) {
30951
+ throw new BadExpressionError(DEFAULT_ERROR_MESSAGE);
30952
+ }
30953
+ switch (current.type) {
30954
+ case "DEBUGGER":
30955
+ const next = parseExpression(tokens, 1000);
30956
+ next.debug = true;
30957
+ return next;
30958
+ case "NUMBER":
30959
+ return { type: "NUMBER", value: parseNumber(current.value, DEFAULT_LOCALE) };
30960
+ case "STRING":
30961
+ return { type: "STRING", value: removeStringQuotes(current.value) };
30962
+ case "INVALID_REFERENCE":
30963
+ return {
30964
+ type: "REFERENCE",
30965
+ value: CellErrorType.InvalidReference,
30966
+ };
30967
+ case "REFERENCE":
30968
+ if (tokens[0]?.value === ":" && tokens[1]?.type === "REFERENCE") {
30969
+ tokens.shift();
30970
+ const rightReference = tokens.shift();
30971
+ return {
30972
+ type: "REFERENCE",
30973
+ value: `${current.value}:${rightReference?.value}`,
30974
+ };
30975
+ }
30976
+ return {
30977
+ type: "REFERENCE",
30978
+ value: current.value,
30979
+ };
30980
+ case "SYMBOL":
30981
+ const value = current.value;
30982
+ const nextToken = tokens[0];
30983
+ if (nextToken?.type === "LEFT_PAREN" && functionRegex.test(current.value)) {
30984
+ const args = parseFunctionArgs(tokens);
30985
+ return { type: "FUNCALL", value: value, args };
30986
+ }
30987
+ const upperCaseValue = value.toUpperCase();
30988
+ if (upperCaseValue === "TRUE" || upperCaseValue === "FALSE") {
30989
+ return { type: "BOOLEAN", value: upperCaseValue === "TRUE" };
30990
+ }
30991
+ throw new BadExpressionError(_t("Invalid formula"));
30992
+ case "LEFT_PAREN":
30993
+ const result = parseExpression(tokens);
30994
+ consumeOrThrow(tokens, "RIGHT_PAREN", _t("Missing closing parenthesis"));
30995
+ return result;
30996
+ case "OPERATOR":
30997
+ const operator = current.value;
30998
+ if (UNARY_OPERATORS_PREFIX.includes(operator)) {
30999
+ return {
31000
+ type: "UNARY_OPERATION",
31001
+ value: operator,
31002
+ operand: parseExpression(tokens, OP_PRIORITY[operator]),
31003
+ };
31004
+ }
31005
+ throw new BadExpressionError(_t("Unexpected token: %s", current.value));
31006
+ default:
31007
+ throw new BadExpressionError(_t("Unexpected token: %s", current.value));
31008
+ }
31009
+ }
31010
+ function parseFunctionArgs(tokens) {
31011
+ consumeOrThrow(tokens, "LEFT_PAREN", _t("Missing opening parenthesis"));
31012
+ const nextToken = tokens[0];
31013
+ if (nextToken?.type === "RIGHT_PAREN") {
31014
+ consumeOrThrow(tokens, "RIGHT_PAREN");
31015
+ return [];
31016
+ }
31017
+ const args = [];
31018
+ args.push(parseOneFunctionArg(tokens));
31019
+ while (tokens[0]?.type !== "RIGHT_PAREN") {
31020
+ consumeOrThrow(tokens, "ARG_SEPARATOR", _t("Wrong function call"));
31021
+ args.push(parseOneFunctionArg(tokens));
31022
+ }
31023
+ consumeOrThrow(tokens, "RIGHT_PAREN");
31024
+ return args;
31025
+ }
31026
+ function parseOneFunctionArg(tokens) {
31027
+ const nextToken = tokens[0];
31028
+ if (nextToken?.type === "ARG_SEPARATOR" || nextToken?.type === "RIGHT_PAREN") {
31029
+ // arg is empty: "sum(1,,2)" "sum(,1)" "sum(1,)"
31030
+ return { type: "EMPTY", value: "" };
31031
+ }
31032
+ return parseExpression(tokens);
31033
+ }
31034
+ function consumeOrThrow(tokens, type, message = DEFAULT_ERROR_MESSAGE) {
31035
+ const token = tokens.shift();
31036
+ if (!token || token.type !== type) {
31037
+ throw new BadExpressionError(message);
31038
+ }
31039
+ }
31040
+ function parseExpression(tokens, parent_priority = 0) {
31041
+ if (tokens.length === 0) {
31042
+ throw new BadExpressionError(DEFAULT_ERROR_MESSAGE);
31043
+ }
31044
+ let left = parseOperand(tokens);
31045
+ // as long as we have operators with higher priority than the parent one,
31046
+ // continue parsing the expression because it is a child sub-expression
31047
+ while (tokens[0]?.type === "OPERATOR" && OP_PRIORITY[tokens[0].value] > parent_priority) {
31048
+ const operator = tokens.shift().value;
31049
+ if (UNARY_OPERATORS_POSTFIX.includes(operator)) {
31050
+ left = {
31051
+ type: "UNARY_OPERATION",
31052
+ value: operator,
31053
+ operand: left,
31054
+ postfix: true,
31055
+ };
31056
+ }
31057
+ else {
31058
+ const right = parseExpression(tokens, OP_PRIORITY[operator]);
31059
+ left = {
31060
+ type: "BIN_OPERATION",
31061
+ value: operator,
31062
+ left,
31063
+ right,
31064
+ };
31065
+ }
31066
+ }
31067
+ return left;
31068
+ }
31069
+ /**
31070
+ * Parse an expression (as a string) into an AST.
31071
+ */
31072
+ function parse(str) {
31073
+ return parseTokens(rangeTokenize(str));
31074
+ }
31075
+ function parseTokens(tokens) {
31076
+ tokens = tokens.filter((x) => x.type !== "SPACE");
31077
+ if (tokens[0]?.value === "=") {
31078
+ tokens.splice(0, 1);
31079
+ }
31080
+ const result = parseExpression(tokens);
31081
+ if (tokens.length) {
31082
+ throw new BadExpressionError(DEFAULT_ERROR_MESSAGE);
31083
+ }
31084
+ return result;
31085
+ }
31086
+ /**
31087
+ * Allows to visit all nodes of an AST and apply a mapping function
31088
+ * to nodes of a specific type.
31089
+ * Useful if you want to convert some part of a formula.
31090
+ *
31091
+ * @example
31092
+ * convertAstNodes(ast, "FUNCALL", convertFormulaToExcel)
31093
+ *
31094
+ * function convertFormulaToExcel(ast: ASTFuncall) {
31095
+ * // ...
31096
+ * return modifiedAst
31097
+ * }
31098
+ */
31099
+ function convertAstNodes(ast, type, fn) {
31100
+ return mapAst(ast, (ast) => {
31101
+ if (ast.type === type) {
31102
+ return fn(ast);
31103
+ }
31104
+ return ast;
31105
+ });
31106
+ }
31107
+ function iterateAstNodes(ast) {
31108
+ return Array.from(astIterator(ast));
31109
+ }
31110
+ function* astIterator(ast) {
31111
+ yield ast;
31112
+ switch (ast.type) {
31113
+ case "FUNCALL":
31114
+ for (const arg of ast.args) {
31115
+ yield* astIterator(arg);
31116
+ }
31117
+ break;
31118
+ case "UNARY_OPERATION":
31119
+ yield* astIterator(ast.operand);
31120
+ break;
31121
+ case "BIN_OPERATION":
31122
+ yield* astIterator(ast.left);
31123
+ yield* astIterator(ast.right);
31124
+ break;
31125
+ }
31126
+ }
31127
+ function mapAst(ast, fn) {
31128
+ ast = fn(ast);
31129
+ switch (ast.type) {
31130
+ case "FUNCALL":
31131
+ return {
31132
+ ...ast,
31133
+ args: ast.args.map((child) => mapAst(child, fn)),
31134
+ };
31135
+ case "UNARY_OPERATION":
31136
+ return {
31137
+ ...ast,
31138
+ operand: mapAst(ast.operand, fn),
31139
+ };
31140
+ case "BIN_OPERATION":
31141
+ return {
31142
+ ...ast,
31143
+ right: mapAst(ast.right, fn),
31144
+ left: mapAst(ast.left, fn),
31145
+ };
31146
+ default:
31147
+ return ast;
31148
+ }
31149
+ }
31150
+ /**
31151
+ * Converts an ast formula to the corresponding string
31152
+ */
31153
+ function astToFormula(ast) {
31154
+ switch (ast.type) {
31155
+ case "FUNCALL":
31156
+ const args = ast.args.map((arg) => astToFormula(arg));
31157
+ return `${ast.value}(${args.join(",")})`;
31158
+ case "NUMBER":
31159
+ return ast.value.toString();
31160
+ case "REFERENCE":
31161
+ return ast.value;
31162
+ case "STRING":
31163
+ return `"${ast.value}"`;
31164
+ case "BOOLEAN":
31165
+ return ast.value ? "TRUE" : "FALSE";
31166
+ case "UNARY_OPERATION":
31167
+ return ast.postfix
31168
+ ? leftOperandToFormula(ast) + ast.value
31169
+ : ast.value + rightOperandToFormula(ast);
31170
+ case "BIN_OPERATION":
31171
+ return leftOperandToFormula(ast) + ast.value + rightOperandToFormula(ast);
31172
+ default:
31173
+ return ast.value;
31174
+ }
31175
+ }
31176
+ /**
31177
+ * Convert the left operand of a binary operation to the corresponding string
31178
+ * and enclose the result inside parenthesis if necessary.
31179
+ */
31180
+ function leftOperandToFormula(operationAST) {
31181
+ const mainOperator = operationAST.value;
31182
+ const leftOperation = "left" in operationAST ? operationAST.left : operationAST.operand;
31183
+ const leftOperator = leftOperation.value;
31184
+ const needParenthesis = leftOperation.type === "BIN_OPERATION" && OP_PRIORITY[leftOperator] < OP_PRIORITY[mainOperator];
31185
+ return needParenthesis ? `(${astToFormula(leftOperation)})` : astToFormula(leftOperation);
31186
+ }
31187
+ /**
31188
+ * Convert the right operand of a binary or unary operation to the corresponding string
31189
+ * and enclose the result inside parenthesis if necessary.
31190
+ */
31191
+ function rightOperandToFormula(operationAST) {
31192
+ const mainOperator = operationAST.value;
31193
+ const rightOperation = "right" in operationAST ? operationAST.right : operationAST.operand;
31194
+ const rightPriority = OP_PRIORITY[rightOperation.value];
31195
+ const mainPriority = OP_PRIORITY[mainOperator];
31196
+ let needParenthesis = false;
31197
+ if (rightOperation.type !== "BIN_OPERATION") {
31198
+ needParenthesis = false;
31199
+ }
31200
+ else if (rightPriority < mainPriority) {
31201
+ needParenthesis = true;
31202
+ }
31203
+ else if (rightPriority === mainPriority && !ASSOCIATIVE_OPERATORS.includes(mainOperator)) {
31204
+ needParenthesis = true;
31205
+ }
31206
+ return needParenthesis ? `(${astToFormula(rightOperation)})` : astToFormula(rightOperation);
31207
+ }
31208
+
31209
+ const functions$1 = functionRegistry.content;
31210
+ const OPERATOR_MAP = {
31211
+ "=": "EQ",
31212
+ "+": "ADD",
31213
+ "-": "MINUS",
31214
+ "*": "MULTIPLY",
31215
+ "/": "DIVIDE",
31216
+ ">=": "GTE",
31217
+ "<>": "NE",
31218
+ ">": "GT",
31219
+ "<=": "LTE",
31220
+ "<": "LT",
31221
+ "^": "POWER",
31222
+ "&": "CONCATENATE",
31223
+ };
31224
+ const UNARY_OPERATOR_MAP = {
31225
+ "-": "UMINUS",
31226
+ "+": "UPLUS",
31227
+ "%": "UNARY.PERCENT",
31228
+ };
31229
+ // this cache contains all compiled function code, grouped by "structure". For
31230
+ // example, "=2*sum(A1:A4)" and "=2*sum(B1:B4)" are compiled into the same
31231
+ // structural function.
31232
+ // It is only exported for testing purposes
31233
+ const functionCache = {};
31234
+ // -----------------------------------------------------------------------------
31235
+ // COMPILER
31236
+ // -----------------------------------------------------------------------------
31237
+ function compile(formula) {
31238
+ const tokens = rangeTokenize(formula);
31239
+ return compileTokens(tokens);
31240
+ }
31241
+ function compileTokens(tokens) {
31242
+ const { dependencies, constantValues } = formulaArguments(tokens);
31243
+ const cacheKey = compilationCacheKey(tokens, dependencies, constantValues);
31244
+ if (!functionCache[cacheKey]) {
31245
+ const ast = parseTokens([...tokens]);
31246
+ const scope = new Scope();
31247
+ if (ast.type === "BIN_OPERATION" && ast.value === ":") {
31248
+ throw new BadExpressionError(_t("Invalid formula"));
31249
+ }
31250
+ if (ast.type === "EMPTY") {
31251
+ throw new BadExpressionError(_t("Invalid formula"));
31252
+ }
31253
+ const compiledAST = compileAST(ast);
31254
+ const code = new FunctionCodeBuilder();
31255
+ code.append(`// ${cacheKey}`);
31256
+ code.append(compiledAST);
31257
+ code.append(`return ${compiledAST.returnExpression};`);
31258
+ let baseFunction = new Function("deps", // the dependencies in the current formula
31259
+ "ref", // a function to access a certain dependency at a given index
31260
+ "range", // same as above, but guarantee that the result is in the form of a range
31261
+ "ctx", code.toString());
31262
+ functionCache[cacheKey] = {
31263
+ // @ts-ignore
31264
+ execute: baseFunction,
31265
+ };
31266
+ /**
31267
+ * This function compile the function arguments. It is mostly straightforward,
31268
+ * except that there is a non trivial transformation in one situation:
31269
+ *
31270
+ * If a function argument is asking for a range, and get a cell, we transform
31271
+ * the cell value into a range. This allow the grid model to differentiate
31272
+ * between a cell value and a non cell value.
31273
+ */
31274
+ function compileFunctionArgs(ast) {
31275
+ const { args } = ast;
31276
+ const functionName = ast.value.toUpperCase();
31277
+ const functionDefinition = functions$1[functionName];
31278
+ if (!functionDefinition) {
31279
+ throw new UnknownFunctionError(ast.value);
31280
+ }
31281
+ assertEnoughArgs(ast);
31282
+ const compiledArgs = [];
31283
+ for (let i = 0; i < args.length; i++) {
31284
+ const argToFocus = functionDefinition.getArgToFocus(i + 1) - 1;
31285
+ const argDefinition = functionDefinition.args[argToFocus];
31286
+ const currentArg = args[i];
31287
+ const argTypes = argDefinition.type || [];
31288
+ // detect when an argument need to be evaluated as a meta argument
31289
+ const isMeta = argTypes.includes("META");
31290
+ // detect when an argument need to be evaluated as a lazy argument
31291
+ const isLazy = argDefinition.lazy;
31292
+ const hasRange = argTypes.some((t) => isRangeType(t));
31293
+ const isRangeOnly = argTypes.every((t) => isRangeType(t));
31294
+ if (isRangeOnly) {
31295
+ if (!isRangeInput(currentArg)) {
31296
+ throw new BadExpressionError(_t("Function %s expects the parameter %s to be reference to a cell or range, not a %s.", functionName, (i + 1).toString(), currentArg.type.toLowerCase()));
31297
+ }
31298
+ }
31299
+ const compiledAST = compileAST(currentArg, isMeta, hasRange, {
31300
+ functionName,
31301
+ paramIndex: i + 1,
31302
+ });
31303
+ compiledArgs.push(isLazy ? compiledAST.wrapInClosure() : compiledAST);
31304
+ }
31305
+ return compiledArgs;
31306
+ }
31307
+ /**
31308
+ * This function compiles all the information extracted by the parser into an
31309
+ * executable code for the evaluation of the cells content. It uses a cash to
31310
+ * not reevaluate identical code structures.
31311
+ *
31312
+ * The function is sensitive to parameter “isMeta”. This
31313
+ * parameter may vary when compiling function arguments:
31314
+ * isMeta: In some cases the function arguments expects information on the
31315
+ * cell/range other than the associated value(s). For example the COLUMN
31316
+ * function needs to receive as argument the coordinates of a cell rather
31317
+ * than its value. For this we have meta arguments.
31318
+ */
31319
+ function compileAST(ast, isMeta = false, hasRange = false, referenceVerification = {}) {
31320
+ const code = new FunctionCodeBuilder(scope);
31321
+ if (ast.type !== "REFERENCE" && !(ast.type === "BIN_OPERATION" && ast.value === ":")) {
31322
+ if (isMeta) {
31323
+ throw new BadExpressionError(_t("Argument must be a reference to a cell or range."));
31324
+ }
31325
+ }
31326
+ if (ast.debug) {
31327
+ code.append("debugger;");
31328
+ }
31329
+ switch (ast.type) {
31330
+ case "BOOLEAN":
31331
+ return code.return(`{ value: ${ast.value} }`);
31332
+ case "NUMBER":
31333
+ return code.return(`{ value: this.constantValues.numbers[${constantValues.numbers.indexOf(ast.value)}] }`);
31334
+ case "STRING":
31335
+ return code.return(`{ value: this.constantValues.strings[${constantValues.strings.indexOf(ast.value)}] }`);
31336
+ case "REFERENCE":
31337
+ const referenceIndex = dependencies.indexOf(ast.value);
31338
+ if (hasRange) {
31339
+ return code.return(`range(deps[${referenceIndex}])`);
31340
+ }
31341
+ else {
31342
+ return code.return(`ref(deps[${referenceIndex}], ${isMeta ? "true" : "false"}, "${referenceVerification.functionName || OPERATOR_MAP["="]}", ${referenceVerification.paramIndex})`);
31343
+ }
31344
+ case "FUNCALL":
31345
+ const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable());
31346
+ code.append(...args);
31347
+ const fnName = ast.value.toUpperCase();
31348
+ code.append(`ctx.__lastFnCalled = '${fnName}';`);
31349
+ return code.return(`ctx['${fnName}'](${args.map((arg) => arg.returnExpression)})`);
31350
+ case "UNARY_OPERATION": {
31351
+ const fnName = UNARY_OPERATOR_MAP[ast.value];
31352
+ const operand = compileAST(ast.operand, false, false, {
31353
+ functionName: fnName,
31354
+ }).assignResultToVariable();
31355
+ code.append(operand);
31356
+ code.append(`ctx.__lastFnCalled = '${fnName}';`);
31357
+ return code.return(`ctx['${fnName}'](${operand.returnExpression})`);
31358
+ }
31359
+ case "BIN_OPERATION": {
31360
+ const fnName = OPERATOR_MAP[ast.value];
31361
+ const left = compileAST(ast.left, false, false, {
31362
+ functionName: fnName,
31363
+ }).assignResultToVariable();
31364
+ const right = compileAST(ast.right, false, false, {
31365
+ functionName: fnName,
31366
+ }).assignResultToVariable();
31367
+ code.append(left);
31368
+ code.append(right);
31369
+ code.append(`ctx.__lastFnCalled = '${fnName}';`);
31370
+ return code.return(`ctx['${fnName}'](${left.returnExpression}, ${right.returnExpression})`);
31371
+ }
31372
+ case "EMPTY":
31373
+ return code.return("undefined");
31374
+ }
31375
+ }
31376
+ }
31377
+ const compiledFormula = {
31378
+ execute: functionCache[cacheKey].execute,
31379
+ dependencies,
31380
+ constantValues,
31381
+ tokens,
31382
+ };
31383
+ return compiledFormula;
31384
+ }
31385
+ /**
31386
+ * Compute a cache key for the formula.
31387
+ * References, numbers and strings are replaced with placeholders because
31388
+ * the compiled formula does not depend on their actual value.
31389
+ * Both `=A1+1+"2"` and `=A2+2+"3"` are compiled to the exact same function.
31390
+ *
31391
+ * Spaces are also ignored to compute the cache key.
31392
+ *
31393
+ * A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|0|+|1|+SUM(|N0|,|N0|,|S0|)`
31394
+ */
31395
+ function compilationCacheKey(tokens, dependencies, constantValues) {
31396
+ return concat(tokens.map((token) => {
31397
+ switch (token.type) {
31398
+ case "STRING":
31399
+ const value = removeStringQuotes(token.value);
31400
+ return `|S${constantValues.strings.indexOf(value)}|`;
31401
+ case "NUMBER":
31402
+ return `|N${constantValues.numbers.indexOf(parseNumber(token.value, DEFAULT_LOCALE))}|`;
31403
+ case "REFERENCE":
31404
+ case "INVALID_REFERENCE":
31405
+ return `|${dependencies.indexOf(token.value)}|`;
31406
+ case "SPACE":
31407
+ return "";
31408
+ default:
31409
+ return token.value;
31410
+ }
31411
+ }));
31412
+ }
31413
+ /**
31414
+ * Return formula arguments which are references, strings and numbers.
31415
+ */
31416
+ function formulaArguments(tokens) {
31417
+ const constantValues = {
31418
+ numbers: [],
31419
+ strings: [],
31420
+ };
31421
+ const dependencies = [];
31422
+ for (const token of tokens) {
31423
+ switch (token.type) {
31424
+ case "INVALID_REFERENCE":
31425
+ case "REFERENCE":
31426
+ dependencies.push(token.value);
31427
+ break;
31428
+ case "STRING":
31429
+ const value = removeStringQuotes(token.value);
31430
+ if (!constantValues.strings.includes(value)) {
31431
+ constantValues.strings.push(value);
31432
+ }
31433
+ break;
31434
+ case "NUMBER": {
31435
+ const value = parseNumber(token.value, DEFAULT_LOCALE);
31436
+ if (!constantValues.numbers.includes(value)) {
31437
+ constantValues.numbers.push(value);
31438
+ }
31439
+ break;
31440
+ }
31441
+ }
31442
+ }
31443
+ return {
31444
+ dependencies,
31445
+ constantValues,
31446
+ };
31447
+ }
31448
+ /**
31449
+ * Check if arguments are supplied in the correct quantities
31450
+ */
31451
+ function assertEnoughArgs(ast) {
31452
+ const nbrArg = ast.args.length;
31453
+ const functionName = ast.value.toUpperCase();
31454
+ const functionDefinition = functions$1[functionName];
31455
+ if (nbrArg < functionDefinition.minArgRequired) {
31456
+ throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected %s minimum, but got %s instead.", functionName, functionDefinition.minArgRequired.toString(), nbrArg.toString()));
31457
+ }
31458
+ if (nbrArg > functionDefinition.maxArgPossible) {
31459
+ throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected %s maximum, but got %s instead.", functionName, functionDefinition.maxArgPossible.toString(), nbrArg.toString()));
31460
+ }
31461
+ const repeatableArgs = functionDefinition.nbrArgRepeating;
31462
+ if (repeatableArgs > 1) {
31463
+ const unrepeatableArgs = functionDefinition.args.length - repeatableArgs;
31464
+ const repeatingArgs = nbrArg - unrepeatableArgs;
31465
+ if (repeatingArgs % repeatableArgs !== 0) {
31466
+ throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected all arguments after position %s to be supplied by groups of %s arguments", functionName, unrepeatableArgs.toString(), repeatableArgs.toString()));
31467
+ }
31468
+ }
31469
+ }
31470
+ function isRangeType(type) {
31471
+ return type.startsWith("RANGE");
31472
+ }
31473
+ function isRangeInput(arg) {
31474
+ if (arg.type === "REFERENCE") {
31475
+ return true;
31476
+ }
31477
+ if (arg.type === "FUNCALL") {
31478
+ const fnDef = functions$1[arg.value.toUpperCase()];
31479
+ return fnDef && isRangeType(fnDef.returns[0]);
31480
+ }
31481
+ return false;
31482
+ }
31483
+
31484
+ /**
31485
+ * Add the following information on tokens:
31486
+ * - length
31487
+ * - start
31488
+ * - end
31489
+ */
31490
+ function enrichTokens(tokens) {
31491
+ let current = 0;
31492
+ return tokens.map((x) => {
31493
+ const len = x.value.toString().length;
31494
+ const token = Object.assign({}, x, {
31495
+ start: current,
31496
+ end: current + len,
31497
+ length: len,
31498
+ });
31499
+ current = token.end;
31500
+ return token;
31501
+ });
31502
+ }
31503
+ /**
31504
+ * add on each token the length, start and end
31505
+ * also matches the opening to its closing parenthesis (using the same number)
31506
+ */
31507
+ function mapParenthesis(tokens) {
31508
+ let maxParen = 1;
31509
+ const stack = [];
31510
+ return tokens.map((token) => {
31511
+ if (token.type === "LEFT_PAREN") {
31512
+ stack.push(maxParen);
31513
+ token.parenIndex = maxParen;
31514
+ maxParen++;
31515
+ }
31516
+ else if (token.type === "RIGHT_PAREN") {
31517
+ token.parenIndex = stack.pop();
31518
+ }
31519
+ return token;
31520
+ });
31521
+ }
31522
+ /**
31523
+ * add on each token its parent function and the index corresponding to
31524
+ * its position as an argument of the function.
31525
+ * In this example "=MIN(42,SUM(MAX(1,2),3))":
31526
+ * - the parent function of the token correspond to number 42 is the MIN function
31527
+ * - the argument position of the token correspond to number 42 is 0
31528
+ * - the parent function of the token correspond to number 3 is the SUM function
31529
+ * - the argument position of the token correspond to number 3 is 1
31530
+ */
31531
+ function mapParentFunction(tokens) {
31532
+ let stack = [];
31533
+ let functionStarted = "";
31534
+ const res = tokens.map((token, i) => {
31535
+ if (!["SPACE", "LEFT_PAREN"].includes(token.type)) {
31536
+ functionStarted = "";
31537
+ }
31538
+ switch (token.type) {
31539
+ case "SYMBOL":
31540
+ functionStarted = token.value;
31541
+ break;
31542
+ case "LEFT_PAREN":
31543
+ stack.push({ parent: functionStarted, argPosition: 0 });
31544
+ functionStarted = "";
31545
+ break;
31546
+ case "RIGHT_PAREN":
31547
+ stack.pop();
31548
+ break;
31549
+ case "ARG_SEPARATOR":
31550
+ if (stack.length) {
31551
+ // increment position on current function
31552
+ stack[stack.length - 1].argPosition++;
31553
+ }
31554
+ break;
31555
+ }
31556
+ if (stack.length) {
31557
+ const functionContext = stack[stack.length - 1];
31558
+ if (functionContext.parent) {
31559
+ token.functionContext = Object.assign({}, functionContext);
31560
+ }
31561
+ }
31562
+ return token;
31563
+ });
31564
+ return res;
31565
+ }
31566
+ /**
31567
+ * Take the result of the tokenizer and transform it to be usable in the composer.
31568
+ *
31569
+ * @param formula
31570
+ */
31571
+ function composerTokenize(formula, locale) {
31572
+ const tokens = rangeTokenize(formula, locale);
31573
+ return mapParentFunction(mapParenthesis(enrichTokens(tokens)));
31574
+ }
31575
+
31576
+ const functions = functionRegistry.content;
31577
+ function isExportableToExcel(tokens) {
31578
+ try {
31579
+ const nonExportableFunctions = iterateAstNodes(parseTokens(tokens)).filter((ast) => ast.type === "FUNCALL" && !functions[ast.value.toUpperCase()]?.isExported);
31580
+ return nonExportableFunctions.length === 0;
31581
+ }
31582
+ catch (error) {
31583
+ return false;
31584
+ }
31585
+ }
31586
+
30696
31587
  /**
30697
31588
  * Map of the different types of conversions warnings and their name in error messages
30698
31589
  */
@@ -31441,9 +32332,9 @@ function convertConditionalFormats(xlsxCfs, dxfs, warningManager) {
31441
32332
  if (!rule.operator || !rule.formula || rule.formula.length === 0)
31442
32333
  continue;
31443
32334
  operator = convertCFCellIsOperator(rule.operator);
31444
- values.push(rule.formula[0]);
32335
+ values.push(prefixFormula(rule.formula[0]));
31445
32336
  if (rule.formula.length === 2) {
31446
- values.push(rule.formula[1]);
32337
+ values.push(prefixFormula(rule.formula[1]));
31447
32338
  }
31448
32339
  break;
31449
32340
  }
@@ -31586,6 +32477,11 @@ function convertIcons(xlsxIconSet, index) {
31586
32477
  ? ICON_SETS[iconSet].neutral
31587
32478
  : ICON_SETS[iconSet].good;
31588
32479
  }
32480
+ /** Prefix the string by "=" if the string looks like a formula */
32481
+ function prefixFormula(formula) {
32482
+ const tokens = tokenize(formula);
32483
+ return tokens.length === 1 && tokens[0].type !== "REFERENCE" ? formula : "=" + formula;
32484
+ }
31589
32485
  // ---------------------------------------------------------------------------
31590
32486
  // Warnings
31591
32487
  // ---------------------------------------------------------------------------
@@ -33923,171 +34819,31 @@ class XlsxReader {
33923
34819
  externalLinks: getXLSXFilesOfType(CONTENT_TYPES.externalLink, this.xmls),
33924
34820
  images: this.images,
33925
34821
  };
33926
- if (!xlsxFileStructure.workbook.rels) {
33927
- throw Error(_t("Cannot find workbook relations file"));
33928
- }
33929
- return xlsxFileStructure;
33930
- }
33931
- // ---------------------------------------------------------------------------
33932
- // Conversion
33933
- // ---------------------------------------------------------------------------
33934
- convertImportedData(data) {
33935
- const convertedData = {
33936
- version: EXCEL_IMPORT_VERSION,
33937
- sheets: convertSheets(data, this.warningManager),
33938
- styles: convertStyles(data, this.warningManager),
33939
- formats: convertFormats(data, this.warningManager),
33940
- borders: convertBorders(data, this.warningManager),
33941
- entities: {},
33942
- revisionId: DEFAULT_REVISION_ID,
33943
- };
33944
- convertTables(convertedData, data);
33945
- // Remove falsy attributes in styles. Not mandatory, but make objects more readable when debugging
33946
- Object.keys(data.styles).map((key) => {
33947
- data.styles[key] = removeFalsyAttributes(data.styles[key]);
33948
- });
33949
- return convertedData;
33950
- }
33951
- }
33952
-
33953
- var State;
33954
- (function (State) {
33955
- /**
33956
- * Initial state.
33957
- * Expecting any reference for the left part of a range
33958
- * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A"
33959
- */
33960
- State[State["LeftRef"] = 0] = "LeftRef";
33961
- /**
33962
- * Expecting any reference for the right part of a range
33963
- * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A"
33964
- */
33965
- State[State["RightRef"] = 1] = "RightRef";
33966
- /**
33967
- * Expecting the separator without any constraint on the right part
33968
- */
33969
- State[State["Separator"] = 2] = "Separator";
33970
- /**
33971
- * Expecting the separator for a full column range
33972
- */
33973
- State[State["FullColumnSeparator"] = 3] = "FullColumnSeparator";
33974
- /**
33975
- * Expecting the separator for a full row range
33976
- */
33977
- State[State["FullRowSeparator"] = 4] = "FullRowSeparator";
33978
- /**
33979
- * Expecting the right part of a full column range
33980
- * e.g. "1", "A1"
33981
- */
33982
- State[State["RightColumnRef"] = 5] = "RightColumnRef";
33983
- /**
33984
- * Expecting the right part of a full row range
33985
- * e.g. "A", "A1"
33986
- */
33987
- State[State["RightRowRef"] = 6] = "RightRowRef";
33988
- /**
33989
- * Final state. A range has been matched
33990
- */
33991
- State[State["Found"] = 7] = "Found";
33992
- })(State || (State = {}));
33993
- const goTo = (state, guard = () => true) => [
33994
- {
33995
- goTo: state,
33996
- guard,
33997
- },
33998
- ];
33999
- const goToMulti = (state, guard = () => true) => ({
34000
- goTo: state,
34001
- guard,
34002
- });
34003
- const machine = {
34004
- [State.LeftRef]: {
34005
- REFERENCE: goTo(State.Separator),
34006
- NUMBER: goTo(State.FullRowSeparator),
34007
- SYMBOL: [
34008
- goToMulti(State.FullColumnSeparator, (token) => isColReference(token.value)),
34009
- goToMulti(State.FullRowSeparator, (token) => isRowReference(token.value)),
34010
- ],
34011
- },
34012
- [State.FullColumnSeparator]: {
34013
- SPACE: goTo(State.FullColumnSeparator),
34014
- OPERATOR: goTo(State.RightColumnRef, (token) => token.value === ":"),
34015
- },
34016
- [State.FullRowSeparator]: {
34017
- SPACE: goTo(State.FullRowSeparator),
34018
- OPERATOR: goTo(State.RightRowRef, (token) => token.value === ":"),
34019
- },
34020
- [State.Separator]: {
34021
- SPACE: goTo(State.Separator),
34022
- OPERATOR: goTo(State.RightRef, (token) => token.value === ":"),
34023
- },
34024
- [State.RightRef]: {
34025
- SPACE: goTo(State.RightRef),
34026
- NUMBER: goTo(State.Found),
34027
- REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
34028
- SYMBOL: goTo(State.Found, (token) => isColHeader(token.value) || isRowHeader(token.value)),
34029
- },
34030
- [State.RightColumnRef]: {
34031
- SPACE: goTo(State.RightColumnRef),
34032
- SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)),
34033
- REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
34034
- },
34035
- [State.RightRowRef]: {
34036
- SPACE: goTo(State.RightRowRef),
34037
- NUMBER: goTo(State.Found),
34038
- REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
34039
- SYMBOL: goTo(State.Found, (token) => isRowHeader(token.value)),
34040
- },
34041
- [State.Found]: {},
34042
- };
34043
- /**
34044
- * Check if the list of tokens starts with a sequence of tokens representing
34045
- * a range.
34046
- * If a range is found, the sequence is removed from the list and is returned
34047
- * as a single token.
34048
- */
34049
- function matchReference(tokens) {
34050
- let head = 0;
34051
- let transitions = machine[State.LeftRef];
34052
- let matchedTokens = "";
34053
- while (transitions !== undefined) {
34054
- const token = tokens[head++];
34055
- if (!token) {
34056
- return null;
34057
- }
34058
- const transition = transitions[token.type]?.find((transition) => transition.guard(token));
34059
- const nextState = transition ? transition.goTo : undefined;
34060
- switch (nextState) {
34061
- case undefined:
34062
- return null;
34063
- case State.Found:
34064
- matchedTokens += token.value;
34065
- tokens.splice(0, head);
34066
- return {
34067
- type: "REFERENCE",
34068
- value: matchedTokens,
34069
- };
34070
- default:
34071
- transitions = machine[nextState];
34072
- matchedTokens += token.value;
34073
- break;
34074
- }
34075
- }
34076
- return null;
34077
- }
34078
- /**
34079
- * Take the result of the tokenizer and transform it to be usable in the
34080
- * manipulations of range
34081
- *
34082
- * @param formula
34083
- */
34084
- function rangeTokenize(formula, locale = DEFAULT_LOCALE) {
34085
- const tokens = tokenize(formula, locale);
34086
- const result = [];
34087
- while (tokens.length) {
34088
- result.push(matchReference(tokens) || tokens.shift());
34822
+ if (!xlsxFileStructure.workbook.rels) {
34823
+ throw Error(_t("Cannot find workbook relations file"));
34824
+ }
34825
+ return xlsxFileStructure;
34826
+ }
34827
+ // ---------------------------------------------------------------------------
34828
+ // Conversion
34829
+ // ---------------------------------------------------------------------------
34830
+ convertImportedData(data) {
34831
+ const convertedData = {
34832
+ version: EXCEL_IMPORT_VERSION,
34833
+ sheets: convertSheets(data, this.warningManager),
34834
+ styles: convertStyles(data, this.warningManager),
34835
+ formats: convertFormats(data, this.warningManager),
34836
+ borders: convertBorders(data, this.warningManager),
34837
+ entities: {},
34838
+ revisionId: DEFAULT_REVISION_ID,
34839
+ };
34840
+ convertTables(convertedData, data);
34841
+ // Remove falsy attributes in styles. Not mandatory, but make objects more readable when debugging
34842
+ Object.keys(data.styles).map((key) => {
34843
+ data.styles[key] = removeFalsyAttributes(data.styles[key]);
34844
+ });
34845
+ return convertedData;
34089
34846
  }
34090
- return result;
34091
34847
  }
34092
34848
 
34093
34849
  /**
@@ -34953,1183 +35709,432 @@ class BordersPlugin extends CorePlugin {
34953
35709
  else {
34954
35710
  this.shiftBordersHorizontally(cmd.sheetId, cmd.base + 1, cmd.quantity, {
34955
35711
  moveFirstLeftBorder: false,
34956
- });
34957
- colLeftOfInsertion = cmd.base;
34958
- colRightOfInsertion = cmd.base + cmd.quantity + 1;
34959
- }
34960
- this.ensureColumnBorderContinuity(cmd.sheetId, colLeftOfInsertion, colRightOfInsertion);
34961
- }
34962
- /**
34963
- * Move borders according to the inserted rows.
34964
- * Ensure borders continuity.
34965
- */
34966
- handleAddRows(cmd) {
34967
- // The new rows have already been inserted at this point.
34968
- let rowAboveInsertion;
34969
- let rowBelowInsertion;
34970
- if (cmd.position === "before") {
34971
- this.shiftBordersVertically(cmd.sheetId, cmd.base, cmd.quantity, {
34972
- moveFirstTopBorder: true,
34973
- });
34974
- rowAboveInsertion = cmd.base - 1;
34975
- rowBelowInsertion = cmd.base + cmd.quantity;
34976
- }
34977
- else {
34978
- this.shiftBordersVertically(cmd.sheetId, cmd.base + 1, cmd.quantity, {
34979
- moveFirstTopBorder: false,
34980
- });
34981
- rowAboveInsertion = cmd.base;
34982
- rowBelowInsertion = cmd.base + cmd.quantity + 1;
34983
- }
34984
- this.ensureRowBorderContinuity(cmd.sheetId, rowAboveInsertion, rowBelowInsertion);
34985
- }
34986
- // ---------------------------------------------------------------------------
34987
- // Getters
34988
- // ---------------------------------------------------------------------------
34989
- getCellBorder({ sheetId, col, row }) {
34990
- const border = {
34991
- top: this.borders[sheetId]?.[col]?.[row]?.horizontal,
34992
- bottom: this.borders[sheetId]?.[col]?.[row + 1]?.horizontal,
34993
- left: this.borders[sheetId]?.[col]?.[row]?.vertical,
34994
- right: this.borders[sheetId]?.[col + 1]?.[row]?.vertical,
34995
- };
34996
- if (!border.bottom && !border.left && !border.right && !border.top) {
34997
- return null;
34998
- }
34999
- return border;
35000
- }
35001
- getBordersColors(sheetId) {
35002
- const colors = [];
35003
- const sheetBorders = this.borders[sheetId];
35004
- if (sheetBorders) {
35005
- for (const borders of sheetBorders.filter(isDefined$1)) {
35006
- for (const cellBorder of borders) {
35007
- if (cellBorder?.horizontal) {
35008
- colors.push(cellBorder.horizontal.color);
35009
- }
35010
- if (cellBorder?.vertical) {
35011
- colors.push(cellBorder.vertical.color);
35012
- }
35013
- }
35014
- }
35015
- }
35016
- return colors;
35017
- }
35018
- // ---------------------------------------------------------------------------
35019
- // Private
35020
- // ---------------------------------------------------------------------------
35021
- /**
35022
- * Ensure border continuity between two columns.
35023
- * If the two columns have the same borders (at each row respectively),
35024
- * the same borders are applied to each cell in between.
35025
- */
35026
- ensureColumnBorderContinuity(sheetId, leftColumn, rightColumn) {
35027
- const targetCols = range(leftColumn + 1, rightColumn);
35028
- for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {
35029
- const leftBorder = this.getCellBorder({ sheetId, col: leftColumn, row });
35030
- const rightBorder = this.getCellBorder({ sheetId, col: rightColumn, row });
35031
- if (leftBorder && rightBorder) {
35032
- const commonSides = this.getCommonSides(leftBorder, rightBorder);
35033
- for (let col of targetCols) {
35034
- this.addBorder(sheetId, col, row, commonSides);
35035
- }
35036
- }
35037
- }
35038
- }
35039
- /**
35040
- * Ensure border continuity between two rows.
35041
- * If the two rows have the same borders (at each column respectively),
35042
- * the same borders are applied to each cell in between.
35043
- */
35044
- ensureRowBorderContinuity(sheetId, topRow, bottomRow) {
35045
- const targetRows = range(topRow + 1, bottomRow);
35046
- for (let col = 0; col < this.getters.getNumberCols(sheetId); col++) {
35047
- const aboveBorder = this.getCellBorder({ sheetId, col, row: topRow });
35048
- const belowBorder = this.getCellBorder({ sheetId, col, row: bottomRow });
35049
- if (aboveBorder && belowBorder) {
35050
- const commonSides = this.getCommonSides(aboveBorder, belowBorder);
35051
- for (let row of targetRows) {
35052
- this.addBorder(sheetId, col, row, commonSides);
35053
- }
35054
- }
35055
- }
35056
- }
35057
- /**
35058
- * From two borders, return a new border with sides defined in both borders.
35059
- * i.e. the intersection of two borders.
35060
- */
35061
- getCommonSides(border1, border2) {
35062
- const commonBorder = {};
35063
- for (let side of ["top", "bottom", "left", "right"]) {
35064
- if (border1[side] && deepEquals(border1[side], border2[side])) {
35065
- commonBorder[side] = border1[side];
35066
- }
35067
- }
35068
- return commonBorder;
35069
- }
35070
- /**
35071
- * Get all the columns which contains at least a border
35072
- */
35073
- getColumnsWithBorders(sheetId) {
35074
- const sheetBorders = this.borders[sheetId];
35075
- if (!sheetBorders)
35076
- return [];
35077
- return Object.keys(sheetBorders).map((index) => parseInt(index, 10));
35078
- }
35079
- /**
35080
- * Get all the rows which contains at least a border
35081
- */
35082
- getRowsWithBorders(sheetId) {
35083
- const sheetBorders = this.borders[sheetId]?.filter(isDefined$1);
35084
- if (!sheetBorders)
35085
- return [];
35086
- const rowsWithBorders = new Set();
35087
- for (const rowBorders of sheetBorders) {
35088
- for (const rowBorder in rowBorders) {
35089
- rowsWithBorders.add(parseInt(rowBorder, 10));
35090
- }
35091
- }
35092
- return Array.from(rowsWithBorders);
35093
- }
35094
- /**
35095
- * Get the range of all the rows in the sheet
35096
- */
35097
- getRowsRange(sheetId) {
35098
- const sheetBorders = this.borders[sheetId];
35099
- if (!sheetBorders)
35100
- return [];
35101
- return range(0, this.getters.getNumberRows(sheetId) + 1);
35102
- }
35103
- /**
35104
- * Move borders of a sheet horizontally.
35105
- * @param sheetId
35106
- * @param start starting column (included)
35107
- * @param delta how much borders will be moved (negative if moved to the left)
35108
- */
35109
- shiftBordersHorizontally(sheetId, start, delta, { moveFirstLeftBorder } = {}) {
35110
- const borders = this.borders[sheetId];
35111
- if (!borders)
35112
- return;
35113
- if (delta < 0) {
35114
- this.moveBordersOfColumn(sheetId, start, delta, "vertical", {
35115
- destructive: false,
35116
- });
35117
- }
35118
- this.getColumnsWithBorders(sheetId)
35119
- .filter((col) => col >= start)
35120
- .sort((a, b) => (delta < 0 ? a - b : b - a)) // start by the end when moving up
35121
- .forEach((col) => {
35122
- if ((col === start && moveFirstLeftBorder) || col !== start) {
35123
- this.moveBordersOfColumn(sheetId, col, delta, "vertical");
35124
- }
35125
- this.moveBordersOfColumn(sheetId, col, delta, "horizontal");
35126
- });
35127
- }
35128
- /**
35129
- * Move borders of a sheet vertically.
35130
- * @param sheetId
35131
- * @param start starting row (included)
35132
- * @param delta how much borders will be moved (negative if moved to the above)
35133
- */
35134
- shiftBordersVertically(sheetId, start, delta, { moveFirstTopBorder } = {}) {
35135
- const borders = this.borders[sheetId];
35136
- if (!borders)
35137
- return;
35138
- if (delta < 0) {
35139
- this.moveBordersOfRow(sheetId, start, delta, "horizontal", {
35140
- destructive: false,
35141
- });
35142
- }
35143
- this.getRowsWithBorders(sheetId)
35144
- .filter((row) => row >= start)
35145
- .sort((a, b) => (delta < 0 ? a - b : b - a)) // start by the end when moving up
35146
- .forEach((row) => {
35147
- if ((row === start && moveFirstTopBorder) || row !== start) {
35148
- this.moveBordersOfRow(sheetId, row, delta, "horizontal");
35149
- }
35150
- this.moveBordersOfRow(sheetId, row, delta, "vertical");
35151
- });
35152
- }
35153
- /**
35154
- * Moves the borders (left if `vertical` or top if `horizontal` depending on
35155
- * `borderDirection`) of all cells in an entire row `delta` rows to the right
35156
- * (`delta` > 0) or to the left (`delta` < 0).
35157
- * Note that as the left of a cell is the right of the cell-1, if the left is
35158
- * moved the right is also moved. However, if `horizontal`, the bottom border
35159
- * is not moved.
35160
- * It does it by replacing the target border by the moved border. If the
35161
- * argument `destructive` is given false, the target border is preserved if
35162
- * the moved border is empty
35163
- */
35164
- moveBordersOfRow(sheetId, row, delta, borderDirection, { destructive } = { destructive: true }) {
35165
- const borders = this.borders[sheetId];
35166
- if (!borders)
35167
- return;
35168
- this.getColumnsWithBorders(sheetId).forEach((col) => {
35169
- const targetBorder = borders[col]?.[row + delta]?.[borderDirection];
35170
- const movedBorder = borders[col]?.[row]?.[borderDirection];
35171
- this.history.update("borders", sheetId, col, row + delta, borderDirection, destructive ? movedBorder : movedBorder || targetBorder);
35172
- this.history.update("borders", sheetId, col, row, borderDirection, undefined);
35173
- });
35174
- }
35175
- /**
35176
- * Moves the borders (left if `vertical` or top if `horizontal` depending on
35177
- * `borderDirection`) of all cells in an entire column `delta` columns below
35178
- * (`delta` > 0) or above (`delta` < 0).
35179
- * Note that as the top of a cell is the bottom of the cell-1, if the top is
35180
- * moved the bottom is also moved. However, if `vertical`, the right border
35181
- * is not moved.
35182
- * It does it by replacing the target border by the moved border. If the
35183
- * argument `destructive` is given false, the target border is preserved if
35184
- * the moved border is empty
35185
- */
35186
- moveBordersOfColumn(sheetId, col, delta, borderDirection, { destructive } = { destructive: true }) {
35187
- const borders = this.borders[sheetId];
35188
- if (!borders)
35189
- return;
35190
- this.getRowsRange(sheetId).forEach((row) => {
35191
- const targetBorder = borders[col + delta]?.[row]?.[borderDirection];
35192
- const movedBorder = borders[col]?.[row]?.[borderDirection];
35193
- this.history.update("borders", sheetId, col + delta, row, borderDirection, destructive ? movedBorder : movedBorder || targetBorder);
35194
- this.history.update("borders", sheetId, col, row, borderDirection, undefined);
35195
- });
35196
- }
35197
- /**
35198
- * Set the borders of a cell.
35199
- * It overrides the current border if override === true.
35200
- */
35201
- setBorder(sheetId, col, row, border, override = true) {
35202
- if (override || !this.borders?.[sheetId]?.[col]?.[row]?.vertical) {
35203
- this.history.update("borders", sheetId, col, row, "vertical", border?.left);
35204
- }
35205
- if (override || !this.borders?.[sheetId]?.[col]?.[row]?.horizontal) {
35206
- this.history.update("borders", sheetId, col, row, "horizontal", border?.top);
35207
- }
35208
- if (override || !this.borders?.[sheetId]?.[col + 1]?.[row]?.vertical) {
35209
- this.history.update("borders", sheetId, col + 1, row, "vertical", border?.right);
35210
- }
35211
- if (override || !this.borders?.[sheetId]?.[col]?.[row + 1]?.horizontal) {
35212
- this.history.update("borders", sheetId, col, row + 1, "horizontal", border?.bottom);
35213
- }
35214
- }
35215
- /**
35216
- * Remove the borders of a zone
35217
- */
35218
- clearBorders(sheetId, zones) {
35219
- for (let zone of zones) {
35220
- for (let row = zone.top; row <= zone.bottom; row++) {
35221
- this.history.update("borders", sheetId, zone.right + 1, row, "vertical", undefined);
35222
- for (let col = zone.left; col <= zone.right; col++) {
35223
- this.history.update("borders", sheetId, col, row, undefined);
35224
- }
35225
- }
35226
- for (let col = zone.left; col <= zone.right; col++) {
35227
- this.history.update("borders", sheetId, col, zone.bottom + 1, "horizontal", undefined);
35228
- }
35229
- }
35230
- }
35231
- /**
35232
- * Remove the borders inside of a zone
35233
- */
35234
- clearInsideBorders(sheetId, zones) {
35235
- for (let zone of zones) {
35236
- for (let row = zone.top; row <= zone.bottom; row++) {
35237
- for (let col = zone.left; col <= zone.right; col++) {
35238
- this.history.update("borders", sheetId, col, row, undefined);
35239
- }
35240
- }
35241
- }
35242
- }
35243
- /**
35244
- * Add a border to the existing one to a cell
35245
- */
35246
- addBorder(sheetId, col, row, border) {
35247
- this.setBorder(sheetId, col, row, {
35248
- ...this.getCellBorder({ sheetId, col, row }),
35249
- ...border,
35250
- });
35251
- }
35252
- /**
35253
- * Set the borders of a zone by computing the borders to add from the given
35254
- * command
35255
- */
35256
- setBorders(sheetId, zones, position, border) {
35257
- if (position === "clear") {
35258
- return this.clearBorders(sheetId, zones);
35259
- }
35260
- for (let zone of zones) {
35261
- if (position === "h" || position === "hv" || position === "all") {
35262
- for (let row = zone.top + 1; row <= zone.bottom; row++) {
35263
- for (let col = zone.left; col <= zone.right; col++) {
35264
- this.addBorder(sheetId, col, row, { top: border });
35265
- }
35266
- }
35267
- }
35268
- if (position === "v" || position === "hv" || position === "all") {
35269
- for (let row = zone.top; row <= zone.bottom; row++) {
35270
- for (let col = zone.left + 1; col <= zone.right; col++) {
35271
- this.addBorder(sheetId, col, row, { left: border });
35272
- }
35273
- }
35274
- }
35275
- if (position === "left" || position === "all" || position === "external") {
35276
- for (let row = zone.top; row <= zone.bottom; row++) {
35277
- this.addBorder(sheetId, zone.left, row, { left: border });
35278
- }
35279
- }
35280
- if (position === "right" || position === "all" || position === "external") {
35281
- for (let row = zone.top; row <= zone.bottom; row++) {
35282
- this.addBorder(sheetId, zone.right + 1, row, { left: border });
35283
- }
35284
- }
35285
- if (position === "top" || position === "all" || position === "external") {
35286
- for (let col = zone.left; col <= zone.right; col++) {
35287
- this.addBorder(sheetId, col, zone.top, { top: border });
35288
- }
35289
- }
35290
- if (position === "bottom" || position === "all" || position === "external") {
35291
- for (let col = zone.left; col <= zone.right; col++) {
35292
- this.addBorder(sheetId, col, zone.bottom + 1, { top: border });
35293
- }
35294
- }
35295
- }
35296
- }
35297
- /**
35298
- * Compute the borders to add to the given zone merged.
35299
- */
35300
- addBordersToMerge(sheetId, zone) {
35301
- const { left, right, top, bottom } = zone;
35302
- const bordersTopLeft = this.getCellBorder({ sheetId, col: left, row: top });
35303
- const bordersBottomRight = this.getCellBorder({ sheetId, col: right, row: bottom });
35304
- this.clearBorders(sheetId, [zone]);
35305
- if (bordersTopLeft?.top) {
35306
- this.setBorders(sheetId, [{ ...zone, bottom: top }], "top", bordersTopLeft.top);
35307
- }
35308
- if (bordersTopLeft?.left) {
35309
- this.setBorders(sheetId, [{ ...zone, right: left }], "left", bordersTopLeft.left);
35310
- }
35311
- if (bordersBottomRight?.bottom) {
35312
- this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom", bordersBottomRight.bottom);
35313
- }
35314
- else if (bordersTopLeft?.bottom) {
35315
- this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom", bordersTopLeft.bottom);
35316
- }
35317
- if (bordersBottomRight?.right) {
35318
- this.setBorders(sheetId, [{ ...zone, left: right }], "right", bordersBottomRight.right);
35319
- }
35320
- else if (bordersTopLeft?.right) {
35321
- this.setBorders(sheetId, [{ ...zone, left: right }], "right", bordersTopLeft.right);
35322
- }
35323
- }
35324
- checkBordersUnchanged(cmd) {
35325
- const currentBorder = this.getCellBorder(cmd);
35326
- const areAllNewBordersUndefined = !cmd.border?.bottom && !cmd.border?.left && !cmd.border?.right && !cmd.border?.top;
35327
- if ((!currentBorder && areAllNewBordersUndefined) || deepEquals(currentBorder, cmd.border)) {
35328
- return "NoChanges" /* CommandResult.NoChanges */;
35329
- }
35330
- return "Success" /* CommandResult.Success */;
35331
- }
35332
- // ---------------------------------------------------------------------------
35333
- // Import/Export
35334
- // ---------------------------------------------------------------------------
35335
- import(data) {
35336
- // Borders
35337
- if (Object.keys(data.borders || {}).length) {
35338
- for (let sheet of data.sheets) {
35339
- for (const xc in sheet.cells) {
35340
- const cell = sheet.cells[xc];
35341
- if (cell?.border) {
35342
- const border = data.borders[cell.border];
35343
- const { col, row } = toCartesian(xc);
35344
- this.setBorder(sheet.id, col, row, border, false);
35345
- }
35346
- }
35347
- }
35348
- }
35349
- // Merges
35350
- for (let sheetData of data.sheets) {
35351
- if (sheetData.merges) {
35352
- for (let merge of sheetData.merges) {
35353
- this.addBordersToMerge(sheetData.id, toZone(merge));
35354
- }
35355
- }
35356
- }
35357
- }
35358
- export(data) {
35359
- const borders = {};
35360
- for (let sheet of data.sheets) {
35361
- for (let col = 0; col < sheet.colNumber; col++) {
35362
- for (let row = 0; row < sheet.rowNumber; row++) {
35363
- const border = this.getCellBorder({ sheetId: sheet.id, col, row });
35364
- if (border) {
35365
- const xc = toXC(col, row);
35366
- const cell = sheet.cells[xc];
35367
- const borderId = getItemId(border, borders);
35368
- if (cell) {
35369
- cell.border = borderId;
35370
- }
35371
- else {
35372
- sheet.cells[xc] = { border: borderId };
35373
- }
35374
- }
35375
- }
35376
- }
35377
- }
35378
- data.borders = borders;
35379
- }
35380
- exportForExcel(data) {
35381
- this.export(data);
35382
- }
35383
- }
35384
-
35385
- class FunctionCodeBuilder {
35386
- scope;
35387
- code = "";
35388
- constructor(scope = new Scope()) {
35389
- this.scope = scope;
35390
- }
35391
- append(...lines) {
35392
- this.code += lines.map((line) => line.toString()).join("\n") + "\n";
35393
- }
35394
- return(expression) {
35395
- return new FunctionCodeImpl(this.scope, this.code, expression);
35396
- }
35397
- toString() {
35398
- return indentCode(this.code);
35399
- }
35400
- }
35401
- class FunctionCodeImpl {
35402
- scope;
35403
- returnExpression;
35404
- code;
35405
- constructor(scope, code, returnExpression) {
35406
- this.scope = scope;
35407
- this.returnExpression = returnExpression;
35408
- this.code = indentCode(code);
35409
- }
35410
- toString() {
35411
- return this.code;
35412
- }
35413
- wrapInClosure() {
35414
- const closureName = this.scope.nextVariableName();
35415
- const code = new FunctionCodeBuilder(this.scope);
35416
- code.append(`const ${closureName} = () => {`);
35417
- code.append(this.code);
35418
- code.append(`return ${this.returnExpression};`);
35419
- code.append(`}`);
35420
- return code.return(closureName);
35421
- }
35422
- assignResultToVariable() {
35423
- if (this.scope.isAlreadyDeclared(this.returnExpression)) {
35424
- return this;
35425
- }
35426
- const variableName = this.scope.nextVariableName();
35427
- const code = new FunctionCodeBuilder(this.scope);
35428
- code.append(this.code);
35429
- code.append(`const ${variableName} = ${this.returnExpression};`);
35430
- return code.return(variableName);
35431
- }
35432
- }
35433
- class Scope {
35434
- nextId = 1;
35435
- declaredVariables = new Set();
35436
- nextVariableName() {
35437
- const name = `_${this.nextId++}`;
35438
- this.declaredVariables.add(name);
35439
- return name;
35440
- }
35441
- isAlreadyDeclared(name) {
35442
- return this.declaredVariables.has(name);
35712
+ });
35713
+ colLeftOfInsertion = cmd.base;
35714
+ colRightOfInsertion = cmd.base + cmd.quantity + 1;
35715
+ }
35716
+ this.ensureColumnBorderContinuity(cmd.sheetId, colLeftOfInsertion, colRightOfInsertion);
35443
35717
  }
35444
- }
35445
- /**
35446
- * Takes a list of strings that might be single or multiline
35447
- * and maps them in a list of single line strings.
35448
- */
35449
- function splitLines(str) {
35450
- return str
35451
- .split("\n")
35452
- .map((line) => line.trim())
35453
- .filter((line) => line !== "");
35454
- }
35455
- function indentCode(code) {
35456
- let result = "";
35457
- let indentLevel = 0;
35458
- const lines = splitLines(code);
35459
- for (const line of lines) {
35460
- if (line.startsWith("}")) {
35461
- indentLevel--;
35718
+ /**
35719
+ * Move borders according to the inserted rows.
35720
+ * Ensure borders continuity.
35721
+ */
35722
+ handleAddRows(cmd) {
35723
+ // The new rows have already been inserted at this point.
35724
+ let rowAboveInsertion;
35725
+ let rowBelowInsertion;
35726
+ if (cmd.position === "before") {
35727
+ this.shiftBordersVertically(cmd.sheetId, cmd.base, cmd.quantity, {
35728
+ moveFirstTopBorder: true,
35729
+ });
35730
+ rowAboveInsertion = cmd.base - 1;
35731
+ rowBelowInsertion = cmd.base + cmd.quantity;
35462
35732
  }
35463
- result += "\t".repeat(indentLevel) + line + "\n";
35464
- if (line.endsWith("{")) {
35465
- indentLevel++;
35733
+ else {
35734
+ this.shiftBordersVertically(cmd.sheetId, cmd.base + 1, cmd.quantity, {
35735
+ moveFirstTopBorder: false,
35736
+ });
35737
+ rowAboveInsertion = cmd.base;
35738
+ rowBelowInsertion = cmd.base + cmd.quantity + 1;
35466
35739
  }
35740
+ this.ensureRowBorderContinuity(cmd.sheetId, rowAboveInsertion, rowBelowInsertion);
35467
35741
  }
35468
- return result.trim();
35469
- }
35470
-
35471
- const functionRegex = /[a-zA-Z0-9\_]+(\.[a-zA-Z0-9\_]+)*/;
35472
- const UNARY_OPERATORS_PREFIX = ["-", "+"];
35473
- const UNARY_OPERATORS_POSTFIX = ["%"];
35474
- const ASSOCIATIVE_OPERATORS = ["*", "+", "&"];
35475
- const OP_PRIORITY = {
35476
- "^": 30,
35477
- "%": 30,
35478
- "*": 20,
35479
- "/": 20,
35480
- "+": 15,
35481
- "-": 15,
35482
- "&": 13,
35483
- ">": 10,
35484
- "<>": 10,
35485
- ">=": 10,
35486
- "<": 10,
35487
- "<=": 10,
35488
- "=": 10,
35489
- };
35490
- /**
35491
- * Parse the next operand in an arithmetic expression.
35492
- * e.g.
35493
- * for 1+2*3, the next operand is 1
35494
- * for (1+2)*3, the next operand is (1+2)
35495
- * for SUM(1,2)+3, the next operand is SUM(1,2)
35496
- */
35497
- function parseOperand(tokens) {
35498
- const current = tokens.shift();
35499
- if (!current) {
35500
- throw new BadExpressionError(DEFAULT_ERROR_MESSAGE);
35742
+ // ---------------------------------------------------------------------------
35743
+ // Getters
35744
+ // ---------------------------------------------------------------------------
35745
+ getCellBorder({ sheetId, col, row }) {
35746
+ const border = {
35747
+ top: this.borders[sheetId]?.[col]?.[row]?.horizontal,
35748
+ bottom: this.borders[sheetId]?.[col]?.[row + 1]?.horizontal,
35749
+ left: this.borders[sheetId]?.[col]?.[row]?.vertical,
35750
+ right: this.borders[sheetId]?.[col + 1]?.[row]?.vertical,
35751
+ };
35752
+ if (!border.bottom && !border.left && !border.right && !border.top) {
35753
+ return null;
35754
+ }
35755
+ return border;
35501
35756
  }
35502
- switch (current.type) {
35503
- case "DEBUGGER":
35504
- const next = parseExpression(tokens, 1000);
35505
- next.debug = true;
35506
- return next;
35507
- case "NUMBER":
35508
- return { type: "NUMBER", value: parseNumber(current.value, DEFAULT_LOCALE) };
35509
- case "STRING":
35510
- return { type: "STRING", value: removeStringQuotes(current.value) };
35511
- case "INVALID_REFERENCE":
35512
- return {
35513
- type: "REFERENCE",
35514
- value: CellErrorType.InvalidReference,
35515
- };
35516
- case "REFERENCE":
35517
- if (tokens[0]?.value === ":" && tokens[1]?.type === "REFERENCE") {
35518
- tokens.shift();
35519
- const rightReference = tokens.shift();
35520
- return {
35521
- type: "REFERENCE",
35522
- value: `${current.value}:${rightReference?.value}`,
35523
- };
35524
- }
35525
- return {
35526
- type: "REFERENCE",
35527
- value: current.value,
35528
- };
35529
- case "SYMBOL":
35530
- const value = current.value;
35531
- const nextToken = tokens[0];
35532
- if (nextToken?.type === "LEFT_PAREN" && functionRegex.test(current.value)) {
35533
- const args = parseFunctionArgs(tokens);
35534
- return { type: "FUNCALL", value: value, args };
35535
- }
35536
- const upperCaseValue = value.toUpperCase();
35537
- if (upperCaseValue === "TRUE" || upperCaseValue === "FALSE") {
35538
- return { type: "BOOLEAN", value: upperCaseValue === "TRUE" };
35539
- }
35540
- throw new BadExpressionError(_t("Invalid formula"));
35541
- case "LEFT_PAREN":
35542
- const result = parseExpression(tokens);
35543
- consumeOrThrow(tokens, "RIGHT_PAREN", _t("Missing closing parenthesis"));
35544
- return result;
35545
- case "OPERATOR":
35546
- const operator = current.value;
35547
- if (UNARY_OPERATORS_PREFIX.includes(operator)) {
35548
- return {
35549
- type: "UNARY_OPERATION",
35550
- value: operator,
35551
- operand: parseExpression(tokens, OP_PRIORITY[operator]),
35552
- };
35757
+ getBordersColors(sheetId) {
35758
+ const colors = [];
35759
+ const sheetBorders = this.borders[sheetId];
35760
+ if (sheetBorders) {
35761
+ for (const borders of sheetBorders.filter(isDefined$1)) {
35762
+ for (const cellBorder of borders) {
35763
+ if (cellBorder?.horizontal) {
35764
+ colors.push(cellBorder.horizontal.color);
35765
+ }
35766
+ if (cellBorder?.vertical) {
35767
+ colors.push(cellBorder.vertical.color);
35768
+ }
35769
+ }
35553
35770
  }
35554
- throw new BadExpressionError(_t("Unexpected token: %s", current.value));
35555
- default:
35556
- throw new BadExpressionError(_t("Unexpected token: %s", current.value));
35557
- }
35558
- }
35559
- function parseFunctionArgs(tokens) {
35560
- consumeOrThrow(tokens, "LEFT_PAREN", _t("Missing opening parenthesis"));
35561
- const nextToken = tokens[0];
35562
- if (nextToken?.type === "RIGHT_PAREN") {
35563
- consumeOrThrow(tokens, "RIGHT_PAREN");
35564
- return [];
35771
+ }
35772
+ return colors;
35565
35773
  }
35566
- const args = [];
35567
- args.push(parseOneFunctionArg(tokens));
35568
- while (tokens[0]?.type !== "RIGHT_PAREN") {
35569
- consumeOrThrow(tokens, "ARG_SEPARATOR", _t("Wrong function call"));
35570
- args.push(parseOneFunctionArg(tokens));
35774
+ // ---------------------------------------------------------------------------
35775
+ // Private
35776
+ // ---------------------------------------------------------------------------
35777
+ /**
35778
+ * Ensure border continuity between two columns.
35779
+ * If the two columns have the same borders (at each row respectively),
35780
+ * the same borders are applied to each cell in between.
35781
+ */
35782
+ ensureColumnBorderContinuity(sheetId, leftColumn, rightColumn) {
35783
+ const targetCols = range(leftColumn + 1, rightColumn);
35784
+ for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {
35785
+ const leftBorder = this.getCellBorder({ sheetId, col: leftColumn, row });
35786
+ const rightBorder = this.getCellBorder({ sheetId, col: rightColumn, row });
35787
+ if (leftBorder && rightBorder) {
35788
+ const commonSides = this.getCommonSides(leftBorder, rightBorder);
35789
+ for (let col of targetCols) {
35790
+ this.addBorder(sheetId, col, row, commonSides);
35791
+ }
35792
+ }
35793
+ }
35571
35794
  }
35572
- consumeOrThrow(tokens, "RIGHT_PAREN");
35573
- return args;
35574
- }
35575
- function parseOneFunctionArg(tokens) {
35576
- const nextToken = tokens[0];
35577
- if (nextToken?.type === "ARG_SEPARATOR" || nextToken?.type === "RIGHT_PAREN") {
35578
- // arg is empty: "sum(1,,2)" "sum(,1)" "sum(1,)"
35579
- return { type: "EMPTY", value: "" };
35795
+ /**
35796
+ * Ensure border continuity between two rows.
35797
+ * If the two rows have the same borders (at each column respectively),
35798
+ * the same borders are applied to each cell in between.
35799
+ */
35800
+ ensureRowBorderContinuity(sheetId, topRow, bottomRow) {
35801
+ const targetRows = range(topRow + 1, bottomRow);
35802
+ for (let col = 0; col < this.getters.getNumberCols(sheetId); col++) {
35803
+ const aboveBorder = this.getCellBorder({ sheetId, col, row: topRow });
35804
+ const belowBorder = this.getCellBorder({ sheetId, col, row: bottomRow });
35805
+ if (aboveBorder && belowBorder) {
35806
+ const commonSides = this.getCommonSides(aboveBorder, belowBorder);
35807
+ for (let row of targetRows) {
35808
+ this.addBorder(sheetId, col, row, commonSides);
35809
+ }
35810
+ }
35811
+ }
35580
35812
  }
35581
- return parseExpression(tokens);
35582
- }
35583
- function consumeOrThrow(tokens, type, message = DEFAULT_ERROR_MESSAGE) {
35584
- const token = tokens.shift();
35585
- if (!token || token.type !== type) {
35586
- throw new BadExpressionError(message);
35813
+ /**
35814
+ * From two borders, return a new border with sides defined in both borders.
35815
+ * i.e. the intersection of two borders.
35816
+ */
35817
+ getCommonSides(border1, border2) {
35818
+ const commonBorder = {};
35819
+ for (let side of ["top", "bottom", "left", "right"]) {
35820
+ if (border1[side] && deepEquals(border1[side], border2[side])) {
35821
+ commonBorder[side] = border1[side];
35822
+ }
35823
+ }
35824
+ return commonBorder;
35587
35825
  }
35588
- }
35589
- function parseExpression(tokens, parent_priority = 0) {
35590
- if (tokens.length === 0) {
35591
- throw new BadExpressionError(DEFAULT_ERROR_MESSAGE);
35826
+ /**
35827
+ * Get all the columns which contains at least a border
35828
+ */
35829
+ getColumnsWithBorders(sheetId) {
35830
+ const sheetBorders = this.borders[sheetId];
35831
+ if (!sheetBorders)
35832
+ return [];
35833
+ return Object.keys(sheetBorders).map((index) => parseInt(index, 10));
35592
35834
  }
35593
- let left = parseOperand(tokens);
35594
- // as long as we have operators with higher priority than the parent one,
35595
- // continue parsing the expression because it is a child sub-expression
35596
- while (tokens[0]?.type === "OPERATOR" && OP_PRIORITY[tokens[0].value] > parent_priority) {
35597
- const operator = tokens.shift().value;
35598
- if (UNARY_OPERATORS_POSTFIX.includes(operator)) {
35599
- left = {
35600
- type: "UNARY_OPERATION",
35601
- value: operator,
35602
- operand: left,
35603
- postfix: true,
35604
- };
35605
- }
35606
- else {
35607
- const right = parseExpression(tokens, OP_PRIORITY[operator]);
35608
- left = {
35609
- type: "BIN_OPERATION",
35610
- value: operator,
35611
- left,
35612
- right,
35613
- };
35835
+ /**
35836
+ * Get all the rows which contains at least a border
35837
+ */
35838
+ getRowsWithBorders(sheetId) {
35839
+ const sheetBorders = this.borders[sheetId]?.filter(isDefined$1);
35840
+ if (!sheetBorders)
35841
+ return [];
35842
+ const rowsWithBorders = new Set();
35843
+ for (const rowBorders of sheetBorders) {
35844
+ for (const rowBorder in rowBorders) {
35845
+ rowsWithBorders.add(parseInt(rowBorder, 10));
35846
+ }
35614
35847
  }
35848
+ return Array.from(rowsWithBorders);
35615
35849
  }
35616
- return left;
35617
- }
35618
- /**
35619
- * Parse an expression (as a string) into an AST.
35620
- */
35621
- function parse(str) {
35622
- return parseTokens(rangeTokenize(str));
35623
- }
35624
- function parseTokens(tokens) {
35625
- tokens = tokens.filter((x) => x.type !== "SPACE");
35626
- if (tokens[0]?.value === "=") {
35627
- tokens.splice(0, 1);
35628
- }
35629
- const result = parseExpression(tokens);
35630
- if (tokens.length) {
35631
- throw new BadExpressionError(DEFAULT_ERROR_MESSAGE);
35850
+ /**
35851
+ * Get the range of all the rows in the sheet
35852
+ */
35853
+ getRowsRange(sheetId) {
35854
+ const sheetBorders = this.borders[sheetId];
35855
+ if (!sheetBorders)
35856
+ return [];
35857
+ return range(0, this.getters.getNumberRows(sheetId) + 1);
35632
35858
  }
35633
- return result;
35634
- }
35635
- /**
35636
- * Allows to visit all nodes of an AST and apply a mapping function
35637
- * to nodes of a specific type.
35638
- * Useful if you want to convert some part of a formula.
35639
- *
35640
- * @example
35641
- * convertAstNodes(ast, "FUNCALL", convertFormulaToExcel)
35642
- *
35643
- * function convertFormulaToExcel(ast: ASTFuncall) {
35644
- * // ...
35645
- * return modifiedAst
35646
- * }
35647
- */
35648
- function convertAstNodes(ast, type, fn) {
35649
- return mapAst(ast, (ast) => {
35650
- if (ast.type === type) {
35651
- return fn(ast);
35859
+ /**
35860
+ * Move borders of a sheet horizontally.
35861
+ * @param sheetId
35862
+ * @param start starting column (included)
35863
+ * @param delta how much borders will be moved (negative if moved to the left)
35864
+ */
35865
+ shiftBordersHorizontally(sheetId, start, delta, { moveFirstLeftBorder } = {}) {
35866
+ const borders = this.borders[sheetId];
35867
+ if (!borders)
35868
+ return;
35869
+ if (delta < 0) {
35870
+ this.moveBordersOfColumn(sheetId, start, delta, "vertical", {
35871
+ destructive: false,
35872
+ });
35652
35873
  }
35653
- return ast;
35654
- });
35655
- }
35656
- function iterateAstNodes(ast) {
35657
- return Array.from(astIterator(ast));
35658
- }
35659
- function* astIterator(ast) {
35660
- yield ast;
35661
- switch (ast.type) {
35662
- case "FUNCALL":
35663
- for (const arg of ast.args) {
35664
- yield* astIterator(arg);
35874
+ this.getColumnsWithBorders(sheetId)
35875
+ .filter((col) => col >= start)
35876
+ .sort((a, b) => (delta < 0 ? a - b : b - a)) // start by the end when moving up
35877
+ .forEach((col) => {
35878
+ if ((col === start && moveFirstLeftBorder) || col !== start) {
35879
+ this.moveBordersOfColumn(sheetId, col, delta, "vertical");
35665
35880
  }
35666
- break;
35667
- case "UNARY_OPERATION":
35668
- yield* astIterator(ast.operand);
35669
- break;
35670
- case "BIN_OPERATION":
35671
- yield* astIterator(ast.left);
35672
- yield* astIterator(ast.right);
35673
- break;
35674
- }
35675
- }
35676
- function mapAst(ast, fn) {
35677
- ast = fn(ast);
35678
- switch (ast.type) {
35679
- case "FUNCALL":
35680
- return {
35681
- ...ast,
35682
- args: ast.args.map((child) => mapAst(child, fn)),
35683
- };
35684
- case "UNARY_OPERATION":
35685
- return {
35686
- ...ast,
35687
- operand: mapAst(ast.operand, fn),
35688
- };
35689
- case "BIN_OPERATION":
35690
- return {
35691
- ...ast,
35692
- right: mapAst(ast.right, fn),
35693
- left: mapAst(ast.left, fn),
35694
- };
35695
- default:
35696
- return ast;
35697
- }
35698
- }
35699
- /**
35700
- * Converts an ast formula to the corresponding string
35701
- */
35702
- function astToFormula(ast) {
35703
- switch (ast.type) {
35704
- case "FUNCALL":
35705
- const args = ast.args.map((arg) => astToFormula(arg));
35706
- return `${ast.value}(${args.join(",")})`;
35707
- case "NUMBER":
35708
- return ast.value.toString();
35709
- case "REFERENCE":
35710
- return ast.value;
35711
- case "STRING":
35712
- return `"${ast.value}"`;
35713
- case "BOOLEAN":
35714
- return ast.value ? "TRUE" : "FALSE";
35715
- case "UNARY_OPERATION":
35716
- return ast.postfix
35717
- ? leftOperandToFormula(ast) + ast.value
35718
- : ast.value + rightOperandToFormula(ast);
35719
- case "BIN_OPERATION":
35720
- return leftOperandToFormula(ast) + ast.value + rightOperandToFormula(ast);
35721
- default:
35722
- return ast.value;
35881
+ this.moveBordersOfColumn(sheetId, col, delta, "horizontal");
35882
+ });
35723
35883
  }
35724
- }
35725
- /**
35726
- * Convert the left operand of a binary operation to the corresponding string
35727
- * and enclose the result inside parenthesis if necessary.
35728
- */
35729
- function leftOperandToFormula(operationAST) {
35730
- const mainOperator = operationAST.value;
35731
- const leftOperation = "left" in operationAST ? operationAST.left : operationAST.operand;
35732
- const leftOperator = leftOperation.value;
35733
- const needParenthesis = leftOperation.type === "BIN_OPERATION" && OP_PRIORITY[leftOperator] < OP_PRIORITY[mainOperator];
35734
- return needParenthesis ? `(${astToFormula(leftOperation)})` : astToFormula(leftOperation);
35735
- }
35736
- /**
35737
- * Convert the right operand of a binary or unary operation to the corresponding string
35738
- * and enclose the result inside parenthesis if necessary.
35739
- */
35740
- function rightOperandToFormula(operationAST) {
35741
- const mainOperator = operationAST.value;
35742
- const rightOperation = "right" in operationAST ? operationAST.right : operationAST.operand;
35743
- const rightPriority = OP_PRIORITY[rightOperation.value];
35744
- const mainPriority = OP_PRIORITY[mainOperator];
35745
- let needParenthesis = false;
35746
- if (rightOperation.type !== "BIN_OPERATION") {
35747
- needParenthesis = false;
35884
+ /**
35885
+ * Move borders of a sheet vertically.
35886
+ * @param sheetId
35887
+ * @param start starting row (included)
35888
+ * @param delta how much borders will be moved (negative if moved to the above)
35889
+ */
35890
+ shiftBordersVertically(sheetId, start, delta, { moveFirstTopBorder } = {}) {
35891
+ const borders = this.borders[sheetId];
35892
+ if (!borders)
35893
+ return;
35894
+ if (delta < 0) {
35895
+ this.moveBordersOfRow(sheetId, start, delta, "horizontal", {
35896
+ destructive: false,
35897
+ });
35898
+ }
35899
+ this.getRowsWithBorders(sheetId)
35900
+ .filter((row) => row >= start)
35901
+ .sort((a, b) => (delta < 0 ? a - b : b - a)) // start by the end when moving up
35902
+ .forEach((row) => {
35903
+ if ((row === start && moveFirstTopBorder) || row !== start) {
35904
+ this.moveBordersOfRow(sheetId, row, delta, "horizontal");
35905
+ }
35906
+ this.moveBordersOfRow(sheetId, row, delta, "vertical");
35907
+ });
35748
35908
  }
35749
- else if (rightPriority < mainPriority) {
35750
- needParenthesis = true;
35909
+ /**
35910
+ * Moves the borders (left if `vertical` or top if `horizontal` depending on
35911
+ * `borderDirection`) of all cells in an entire row `delta` rows to the right
35912
+ * (`delta` > 0) or to the left (`delta` < 0).
35913
+ * Note that as the left of a cell is the right of the cell-1, if the left is
35914
+ * moved the right is also moved. However, if `horizontal`, the bottom border
35915
+ * is not moved.
35916
+ * It does it by replacing the target border by the moved border. If the
35917
+ * argument `destructive` is given false, the target border is preserved if
35918
+ * the moved border is empty
35919
+ */
35920
+ moveBordersOfRow(sheetId, row, delta, borderDirection, { destructive } = { destructive: true }) {
35921
+ const borders = this.borders[sheetId];
35922
+ if (!borders)
35923
+ return;
35924
+ this.getColumnsWithBorders(sheetId).forEach((col) => {
35925
+ const targetBorder = borders[col]?.[row + delta]?.[borderDirection];
35926
+ const movedBorder = borders[col]?.[row]?.[borderDirection];
35927
+ this.history.update("borders", sheetId, col, row + delta, borderDirection, destructive ? movedBorder : movedBorder || targetBorder);
35928
+ this.history.update("borders", sheetId, col, row, borderDirection, undefined);
35929
+ });
35751
35930
  }
35752
- else if (rightPriority === mainPriority && !ASSOCIATIVE_OPERATORS.includes(mainOperator)) {
35753
- needParenthesis = true;
35931
+ /**
35932
+ * Moves the borders (left if `vertical` or top if `horizontal` depending on
35933
+ * `borderDirection`) of all cells in an entire column `delta` columns below
35934
+ * (`delta` > 0) or above (`delta` < 0).
35935
+ * Note that as the top of a cell is the bottom of the cell-1, if the top is
35936
+ * moved the bottom is also moved. However, if `vertical`, the right border
35937
+ * is not moved.
35938
+ * It does it by replacing the target border by the moved border. If the
35939
+ * argument `destructive` is given false, the target border is preserved if
35940
+ * the moved border is empty
35941
+ */
35942
+ moveBordersOfColumn(sheetId, col, delta, borderDirection, { destructive } = { destructive: true }) {
35943
+ const borders = this.borders[sheetId];
35944
+ if (!borders)
35945
+ return;
35946
+ this.getRowsRange(sheetId).forEach((row) => {
35947
+ const targetBorder = borders[col + delta]?.[row]?.[borderDirection];
35948
+ const movedBorder = borders[col]?.[row]?.[borderDirection];
35949
+ this.history.update("borders", sheetId, col + delta, row, borderDirection, destructive ? movedBorder : movedBorder || targetBorder);
35950
+ this.history.update("borders", sheetId, col, row, borderDirection, undefined);
35951
+ });
35754
35952
  }
35755
- return needParenthesis ? `(${astToFormula(rightOperation)})` : astToFormula(rightOperation);
35756
- }
35757
-
35758
- const functions$1 = functionRegistry.content;
35759
- const OPERATOR_MAP = {
35760
- "=": "EQ",
35761
- "+": "ADD",
35762
- "-": "MINUS",
35763
- "*": "MULTIPLY",
35764
- "/": "DIVIDE",
35765
- ">=": "GTE",
35766
- "<>": "NE",
35767
- ">": "GT",
35768
- "<=": "LTE",
35769
- "<": "LT",
35770
- "^": "POWER",
35771
- "&": "CONCATENATE",
35772
- };
35773
- const UNARY_OPERATOR_MAP = {
35774
- "-": "UMINUS",
35775
- "+": "UPLUS",
35776
- "%": "UNARY.PERCENT",
35777
- };
35778
- // this cache contains all compiled function code, grouped by "structure". For
35779
- // example, "=2*sum(A1:A4)" and "=2*sum(B1:B4)" are compiled into the same
35780
- // structural function.
35781
- // It is only exported for testing purposes
35782
- const functionCache = {};
35783
- // -----------------------------------------------------------------------------
35784
- // COMPILER
35785
- // -----------------------------------------------------------------------------
35786
- function compile(formula) {
35787
- const tokens = rangeTokenize(formula);
35788
- return compileTokens(tokens);
35789
- }
35790
- function compileTokens(tokens) {
35791
- const { dependencies, constantValues } = formulaArguments(tokens);
35792
- const cacheKey = compilationCacheKey(tokens, dependencies, constantValues);
35793
- if (!functionCache[cacheKey]) {
35794
- const ast = parseTokens([...tokens]);
35795
- const scope = new Scope();
35796
- if (ast.type === "BIN_OPERATION" && ast.value === ":") {
35797
- throw new BadExpressionError(_t("Invalid formula"));
35953
+ /**
35954
+ * Set the borders of a cell.
35955
+ * It overrides the current border if override === true.
35956
+ */
35957
+ setBorder(sheetId, col, row, border, override = true) {
35958
+ if (override || !this.borders?.[sheetId]?.[col]?.[row]?.vertical) {
35959
+ this.history.update("borders", sheetId, col, row, "vertical", border?.left);
35798
35960
  }
35799
- if (ast.type === "EMPTY") {
35800
- throw new BadExpressionError(_t("Invalid formula"));
35961
+ if (override || !this.borders?.[sheetId]?.[col]?.[row]?.horizontal) {
35962
+ this.history.update("borders", sheetId, col, row, "horizontal", border?.top);
35801
35963
  }
35802
- const compiledAST = compileAST(ast);
35803
- const code = new FunctionCodeBuilder();
35804
- code.append(`// ${cacheKey}`);
35805
- code.append(compiledAST);
35806
- code.append(`return ${compiledAST.returnExpression};`);
35807
- let baseFunction = new Function("deps", // the dependencies in the current formula
35808
- "ref", // a function to access a certain dependency at a given index
35809
- "range", // same as above, but guarantee that the result is in the form of a range
35810
- "ctx", code.toString());
35811
- functionCache[cacheKey] = {
35812
- // @ts-ignore
35813
- execute: baseFunction,
35814
- };
35815
- /**
35816
- * This function compile the function arguments. It is mostly straightforward,
35817
- * except that there is a non trivial transformation in one situation:
35818
- *
35819
- * If a function argument is asking for a range, and get a cell, we transform
35820
- * the cell value into a range. This allow the grid model to differentiate
35821
- * between a cell value and a non cell value.
35822
- */
35823
- function compileFunctionArgs(ast) {
35824
- const { args } = ast;
35825
- const functionName = ast.value.toUpperCase();
35826
- const functionDefinition = functions$1[functionName];
35827
- if (!functionDefinition) {
35828
- throw new UnknownFunctionError(ast.value);
35829
- }
35830
- assertEnoughArgs(ast);
35831
- const compiledArgs = [];
35832
- for (let i = 0; i < args.length; i++) {
35833
- const argToFocus = functionDefinition.getArgToFocus(i + 1) - 1;
35834
- const argDefinition = functionDefinition.args[argToFocus];
35835
- const currentArg = args[i];
35836
- const argTypes = argDefinition.type || [];
35837
- // detect when an argument need to be evaluated as a meta argument
35838
- const isMeta = argTypes.includes("META");
35839
- // detect when an argument need to be evaluated as a lazy argument
35840
- const isLazy = argDefinition.lazy;
35841
- const hasRange = argTypes.some((t) => isRangeType(t));
35842
- const isRangeOnly = argTypes.every((t) => isRangeType(t));
35843
- if (isRangeOnly) {
35844
- if (!isRangeInput(currentArg)) {
35845
- throw new BadExpressionError(_t("Function %s expects the parameter %s to be reference to a cell or range, not a %s.", functionName, (i + 1).toString(), currentArg.type.toLowerCase()));
35846
- }
35964
+ if (override || !this.borders?.[sheetId]?.[col + 1]?.[row]?.vertical) {
35965
+ this.history.update("borders", sheetId, col + 1, row, "vertical", border?.right);
35966
+ }
35967
+ if (override || !this.borders?.[sheetId]?.[col]?.[row + 1]?.horizontal) {
35968
+ this.history.update("borders", sheetId, col, row + 1, "horizontal", border?.bottom);
35969
+ }
35970
+ }
35971
+ /**
35972
+ * Remove the borders of a zone
35973
+ */
35974
+ clearBorders(sheetId, zones) {
35975
+ for (let zone of zones) {
35976
+ for (let row = zone.top; row <= zone.bottom; row++) {
35977
+ this.history.update("borders", sheetId, zone.right + 1, row, "vertical", undefined);
35978
+ for (let col = zone.left; col <= zone.right; col++) {
35979
+ this.history.update("borders", sheetId, col, row, undefined);
35847
35980
  }
35848
- const compiledAST = compileAST(currentArg, isMeta, hasRange, {
35849
- functionName,
35850
- paramIndex: i + 1,
35851
- });
35852
- compiledArgs.push(isLazy ? compiledAST.wrapInClosure() : compiledAST);
35853
35981
  }
35854
- return compiledArgs;
35982
+ for (let col = zone.left; col <= zone.right; col++) {
35983
+ this.history.update("borders", sheetId, col, zone.bottom + 1, "horizontal", undefined);
35984
+ }
35855
35985
  }
35856
- /**
35857
- * This function compiles all the information extracted by the parser into an
35858
- * executable code for the evaluation of the cells content. It uses a cash to
35859
- * not reevaluate identical code structures.
35860
- *
35861
- * The function is sensitive to parameter “isMeta”. This
35862
- * parameter may vary when compiling function arguments:
35863
- * isMeta: In some cases the function arguments expects information on the
35864
- * cell/range other than the associated value(s). For example the COLUMN
35865
- * function needs to receive as argument the coordinates of a cell rather
35866
- * than its value. For this we have meta arguments.
35867
- */
35868
- function compileAST(ast, isMeta = false, hasRange = false, referenceVerification = {}) {
35869
- const code = new FunctionCodeBuilder(scope);
35870
- if (ast.type !== "REFERENCE" && !(ast.type === "BIN_OPERATION" && ast.value === ":")) {
35871
- if (isMeta) {
35872
- throw new BadExpressionError(_t("Argument must be a reference to a cell or range."));
35986
+ }
35987
+ /**
35988
+ * Remove the borders inside of a zone
35989
+ */
35990
+ clearInsideBorders(sheetId, zones) {
35991
+ for (let zone of zones) {
35992
+ for (let row = zone.top; row <= zone.bottom; row++) {
35993
+ for (let col = zone.left; col <= zone.right; col++) {
35994
+ this.history.update("borders", sheetId, col, row, undefined);
35873
35995
  }
35874
35996
  }
35875
- if (ast.debug) {
35876
- code.append("debugger;");
35877
- }
35878
- switch (ast.type) {
35879
- case "BOOLEAN":
35880
- return code.return(`{ value: ${ast.value} }`);
35881
- case "NUMBER":
35882
- return code.return(`{ value: this.constantValues.numbers[${constantValues.numbers.indexOf(ast.value)}] }`);
35883
- case "STRING":
35884
- return code.return(`{ value: this.constantValues.strings[${constantValues.strings.indexOf(ast.value)}] }`);
35885
- case "REFERENCE":
35886
- const referenceIndex = dependencies.indexOf(ast.value);
35887
- if (hasRange) {
35888
- return code.return(`range(deps[${referenceIndex}])`);
35997
+ }
35998
+ }
35999
+ /**
36000
+ * Add a border to the existing one to a cell
36001
+ */
36002
+ addBorder(sheetId, col, row, border) {
36003
+ this.setBorder(sheetId, col, row, {
36004
+ ...this.getCellBorder({ sheetId, col, row }),
36005
+ ...border,
36006
+ });
36007
+ }
36008
+ /**
36009
+ * Set the borders of a zone by computing the borders to add from the given
36010
+ * command
36011
+ */
36012
+ setBorders(sheetId, zones, position, border) {
36013
+ if (position === "clear") {
36014
+ return this.clearBorders(sheetId, zones);
36015
+ }
36016
+ for (let zone of zones) {
36017
+ if (position === "h" || position === "hv" || position === "all") {
36018
+ for (let row = zone.top + 1; row <= zone.bottom; row++) {
36019
+ for (let col = zone.left; col <= zone.right; col++) {
36020
+ this.addBorder(sheetId, col, row, { top: border });
35889
36021
  }
35890
- else {
35891
- return code.return(`ref(deps[${referenceIndex}], ${isMeta ? "true" : "false"}, "${referenceVerification.functionName || OPERATOR_MAP["="]}", ${referenceVerification.paramIndex})`);
36022
+ }
36023
+ }
36024
+ if (position === "v" || position === "hv" || position === "all") {
36025
+ for (let row = zone.top; row <= zone.bottom; row++) {
36026
+ for (let col = zone.left + 1; col <= zone.right; col++) {
36027
+ this.addBorder(sheetId, col, row, { left: border });
35892
36028
  }
35893
- case "FUNCALL":
35894
- const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable());
35895
- code.append(...args);
35896
- const fnName = ast.value.toUpperCase();
35897
- code.append(`ctx.__lastFnCalled = '${fnName}';`);
35898
- return code.return(`ctx['${fnName}'](${args.map((arg) => arg.returnExpression)})`);
35899
- case "UNARY_OPERATION": {
35900
- const fnName = UNARY_OPERATOR_MAP[ast.value];
35901
- const operand = compileAST(ast.operand, false, false, {
35902
- functionName: fnName,
35903
- }).assignResultToVariable();
35904
- code.append(operand);
35905
- code.append(`ctx.__lastFnCalled = '${fnName}';`);
35906
- return code.return(`ctx['${fnName}'](${operand.returnExpression})`);
35907
36029
  }
35908
- case "BIN_OPERATION": {
35909
- const fnName = OPERATOR_MAP[ast.value];
35910
- const left = compileAST(ast.left, false, false, {
35911
- functionName: fnName,
35912
- }).assignResultToVariable();
35913
- const right = compileAST(ast.right, false, false, {
35914
- functionName: fnName,
35915
- }).assignResultToVariable();
35916
- code.append(left);
35917
- code.append(right);
35918
- code.append(`ctx.__lastFnCalled = '${fnName}';`);
35919
- return code.return(`ctx['${fnName}'](${left.returnExpression}, ${right.returnExpression})`);
36030
+ }
36031
+ if (position === "left" || position === "all" || position === "external") {
36032
+ for (let row = zone.top; row <= zone.bottom; row++) {
36033
+ this.addBorder(sheetId, zone.left, row, { left: border });
35920
36034
  }
35921
- case "EMPTY":
35922
- return code.return("undefined");
35923
36035
  }
35924
- }
35925
- }
35926
- const compiledFormula = {
35927
- execute: functionCache[cacheKey].execute,
35928
- dependencies,
35929
- constantValues,
35930
- tokens,
35931
- };
35932
- return compiledFormula;
35933
- }
35934
- /**
35935
- * Compute a cache key for the formula.
35936
- * References, numbers and strings are replaced with placeholders because
35937
- * the compiled formula does not depend on their actual value.
35938
- * Both `=A1+1+"2"` and `=A2+2+"3"` are compiled to the exact same function.
35939
- *
35940
- * Spaces are also ignored to compute the cache key.
35941
- *
35942
- * A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|0|+|1|+SUM(|N0|,|N0|,|S0|)`
35943
- */
35944
- function compilationCacheKey(tokens, dependencies, constantValues) {
35945
- return concat(tokens.map((token) => {
35946
- switch (token.type) {
35947
- case "STRING":
35948
- const value = removeStringQuotes(token.value);
35949
- return `|S${constantValues.strings.indexOf(value)}|`;
35950
- case "NUMBER":
35951
- return `|N${constantValues.numbers.indexOf(parseNumber(token.value, DEFAULT_LOCALE))}|`;
35952
- case "REFERENCE":
35953
- case "INVALID_REFERENCE":
35954
- return `|${dependencies.indexOf(token.value)}|`;
35955
- case "SPACE":
35956
- return "";
35957
- default:
35958
- return token.value;
35959
- }
35960
- }));
35961
- }
35962
- /**
35963
- * Return formula arguments which are references, strings and numbers.
35964
- */
35965
- function formulaArguments(tokens) {
35966
- const constantValues = {
35967
- numbers: [],
35968
- strings: [],
35969
- };
35970
- const dependencies = [];
35971
- for (const token of tokens) {
35972
- switch (token.type) {
35973
- case "INVALID_REFERENCE":
35974
- case "REFERENCE":
35975
- dependencies.push(token.value);
35976
- break;
35977
- case "STRING":
35978
- const value = removeStringQuotes(token.value);
35979
- if (!constantValues.strings.includes(value)) {
35980
- constantValues.strings.push(value);
36036
+ if (position === "right" || position === "all" || position === "external") {
36037
+ for (let row = zone.top; row <= zone.bottom; row++) {
36038
+ this.addBorder(sheetId, zone.right + 1, row, { left: border });
35981
36039
  }
35982
- break;
35983
- case "NUMBER": {
35984
- const value = parseNumber(token.value, DEFAULT_LOCALE);
35985
- if (!constantValues.numbers.includes(value)) {
35986
- constantValues.numbers.push(value);
36040
+ }
36041
+ if (position === "top" || position === "all" || position === "external") {
36042
+ for (let col = zone.left; col <= zone.right; col++) {
36043
+ this.addBorder(sheetId, col, zone.top, { top: border });
36044
+ }
36045
+ }
36046
+ if (position === "bottom" || position === "all" || position === "external") {
36047
+ for (let col = zone.left; col <= zone.right; col++) {
36048
+ this.addBorder(sheetId, col, zone.bottom + 1, { top: border });
35987
36049
  }
35988
- break;
35989
36050
  }
35990
36051
  }
35991
36052
  }
35992
- return {
35993
- dependencies,
35994
- constantValues,
35995
- };
35996
- }
35997
- /**
35998
- * Check if arguments are supplied in the correct quantities
35999
- */
36000
- function assertEnoughArgs(ast) {
36001
- const nbrArg = ast.args.length;
36002
- const functionName = ast.value.toUpperCase();
36003
- const functionDefinition = functions$1[functionName];
36004
- if (nbrArg < functionDefinition.minArgRequired) {
36005
- throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected %s minimum, but got %s instead.", functionName, functionDefinition.minArgRequired.toString(), nbrArg.toString()));
36006
- }
36007
- if (nbrArg > functionDefinition.maxArgPossible) {
36008
- throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected %s maximum, but got %s instead.", functionName, functionDefinition.maxArgPossible.toString(), nbrArg.toString()));
36009
- }
36010
- const repeatableArgs = functionDefinition.nbrArgRepeating;
36011
- if (repeatableArgs > 1) {
36012
- const unrepeatableArgs = functionDefinition.args.length - repeatableArgs;
36013
- const repeatingArgs = nbrArg - unrepeatableArgs;
36014
- if (repeatingArgs % repeatableArgs !== 0) {
36015
- throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected all arguments after position %s to be supplied by groups of %s arguments", functionName, unrepeatableArgs.toString(), repeatableArgs.toString()));
36053
+ /**
36054
+ * Compute the borders to add to the given zone merged.
36055
+ */
36056
+ addBordersToMerge(sheetId, zone) {
36057
+ const { left, right, top, bottom } = zone;
36058
+ const bordersTopLeft = this.getCellBorder({ sheetId, col: left, row: top });
36059
+ const bordersBottomRight = this.getCellBorder({ sheetId, col: right, row: bottom });
36060
+ this.clearBorders(sheetId, [zone]);
36061
+ if (bordersTopLeft?.top) {
36062
+ this.setBorders(sheetId, [{ ...zone, bottom: top }], "top", bordersTopLeft.top);
36016
36063
  }
36017
- }
36018
- }
36019
- function isRangeType(type) {
36020
- return type.startsWith("RANGE");
36021
- }
36022
- function isRangeInput(arg) {
36023
- if (arg.type === "REFERENCE") {
36024
- return true;
36025
- }
36026
- if (arg.type === "FUNCALL") {
36027
- const fnDef = functions$1[arg.value.toUpperCase()];
36028
- return fnDef && isRangeType(fnDef.returns[0]);
36029
- }
36030
- return false;
36031
- }
36032
-
36033
- /**
36034
- * Add the following information on tokens:
36035
- * - length
36036
- * - start
36037
- * - end
36038
- */
36039
- function enrichTokens(tokens) {
36040
- let current = 0;
36041
- return tokens.map((x) => {
36042
- const len = x.value.toString().length;
36043
- const token = Object.assign({}, x, {
36044
- start: current,
36045
- end: current + len,
36046
- length: len,
36047
- });
36048
- current = token.end;
36049
- return token;
36050
- });
36051
- }
36052
- /**
36053
- * add on each token the length, start and end
36054
- * also matches the opening to its closing parenthesis (using the same number)
36055
- */
36056
- function mapParenthesis(tokens) {
36057
- let maxParen = 1;
36058
- const stack = [];
36059
- return tokens.map((token) => {
36060
- if (token.type === "LEFT_PAREN") {
36061
- stack.push(maxParen);
36062
- token.parenIndex = maxParen;
36063
- maxParen++;
36064
+ if (bordersTopLeft?.left) {
36065
+ this.setBorders(sheetId, [{ ...zone, right: left }], "left", bordersTopLeft.left);
36064
36066
  }
36065
- else if (token.type === "RIGHT_PAREN") {
36066
- token.parenIndex = stack.pop();
36067
+ if (bordersBottomRight?.bottom) {
36068
+ this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom", bordersBottomRight.bottom);
36067
36069
  }
36068
- return token;
36069
- });
36070
- }
36071
- /**
36072
- * add on each token its parent function and the index corresponding to
36073
- * its position as an argument of the function.
36074
- * In this example "=MIN(42,SUM(MAX(1,2),3))":
36075
- * - the parent function of the token correspond to number 42 is the MIN function
36076
- * - the argument position of the token correspond to number 42 is 0
36077
- * - the parent function of the token correspond to number 3 is the SUM function
36078
- * - the argument position of the token correspond to number 3 is 1
36079
- */
36080
- function mapParentFunction(tokens) {
36081
- let stack = [];
36082
- let functionStarted = "";
36083
- const res = tokens.map((token, i) => {
36084
- if (!["SPACE", "LEFT_PAREN"].includes(token.type)) {
36085
- functionStarted = "";
36070
+ else if (bordersTopLeft?.bottom) {
36071
+ this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom", bordersTopLeft.bottom);
36086
36072
  }
36087
- switch (token.type) {
36088
- case "SYMBOL":
36089
- functionStarted = token.value;
36090
- break;
36091
- case "LEFT_PAREN":
36092
- stack.push({ parent: functionStarted, argPosition: 0 });
36093
- functionStarted = "";
36094
- break;
36095
- case "RIGHT_PAREN":
36096
- stack.pop();
36097
- break;
36098
- case "ARG_SEPARATOR":
36099
- if (stack.length) {
36100
- // increment position on current function
36101
- stack[stack.length - 1].argPosition++;
36073
+ if (bordersBottomRight?.right) {
36074
+ this.setBorders(sheetId, [{ ...zone, left: right }], "right", bordersBottomRight.right);
36075
+ }
36076
+ else if (bordersTopLeft?.right) {
36077
+ this.setBorders(sheetId, [{ ...zone, left: right }], "right", bordersTopLeft.right);
36078
+ }
36079
+ }
36080
+ checkBordersUnchanged(cmd) {
36081
+ const currentBorder = this.getCellBorder(cmd);
36082
+ const areAllNewBordersUndefined = !cmd.border?.bottom && !cmd.border?.left && !cmd.border?.right && !cmd.border?.top;
36083
+ if ((!currentBorder && areAllNewBordersUndefined) || deepEquals(currentBorder, cmd.border)) {
36084
+ return "NoChanges" /* CommandResult.NoChanges */;
36085
+ }
36086
+ return "Success" /* CommandResult.Success */;
36087
+ }
36088
+ // ---------------------------------------------------------------------------
36089
+ // Import/Export
36090
+ // ---------------------------------------------------------------------------
36091
+ import(data) {
36092
+ // Borders
36093
+ if (Object.keys(data.borders || {}).length) {
36094
+ for (let sheet of data.sheets) {
36095
+ for (const xc in sheet.cells) {
36096
+ const cell = sheet.cells[xc];
36097
+ if (cell?.border) {
36098
+ const border = data.borders[cell.border];
36099
+ const { col, row } = toCartesian(xc);
36100
+ this.setBorder(sheet.id, col, row, border, false);
36101
+ }
36102
36102
  }
36103
- break;
36103
+ }
36104
36104
  }
36105
- if (stack.length) {
36106
- const functionContext = stack[stack.length - 1];
36107
- if (functionContext.parent) {
36108
- token.functionContext = Object.assign({}, functionContext);
36105
+ // Merges
36106
+ for (let sheetData of data.sheets) {
36107
+ if (sheetData.merges) {
36108
+ for (let merge of sheetData.merges) {
36109
+ this.addBordersToMerge(sheetData.id, toZone(merge));
36110
+ }
36109
36111
  }
36110
36112
  }
36111
- return token;
36112
- });
36113
- return res;
36114
- }
36115
- /**
36116
- * Take the result of the tokenizer and transform it to be usable in the composer.
36117
- *
36118
- * @param formula
36119
- */
36120
- function composerTokenize(formula, locale) {
36121
- const tokens = rangeTokenize(formula, locale);
36122
- return mapParentFunction(mapParenthesis(enrichTokens(tokens)));
36123
- }
36124
-
36125
- const functions = functionRegistry.content;
36126
- function isExportableToExcel(tokens) {
36127
- try {
36128
- const nonExportableFunctions = iterateAstNodes(parseTokens(tokens)).filter((ast) => ast.type === "FUNCALL" && !functions[ast.value.toUpperCase()]?.isExported);
36129
- return nonExportableFunctions.length === 0;
36130
36113
  }
36131
- catch (error) {
36132
- return false;
36114
+ export(data) {
36115
+ const borders = {};
36116
+ for (let sheet of data.sheets) {
36117
+ for (let col = 0; col < sheet.colNumber; col++) {
36118
+ for (let row = 0; row < sheet.rowNumber; row++) {
36119
+ const border = this.getCellBorder({ sheetId: sheet.id, col, row });
36120
+ if (border) {
36121
+ const xc = toXC(col, row);
36122
+ const cell = sheet.cells[xc];
36123
+ const borderId = getItemId(border, borders);
36124
+ if (cell) {
36125
+ cell.border = borderId;
36126
+ }
36127
+ else {
36128
+ sheet.cells[xc] = { border: borderId };
36129
+ }
36130
+ }
36131
+ }
36132
+ }
36133
+ }
36134
+ data.borders = borders;
36135
+ }
36136
+ exportForExcel(data) {
36137
+ this.export(data);
36133
36138
  }
36134
36139
  }
36135
36140
 
@@ -58412,6 +58417,6 @@ const constants = {
58412
58417
  export { AbstractChart, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, UIPlugin, __info__, addFunction, astToFormula, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, tokenize };
58413
58418
 
58414
58419
 
58415
- __info__.version = "17.0.70";
58416
- __info__.date = "2025-07-28T13:26:37.626Z";
58417
- __info__.hash = "2ec8d82";
58420
+ __info__.version = "17.0.71";
58421
+ __info__.date = "2025-08-21T06:37:34.192Z";
58422
+ __info__.hash = "1592964";