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