@borgar/fx 3.1.0 → 4.0.0-rc.2

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/rc.js CHANGED
@@ -10,6 +10,8 @@ import { MAX_ROWS, MAX_COLS } from './constants.js';
10
10
  import { parseRef } from './parseRef.js';
11
11
  import { stringifyPrefix } from './stringifyPrefix.js';
12
12
 
13
+ const clamp = (min, val, max) => Math.min(Math.max(val, min), max);
14
+
13
15
  function toCoord (value, isAbs) {
14
16
  if (isAbs) {
15
17
  return String(value + 1);
@@ -17,12 +19,33 @@ function toCoord (value, isAbs) {
17
19
  return value ? '[' + value + ']' : '';
18
20
  }
19
21
 
20
- export function toRC (range) {
21
- const { r0, c0, r1, c1, $c0, $c1, $r0, $r1 } = range;
22
+ /**
23
+ * Stringify a range object into R1C1 syntax.
24
+ *
25
+ * @private
26
+ * @see parseR1C1Ref
27
+ * @param {Object} range A range object
28
+ * @return {string} An R1C1-style string represenation of a range
29
+ */
30
+ export function toR1C1 (range) {
31
+ let { r0, c0, r1, c1 } = range;
32
+ const { $c0, $c1, $r0, $r1 } = range;
22
33
  const nullR0 = r0 == null;
23
- const nullR1 = r1 == null;
24
34
  const nullC0 = c0 == null;
25
- const nullC1 = c1 == null;
35
+ let nullR1 = r1 == null;
36
+ let nullC1 = c1 == null;
37
+ r0 = clamp($r0 ? 0 : -MAX_ROWS, r0 | 0, MAX_ROWS);
38
+ c0 = clamp($c0 ? 0 : -MAX_COLS, c0 | 0, MAX_COLS);
39
+ if (!nullR0 && nullR1 && !nullC0 && nullC1) {
40
+ r1 = r0;
41
+ nullR1 = false;
42
+ c1 = c0;
43
+ nullC1 = false;
44
+ }
45
+ else {
46
+ r1 = clamp($r1 ? 0 : -MAX_ROWS, r1 | 0, MAX_ROWS);
47
+ c1 = clamp($c1 ? 0 : -MAX_COLS, c1 | 0, MAX_COLS);
48
+ }
26
49
  // C:C
27
50
  if ((r0 === 0 && r1 >= MAX_ROWS) || (nullR0 && nullR1)) {
28
51
  const a = toCoord(c0, $c0);
@@ -57,7 +80,7 @@ export function toRC (range) {
57
80
  return 'R' + s_r0 + 'C' + s_c0;
58
81
  }
59
82
 
60
- function parseRCPart (ref) {
83
+ function parseR1C1Part (ref) {
61
84
  let r0 = null;
62
85
  let c0 = null;
63
86
  let $r0 = null;
@@ -103,14 +126,22 @@ function parseRCPart (ref) {
103
126
  return [ r0, c0, $r0, $c0 ];
104
127
  }
105
128
 
106
- export function fromRC (ref) {
129
+ /**
130
+ * Parse a simple string reference to an R1C1 range into a range object.
131
+ *
132
+ * @private
133
+ * @see parseA1Ref
134
+ * @param {string} rangeString A range string
135
+ * @return {(Object|null)} An object representing a valid reference or null if it is invalid.
136
+ */
137
+ export function fromR1C1 (ref) {
107
138
  let final = null;
108
139
  const [ part1, part2 ] = ref.split(':', 2);
109
- const range = parseRCPart(part1);
140
+ const range = parseR1C1Part(part1);
110
141
  if (range) {
111
142
  const [ r0, c0, $r0, $c0 ] = range;
112
143
  if (part2) {
113
- const extendTo = parseRCPart(part2);
144
+ const extendTo = parseR1C1Part(part2);
114
145
  if (extendTo) {
115
146
  final = {};
116
147
  const [ r1, c1, $r1, $c1 ] = extendTo;
@@ -213,12 +244,38 @@ export function fromRC (ref) {
213
244
  return final;
214
245
  }
215
246
 
216
- export function parseRCRef (ref, { allowNamed = true, allowTernary = false } = {}) {
217
- const d = parseRef(ref, { allowNamed, allowTernary, r1c1: true });
247
+ /**
248
+ * Parse a string reference into an object representing it.
249
+ *
250
+ * ```js
251
+ * parseR1C1Ref('Sheet1!R[9]C9:R[9]C9');
252
+ * // => {
253
+ * // context: [ 'Sheet1' ],
254
+ * // range: {
255
+ * // r0: 9,
256
+ * // c0: 8,
257
+ * // r1: 9,
258
+ * // c1: 8,
259
+ * // $c0: true,
260
+ * // $c1: true
261
+ * // $r0: false,
262
+ * // $r1: false
263
+ * // }
264
+ * // }
265
+ * ```
266
+ *
267
+ * @param {string} refString An R1C1-style reference string
268
+ * @param {Object} [options={}] Options
269
+ * @param {boolean} [options.allowNamed=true] Enable parsing names as well as ranges.
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
+ * @return {(Object|null)} An object representing a valid reference or null if it is invalid.
272
+ */
273
+ export function parseR1C1Ref (refString, { allowNamed = true, allowTernary = false } = {}) {
274
+ const d = parseRef(refString, { allowNamed, allowTernary, r1c1: true });
218
275
  if (d && (d.r0 || d.name)) {
219
276
  const range = d.r1
220
- ? fromRC(d.r0 + ':' + d.r1)
221
- : fromRC(d.r0);
277
+ ? fromR1C1(d.r0 + ':' + d.r1)
278
+ : fromR1C1(d.r0);
222
279
  if (d.name || range) {
223
280
  d.range = range;
224
281
  delete d.r0;
@@ -232,15 +289,31 @@ export function parseRCRef (ref, { allowNamed = true, allowTernary = false } = {
232
289
  return null;
233
290
  }
234
291
 
235
- export function stringifyRCRef (ref) {
236
- return stringifyPrefix(ref) + (
237
- ref.name ? ref.name : toRC(ref.range)
292
+ /**
293
+ * Get an R1C1-style string representation of a reference object.
294
+ *
295
+ * ```js
296
+ * stringifyR1C1Ref({
297
+ * context: [ 'Sheet1' ],
298
+ * range: {
299
+ * r0: 9,
300
+ * c0: 8,
301
+ * r1: 9,
302
+ * c1: 8,
303
+ * $c0: true,
304
+ * $c1: true
305
+ * $r0: false,
306
+ * $r1: false
307
+ * }
308
+ * });
309
+ * // => 'Sheet1!R[9]C9:R[9]C9'
310
+ * ```
311
+ *
312
+ * @param {Object} refObject A reference object
313
+ * @return {Object} The reference in R1C1-style string format
314
+ */
315
+ export function stringifyR1C1Ref (refObject) {
316
+ return stringifyPrefix(refObject) + (
317
+ refObject.name ? refObject.name : toR1C1(refObject.range)
238
318
  );
239
319
  }
240
-
241
- export default {
242
- to: toRC,
243
- from: fromRC,
244
- parse: parseRCRef,
245
- stringify: stringifyRCRef
246
- };
package/lib/rc.spec.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable object-property-newline, object-curly-newline */
2
2
  import { test, Test } from 'tape';
3
3
  import { MAX_COLS, MAX_ROWS } from './constants.js';
4
- import { parseRCRef, stringifyRCRef, toRC } from './rc.js';
4
+ import { parseR1C1Ref, stringifyR1C1Ref, toR1C1 } from './rc.js';
5
5
 
6
6
  Test.prototype.isRCEqual = function isTokens (expr, result, opts) {
7
7
  if (result) {
@@ -20,11 +20,11 @@ Test.prototype.isRCEqual = function isTokens (expr, result, opts) {
20
20
  };
21
21
  }
22
22
  }
23
- this.deepEqual(parseRCRef(expr, opts), result, expr);
23
+ this.deepEqual(parseR1C1Ref(expr, opts), result, expr);
24
24
  };
25
25
 
26
26
  Test.prototype.isR1C1Rendered = function isTokens (range, expect, d) {
27
- this.is(toRC(range, d), expect, expect);
27
+ this.is(toR1C1(range, d), expect, expect);
28
28
  };
29
29
 
30
30
  test('parse single R1C1 references', t => {
@@ -196,12 +196,23 @@ test('R1C1 serialization', t => {
196
196
  t.isR1C1Rendered({ r0: -6, c0: 15, r1: 3, $c0: true, $c1: true }, 'R[-6]C16:R[3]');
197
197
  t.isR1C1Rendered({ r0: 0, c0: 10, r1: 9, $r0: true, $r1: true }, 'R1C[10]:R10');
198
198
  t.isR1C1Rendered({ r0: 0, c0: 15, r1: 9, $r0: true, $c0: true, $r1: true, $c1: true }, 'R1C16:R10');
199
+ // allow skipping right/bottom for cells
200
+ t.isR1C1Rendered({ r0: -5, c0: -2 }, 'R[-5]C[-2]');
201
+ // clamp the range at min/max dimensions
202
+ const abs = { $r0: true, $c0: true, $r1: true, $c1: true };
203
+ t.isR1C1Rendered({ r0: 1, c0: -20000, r1: 1, c1: 20000, ...abs }, 'R2');
204
+ t.isR1C1Rendered({ r0: -15e5, c0: 1, r1: 15e5, c1: 1, ...abs }, 'C2');
205
+ t.isR1C1Rendered({ r0: -5, c0: -2, r1: -8, c1: -7, ...abs }, 'R1C1');
206
+ t.isR1C1Rendered({ r0: 0, c0: -20000, r1: 0, c1: 20000 }, 'RC[-16383]:RC[16383]');
207
+ t.isR1C1Rendered({ r0: -15e5, c0: 0, r1: 15e5, c1: 0 }, 'R[-1048575]C:R[1048575]C');
208
+ t.isR1C1Rendered({ r0: 0.5, c0: 0.5, r1: 0.5, c1: 0.5, ...abs }, 'R1C1');
209
+ t.isR1C1Rendered({ r0: 0.5, c0: 0.5, r1: 0.5, c1: 0.5 }, 'RC');
199
210
  t.end();
200
211
  });
201
212
 
202
- test('stringifyRCRef', t => {
213
+ test('stringifyR1C1Ref', t => {
203
214
  const rangeA1 = { r0: 2, c0: 4, r1: 2, c1: 4 };
204
- const testRef = (ref, expect) => t.is(stringifyRCRef(ref), expect, expect);
215
+ const testRef = (ref, expect) => t.is(stringifyR1C1Ref(ref), expect, expect);
205
216
  testRef({ range: rangeA1 }, 'R[2]C[4]');
206
217
  testRef({ context: [ 'Sheet1' ], range: rangeA1 }, 'Sheet1!R[2]C[4]');
207
218
  testRef({ context: [ 'Sheet 1' ], range: rangeA1 }, "'Sheet 1'!R[2]C[4]");
package/lib/sr.js ADDED
@@ -0,0 +1,277 @@
1
+ import { parseRef } from './parseRef.js';
2
+ import { stringifyPrefix } from './stringifyPrefix.js';
3
+
4
+ const re_SRcolumnB = /^\[('['#@[\]]|[^'#@[\]])+\]/i;
5
+ const re_SRcolumnN = /^([^#@[\]:]+)/i;
6
+
7
+ const keyTerms = {
8
+ 'headers': 1,
9
+ 'data': 2,
10
+ 'totals': 4,
11
+ 'all': 8,
12
+ 'this row': 16,
13
+ '@': 16
14
+ };
15
+
16
+ const fz = (...a) => Object.freeze(a);
17
+
18
+ // only combinations allowed are: #data + (#headers | #totals | #data)
19
+ const sectionMap = {
20
+ // no terms
21
+ 0: fz(),
22
+ // single term
23
+ 1: fz('headers'),
24
+ 2: fz('data'),
25
+ 4: fz('totals'),
26
+ 8: fz('all'),
27
+ 16: fz('this row'),
28
+ // headers+data
29
+ 3: fz('headers', 'data'),
30
+ // totals+data
31
+ 6: fz('data', 'totals')
32
+ };
33
+
34
+ const matchColumn = (s, allowUnbraced = true) => {
35
+ let m = re_SRcolumnB.exec(s);
36
+ if (m) {
37
+ const value = m[0].slice(1, -1).replace(/'(['#@[\]])/g, '$1');
38
+ return [ m[0], value ];
39
+ }
40
+ if (allowUnbraced) {
41
+ m = re_SRcolumnN.exec(s);
42
+ if (m) {
43
+ return [ m[0], m[0] ];
44
+ }
45
+ }
46
+ return null;
47
+ };
48
+
49
+ export function parseSRange (raw) {
50
+ const columns = [];
51
+ let pos = 0;
52
+ let s = raw;
53
+ let m;
54
+ let m1;
55
+ let terms = 0;
56
+
57
+ // start of structured ref?
58
+ if ((m = /^(\[\s*)/.exec(s))) {
59
+ // quickly determine if this is a simple keyword or column
60
+ // [#keyword]
61
+ if ((m1 = /^\[#([a-z ]+)\]/i.exec(s))) {
62
+ const k = m1[1].toLowerCase();
63
+ pos += m1[0].length;
64
+ if (keyTerms[k]) {
65
+ terms |= keyTerms[k];
66
+ }
67
+ else {
68
+ return null;
69
+ }
70
+ }
71
+ // [column]
72
+ else if ((m1 = matchColumn(s, false))) {
73
+ pos += m1[0].length;
74
+ columns.push(m1[1].trim());
75
+ }
76
+ // use the "normal" method
77
+ // [[#keyword]]
78
+ // [[column]]
79
+ // [@]
80
+ // [@column]
81
+ // [@[column]]
82
+ // [@column:column]
83
+ // [@column:[column]]
84
+ // [@[column]:column]
85
+ // [@[column]:[column]]
86
+ // [column:column]
87
+ // [column:[column]]
88
+ // [[column]:column]
89
+ // [[column]:[column]]
90
+ // [[#keyword],column]
91
+ // [[#keyword],column:column]
92
+ // [[#keyword],[#keyword],column:column]
93
+ // ...
94
+ else {
95
+ let expect_more = true;
96
+ s = s.slice(m[1].length);
97
+ pos += m[1].length;
98
+ // match keywords as we find them
99
+ while (
100
+ expect_more &&
101
+ (m = /^\[#([a-z ]+)\](\s*,\s*)?/i.exec(s))
102
+ ) {
103
+ const k = m[1].toLowerCase();
104
+ if (keyTerms[k]) {
105
+ terms |= keyTerms[k];
106
+ s = s.slice(m[0].length);
107
+ pos += m[0].length;
108
+ expect_more = !!m[2];
109
+ }
110
+ else {
111
+ return null;
112
+ }
113
+ }
114
+ // is there an @ specifier?
115
+ if (expect_more && (m = /^@/.exec(s))) {
116
+ terms |= keyTerms['@'];
117
+ s = s.slice(1);
118
+ pos += 1;
119
+ expect_more = s[0] !== ']';
120
+ }
121
+ // not all keyword terms may be combined
122
+ if (!(terms in sectionMap)) {
123
+ return null;
124
+ }
125
+ // column definitions
126
+ const leftCol = expect_more ? matchColumn(raw.slice(pos)) : null;
127
+ if (leftCol) {
128
+ pos += leftCol[0].length;
129
+ columns.push(leftCol[1].trim());
130
+ s = raw.slice(pos);
131
+ if (s[0] === ':') {
132
+ s = s.slice(1);
133
+ pos++;
134
+ const rightCol = matchColumn(s);
135
+ if (rightCol) {
136
+ pos += rightCol[0].length;
137
+ columns.push(rightCol[1].trim());
138
+ }
139
+ else {
140
+ return null;
141
+ }
142
+ }
143
+ expect_more = false;
144
+ }
145
+ // advance ws
146
+ while (raw[pos] === ' ') {
147
+ pos++;
148
+ }
149
+ // close the ref
150
+ if (expect_more || raw[pos] !== ']') {
151
+ return null;
152
+ }
153
+ // step over the closing ]
154
+ pos++;
155
+ }
156
+ }
157
+ else {
158
+ return null;
159
+ }
160
+
161
+ const sections = sectionMap[terms];
162
+ return {
163
+ columns,
164
+ sections: sections ? sections.concat() : sections,
165
+ length: pos,
166
+ token: raw.slice(0, pos)
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Parse a structured reference string into an object representing it.
172
+ *
173
+ * ```js
174
+ * parseStructRef('workbook.xlsx!tableName[[#Data],[Column1]:[Column2]]');
175
+ * // => {
176
+ * // context: [ 'workbook.xlsx' ],
177
+ * // sections: [ 'data' ],
178
+ * // columns: [ 'my column', '@foo' ],
179
+ * // table: 'tableName',
180
+ * // }
181
+ * ```
182
+ *
183
+ * For A:A or A1:A style ranges, `null` will be used for any dimensions that the
184
+ * syntax does not specify:
185
+ *
186
+ * @tutorial References.md
187
+ * @param {string} ref A structured reference string
188
+ * @param {Object} [options={}] Options
189
+ * @return {(Object|null)} An object representing a valid reference or null if it is invalid.
190
+ */
191
+ export function parseStructRef (ref, opts = {}) {
192
+ const r = parseRef(ref, opts);
193
+ if (r && r.struct) {
194
+ const structData = parseSRange(r.struct);
195
+ 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
+ };
202
+ }
203
+ }
204
+ return null;
205
+ }
206
+
207
+ function quoteColname (str) {
208
+ return str.replace(/([[\]#'@])/g, '\'$1');
209
+ }
210
+
211
+ function needsBraces (str) {
212
+ return !/^[a-zA-Z0-9\u00a1-\uffff]+$/.test(str);
213
+ }
214
+
215
+ function toSentenceCase (str) {
216
+ return str[0].toUpperCase() + str.slice(1).toLowerCase();
217
+ }
218
+
219
+ /**
220
+ * Get a string representation of a structured reference object.
221
+ *
222
+ * ```js
223
+ * stringifyStructRef({
224
+ * context: [ 'workbook.xlsx' ],
225
+ * sections: [ 'data' ],
226
+ * columns: [ 'my column', '@foo' ],
227
+ * table: 'tableName',
228
+ * });
229
+ * // => 'workbook.xlsx!tableName[[#Data],[Column1]:[Column2]]'
230
+ * ```
231
+ *
232
+ * @param {Object} refObject A structured reference object
233
+ * @return {Object} The structured reference in string format
234
+ */
235
+ export function stringifyStructRef (ref) {
236
+ let s = stringifyPrefix(ref);
237
+ if (ref.table) {
238
+ s += ref.table;
239
+ }
240
+ const numColumns = ref.columns?.length ?? 0;
241
+ const numSections = ref.sections?.length ?? 0;
242
+ // single section
243
+ if (numSections === 1 && !numColumns) {
244
+ s += `[#${toSentenceCase(ref.sections[0])}]`;
245
+ }
246
+ // single column
247
+ else if (!numSections && numColumns === 1) {
248
+ s += `[${quoteColname(ref.columns[0])}]`;
249
+ }
250
+ else {
251
+ s += '[';
252
+ // single [#this row] sections get normalized to an @
253
+ const singleAt = numSections === 1 && ref.sections[0].toLowerCase() === 'this row';
254
+ if (singleAt) {
255
+ s += '@';
256
+ }
257
+ else if (numSections) {
258
+ s += ref.sections
259
+ .map(d => `[#${toSentenceCase(d)}]`)
260
+ .join(',');
261
+ if (numColumns) {
262
+ s += ',';
263
+ }
264
+ }
265
+ // a case of a single alphanumberic column with a [#this row] becomes [@col]
266
+ if (singleAt && ref.columns.length === 1 && !needsBraces(ref.columns[0])) {
267
+ s += quoteColname(ref.columns[0]);
268
+ }
269
+ else if (numColumns) {
270
+ s += ref.columns.slice(0, 2)
271
+ .map(d => (`[${quoteColname(d)}]`))
272
+ .join(':');
273
+ }
274
+ s += ']';
275
+ }
276
+ return s;
277
+ }
package/lib/sr.spec.js ADDED
@@ -0,0 +1,179 @@
1
+ /* eslint-disable object-property-newline, object-curly-newline */
2
+ import { test, Test } from 'tape';
3
+ import { parseStructRef, stringifyStructRef } from './sr.js';
4
+
5
+ Test.prototype.isSREqual = function isSREqual (expr, expect, opts) {
6
+ if (expect) {
7
+ expect = {
8
+ context: [],
9
+ table: '',
10
+ columns: [],
11
+ sections: [],
12
+ ...expect
13
+ };
14
+ }
15
+ this.deepEqual(parseStructRef(expr, opts), expect, expr);
16
+ };
17
+
18
+ test('parse structured references', t => {
19
+ t.isSREqual('table[col]', {
20
+ table: 'table',
21
+ columns: [ 'col' ]
22
+ });
23
+
24
+ t.isSREqual('[#All]', {
25
+ sections: [ 'all' ]
26
+ });
27
+
28
+ t.isSREqual('[column name]', {
29
+ columns: [ 'column name' ]
30
+ });
31
+
32
+ t.isSREqual('[column name]!foo', null);
33
+ t.isSREqual('[foo]bar', null);
34
+
35
+ t.isSREqual('[[my column]]', {
36
+ columns: [ 'my column' ]
37
+ });
38
+
39
+ t.isSREqual('[ [my column]:otherColumn ]', {
40
+ columns: [ 'my column', 'otherColumn' ]
41
+ });
42
+
43
+ t.isSREqual('[ @[my column]:otherColumn ]', {
44
+ columns: [ 'my column', 'otherColumn' ],
45
+ sections: [ 'this row' ]
46
+ });
47
+
48
+ t.isSREqual('[ [#Data], [my column]:otherColumn ]', {
49
+ columns: [ 'my column', 'otherColumn' ],
50
+ sections: [ 'data' ]
51
+ });
52
+
53
+ t.isSREqual('[ [#Data], [my column]:[\'@foo] ]', {
54
+ columns: [ 'my column', '@foo' ],
55
+ sections: [ 'data' ]
56
+ });
57
+
58
+ t.isSREqual('workbook.xlsx!tableName[ [#Data], [my column]:[\'@foo] ]', {
59
+ columns: [ 'my column', '@foo' ],
60
+ sections: [ 'data' ],
61
+ table: 'tableName',
62
+ context: [ 'workbook.xlsx' ]
63
+ });
64
+
65
+ t.isSREqual('[[#Data],[#data],[#Data],[#Data],[#Totals],[#Totals],[#Totals],foo]', {
66
+ columns: [ 'foo' ],
67
+ sections: [ 'data', 'totals' ]
68
+ });
69
+
70
+ t.end();
71
+ });
72
+
73
+ test('serialize structured references', t => {
74
+ t.is(
75
+ stringifyStructRef({
76
+ columns: [ 'foo' ]
77
+ }),
78
+ '[foo]',
79
+ '[foo]'
80
+ );
81
+
82
+ t.is(
83
+ stringifyStructRef({
84
+ columns: [ 'foo' ],
85
+ table: 'tableName'
86
+ }),
87
+ 'tableName[foo]',
88
+ 'tableName[foo]'
89
+ );
90
+
91
+ t.is(
92
+ stringifyStructRef({
93
+ columns: [ 'lorem ipsum' ],
94
+ table: 'tableName'
95
+ }),
96
+ 'tableName[lorem ipsum]',
97
+ 'tableName[lorem ipsum]'
98
+ );
99
+
100
+ t.is(
101
+ stringifyStructRef({
102
+ columns: [ 'foo', 'sævör' ],
103
+ table: 'tableName'
104
+ }),
105
+ 'tableName[[foo]:[sævör]]',
106
+ 'tableName[[foo]:[sævör]]'
107
+ );
108
+
109
+ t.is(
110
+ stringifyStructRef({
111
+ sections: [ 'data' ],
112
+ table: 'tableName'
113
+ }),
114
+ 'tableName[#Data]',
115
+ 'tableName[#Data]'
116
+ );
117
+
118
+ t.is(
119
+ stringifyStructRef({
120
+ columns: [ 'lorem ipsum', 'sævör' ],
121
+ table: 'tableName'
122
+ }),
123
+ 'tableName[[lorem ipsum]:[sævör]]',
124
+ 'tableName[[lorem ipsum]:[sævör]]'
125
+ );
126
+
127
+ t.is(
128
+ stringifyStructRef({
129
+ columns: [ 'my column', 'fo@o' ],
130
+ sections: [ 'data' ],
131
+ table: 'tableName',
132
+ context: [ 'workbook.xlsx' ]
133
+ }),
134
+ 'workbook.xlsx!tableName[[#Data],[my column]:[fo\'@o]]',
135
+ 'workbook.xlsx!tableName[[#Data],[my column]:[fo\'@o]]'
136
+ );
137
+
138
+ t.is(
139
+ stringifyStructRef({
140
+ columns: [ 'bar' ],
141
+ sections: [ 'this row' ],
142
+ table: 'foo'
143
+ }),
144
+ 'foo[@bar]',
145
+ 'foo[@bar]'
146
+ );
147
+
148
+ t.is(
149
+ stringifyStructRef({
150
+ columns: [ 'bar', 'baz' ],
151
+ sections: [ 'this row' ],
152
+ table: 'foo'
153
+ }),
154
+ 'foo[@[bar]:[baz]]',
155
+ 'foo[@[bar]:[baz]]'
156
+ );
157
+
158
+ t.is(
159
+ stringifyStructRef({
160
+ columns: [ 'lorem ipsum', 'baz' ],
161
+ sections: [ 'this row' ],
162
+ table: 'foo'
163
+ }),
164
+ 'foo[@[lorem ipsum]:[baz]]',
165
+ 'foo[@[lorem ipsum]:[baz]]'
166
+ );
167
+
168
+ t.is(
169
+ stringifyStructRef({
170
+ sections: [ 'data', 'headers' ],
171
+ table: 'table'
172
+ }),
173
+ 'table[[#Data],[#Headers]]',
174
+ 'table[[#Data],[#Headers]]'
175
+ );
176
+
177
+ t.end();
178
+ });
179
+