@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/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
+ });
package/lib/rc.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { MAX_ROWS, MAX_COLS } from './constants.js';
10
10
  import { parseRef } from './parseRef.js';
11
- import { stringifyPrefix } from './stringifyPrefix.js';
11
+ import { stringifyPrefix, stringifyPrefixAlt } from './stringifyPrefix.js';
12
12
 
13
13
  const clamp = (min, val, max) => Math.min(Math.max(val, min), max);
14
14
 
@@ -268,10 +268,11 @@ export function fromR1C1 (ref) {
268
268
  * @param {Object} [options={}] Options
269
269
  * @param {boolean} [options.allowNamed=true] Enable parsing names as well as ranges.
270
270
  * @param {boolean} [options.allowTernary=false] Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`. These are supported by Google Sheets but not Excel. See: References.md.
271
+ * @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)
271
272
  * @return {(Object|null)} An object representing a valid reference or null if it is invalid.
272
273
  */
273
- export function parseR1C1Ref (refString, { allowNamed = true, allowTernary = false } = {}) {
274
- const d = parseRef(refString, { allowNamed, allowTernary, r1c1: true });
274
+ export function parseR1C1Ref (refString, { allowNamed = true, allowTernary = false, xlsx = false } = {}) {
275
+ const d = parseRef(refString, { allowNamed, allowTernary, xlsx, r1c1: true });
275
276
  if (d && (d.r0 || d.name)) {
276
277
  const range = d.r1
277
278
  ? fromR1C1(d.r0 + ':' + d.r1)
@@ -310,10 +311,15 @@ export function parseR1C1Ref (refString, { allowNamed = true, allowTernary = fal
310
311
  * ```
311
312
  *
312
313
  * @param {Object} refObject A reference object
314
+ * @param {Object} [options={}] Options
315
+ * @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)
313
316
  * @return {Object} The reference in R1C1-style string format
314
317
  */
315
- export function stringifyR1C1Ref (refObject) {
316
- return stringifyPrefix(refObject) + (
318
+ export function stringifyR1C1Ref (refObject, { xlsx = false } = {}) {
319
+ const prefix = xlsx
320
+ ? stringifyPrefixAlt(refObject)
321
+ : stringifyPrefix(refObject);
322
+ return prefix + (
317
323
  refObject.name ? refObject.name : toR1C1(refObject.range)
318
324
  );
319
325
  }
package/lib/rc.spec.js CHANGED
@@ -3,24 +3,32 @@ import { test, Test } from 'tape';
3
3
  import { MAX_COLS, MAX_ROWS } from './constants.js';
4
4
  import { parseR1C1Ref, stringifyR1C1Ref, toR1C1 } from './rc.js';
5
5
 
6
- Test.prototype.isRCEqual = function isTokens (expr, result, opts) {
7
- if (result) {
8
- result = {
9
- context: [],
10
- name: '',
11
- range: null,
12
- ...result
13
- };
14
- if (result.range && typeof result.range === 'object') {
6
+ Test.prototype.isRCEqual = function isTokens (expr, expect, opts) {
7
+ if (expect) {
8
+ expect = (opts?.xlsx)
9
+ ? {
10
+ workbookName: '',
11
+ sheetName: '',
12
+ name: '',
13
+ range: null,
14
+ ...expect
15
+ }
16
+ : {
17
+ context: [],
18
+ name: '',
19
+ range: null,
20
+ ...expect
21
+ };
22
+ if (expect.range && typeof expect.range === 'object') {
15
23
  // mix in some defaults so we don't have to write things out in full
16
- result.range = {
24
+ expect.range = {
17
25
  r0: null, c0: null, r1: null, c1: null,
18
26
  $r0: false, $c0: false, $r1: false, $c1: false,
19
- ...result.range
27
+ ...expect.range
20
28
  };
21
29
  }
22
30
  }
23
- this.deepEqual(parseR1C1Ref(expr, opts), result, expr);
31
+ this.deepEqual(parseR1C1Ref(expr, opts), expect, expr);
24
32
  };
25
33
 
26
34
  Test.prototype.isR1C1Rendered = function isTokens (range, expect, d) {
@@ -151,6 +159,108 @@ test('parse joined R1C1 references', t => {
151
159
  t.end();
152
160
  });
153
161
 
162
+ test('parse R1C1 ranges in XLSX mode', t => {
163
+ const opts = { xlsx: true };
164
+ const rcRange = { r0: 0, c0: 0, r1: 0, c1: 0 };
165
+ t.isRCEqual('[1]!RC', {
166
+ workbookName: '1',
167
+ sheetName: '',
168
+ range: rcRange
169
+ }, opts);
170
+
171
+ t.isRCEqual('[Workbook.xlsx]!RC', {
172
+ workbookName: 'Workbook.xlsx',
173
+ sheetName: '',
174
+ range: rcRange
175
+ }, opts);
176
+
177
+ t.isRCEqual('[1]Sheet1!RC', {
178
+ workbookName: '1',
179
+ sheetName: 'Sheet1',
180
+ range: rcRange
181
+ }, opts);
182
+
183
+ t.isRCEqual('[Workbook.xlsx]Sheet1!RC', {
184
+ workbookName: 'Workbook.xlsx',
185
+ sheetName: 'Sheet1',
186
+ range: rcRange
187
+ }, opts);
188
+
189
+ t.isRCEqual('[4]!name', {
190
+ workbookName: '4',
191
+ sheetName: '',
192
+ name: 'name'
193
+ }, opts);
194
+
195
+ t.isRCEqual('[Workbook.xlsx]!name', {
196
+ workbookName: 'Workbook.xlsx',
197
+ sheetName: '',
198
+ name: 'name'
199
+ }, opts);
200
+
201
+ t.isRCEqual('[16]Sheet1!name', {
202
+ workbookName: '16',
203
+ sheetName: 'Sheet1',
204
+ name: 'name'
205
+ }, opts);
206
+
207
+ t.isRCEqual('[Workbook.xlsx]Sheet1!name', {
208
+ workbookName: 'Workbook.xlsx',
209
+ sheetName: 'Sheet1',
210
+ name: 'name'
211
+ }, opts);
212
+
213
+ t.isRCEqual("='[1]'!RC", {
214
+ workbookName: '1',
215
+ sheetName: '',
216
+ range: rcRange
217
+ }, opts);
218
+
219
+ t.isRCEqual("='[Workbook.xlsx]'!RC", {
220
+ workbookName: 'Workbook.xlsx',
221
+ sheetName: '',
222
+ range: rcRange
223
+ }, opts);
224
+
225
+ t.isRCEqual("'[1]Sheet1'!RC", {
226
+ workbookName: '1',
227
+ sheetName: 'Sheet1',
228
+ range: rcRange
229
+ }, opts);
230
+
231
+ t.isRCEqual("'[Workbook.xlsx]Sheet1'!RC", {
232
+ workbookName: 'Workbook.xlsx',
233
+ sheetName: 'Sheet1',
234
+ range: rcRange
235
+ }, opts);
236
+
237
+ t.isRCEqual("'[4]'!name", {
238
+ workbookName: '4',
239
+ sheetName: '',
240
+ name: 'name'
241
+ }, opts);
242
+
243
+ t.isRCEqual("'[Workbook.xlsx]'!name", {
244
+ workbookName: 'Workbook.xlsx',
245
+ sheetName: '',
246
+ name: 'name'
247
+ }, opts);
248
+
249
+ t.isRCEqual("'[16]Sheet1'!name", {
250
+ workbookName: '16',
251
+ sheetName: 'Sheet1',
252
+ name: 'name'
253
+ }, opts);
254
+
255
+ t.isRCEqual("'[Workbook.xlsx]Sheet1'!name", {
256
+ workbookName: 'Workbook.xlsx',
257
+ sheetName: 'Sheet1',
258
+ name: 'name'
259
+ }, opts);
260
+
261
+ t.end();
262
+ });
263
+
154
264
  test('R1C1 serialization', t => {
155
265
  // ray
156
266
  t.isR1C1Rendered({ r0: 0, c0: 0, r1: 0, c1: MAX_COLS }, 'R');
@@ -229,3 +339,26 @@ test('stringifyR1C1Ref', t => {
229
339
  testRef({ context: [ 'My File.xlsx' ], name: 'foo' }, "'My File.xlsx'!foo");
230
340
  t.end();
231
341
  });
342
+
343
+ test('stringifyR1C1Ref in XLSX mode', t => {
344
+ const rangeA1 = { r0: 2, c0: 4, r1: 2, c1: 4 };
345
+ const testRef = (ref, expect) => t.is(stringifyR1C1Ref(ref, { xlsx: true }), expect, expect);
346
+ testRef({ range: rangeA1 }, 'R[2]C[4]');
347
+ testRef({ sheetName: 'Sheet1', range: rangeA1 }, 'Sheet1!R[2]C[4]');
348
+ testRef({ sheetName: 'Sheet 1', range: rangeA1 }, "'Sheet 1'!R[2]C[4]");
349
+ testRef({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', range: rangeA1 }, '[MyFile.xlsx]Sheet1!R[2]C[4]');
350
+ testRef({ workbookName: 'My File.xlsx', sheetName: 'Sheet1', range: rangeA1 }, "'[My File.xlsx]Sheet1'!R[2]C[4]");
351
+ testRef({ workbookName: 'MyFile.xlsx', range: rangeA1 }, '[MyFile.xlsx]!R[2]C[4]');
352
+ testRef({ workbookName: 'My File.xlsx', range: rangeA1 }, "'[My File.xlsx]'!R[2]C[4]");
353
+ testRef({ name: 'foo' }, 'foo');
354
+ testRef({ sheetName: 'Sheet1', name: 'foo' }, 'Sheet1!foo');
355
+ testRef({ sheetName: 'Sheet 1', name: 'foo' }, "'Sheet 1'!foo");
356
+ testRef({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', name: 'foo' }, '[MyFile.xlsx]Sheet1!foo');
357
+ testRef({ workbookName: 'My File.xlsx', sheetName: 'Sheet1', name: 'foo' }, "'[My File.xlsx]Sheet1'!foo");
358
+ testRef({ workbookName: 'MyFile.xlsx', name: 'foo' }, '[MyFile.xlsx]!foo');
359
+ testRef({ workbookName: 'My File.xlsx', name: 'foo' }, "'[My File.xlsx]'!foo");
360
+ // ignore .context
361
+ testRef({ context: [ 'MyFile.xlsx', 'Sheet1' ], range: rangeA1 }, 'R[2]C[4]');
362
+ testRef({ context: [ 'MyFile.xlsx', 'Sheet1' ], name: 'foo' }, 'foo');
363
+ t.end();
364
+ });
package/lib/sr.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { parseRef } from './parseRef.js';
2
- import { stringifyPrefix } from './stringifyPrefix.js';
2
+ import { stringifyPrefix, stringifyPrefixAlt } from './stringifyPrefix.js';
3
3
 
4
4
  const re_SRcolumnB = /^\[('['#@[\]]|[^'#@[\]])+\]/i;
5
5
  const re_SRcolumnN = /^([^#@[\]:]+)/i;
@@ -186,19 +186,28 @@ export function parseSRange (raw) {
186
186
  * @tutorial References.md
187
187
  * @param {string} ref A structured reference string
188
188
  * @param {Object} [options={}] Options
189
+ * @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)
189
190
  * @return {(Object|null)} An object representing a valid reference or null if it is invalid.
190
191
  */
191
- export function parseStructRef (ref, opts = {}) {
192
+ export function parseStructRef (ref, opts = { xlsx: false }) {
192
193
  const r = parseRef(ref, opts);
193
194
  if (r && r.struct) {
194
195
  const structData = parseSRange(r.struct);
195
196
  if (structData && structData.length === r.struct.length) {
196
- return {
197
- context: r.context,
198
- table: r.name,
199
- columns: structData.columns,
200
- sections: structData.sections
201
- };
197
+ return opts.xlsx
198
+ ? {
199
+ workbookName: r.workbookName,
200
+ sheetName: r.sheetName,
201
+ table: r.name,
202
+ columns: structData.columns,
203
+ sections: structData.sections
204
+ }
205
+ : {
206
+ context: r.context,
207
+ table: r.name,
208
+ columns: structData.columns,
209
+ sections: structData.sections
210
+ };
202
211
  }
203
212
  }
204
213
  return null;
@@ -230,10 +239,15 @@ function toSentenceCase (str) {
230
239
  * ```
231
240
  *
232
241
  * @param {Object} refObject A structured reference object
242
+ * @param {Object} [options={}] Options
243
+ * @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)
233
244
  * @return {Object} The structured reference in string format
234
245
  */
235
- export function stringifyStructRef (ref) {
236
- let s = stringifyPrefix(ref);
246
+ export function stringifyStructRef (ref, { xlsx = false } = {}) {
247
+ let s = xlsx
248
+ ? stringifyPrefixAlt(ref)
249
+ : stringifyPrefix(ref);
250
+
237
251
  if (ref.table) {
238
252
  s += ref.table;
239
253
  }
package/lib/sr.spec.js CHANGED
@@ -4,13 +4,22 @@ import { parseStructRef, stringifyStructRef } from './sr.js';
4
4
 
5
5
  Test.prototype.isSREqual = function isSREqual (expr, expect, opts) {
6
6
  if (expect) {
7
- expect = {
8
- context: [],
9
- table: '',
10
- columns: [],
11
- sections: [],
12
- ...expect
13
- };
7
+ expect = opts?.xlsx
8
+ ? {
9
+ workbookName: '',
10
+ sheetName: '',
11
+ table: '',
12
+ columns: [],
13
+ sections: [],
14
+ ...expect
15
+ }
16
+ : {
17
+ context: [],
18
+ table: '',
19
+ columns: [],
20
+ sections: [],
21
+ ...expect
22
+ };
14
23
  }
15
24
  this.deepEqual(parseStructRef(expr, opts), expect, expr);
16
25
  };
@@ -67,6 +76,18 @@ test('parse structured references', t => {
67
76
  sections: [ 'data', 'totals' ]
68
77
  });
69
78
 
79
+ t.isSREqual("'Sheet'!Table[Column]", {
80
+ columns: [ 'Column' ],
81
+ table: 'Table',
82
+ context: [ 'Sheet' ]
83
+ });
84
+
85
+ t.isSREqual("Sheet1!Table1[foo '[bar']]", {
86
+ columns: [ 'foo [bar]' ],
87
+ table: 'Table1',
88
+ context: [ 'Sheet1' ]
89
+ });
90
+
70
91
  t.end();
71
92
  });
72
93
 
@@ -177,3 +198,68 @@ test('serialize structured references', t => {
177
198
  t.end();
178
199
  });
179
200
 
201
+ test('structured references parse and serialize in xlsx mode', t => {
202
+ t.isSREqual('[Workbook.xlsx]!Table[#Data]', {
203
+ workbookName: 'Workbook.xlsx',
204
+ table: 'Table',
205
+ sections: [ 'data' ]
206
+ }, { xlsx: true });
207
+
208
+ t.isSREqual('[Workbook.xlsx]Sheet1!Table[#Data]', {
209
+ workbookName: 'Workbook.xlsx',
210
+ sheetName: 'Sheet1',
211
+ table: 'Table',
212
+ sections: [ 'data' ]
213
+ }, { xlsx: true });
214
+
215
+ t.isSREqual('Sheet1!Table[#Data]', {
216
+ sheetName: 'Sheet1',
217
+ table: 'Table',
218
+ sections: [ 'data' ]
219
+ }, { xlsx: true });
220
+
221
+ t.is(
222
+ stringifyStructRef({
223
+ context: [ 'Lorem', 'Ipsum' ],
224
+ columns: [ 'foo' ]
225
+ }, { xlsx: true }),
226
+ '[foo]',
227
+ 'context prop is ignored in xlsx mode'
228
+ );
229
+ t.is(
230
+ stringifyStructRef({
231
+ workbookName: 'Lorem',
232
+ sheetName: 'Ipsum',
233
+ columns: [ 'foo' ]
234
+ }, { xlsx: false }),
235
+ '[foo]',
236
+ 'workbookName+sheetName props are ignored in default mode'
237
+ );
238
+ t.is(
239
+ stringifyStructRef({
240
+ workbookName: 'Lorem',
241
+ sheetName: 'Ipsum',
242
+ columns: [ 'foo' ]
243
+ }, { xlsx: true }),
244
+ '[Lorem]Ipsum![foo]',
245
+ 'workbookName+sheetName props are rendered correctly'
246
+ );
247
+ t.is(
248
+ stringifyStructRef({
249
+ workbookName: 'Lorem',
250
+ columns: [ 'foo' ]
251
+ }, { xlsx: true }),
252
+ '[Lorem]![foo]',
253
+ 'workbookName prop is rendered correctly'
254
+ );
255
+ t.is(
256
+ stringifyStructRef({
257
+ sheetName: 'Ipsum',
258
+ columns: [ 'foo' ]
259
+ }, { xlsx: true }),
260
+ 'Ipsum![foo]',
261
+ 'sheetName prop is rendered correctly'
262
+ );
263
+ t.end();
264
+ });
265
+
@@ -19,3 +19,21 @@ export function stringifyPrefix (ref) {
19
19
  }
20
20
  return pre ? pre + '!' : pre;
21
21
  }
22
+
23
+ export function stringifyPrefixAlt (ref) {
24
+ let pre = '';
25
+ let quote = 0;
26
+ const { workbookName, sheetName } = ref;
27
+ if (workbookName) {
28
+ pre += '[' + workbookName + ']';
29
+ quote += reBannedChars.test(workbookName);
30
+ }
31
+ if (sheetName) {
32
+ pre += sheetName;
33
+ quote += reBannedChars.test(sheetName);
34
+ }
35
+ if (quote) {
36
+ pre = "'" + pre.replace(/'/g, "''") + "'";
37
+ }
38
+ return pre ? pre + '!' : pre;
39
+ }
@@ -2,10 +2,10 @@ import { test, Test } from 'tape';
2
2
  import { translateToA1 } from './translate.js';
3
3
  import { tokenize } from './lexer.js';
4
4
  import { addTokenMeta } from './addTokenMeta.js';
5
- import { ERROR, FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_BEAM } from './constants.js';
5
+ import { ERROR, FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_BEAM, REF_STRUCT } from './constants.js';
6
6
 
7
- Test.prototype.isR2A = function isTokens (expr, anchor, result) {
8
- this.is(translateToA1(expr, anchor), result, expr);
7
+ Test.prototype.isR2A = function isTokens (expr, anchor, result, opts) {
8
+ this.is(translateToA1(expr, anchor, opts), result, expr);
9
9
  };
10
10
 
11
11
  test('translate absolute cells from RC to A1', t => {
@@ -175,3 +175,23 @@ test('translate works with merged ranges', t => {
175
175
  t.deepEqual(translateToA1(tokens, 'D10'), expected, expr);
176
176
  t.end();
177
177
  });
178
+
179
+ test('translate works with xlsx mode references', t => {
180
+ const testExpr = (expr, anchor, expected) => {
181
+ const opts = { mergeRefs: true, xlsx: true, r1c1: true };
182
+ t.deepEqual(translateToA1(tokenize(expr, opts), anchor, opts), expected, expr);
183
+ };
184
+ testExpr("'[My Fancy Workbook.xlsx]'!R1C", 'B2', [
185
+ { type: REF_RANGE, value: "'[My Fancy Workbook.xlsx]'!B$1" }
186
+ ]);
187
+ testExpr('[Workbook.xlsx]!R1C', 'B2', [
188
+ { type: REF_RANGE, value: '[Workbook.xlsx]!B$1' }
189
+ ]);
190
+ testExpr('[Workbook.xlsx]Sheet1!R1C', 'B2', [
191
+ { type: REF_RANGE, value: '[Workbook.xlsx]Sheet1!B$1' }
192
+ ]);
193
+ testExpr('[Workbook.xlsx]!table[#data]', 'B2', [
194
+ { type: REF_STRUCT, value: '[Workbook.xlsx]!table[#data]' }
195
+ ]);
196
+ t.end();
197
+ });