@borgar/fx 4.13.0 → 5.0.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/dist/index-BMr6cTgc.d.cts +1444 -0
- package/dist/index-BMr6cTgc.d.ts +1444 -0
- package/dist/index.cjs +3054 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2984 -0
- package/dist/index.js.map +1 -0
- package/dist/xlsx/index.cjs +3120 -0
- package/dist/xlsx/index.cjs.map +1 -0
- package/dist/xlsx/index.d.cts +55 -0
- package/dist/xlsx/index.d.ts +55 -0
- package/dist/xlsx/index.js +3049 -0
- package/dist/xlsx/index.js.map +1 -0
- package/docs/API.md +2959 -718
- package/docs/AST_format.md +2 -2
- package/eslint.config.mjs +40 -0
- package/lib/a1.spec.ts +32 -0
- package/lib/a1.ts +26 -0
- package/lib/addA1RangeBounds.ts +50 -0
- package/lib/addTokenMeta.spec.ts +166 -0
- package/lib/{addTokenMeta.js → addTokenMeta.ts} +53 -33
- package/lib/astTypes.ts +211 -0
- package/lib/cloneToken.ts +29 -0
- package/lib/{constants.js → constants.ts} +6 -3
- package/lib/fixRanges.spec.ts +220 -0
- package/lib/fixRanges.ts +260 -0
- package/lib/fromCol.spec.ts +15 -0
- package/lib/{fromCol.js → fromCol.ts} +1 -1
- package/lib/index.spec.ts +119 -0
- package/lib/index.ts +76 -0
- package/lib/isNodeType.ts +151 -0
- package/lib/isType.spec.ts +208 -0
- package/lib/{isType.js → isType.ts} +26 -25
- package/lib/lexers/{advRangeOp.js → advRangeOp.ts} +1 -1
- package/lib/lexers/{canEndRange.js → canEndRange.ts} +2 -2
- package/lib/lexers/{lexBoolean.js → lexBoolean.ts} +25 -6
- package/lib/lexers/{lexContext.js → lexContext.ts} +14 -6
- package/lib/lexers/{lexError.js → lexError.ts} +3 -3
- package/lib/lexers/{lexFunction.js → lexFunction.ts} +3 -2
- package/lib/lexers/lexNameFuncCntx.ts +112 -0
- package/lib/lexers/{lexNamed.js → lexNamed.ts} +4 -4
- package/lib/lexers/{lexNewLine.js → lexNewLine.ts} +3 -2
- package/lib/lexers/{lexNumber.js → lexNumber.ts} +4 -3
- package/lib/lexers/{lexOperator.js → lexOperator.ts} +5 -4
- package/lib/lexers/lexRange.ts +15 -0
- package/lib/lexers/{lexRangeA1.js → lexRangeA1.ts} +11 -7
- package/lib/lexers/{lexRangeR1C1.js → lexRangeR1C1.ts} +10 -6
- package/lib/lexers/{lexRangeTrim.js → lexRangeTrim.ts} +3 -2
- package/lib/lexers/{lexRefOp.js → lexRefOp.ts} +4 -3
- package/lib/lexers/{lexString.js → lexString.ts} +3 -3
- package/lib/lexers/{lexStructured.js → lexStructured.ts} +5 -5
- package/lib/lexers/{lexWhitespace.js → lexWhitespace.ts} +3 -2
- package/lib/lexers/sets.ts +51 -0
- package/lib/mergeRefTokens.spec.ts +141 -0
- package/lib/{mergeRefTokens.js → mergeRefTokens.ts} +14 -9
- package/lib/nodeTypes.ts +54 -0
- package/lib/parse.spec.ts +1410 -0
- package/lib/{parser.js → parse.ts} +81 -63
- package/lib/parseA1Range.spec.ts +233 -0
- package/lib/parseA1Range.ts +206 -0
- package/lib/parseA1Ref.spec.ts +337 -0
- package/lib/parseA1Ref.ts +115 -0
- package/lib/parseR1C1Range.ts +191 -0
- package/lib/parseR1C1Ref.spec.ts +323 -0
- package/lib/parseR1C1Ref.ts +127 -0
- package/lib/parseRef.spec.ts +90 -0
- package/lib/parseRef.ts +240 -0
- package/lib/{parseSRange.js → parseSRange.ts} +15 -10
- package/lib/parseStructRef.spec.ts +168 -0
- package/lib/parseStructRef.ts +76 -0
- package/lib/stringifyA1Range.spec.ts +72 -0
- package/lib/stringifyA1Range.ts +72 -0
- package/lib/stringifyA1Ref.spec.ts +64 -0
- package/lib/stringifyA1Ref.ts +59 -0
- package/lib/{stringifyPrefix.js → stringifyPrefix.ts} +17 -2
- package/lib/stringifyR1C1Range.spec.ts +92 -0
- package/lib/stringifyR1C1Range.ts +73 -0
- package/lib/stringifyR1C1Ref.spec.ts +63 -0
- package/lib/stringifyR1C1Ref.ts +67 -0
- package/lib/stringifyStructRef.spec.ts +124 -0
- package/lib/stringifyStructRef.ts +113 -0
- package/lib/stringifyTokens.ts +15 -0
- package/lib/toCol.spec.ts +11 -0
- package/lib/{toCol.js → toCol.ts} +4 -4
- package/lib/tokenTypes.ts +76 -0
- package/lib/tokenize-srefs.spec.ts +429 -0
- package/lib/tokenize.spec.ts +2103 -0
- package/lib/tokenize.ts +346 -0
- package/lib/translate.spec.ts +35 -0
- package/lib/translateToA1.spec.ts +247 -0
- package/lib/translateToA1.ts +231 -0
- package/lib/translateToR1C1.spec.ts +227 -0
- package/lib/translateToR1C1.ts +145 -0
- package/lib/types.ts +179 -0
- package/lib/xlsx/index.spec.ts +27 -0
- package/lib/xlsx/index.ts +32 -0
- package/package.json +45 -31
- package/tsconfig.json +28 -0
- package/typedoc-ignore-links.ts +17 -0
- package/typedoc.json +41 -0
- package/.eslintrc +0 -22
- package/benchmark/benchmark.js +0 -48
- package/benchmark/formulas.json +0 -15677
- package/dist/fx.d.ts +0 -823
- package/dist/fx.js +0 -2
- package/dist/package.json +0 -1
- package/lib/a1.js +0 -348
- package/lib/a1.spec.js +0 -458
- package/lib/addTokenMeta.spec.js +0 -153
- package/lib/astTypes.js +0 -96
- package/lib/extraTypes.js +0 -74
- package/lib/fixRanges.js +0 -104
- package/lib/fixRanges.spec.js +0 -171
- package/lib/fromCol.spec.js +0 -11
- package/lib/index.js +0 -134
- package/lib/index.spec.js +0 -67
- package/lib/isType.spec.js +0 -168
- package/lib/lexer-srefs.spec.js +0 -324
- package/lib/lexer.js +0 -264
- package/lib/lexer.spec.js +0 -1953
- package/lib/lexers/lexRange.js +0 -8
- package/lib/lexers/sets.js +0 -38
- package/lib/mergeRefTokens.spec.js +0 -121
- package/lib/package.json +0 -1
- package/lib/parseRef.js +0 -157
- package/lib/parseRef.spec.js +0 -71
- package/lib/parseStructRef.js +0 -48
- package/lib/parseStructRef.spec.js +0 -164
- package/lib/parser.spec.js +0 -1208
- package/lib/rc.js +0 -341
- package/lib/rc.spec.js +0 -403
- package/lib/stringifyStructRef.js +0 -80
- package/lib/stringifyStructRef.spec.js +0 -182
- package/lib/toCol.spec.js +0 -11
- package/lib/translate-toA1.spec.js +0 -214
- package/lib/translate-toRC.spec.js +0 -197
- package/lib/translate.js +0 -239
- package/lib/translate.spec.js +0 -21
- package/rollup.config.mjs +0 -22
- package/tsd.json +0 -12
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { splitPrefix } from './parseRef.ts';
|
|
3
|
+
|
|
4
|
+
function testStr (str: string, opt: boolean, expected: any) {
|
|
5
|
+
expect(splitPrefix(str, opt)).toEqual(expected);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('splitPrefix', () => {
|
|
9
|
+
describe('with simple mode (opt = true)', () => {
|
|
10
|
+
test('fully bracketed references', () => {
|
|
11
|
+
testStr('[foo][bar][baz]', true, [ 'foo', 'bar', 'baz' ]);
|
|
12
|
+
testStr('[foo][bar]', true, [ 'foo', 'bar' ]);
|
|
13
|
+
testStr('[foo]', true, [ 'foo' ]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('mixed bracketed and unbracketed references', () => {
|
|
17
|
+
testStr('foo[bar][baz]', true, [ 'foo', 'bar', 'baz' ]);
|
|
18
|
+
testStr('[foo]bar[baz]', true, [ 'foo', 'bar', 'baz' ]);
|
|
19
|
+
testStr('[foo][bar]baz', true, [ 'foo', 'bar', 'baz' ]);
|
|
20
|
+
testStr('foo[bar]baz', true, [ 'foo', 'bar', 'baz' ]);
|
|
21
|
+
testStr('[foo]bar', true, [ 'foo', 'bar' ]);
|
|
22
|
+
testStr('foo[bar]', true, [ 'foo', 'bar' ]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('unbracketed references', () => {
|
|
26
|
+
testStr('foo', true, [ 'foo' ]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('with detailed mode (opt = false)', () => {
|
|
31
|
+
test('fully bracketed references', () => {
|
|
32
|
+
testStr('[foo][bar][baz]', false, [
|
|
33
|
+
{ value: 'foo', braced: true },
|
|
34
|
+
{ value: 'bar', braced: true },
|
|
35
|
+
{ value: 'baz', braced: true }
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
testStr('[foo][bar]', false, [
|
|
39
|
+
{ value: 'foo', braced: true },
|
|
40
|
+
{ value: 'bar', braced: true }
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
testStr('[foo]', false, [
|
|
44
|
+
{ value: 'foo', braced: true }
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('mixed bracketed and unbracketed references', () => {
|
|
49
|
+
testStr('foo[bar][baz]', false, [
|
|
50
|
+
{ value: 'foo', braced: false },
|
|
51
|
+
{ value: 'bar', braced: true },
|
|
52
|
+
{ value: 'baz', braced: true }
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
testStr('[foo]bar[baz]', false, [
|
|
56
|
+
{ value: 'foo', braced: true },
|
|
57
|
+
{ value: 'bar', braced: false },
|
|
58
|
+
{ value: 'baz', braced: true }
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
testStr('[foo][bar]baz', false, [
|
|
62
|
+
{ value: 'foo', braced: true },
|
|
63
|
+
{ value: 'bar', braced: true },
|
|
64
|
+
{ value: 'baz', braced: false }
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
testStr('foo[bar]baz', false, [
|
|
68
|
+
{ value: 'foo', braced: false },
|
|
69
|
+
{ value: 'bar', braced: true },
|
|
70
|
+
{ value: 'baz', braced: false }
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
testStr('[foo]bar', false, [
|
|
74
|
+
{ value: 'foo', braced: true },
|
|
75
|
+
{ value: 'bar', braced: false }
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
testStr('foo[bar]', false, [
|
|
79
|
+
{ value: 'foo', braced: false },
|
|
80
|
+
{ value: 'bar', braced: true }
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('unbracketed references', () => {
|
|
85
|
+
testStr('foo', false, [
|
|
86
|
+
{ value: 'foo', braced: false }
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
package/lib/parseRef.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FX_PREFIX,
|
|
3
|
+
CONTEXT,
|
|
4
|
+
CONTEXT_QUOTE,
|
|
5
|
+
REF_RANGE,
|
|
6
|
+
REF_TERNARY,
|
|
7
|
+
REF_NAMED,
|
|
8
|
+
REF_BEAM,
|
|
9
|
+
REF_STRUCT,
|
|
10
|
+
OPERATOR
|
|
11
|
+
} from './constants.ts';
|
|
12
|
+
import { lexersRefs } from './lexers/sets.ts';
|
|
13
|
+
import { getTokens } from './tokenize.ts';
|
|
14
|
+
import type { Token } from './types.ts';
|
|
15
|
+
|
|
16
|
+
// Liberally split a context string up into parts.
|
|
17
|
+
// Permits any combination of braced and unbraced items.
|
|
18
|
+
export function splitPrefix (str: string, stringsOnly = false) {
|
|
19
|
+
let inBrace = false;
|
|
20
|
+
let currStr = '';
|
|
21
|
+
const parts = [];
|
|
22
|
+
const flush = () => {
|
|
23
|
+
if (currStr) {
|
|
24
|
+
parts.push(
|
|
25
|
+
stringsOnly
|
|
26
|
+
? currStr
|
|
27
|
+
: { value: currStr, braced: inBrace }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
currStr = '';
|
|
31
|
+
};
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/prefer-for-of
|
|
33
|
+
for (let i = 0; i < str.length; i++) {
|
|
34
|
+
const char = str[i];
|
|
35
|
+
if (char === '[') {
|
|
36
|
+
flush();
|
|
37
|
+
inBrace = true;
|
|
38
|
+
}
|
|
39
|
+
else if (char === ']') {
|
|
40
|
+
flush();
|
|
41
|
+
inBrace = false;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
currStr += char;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
flush();
|
|
48
|
+
return parts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function splitContext (contextString, data, xlsx) {
|
|
52
|
+
const ctx = splitPrefix(contextString, !xlsx);
|
|
53
|
+
if (xlsx) {
|
|
54
|
+
if (ctx.length > 1) {
|
|
55
|
+
data.workbookName = ctx[ctx.length - 2].value;
|
|
56
|
+
data.sheetName = ctx[ctx.length - 1].value;
|
|
57
|
+
}
|
|
58
|
+
else if (ctx.length === 1) {
|
|
59
|
+
const item = ctx[0];
|
|
60
|
+
if (item.braced) {
|
|
61
|
+
data.workbookName = item.value;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
data.sheetName = item.value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
data.context = ctx;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type RefParseData = {
|
|
74
|
+
operator: string,
|
|
75
|
+
r0: string,
|
|
76
|
+
r1: string,
|
|
77
|
+
name: string,
|
|
78
|
+
struct: string,
|
|
79
|
+
};
|
|
80
|
+
type RefParserPart = (t: Token | undefined, data: Partial<RefParseData>, xlsx?: boolean) => 1 | undefined;
|
|
81
|
+
|
|
82
|
+
const unquote = d => d.slice(1, -1).replace(/''/g, "'");
|
|
83
|
+
|
|
84
|
+
const pRangeOp: RefParserPart = (t, data) => {
|
|
85
|
+
const value = t?.value;
|
|
86
|
+
if (value === ':' || value === '.:' || value === ':.' || value === '.:.') {
|
|
87
|
+
data.operator = value;
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const pRange: RefParserPart = (t, data) => {
|
|
92
|
+
if (t?.type === REF_RANGE) {
|
|
93
|
+
data.r0 = t.value;
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const pPartial: RefParserPart = (t, data) => {
|
|
98
|
+
if (t?.type === REF_TERNARY) {
|
|
99
|
+
data.r0 = t.value;
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const pRange2: RefParserPart = (t, data) => {
|
|
104
|
+
if (t?.type === REF_RANGE) {
|
|
105
|
+
data.r1 = t.value;
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const pBang: RefParserPart = t => {
|
|
110
|
+
if (t?.type === OPERATOR && t.value === '!') {
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const pBeam: RefParserPart = (t, data) => {
|
|
115
|
+
if (t?.type === REF_BEAM) {
|
|
116
|
+
data.r0 = t.value;
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const pStrucured: RefParserPart = (t, data) => {
|
|
121
|
+
if (t.type === REF_STRUCT) {
|
|
122
|
+
data.struct = t.value;
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const pContext: RefParserPart = (t, data, xlsx) => {
|
|
127
|
+
const type = t?.type;
|
|
128
|
+
if (type === CONTEXT) {
|
|
129
|
+
splitContext(t.value, data, xlsx);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
if (type === CONTEXT_QUOTE) {
|
|
133
|
+
splitContext(unquote(t.value), data, xlsx);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const pNamed: RefParserPart = (t, data) => {
|
|
138
|
+
if (t?.type === REF_NAMED) {
|
|
139
|
+
data.name = t.value;
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const validRuns = [
|
|
145
|
+
[ pPartial ],
|
|
146
|
+
[ pRange, pRangeOp, pRange2 ],
|
|
147
|
+
[ pRange ],
|
|
148
|
+
[ pBeam ],
|
|
149
|
+
[ pContext, pBang, pPartial ],
|
|
150
|
+
[ pContext, pBang, pRange, pRangeOp, pRange2 ],
|
|
151
|
+
[ pContext, pBang, pRange ],
|
|
152
|
+
[ pContext, pBang, pBeam ]
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const validRunsNamed = validRuns.concat([
|
|
156
|
+
[ pNamed ],
|
|
157
|
+
[ pContext, pBang, pNamed ],
|
|
158
|
+
[ pStrucured ],
|
|
159
|
+
[ pNamed, pStrucured ],
|
|
160
|
+
[ pContext, pBang, pNamed, pStrucured ]
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
type ParseRefOptions = {
|
|
164
|
+
withLocation?: boolean,
|
|
165
|
+
mergeRefs?: boolean,
|
|
166
|
+
allowTernary?: boolean,
|
|
167
|
+
allowNamed?: boolean,
|
|
168
|
+
r1c1?: boolean,
|
|
169
|
+
};
|
|
170
|
+
export type RefParseDataXls = RefParseData & { workbookName: string, sheetName: string };
|
|
171
|
+
export type RefParseDataCtx = RefParseData & { context: string[] };
|
|
172
|
+
|
|
173
|
+
export function parseRefCtx (ref: string, opts: ParseRefOptions = {}): RefParseDataCtx | null {
|
|
174
|
+
const options = {
|
|
175
|
+
withLocation: opts.withLocation ?? false,
|
|
176
|
+
mergeRefs: opts.mergeRefs ?? false,
|
|
177
|
+
allowTernary: opts.allowTernary ?? false,
|
|
178
|
+
allowNamed: opts.allowNamed ?? true,
|
|
179
|
+
r1c1: opts.r1c1 ?? false
|
|
180
|
+
};
|
|
181
|
+
const tokens = getTokens(ref, lexersRefs, options);
|
|
182
|
+
|
|
183
|
+
// discard the "="-prefix if it is there
|
|
184
|
+
if (tokens.length && tokens[0].type === FX_PREFIX) {
|
|
185
|
+
tokens.shift();
|
|
186
|
+
}
|
|
187
|
+
const runs = options.allowNamed ? validRunsNamed : validRuns;
|
|
188
|
+
for (const run of runs) {
|
|
189
|
+
// const len = run.length;
|
|
190
|
+
if (run.length === tokens.length) {
|
|
191
|
+
const data: RefParseDataCtx = {
|
|
192
|
+
context: [],
|
|
193
|
+
r0: '',
|
|
194
|
+
r1: '',
|
|
195
|
+
name: '',
|
|
196
|
+
struct: '',
|
|
197
|
+
operator: ''
|
|
198
|
+
};
|
|
199
|
+
const valid = run.every((parse, i) => parse(tokens[i], data, false));
|
|
200
|
+
if (valid) {
|
|
201
|
+
return data;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function parseRefXlsx (ref: string, opts: ParseRefOptions = {}): RefParseDataXls | null {
|
|
208
|
+
const options = {
|
|
209
|
+
withLocation: opts.withLocation ?? false,
|
|
210
|
+
mergeRefs: opts.mergeRefs ?? false,
|
|
211
|
+
allowTernary: opts.allowTernary ?? false,
|
|
212
|
+
allowNamed: opts.allowNamed ?? true,
|
|
213
|
+
r1c1: opts.r1c1 ?? false,
|
|
214
|
+
xlsx: true
|
|
215
|
+
};
|
|
216
|
+
const tokens = getTokens(ref, lexersRefs, options);
|
|
217
|
+
|
|
218
|
+
// discard the "="-prefix if it is there
|
|
219
|
+
if (tokens.length && tokens[0].type === FX_PREFIX) {
|
|
220
|
+
tokens.shift();
|
|
221
|
+
}
|
|
222
|
+
const runs = options.allowNamed ? validRunsNamed : validRuns;
|
|
223
|
+
for (const run of runs) {
|
|
224
|
+
if (run.length === tokens.length) {
|
|
225
|
+
const data: RefParseDataXls = {
|
|
226
|
+
workbookName: '',
|
|
227
|
+
sheetName: '',
|
|
228
|
+
r0: '',
|
|
229
|
+
r1: '',
|
|
230
|
+
name: '',
|
|
231
|
+
struct: '',
|
|
232
|
+
operator: ''
|
|
233
|
+
};
|
|
234
|
+
const valid = run.every((parse, i) => parse(tokens[i], data, true));
|
|
235
|
+
if (valid) {
|
|
236
|
+
return data as any;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
/* eslint-disable no-undefined */
|
|
3
|
-
import { isWS } from './lexers/lexWhitespace.js';
|
|
1
|
+
import { isWS } from './lexers/lexWhitespace.ts';
|
|
4
2
|
|
|
5
3
|
const AT = 64; // @
|
|
6
4
|
const BR_CLOSE = 93; // ]
|
|
@@ -20,7 +18,7 @@ const keyTerms = {
|
|
|
20
18
|
};
|
|
21
19
|
|
|
22
20
|
// only combinations allowed are: #data + (#headers | #totals | #data)
|
|
23
|
-
const fz = (...a) => Object.freeze(a);
|
|
21
|
+
const fz = (...a: string[]) => Object.freeze(a);
|
|
24
22
|
const sectionMap = {
|
|
25
23
|
// no terms
|
|
26
24
|
0: fz(),
|
|
@@ -36,7 +34,7 @@ const sectionMap = {
|
|
|
36
34
|
6: fz('data', 'totals')
|
|
37
35
|
};
|
|
38
36
|
|
|
39
|
-
function matchKeyword (str, pos) {
|
|
37
|
+
function matchKeyword (str: string, pos: number): number {
|
|
40
38
|
let p = pos;
|
|
41
39
|
if (str.charCodeAt(p++) !== BR_OPEN) {
|
|
42
40
|
return;
|
|
@@ -64,13 +62,13 @@ function matchKeyword (str, pos) {
|
|
|
64
62
|
return p - pos;
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
function skipWhitespace (str, pos) {
|
|
65
|
+
function skipWhitespace (str: string, pos: number): number {
|
|
68
66
|
let p = pos;
|
|
69
67
|
while (isWS(str.charCodeAt(p))) { p++; }
|
|
70
68
|
return p - pos;
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
function matchColumn (str, pos, allowUnbraced = true) {
|
|
71
|
+
function matchColumn (str: string, pos: number, allowUnbraced = true): [ string, string ] {
|
|
74
72
|
let p = pos;
|
|
75
73
|
let column = '';
|
|
76
74
|
if (str.charCodeAt(p) === BR_OPEN) {
|
|
@@ -125,8 +123,15 @@ function matchColumn (str, pos, allowUnbraced = true) {
|
|
|
125
123
|
}
|
|
126
124
|
}
|
|
127
125
|
|
|
128
|
-
export
|
|
129
|
-
|
|
126
|
+
export type SRange = {
|
|
127
|
+
columns: string[],
|
|
128
|
+
sections: string[],
|
|
129
|
+
length: number,
|
|
130
|
+
token: string
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export function parseSRange (str: string, pos: number = 0): SRange {
|
|
134
|
+
const columns: string[] = [];
|
|
130
135
|
const start = pos;
|
|
131
136
|
let m;
|
|
132
137
|
let terms = 0;
|
|
@@ -225,7 +230,7 @@ export function parseSRange (str, pos = 0) {
|
|
|
225
230
|
pos++;
|
|
226
231
|
}
|
|
227
232
|
|
|
228
|
-
const sections = sectionMap[terms];
|
|
233
|
+
const sections: string[] = sectionMap[terms];
|
|
229
234
|
return {
|
|
230
235
|
columns,
|
|
231
236
|
sections: sections ? sections.concat() : sections,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { parseStructRef, parseStructRefXlsx } from './parseStructRef.ts';
|
|
3
|
+
|
|
4
|
+
function isSREqual (expr: string, expected: any, opts?: any) {
|
|
5
|
+
if (expected) {
|
|
6
|
+
expected = opts?.xlsx
|
|
7
|
+
? {
|
|
8
|
+
workbookName: '',
|
|
9
|
+
sheetName: '',
|
|
10
|
+
table: '',
|
|
11
|
+
columns: [],
|
|
12
|
+
sections: [],
|
|
13
|
+
...expected
|
|
14
|
+
}
|
|
15
|
+
: {
|
|
16
|
+
context: [],
|
|
17
|
+
table: '',
|
|
18
|
+
columns: [],
|
|
19
|
+
sections: [],
|
|
20
|
+
...expected
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
expect(opts?.xlsx ? parseStructRefXlsx(expr) : parseStructRef(expr)).toEqual(expected);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('parse structured references', () => {
|
|
27
|
+
test('basic table and column references', () => {
|
|
28
|
+
isSREqual('table[col]', {
|
|
29
|
+
table: 'table',
|
|
30
|
+
columns: [ 'col' ]
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
isSREqual('table[]', {
|
|
34
|
+
table: 'table'
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('section references', () => {
|
|
39
|
+
isSREqual('[#All]', {
|
|
40
|
+
sections: [ 'all' ]
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('column name references', () => {
|
|
45
|
+
isSREqual('[column name]', {
|
|
46
|
+
columns: [ 'column name' ]
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
isSREqual('[[my column]]', {
|
|
50
|
+
columns: [ 'my column' ]
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('invalid references', () => {
|
|
55
|
+
isSREqual('[column name]!foo', undefined);
|
|
56
|
+
isSREqual('[foo]bar', undefined);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('column range references', () => {
|
|
60
|
+
isSREqual('[[my column]:otherColumn]', {
|
|
61
|
+
columns: [ 'my column', 'otherColumn' ]
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
isSREqual('[ [my column]:otherColumn ]', {
|
|
65
|
+
columns: [ 'my column', 'otherColumn ' ]
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
isSREqual('[ [my column]: otherColumn ]', {
|
|
69
|
+
columns: [ 'my column', ' otherColumn ' ]
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('this row references', () => {
|
|
74
|
+
isSREqual('[ @[ my column ]: otherColumn ]', {
|
|
75
|
+
columns: [ ' my column ', ' otherColumn ' ],
|
|
76
|
+
sections: [ 'this row' ]
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('section and column combinations', () => {
|
|
81
|
+
isSREqual('[[#Data], [my column]:otherColumn]', {
|
|
82
|
+
columns: [ 'my column', 'otherColumn' ],
|
|
83
|
+
sections: [ 'data' ]
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
isSREqual('[ [#Data], [my column]:[\'@foo] ]', {
|
|
87
|
+
columns: [ 'my column', '@foo' ],
|
|
88
|
+
sections: [ 'data' ]
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('context-qualified references', () => {
|
|
93
|
+
isSREqual('workbook.xlsx!tableName[ [#Data], [my column]:[\'@foo] ]', {
|
|
94
|
+
columns: [ 'my column', '@foo' ],
|
|
95
|
+
sections: [ 'data' ],
|
|
96
|
+
table: 'tableName',
|
|
97
|
+
context: [ 'workbook.xlsx' ]
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
isSREqual("'Sheet'!Table[Column]", {
|
|
101
|
+
columns: [ 'Column' ],
|
|
102
|
+
table: 'Table',
|
|
103
|
+
context: [ 'Sheet' ]
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
isSREqual("Sheet1!Table1[foo '[bar']]", {
|
|
107
|
+
columns: [ 'foo [bar]' ],
|
|
108
|
+
table: 'Table1',
|
|
109
|
+
context: [ 'Sheet1' ]
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
isSREqual('[myworkbook.xlsx]Sheet1!TMP8w0habhr[#All]', {
|
|
113
|
+
columns: [],
|
|
114
|
+
table: 'TMP8w0habhr',
|
|
115
|
+
context: [ 'myworkbook.xlsx', 'Sheet1' ],
|
|
116
|
+
sections: [ 'all' ]
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('duplicate section handling', () => {
|
|
121
|
+
isSREqual('[[#Data],[#data],[#Data],[#Data],[#Totals],[#Totals],[#Totals],foo]', {
|
|
122
|
+
columns: [ 'foo' ],
|
|
123
|
+
sections: [ 'data', 'totals' ]
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('structured references parse in xlsx mode', () => {
|
|
129
|
+
test('workbook-only references', () => {
|
|
130
|
+
isSREqual('[Workbook.xlsx]!Table[#Data]', {
|
|
131
|
+
workbookName: 'Workbook.xlsx',
|
|
132
|
+
table: 'Table',
|
|
133
|
+
sections: [ 'data' ]
|
|
134
|
+
}, { xlsx: true });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('workbook and sheet references', () => {
|
|
138
|
+
isSREqual('[Workbook.xlsx]Sheet1!Table[#Data]', {
|
|
139
|
+
workbookName: 'Workbook.xlsx',
|
|
140
|
+
sheetName: 'Sheet1',
|
|
141
|
+
table: 'Table',
|
|
142
|
+
sections: [ 'data' ]
|
|
143
|
+
}, { xlsx: true });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('sheet-only references', () => {
|
|
147
|
+
isSREqual('Sheet1!Table[#Data]', {
|
|
148
|
+
sheetName: 'Sheet1',
|
|
149
|
+
table: 'Table',
|
|
150
|
+
sections: [ 'data' ]
|
|
151
|
+
}, { xlsx: true });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('longform parse (in xlsx mode)', () => {
|
|
156
|
+
test('thisRow option should have no effect when parsing', () => {
|
|
157
|
+
const expectedResult = {
|
|
158
|
+
table: 'Table2',
|
|
159
|
+
columns: [ 'col1' ],
|
|
160
|
+
sections: [ 'this row' ]
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
isSREqual('Table2[[#This Row],[col1]]', expectedResult, { xlsx: true, thisRow: true });
|
|
164
|
+
isSREqual('Table2[[#This Row],[col1]]', expectedResult, { xlsx: true, thisRow: false });
|
|
165
|
+
isSREqual('Table2[[#This Row],[col1]]', expectedResult, { xlsx: false, thisRow: true });
|
|
166
|
+
isSREqual('Table2[[#This Row],[col1]]', expectedResult, { xlsx: false, thisRow: false });
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ReferenceStruct, ReferenceStructXlsx } from './types.ts';
|
|
2
|
+
import { parseRefCtx, parseRefXlsx } from './parseRef.ts';
|
|
3
|
+
import { parseSRange } from './parseSRange.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse a structured reference string into an object representing it.
|
|
7
|
+
*
|
|
8
|
+
* ```js
|
|
9
|
+
* parseStructRef('workbook.xlsx!tableName[[#Data],[Column1]:[Column2]]');
|
|
10
|
+
* // => {
|
|
11
|
+
* // context: [ 'workbook.xlsx' ],
|
|
12
|
+
* // sections: [ 'data' ],
|
|
13
|
+
* // columns: [ 'my column', '@foo' ],
|
|
14
|
+
* // table: 'tableName',
|
|
15
|
+
* // }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* For A:A or A1:A style ranges, `null` will be used for any dimensions that the
|
|
19
|
+
* syntax does not specify:
|
|
20
|
+
*
|
|
21
|
+
* See [References.md](./References.md).
|
|
22
|
+
*
|
|
23
|
+
* @param ref A structured reference string
|
|
24
|
+
* @returns An object representing a valid reference or `undefined` if it is invalid.
|
|
25
|
+
*/
|
|
26
|
+
export function parseStructRef (ref: string): ReferenceStruct | undefined {
|
|
27
|
+
const r = parseRefCtx(ref);
|
|
28
|
+
if (r && r.struct) {
|
|
29
|
+
const structData = parseSRange(r.struct);
|
|
30
|
+
if (structData && structData.length === r.struct.length) {
|
|
31
|
+
return {
|
|
32
|
+
context: r.context,
|
|
33
|
+
table: r.name,
|
|
34
|
+
columns: structData.columns,
|
|
35
|
+
sections: structData.sections
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a structured reference string into an object representing it.
|
|
43
|
+
*
|
|
44
|
+
* ```js
|
|
45
|
+
* parseStructRef('[workbook.xlsx]!tableName[[#Data],[Column1]:[Column2]]');
|
|
46
|
+
* // => {
|
|
47
|
+
* // workbookName: 'workbook.xlsx',
|
|
48
|
+
* // sections: [ 'data' ],
|
|
49
|
+
* // columns: [ 'my column', '@foo' ],
|
|
50
|
+
* // table: 'tableName',
|
|
51
|
+
* // }
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* For A:A or A1:A style ranges, `null` will be used for any dimensions that the
|
|
55
|
+
* syntax does not specify:
|
|
56
|
+
*
|
|
57
|
+
* See [References.md](./References.md).
|
|
58
|
+
*
|
|
59
|
+
* @param ref A structured reference string
|
|
60
|
+
* @returns An object representing a valid reference or null if it is invalid.
|
|
61
|
+
*/
|
|
62
|
+
export function parseStructRefXlsx (ref: string): ReferenceStructXlsx | undefined {
|
|
63
|
+
const r = parseRefXlsx(ref);
|
|
64
|
+
if (r && r.struct) {
|
|
65
|
+
const structData = parseSRange(r.struct);
|
|
66
|
+
if (structData && structData.length === r.struct.length) {
|
|
67
|
+
return {
|
|
68
|
+
workbookName: r.workbookName,
|
|
69
|
+
sheetName: r.sheetName,
|
|
70
|
+
table: r.name,
|
|
71
|
+
columns: structData.columns,
|
|
72
|
+
sections: structData.sections
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|