@borgar/fx 4.12.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.ts +18 -0
- package/lib/lexers/canEndRange.ts +25 -0
- package/lib/lexers/lexBoolean.ts +55 -0
- package/lib/lexers/lexContext.ts +104 -0
- package/lib/lexers/lexError.ts +15 -0
- package/lib/lexers/lexFunction.ts +37 -0
- package/lib/lexers/lexNameFuncCntx.ts +112 -0
- package/lib/lexers/lexNamed.ts +60 -0
- package/lib/lexers/lexNewLine.ts +12 -0
- package/lib/lexers/lexNumber.ts +48 -0
- package/lib/lexers/lexOperator.ts +26 -0
- package/lib/lexers/lexRange.ts +15 -0
- package/lib/lexers/lexRangeA1.ts +134 -0
- package/lib/lexers/lexRangeR1C1.ts +146 -0
- package/lib/lexers/lexRangeTrim.ts +26 -0
- package/lib/lexers/lexRefOp.ts +19 -0
- package/lib/lexers/lexString.ts +22 -0
- package/lib/lexers/lexStructured.ts +25 -0
- package/lib/lexers/lexWhitespace.ts +31 -0
- package/lib/lexers/sets.ts +51 -0
- package/lib/mergeRefTokens.spec.ts +141 -0
- package/lib/{mergeRefTokens.js → mergeRefTokens.ts} +47 -32
- 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.ts +240 -0
- 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 +46 -30
- package/tsconfig.json +28 -0
- package/typedoc-ignore-links.ts +17 -0
- package/typedoc.json +41 -0
- package/.eslintrc +0 -22
- 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 -170
- 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 -283
- package/lib/lexer.spec.js +0 -1953
- package/lib/lexerParts.js +0 -228
- 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/parseSRange.js +0 -167
- 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
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { isWS } from './lexers/lexWhitespace.ts';
|
|
2
|
+
|
|
3
|
+
const AT = 64; // @
|
|
4
|
+
const BR_CLOSE = 93; // ]
|
|
5
|
+
const BR_OPEN = 91; // [
|
|
6
|
+
const COLON = 58; // :
|
|
7
|
+
const COMMA = 44; // ,
|
|
8
|
+
const HASH = 35; // #
|
|
9
|
+
const QUOT_SINGLE = 39; // '
|
|
10
|
+
|
|
11
|
+
const keyTerms = {
|
|
12
|
+
'headers': 1,
|
|
13
|
+
'data': 2,
|
|
14
|
+
'totals': 4,
|
|
15
|
+
'all': 8,
|
|
16
|
+
'this row': 16,
|
|
17
|
+
'@': 16
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// only combinations allowed are: #data + (#headers | #totals | #data)
|
|
21
|
+
const fz = (...a: string[]) => Object.freeze(a);
|
|
22
|
+
const sectionMap = {
|
|
23
|
+
// no terms
|
|
24
|
+
0: fz(),
|
|
25
|
+
// single term
|
|
26
|
+
1: fz('headers'),
|
|
27
|
+
2: fz('data'),
|
|
28
|
+
4: fz('totals'),
|
|
29
|
+
8: fz('all'),
|
|
30
|
+
16: fz('this row'),
|
|
31
|
+
// headers+data
|
|
32
|
+
3: fz('headers', 'data'),
|
|
33
|
+
// totals+data
|
|
34
|
+
6: fz('data', 'totals')
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function matchKeyword (str: string, pos: number): number {
|
|
38
|
+
let p = pos;
|
|
39
|
+
if (str.charCodeAt(p++) !== BR_OPEN) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (str.charCodeAt(p++) !== HASH) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
do {
|
|
46
|
+
const c = str.charCodeAt(p);
|
|
47
|
+
if (
|
|
48
|
+
(c >= 65 && c <= 90) || // A-Z
|
|
49
|
+
(c >= 97 && c <= 122) || // a-z
|
|
50
|
+
(c === 32) // space
|
|
51
|
+
) {
|
|
52
|
+
p++;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
while (p < pos + 11); // max length: '[#this row'
|
|
59
|
+
if (str.charCodeAt(p++) !== BR_CLOSE) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
return p - pos;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function skipWhitespace (str: string, pos: number): number {
|
|
66
|
+
let p = pos;
|
|
67
|
+
while (isWS(str.charCodeAt(p))) { p++; }
|
|
68
|
+
return p - pos;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function matchColumn (str: string, pos: number, allowUnbraced = true): [ string, string ] {
|
|
72
|
+
let p = pos;
|
|
73
|
+
let column = '';
|
|
74
|
+
if (str.charCodeAt(p) === BR_OPEN) {
|
|
75
|
+
p++;
|
|
76
|
+
let c;
|
|
77
|
+
do {
|
|
78
|
+
c = str.charCodeAt(p);
|
|
79
|
+
if (c === QUOT_SINGLE) {
|
|
80
|
+
p++;
|
|
81
|
+
c = str.charCodeAt(p);
|
|
82
|
+
// Allowed set: '#@[]
|
|
83
|
+
if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN || c === BR_CLOSE) {
|
|
84
|
+
column += String.fromCharCode(c);
|
|
85
|
+
p++;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Allowed set is all chars BUT: '#@[]
|
|
92
|
+
else if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
else if (c === BR_CLOSE) {
|
|
96
|
+
p++;
|
|
97
|
+
return [ str.slice(pos, p), column ];
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
column += String.fromCharCode(c);
|
|
101
|
+
p++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
while (p < str.length);
|
|
105
|
+
}
|
|
106
|
+
else if (allowUnbraced) {
|
|
107
|
+
let c;
|
|
108
|
+
do {
|
|
109
|
+
c = str.charCodeAt(p);
|
|
110
|
+
// Allowed set is all chars BUT: '#@[]:
|
|
111
|
+
if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN || c === BR_CLOSE || c === COLON) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
column += String.fromCharCode(c);
|
|
116
|
+
p++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
while (p < str.length);
|
|
120
|
+
if (p !== pos) {
|
|
121
|
+
return [ column, column ];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
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[] = [];
|
|
135
|
+
const start = pos;
|
|
136
|
+
let m;
|
|
137
|
+
let terms = 0;
|
|
138
|
+
|
|
139
|
+
// structured refs start with a [
|
|
140
|
+
if (str.charCodeAt(pos) !== BR_OPEN) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// simple keyword: [#keyword]
|
|
145
|
+
if ((m = matchKeyword(str, pos))) {
|
|
146
|
+
const k = str.slice(pos + 2, pos + m - 1);
|
|
147
|
+
pos += m;
|
|
148
|
+
const term = keyTerms[k.toLowerCase()];
|
|
149
|
+
if (!term) { return; }
|
|
150
|
+
terms |= term;
|
|
151
|
+
}
|
|
152
|
+
// simple column: [column]
|
|
153
|
+
else if ((m = matchColumn(str, pos, false))) {
|
|
154
|
+
pos += m[0].length;
|
|
155
|
+
if (m[1]) {
|
|
156
|
+
columns.push(m[1]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// use the "normal" method
|
|
160
|
+
// [[#keyword]]
|
|
161
|
+
// [[column]]
|
|
162
|
+
// [@]
|
|
163
|
+
// [@column]
|
|
164
|
+
// [@[column]]
|
|
165
|
+
// [@column:column]
|
|
166
|
+
// [@column:[column]]
|
|
167
|
+
// [@[column]:column]
|
|
168
|
+
// [@[column]:[column]]
|
|
169
|
+
// [column:column]
|
|
170
|
+
// [column:[column]]
|
|
171
|
+
// [[column]:column]
|
|
172
|
+
// [[column]:[column]]
|
|
173
|
+
// [[#keyword],column]
|
|
174
|
+
// [[#keyword],column:column]
|
|
175
|
+
// [[#keyword],[#keyword],column:column]
|
|
176
|
+
// ...
|
|
177
|
+
else {
|
|
178
|
+
let expect_more = true;
|
|
179
|
+
pos++; // skip open brace
|
|
180
|
+
pos += skipWhitespace(str, pos);
|
|
181
|
+
// match keywords as we find them
|
|
182
|
+
while (expect_more && (m = matchKeyword(str, pos))) {
|
|
183
|
+
const k = str.slice(pos + 2, pos + m - 1);
|
|
184
|
+
const term = keyTerms[k.toLowerCase()];
|
|
185
|
+
if (!term) { return; }
|
|
186
|
+
terms |= term;
|
|
187
|
+
pos += m;
|
|
188
|
+
pos += skipWhitespace(str, pos);
|
|
189
|
+
expect_more = str.charCodeAt(pos) === COMMA;
|
|
190
|
+
if (expect_more) {
|
|
191
|
+
pos++;
|
|
192
|
+
pos += skipWhitespace(str, pos);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// is there an @ specifier?
|
|
196
|
+
if (expect_more && (str.charCodeAt(pos) === AT)) {
|
|
197
|
+
terms |= keyTerms['@'];
|
|
198
|
+
pos += 1;
|
|
199
|
+
expect_more = str.charCodeAt(pos) !== BR_CLOSE;
|
|
200
|
+
}
|
|
201
|
+
// not all keyword terms may be combined
|
|
202
|
+
if (!sectionMap[terms]) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// column definitions
|
|
206
|
+
const leftCol = expect_more && matchColumn(str, pos, true);
|
|
207
|
+
if (leftCol) {
|
|
208
|
+
pos += leftCol[0].length;
|
|
209
|
+
columns.push(leftCol[1]);
|
|
210
|
+
if (str.charCodeAt(pos) === COLON) {
|
|
211
|
+
pos++;
|
|
212
|
+
const rightCol = matchColumn(str, pos, true);
|
|
213
|
+
if (rightCol) {
|
|
214
|
+
pos += rightCol[0].length;
|
|
215
|
+
columns.push(rightCol[1]);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
expect_more = false;
|
|
222
|
+
}
|
|
223
|
+
// advance ws
|
|
224
|
+
pos += skipWhitespace(str, pos);
|
|
225
|
+
// close the ref
|
|
226
|
+
if (expect_more || str.charCodeAt(pos) !== BR_CLOSE) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// step over the closing ]
|
|
230
|
+
pos++;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const sections: string[] = sectionMap[terms];
|
|
234
|
+
return {
|
|
235
|
+
columns,
|
|
236
|
+
sections: sections ? sections.concat() : sections,
|
|
237
|
+
length: pos - start,
|
|
238
|
+
token: str.slice(start, pos)
|
|
239
|
+
};
|
|
240
|
+
}
|