@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.
Files changed (139) hide show
  1. package/dist/index-BMr6cTgc.d.cts +1444 -0
  2. package/dist/index-BMr6cTgc.d.ts +1444 -0
  3. package/dist/index.cjs +3054 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +1 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +2984 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/xlsx/index.cjs +3120 -0
  10. package/dist/xlsx/index.cjs.map +1 -0
  11. package/dist/xlsx/index.d.cts +55 -0
  12. package/dist/xlsx/index.d.ts +55 -0
  13. package/dist/xlsx/index.js +3049 -0
  14. package/dist/xlsx/index.js.map +1 -0
  15. package/docs/API.md +2959 -718
  16. package/docs/AST_format.md +2 -2
  17. package/eslint.config.mjs +40 -0
  18. package/lib/a1.spec.ts +32 -0
  19. package/lib/a1.ts +26 -0
  20. package/lib/addA1RangeBounds.ts +50 -0
  21. package/lib/addTokenMeta.spec.ts +166 -0
  22. package/lib/{addTokenMeta.js → addTokenMeta.ts} +53 -33
  23. package/lib/astTypes.ts +211 -0
  24. package/lib/cloneToken.ts +29 -0
  25. package/lib/{constants.js → constants.ts} +6 -3
  26. package/lib/fixRanges.spec.ts +220 -0
  27. package/lib/fixRanges.ts +260 -0
  28. package/lib/fromCol.spec.ts +15 -0
  29. package/lib/{fromCol.js → fromCol.ts} +1 -1
  30. package/lib/index.spec.ts +119 -0
  31. package/lib/index.ts +76 -0
  32. package/lib/isNodeType.ts +151 -0
  33. package/lib/isType.spec.ts +208 -0
  34. package/lib/{isType.js → isType.ts} +26 -25
  35. package/lib/lexers/advRangeOp.ts +18 -0
  36. package/lib/lexers/canEndRange.ts +25 -0
  37. package/lib/lexers/lexBoolean.ts +55 -0
  38. package/lib/lexers/lexContext.ts +104 -0
  39. package/lib/lexers/lexError.ts +15 -0
  40. package/lib/lexers/lexFunction.ts +37 -0
  41. package/lib/lexers/lexNameFuncCntx.ts +112 -0
  42. package/lib/lexers/lexNamed.ts +60 -0
  43. package/lib/lexers/lexNewLine.ts +12 -0
  44. package/lib/lexers/lexNumber.ts +48 -0
  45. package/lib/lexers/lexOperator.ts +26 -0
  46. package/lib/lexers/lexRange.ts +15 -0
  47. package/lib/lexers/lexRangeA1.ts +134 -0
  48. package/lib/lexers/lexRangeR1C1.ts +146 -0
  49. package/lib/lexers/lexRangeTrim.ts +26 -0
  50. package/lib/lexers/lexRefOp.ts +19 -0
  51. package/lib/lexers/lexString.ts +22 -0
  52. package/lib/lexers/lexStructured.ts +25 -0
  53. package/lib/lexers/lexWhitespace.ts +31 -0
  54. package/lib/lexers/sets.ts +51 -0
  55. package/lib/mergeRefTokens.spec.ts +141 -0
  56. package/lib/{mergeRefTokens.js → mergeRefTokens.ts} +47 -32
  57. package/lib/nodeTypes.ts +54 -0
  58. package/lib/parse.spec.ts +1410 -0
  59. package/lib/{parser.js → parse.ts} +81 -63
  60. package/lib/parseA1Range.spec.ts +233 -0
  61. package/lib/parseA1Range.ts +206 -0
  62. package/lib/parseA1Ref.spec.ts +337 -0
  63. package/lib/parseA1Ref.ts +115 -0
  64. package/lib/parseR1C1Range.ts +191 -0
  65. package/lib/parseR1C1Ref.spec.ts +323 -0
  66. package/lib/parseR1C1Ref.ts +127 -0
  67. package/lib/parseRef.spec.ts +90 -0
  68. package/lib/parseRef.ts +240 -0
  69. package/lib/parseSRange.ts +240 -0
  70. package/lib/parseStructRef.spec.ts +168 -0
  71. package/lib/parseStructRef.ts +76 -0
  72. package/lib/stringifyA1Range.spec.ts +72 -0
  73. package/lib/stringifyA1Range.ts +72 -0
  74. package/lib/stringifyA1Ref.spec.ts +64 -0
  75. package/lib/stringifyA1Ref.ts +59 -0
  76. package/lib/{stringifyPrefix.js → stringifyPrefix.ts} +17 -2
  77. package/lib/stringifyR1C1Range.spec.ts +92 -0
  78. package/lib/stringifyR1C1Range.ts +73 -0
  79. package/lib/stringifyR1C1Ref.spec.ts +63 -0
  80. package/lib/stringifyR1C1Ref.ts +67 -0
  81. package/lib/stringifyStructRef.spec.ts +124 -0
  82. package/lib/stringifyStructRef.ts +113 -0
  83. package/lib/stringifyTokens.ts +15 -0
  84. package/lib/toCol.spec.ts +11 -0
  85. package/lib/{toCol.js → toCol.ts} +4 -4
  86. package/lib/tokenTypes.ts +76 -0
  87. package/lib/tokenize-srefs.spec.ts +429 -0
  88. package/lib/tokenize.spec.ts +2103 -0
  89. package/lib/tokenize.ts +346 -0
  90. package/lib/translate.spec.ts +35 -0
  91. package/lib/translateToA1.spec.ts +247 -0
  92. package/lib/translateToA1.ts +231 -0
  93. package/lib/translateToR1C1.spec.ts +227 -0
  94. package/lib/translateToR1C1.ts +145 -0
  95. package/lib/types.ts +179 -0
  96. package/lib/xlsx/index.spec.ts +27 -0
  97. package/lib/xlsx/index.ts +32 -0
  98. package/package.json +46 -30
  99. package/tsconfig.json +28 -0
  100. package/typedoc-ignore-links.ts +17 -0
  101. package/typedoc.json +41 -0
  102. package/.eslintrc +0 -22
  103. package/dist/fx.d.ts +0 -823
  104. package/dist/fx.js +0 -2
  105. package/dist/package.json +0 -1
  106. package/lib/a1.js +0 -348
  107. package/lib/a1.spec.js +0 -458
  108. package/lib/addTokenMeta.spec.js +0 -153
  109. package/lib/astTypes.js +0 -96
  110. package/lib/extraTypes.js +0 -74
  111. package/lib/fixRanges.js +0 -104
  112. package/lib/fixRanges.spec.js +0 -170
  113. package/lib/fromCol.spec.js +0 -11
  114. package/lib/index.js +0 -134
  115. package/lib/index.spec.js +0 -67
  116. package/lib/isType.spec.js +0 -168
  117. package/lib/lexer-srefs.spec.js +0 -324
  118. package/lib/lexer.js +0 -283
  119. package/lib/lexer.spec.js +0 -1953
  120. package/lib/lexerParts.js +0 -228
  121. package/lib/mergeRefTokens.spec.js +0 -121
  122. package/lib/package.json +0 -1
  123. package/lib/parseRef.js +0 -157
  124. package/lib/parseRef.spec.js +0 -71
  125. package/lib/parseSRange.js +0 -167
  126. package/lib/parseStructRef.js +0 -48
  127. package/lib/parseStructRef.spec.js +0 -164
  128. package/lib/parser.spec.js +0 -1208
  129. package/lib/rc.js +0 -341
  130. package/lib/rc.spec.js +0 -403
  131. package/lib/stringifyStructRef.js +0 -80
  132. package/lib/stringifyStructRef.spec.js +0 -182
  133. package/lib/toCol.spec.js +0 -11
  134. package/lib/translate-toA1.spec.js +0 -214
  135. package/lib/translate-toRC.spec.js +0 -197
  136. package/lib/translate.js +0 -239
  137. package/lib/translate.spec.js +0 -21
  138. package/rollup.config.mjs +0 -22
  139. package/tsd.json +0 -12
@@ -0,0 +1,1410 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { parse } from './parse.ts';
3
+ import { tokenize } from './tokenize.ts';
4
+
5
+ function isParsed (expr: string, expected: any, opts?: any) {
6
+ const result = parse(tokenize(expr, { ...opts, allowTernary: true }), opts);
7
+ const cleaned = JSON.parse(JSON.stringify(result));
8
+ expect(cleaned).toEqual(expected);
9
+ }
10
+
11
+ function isInvalidExpr (expr: string, opts?: any) {
12
+ expect(() => parse(tokenize(expr, { allowTernary: true }), { allowTernary: true, ...opts })).toThrow();
13
+ }
14
+
15
+ describe('parser', () => {
16
+ describe('parse numbers', () => {
17
+ test('basic number parsing', () => {
18
+ isParsed('1', { type: 'Literal', value: 1, raw: '1' });
19
+ isParsed('-1', { type: 'Literal', value: -1, raw: '-1' });
20
+ isParsed('2.4e+3', { type: 'Literal', value: 2400, raw: '2.4e+3' });
21
+ isParsed('-1e+10', { type: 'Literal', value: -10000000000, raw: '-1e+10' });
22
+ isParsed('1e-3', { type: 'Literal', value: 0.001, raw: '1e-3' });
23
+ });
24
+ });
25
+
26
+ describe('parse booleans', () => {
27
+ test('true values with different cases', () => {
28
+ isParsed('TRUE', { type: 'Literal', value: true, raw: 'TRUE' });
29
+ isParsed('true', { type: 'Literal', value: true, raw: 'true' });
30
+ isParsed('trUe', { type: 'Literal', value: true, raw: 'trUe' });
31
+ isParsed('TRue', { type: 'Literal', value: true, raw: 'TRue' });
32
+ });
33
+
34
+ test('false values with different cases', () => {
35
+ isParsed('FALSE', { type: 'Literal', value: false, raw: 'FALSE' });
36
+ isParsed('false', { type: 'Literal', value: false, raw: 'false' });
37
+ isParsed('False', { type: 'Literal', value: false, raw: 'False' });
38
+ isParsed('fAlSe', { type: 'Literal', value: false, raw: 'fAlSe' });
39
+ });
40
+ });
41
+
42
+ describe('parse strings', () => {
43
+ test('string literal parsing', () => {
44
+ isParsed('""', { type: 'Literal', value: '', raw: '""' });
45
+ isParsed('""""', { type: 'Literal', value: '"', raw: '""""' });
46
+ isParsed('" "', { type: 'Literal', value: ' ', raw: '" "' });
47
+ isParsed('"foobar"', { type: 'Literal', value: 'foobar', raw: '"foobar"' });
48
+ isParsed('"foo bar"', { type: 'Literal', value: 'foo bar', raw: '"foo bar"' });
49
+ isParsed('"foo""bar"', { type: 'Literal', value: 'foo"bar', raw: '"foo""bar"' });
50
+ });
51
+ });
52
+
53
+ describe('parse errors', () => {
54
+ test('error literal parsing', () => {
55
+ isParsed('#CALC!', { type: 'ErrorLiteral', value: '#CALC!', raw: '#CALC!' });
56
+ isParsed('#DIV/0!', { type: 'ErrorLiteral', value: '#DIV/0!', raw: '#DIV/0!' });
57
+ isParsed('#FIELD!', { type: 'ErrorLiteral', value: '#FIELD!', raw: '#FIELD!' });
58
+ isParsed('#GETTING_DATA', { type: 'ErrorLiteral', value: '#GETTING_DATA', raw: '#GETTING_DATA' });
59
+ isParsed('#N/A', { type: 'ErrorLiteral', value: '#N/A', raw: '#N/A' });
60
+ isParsed('#NAME?', { type: 'ErrorLiteral', value: '#NAME?', raw: '#NAME?' });
61
+ isParsed('#NULL!', { type: 'ErrorLiteral', value: '#NULL!', raw: '#NULL!' });
62
+ isParsed('#NUM!', { type: 'ErrorLiteral', value: '#NUM!', raw: '#NUM!' });
63
+ isParsed('#REF!', { type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' });
64
+ isParsed('#SPILL!', { type: 'ErrorLiteral', value: '#SPILL!', raw: '#SPILL!' });
65
+ isParsed('#SYNTAX?', { type: 'ErrorLiteral', value: '#SYNTAX?', raw: '#SYNTAX?' });
66
+ isParsed('#UNKNOWN!', { type: 'ErrorLiteral', value: '#UNKNOWN!', raw: '#UNKNOWN!' });
67
+ isParsed('#VALUE!', { type: 'ErrorLiteral', value: '#VALUE!', raw: '#VALUE!' });
68
+ });
69
+ });
70
+
71
+ describe('parse ranges', () => {
72
+ test('basic range references', () => {
73
+ isParsed('A1', { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' });
74
+ isParsed('A1:B2', { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' });
75
+ isParsed('A:B', { type: 'ReferenceIdentifier', value: 'A:B', kind: 'beam' });
76
+ isParsed('1:2', { type: 'ReferenceIdentifier', value: '1:2', kind: 'beam' });
77
+ isParsed('A1:2', { type: 'ReferenceIdentifier', value: 'A1:2', kind: 'range' });
78
+ isParsed('1:A2', { type: 'ReferenceIdentifier', value: '1:A2', kind: 'range' });
79
+ isParsed('A1.:.B2', { type: 'ReferenceIdentifier', value: 'A1.:.B2', kind: 'range' });
80
+ });
81
+
82
+ test('sheet qualified references', () => {
83
+ isParsed('Sheet!A1', { type: 'ReferenceIdentifier', value: 'Sheet!A1', kind: 'range' });
84
+ isParsed('[Workbook]Sheet!A1', { type: 'ReferenceIdentifier', value: '[Workbook]Sheet!A1', kind: 'range' });
85
+ isParsed('\'Sheet\'!A1', { type: 'ReferenceIdentifier', value: '\'Sheet\'!A1', kind: 'range' });
86
+ isParsed('\'[Workbook]Sheet\'!A1', { type: 'ReferenceIdentifier', value: '\'[Workbook]Sheet\'!A1', kind: 'range' });
87
+ isParsed('\'Workbook\'!A1', { type: 'ReferenceIdentifier', value: '\'Workbook\'!A1', kind: 'range' });
88
+ isParsed('\'[Workbook]Sheet\'!A1', { type: 'ReferenceIdentifier', value: '\'[Workbook]Sheet\'!A1', kind: 'range' });
89
+ });
90
+
91
+ test('named references', () => {
92
+ isParsed('foo', { type: 'ReferenceIdentifier', value: 'foo', kind: 'name' });
93
+ isParsed('Workbook!foo', { type: 'ReferenceIdentifier', value: 'Workbook!foo', kind: 'name' });
94
+ isParsed('[Workbook]Sheet!foo', { type: 'ReferenceIdentifier', value: '[Workbook]Sheet!foo', kind: 'name' });
95
+ });
96
+ });
97
+
98
+ describe('parse array literals', () => {
99
+ test('single element arrays', () => {
100
+ isParsed('{1}', {
101
+ type: 'ArrayExpression',
102
+ elements: [ [
103
+ { type: 'Literal', value: 1, raw: '1' }
104
+ ] ]
105
+ });
106
+
107
+ isParsed('{-1}', {
108
+ type: 'ArrayExpression',
109
+ elements: [ [
110
+ { type: 'Literal', value: -1, raw: '-1' }
111
+ ] ]
112
+ });
113
+
114
+ isParsed('{#DIV/0!}', {
115
+ type: 'ArrayExpression',
116
+ elements: [ [
117
+ { type: 'ErrorLiteral', value: '#DIV/0!', raw: '#DIV/0!' }
118
+ ] ]
119
+ });
120
+
121
+ isParsed('{TRUE}', {
122
+ type: 'ArrayExpression',
123
+ elements: [ [
124
+ { type: 'Literal', value: true, raw: 'TRUE' }
125
+ ] ]
126
+ });
127
+
128
+ isParsed('{"foo"}', {
129
+ type: 'ArrayExpression',
130
+ elements: [ [
131
+ { type: 'Literal', value: 'foo', raw: '"foo"' }
132
+ ] ]
133
+ });
134
+ });
135
+
136
+ test('multi-element arrays', () => {
137
+ isParsed('{1,2}', {
138
+ type: 'ArrayExpression',
139
+ elements: [ [
140
+ { type: 'Literal', value: 1, raw: '1' },
141
+ { type: 'Literal', value: 2, raw: '2' }
142
+ ] ]
143
+ });
144
+
145
+ isParsed('{1,2;3}', {
146
+ type: 'ArrayExpression',
147
+ elements: [ [
148
+ { type: 'Literal', value: 1, raw: '1' },
149
+ { type: 'Literal', value: 2, raw: '2' }
150
+ ], [
151
+ { type: 'Literal', value: 3, raw: '3' }
152
+ ] ]
153
+ });
154
+
155
+ isParsed('{1,2;3,4}', {
156
+ type: 'ArrayExpression',
157
+ elements: [ [
158
+ { type: 'Literal', value: 1, raw: '1' },
159
+ { type: 'Literal', value: 2, raw: '2' }
160
+ ], [
161
+ { type: 'Literal', value: 3, raw: '3' },
162
+ { type: 'Literal', value: 4, raw: '4' }
163
+ ] ]
164
+ });
165
+
166
+ isParsed('{1;2}', {
167
+ type: 'ArrayExpression',
168
+ elements: [ [
169
+ { type: 'Literal', value: 1, raw: '1' }
170
+ ], [
171
+ { type: 'Literal', value: 2, raw: '2' }
172
+ ] ]
173
+ });
174
+
175
+ isParsed('{1;2,3}', {
176
+ type: 'ArrayExpression',
177
+ elements: [ [
178
+ { type: 'Literal', value: 1, raw: '1' }
179
+ ], [
180
+ { type: 'Literal', value: 2, raw: '2' },
181
+ { type: 'Literal', value: 3, raw: '3' }
182
+ ] ]
183
+ });
184
+ });
185
+
186
+ test('arrays with ranges', () => {
187
+ isParsed('{A1,A1:B2;A:A}', {
188
+ type: 'ArrayExpression',
189
+ elements: [ [
190
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
191
+ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' }
192
+ ], [
193
+ { type: 'ReferenceIdentifier', value: 'A:A', kind: 'beam' }
194
+ ] ]
195
+ }, { permitArrayRanges: true });
196
+ });
197
+
198
+ test('mixed type arrays', () => {
199
+ isParsed('{-0.1,"foo";#NAME?,false}', {
200
+ type: 'ArrayExpression',
201
+ elements: [ [
202
+ { type: 'Literal', value: -0.1, raw: '-0.1' },
203
+ { type: 'Literal', value: 'foo', raw: '"foo"' }
204
+ ], [
205
+ { type: 'ErrorLiteral', value: '#NAME?', raw: '#NAME?' },
206
+ { type: 'Literal', value: false, raw: 'false' }
207
+ ] ]
208
+ });
209
+ });
210
+
211
+ test('invalid array expressions', () => {
212
+ isInvalidExpr('{A1}', { permitArrayRanges: false });
213
+ isInvalidExpr('{--1}', { negativeNumbers: true });
214
+ isInvalidExpr('{--1}', { negativeNumbers: false });
215
+ isInvalidExpr('{---1}', { negativeNumbers: true });
216
+ isInvalidExpr('{---1}', { negativeNumbers: false });
217
+ isInvalidExpr('{+1}'); // Excel silently corrects this 🤔
218
+ isInvalidExpr('{(1)}');
219
+ isInvalidExpr('{SUM(1)}');
220
+ isInvalidExpr('{{}}');
221
+ isInvalidExpr('{{}');
222
+ isInvalidExpr('{}}');
223
+ isInvalidExpr('{2+2}');
224
+ isInvalidExpr('{}');
225
+ isInvalidExpr('{,}');
226
+ isInvalidExpr('{1,}');
227
+ isInvalidExpr('{,1}');
228
+ isInvalidExpr('{;}');
229
+ });
230
+
231
+ test('array expressions with function calls', () => {
232
+ isParsed('={1234; UNIQUE(A:A)}',
233
+ { type: 'ArrayExpression',
234
+ elements: [
235
+ [ { type: 'Literal', value: 1234, raw: '1234' } ],
236
+ [ { type: 'CallExpression',
237
+ callee: { type: 'Identifier', name: 'UNIQUE' },
238
+ arguments: [
239
+ { type: 'ReferenceIdentifier', value: 'A:A', kind: 'beam' }
240
+ ] } ]
241
+ ] },
242
+ { permitArrayCalls: true });
243
+
244
+ isParsed('={SUM({1,2}),3}',
245
+ { type: 'ArrayExpression',
246
+ elements: [
247
+ [ { type: 'CallExpression',
248
+ callee: { type: 'Identifier', name: 'SUM' },
249
+ arguments: [
250
+ { type: 'ArrayExpression',
251
+ elements: [ [
252
+ { type: 'Literal', value: 1, raw: '1' },
253
+ { type: 'Literal', value: 2, raw: '2' }
254
+ ] ] }
255
+ ] },
256
+ { type: 'Literal', value: 3, raw: '3' } ]
257
+ ] },
258
+ { permitArrayCalls: true });
259
+ });
260
+ });
261
+
262
+ describe('parse function calls', () => {
263
+ test('basic function calls', () => {
264
+ isParsed('=foo()', {
265
+ type: 'CallExpression',
266
+ callee: { type: 'Identifier', name: 'foo' },
267
+ arguments: []
268
+ });
269
+
270
+ isParsed('=FOO()', {
271
+ type: 'CallExpression',
272
+ callee: { type: 'Identifier', name: 'FOO' },
273
+ arguments: []
274
+ });
275
+
276
+ isParsed('=FOO(1)', {
277
+ type: 'CallExpression',
278
+ callee: { type: 'Identifier', name: 'FOO' },
279
+ arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
280
+ });
281
+
282
+ isParsed('=FOO(1,2)', {
283
+ type: 'CallExpression',
284
+ callee: { type: 'Identifier', name: 'FOO' },
285
+ arguments: [
286
+ { type: 'Literal', value: 1, raw: '1' },
287
+ { type: 'Literal', value: 2, raw: '2' }
288
+ ]
289
+ });
290
+ });
291
+
292
+ test('function calls with many arguments', () => {
293
+ const args = Array(300).fill('1');
294
+ isParsed(`=FOO(${args.join(',')})`, {
295
+ type: 'CallExpression',
296
+ callee: { type: 'Identifier', name: 'FOO' },
297
+ arguments: [ ...args.map(() => ({ type: 'Literal', value: 1, raw: '1' })) ]
298
+ });
299
+ });
300
+
301
+ test('function calls with ranges', () => {
302
+ isParsed('=FOO(A1,B2)', {
303
+ type: 'CallExpression',
304
+ callee: { type: 'Identifier', name: 'FOO' },
305
+ arguments: [
306
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
307
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
308
+ ]
309
+ });
310
+
311
+ isParsed('=FOO((A1,B2))', {
312
+ type: 'CallExpression',
313
+ callee: { type: 'Identifier', name: 'FOO' },
314
+ arguments: [
315
+ { type: 'BinaryExpression',
316
+ operator: ',',
317
+ arguments: [
318
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
319
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
320
+ ] }
321
+ ]
322
+ });
323
+ });
324
+
325
+ test('nested function calls', () => {
326
+ isParsed('=FOO(BAR())', {
327
+ type: 'CallExpression',
328
+ callee: { type: 'Identifier', name: 'FOO' },
329
+ arguments: [
330
+ { type: 'CallExpression',
331
+ callee: { type: 'Identifier', name: 'BAR' },
332
+ arguments: [] }
333
+ ]
334
+ });
335
+ });
336
+
337
+ test('function calls with null arguments', () => {
338
+ isParsed('=FOO(,)', {
339
+ type: 'CallExpression',
340
+ callee: { type: 'Identifier', name: 'FOO' },
341
+ arguments: [ null, null ]
342
+ });
343
+
344
+ isParsed('=FOO(,,)', {
345
+ type: 'CallExpression',
346
+ callee: { type: 'Identifier', name: 'FOO' },
347
+ arguments: [ null, null, null ]
348
+ });
349
+
350
+ isParsed('=FOO(1,)', {
351
+ type: 'CallExpression',
352
+ callee: { type: 'Identifier', name: 'FOO' },
353
+ arguments: [ { type: 'Literal', value: 1, raw: '1' }, null ]
354
+ });
355
+
356
+ isParsed('=FOO(,1)', {
357
+ type: 'CallExpression',
358
+ callee: { type: 'Identifier', name: 'FOO' },
359
+ arguments: [ null, { type: 'Literal', value: 1, raw: '1' } ]
360
+ });
361
+ });
362
+
363
+ test('boolean function names', () => {
364
+ isParsed('=FALSE()', {
365
+ type: 'CallExpression',
366
+ callee: { type: 'Identifier', name: 'FALSE' },
367
+ arguments: []
368
+ });
369
+
370
+ isParsed('=TRUE()', {
371
+ type: 'CallExpression',
372
+ callee: { type: 'Identifier', name: 'TRUE' },
373
+ arguments: []
374
+ });
375
+ });
376
+
377
+ test('invalid function calls', () => {
378
+ isInvalidExpr('=FOO((1,2))');
379
+ isInvalidExpr('=FOO(');
380
+ isInvalidExpr('=FOO ()');
381
+ });
382
+ });
383
+
384
+ describe('parse unary operators', () => {
385
+ describe('unary operator %', () => {
386
+ test('percentage operator', () => {
387
+ isParsed('A1%', {
388
+ type: 'UnaryExpression',
389
+ operator: '%',
390
+ arguments: [ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' } ]
391
+ });
392
+
393
+ isParsed('1%', {
394
+ type: 'UnaryExpression',
395
+ operator: '%',
396
+ arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
397
+ });
398
+
399
+ isParsed('(1)%', {
400
+ type: 'UnaryExpression',
401
+ operator: '%',
402
+ arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
403
+ });
404
+ });
405
+
406
+ test('invalid percentage usage', () => {
407
+ isInvalidExpr('%');
408
+ });
409
+ });
410
+
411
+ describe('unary operator -', () => {
412
+ test('negative numbers', () => {
413
+ isParsed('-1', { type: 'Literal', value: -1, raw: '-1' });
414
+
415
+ isParsed('-1', {
416
+ type: 'UnaryExpression',
417
+ operator: '-',
418
+ arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
419
+ }, { negativeNumbers: false });
420
+
421
+ isParsed('-"1"', {
422
+ type: 'UnaryExpression',
423
+ operator: '-',
424
+ arguments: [ { type: 'Literal', value: '1', raw: '"1"' } ]
425
+ });
426
+
427
+ isParsed('-A1:B2', {
428
+ type: 'UnaryExpression',
429
+ operator: '-',
430
+ arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
431
+ });
432
+ });
433
+
434
+ test('double negative', () => {
435
+ isParsed('--1', {
436
+ type: 'UnaryExpression',
437
+ operator: '-',
438
+ arguments: [ { type: 'Literal', value: -1, raw: '-1' } ]
439
+ });
440
+
441
+ isParsed('--1', {
442
+ type: 'UnaryExpression',
443
+ operator: '-',
444
+ arguments: [ {
445
+ type: 'UnaryExpression',
446
+ operator: '-',
447
+ arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
448
+ } ]
449
+ }, { negativeNumbers: false });
450
+ });
451
+
452
+ test('invalid negative usage', () => {
453
+ isInvalidExpr('--');
454
+ isInvalidExpr('-');
455
+ });
456
+ });
457
+
458
+ describe('unary operator +', () => {
459
+ test('positive operator', () => {
460
+ isParsed('+1', {
461
+ type: 'UnaryExpression',
462
+ operator: '+',
463
+ arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
464
+ });
465
+
466
+ isParsed('+(1)', {
467
+ type: 'UnaryExpression',
468
+ operator: '+',
469
+ arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
470
+ });
471
+
472
+ isParsed('+"1"', {
473
+ type: 'UnaryExpression',
474
+ operator: '+',
475
+ arguments: [ { type: 'Literal', value: '1', raw: '"1"' } ]
476
+ });
477
+
478
+ isParsed('+A1:B2', {
479
+ type: 'UnaryExpression',
480
+ operator: '+',
481
+ arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
482
+ });
483
+ });
484
+
485
+ test('invalid positive usage', () => {
486
+ isInvalidExpr('++');
487
+ isInvalidExpr('+');
488
+ });
489
+ });
490
+
491
+ describe('unary operator #', () => {
492
+ test('spill operator', () => {
493
+ isParsed('D9#', {
494
+ type: 'UnaryExpression',
495
+ operator: '#',
496
+ arguments: [ { type: 'ReferenceIdentifier', value: 'D9', kind: 'range' } ]
497
+ });
498
+
499
+ isParsed('A1:B2#', { // this parses but is a runtime error in Excel
500
+ type: 'UnaryExpression',
501
+ operator: '#',
502
+ arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
503
+ });
504
+
505
+ isParsed('(A1):(B2)#', { // this parses but is a runtime error in Excel
506
+ type: 'UnaryExpression',
507
+ operator: '#',
508
+ arguments: [ { type: 'BinaryExpression',
509
+ operator: ':',
510
+ arguments: [
511
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
512
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
513
+ ] } ]
514
+ });
515
+
516
+ isParsed('(A1,B2)#', {
517
+ type: 'UnaryExpression',
518
+ operator: '#',
519
+ arguments: [ { type: 'BinaryExpression',
520
+ operator: ',',
521
+ arguments: [
522
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
523
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
524
+ ] } ]
525
+ });
526
+
527
+ isParsed('(A1 B2)#', {
528
+ type: 'UnaryExpression',
529
+ operator: '#',
530
+ arguments: [ { type: 'BinaryExpression',
531
+ operator: ' ',
532
+ arguments: [
533
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
534
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
535
+ ] } ]
536
+ });
537
+
538
+ isParsed('#REF!#', {
539
+ type: 'UnaryExpression',
540
+ operator: '#',
541
+ arguments: [ { type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' } ]
542
+ });
543
+
544
+ isParsed('INDIRECT("d9")#', {
545
+ type: 'UnaryExpression',
546
+ operator: '#',
547
+ arguments: [ {
548
+ type: 'CallExpression',
549
+ callee: { type: 'Identifier', name: 'INDIRECT' },
550
+ arguments: [ { type: 'Literal', value: 'd9', raw: '"d9"' } ]
551
+ } ]
552
+ });
553
+ });
554
+
555
+ test('invalid spill operator usage', () => {
556
+ isInvalidExpr('1#');
557
+ isInvalidExpr('"foo"#');
558
+ isInvalidExpr('#A1');
559
+ isInvalidExpr('##');
560
+ isInvalidExpr('#VALUE!#');
561
+ isInvalidExpr('#');
562
+ isInvalidExpr('#A1');
563
+ });
564
+ });
565
+
566
+ describe('unary operator @', () => {
567
+ test('implicit intersection operator', () => {
568
+ isParsed('@1', {
569
+ type: 'UnaryExpression',
570
+ operator: '@',
571
+ arguments: [ { type: 'Literal', raw: '1', value: 1 } ]
572
+ });
573
+
574
+ isParsed('@"foo"', {
575
+ type: 'UnaryExpression',
576
+ operator: '@',
577
+ arguments: [ { type: 'Literal', raw: '"foo"', value: 'foo' } ]
578
+ });
579
+
580
+ isParsed('@D9', {
581
+ type: 'UnaryExpression',
582
+ operator: '@',
583
+ arguments: [ { type: 'ReferenceIdentifier', value: 'D9', kind: 'range' } ]
584
+ });
585
+
586
+ isParsed('@A1:B2', {
587
+ type: 'UnaryExpression',
588
+ operator: '@',
589
+ arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
590
+ });
591
+
592
+ isParsed('@#REF!', {
593
+ type: 'UnaryExpression',
594
+ operator: '@',
595
+ arguments: [ { type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' } ]
596
+ });
597
+
598
+ isParsed('@FOO()', {
599
+ type: 'UnaryExpression',
600
+ operator: '@',
601
+ arguments: [ {
602
+ type: 'CallExpression',
603
+ callee: { type: 'Identifier', name: 'FOO' },
604
+ arguments: []
605
+ } ]
606
+ });
607
+ });
608
+
609
+ test('invalid implicit intersection usage', () => {
610
+ isInvalidExpr('@');
611
+ isInvalidExpr('@@');
612
+ });
613
+ });
614
+ });
615
+
616
+ describe('parse binary operators', () => {
617
+ const operators = [ '+', '-', '^', '*', '/', '&', '=', '<', '>', '<=', '>=', '<>' ];
618
+
619
+ operators.forEach(op => {
620
+ describe(`binary operator ${op}`, () => {
621
+ test(`basic ${op} operations`, () => {
622
+ isParsed(`1${op}2`, {
623
+ type: 'BinaryExpression',
624
+ operator: op,
625
+ arguments: [
626
+ { type: 'Literal', value: 1, raw: '1' },
627
+ { type: 'Literal', value: 2, raw: '2' }
628
+ ]
629
+ });
630
+
631
+ isParsed(`1${op}2${op}3`, {
632
+ type: 'BinaryExpression',
633
+ operator: op,
634
+ arguments: [
635
+ { type: 'BinaryExpression',
636
+ operator: op,
637
+ arguments: [
638
+ { type: 'Literal', value: 1, raw: '1' },
639
+ { type: 'Literal', value: 2, raw: '2' }
640
+ ] },
641
+ { type: 'Literal', value: 3, raw: '3' }
642
+ ]
643
+ });
644
+ });
645
+
646
+ test(`${op} with strings`, () => {
647
+ isParsed(`"foo"${op}"bar"`, {
648
+ type: 'BinaryExpression',
649
+ operator: op,
650
+ arguments: [
651
+ { type: 'Literal', value: 'foo', raw: '"foo"' },
652
+ { type: 'Literal', value: 'bar', raw: '"bar"' }
653
+ ]
654
+ });
655
+ });
656
+
657
+ test(`${op} with arrays`, () => {
658
+ isParsed(`{1,2}${op}{3,4}`, {
659
+ type: 'BinaryExpression',
660
+ operator: op,
661
+ arguments: [
662
+ { type: 'ArrayExpression',
663
+ elements: [ [
664
+ { type: 'Literal', value: 1, raw: '1' },
665
+ { type: 'Literal', value: 2, raw: '2' }
666
+ ] ] },
667
+ { type: 'ArrayExpression',
668
+ elements: [ [
669
+ { type: 'Literal', value: 3, raw: '3' },
670
+ { type: 'Literal', value: 4, raw: '4' }
671
+ ] ] }
672
+ ]
673
+ });
674
+ });
675
+
676
+ test(`${op} with function calls`, () => {
677
+ isParsed(`FOO()${op}BAR()`, {
678
+ type: 'BinaryExpression',
679
+ operator: op,
680
+ arguments: [
681
+ { type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' }, arguments: [] },
682
+ { type: 'CallExpression', callee: { type: 'Identifier', name: 'BAR' }, arguments: [] }
683
+ ]
684
+ });
685
+ });
686
+
687
+ test(`invalid ${op} usage`, () => {
688
+ isInvalidExpr(op);
689
+ isInvalidExpr(op + op);
690
+ isInvalidExpr('1' + op);
691
+ if (op !== '+' && op !== '-') {
692
+ isInvalidExpr('=' + op + '1');
693
+ }
694
+ });
695
+ });
696
+ });
697
+ });
698
+
699
+ describe('parse range operators', () => {
700
+ const rangeOps = [
701
+ [ ':', 'range-join' ],
702
+ [ ',', 'union' ],
703
+ [ ' ', 'intersection' ]
704
+ ];
705
+
706
+ rangeOps.forEach(([ op, opName ]) => {
707
+ describe(`${opName} operator "${op}"`, () => {
708
+ test('basic range operations', () => {
709
+ isParsed(`named1${op}named2`, { type: 'BinaryExpression',
710
+ operator: op,
711
+ arguments: [
712
+ { type: 'ReferenceIdentifier', value: 'named1', kind: 'name' },
713
+ { type: 'ReferenceIdentifier', value: 'named2', kind: 'name' }
714
+ ] });
715
+
716
+ isParsed(`A1${op}named2`, { type: 'BinaryExpression',
717
+ operator: op,
718
+ arguments: [
719
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
720
+ { type: 'ReferenceIdentifier', value: 'named2', kind: 'name' }
721
+ ] });
722
+
723
+ isParsed(`named1${op}B2`, { type: 'BinaryExpression',
724
+ operator: op,
725
+ arguments: [
726
+ { type: 'ReferenceIdentifier', value: 'named1', kind: 'name' },
727
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
728
+ ] });
729
+
730
+ isParsed(`(A1)${op}(B2)`, { type: 'BinaryExpression',
731
+ operator: op,
732
+ arguments: [
733
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
734
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
735
+ ] });
736
+ });
737
+
738
+ test('range operator with whitespace', () => {
739
+ isParsed(`A1 ${op} B2`, { type: 'BinaryExpression',
740
+ operator: op,
741
+ arguments: [
742
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
743
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
744
+ ] });
745
+ });
746
+
747
+ test('invalid range operations', () => {
748
+ isInvalidExpr(`A1${op}0`);
749
+ isInvalidExpr(`0${op}A1`);
750
+ isInvalidExpr(`0${op}0`);
751
+ isInvalidExpr(`"foo"${op}"bar"`);
752
+ isInvalidExpr(`TRUE${op}FALSE`);
753
+ isInvalidExpr(`A1${op}#NAME?`);
754
+ isInvalidExpr(`A1${op}#VALUE!`);
755
+ isInvalidExpr(`#NULL!${op}A1`);
756
+ });
757
+
758
+ test('range operations with REF errors', () => {
759
+ isParsed(`A1${op}#REF!`, { type: 'BinaryExpression',
760
+ operator: op,
761
+ arguments: [
762
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
763
+ { type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' }
764
+ ] });
765
+
766
+ isParsed(`#REF!${op}B2`, { type: 'BinaryExpression',
767
+ operator: op,
768
+ arguments: [
769
+ { type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' },
770
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
771
+ ] });
772
+ });
773
+
774
+ test('range operations with complex expressions', () => {
775
+ isParsed(`(A1,B2)${op}C3`, { type: 'BinaryExpression',
776
+ operator: op,
777
+ arguments: [
778
+ { type: 'BinaryExpression',
779
+ operator: ',',
780
+ arguments: [
781
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
782
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
783
+ ] },
784
+ { type: 'ReferenceIdentifier', value: 'C3', kind: 'range' }
785
+ ] });
786
+
787
+ isParsed(`C3${op}(A1,B2)`, { type: 'BinaryExpression',
788
+ operator: op,
789
+ arguments: [
790
+ { type: 'ReferenceIdentifier', value: 'C3', kind: 'range' },
791
+ { type: 'BinaryExpression',
792
+ operator: ',',
793
+ arguments: [
794
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
795
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
796
+ ] }
797
+ ] });
798
+
799
+ isParsed(`(A1 B2)${op}C3`, { type: 'BinaryExpression',
800
+ operator: op,
801
+ arguments: [
802
+ { type: 'BinaryExpression',
803
+ operator: ' ',
804
+ arguments: [
805
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
806
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
807
+ ] },
808
+ { type: 'ReferenceIdentifier', value: 'C3', kind: 'range' }
809
+ ] });
810
+
811
+ isParsed(`C3${op}(A1 B2)`, { type: 'BinaryExpression',
812
+ operator: op,
813
+ arguments: [
814
+ { type: 'ReferenceIdentifier', value: 'C3', kind: 'range' },
815
+ { type: 'BinaryExpression',
816
+ operator: ' ',
817
+ arguments: [
818
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
819
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
820
+ ] }
821
+ ] });
822
+
823
+ isParsed(`(A1:(B2))${op}C3`, { type: 'BinaryExpression',
824
+ operator: op,
825
+ arguments: [
826
+ { type: 'BinaryExpression',
827
+ operator: ':',
828
+ arguments: [
829
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
830
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
831
+ ] },
832
+ { type: 'ReferenceIdentifier', value: 'C3', kind: 'range' }
833
+ ] });
834
+
835
+ isParsed(`C3${op}(A1:(B2))`, { type: 'BinaryExpression',
836
+ operator: op,
837
+ arguments: [
838
+ { type: 'ReferenceIdentifier', value: 'C3', kind: 'range' },
839
+ { type: 'BinaryExpression',
840
+ operator: ':',
841
+ arguments: [
842
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
843
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
844
+ ] }
845
+ ] });
846
+ });
847
+
848
+ test('range operations with ref functions', () => {
849
+ const refFunctions = [
850
+ [ 'ANCHORARRAY', true ],
851
+ [ 'CHOOSE', true ],
852
+ [ 'DROP', true ],
853
+ [ 'IF', true ],
854
+ [ 'IFS', true ],
855
+ [ 'INDEX', true ],
856
+ [ 'INDIRECT', true ],
857
+ [ 'OFFSET', true ],
858
+ [ 'REDUCE', true ],
859
+ [ 'SINGLE', true ],
860
+ [ 'SWITCH', true ],
861
+ [ 'TAKE', true ],
862
+ [ 'XLOOKUP', true ],
863
+ [ 'CELL', false ],
864
+ [ 'COUNT', false ],
865
+ [ 'HSTACK', false ],
866
+ [ 'N', false ],
867
+ [ 'SUM', false ]
868
+ ];
869
+
870
+ refFunctions.forEach(([ funcName, shouldWork ]) => {
871
+ if (shouldWork) {
872
+ isParsed(`${funcName}()${op}C3`, { type: 'BinaryExpression',
873
+ operator: op,
874
+ arguments: [
875
+ { type: 'CallExpression', callee: { type: 'Identifier', name: funcName }, arguments: [] },
876
+ { type: 'ReferenceIdentifier', value: 'C3', kind: 'range' }
877
+ ] });
878
+
879
+ isParsed(`C3${op}${funcName}()`, { type: 'BinaryExpression',
880
+ operator: op,
881
+ arguments: [
882
+ { type: 'ReferenceIdentifier', value: 'C3', kind: 'range' },
883
+ { type: 'CallExpression', callee: { type: 'Identifier', name: funcName }, arguments: [] }
884
+ ] });
885
+ }
886
+ else {
887
+ isInvalidExpr(`${funcName}()${op}C3`);
888
+ isInvalidExpr(`C3${op}${funcName}()`);
889
+ }
890
+ });
891
+ });
892
+ });
893
+ });
894
+ });
895
+
896
+ describe('advanced parsing features', () => {
897
+ test('union operators are normalized', () => {
898
+ isParsed('A1 B2', {
899
+ type: 'BinaryExpression',
900
+ operator: ' ',
901
+ arguments: [
902
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
903
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
904
+ ]
905
+ });
906
+
907
+ isParsed('A1 B2', {
908
+ type: 'BinaryExpression',
909
+ operator: ' ',
910
+ arguments: [
911
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
912
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
913
+ ]
914
+ });
915
+ });
916
+
917
+ test('does not tolerate unterminated tokens', () => {
918
+ isInvalidExpr('="foo');
919
+ });
920
+
921
+ test('position information is correct', () => {
922
+ isParsed(
923
+ '=123.45',
924
+ { type: 'Literal', value: 123.45, loc: [ 1, 7 ], raw: '123.45' },
925
+ { withLocation: true }
926
+ );
927
+
928
+ isParsed(
929
+ '="foo"',
930
+ { type: 'Literal', value: 'foo', loc: [ 1, 6 ], raw: '"foo"' },
931
+ { withLocation: true }
932
+ );
933
+
934
+ isParsed(
935
+ '=true',
936
+ { type: 'Literal', value: true, loc: [ 1, 5 ], raw: 'true' },
937
+ { withLocation: true }
938
+ );
939
+
940
+ isParsed(
941
+ '=Sheet1!A1:B2',
942
+ { type: 'ReferenceIdentifier', value: 'Sheet1!A1:B2', kind: 'range', loc: [ 1, 13 ] },
943
+ { withLocation: true }
944
+ );
945
+
946
+ isParsed(
947
+ '=(#VALUE!)',
948
+ { type: 'ErrorLiteral', value: '#VALUE!', loc: [ 2, 9 ], raw: '#VALUE!' },
949
+ { withLocation: true }
950
+ );
951
+
952
+ // UnaryExpression
953
+ isParsed(
954
+ '=-A1',
955
+ { type: 'UnaryExpression',
956
+ loc: [ 1, 4 ],
957
+ operator: '-',
958
+ arguments: [
959
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range', loc: [ 2, 4 ] }
960
+ ] },
961
+ { withLocation: true }
962
+ );
963
+
964
+ isParsed(
965
+ '=10%',
966
+ { type: 'UnaryExpression',
967
+ loc: [ 1, 4 ],
968
+ operator: '%',
969
+ arguments: [
970
+ { type: 'Literal', value: 10, loc: [ 1, 3 ], raw: '10' }
971
+ ] },
972
+ { withLocation: true }
973
+ );
974
+
975
+ isParsed(
976
+ '=-(123)',
977
+ { type: 'UnaryExpression',
978
+ loc: [ 1, 6 ],
979
+ operator: '-',
980
+ arguments: [
981
+ { type: 'Literal', value: 123, loc: [ 3, 6 ], raw: '123' }
982
+ ] },
983
+ { withLocation: true }
984
+ );
985
+
986
+ isParsed(
987
+ '(123+(234))',
988
+ { type: 'BinaryExpression',
989
+ loc: [ 1, 9 ],
990
+ operator: '+',
991
+ arguments: [
992
+ { type: 'Literal', value: 123, loc: [ 1, 4 ], raw: '123' },
993
+ { type: 'Literal', value: 234, loc: [ 6, 9 ], raw: '234' }
994
+ ] },
995
+ { withLocation: true }
996
+ );
997
+
998
+ isParsed(
999
+ '=(A1 B2)',
1000
+ { type: 'BinaryExpression',
1001
+ loc: [ 2, 7 ],
1002
+ operator: ' ',
1003
+ arguments: [
1004
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range', loc: [ 2, 4 ] },
1005
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range', loc: [ 5, 7 ] }
1006
+ ] },
1007
+ { withLocation: true }
1008
+ );
1009
+
1010
+ isParsed(
1011
+ '=SUM(4,5)',
1012
+ { type: 'CallExpression',
1013
+ loc: [ 1, 9 ],
1014
+ callee: { type: 'Identifier', name: 'SUM', loc: [ 1, 4 ] },
1015
+ arguments: [
1016
+ { type: 'Literal', value: 4, loc: [ 5, 6 ], raw: '4' },
1017
+ { type: 'Literal', value: 5, loc: [ 7, 8 ], raw: '5' }
1018
+ ] },
1019
+ { withLocation: true }
1020
+ );
1021
+
1022
+ // ArrayExpression
1023
+ isParsed(
1024
+ '={ 1, 2; 3, 4 }',
1025
+ { type: 'ArrayExpression',
1026
+ loc: [ 1, 15 ],
1027
+ elements: [
1028
+ [ { type: 'Literal', value: 1, loc: [ 3, 4 ], raw: '1' },
1029
+ { type: 'Literal', value: 2, loc: [ 6, 7 ], raw: '2' } ],
1030
+ [ { type: 'Literal', value: 3, loc: [ 9, 10 ], raw: '3' },
1031
+ { type: 'Literal', value: 4, loc: [ 12, 13 ], raw: '4' } ]
1032
+ ] },
1033
+ { withLocation: true }
1034
+ );
1035
+ });
1036
+
1037
+ test('whitespace handling in various contexts', () => {
1038
+ // whitespace in arrays
1039
+ isParsed('=SORT({ A:A, B:B })',
1040
+ { type: 'CallExpression',
1041
+ callee: { type: 'Identifier', name: 'SORT' },
1042
+ arguments: [
1043
+ { type: 'ArrayExpression',
1044
+ elements: [
1045
+ [ { type: 'ReferenceIdentifier', value: 'A:A', kind: 'beam' },
1046
+ { type: 'ReferenceIdentifier', value: 'B:B', kind: 'beam' } ]
1047
+ ] }
1048
+ ] },
1049
+ { permitArrayRanges: true });
1050
+
1051
+ // whitespace in arguments
1052
+ isParsed('=A2:A5=XLOOKUP(B1,C:C, D:D)',
1053
+ { type: 'BinaryExpression',
1054
+ operator: '=',
1055
+ arguments: [
1056
+ { type: 'ReferenceIdentifier', value: 'A2:A5', kind: 'range' },
1057
+ { type: 'CallExpression',
1058
+ callee: { type: 'Identifier', name: 'XLOOKUP' },
1059
+ arguments: [
1060
+ { type: 'ReferenceIdentifier', value: 'B1', kind: 'range' },
1061
+ { type: 'ReferenceIdentifier', value: 'C:C', kind: 'beam' },
1062
+ { type: 'ReferenceIdentifier', value: 'D:D', kind: 'beam' }
1063
+ ] }
1064
+ ] },
1065
+ { permitArrayRanges: true });
1066
+
1067
+ // whitespace surrounding comma
1068
+ isParsed('=SUM(12 , B:B)',
1069
+ { type: 'CallExpression',
1070
+ callee: { type: 'Identifier', name: 'SUM' },
1071
+ arguments: [
1072
+ { type: 'Literal', value: 12, raw: '12' },
1073
+ { type: 'ReferenceIdentifier', value: 'B:B', kind: 'beam' }
1074
+ ] },
1075
+ { permitArrayCalls: true });
1076
+
1077
+ // whitespace tailing operator
1078
+ isParsed('=A:A= C1',
1079
+ { type: 'BinaryExpression',
1080
+ operator: '=',
1081
+ arguments: [
1082
+ { type: 'ReferenceIdentifier', value: 'A:A', kind: 'beam' },
1083
+ { type: 'ReferenceIdentifier', value: 'C1', kind: 'range' }
1084
+ ] },
1085
+ { permitArrayCalls: true });
1086
+ });
1087
+
1088
+ test('parser can permit xlsx mode references', () => {
1089
+ isInvalidExpr('=SUM([Workbook.xlsx]!A1+[Workbook.xlsx]!Table1[#Data])');
1090
+ isParsed('=SUM([Workbook.xlsx]!A1+[Workbook.xlsx]!Table1[#Data])',
1091
+ { type: 'CallExpression',
1092
+ callee: { type: 'Identifier', name: 'SUM' },
1093
+ arguments: [
1094
+ { type: 'BinaryExpression',
1095
+ operator: '+',
1096
+ arguments: [
1097
+ { type: 'ReferenceIdentifier', value: '[Workbook.xlsx]!A1', kind: 'range' },
1098
+ { type: 'ReferenceIdentifier', value: '[Workbook.xlsx]!Table1[#Data]', kind: 'table' }
1099
+ ] }
1100
+ ] },
1101
+ { xlsx: true });
1102
+ });
1103
+
1104
+ test('parser supports LAMBDA expressions', () => {
1105
+ // Invalid LAMBDA expressions
1106
+ isInvalidExpr('LAMBDA(,)');
1107
+ isInvalidExpr('LAMBDA(a,)');
1108
+ isInvalidExpr('LAMBDA(a,,)');
1109
+ isInvalidExpr('=LAMBDA(1,1)');
1110
+ isInvalidExpr('=LAMBDA(a,1,a)');
1111
+ isInvalidExpr('=LAMBDA(a,A,1)');
1112
+ isInvalidExpr('=LAMBDA(a,a,1)');
1113
+ isInvalidExpr('=LAMBDA(A1,B1,1)');
1114
+
1115
+ // Valid LAMBDA expressions
1116
+ isParsed('=LAMBDA()', {
1117
+ type: 'LambdaExpression',
1118
+ params: [],
1119
+ body: null
1120
+ });
1121
+
1122
+ isParsed('=LAMBDA(1)', {
1123
+ type: 'LambdaExpression',
1124
+ params: [],
1125
+ body: {
1126
+ type: 'Literal',
1127
+ value: 1,
1128
+ raw: '1'
1129
+ }
1130
+ });
1131
+
1132
+ isParsed('=LAMBDA(1+1)', {
1133
+ type: 'LambdaExpression',
1134
+ params: [],
1135
+ body: {
1136
+ type: 'BinaryExpression',
1137
+ operator: '+',
1138
+ arguments: [
1139
+ { type: 'Literal', value: 1, raw: '1' },
1140
+ { type: 'Literal', value: 1, raw: '1' }
1141
+ ]
1142
+ }
1143
+ });
1144
+
1145
+ isParsed('=LAMBDA(a,1)', {
1146
+ type: 'LambdaExpression',
1147
+ body: { type: 'Literal', value: 1, raw: '1' },
1148
+ params: [
1149
+ { type: 'Identifier', name: 'a' }
1150
+ ]
1151
+ });
1152
+
1153
+ isParsed('=LAMBDA(a,b,1)', {
1154
+ type: 'LambdaExpression',
1155
+ body: { type: 'Literal', value: 1, raw: '1' },
1156
+ params: [
1157
+ { type: 'Identifier', name: 'a' },
1158
+ { type: 'Identifier', name: 'b' }
1159
+ ]
1160
+ });
1161
+
1162
+ isParsed('=lambda(a,b,a*b)', {
1163
+ type: 'LambdaExpression',
1164
+ body: {
1165
+ type: 'BinaryExpression',
1166
+ operator: '*',
1167
+ arguments: [
1168
+ { type: 'ReferenceIdentifier', value: 'a', kind: 'name' },
1169
+ { type: 'ReferenceIdentifier', value: 'b', kind: 'name' }
1170
+ ]
1171
+ },
1172
+ params: [
1173
+ { type: 'Identifier', name: 'a' },
1174
+ { type: 'Identifier', name: 'b' }
1175
+ ]
1176
+ });
1177
+
1178
+ isParsed('=lambda( a , b , a b )', {
1179
+ type: 'LambdaExpression',
1180
+ body: {
1181
+ type: 'BinaryExpression',
1182
+ operator: ' ',
1183
+ arguments: [
1184
+ { type: 'ReferenceIdentifier', value: 'a', kind: 'name' },
1185
+ { type: 'ReferenceIdentifier', value: 'b', kind: 'name' }
1186
+ ]
1187
+ },
1188
+ params: [
1189
+ { type: 'Identifier', name: 'a' },
1190
+ { type: 'Identifier', name: 'b' }
1191
+ ]
1192
+ });
1193
+
1194
+ // r and c are forbidden as names, but should work here
1195
+ isParsed('=LAMBDA(r,c,r*c)', {
1196
+ type: 'LambdaExpression',
1197
+ body: {
1198
+ type: 'BinaryExpression',
1199
+ operator: '*',
1200
+ arguments: [
1201
+ { type: 'ReferenceIdentifier', value: 'r', kind: 'name' },
1202
+ { type: 'ReferenceIdentifier', value: 'c', kind: 'name' }
1203
+ ]
1204
+ },
1205
+ params: [
1206
+ { type: 'Identifier', name: 'r' },
1207
+ { type: 'Identifier', name: 'c' }
1208
+ ]
1209
+ });
1210
+ });
1211
+
1212
+ test('parser allows calling refs, lambda, let, and call expressions', () => {
1213
+ // Invalid function calls
1214
+ isInvalidExpr('1()');
1215
+ isInvalidExpr('"str"()');
1216
+ isInvalidExpr('#VALUE!()');
1217
+ isInvalidExpr('foo%()');
1218
+ isInvalidExpr('foo ()');
1219
+
1220
+ // Valid callable expressions
1221
+ isParsed('=lambda()()', {
1222
+ type: 'CallExpression',
1223
+ callee: {
1224
+ type: 'LambdaExpression',
1225
+ params: [],
1226
+ body: null
1227
+ },
1228
+ arguments: []
1229
+ });
1230
+
1231
+ isParsed('=lambda(1)(1)', {
1232
+ type: 'CallExpression',
1233
+ callee: {
1234
+ type: 'LambdaExpression',
1235
+ params: [],
1236
+ body: { type: 'Literal', value: 1, raw: '1' }
1237
+ },
1238
+ arguments: [
1239
+ { type: 'Literal', value: 1, raw: '1' }
1240
+ ]
1241
+ });
1242
+
1243
+ isParsed('=FOO()()', {
1244
+ type: 'CallExpression',
1245
+ callee: {
1246
+ type: 'CallExpression',
1247
+ callee: { type: 'Identifier', name: 'FOO' },
1248
+ arguments: []
1249
+ },
1250
+ arguments: []
1251
+ });
1252
+
1253
+ isParsed('=(A1)()', {
1254
+ type: 'CallExpression',
1255
+ callee: { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
1256
+ arguments: []
1257
+ });
1258
+
1259
+ isParsed('=LET(a,1,a)()', {
1260
+ type: 'CallExpression',
1261
+ callee: {
1262
+ type: 'LetExpression',
1263
+ declarations: [
1264
+ { type: 'LetDeclarator',
1265
+ id: { type: 'Identifier', name: 'a' },
1266
+ init: { type: 'Literal', value: 1, raw: '1' } }
1267
+ ],
1268
+ body: { type: 'ReferenceIdentifier', value: 'a', kind: 'name' }
1269
+ },
1270
+ arguments: []
1271
+ });
1272
+
1273
+ isParsed('=#REF!()', {
1274
+ type: 'CallExpression',
1275
+ callee: {
1276
+ type: 'ErrorLiteral',
1277
+ value: '#REF!',
1278
+ raw: '#REF!'
1279
+ },
1280
+ arguments: []
1281
+ });
1282
+
1283
+ // this is allowed because in Excel: `foo#()` is really `ANCHORARRAY(foo)()`
1284
+ isParsed('foo#()', {
1285
+ type: 'CallExpression',
1286
+ callee: {
1287
+ type: 'UnaryExpression',
1288
+ operator: '#',
1289
+ arguments: [
1290
+ { type: 'ReferenceIdentifier', value: 'foo', kind: 'name' }
1291
+ ]
1292
+ },
1293
+ arguments: []
1294
+ });
1295
+ });
1296
+
1297
+ test('parser supports LET expressions', () => {
1298
+ // Invalid LET expressions
1299
+ isInvalidExpr('LET(,)');
1300
+ isInvalidExpr('LET(1,a,1)');
1301
+ isInvalidExpr('LET()');
1302
+ isInvalidExpr('LET(a,b)');
1303
+ isInvalidExpr('LET(a,)');
1304
+ isInvalidExpr('LET(a,a,1,1)');
1305
+ isInvalidExpr('LET(a,a,1,a)');
1306
+ isInvalidExpr('LET(a,1,b,1,c,1,a1+b,1)');
1307
+ isInvalidExpr('LET(a,1,a,1,1)');
1308
+ isInvalidExpr('LET(a,1,A,1,1)');
1309
+
1310
+ // Valid LET expressions
1311
+ isParsed('=LET(a,1,)', {
1312
+ type: 'LetExpression',
1313
+ declarations: [
1314
+ {
1315
+ type: 'LetDeclarator',
1316
+ id: { type: 'Identifier', name: 'a' },
1317
+ init: { type: 'Literal', value: 1, raw: '1' }
1318
+ }
1319
+ ],
1320
+ body: null
1321
+ });
1322
+
1323
+ isParsed('=LET(a,1,a)', {
1324
+ type: 'LetExpression',
1325
+ declarations: [
1326
+ {
1327
+ type: 'LetDeclarator',
1328
+ id: { type: 'Identifier', name: 'a' },
1329
+ init: { type: 'Literal', value: 1, raw: '1' }
1330
+ }
1331
+ ],
1332
+ body: { type: 'ReferenceIdentifier', value: 'a', kind: 'name' }
1333
+ });
1334
+
1335
+ isParsed('=LET(a,1,b,1,c,1,a+b*c)', {
1336
+ type: 'LetExpression',
1337
+ declarations: [
1338
+ { type: 'LetDeclarator',
1339
+ id: { type: 'Identifier', name: 'a' },
1340
+ init: { type: 'Literal', value: 1, raw: '1' } },
1341
+ { type: 'LetDeclarator',
1342
+ id: { type: 'Identifier', name: 'b' },
1343
+ init: { type: 'Literal', value: 1, raw: '1' } },
1344
+ { type: 'LetDeclarator',
1345
+ id: { type: 'Identifier', name: 'c' },
1346
+ init: { type: 'Literal', value: 1, raw: '1' } }
1347
+ ],
1348
+ body: {
1349
+ type: 'BinaryExpression',
1350
+ operator: '+',
1351
+ arguments: [
1352
+ { type: 'ReferenceIdentifier', value: 'a', kind: 'name' },
1353
+ { type: 'BinaryExpression',
1354
+ operator: '*',
1355
+ arguments: [
1356
+ { type: 'ReferenceIdentifier', value: 'b', kind: 'name' },
1357
+ { type: 'ReferenceIdentifier', value: 'c', kind: 'name' }
1358
+ ] }
1359
+ ]
1360
+ }
1361
+ });
1362
+
1363
+ // r and c are forbidden as names, but should work here
1364
+ isParsed('LET(r,1,c,1,r*c)', {
1365
+ type: 'LetExpression',
1366
+ declarations: [
1367
+ { type: 'LetDeclarator',
1368
+ id: { type: 'Identifier', name: 'r' },
1369
+ init: { type: 'Literal', value: 1, raw: '1' } },
1370
+ {
1371
+ type: 'LetDeclarator',
1372
+ id: { type: 'Identifier', name: 'c' },
1373
+ init: { type: 'Literal', value: 1, raw: '1' }
1374
+ }
1375
+ ],
1376
+ body: {
1377
+ type: 'BinaryExpression',
1378
+ operator: '*',
1379
+ arguments: [
1380
+ { type: 'ReferenceIdentifier', value: 'r', kind: 'name' },
1381
+ { type: 'ReferenceIdentifier', value: 'c', kind: 'name' }
1382
+ ]
1383
+ }
1384
+ });
1385
+ });
1386
+
1387
+ test('parser whitespace handling', () => {
1388
+ isParsed('\tA1\u00a0+\nB2\r', {
1389
+ type: 'BinaryExpression',
1390
+ operator: '+',
1391
+ arguments: [
1392
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
1393
+ { type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
1394
+ ]
1395
+ });
1396
+ });
1397
+
1398
+ test('looseRefCalls: true relaxes ref function restrictions', () => {
1399
+ isInvalidExpr('A1:TESTFN()');
1400
+ isParsed('A1:TESTFN()', {
1401
+ type: 'BinaryExpression',
1402
+ operator: ':',
1403
+ arguments: [
1404
+ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
1405
+ { type: 'CallExpression', callee: { type: 'Identifier', name: 'TESTFN' }, arguments: [] }
1406
+ ]
1407
+ }, { looseRefCalls: true });
1408
+ });
1409
+ });
1410
+ });