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