@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/.eslintrc +25 -12
- package/.jsdoc/config.json +17 -0
- package/.jsdoc/publish.js +195 -0
- package/README.md +8 -311
- package/dist/fx.js +1 -1
- package/docs/API.md +708 -0
- package/docs/AST format.md +144 -0
- package/docs/References.md +60 -0
- package/lib/a1.js +156 -30
- package/lib/a1.spec.js +9 -2
- package/lib/{addMeta.js → addTokenMeta.js} +50 -5
- package/lib/{addMeta.spec.js → addTokenMeta.spec.js} +16 -16
- package/lib/constants.js +14 -4
- package/lib/fixRanges.js +64 -10
- package/lib/fixRanges.spec.js +35 -6
- package/lib/index.js +105 -17
- package/lib/isType.js +119 -8
- package/lib/lexer-srefs.spec.js +311 -0
- package/lib/lexer.js +55 -15
- package/lib/lexer.spec.js +223 -214
- package/lib/lexerParts.js +38 -14
- package/lib/mergeRefTokens.js +38 -25
- package/lib/mergeRefTokens.spec.js +39 -39
- package/lib/parseRef.js +17 -12
- package/lib/parser.js +498 -0
- package/lib/parser.spec.js +777 -0
- package/lib/rc.js +95 -22
- package/lib/rc.spec.js +16 -5
- package/lib/sr.js +277 -0
- package/lib/sr.spec.js +179 -0
- package/lib/translate-toA1.spec.js +38 -20
- package/lib/translate-toRC.spec.js +23 -23
- package/lib/translate.js +111 -30
- package/package.json +3 -1
- package/References.md +0 -39
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
140
|
+
const range = parseR1C1Part(part1);
|
|
110
141
|
if (range) {
|
|
111
142
|
const [ r0, c0, $r0, $c0 ] = range;
|
|
112
143
|
if (part2) {
|
|
113
|
-
const extendTo =
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
?
|
|
221
|
-
:
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 {
|
|
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(
|
|
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(
|
|
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('
|
|
213
|
+
test('stringifyR1C1Ref', t => {
|
|
203
214
|
const rangeA1 = { r0: 2, c0: 4, r1: 2, c1: 4 };
|
|
204
|
-
const testRef = (ref, expect) => t.is(
|
|
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
|
+
|