@borgar/fx 3.0.0 → 4.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +25 -12
- package/.jsdoc/config.json +17 -0
- package/.jsdoc/publish.js +195 -0
- package/README.md +8 -311
- package/dist/fx.js +1 -1
- package/docs/API.md +708 -0
- package/docs/AST format.md +144 -0
- package/docs/References.md +60 -0
- package/lib/a1.js +156 -30
- package/lib/a1.spec.js +9 -2
- package/lib/{addMeta.js → addTokenMeta.js} +50 -5
- package/lib/{addMeta.spec.js → addTokenMeta.spec.js} +16 -16
- package/lib/constants.js +14 -4
- package/lib/fixRanges.js +64 -10
- package/lib/fixRanges.spec.js +35 -6
- package/lib/index.js +95 -16
- package/lib/isType.js +119 -8
- package/lib/lexer-srefs.spec.js +311 -0
- package/lib/lexer.js +56 -16
- package/lib/lexer.spec.js +247 -214
- package/lib/lexerParts.js +40 -16
- package/lib/mergeRefTokens.js +38 -25
- package/lib/mergeRefTokens.spec.js +39 -39
- package/lib/parseRef.js +17 -12
- package/lib/parser.js +498 -0
- package/lib/parser.spec.js +777 -0
- package/lib/rc.js +95 -22
- package/lib/rc.spec.js +16 -5
- package/lib/sr.js +277 -0
- package/lib/sr.spec.js +179 -0
- package/lib/translate-toA1.spec.js +53 -2
- package/lib/translate-toRC.spec.js +35 -2
- package/lib/translate.js +137 -21
- package/package.json +3 -1
- package/References.md +0 -39
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Abstract Syntax Tree Format
|
|
2
|
+
|
|
3
|
+
This document specifies the core AST node types that support the Excel grammar. The format is based on the [ESTree AST](https://github.com/estree/estree) (and so is this document).
|
|
4
|
+
|
|
5
|
+
## Node objects
|
|
6
|
+
|
|
7
|
+
All AST nodes are represented by `Node` objects. They may have any prototype inheritance but implement the following basic interface:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
interface Node {
|
|
11
|
+
type: string;
|
|
12
|
+
loc?: Location | null;
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The `type` field is a string representing the AST variant type. Each subtype of Node is documented below with the specific string of its `type` field. You can use this field to determine which interface a node implements.
|
|
17
|
+
|
|
18
|
+
The `loc` field represents the source location information of the node. If the node contains no information about the source location, the field is `null`; otherwise it is an array consisting of a two numbers: A start offset (the position of the first character of the parsed source region) and an end offset (the position of the first character after the parsed source region):
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
interface Location extends Array<number> {
|
|
22
|
+
0: number;
|
|
23
|
+
1: number;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Identifier
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
interface Identifier extends Node {
|
|
31
|
+
type: "Identifier";
|
|
32
|
+
name: string;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
An identifier. These only appear on `CallExpression` and will always be a static string representing the name of a function call.
|
|
37
|
+
|
|
38
|
+
## ReferenceIdentifier
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
interface ReferenceIdentifier extends Node {
|
|
42
|
+
type: "ReferenceIdentifier";
|
|
43
|
+
value: string;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A range identifier.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Literal
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
interface Literal extends Node {
|
|
54
|
+
type: "Literal";
|
|
55
|
+
raw: string;
|
|
56
|
+
value: string | number | boolean;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A literal token. Captures numbers, strings, and booleans. Literal errors have their own variant type.
|
|
61
|
+
|
|
62
|
+
## ErrorLiteral
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
interface ErrorLiteral extends Node {
|
|
66
|
+
type: "ErrorLiteral";
|
|
67
|
+
raw: string;
|
|
68
|
+
value: string;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
An Error expression.
|
|
73
|
+
|
|
74
|
+
## UnaryExpression
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
interface UnaryExpression extends Node {
|
|
78
|
+
type: "UnaryExpression";
|
|
79
|
+
operator: UnaryOperator;
|
|
80
|
+
arguments: Array<[ Node ]>;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A unary operator expression.
|
|
85
|
+
|
|
86
|
+
### UnaryOperator
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
type UnaryOperator = (
|
|
90
|
+
"+" | "-" | "%" | "#" | "@"
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
A unary operator token.
|
|
95
|
+
|
|
96
|
+
## BinaryExpression
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
interface BinaryExpression extends Node {
|
|
100
|
+
type: "BinaryExpression";
|
|
101
|
+
operator: BinaryOperator;
|
|
102
|
+
arguments: Array<[ Node, Node ]>;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A binary operator expression.
|
|
107
|
+
|
|
108
|
+
### BinaryOperator
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
type BinaryOperator = (
|
|
112
|
+
"=" | "<" | ">" | "<=" | ">=" | "<>" |
|
|
113
|
+
"-" | "+" | "*" | "/" | "^" |
|
|
114
|
+
":" | " " | "," |
|
|
115
|
+
"&"
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A binary operator token. Note that Excels union operator is whitespace so a parser must normalize this to a single space.
|
|
120
|
+
|
|
121
|
+
## CallExpression
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
interface CallExpression extends Node {
|
|
125
|
+
type: "CallExpression";
|
|
126
|
+
callee: Identifier;
|
|
127
|
+
arguments: Array<Node>;
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A function call expression.
|
|
132
|
+
|
|
133
|
+
## ArrayExpression
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
interface ArrayExpression extends Node {
|
|
137
|
+
type: "ArrayExpression";
|
|
138
|
+
elements: Array<Array<Literal | Error | ReferenceIdentifier>>;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
An array expression. Excel does not have empty or sparse arrays and restricts array elements to literals. Google Sheets allows `ReferenceIdentifier`s as elements of arrays, the fx parser as an option for this but it is off by default.
|
|
143
|
+
|
|
144
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# References and Ranges
|
|
2
|
+
|
|
3
|
+
In Excels spreadsheet formula language terminology, a reference is similar to what is in most programming is called a variable. Spreadsheets do not have variables though, they have cells. The cells can be referenced in formulas, either directly (such as `=SUM(A1)`), or through aliases (such as `=SUM(someName)`).
|
|
4
|
+
|
|
5
|
+
A range is when a cell, or a set of cells, is referenced directly. Ranges in formulas can come in one of two syntax styles: The commonly known A1 style, as well as R1C1 style where both axes are numerical. Only one style can be used at a time in a formula.
|
|
6
|
+
|
|
7
|
+
## Ranges
|
|
8
|
+
|
|
9
|
+
This tokenizer considers there to be three "types" of ranges:
|
|
10
|
+
|
|
11
|
+
### Ranges (`REF_RANGE`)
|
|
12
|
+
|
|
13
|
+
The basic type of range will be referencing either:
|
|
14
|
+
|
|
15
|
+
* A single cell, like `A1` or `AF31`.
|
|
16
|
+
* A bounded rectangle of cells, like `A1:B2` or `AF17:AF31`.
|
|
17
|
+
|
|
18
|
+
### Range ternary (`REF_TERNARY`)
|
|
19
|
+
|
|
20
|
+
Ternary ranges are rectangles of cells defined by only three of the four possible sides. They are are unbounded in either bottom or right dimension:
|
|
21
|
+
|
|
22
|
+
* A rectangle of cells that is unbounded to the bottom, like `A1:A` or `C3:D`.
|
|
23
|
+
* A rectangle of cells that is unbounded to the right, like `A1:1` or `F2:5`.
|
|
24
|
+
|
|
25
|
+
This type of range is not supported in Excel, so it is an opt-in for the tokenizer (see [README.md](./README.md)).
|
|
26
|
+
|
|
27
|
+
### Range beams (`REF_BEAM`)
|
|
28
|
+
|
|
29
|
+
Range beams are rectangles of cells that are unbounded in either left and right, or top and bottom dimensions.
|
|
30
|
+
|
|
31
|
+
* A rectangle of cells that is unbounded to the top and bottom, like `A:A` or `C:D`.
|
|
32
|
+
* A rectangle of cells that is unbounded to the left and right, like `1:1` or `2:5`.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## References
|
|
36
|
+
|
|
37
|
+
As well as ranges, a reference can be either of:
|
|
38
|
+
|
|
39
|
+
### Named Ranges or Names (`REF_NAMED`)
|
|
40
|
+
|
|
41
|
+
[Named Ranges][named], or Names as Excel prefers to call them most of the time, are usually a references to a range or table but may also be references to entire expressions.
|
|
42
|
+
|
|
43
|
+
### Structured references (`REF_STRUCT`)
|
|
44
|
+
|
|
45
|
+
[Structured references][srefs] are references into an area or slice of a table. They can include the table name (which is essentially a Name), as well as either or both of a section directive and/or column name or a range of columns.
|
|
46
|
+
|
|
47
|
+
* A structured reference to a column: `Table1[Column1]`
|
|
48
|
+
* A structured reference to the tables totals: `Table1[#Totals]`
|
|
49
|
+
* A structured reference to a range of columns: `[[Column1]:[Column3]]`
|
|
50
|
+
* A structured reference to a specific column of a tables totals: `Table1[[#Totals],[Column2]]`
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
Spreadsheet applications will normalize all ranges when you enter a formula, flipping the left/right and top/bottom coordinates as needed to keep the range top to bottom and left to right. Structured references will also be normalized as appropriate.
|
|
55
|
+
|
|
56
|
+
The library has tools to both normalize the ranges, as well as filling in the missing boundaries (see [README.md](./README.md)).
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
[named]: https://support.microsoft.com/en-us/office/define-and-use-names-in-formulas-4d0f13ac-53b7-422e-afd2-abd7ff379c64
|
|
60
|
+
[srefs]: https://support.microsoft.com/en-us/office/using-structured-references-with-excel-tables-f5ed2452-2337-4f71-bed3-c8ae6d2b276e
|
package/lib/a1.js
CHANGED
|
@@ -2,8 +2,23 @@ import { MAX_ROWS, MAX_COLS } from './constants.js';
|
|
|
2
2
|
import { parseRef } from './parseRef.js';
|
|
3
3
|
import { stringifyPrefix } from './stringifyPrefix.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const clamp = (min, val, max) => Math.min(Math.max(val, min), max);
|
|
6
|
+
const toColStr = (c, a) => (a ? '$' : '') + toCol(c);
|
|
7
|
+
const toRowStr = (r, a) => (a ? '$' : '') + toRow(r);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a column string representation to a 0 based
|
|
11
|
+
* offset number (`"C"` = `2`).
|
|
12
|
+
*
|
|
13
|
+
* The method expects a valid column identifier made up of _only_
|
|
14
|
+
* A-Z letters, which may be either upper or lower case. Other input will
|
|
15
|
+
* return garbage.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} columnString The column string identifier
|
|
18
|
+
* @return {number} Zero based column index number
|
|
19
|
+
*/
|
|
20
|
+
export function fromCol (columnString) {
|
|
21
|
+
const x = (columnString || '');
|
|
7
22
|
const l = x.length;
|
|
8
23
|
let n = 0;
|
|
9
24
|
if (l > 2) {
|
|
@@ -24,11 +39,21 @@ export function fromCol (columnId) {
|
|
|
24
39
|
return n;
|
|
25
40
|
}
|
|
26
41
|
|
|
27
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Convert a 0 based offset number to a column string
|
|
44
|
+
* representation (`2` = `"C"`).
|
|
45
|
+
*
|
|
46
|
+
* The method expects a number between 0 and 16383. Other input will
|
|
47
|
+
* return garbage.
|
|
48
|
+
*
|
|
49
|
+
* @param {number} columnIndex Zero based column index number
|
|
50
|
+
* @return {string} The column string identifier
|
|
51
|
+
*/
|
|
52
|
+
export function toCol (columnIndex) {
|
|
28
53
|
return (
|
|
29
|
-
(
|
|
30
|
-
(
|
|
31
|
-
String.fromCharCode((
|
|
54
|
+
(columnIndex >= 702 ? String.fromCharCode((((columnIndex - 702) / 676) - 0) % 26 + 65) : '') +
|
|
55
|
+
(columnIndex >= 26 ? String.fromCharCode(Math.floor(((columnIndex / 26) - 1) % 26 + 65)) : '') +
|
|
56
|
+
String.fromCharCode((columnIndex % 26 + 65))
|
|
32
57
|
);
|
|
33
58
|
}
|
|
34
59
|
|
|
@@ -50,15 +75,32 @@ export function toAbsolute (range) {
|
|
|
50
75
|
return { top, left, bottom, right, $left: true, $right: true, $top: true, $bottom: true };
|
|
51
76
|
}
|
|
52
77
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Stringify a range object into A1 syntax.
|
|
80
|
+
*
|
|
81
|
+
* @private
|
|
82
|
+
* @see parseA1Ref
|
|
83
|
+
* @param {Object} range A range object
|
|
84
|
+
* @return {string} An A1-style string represenation of a range
|
|
85
|
+
*/
|
|
56
86
|
export function toA1 (range) {
|
|
57
|
-
|
|
87
|
+
let { top, left, bottom, right } = range;
|
|
88
|
+
const { $left, $right, $top, $bottom } = range;
|
|
58
89
|
const noLeft = left == null;
|
|
59
90
|
const noRight = right == null;
|
|
60
91
|
const noTop = top == null;
|
|
61
92
|
const noBottom = bottom == null;
|
|
93
|
+
// allow skipping right and bottom to define a cell
|
|
94
|
+
top = clamp(0, top | 0, MAX_ROWS);
|
|
95
|
+
left = clamp(0, left | 0, MAX_COLS);
|
|
96
|
+
if (!noLeft && !noTop && noRight && noBottom) {
|
|
97
|
+
bottom = top;
|
|
98
|
+
right = left;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
bottom = clamp(0, bottom | 0, MAX_ROWS);
|
|
102
|
+
right = clamp(0, right | 0, MAX_COLS);
|
|
103
|
+
}
|
|
62
104
|
// A:A
|
|
63
105
|
if ((top === 0 && bottom >= MAX_ROWS) || (noTop && noBottom)) {
|
|
64
106
|
return toColStr(left, $left) + ':' + toColStr(right, $right);
|
|
@@ -84,8 +126,9 @@ export function toA1 (range) {
|
|
|
84
126
|
return toColStr(right, $right) + toRowStr(top, $top) + ':' + toRowStr(bottom, $bottom);
|
|
85
127
|
}
|
|
86
128
|
// A1:A1
|
|
87
|
-
|
|
88
|
-
return toColStr(left, $left) + toRowStr(top, $top) + ':' +
|
|
129
|
+
if (right !== left || bottom !== top || $right !== $left || $bottom !== $top) {
|
|
130
|
+
return toColStr(left, $left) + toRowStr(top, $top) + ':' +
|
|
131
|
+
toColStr(right, $right) + toRowStr(bottom, $bottom);
|
|
89
132
|
}
|
|
90
133
|
// A1
|
|
91
134
|
return toColStr(left, $left) + toRowStr(top, $top);
|
|
@@ -104,6 +147,15 @@ function splitA1 (str) {
|
|
|
104
147
|
];
|
|
105
148
|
}
|
|
106
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Parse a simple string reference to an A1 range into a range object.
|
|
152
|
+
* Will accept `A1`, `A2`, `A:A`, or `1:1`.
|
|
153
|
+
*
|
|
154
|
+
* @private
|
|
155
|
+
* @see parseA1Ref
|
|
156
|
+
* @param {string} rangeString A range string
|
|
157
|
+
* @return {(Object|null)} An object representing a valid reference or null if it is invalid.
|
|
158
|
+
*/
|
|
107
159
|
export function fromA1 (rangeStr) {
|
|
108
160
|
let top = null;
|
|
109
161
|
let left = null;
|
|
@@ -165,8 +217,37 @@ export function fromA1 (rangeStr) {
|
|
|
165
217
|
return { top, left, bottom, right, $top, $left, $bottom, $right };
|
|
166
218
|
}
|
|
167
219
|
|
|
168
|
-
|
|
169
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Parse a string reference into an object representing it.
|
|
222
|
+
*
|
|
223
|
+
* ```js
|
|
224
|
+
* parseA1Ref('Sheet1!A$1:$B2');
|
|
225
|
+
* // => {
|
|
226
|
+
* // context: [ 'Sheet1' ],
|
|
227
|
+
* // range: {
|
|
228
|
+
* // top: 0,
|
|
229
|
+
* // left: 0,
|
|
230
|
+
* // bottom: 1,
|
|
231
|
+
* // right: 1
|
|
232
|
+
* // $top: true,
|
|
233
|
+
* // $left: false,
|
|
234
|
+
* // $bottom: false,
|
|
235
|
+
* // $right: true
|
|
236
|
+
* // }
|
|
237
|
+
* // }
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* For A:A or A1:A style ranges, `null` will be used for any dimensions that the
|
|
241
|
+
* syntax does not specify:
|
|
242
|
+
*
|
|
243
|
+
* @param {string} refString An A1-style reference string
|
|
244
|
+
* @param {Object} [options={}] Options
|
|
245
|
+
* @param {boolean} [options.allowNamed=true] Enable parsing names as well as ranges.
|
|
246
|
+
* @param {boolean} [options.allowTernary=false] Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`. These are supported by Google Sheets but not Excel. See: References.md.
|
|
247
|
+
* @return {(Object|null)} An object representing a valid reference or null if it is invalid.
|
|
248
|
+
*/
|
|
249
|
+
export function parseA1Ref (refString, { allowNamed = true, allowTernary = false } = {}) {
|
|
250
|
+
const d = parseRef(refString, { allowNamed, allowTernary, r1c1: false });
|
|
170
251
|
if (d && (d.r0 || d.name)) {
|
|
171
252
|
let range = null;
|
|
172
253
|
if (d.r0) {
|
|
@@ -185,13 +266,70 @@ export function parseA1Ref (ref, { allowNamed = true, allowTernary = false } = {
|
|
|
185
266
|
return null;
|
|
186
267
|
}
|
|
187
268
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Get an A1-style string representation of a reference object.
|
|
271
|
+
*
|
|
272
|
+
* ```js
|
|
273
|
+
* stringifyA1Ref({
|
|
274
|
+
* context: [ 'Sheet1' ],
|
|
275
|
+
* range: {
|
|
276
|
+
* top: 0,
|
|
277
|
+
* left: 0,
|
|
278
|
+
* bottom: 1,
|
|
279
|
+
* right: 1,
|
|
280
|
+
* $top: true,
|
|
281
|
+
* $left: false,
|
|
282
|
+
* $bottom: false,
|
|
283
|
+
* $right: true
|
|
284
|
+
* }
|
|
285
|
+
* });
|
|
286
|
+
* // => 'Sheet1!A$1:$B2'
|
|
287
|
+
* ```
|
|
288
|
+
*
|
|
289
|
+
* @param {Object} refObject A reference object
|
|
290
|
+
* @return {Object} The reference in A1-style string format
|
|
291
|
+
*/
|
|
292
|
+
export function stringifyA1Ref (refObject) {
|
|
293
|
+
return stringifyPrefix(refObject) + (
|
|
294
|
+
refObject.name ? refObject.name : toA1(refObject.range)
|
|
191
295
|
);
|
|
192
296
|
}
|
|
193
297
|
|
|
194
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Fill the any missing bounds in range objects. Top will be set to 0, bottom to
|
|
300
|
+
* 1048575, left to 0, and right to 16383, if they are `null` or `undefined`.
|
|
301
|
+
*
|
|
302
|
+
* ```js
|
|
303
|
+
* addA1RangeBounds({
|
|
304
|
+
* context: [ 'Sheet1' ],
|
|
305
|
+
* range: {
|
|
306
|
+
* top: 0,
|
|
307
|
+
* left: 0,
|
|
308
|
+
* bottom: 1,
|
|
309
|
+
* $top: true,
|
|
310
|
+
* $left: false,
|
|
311
|
+
* $bottom: false,
|
|
312
|
+
* }
|
|
313
|
+
* });
|
|
314
|
+
* // => {
|
|
315
|
+
* // context: [ 'Sheet1' ],
|
|
316
|
+
* // range: {
|
|
317
|
+
* // top: 0,
|
|
318
|
+
* // left: 0,
|
|
319
|
+
* // bottom: 1,
|
|
320
|
+
* // right: 16383,
|
|
321
|
+
* // $top: true,
|
|
322
|
+
* // $left: false,
|
|
323
|
+
* // $bottom: false,
|
|
324
|
+
* // $right: false
|
|
325
|
+
* // }
|
|
326
|
+
* // }
|
|
327
|
+
* ```
|
|
328
|
+
*
|
|
329
|
+
* @param {Object} range The range part of a reference object.
|
|
330
|
+
* @return {Object} same range with missing bounds filled in.
|
|
331
|
+
*/
|
|
332
|
+
export function addA1RangeBounds (range) {
|
|
195
333
|
if (range.top == null) {
|
|
196
334
|
range.top = 0;
|
|
197
335
|
range.$top = false;
|
|
@@ -210,15 +348,3 @@ export function addRangeBounds (range) {
|
|
|
210
348
|
}
|
|
211
349
|
return range;
|
|
212
350
|
}
|
|
213
|
-
|
|
214
|
-
export default {
|
|
215
|
-
fromCol,
|
|
216
|
-
toCol,
|
|
217
|
-
toRelative,
|
|
218
|
-
toAbsolute,
|
|
219
|
-
to: toA1,
|
|
220
|
-
from: fromA1,
|
|
221
|
-
parse: parseA1Ref,
|
|
222
|
-
addBounds: addRangeBounds,
|
|
223
|
-
stringify: stringifyA1Ref
|
|
224
|
-
};
|
package/lib/a1.spec.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from './a1.js';
|
|
14
14
|
import { MAX_COLS, MAX_ROWS } from './constants.js';
|
|
15
15
|
|
|
16
|
-
Test.prototype.isA1Equal = function
|
|
16
|
+
Test.prototype.isA1Equal = function isA1Equal (expr, expect, opts) {
|
|
17
17
|
if (expect) {
|
|
18
18
|
expect = {
|
|
19
19
|
context: [],
|
|
@@ -224,7 +224,14 @@ test('A1 serialization', t => {
|
|
|
224
224
|
t.is(toA1({ top: 0, right: 3, bottom: 0 }), 'D1:1', '1:D1 → D1:1');
|
|
225
225
|
t.is(toA1({ top: 0, left: 3, bottom: 0, $top: true }), 'D$1:1', 'D$1:1');
|
|
226
226
|
t.is(toA1({ top: 0, left: 3, bottom: 0, $left: true }), '$D1:1', '$D1:1');
|
|
227
|
-
t.is(toA1({ top: 0, left: 3, bottom: 0, $
|
|
227
|
+
t.is(toA1({ top: 0, left: 3, bottom: 0, $left: true }), '$D1:1', '$D1:1');
|
|
228
|
+
// allow skipping right/bottom for cells
|
|
229
|
+
t.is(toA1({ top: 0, left: 0 }), 'A1', 'A1');
|
|
230
|
+
// clamp the range at min/max dimensions
|
|
231
|
+
t.is(toA1({ top: -10, bottom: -5, left: -10, right: -5 }), 'A1', 'A1');
|
|
232
|
+
t.is(toA1({ top: 15e5, bottom: 15e5, left: 20000, right: 20000 }), 'XFD1048576', 'XFD1048576');
|
|
233
|
+
t.is(toA1({ top: 2, bottom: 2, left: 2.5, right: 2.5 }), 'C3', 'C3');
|
|
234
|
+
t.is(toA1({ top: 1.5, bottom: 2.5, left: 4.5, right: 8.5 }), 'E2:I3', 'E2:I3');
|
|
228
235
|
t.end();
|
|
229
236
|
});
|
|
230
237
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { REF_RANGE, REF_BEAM, REF_TERNARY, UNKNOWN } from './constants.js';
|
|
2
2
|
import { parseA1Ref } from './a1.js';
|
|
3
3
|
|
|
4
4
|
function getIDer () {
|
|
@@ -46,8 +46,53 @@ function isEquivalent (refA, refB) {
|
|
|
46
46
|
return true;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Runs through a list of tokens and adds extra attributes such as matching
|
|
51
|
+
* parens and ranges.
|
|
52
|
+
*
|
|
53
|
+
* The `context` parameter defines default reference attributes:
|
|
54
|
+
* `{ workbookName: 'report.xlsx', sheetName: 'Sheet1' }`.
|
|
55
|
+
* If supplied, these are used to match `A1` to `Sheet1!A1`.
|
|
56
|
+
*
|
|
57
|
+
* All tokens will be tagged with a `.depth` number value to indicating the
|
|
58
|
+
* level of nesting in parentheses as well as an `.index` number indicating
|
|
59
|
+
* their zero based position in the list.
|
|
60
|
+
*
|
|
61
|
+
* The returned output will be the same array of tokens but the following
|
|
62
|
+
* properties will added to tokens (as applicable):
|
|
63
|
+
*
|
|
64
|
+
* #### Parentheses ( )
|
|
65
|
+
*
|
|
66
|
+
* Matching parens will be tagged with `.groupId` string identifier as well as
|
|
67
|
+
* a `.depth` number value (indicating the level of nesting).
|
|
68
|
+
*
|
|
69
|
+
* Closing parens without a counterpart will be tagged with `.error`
|
|
70
|
+
* (boolean true).
|
|
71
|
+
*
|
|
72
|
+
* #### Curly brackets { }
|
|
73
|
+
*
|
|
74
|
+
* Matching curly brackets will be tagged with `.groupId` string identifier.
|
|
75
|
+
* These may not be nested in Excel.
|
|
76
|
+
*
|
|
77
|
+
* Closing curly brackets without a counterpart will be tagged with `.error`
|
|
78
|
+
* (boolean `true`).
|
|
79
|
+
*
|
|
80
|
+
* #### Ranges (`REF_RANGE` or `REF_BEAM` type tokens)
|
|
81
|
+
*
|
|
82
|
+
* All ranges will be tagged with `.groupId` string identifier regardless of
|
|
83
|
+
* the number of times they occur.
|
|
84
|
+
*
|
|
85
|
+
* #### Tokens of type `UNKNOWN`
|
|
86
|
+
*
|
|
87
|
+
* All will be tagged with `.error` (boolean `true`).
|
|
88
|
+
*
|
|
89
|
+
* @param {Array<Object>} tokenlist An array of tokens (from `tokenize()`)
|
|
90
|
+
* @param {Object} [context={}] A contest used to match `A1` to `Sheet1!A1`.
|
|
91
|
+
* @param {string} [context.sheetName=''] An implied sheet name ('Sheet1')
|
|
92
|
+
* @param {string} [context.workbookName=''] An implied workbook name ('report.xlsx')
|
|
93
|
+
* @return {Array<Object>} The input array with the enchanced tokens
|
|
94
|
+
*/
|
|
95
|
+
export function addTokenMeta (tokens, { sheetName = '', workbookName = '' } = {}) {
|
|
51
96
|
const parenStack = [];
|
|
52
97
|
let arrayStart = null;
|
|
53
98
|
const uid = getIDer();
|
|
@@ -95,8 +140,8 @@ export function addMeta (tokens, { sheetName = '', workbookName = '' } = {}) {
|
|
|
95
140
|
}
|
|
96
141
|
arrayStart = null;
|
|
97
142
|
}
|
|
98
|
-
else if (token.type ===
|
|
99
|
-
const ref = parseA1Ref(token.value, {
|
|
143
|
+
else if (token.type === REF_RANGE || token.type === REF_BEAM || token.type === REF_TERNARY) {
|
|
144
|
+
const ref = parseA1Ref(token.value, { allowTernary: true });
|
|
100
145
|
if (ref && ref.range) {
|
|
101
146
|
ref.source = token.value;
|
|
102
147
|
if (!ref.context.length) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { test, Test } from 'tape';
|
|
2
|
-
import { FX_PREFIX, OPERATOR, NUMBER,
|
|
3
|
-
import {
|
|
2
|
+
import { FX_PREFIX, OPERATOR, NUMBER, REF_RANGE, REF_BEAM, FUNCTION, WHITESPACE } from './constants.js';
|
|
3
|
+
import { addTokenMeta } from './addTokenMeta.js';
|
|
4
4
|
import { tokenize } from './lexer.js';
|
|
5
5
|
|
|
6
6
|
Test.prototype.isMetaTokens = function isTokens (expr, expect, opts) {
|
|
7
|
-
const actual =
|
|
7
|
+
const actual = addTokenMeta(tokenize(expr), opts);
|
|
8
8
|
if (actual.length === expect.length) {
|
|
9
9
|
actual.forEach((d, i) => {
|
|
10
10
|
const keys = Object.keys(d).concat(Object.keys(expect[i]));
|
|
@@ -55,34 +55,34 @@ test('add extra meta to operators', t => {
|
|
|
55
55
|
// group ranges if they are equivalent
|
|
56
56
|
t.isMetaTokens("=B11,B11:B12,'Sheet11'!B11,SHEET1!$B11,sheet1!$b$11,A1:B11,[foo]Sheet1!B11,'[foo]Sheet1'!B11", [
|
|
57
57
|
{ index: 0, depth: 0, type: FX_PREFIX, value: '=' },
|
|
58
|
-
{ index: 1, depth: 0, type:
|
|
58
|
+
{ index: 1, depth: 0, type: REF_RANGE, value: 'B11', groupId: 'fxg1' },
|
|
59
59
|
{ index: 2, depth: 0, type: OPERATOR, value: ',' },
|
|
60
|
-
{ index: 3, depth: 0, type:
|
|
60
|
+
{ index: 3, depth: 0, type: REF_RANGE, value: 'B11:B12', groupId: 'fxg2' },
|
|
61
61
|
{ index: 4, depth: 0, type: OPERATOR, value: ',' },
|
|
62
|
-
{ index: 5, depth: 0, type:
|
|
62
|
+
{ index: 5, depth: 0, type: REF_RANGE, value: "'Sheet11'!B11", groupId: 'fxg3' },
|
|
63
63
|
{ index: 6, depth: 0, type: OPERATOR, value: ',' },
|
|
64
|
-
{ index: 7, depth: 0, type:
|
|
64
|
+
{ index: 7, depth: 0, type: REF_RANGE, value: 'SHEET1!$B11', groupId: 'fxg1' },
|
|
65
65
|
{ index: 8, depth: 0, type: OPERATOR, value: ',' },
|
|
66
|
-
{ index: 9, depth: 0, type:
|
|
66
|
+
{ index: 9, depth: 0, type: REF_RANGE, value: 'sheet1!$b$11', groupId: 'fxg1' },
|
|
67
67
|
{ index: 10, depth: 0, type: OPERATOR, value: ',' },
|
|
68
|
-
{ index: 11, depth: 0, type:
|
|
68
|
+
{ index: 11, depth: 0, type: REF_RANGE, value: 'A1:B11', groupId: 'fxg4' },
|
|
69
69
|
{ index: 12, depth: 0, type: OPERATOR, value: ',' },
|
|
70
|
-
{ index: 13, depth: 0, type:
|
|
70
|
+
{ index: 13, depth: 0, type: REF_RANGE, value: '[foo]Sheet1!B11', groupId: 'fxg1' },
|
|
71
71
|
{ index: 14, depth: 0, type: OPERATOR, value: ',' },
|
|
72
|
-
{ index: 15, depth: 0, type:
|
|
72
|
+
{ index: 15, depth: 0, type: REF_RANGE, value: "'[foo]Sheet1'!B11", groupId: 'fxg1' }
|
|
73
73
|
], { sheetName: 'Sheet1', workbookName: 'foo' });
|
|
74
74
|
|
|
75
75
|
t.isMetaTokens('=A:A,1:1,Sheet1!A:A:1:1,[foo]Sheet1!1:1', [
|
|
76
76
|
{ index: 0, depth: 0, type: FX_PREFIX, value: '=' },
|
|
77
|
-
{ index: 1, depth: 0, type:
|
|
77
|
+
{ index: 1, depth: 0, type: REF_BEAM, value: 'A:A', groupId: 'fxg1' },
|
|
78
78
|
{ index: 2, depth: 0, type: OPERATOR, value: ',' },
|
|
79
|
-
{ index: 3, depth: 0, type:
|
|
79
|
+
{ index: 3, depth: 0, type: REF_BEAM, value: '1:1', groupId: 'fxg2' },
|
|
80
80
|
{ index: 4, depth: 0, type: OPERATOR, value: ',' },
|
|
81
|
-
{ index: 5, depth: 0, type:
|
|
81
|
+
{ index: 5, depth: 0, type: REF_BEAM, value: 'Sheet1!A:A', groupId: 'fxg1' },
|
|
82
82
|
{ index: 6, depth: 0, type: OPERATOR, value: ':' },
|
|
83
|
-
{ index: 7, depth: 0, type:
|
|
83
|
+
{ index: 7, depth: 0, type: REF_BEAM, value: '1:1', groupId: 'fxg2' },
|
|
84
84
|
{ index: 8, depth: 0, type: OPERATOR, value: ',' },
|
|
85
|
-
{ index: 9, depth: 0, type:
|
|
85
|
+
{ index: 9, depth: 0, type: REF_BEAM, value: '[foo]Sheet1!1:1', groupId: 'fxg2' }
|
|
86
86
|
], { sheetName: 'Sheet1', workbookName: 'foo' });
|
|
87
87
|
|
|
88
88
|
t.isMetaTokens('=SUM((1, 2), {3, 4})', [
|
package/lib/constants.js
CHANGED
|
@@ -8,12 +8,22 @@ export const WHITESPACE = 'whitespace';
|
|
|
8
8
|
export const STRING = 'string';
|
|
9
9
|
export const CONTEXT_QUOTE = 'context_quote';
|
|
10
10
|
export const CONTEXT = 'context';
|
|
11
|
-
export const
|
|
12
|
-
export const
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
11
|
+
export const REF_RANGE = 'range';
|
|
12
|
+
export const REF_BEAM = 'range_beam';
|
|
13
|
+
export const REF_TERNARY = 'range_ternary';
|
|
14
|
+
export const REF_NAMED = 'range_named';
|
|
15
|
+
export const REF_STRUCT = 'structured';
|
|
15
16
|
export const FX_PREFIX = 'fx_prefix';
|
|
16
17
|
export const UNKNOWN = 'unknown';
|
|
17
18
|
|
|
19
|
+
export const UNARY = 'UnaryExpression';
|
|
20
|
+
export const BINARY = 'BinaryExpression';
|
|
21
|
+
export const REFERENCE = 'ReferenceIdentifier';
|
|
22
|
+
export const LITERAL = 'Literal';
|
|
23
|
+
export const ERROR_LITERAL = 'ErrorLiteral';
|
|
24
|
+
export const CALL = 'CallExpression';
|
|
25
|
+
export const ARRAY = 'ArrayExpression';
|
|
26
|
+
export const IDENTIFIER = 'Identifier';
|
|
27
|
+
|
|
18
28
|
export const MAX_COLS = 2 ** 14 - 1; // 16383
|
|
19
29
|
export const MAX_ROWS = 2 ** 20 - 1; // 1048575
|