@borgar/fx 4.2.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
-
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)
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
- const m = /(?:\[(.+?)\])?([^[\]]+?)$/.exec(contextString);
17
- if (m) {
18
- const [ , a, b ] = m;
19
- const context = [ a, b ].filter(Boolean);
20
- return { context };
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
- if (t && t.type === CONTEXT) { return splitContext(t.value); }
35
- if (t && t.type === CONTEXT_QUOTE) { return splitContext(unquote(t.value)); }
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
- context: [],
70
- r0: '',
71
- r1: '',
72
- name: ''
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
  });
@@ -0,0 +1,60 @@
1
+ import { test } from 'tape';
2
+ import { splitPrefix } from './parseRef.js';
3
+
4
+ test('splitPrefix', t => {
5
+ const testStr = (str, opt, expected) => {
6
+ t.deepEqual(splitPrefix(str, opt), expected, str);
7
+ };
8
+
9
+ testStr('[foo][bar][baz]', true, [ 'foo', 'bar', 'baz' ]);
10
+ testStr('foo[bar][baz]', true, [ 'foo', 'bar', 'baz' ]);
11
+ testStr('[foo]bar[baz]', true, [ 'foo', 'bar', 'baz' ]);
12
+ testStr('[foo][bar]baz', true, [ 'foo', 'bar', 'baz' ]);
13
+ testStr('foo[bar]baz', true, [ 'foo', 'bar', 'baz' ]);
14
+ testStr('[foo]bar[baz]', true, [ 'foo', 'bar', 'baz' ]);
15
+ testStr('[foo]bar', true, [ 'foo', 'bar' ]);
16
+ testStr('foo[bar]', true, [ 'foo', 'bar' ]);
17
+ testStr('[foo][bar]', true, [ 'foo', 'bar' ]);
18
+ testStr('[foo]', true, [ 'foo' ]);
19
+ testStr('foo', true, [ 'foo' ]);
20
+
21
+ testStr('[foo][bar][baz]', false, [
22
+ { value: 'foo', braced: true },
23
+ { value: 'bar', braced: true },
24
+ { value: 'baz', braced: true } ]);
25
+ testStr('foo[bar][baz]', false, [
26
+ { value: 'foo', braced: false },
27
+ { value: 'bar', braced: true },
28
+ { value: 'baz', braced: true } ]);
29
+ testStr('[foo]bar[baz]', false, [
30
+ { value: 'foo', braced: true },
31
+ { value: 'bar', braced: false },
32
+ { value: 'baz', braced: true } ]);
33
+ testStr('[foo][bar]baz', false, [
34
+ { value: 'foo', braced: true },
35
+ { value: 'bar', braced: true },
36
+ { value: 'baz', braced: false } ]);
37
+ testStr('foo[bar]baz', false, [
38
+ { value: 'foo', braced: false },
39
+ { value: 'bar', braced: true },
40
+ { value: 'baz', braced: false } ]);
41
+ testStr('[foo]bar[baz]', false, [
42
+ { value: 'foo', braced: true },
43
+ { value: 'bar', braced: false },
44
+ { value: 'baz', braced: true } ]);
45
+ testStr('[foo]bar', false, [
46
+ { value: 'foo', braced: true },
47
+ { value: 'bar', braced: false } ]);
48
+ testStr('foo[bar]', false, [
49
+ { value: 'foo', braced: false },
50
+ { value: 'bar', braced: true } ]);
51
+ testStr('[foo][bar]', false, [
52
+ { value: 'foo', braced: true },
53
+ { value: 'bar', braced: true } ]);
54
+ testStr('[foo]', false, [
55
+ { value: 'foo', braced: true } ]);
56
+ testStr('foo', false, [
57
+ { value: 'foo', braced: false } ]);
58
+
59
+ t.end();
60
+ });
package/lib/parser.js CHANGED
@@ -475,7 +475,7 @@ prefix('{', function () {
475
475
  * because it does not recognize reference context tokens.
476
476
  *
477
477
  * The AST Abstract Syntax Tree's format is documented in
478
- * [AST_format.md][./AST_format.md]
478
+ * [AST_format.md](./AST_format.md)
479
479
  *
480
480
  * @see nodeTypes
481
481
  * @param {(string | Array<Object>)} formula An Excel formula string (an Excel expression) or an array of tokens.
@@ -486,7 +486,8 @@ prefix('{', function () {
486
486
  * @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.
487
487
  * @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.
488
488
  * @param {boolean} [options.r1c1=false] Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
489
- * @param {boolean} [options.withLocation=true] Nodes will include source position offsets to the tokens: `{ loc: [ start, end ] }`
489
+ * @param {boolean} [options.withLocation=false] Nodes will include source position offsets to the tokens: `{ loc: [ start, end ] }`
490
+ * @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)
490
491
  * @return {Object} An AST of nodes
491
492
  */
492
493
  export function parse (source, options) {
@@ -828,3 +828,14 @@ test('does not tolerate unterminated tokens', t => {
828
828
  { permitArrayCalls: true });
829
829
  t.end();
830
830
  });
831
+
832
+ test('parser can permit xlsx mode references', t => {
833
+ t.isInvalidExpr('=SUM([Workbook.xlsx]!A1+[Workbook.xlsx]!Table1[#Data])');
834
+ t.isParsed('=SUM([Workbook.xlsx]!A1+[Workbook.xlsx]!Table1[#Data])',
835
+ { type: 'CallExpression', callee: { type: 'Identifier', name: 'SUM' }, arguments: [
836
+ { type: 'BinaryExpression', operator: '+', arguments: [
837
+ { type: 'ReferenceIdentifier', value: '[Workbook.xlsx]!A1' },
838
+ { type: 'ReferenceIdentifier', value: '[Workbook.xlsx]!Table1[#Data]' } ] } ] },
839
+ { xlsx: true });
840
+ t.end();
841
+ });