@borgar/fx 4.2.0 → 4.3.1
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/.jsdoc/publish.js +30 -8
- package/dist/fx.js +1 -1
- package/docs/API.md +74 -24
- package/docs/Prefixes.md +82 -0
- package/lib/a1.js +11 -5
- package/lib/a1.spec.js +143 -7
- package/lib/addTokenMeta.js +8 -15
- package/lib/addTokenMeta.spec.js +24 -2
- package/lib/fixRanges.js +15 -8
- package/lib/fixRanges.spec.js +19 -0
- package/lib/lexer-srefs.spec.js +9 -1
- package/lib/lexer.js +1 -0
- package/lib/lexer.spec.js +72 -0
- package/lib/lexerParts.js +33 -6
- package/lib/parseRef.js +77 -15
- package/lib/parseRef.spec.js +60 -0
- package/lib/parser.js +3 -2
- package/lib/parser.spec.js +11 -0
- package/lib/rc.js +11 -5
- package/lib/rc.spec.js +145 -12
- package/lib/sr.js +24 -10
- package/lib/sr.spec.js +93 -7
- package/lib/stringifyPrefix.js +18 -0
- package/lib/translate-toA1.spec.js +23 -3
- package/lib/translate-toRC.spec.js +31 -2
- package/lib/translate.js +16 -11
- package/package.json +2 -2
package/lib/a1.spec.js
CHANGED
|
@@ -15,12 +15,21 @@ import { MAX_COLS, MAX_ROWS } from './constants.js';
|
|
|
15
15
|
|
|
16
16
|
Test.prototype.isA1Equal = function isA1Equal (expr, expect, opts) {
|
|
17
17
|
if (expect) {
|
|
18
|
-
expect =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
expect = opts?.xlsx
|
|
19
|
+
? {
|
|
20
|
+
workbookName: '',
|
|
21
|
+
sheetName: '',
|
|
22
|
+
name: '',
|
|
23
|
+
range: null,
|
|
24
|
+
...expect
|
|
25
|
+
}
|
|
26
|
+
: {
|
|
27
|
+
context: [],
|
|
28
|
+
name: '',
|
|
29
|
+
range: null,
|
|
30
|
+
...expect
|
|
31
|
+
};
|
|
32
|
+
Object.assign(expect, expect);
|
|
24
33
|
if (expect.range && typeof expect.range === 'object') {
|
|
25
34
|
// mix in some defaults so we don't have to write things out in full
|
|
26
35
|
expect.range = {
|
|
@@ -156,6 +165,108 @@ test('parse A1 references', t => {
|
|
|
156
165
|
t.end();
|
|
157
166
|
});
|
|
158
167
|
|
|
168
|
+
test('parse A1 ranges in XLSX mode', t => {
|
|
169
|
+
const opts = { xlsx: true };
|
|
170
|
+
|
|
171
|
+
t.isA1Equal('[1]!A1', {
|
|
172
|
+
workbookName: '1',
|
|
173
|
+
sheetName: '',
|
|
174
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
175
|
+
}, opts);
|
|
176
|
+
|
|
177
|
+
t.isA1Equal('[Workbook.xlsx]!A1', {
|
|
178
|
+
workbookName: 'Workbook.xlsx',
|
|
179
|
+
sheetName: '',
|
|
180
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
181
|
+
}, opts);
|
|
182
|
+
|
|
183
|
+
t.isA1Equal('[1]Sheet1!A1', {
|
|
184
|
+
workbookName: '1',
|
|
185
|
+
sheetName: 'Sheet1',
|
|
186
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
187
|
+
}, opts);
|
|
188
|
+
|
|
189
|
+
t.isA1Equal('[Workbook.xlsx]Sheet1!A1', {
|
|
190
|
+
workbookName: 'Workbook.xlsx',
|
|
191
|
+
sheetName: 'Sheet1',
|
|
192
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
193
|
+
}, opts);
|
|
194
|
+
|
|
195
|
+
t.isA1Equal('[4]!name', {
|
|
196
|
+
workbookName: '4',
|
|
197
|
+
sheetName: '',
|
|
198
|
+
name: 'name'
|
|
199
|
+
}, opts);
|
|
200
|
+
|
|
201
|
+
t.isA1Equal('[Workbook.xlsx]!name', {
|
|
202
|
+
workbookName: 'Workbook.xlsx',
|
|
203
|
+
sheetName: '',
|
|
204
|
+
name: 'name'
|
|
205
|
+
}, opts);
|
|
206
|
+
|
|
207
|
+
t.isA1Equal('[16]Sheet1!name', {
|
|
208
|
+
workbookName: '16',
|
|
209
|
+
sheetName: 'Sheet1',
|
|
210
|
+
name: 'name'
|
|
211
|
+
}, opts);
|
|
212
|
+
|
|
213
|
+
t.isA1Equal('[Workbook.xlsx]Sheet1!name', {
|
|
214
|
+
workbookName: 'Workbook.xlsx',
|
|
215
|
+
sheetName: 'Sheet1',
|
|
216
|
+
name: 'name'
|
|
217
|
+
}, opts);
|
|
218
|
+
|
|
219
|
+
t.isA1Equal("='[1]'!A1", {
|
|
220
|
+
workbookName: '1',
|
|
221
|
+
sheetName: '',
|
|
222
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
223
|
+
}, opts);
|
|
224
|
+
|
|
225
|
+
t.isA1Equal("='[Workbook.xlsx]'!A1", {
|
|
226
|
+
workbookName: 'Workbook.xlsx',
|
|
227
|
+
sheetName: '',
|
|
228
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
229
|
+
}, opts);
|
|
230
|
+
|
|
231
|
+
t.isA1Equal("'[1]Sheet1'!A1", {
|
|
232
|
+
workbookName: '1',
|
|
233
|
+
sheetName: 'Sheet1',
|
|
234
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
235
|
+
}, opts);
|
|
236
|
+
|
|
237
|
+
t.isA1Equal("'[Workbook.xlsx]Sheet1'!A1", {
|
|
238
|
+
workbookName: 'Workbook.xlsx',
|
|
239
|
+
sheetName: 'Sheet1',
|
|
240
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
241
|
+
}, opts);
|
|
242
|
+
|
|
243
|
+
t.isA1Equal("'[4]'!name", {
|
|
244
|
+
workbookName: '4',
|
|
245
|
+
sheetName: '',
|
|
246
|
+
name: 'name'
|
|
247
|
+
}, opts);
|
|
248
|
+
|
|
249
|
+
t.isA1Equal("'[Workbook.xlsx]'!name", {
|
|
250
|
+
workbookName: 'Workbook.xlsx',
|
|
251
|
+
sheetName: '',
|
|
252
|
+
name: 'name'
|
|
253
|
+
}, opts);
|
|
254
|
+
|
|
255
|
+
t.isA1Equal("'[16]Sheet1'!name", {
|
|
256
|
+
workbookName: '16',
|
|
257
|
+
sheetName: 'Sheet1',
|
|
258
|
+
name: 'name'
|
|
259
|
+
}, opts);
|
|
260
|
+
|
|
261
|
+
t.isA1Equal("'[Workbook.xlsx]Sheet1'!name", {
|
|
262
|
+
workbookName: 'Workbook.xlsx',
|
|
263
|
+
sheetName: 'Sheet1',
|
|
264
|
+
name: 'name'
|
|
265
|
+
}, opts);
|
|
266
|
+
|
|
267
|
+
t.end();
|
|
268
|
+
});
|
|
269
|
+
|
|
159
270
|
test('A1 partial ranges', t => {
|
|
160
271
|
const opt = { allowTernary: true };
|
|
161
272
|
// partials are not allowed by defult
|
|
@@ -252,6 +363,32 @@ test('stringifyA1Ref', t => {
|
|
|
252
363
|
testRef({ context: [ 'My File.xlsx', 'Sheet1' ], name: 'foo' }, "'[My File.xlsx]Sheet1'!foo");
|
|
253
364
|
testRef({ context: [ 'MyFile.xlsx' ], name: 'foo' }, 'MyFile.xlsx!foo');
|
|
254
365
|
testRef({ context: [ 'My File.xlsx' ], name: 'foo' }, "'My File.xlsx'!foo");
|
|
366
|
+
// ignore .workbookName/.sheetName
|
|
367
|
+
testRef({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', range: rangeA1 }, 'A1');
|
|
368
|
+
testRef({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', name: 'foo' }, 'foo');
|
|
369
|
+
t.end();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('stringifyA1Ref in XLSX mode', t => {
|
|
373
|
+
const rangeA1 = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
374
|
+
const testRef = (ref, expect) => t.is(stringifyA1Ref(ref, { xlsx: true }), expect, expect);
|
|
375
|
+
testRef({ range: rangeA1 }, 'A1');
|
|
376
|
+
testRef({ sheetName: 'Sheet1', range: rangeA1 }, 'Sheet1!A1');
|
|
377
|
+
testRef({ sheetName: 'Sheet 1', range: rangeA1 }, "'Sheet 1'!A1");
|
|
378
|
+
testRef({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', range: rangeA1 }, '[MyFile.xlsx]Sheet1!A1');
|
|
379
|
+
testRef({ workbookName: 'My File.xlsx', sheetName: 'Sheet1', range: rangeA1 }, "'[My File.xlsx]Sheet1'!A1");
|
|
380
|
+
testRef({ workbookName: 'MyFile.xlsx', range: rangeA1 }, '[MyFile.xlsx]!A1');
|
|
381
|
+
testRef({ workbookName: 'My File.xlsx', range: rangeA1 }, "'[My File.xlsx]'!A1");
|
|
382
|
+
testRef({ name: 'foo' }, 'foo');
|
|
383
|
+
testRef({ sheetName: 'Sheet1', name: 'foo' }, 'Sheet1!foo');
|
|
384
|
+
testRef({ sheetName: 'Sheet 1', name: 'foo' }, "'Sheet 1'!foo");
|
|
385
|
+
testRef({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', name: 'foo' }, '[MyFile.xlsx]Sheet1!foo');
|
|
386
|
+
testRef({ workbookName: 'My File.xlsx', sheetName: 'Sheet1', name: 'foo' }, "'[My File.xlsx]Sheet1'!foo");
|
|
387
|
+
testRef({ workbookName: 'MyFile.xlsx', name: 'foo' }, '[MyFile.xlsx]!foo');
|
|
388
|
+
testRef({ workbookName: 'My File.xlsx', name: 'foo' }, "'[My File.xlsx]'!foo");
|
|
389
|
+
// ignore .context
|
|
390
|
+
testRef({ context: [ 'MyFile.xlsx', 'Sheet1' ], range: rangeA1 }, 'A1');
|
|
391
|
+
testRef({ context: [ 'MyFile.xlsx', 'Sheet1' ], name: 'foo' }, 'foo');
|
|
255
392
|
t.end();
|
|
256
393
|
});
|
|
257
394
|
|
|
@@ -268,4 +405,3 @@ test('A1 utilities', t => {
|
|
|
268
405
|
t.deepEqual(toRelative(absA1Range), relA1Range, 'toRelative');
|
|
269
406
|
t.end();
|
|
270
407
|
});
|
|
271
|
-
|
package/lib/addTokenMeta.js
CHANGED
|
@@ -63,8 +63,8 @@ function isEquivalent (refA, refB) {
|
|
|
63
63
|
}
|
|
64
64
|
// must have same context
|
|
65
65
|
if (
|
|
66
|
-
!sameStr(refA.
|
|
67
|
-
!sameStr(refA.
|
|
66
|
+
!sameStr(refA.workbookName, refB.workbookName) ||
|
|
67
|
+
!sameStr(refA.sheetName, refB.sheetName)
|
|
68
68
|
) {
|
|
69
69
|
return false;
|
|
70
70
|
}
|
|
@@ -72,18 +72,11 @@ function isEquivalent (refA, refB) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function addContext (ref, sheetName, workbookName) {
|
|
75
|
-
if (!ref.
|
|
76
|
-
ref.
|
|
75
|
+
if (!ref.sheetName) {
|
|
76
|
+
ref.sheetName = sheetName;
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (scope === sheetName || scope === workbookName) {
|
|
81
|
-
ref.context = [ workbookName, sheetName ];
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
// a single scope on a non-named range is going to be a sheet name
|
|
85
|
-
ref.context = [ workbookName, scope ];
|
|
86
|
-
}
|
|
78
|
+
if (!ref.workbookName) {
|
|
79
|
+
ref.workbookName = workbookName;
|
|
87
80
|
}
|
|
88
81
|
return ref;
|
|
89
82
|
}
|
|
@@ -189,8 +182,8 @@ export function addTokenMeta (tokens, { sheetName = '', workbookName = '' } = {}
|
|
|
189
182
|
token.type === REF_STRUCT
|
|
190
183
|
) {
|
|
191
184
|
const ref = (token.type === REF_STRUCT)
|
|
192
|
-
? parseStructRef(token.value, { allowTernary: true })
|
|
193
|
-
: parseA1Ref(token.value, { allowTernary: true });
|
|
185
|
+
? parseStructRef(token.value, { allowTernary: true, xlsx: true })
|
|
186
|
+
: parseA1Ref(token.value, { allowTernary: true, xlsx: true });
|
|
194
187
|
if (ref && (ref.range || ref.columns)) {
|
|
195
188
|
ref.source = token.value;
|
|
196
189
|
addContext(ref, sheetName, workbookName);
|
package/lib/addTokenMeta.spec.js
CHANGED
|
@@ -3,8 +3,8 @@ import { FX_PREFIX, OPERATOR, NUMBER, REF_RANGE, REF_BEAM, FUNCTION, WHITESPACE,
|
|
|
3
3
|
import { addTokenMeta } from './addTokenMeta.js';
|
|
4
4
|
import { tokenize } from './lexer.js';
|
|
5
5
|
|
|
6
|
-
Test.prototype.isMetaTokens = function isTokens (expr, expect, opts) {
|
|
7
|
-
const actual = addTokenMeta(tokenize(expr
|
|
6
|
+
Test.prototype.isMetaTokens = function isTokens (expr, expect, context, opts) {
|
|
7
|
+
const actual = addTokenMeta(tokenize(expr, opts), context);
|
|
8
8
|
if (actual.length === expect.length) {
|
|
9
9
|
actual.forEach((d, i) => {
|
|
10
10
|
const keys = Object.keys(d).concat(Object.keys(expect[i]));
|
|
@@ -115,5 +115,27 @@ test('add extra meta to operators', t => {
|
|
|
115
115
|
{ index: 5, depth: 0, type: REF_STRUCT, value: 'table[[#All]]', groupId: 'fxg1' }
|
|
116
116
|
], { sheetName: 'Sheet1', workbookName: 'foo' });
|
|
117
117
|
|
|
118
|
+
t.isMetaTokens('=[foo]!A1+[foo]Sheet1!A1+Sheet1!A1+A1', [
|
|
119
|
+
{ index: 0, depth: 0, type: FX_PREFIX, value: '=' },
|
|
120
|
+
{ index: 1, depth: 0, type: REF_RANGE, value: '[foo]!A1', groupId: 'fxg1' },
|
|
121
|
+
{ index: 2, depth: 0, type: OPERATOR, value: '+' },
|
|
122
|
+
{ index: 3, depth: 0, type: REF_RANGE, value: '[foo]Sheet1!A1', groupId: 'fxg1' },
|
|
123
|
+
{ index: 4, depth: 0, type: OPERATOR, value: '+' },
|
|
124
|
+
{ index: 5, depth: 0, type: REF_RANGE, value: 'Sheet1!A1', groupId: 'fxg1' },
|
|
125
|
+
{ index: 6, depth: 0, type: OPERATOR, value: '+' },
|
|
126
|
+
{ index: 7, depth: 0, type: REF_RANGE, value: 'A1', groupId: 'fxg1' }
|
|
127
|
+
], { sheetName: 'Sheet1', workbookName: 'foo' }, { xlsx: true });
|
|
128
|
+
|
|
129
|
+
t.isMetaTokens('=[foo]!table[#data]+[foo]Sheet1!table[#data]+Sheet1!table[#data]+table[#data]', [
|
|
130
|
+
{ index: 0, depth: 0, type: FX_PREFIX, value: '=' },
|
|
131
|
+
{ index: 1, depth: 0, type: REF_STRUCT, value: '[foo]!table[#data]', groupId: 'fxg1' },
|
|
132
|
+
{ index: 2, depth: 0, type: OPERATOR, value: '+' },
|
|
133
|
+
{ index: 3, depth: 0, type: REF_STRUCT, value: '[foo]Sheet1!table[#data]', groupId: 'fxg1' },
|
|
134
|
+
{ index: 4, depth: 0, type: OPERATOR, value: '+' },
|
|
135
|
+
{ index: 5, depth: 0, type: REF_STRUCT, value: 'Sheet1!table[#data]', groupId: 'fxg1' },
|
|
136
|
+
{ index: 6, depth: 0, type: OPERATOR, value: '+' },
|
|
137
|
+
{ index: 7, depth: 0, type: REF_STRUCT, value: 'table[#data]', groupId: 'fxg1' }
|
|
138
|
+
], { sheetName: 'Sheet1', workbookName: 'foo' }, { xlsx: true });
|
|
139
|
+
|
|
118
140
|
t.end();
|
|
119
141
|
});
|
package/lib/fixRanges.js
CHANGED
|
@@ -4,14 +4,17 @@ import { parseStructRef, stringifyStructRef } from './sr.js';
|
|
|
4
4
|
import { tokenize } from './lexer.js';
|
|
5
5
|
import { REF_STRUCT } from './constants.js';
|
|
6
6
|
|
|
7
|
-
// There is no R1C1
|
|
7
|
+
// There is no R1C1 counterpart to this. This is because without an anchor cell
|
|
8
8
|
// it is impossible to determine if a relative+absolute range (R[1]C[1]:R5C5)
|
|
9
9
|
// needs to be flipped or not. The solution is to convert to A1 first:
|
|
10
10
|
// translateToRC(fixRanges(translateToA1(...)))
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Normalizes A1 style ranges in a formula or list of
|
|
14
|
-
*
|
|
13
|
+
* Normalizes A1 style ranges and structured references in a formula or list of
|
|
14
|
+
* tokens.
|
|
15
|
+
*
|
|
16
|
+
* It ensures that that the top and left coordinates of an A1 range are on the
|
|
17
|
+
* left-hand side of a colon operator:
|
|
15
18
|
*
|
|
16
19
|
* `B2:A1` → `A1:B2`
|
|
17
20
|
* `1:A1` → `A1:1`
|
|
@@ -31,13 +34,16 @@ import { REF_STRUCT } from './constants.js';
|
|
|
31
34
|
* `B2:B` → `B2:1048576`
|
|
32
35
|
* `B2:2` → `B2:XFD2`
|
|
33
36
|
*
|
|
37
|
+
* Structured ranges are normalized cleaned up to have consistent order and
|
|
38
|
+
* capitalization of sections as well as removing redundant ones.
|
|
39
|
+
*
|
|
34
40
|
* Returns the same formula with the ranges updated. If an array of tokens was
|
|
35
41
|
* supplied, then a new array is returned.
|
|
36
42
|
*
|
|
37
43
|
* @param {(string | Array<Object>)} formula A string (an Excel formula) or a token list that should be adjusted.
|
|
38
44
|
* @param {Object} [options={}] Options
|
|
39
45
|
* @param {boolean} [options.addBounds=false] Fill in any undefined bounds of range objects. Top to 0, bottom to 1048575, left to 0, and right to 16383.
|
|
40
|
-
* @param {boolean} [options.
|
|
46
|
+
* @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)
|
|
41
47
|
* @return {(string | Array<Object>)} A formula string or token list (depending on which was input)
|
|
42
48
|
*/
|
|
43
49
|
export function fixRanges (tokens, options = { addBounds: false }) {
|
|
@@ -49,7 +55,7 @@ export function fixRanges (tokens, options = { addBounds: false }) {
|
|
|
49
55
|
if (!Array.isArray(tokens)) {
|
|
50
56
|
throw new Error('fixRanges expects an array of tokens');
|
|
51
57
|
}
|
|
52
|
-
const { addBounds, r1c1 } = options;
|
|
58
|
+
const { addBounds, r1c1, xlsx } = options;
|
|
53
59
|
if (r1c1) {
|
|
54
60
|
throw new Error('fixRanges does not have an R1C1 mode');
|
|
55
61
|
}
|
|
@@ -61,18 +67,19 @@ export function fixRanges (tokens, options = { addBounds: false }) {
|
|
|
61
67
|
}
|
|
62
68
|
let offsetDelta = 0;
|
|
63
69
|
if (token.type === REF_STRUCT) {
|
|
64
|
-
const
|
|
70
|
+
const sref = parseStructRef(token.value, { xlsx });
|
|
71
|
+
const newValue = stringifyStructRef(sref, { xlsx });
|
|
65
72
|
offsetDelta = newValue.length - token.value.length;
|
|
66
73
|
token.value = newValue;
|
|
67
74
|
}
|
|
68
75
|
else if (isRange(token)) {
|
|
69
|
-
const ref = parseA1Ref(token.value, { allowTernary: true });
|
|
76
|
+
const ref = parseA1Ref(token.value, { xlsx, allowTernary: true });
|
|
70
77
|
const range = ref.range;
|
|
71
78
|
// fill missing dimensions?
|
|
72
79
|
if (addBounds) {
|
|
73
80
|
addA1RangeBounds(range);
|
|
74
81
|
}
|
|
75
|
-
const newValue = stringifyA1Ref(ref);
|
|
82
|
+
const newValue = stringifyA1Ref(ref, { xlsx });
|
|
76
83
|
offsetDelta = newValue.length - token.value.length;
|
|
77
84
|
token.value = newValue;
|
|
78
85
|
}
|
package/lib/fixRanges.spec.js
CHANGED
|
@@ -138,3 +138,22 @@ test('fixRanges structured references', t => {
|
|
|
138
138
|
t.isFixed('[ @foo bar ]', '[@[foo bar]]');
|
|
139
139
|
t.end();
|
|
140
140
|
});
|
|
141
|
+
|
|
142
|
+
test('fixRanges works with xlsx mode', t => {
|
|
143
|
+
// should not mess with invalid ranges in normal mode
|
|
144
|
+
t.isFixed("='[Workbook]'!Table[Column]", "='[Workbook]'!Table[Column]");
|
|
145
|
+
t.isFixed('=[Workbook]!Table[Column]', '=[Workbook]!Table[Column]');
|
|
146
|
+
t.isFixed("='[Foo]'!A1", "='[Foo]'!A1");
|
|
147
|
+
t.isFixed('=[Foo]!A1', '=[Foo]!A1');
|
|
148
|
+
// should fix things in xlsx mode
|
|
149
|
+
const opts = { xlsx: true };
|
|
150
|
+
t.isFixed("='[Workbook]'!Table[Column]", '=[Workbook]!Table[Column]', opts);
|
|
151
|
+
t.isFixed('=[Workbook]!Table[Column]', '=[Workbook]!Table[Column]', opts);
|
|
152
|
+
t.isFixed('=[Lorem Ipsum]!Table[Column]', "='[Lorem Ipsum]'!Table[Column]", opts);
|
|
153
|
+
t.isFixed("='[Foo]'!A1", '=[Foo]!A1', opts);
|
|
154
|
+
t.isFixed('=[Foo]Bar!A1', '=[Foo]Bar!A1', opts);
|
|
155
|
+
t.isFixed('=[Foo Bar]Baz!A1', "='[Foo Bar]Baz'!A1", opts);
|
|
156
|
+
t.isFixed('=[Foo]!A1', '=[Foo]!A1', opts);
|
|
157
|
+
t.isFixed('=[Lorem Ipsum]!A1', "='[Lorem Ipsum]'!A1", opts);
|
|
158
|
+
t.end();
|
|
159
|
+
});
|
package/lib/lexer-srefs.spec.js
CHANGED
|
@@ -2,7 +2,7 @@ import { test, Test } from 'tape';
|
|
|
2
2
|
import {
|
|
3
3
|
FX_PREFIX, UNKNOWN,
|
|
4
4
|
OPERATOR, WHITESPACE,
|
|
5
|
-
REF_NAMED, CONTEXT_QUOTE, REF_STRUCT
|
|
5
|
+
REF_NAMED, CONTEXT_QUOTE, REF_STRUCT, REF_RANGE
|
|
6
6
|
} from './constants.js';
|
|
7
7
|
import { tokenize } from './lexer.js';
|
|
8
8
|
|
|
@@ -166,6 +166,14 @@ test('tokenize structured references (merges on)', t => {
|
|
|
166
166
|
t.isTokens('[[#Totals],col name:Foo]', [
|
|
167
167
|
{ type: REF_STRUCT, value: '[[#Totals],col name:Foo]' }
|
|
168
168
|
]);
|
|
169
|
+
t.isTokens('Table1[[#This Row],[a]]*[1]Sheet1!$A$1', [
|
|
170
|
+
{ type: REF_STRUCT, value: 'Table1[[#This Row],[a]]' },
|
|
171
|
+
{ type: OPERATOR, value: '*' },
|
|
172
|
+
{ type: REF_RANGE, value: '[1]Sheet1!$A$1' }
|
|
173
|
+
], { xlsx: true });
|
|
174
|
+
t.isTokens("Sheet1!Table1[foo '[bar']]", [
|
|
175
|
+
{ type: REF_STRUCT, value: "Sheet1!Table1[foo '[bar']]" }
|
|
176
|
+
]);
|
|
169
177
|
t.end();
|
|
170
178
|
});
|
|
171
179
|
|
package/lib/lexer.js
CHANGED
|
@@ -202,6 +202,7 @@ export function getTokens (fx, tokenHandlers, options = {}) {
|
|
|
202
202
|
* @param {boolean} [options.r1c1=false] Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
|
|
203
203
|
* @param {boolean} [options.withLocation=true] Nodes will include source position offsets to the tokens: `{ loc: [ start, end ] }`
|
|
204
204
|
* @param {boolean} [options.mergeRefs=true] Should ranges be returned as whole references (`Sheet1!A1:B2`) or as separate tokens for each part: (`Sheet1`,`!`,`A1`,`:`,`B2`). This is the same as calling [`mergeRefTokens`](#mergeRefTokens)
|
|
205
|
+
* @param {boolean} [options.xlsx=false] Enables a `[1]Sheet1!A1` or `[1]!name` syntax form for external workbooks found only in XLSX files.
|
|
205
206
|
* @return {Array<Object>} An AST of nodes
|
|
206
207
|
*/
|
|
207
208
|
export function tokenize (formula, options = {}) {
|
package/lib/lexer.spec.js
CHANGED
|
@@ -1697,3 +1697,75 @@ test('tokenize partial ranges', t => {
|
|
|
1697
1697
|
|
|
1698
1698
|
t.end();
|
|
1699
1699
|
});
|
|
1700
|
+
|
|
1701
|
+
test('tokenize external refs syntax from XLSX files', t => {
|
|
1702
|
+
const opts = { xlsx: true };
|
|
1703
|
+
// Excel XLS files only use positive integers as workbooks...
|
|
1704
|
+
t.isTokens('=[1]!A1', [
|
|
1705
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1706
|
+
{ type: REF_RANGE, value: '[1]!A1' }
|
|
1707
|
+
], opts);
|
|
1708
|
+
t.isTokens('=[1]Sheet1!A1', [
|
|
1709
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1710
|
+
{ type: REF_RANGE, value: '[1]Sheet1!A1' }
|
|
1711
|
+
], opts);
|
|
1712
|
+
t.isTokens('=[4]!name', [
|
|
1713
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1714
|
+
{ type: REF_NAMED, value: '[4]!name' }
|
|
1715
|
+
], opts);
|
|
1716
|
+
t.isTokens('=[16]Sheet1!name', [
|
|
1717
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1718
|
+
{ type: REF_NAMED, value: '[16]Sheet1!name' }
|
|
1719
|
+
], opts);
|
|
1720
|
+
t.isTokens("='[1]'!A1", [
|
|
1721
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1722
|
+
{ type: REF_RANGE, value: "'[1]'!A1" }
|
|
1723
|
+
], opts);
|
|
1724
|
+
t.isTokens("='[1]Sheet1'!A1", [
|
|
1725
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1726
|
+
{ type: REF_RANGE, value: "'[1]Sheet1'!A1" }
|
|
1727
|
+
], opts);
|
|
1728
|
+
t.isTokens("='[4]'!name", [
|
|
1729
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1730
|
+
{ type: REF_NAMED, value: "'[4]'!name" }
|
|
1731
|
+
], opts);
|
|
1732
|
+
t.isTokens("='[16]Sheet1'!name", [
|
|
1733
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1734
|
+
{ type: REF_NAMED, value: "'[16]Sheet1'!name" }
|
|
1735
|
+
], opts);
|
|
1736
|
+
// ...fx additionally permits workbook names
|
|
1737
|
+
t.isTokens('=[Workbook.xlsx]!A1', [
|
|
1738
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1739
|
+
{ type: REF_RANGE, value: '[Workbook.xlsx]!A1' }
|
|
1740
|
+
], opts);
|
|
1741
|
+
t.isTokens('=[Workbook.xlsx]Sheet1!A1', [
|
|
1742
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1743
|
+
{ type: REF_RANGE, value: '[Workbook.xlsx]Sheet1!A1' }
|
|
1744
|
+
], opts);
|
|
1745
|
+
t.isTokens('=[Workbook.xlsx]!name', [
|
|
1746
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1747
|
+
{ type: REF_NAMED, value: '[Workbook.xlsx]!name' }
|
|
1748
|
+
], opts);
|
|
1749
|
+
t.isTokens('=[Workbook.xlsx]Sheet1!name', [
|
|
1750
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1751
|
+
{ type: REF_NAMED, value: '[Workbook.xlsx]Sheet1!name' }
|
|
1752
|
+
], opts);
|
|
1753
|
+
t.isTokens("='[Workbook.xlsx]'!A1", [
|
|
1754
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1755
|
+
{ type: REF_RANGE, value: "'[Workbook.xlsx]'!A1" }
|
|
1756
|
+
], opts);
|
|
1757
|
+
t.isTokens("='[Workbook.xlsx]Sheet1'!A1", [
|
|
1758
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1759
|
+
{ type: REF_RANGE, value: "'[Workbook.xlsx]Sheet1'!A1" }
|
|
1760
|
+
], opts);
|
|
1761
|
+
t.isTokens("='[Workbook.xlsx]'!name", [
|
|
1762
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1763
|
+
{ type: REF_NAMED, value: "'[Workbook.xlsx]'!name" }
|
|
1764
|
+
], opts);
|
|
1765
|
+
t.isTokens("='[Workbook.xlsx]Sheet1'!name", [
|
|
1766
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1767
|
+
{ type: REF_NAMED, value: "'[Workbook.xlsx]Sheet1'!name" }
|
|
1768
|
+
], opts);
|
|
1769
|
+
|
|
1770
|
+
t.end();
|
|
1771
|
+
});
|
package/lib/lexerParts.js
CHANGED
|
@@ -28,8 +28,7 @@ const re_NEWLINE = /^\n+/;
|
|
|
28
28
|
const re_WHITESPACE = /^[ \f\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/;
|
|
29
29
|
const re_STRING = /^"(?:""|[^"])*("|$)/;
|
|
30
30
|
const re_NUMBER = /^(?:\d+(\.\d+)?(?:[eE][+-]?\d+)?|\d+)/;
|
|
31
|
-
|
|
32
|
-
const re_CONTEXT = /^(\[(?:[^\]])+\])?([0-9A-Za-z._¡¤§¨ª\u00ad¯-\uffff]+)(?=!)/;
|
|
31
|
+
const re_CONTEXT = /^(?!!)(\[(?:[^\]])+\])?([0-9A-Za-z._¡¤§¨ª\u00ad¯-\uffff]+)?(?=!)/;
|
|
33
32
|
const re_CONTEXT_QUOTE = /^'(?:''|[^'])*('|$)(?=!)/;
|
|
34
33
|
|
|
35
34
|
const rngPart = '\\$?[A-Z]{1,3}\\$?[1-9][0-9]{0,6}';
|
|
@@ -63,6 +62,36 @@ function makeHandler (type, re) {
|
|
|
63
62
|
};
|
|
64
63
|
}
|
|
65
64
|
|
|
65
|
+
const re_QUOTED_VALUE = /^'(?:[^[\]]+?)?(?:\[(.+?)\])?(?:[^[\]]+?)'$/;
|
|
66
|
+
const re_QUOTED_VALUE_XLSX = /^'\[(.+?)\]'$/;
|
|
67
|
+
function lexContext (str, options) {
|
|
68
|
+
const mq = re_CONTEXT_QUOTE.exec(str);
|
|
69
|
+
if (mq) {
|
|
70
|
+
const value = mq[0];
|
|
71
|
+
const isValid = options.xlsx
|
|
72
|
+
? re_QUOTED_VALUE_XLSX.test(value) || re_QUOTED_VALUE.test(value)
|
|
73
|
+
: re_QUOTED_VALUE.test(value);
|
|
74
|
+
if (isValid) {
|
|
75
|
+
return { type: CONTEXT_QUOTE, value: value };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// xlsx xml uses a variant of the syntax that has external references in
|
|
79
|
+
// bracets. Any of: [1]Sheet1!A1, '[1]Sheet one'!A1, [1]!named
|
|
80
|
+
// We're only concerned with the non quoted version here as the quoted version
|
|
81
|
+
// doesn't currently examine what is in the quotes.
|
|
82
|
+
const m = re_CONTEXT.exec(str);
|
|
83
|
+
if (m) {
|
|
84
|
+
const [ , a, b ] = m;
|
|
85
|
+
const valid = (
|
|
86
|
+
((a && b) || b) || // "[a]b!" or "b!" forms
|
|
87
|
+
(a && !b && options.xlsx) // "[a]" form (allowed in xlsx mode)
|
|
88
|
+
);
|
|
89
|
+
if (valid) {
|
|
90
|
+
return { type: CONTEXT, value: m[0] };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
66
95
|
function lexStructured (str) {
|
|
67
96
|
const structData = parseSRange(str);
|
|
68
97
|
if (structData) {
|
|
@@ -159,8 +188,7 @@ export const lexers = [
|
|
|
159
188
|
makeHandler(NEWLINE, re_NEWLINE),
|
|
160
189
|
makeHandler(WHITESPACE, re_WHITESPACE),
|
|
161
190
|
makeHandler(STRING, re_STRING),
|
|
162
|
-
|
|
163
|
-
makeHandler(CONTEXT, re_CONTEXT),
|
|
191
|
+
lexContext,
|
|
164
192
|
lexRange,
|
|
165
193
|
lexStructured,
|
|
166
194
|
makeHandler(NUMBER, re_NUMBER),
|
|
@@ -169,8 +197,7 @@ export const lexers = [
|
|
|
169
197
|
|
|
170
198
|
export const lexersRefs = [
|
|
171
199
|
lexRefOp,
|
|
172
|
-
|
|
173
|
-
makeHandler(CONTEXT, re_CONTEXT),
|
|
200
|
+
lexContext,
|
|
174
201
|
lexRange,
|
|
175
202
|
lexStructured,
|
|
176
203
|
makeHandler(REF_NAMED, re_NAMED)
|
package/lib/parseRef.js
CHANGED
|
@@ -12,13 +12,61 @@ import {
|
|
|
12
12
|
import { lexersRefs } from './lexerParts.js';
|
|
13
13
|
import { getTokens } from './lexer.js';
|
|
14
14
|
|
|
15
|
+
// Liberally split a context string up into parts.
|
|
16
|
+
// Permits any combination of braced and unbraced items.
|
|
17
|
+
export function splitPrefix (str, stringsOnly = false) {
|
|
18
|
+
let inBrace = false;
|
|
19
|
+
let currStr = '';
|
|
20
|
+
const parts = [];
|
|
21
|
+
const flush = () => {
|
|
22
|
+
if (currStr) {
|
|
23
|
+
parts.push(
|
|
24
|
+
stringsOnly
|
|
25
|
+
? currStr
|
|
26
|
+
: { value: currStr, braced: inBrace }
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
currStr = '';
|
|
30
|
+
};
|
|
31
|
+
for (let i = 0; i < str.length; i++) {
|
|
32
|
+
const char = str[i];
|
|
33
|
+
if (char === '[') {
|
|
34
|
+
flush();
|
|
35
|
+
inBrace = true;
|
|
36
|
+
}
|
|
37
|
+
else if (char === ']') {
|
|
38
|
+
flush();
|
|
39
|
+
inBrace = false;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
currStr += char;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
flush();
|
|
46
|
+
return parts;
|
|
47
|
+
}
|
|
48
|
+
|
|
15
49
|
function splitContext (contextString) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
50
|
+
return { context: splitPrefix(contextString, true) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function splitContextXlsx (contextString) {
|
|
54
|
+
const context = {};
|
|
55
|
+
const ctx = splitPrefix(contextString);
|
|
56
|
+
if (ctx.length > 1) {
|
|
57
|
+
context.workbookName = ctx[ctx.length - 2].value;
|
|
58
|
+
context.sheetName = ctx[ctx.length - 1].value;
|
|
59
|
+
}
|
|
60
|
+
else if (ctx.length === 1) {
|
|
61
|
+
const item = ctx[0];
|
|
62
|
+
if (item.braced) {
|
|
63
|
+
context.workbookName = item.value;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
context.sheetName = item.value;
|
|
67
|
+
}
|
|
21
68
|
}
|
|
69
|
+
return context;
|
|
22
70
|
}
|
|
23
71
|
|
|
24
72
|
const unquote = d => d.slice(1, -1).replace(/''/g, "'");
|
|
@@ -30,9 +78,14 @@ const pRange2 = t => t && t.type === REF_RANGE && { r1: t.value };
|
|
|
30
78
|
const pBang = t => t && t.type === OPERATOR && t.value === '!' && {};
|
|
31
79
|
const pBeam = t => t && t.type === REF_BEAM && { r0: t.value };
|
|
32
80
|
const pStrucured = t => t && t.type === REF_STRUCT && { struct: t.value };
|
|
33
|
-
const pContext = t => {
|
|
34
|
-
|
|
35
|
-
if (t && t.type ===
|
|
81
|
+
const pContext = (t, opts) => {
|
|
82
|
+
const splitter = opts.xlsx ? splitContextXlsx : splitContext;
|
|
83
|
+
if (t && t.type === CONTEXT) {
|
|
84
|
+
return splitter(t.value);
|
|
85
|
+
}
|
|
86
|
+
if (t && t.type === CONTEXT_QUOTE) {
|
|
87
|
+
return splitter(unquote(t.value));
|
|
88
|
+
}
|
|
36
89
|
};
|
|
37
90
|
const pNamed = t => t && t.type === REF_NAMED && { name: t.value };
|
|
38
91
|
|
|
@@ -62,15 +115,24 @@ export function parseRef (ref, opts) {
|
|
|
62
115
|
allowTernary: false,
|
|
63
116
|
allowNamed: true,
|
|
64
117
|
r1c1: false,
|
|
118
|
+
xlsx: false,
|
|
65
119
|
...opts
|
|
66
120
|
};
|
|
67
121
|
const tokens = getTokens(ref, lexersRefs, options);
|
|
68
|
-
const refData =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
122
|
+
const refData = options.xlsx
|
|
123
|
+
? {
|
|
124
|
+
workbookName: '',
|
|
125
|
+
sheetName: '',
|
|
126
|
+
r0: '',
|
|
127
|
+
r1: '',
|
|
128
|
+
name: ''
|
|
129
|
+
}
|
|
130
|
+
: {
|
|
131
|
+
context: [],
|
|
132
|
+
r0: '',
|
|
133
|
+
r1: '',
|
|
134
|
+
name: ''
|
|
135
|
+
};
|
|
74
136
|
// discard the "="-prefix if it is there
|
|
75
137
|
if (tokens.length && tokens[0].type === FX_PREFIX) {
|
|
76
138
|
tokens.shift();
|
|
@@ -80,7 +142,7 @@ export function parseRef (ref, opts) {
|
|
|
80
142
|
const data = { ...refData };
|
|
81
143
|
if (runs[i].length === tokens.length) {
|
|
82
144
|
const valid = runs[i].every((parse, j) => {
|
|
83
|
-
const d = parse(tokens[j]);
|
|
145
|
+
const d = parse(tokens[j], options);
|
|
84
146
|
Object.assign(data, d);
|
|
85
147
|
return d;
|
|
86
148
|
});
|