@borgar/fx 4.11.2 → 4.13.0
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/benchmark/benchmark.js +48 -0
- package/benchmark/formulas.json +15677 -0
- package/dist/fx.d.ts +3 -0
- package/dist/fx.js +2 -2
- package/docs/API.md +1 -0
- package/lib/fixRanges.spec.js +2 -1
- package/lib/lexer.js +38 -57
- package/lib/lexers/advRangeOp.js +18 -0
- package/lib/lexers/canEndRange.js +25 -0
- package/lib/lexers/lexBoolean.js +36 -0
- package/lib/lexers/lexContext.js +96 -0
- package/lib/lexers/lexError.js +15 -0
- package/lib/lexers/lexFunction.js +36 -0
- package/lib/lexers/lexNamed.js +60 -0
- package/lib/lexers/lexNewLine.js +11 -0
- package/lib/lexers/lexNumber.js +47 -0
- package/lib/lexers/lexOperator.js +25 -0
- package/lib/lexers/lexRange.js +8 -0
- package/lib/lexers/lexRangeA1.js +130 -0
- package/lib/lexers/lexRangeR1C1.js +142 -0
- package/lib/lexers/lexRangeTrim.js +25 -0
- package/lib/lexers/lexRefOp.js +18 -0
- package/lib/lexers/lexString.js +22 -0
- package/lib/lexers/lexStructured.js +25 -0
- package/lib/lexers/lexWhitespace.js +30 -0
- package/lib/lexers/sets.js +38 -0
- package/lib/mergeRefTokens.js +33 -23
- package/lib/parseRef.js +1 -1
- package/lib/parseSRange.js +184 -114
- package/lib/parseStructRef.spec.js +4 -0
- package/lib/parser.js +12 -8
- package/lib/parser.spec.js +12 -0
- package/package.json +12 -10
- package/lib/lexerParts.js +0 -228
package/lib/parseSRange.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/* eslint-disable no-multi-spaces */
|
|
2
|
+
/* eslint-disable no-undefined */
|
|
3
|
+
import { isWS } from './lexers/lexWhitespace.js';
|
|
4
|
+
|
|
5
|
+
const AT = 64; // @
|
|
6
|
+
const BR_CLOSE = 93; // ]
|
|
7
|
+
const BR_OPEN = 91; // [
|
|
8
|
+
const COLON = 58; // :
|
|
9
|
+
const COMMA = 44; // ,
|
|
10
|
+
const HASH = 35; // #
|
|
11
|
+
const QUOT_SINGLE = 39; // '
|
|
3
12
|
|
|
4
13
|
const keyTerms = {
|
|
5
14
|
'headers': 1,
|
|
@@ -10,9 +19,8 @@ const keyTerms = {
|
|
|
10
19
|
'@': 16
|
|
11
20
|
};
|
|
12
21
|
|
|
13
|
-
const fz = (...a) => Object.freeze(a);
|
|
14
|
-
|
|
15
22
|
// only combinations allowed are: #data + (#headers | #totals | #data)
|
|
23
|
+
const fz = (...a) => Object.freeze(a);
|
|
16
24
|
const sectionMap = {
|
|
17
25
|
// no terms
|
|
18
26
|
0: fz(),
|
|
@@ -28,138 +36,200 @@ const sectionMap = {
|
|
|
28
36
|
6: fz('data', 'totals')
|
|
29
37
|
};
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
let
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
return [ m[0], value ];
|
|
39
|
+
function matchKeyword (str, pos) {
|
|
40
|
+
let p = pos;
|
|
41
|
+
if (str.charCodeAt(p++) !== BR_OPEN) {
|
|
42
|
+
return;
|
|
36
43
|
}
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
if (str.charCodeAt(p++) !== HASH) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
do {
|
|
48
|
+
const c = str.charCodeAt(p);
|
|
49
|
+
if (
|
|
50
|
+
(c >= 65 && c <= 90) || // A-Z
|
|
51
|
+
(c >= 97 && c <= 122) || // a-z
|
|
52
|
+
(c === 32) // space
|
|
53
|
+
) {
|
|
54
|
+
p++;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
break;
|
|
41
58
|
}
|
|
42
59
|
}
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
while (p < pos + 11); // max length: '[#this row'
|
|
61
|
+
if (str.charCodeAt(p++) !== BR_CLOSE) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
return p - pos;
|
|
65
|
+
}
|
|
45
66
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
let m1;
|
|
52
|
-
let terms = 0;
|
|
67
|
+
function skipWhitespace (str, pos) {
|
|
68
|
+
let p = pos;
|
|
69
|
+
while (isWS(str.charCodeAt(p))) { p++; }
|
|
70
|
+
return p - pos;
|
|
71
|
+
}
|
|
53
72
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
else if ((m1 = matchColumn(s, false))) {
|
|
70
|
-
pos += m1[0].length;
|
|
71
|
-
columns.push(m1[1]);
|
|
72
|
-
}
|
|
73
|
-
// use the "normal" method
|
|
74
|
-
// [[#keyword]]
|
|
75
|
-
// [[column]]
|
|
76
|
-
// [@]
|
|
77
|
-
// [@column]
|
|
78
|
-
// [@[column]]
|
|
79
|
-
// [@column:column]
|
|
80
|
-
// [@column:[column]]
|
|
81
|
-
// [@[column]:column]
|
|
82
|
-
// [@[column]:[column]]
|
|
83
|
-
// [column:column]
|
|
84
|
-
// [column:[column]]
|
|
85
|
-
// [[column]:column]
|
|
86
|
-
// [[column]:[column]]
|
|
87
|
-
// [[#keyword],column]
|
|
88
|
-
// [[#keyword],column:column]
|
|
89
|
-
// [[#keyword],[#keyword],column:column]
|
|
90
|
-
// ...
|
|
91
|
-
else {
|
|
92
|
-
let expect_more = true;
|
|
93
|
-
s = s.slice(m[1].length);
|
|
94
|
-
pos += m[1].length;
|
|
95
|
-
// match keywords as we find them
|
|
96
|
-
while (
|
|
97
|
-
expect_more &&
|
|
98
|
-
(m = /^\[#([a-z ]+)\](\s*,\s*)?/i.exec(s))
|
|
99
|
-
) {
|
|
100
|
-
const k = m[1].toLowerCase();
|
|
101
|
-
if (keyTerms[k]) {
|
|
102
|
-
terms |= keyTerms[k];
|
|
103
|
-
s = s.slice(m[0].length);
|
|
104
|
-
pos += m[0].length;
|
|
105
|
-
expect_more = !!m[2];
|
|
73
|
+
function matchColumn (str, pos, allowUnbraced = true) {
|
|
74
|
+
let p = pos;
|
|
75
|
+
let column = '';
|
|
76
|
+
if (str.charCodeAt(p) === BR_OPEN) {
|
|
77
|
+
p++;
|
|
78
|
+
let c;
|
|
79
|
+
do {
|
|
80
|
+
c = str.charCodeAt(p);
|
|
81
|
+
if (c === QUOT_SINGLE) {
|
|
82
|
+
p++;
|
|
83
|
+
c = str.charCodeAt(p);
|
|
84
|
+
// Allowed set: '#@[]
|
|
85
|
+
if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN || c === BR_CLOSE) {
|
|
86
|
+
column += String.fromCharCode(c);
|
|
87
|
+
p++;
|
|
106
88
|
}
|
|
107
89
|
else {
|
|
108
|
-
return
|
|
90
|
+
return;
|
|
109
91
|
}
|
|
110
92
|
}
|
|
111
|
-
// is
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
s = s.slice(1);
|
|
115
|
-
pos += 1;
|
|
116
|
-
expect_more = s[0] !== ']';
|
|
93
|
+
// Allowed set is all chars BUT: '#@[]
|
|
94
|
+
else if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN) {
|
|
95
|
+
return;
|
|
117
96
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
97
|
+
else if (c === BR_CLOSE) {
|
|
98
|
+
p++;
|
|
99
|
+
return [ str.slice(pos, p), column ];
|
|
121
100
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
pos += leftCol[0].length;
|
|
126
|
-
columns.push(leftCol[1]);
|
|
127
|
-
s = raw.slice(pos);
|
|
128
|
-
if (s[0] === ':') {
|
|
129
|
-
s = s.slice(1);
|
|
130
|
-
pos++;
|
|
131
|
-
const rightCol = matchColumn(s);
|
|
132
|
-
if (rightCol) {
|
|
133
|
-
pos += rightCol[0].length;
|
|
134
|
-
columns.push(rightCol[1]);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
expect_more = false;
|
|
101
|
+
else {
|
|
102
|
+
column += String.fromCharCode(c);
|
|
103
|
+
p++;
|
|
141
104
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
105
|
+
}
|
|
106
|
+
while (p < str.length);
|
|
107
|
+
}
|
|
108
|
+
else if (allowUnbraced) {
|
|
109
|
+
let c;
|
|
110
|
+
do {
|
|
111
|
+
c = str.charCodeAt(p);
|
|
112
|
+
// Allowed set is all chars BUT: '#@[]:
|
|
113
|
+
if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN || c === BR_CLOSE || c === COLON) {
|
|
114
|
+
break;
|
|
145
115
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
116
|
+
else {
|
|
117
|
+
column += String.fromCharCode(c);
|
|
118
|
+
p++;
|
|
149
119
|
}
|
|
150
|
-
|
|
151
|
-
|
|
120
|
+
}
|
|
121
|
+
while (p < str.length);
|
|
122
|
+
if (p !== pos) {
|
|
123
|
+
return [ column, column ];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function parseSRange (str, pos = 0) {
|
|
129
|
+
const columns = [];
|
|
130
|
+
const start = pos;
|
|
131
|
+
let m;
|
|
132
|
+
let terms = 0;
|
|
133
|
+
|
|
134
|
+
// structured refs start with a [
|
|
135
|
+
if (str.charCodeAt(pos) !== BR_OPEN) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// simple keyword: [#keyword]
|
|
140
|
+
if ((m = matchKeyword(str, pos))) {
|
|
141
|
+
const k = str.slice(pos + 2, pos + m - 1);
|
|
142
|
+
pos += m;
|
|
143
|
+
const term = keyTerms[k.toLowerCase()];
|
|
144
|
+
if (!term) { return; }
|
|
145
|
+
terms |= term;
|
|
146
|
+
}
|
|
147
|
+
// simple column: [column]
|
|
148
|
+
else if ((m = matchColumn(str, pos, false))) {
|
|
149
|
+
pos += m[0].length;
|
|
150
|
+
if (m[1]) {
|
|
151
|
+
columns.push(m[1]);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
+
// use the "normal" method
|
|
155
|
+
// [[#keyword]]
|
|
156
|
+
// [[column]]
|
|
157
|
+
// [@]
|
|
158
|
+
// [@column]
|
|
159
|
+
// [@[column]]
|
|
160
|
+
// [@column:column]
|
|
161
|
+
// [@column:[column]]
|
|
162
|
+
// [@[column]:column]
|
|
163
|
+
// [@[column]:[column]]
|
|
164
|
+
// [column:column]
|
|
165
|
+
// [column:[column]]
|
|
166
|
+
// [[column]:column]
|
|
167
|
+
// [[column]:[column]]
|
|
168
|
+
// [[#keyword],column]
|
|
169
|
+
// [[#keyword],column:column]
|
|
170
|
+
// [[#keyword],[#keyword],column:column]
|
|
171
|
+
// ...
|
|
154
172
|
else {
|
|
155
|
-
|
|
173
|
+
let expect_more = true;
|
|
174
|
+
pos++; // skip open brace
|
|
175
|
+
pos += skipWhitespace(str, pos);
|
|
176
|
+
// match keywords as we find them
|
|
177
|
+
while (expect_more && (m = matchKeyword(str, pos))) {
|
|
178
|
+
const k = str.slice(pos + 2, pos + m - 1);
|
|
179
|
+
const term = keyTerms[k.toLowerCase()];
|
|
180
|
+
if (!term) { return; }
|
|
181
|
+
terms |= term;
|
|
182
|
+
pos += m;
|
|
183
|
+
pos += skipWhitespace(str, pos);
|
|
184
|
+
expect_more = str.charCodeAt(pos) === COMMA;
|
|
185
|
+
if (expect_more) {
|
|
186
|
+
pos++;
|
|
187
|
+
pos += skipWhitespace(str, pos);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// is there an @ specifier?
|
|
191
|
+
if (expect_more && (str.charCodeAt(pos) === AT)) {
|
|
192
|
+
terms |= keyTerms['@'];
|
|
193
|
+
pos += 1;
|
|
194
|
+
expect_more = str.charCodeAt(pos) !== BR_CLOSE;
|
|
195
|
+
}
|
|
196
|
+
// not all keyword terms may be combined
|
|
197
|
+
if (!sectionMap[terms]) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// column definitions
|
|
201
|
+
const leftCol = expect_more && matchColumn(str, pos, true);
|
|
202
|
+
if (leftCol) {
|
|
203
|
+
pos += leftCol[0].length;
|
|
204
|
+
columns.push(leftCol[1]);
|
|
205
|
+
if (str.charCodeAt(pos) === COLON) {
|
|
206
|
+
pos++;
|
|
207
|
+
const rightCol = matchColumn(str, pos, true);
|
|
208
|
+
if (rightCol) {
|
|
209
|
+
pos += rightCol[0].length;
|
|
210
|
+
columns.push(rightCol[1]);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
expect_more = false;
|
|
217
|
+
}
|
|
218
|
+
// advance ws
|
|
219
|
+
pos += skipWhitespace(str, pos);
|
|
220
|
+
// close the ref
|
|
221
|
+
if (expect_more || str.charCodeAt(pos) !== BR_CLOSE) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// step over the closing ]
|
|
225
|
+
pos++;
|
|
156
226
|
}
|
|
157
227
|
|
|
158
228
|
const sections = sectionMap[terms];
|
|
159
229
|
return {
|
|
160
230
|
columns,
|
|
161
231
|
sections: sections ? sections.concat() : sections,
|
|
162
|
-
length: pos,
|
|
163
|
-
token:
|
|
232
|
+
length: pos - start,
|
|
233
|
+
token: str.slice(start, pos)
|
|
164
234
|
};
|
|
165
235
|
}
|
package/lib/parser.js
CHANGED
|
@@ -63,8 +63,16 @@ const refFunctions = [
|
|
|
63
63
|
'XLOOKUP'
|
|
64
64
|
];
|
|
65
65
|
|
|
66
|
+
const symbolTable = {};
|
|
67
|
+
let currentNode;
|
|
68
|
+
let tokens;
|
|
69
|
+
let tokenIndex;
|
|
70
|
+
let permitArrayRanges = false;
|
|
71
|
+
let permitArrayCalls = false;
|
|
72
|
+
let looseRefCalls = false;
|
|
73
|
+
|
|
66
74
|
const isReferenceFunctionName = fnName => {
|
|
67
|
-
return refFunctions.includes(fnName.toUpperCase());
|
|
75
|
+
return looseRefCalls || refFunctions.includes(fnName.toUpperCase());
|
|
68
76
|
};
|
|
69
77
|
|
|
70
78
|
const isReferenceToken = (token, allowOperators = false) => {
|
|
@@ -98,13 +106,6 @@ const isReferenceNode = node => {
|
|
|
98
106
|
);
|
|
99
107
|
};
|
|
100
108
|
|
|
101
|
-
const symbolTable = {};
|
|
102
|
-
let currentNode;
|
|
103
|
-
let tokens;
|
|
104
|
-
let tokenIndex;
|
|
105
|
-
let permitArrayRanges = false;
|
|
106
|
-
let permitArrayCalls = false;
|
|
107
|
-
|
|
108
109
|
function halt (message, atIndex = null) {
|
|
109
110
|
const err = new Error(message);
|
|
110
111
|
err.source = tokens.map(d => d.value).join('');
|
|
@@ -688,6 +689,7 @@ prefix('{', function () {
|
|
|
688
689
|
* @param {boolean} [options.negativeNumbers=true] Merges unary minuses with their immediately following number tokens (`-`,`1`) => `-1` (alternatively these will be unary operations in the tree).
|
|
689
690
|
* @param {boolean} [options.permitArrayRanges=false] Ranges are allowed as elements of arrays. This is a feature in Google Sheets while Excel does not allow it.
|
|
690
691
|
* @param {boolean} [options.permitArrayCalls=false] Function calls are allowed as elements of arrays. This is a feature in Google Sheets while Excel does not allow it.
|
|
692
|
+
* @param {boolean} [options.looseRefCalls=false] Permits any function call where otherwise only functions that return references would be permitted.
|
|
691
693
|
* @param {boolean} [options.r1c1=false] Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
|
|
692
694
|
* @param {boolean} [options.withLocation=false] Nodes will include source position offsets to the tokens: `{ loc: [ start, end ] }`
|
|
693
695
|
* @param {boolean} [options.xlsx=false] Switches to the `[1]Sheet1!A1` or `[1]!name` prefix syntax form for external workbooks. See: [Prefixes.md](./Prefixes.md)
|
|
@@ -711,6 +713,8 @@ export function parse (formula, options) {
|
|
|
711
713
|
permitArrayRanges = options?.permitArrayRanges;
|
|
712
714
|
// allow calls in arrays "literals"?
|
|
713
715
|
permitArrayCalls = options?.permitArrayCalls;
|
|
716
|
+
// allow any function call in range operations?
|
|
717
|
+
looseRefCalls = options?.looseRefCalls;
|
|
714
718
|
// set index to start
|
|
715
719
|
tokenIndex = 0;
|
|
716
720
|
// discard redundant whitespace and = prefix
|
package/lib/parser.spec.js
CHANGED
|
@@ -1194,3 +1194,15 @@ test('parser whitespace handling', t => {
|
|
|
1194
1194
|
});
|
|
1195
1195
|
t.end();
|
|
1196
1196
|
});
|
|
1197
|
+
|
|
1198
|
+
test('looseRefCalls: true relaxes ref function restrictions', t => {
|
|
1199
|
+
t.isInvalidExpr('A1:TESTFN()');
|
|
1200
|
+
t.isParsed('A1:TESTFN()', {
|
|
1201
|
+
type: 'BinaryExpression', operator: ':',
|
|
1202
|
+
arguments: [
|
|
1203
|
+
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
|
|
1204
|
+
{ type: 'CallExpression', callee: { type: 'Identifier', name: 'TESTFN' }, arguments: [] }
|
|
1205
|
+
]
|
|
1206
|
+
}, { looseRefCalls: true });
|
|
1207
|
+
t.end();
|
|
1208
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@borgar/fx",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.0",
|
|
4
4
|
"description": "Utilities for working with Excel formulas",
|
|
5
5
|
"main": "dist/fx.js",
|
|
6
6
|
"types": "dist/fx.d.ts",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"preversion": "npm test && npm run lint",
|
|
17
17
|
"version": "npm run build",
|
|
18
18
|
"lint": "eslint lib/*.js",
|
|
19
|
+
"benchmark": "node benchmark/benchmark.js",
|
|
19
20
|
"test": "tape lib/*.spec.js | tap-min",
|
|
20
21
|
"build:all": "npm run build:types && npm run build:docs && npm run build",
|
|
21
22
|
"build:types": "jsdoc -c tsd.json lib>dist/fx.d.ts",
|
|
@@ -43,20 +44,21 @@
|
|
|
43
44
|
"author": "Borgar Þorsteinsson <borgar@borgar.net> (http://borgar.net/)",
|
|
44
45
|
"license": "MIT",
|
|
45
46
|
"devDependencies": {
|
|
46
|
-
"@babel/core": "~7.
|
|
47
|
-
"@babel/eslint-parser": "~7.
|
|
48
|
-
"@babel/preset-env": "~7.
|
|
47
|
+
"@babel/core": "~7.28.5",
|
|
48
|
+
"@babel/eslint-parser": "~7.28.5",
|
|
49
|
+
"@babel/preset-env": "~7.28.5",
|
|
49
50
|
"@borgar/eslint-config": "~3.1.0",
|
|
50
|
-
"@borgar/jsdoc-tsmd": "~0.2.
|
|
51
|
-
"@rollup/plugin-babel": "~6.0
|
|
51
|
+
"@borgar/jsdoc-tsmd": "~0.2.2",
|
|
52
|
+
"@rollup/plugin-babel": "~6.1.0",
|
|
52
53
|
"@rollup/plugin-terser": "~0.4.4",
|
|
53
54
|
"babel-eslint": "~10.1.0",
|
|
55
|
+
"benchmark": "~2.1.4",
|
|
54
56
|
"eslint": "~8.56.0",
|
|
55
57
|
"eslint-plugin-jsdoc": "~48.1.0",
|
|
56
|
-
"jsdoc": "~4.0.
|
|
57
|
-
"rollup": "~4.
|
|
58
|
+
"jsdoc": "~4.0.5",
|
|
59
|
+
"rollup": "~4.52.5",
|
|
58
60
|
"tap-min": "~3.0.0",
|
|
59
|
-
"tape": "~5.
|
|
60
|
-
"typescript": "~5.
|
|
61
|
+
"tape": "~5.9.0",
|
|
62
|
+
"typescript": "~5.9.3"
|
|
61
63
|
}
|
|
62
64
|
}
|
package/lib/lexerParts.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
OPERATOR,
|
|
3
|
-
BOOLEAN,
|
|
4
|
-
ERROR,
|
|
5
|
-
NUMBER,
|
|
6
|
-
FUNCTION,
|
|
7
|
-
NEWLINE,
|
|
8
|
-
WHITESPACE,
|
|
9
|
-
STRING,
|
|
10
|
-
CONTEXT,
|
|
11
|
-
CONTEXT_QUOTE,
|
|
12
|
-
REF_RANGE,
|
|
13
|
-
REF_BEAM,
|
|
14
|
-
REF_NAMED,
|
|
15
|
-
REF_TERNARY,
|
|
16
|
-
REF_STRUCT,
|
|
17
|
-
MAX_COLS,
|
|
18
|
-
MAX_ROWS,
|
|
19
|
-
OPERATOR_TRIM
|
|
20
|
-
} from './constants.js';
|
|
21
|
-
import { fromCol } from './fromCol.js';
|
|
22
|
-
import { parseSRange } from './parseSRange.js';
|
|
23
|
-
|
|
24
|
-
const re_ERROR = /^#(NAME\?|FIELD!|CALC!|VALUE!|REF!|DIV\/0!|NULL!|NUM!|N\/A|GETTING_DATA\b|SPILL!|UNKNOWN!|FIELD\b|CALC\b|SYNTAX\?|ERROR!|CONNECT!|BLOCKED!|EXTERNAL!)/i;
|
|
25
|
-
const re_OPERATOR = /^(<=|>=|<>|[-+/*^%&<>=]|[{},;]|[()]|@|:|!|#)/;
|
|
26
|
-
const re_BOOLEAN = /^(TRUE|FALSE)\b/i;
|
|
27
|
-
const re_FUNCTION = /^[A-Z_]+[A-Z\d_.]*(?=\()/i;
|
|
28
|
-
const re_NEWLINE = /^\n+/;
|
|
29
|
-
const re_WHITESPACE = /^[ \f\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/;
|
|
30
|
-
const re_STRING = /^"(?:""|[^"])*("|$)/;
|
|
31
|
-
const re_NUMBER = /^(?:\d+(\.\d+)?(?:[eE][+-]?\d+)?|\d+)/;
|
|
32
|
-
const re_CONTEXT = /^(?!!)(\[(?:[^\]])+\])?([0-9A-Za-z._¡¤§¨ª\u00ad¯-\uffff]+)?(?=!)/;
|
|
33
|
-
const re_CONTEXT_QUOTE = /^'(?:''|[^'])*('|$)(?=!)/;
|
|
34
|
-
const re_RANGE_TRIM = /^(\.:\.|\.:|:\.)/;
|
|
35
|
-
|
|
36
|
-
const rngPart = '\\$?[A-Z]{1,3}\\$?[1-9][0-9]{0,6}';
|
|
37
|
-
const colPart = '\\$?[A-Z]{1,3}';
|
|
38
|
-
const rowPart = '\\$?[1-9][0-9]{0,6}';
|
|
39
|
-
const rangeOp = '\\.?:\\.?';
|
|
40
|
-
const nextNotChar = '(?![a-z0-9_\\u00a1-\\uffff])';
|
|
41
|
-
const re_A1COL = new RegExp(`^${colPart}${rangeOp}${colPart}${nextNotChar}`, 'i');
|
|
42
|
-
const re_A1ROW = new RegExp(`^${rowPart}${rangeOp}${rowPart}${nextNotChar}`, 'i');
|
|
43
|
-
const re_A1RANGE = new RegExp(`^${rngPart}${nextNotChar}`, 'i');
|
|
44
|
-
const re_A1PARTIAL = new RegExp(`^((${colPart}|${rowPart})${rangeOp}${rngPart}|${rngPart}${rangeOp}(${colPart}|${rowPart}))(?![\\w($.])`, 'i');
|
|
45
|
-
const rPart = '(?:R(?:\\[[+-]?\\d+\\]|[1-9][0-9]{0,6})?)';
|
|
46
|
-
const cPart = '(?:C(?:\\[[+-]?\\d+\\]|[1-9][0-9]{0,4})?)';
|
|
47
|
-
const re_RCCOL = new RegExp(`^${cPart}(${rangeOp}${cPart})?${nextNotChar}`, 'i');
|
|
48
|
-
const re_RCROW = new RegExp(`^${rPart}(${rangeOp}${rPart})?${nextNotChar}`, 'i');
|
|
49
|
-
const re_RCRANGE = new RegExp(`^(?:(?=[RC])${rPart}${cPart})${nextNotChar}`, 'i');
|
|
50
|
-
const re_RCPARTIAL = new RegExp(`^(${rPart}${cPart}(${rangeOp}${cPart}|${rangeOp}${rPart})(?![[\\d])|(${rPart}|${cPart})(${rangeOp}${rPart}${cPart}))${nextNotChar}`, 'i');
|
|
51
|
-
|
|
52
|
-
// The advertized named ranges rules are a bit off from what Excel seems to do:
|
|
53
|
-
// in the "extended range" of chars, it looks like it allows most things above
|
|
54
|
-
// U+00B0 with the range between U+00A0-U+00AF rather random.
|
|
55
|
-
// eslint-disable-next-line
|
|
56
|
-
// const re_NAMED = /^[a-zA-Z\\_¡¤§¨ª\u00ad¯\u00b0-\uffff][a-zA-Z0-9\\_.?¡¤§¨ª\u00ad¯\u00b0-\uffff]{0,254}/i;
|
|
57
|
-
// I've simplified to allowing everything above U+00A1:
|
|
58
|
-
const re_NAMED = /^[a-zA-Z\\_\u00a1-\uffff][a-zA-Z0-9\\_.?\u00a1-\uffff]{0,254}/i;
|
|
59
|
-
|
|
60
|
-
function makeHandler (type, re) {
|
|
61
|
-
return str => {
|
|
62
|
-
const m = re.exec(str);
|
|
63
|
-
if (m) {
|
|
64
|
-
return { type: type, value: m[0] };
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function lexNamed (str) {
|
|
70
|
-
const m = re_NAMED.exec(str);
|
|
71
|
-
if (m) {
|
|
72
|
-
const lc = m[0].toLowerCase();
|
|
73
|
-
// names starting with \ must be at least 3 char long
|
|
74
|
-
if (lc[0] === '\\' && m[0].length < 3) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
// single characters R and C are forbidden as names
|
|
78
|
-
if (lc === 'r' || lc === 'c') {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
return { type: REF_NAMED, value: m[0] };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const re_QUOTED_VALUE = /^'(?:[^[\]]+?)?(?:\[(.+?)\])?(?:[^[\]]+?)'$/;
|
|
86
|
-
const re_QUOTED_VALUE_XLSX = /^'\[(.+?)\]'$/;
|
|
87
|
-
function lexContext (str, options) {
|
|
88
|
-
const mq = re_CONTEXT_QUOTE.exec(str);
|
|
89
|
-
if (mq) {
|
|
90
|
-
const value = mq[0];
|
|
91
|
-
const isValid = options.xlsx
|
|
92
|
-
? re_QUOTED_VALUE_XLSX.test(value) || re_QUOTED_VALUE.test(value)
|
|
93
|
-
: re_QUOTED_VALUE.test(value);
|
|
94
|
-
if (isValid) {
|
|
95
|
-
return { type: CONTEXT_QUOTE, value: value };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// xlsx xml uses a variant of the syntax that has external references in
|
|
99
|
-
// bracets. Any of: [1]Sheet1!A1, '[1]Sheet one'!A1, [1]!named
|
|
100
|
-
// We're only concerned with the non quoted version here as the quoted version
|
|
101
|
-
// doesn't currently examine what is in the quotes.
|
|
102
|
-
const m = re_CONTEXT.exec(str);
|
|
103
|
-
if (m) {
|
|
104
|
-
const [ , a, b ] = m;
|
|
105
|
-
const valid = (
|
|
106
|
-
((a && b) || b) || // "[a]b!" or "b!" forms
|
|
107
|
-
(a && !b && options.xlsx) // "[a]" form (allowed in xlsx mode)
|
|
108
|
-
);
|
|
109
|
-
if (valid) {
|
|
110
|
-
return { type: CONTEXT, value: m[0] };
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function lexStructured (str) {
|
|
116
|
-
const structData = parseSRange(str);
|
|
117
|
-
if (structData) {
|
|
118
|
-
// we have a match for a valid SR
|
|
119
|
-
let i = structData.length;
|
|
120
|
-
// skip tailing whitespace
|
|
121
|
-
while (str[i] === ' ') {
|
|
122
|
-
i++;
|
|
123
|
-
}
|
|
124
|
-
// and ensure that it isn't followed by a !
|
|
125
|
-
if (str[i] !== '!') {
|
|
126
|
-
return {
|
|
127
|
-
type: REF_STRUCT,
|
|
128
|
-
value: structData.token
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const reRCNums = /([RC])(\[?)(-?\d+)/gi;
|
|
136
|
-
const reA1Nums = /(\d+|[a-zA-Z]+)/gi;
|
|
137
|
-
function lexRange (str, options) {
|
|
138
|
-
let m, t;
|
|
139
|
-
if (options.r1c1) {
|
|
140
|
-
// RC notation
|
|
141
|
-
if (options.allowTernary && (m = re_RCPARTIAL.exec(str))) {
|
|
142
|
-
t = { type: REF_TERNARY, value: m[0] };
|
|
143
|
-
}
|
|
144
|
-
else if ((m = re_RCRANGE.exec(str))) {
|
|
145
|
-
t = { type: REF_RANGE, value: m[0] };
|
|
146
|
-
}
|
|
147
|
-
else if ((m = re_RCROW.exec(str)) || (m = re_RCCOL.exec(str))) {
|
|
148
|
-
t = { type: REF_BEAM, value: m[0] };
|
|
149
|
-
}
|
|
150
|
-
if (t) {
|
|
151
|
-
reRCNums.lastIndex = 0;
|
|
152
|
-
while ((m = reRCNums.exec(t.value)) !== null) {
|
|
153
|
-
const x = (m[1] === 'R' ? MAX_ROWS : MAX_COLS) + (m[2] ? 0 : 1);
|
|
154
|
-
const val = parseInt(m[3], 10);
|
|
155
|
-
if (val > x || val < -x) {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return t;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
// A1 notation
|
|
164
|
-
if (options.allowTernary && (m = re_A1PARTIAL.exec(str))) {
|
|
165
|
-
t = { type: REF_TERNARY, value: m[0] };
|
|
166
|
-
}
|
|
167
|
-
else if ((m = re_A1COL.exec(str)) || (m = re_A1ROW.exec(str))) {
|
|
168
|
-
t = { type: REF_BEAM, value: m[0] };
|
|
169
|
-
}
|
|
170
|
-
else if ((m = re_A1RANGE.exec(str))) {
|
|
171
|
-
t = { type: REF_RANGE, value: m[0] };
|
|
172
|
-
}
|
|
173
|
-
if (t) {
|
|
174
|
-
reA1Nums.lastIndex = 0;
|
|
175
|
-
// XXX: can probably optimize this as we know letters can only be 3 at max
|
|
176
|
-
while ((m = reA1Nums.exec(t.value)) !== null) {
|
|
177
|
-
if (/^\d/.test(m[1])) { // row
|
|
178
|
-
if ((parseInt(m[1], 10) - 1) > MAX_ROWS) {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
else if (fromCol(m[1]) > MAX_COLS) {
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return t;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function lexRefOp (s, opts) {
|
|
192
|
-
// in R1C1 mode we only allow !
|
|
193
|
-
if (opts.r1c1) {
|
|
194
|
-
return (s[0] === '!')
|
|
195
|
-
? { type: OPERATOR, value: s[0] }
|
|
196
|
-
: null;
|
|
197
|
-
}
|
|
198
|
-
// in A1 mode we allow [ '!', ':', '.:', ':.', '.:.']
|
|
199
|
-
const m = /^(!|\.?:\.?)/.exec(s);
|
|
200
|
-
if (m) {
|
|
201
|
-
return { type: OPERATOR, value: m[1] };
|
|
202
|
-
}
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export const lexers = [
|
|
207
|
-
makeHandler(ERROR, re_ERROR),
|
|
208
|
-
makeHandler(OPERATOR_TRIM, re_RANGE_TRIM),
|
|
209
|
-
makeHandler(OPERATOR, re_OPERATOR),
|
|
210
|
-
makeHandler(FUNCTION, re_FUNCTION),
|
|
211
|
-
makeHandler(BOOLEAN, re_BOOLEAN),
|
|
212
|
-
makeHandler(NEWLINE, re_NEWLINE),
|
|
213
|
-
makeHandler(WHITESPACE, re_WHITESPACE),
|
|
214
|
-
makeHandler(STRING, re_STRING),
|
|
215
|
-
lexContext,
|
|
216
|
-
lexRange,
|
|
217
|
-
lexStructured,
|
|
218
|
-
makeHandler(NUMBER, re_NUMBER),
|
|
219
|
-
lexNamed
|
|
220
|
-
];
|
|
221
|
-
|
|
222
|
-
export const lexersRefs = [
|
|
223
|
-
lexRefOp,
|
|
224
|
-
lexContext,
|
|
225
|
-
lexRange,
|
|
226
|
-
lexStructured,
|
|
227
|
-
lexNamed
|
|
228
|
-
];
|