@borgar/fx 4.1.0 → 4.3.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/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
- context: [],
20
- name: '',
21
- range: null,
22
- ...expect
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
-
@@ -1,5 +1,6 @@
1
- import { REF_RANGE, REF_BEAM, REF_TERNARY, UNKNOWN } from './constants.js';
1
+ import { REF_RANGE, REF_BEAM, REF_TERNARY, UNKNOWN, REF_STRUCT } from './constants.js';
2
2
  import { parseA1Ref } from './a1.js';
3
+ import { parseStructRef } from './sr.js';
3
4
 
4
5
  function getIDer () {
5
6
  let i = 1;
@@ -13,6 +14,18 @@ function sameValue (a, b) {
13
14
  return a === b;
14
15
  }
15
16
 
17
+ function sameArray (a, b) {
18
+ if ((Array.isArray(a) !== Array.isArray(b)) || a.length !== b.length) {
19
+ return false;
20
+ }
21
+ for (let i = 0; i < a.length; i++) {
22
+ if (!sameValue(a[i], b[i])) {
23
+ return false;
24
+ }
25
+ }
26
+ return true;
27
+ }
28
+
16
29
  function sameStr (a, b) {
17
30
  if (!a && !b) {
18
31
  return true;
@@ -25,6 +38,18 @@ function isEquivalent (refA, refB) {
25
38
  if ((refA.name || refB.name) && refA.name !== refB.name) {
26
39
  return false;
27
40
  }
41
+ // if structured
42
+ if ((refA.columns || refB.columns)) {
43
+ if (refA.table !== refB.table) {
44
+ return false;
45
+ }
46
+ if (!sameArray(refA.columns, refB.columns)) {
47
+ return false;
48
+ }
49
+ if (!sameArray(refA.sections, refB.sections)) {
50
+ return false;
51
+ }
52
+ }
28
53
  // if ranged, range must have the same dimensions (we don't care about $)
29
54
  if (refA.range || refB.range) {
30
55
  if (
@@ -46,6 +71,23 @@ function isEquivalent (refA, refB) {
46
71
  return true;
47
72
  }
48
73
 
74
+ function addContext (ref, sheetName, workbookName) {
75
+ if (!ref.context.length) {
76
+ ref.context = [ workbookName, sheetName ];
77
+ }
78
+ else if (ref.context.length === 1) {
79
+ const scope = ref.context[0];
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
+ }
87
+ }
88
+ return ref;
89
+ }
90
+
49
91
  /**
50
92
  * Runs through a list of tokens and adds extra attributes such as matching
51
93
  * parens and ranges.
@@ -140,23 +182,18 @@ export function addTokenMeta (tokens, { sheetName = '', workbookName = '' } = {}
140
182
  }
141
183
  arrayStart = null;
142
184
  }
143
- else if (token.type === REF_RANGE || token.type === REF_BEAM || token.type === REF_TERNARY) {
144
- const ref = parseA1Ref(token.value, { allowTernary: true });
145
- if (ref && ref.range) {
185
+ else if (
186
+ token.type === REF_RANGE ||
187
+ token.type === REF_BEAM ||
188
+ token.type === REF_TERNARY ||
189
+ token.type === REF_STRUCT
190
+ ) {
191
+ const ref = (token.type === REF_STRUCT)
192
+ ? parseStructRef(token.value, { allowTernary: true })
193
+ : parseA1Ref(token.value, { allowTernary: true });
194
+ if (ref && (ref.range || ref.columns)) {
146
195
  ref.source = token.value;
147
- if (!ref.context.length) {
148
- ref.context = [ workbookName, sheetName ];
149
- }
150
- else if (ref.context.length === 1) {
151
- const scope = ref.context[0];
152
- if (scope === sheetName || scope === workbookName) {
153
- ref.context = [ workbookName, sheetName ];
154
- }
155
- else {
156
- // a single scope on a non-named range is going to be a sheet name
157
- ref.context = [ workbookName, scope ];
158
- }
159
- }
196
+ addContext(ref, sheetName, workbookName);
160
197
  const known = knownRefs.find(d => isEquivalent(d, ref));
161
198
  if (known) {
162
199
  token.groupId = known.groupId;
@@ -1,5 +1,5 @@
1
1
  import { test, Test } from 'tape';
2
- import { FX_PREFIX, OPERATOR, NUMBER, REF_RANGE, REF_BEAM, FUNCTION, WHITESPACE } from './constants.js';
2
+ import { FX_PREFIX, OPERATOR, NUMBER, REF_RANGE, REF_BEAM, FUNCTION, WHITESPACE, REF_STRUCT } from './constants.js';
3
3
  import { addTokenMeta } from './addTokenMeta.js';
4
4
  import { tokenize } from './lexer.js';
5
5
 
@@ -106,5 +106,14 @@ test('add extra meta to operators', t => {
106
106
  { index: 17, depth: 1, type: OPERATOR, value: ')', groupId: 'fxg3' }
107
107
  ], { sheetName: 'Sheet1', workbookName: 'foo' });
108
108
 
109
+ t.isMetaTokens('=table[#all]+table[foobar]+table[[#All]]', [
110
+ { index: 0, depth: 0, type: FX_PREFIX, value: '=' },
111
+ { index: 1, depth: 0, type: REF_STRUCT, value: 'table[#all]', groupId: 'fxg1' },
112
+ { index: 2, depth: 0, type: OPERATOR, value: '+' },
113
+ { index: 3, depth: 0, type: REF_STRUCT, value: 'table[foobar]', groupId: 'fxg2' },
114
+ { index: 4, depth: 0, type: OPERATOR, value: '+' },
115
+ { index: 5, depth: 0, type: REF_STRUCT, value: 'table[[#All]]', groupId: 'fxg1' }
116
+ ], { sheetName: 'Sheet1', workbookName: 'foo' });
117
+
109
118
  t.end();
110
119
  });
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 counerpart to this. This is because without an anchor cell
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 tokens so that the top and
14
- * left coordinates of the range are on the left-hand side of a colon operator:
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.r1c1=false] Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
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 newValue = stringifyStructRef(parseStructRef(token.value));
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
  }
@@ -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
+ });
@@ -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
- makeHandler(CONTEXT_QUOTE, re_CONTEXT_QUOTE),
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
- makeHandler(CONTEXT_QUOTE, re_CONTEXT_QUOTE),
173
- makeHandler(CONTEXT, re_CONTEXT),
200
+ lexContext,
174
201
  lexRange,
175
202
  lexStructured,
176
203
  makeHandler(REF_NAMED, re_NAMED)