@borgar/fx 3.1.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 CHANGED
@@ -1,18 +1,31 @@
1
1
  {
2
- parser: '@babel/eslint-parser',
3
- extends: [ '@borgar/eslint-config' ],
4
- env: {
5
- es6: true,
6
- node: true,
2
+ "parser": "@babel/eslint-parser",
3
+ "extends": [ "@borgar/eslint-config" ],
4
+ "env": {
5
+ "es6": true,
6
+ "node": true,
7
7
  },
8
- parserOptions: {
9
- ecmaVersion: 2021,
10
- sourceType: 'module',
11
- requireConfigFile: false,
8
+ "parserOptions": {
9
+ "ecmaVersion": 2021,
10
+ "sourceType": "module",
11
+ "requireConfigFile": false,
12
12
  },
13
- rules: {
13
+ "rules": {
14
+ "max-len": [ "error", {
15
+ "code": 120,
16
+ "comments": 80,
17
+ "tabWidth": 2,
18
+ "ignoreUrls": true,
19
+ "ignoreStrings": true,
20
+ "ignoreTemplateLiterals": true,
21
+ "ignoreRegExpLiterals": true,
22
+ "ignorePattern": "^\\s*\\*\\s*@(param|return|property)\\s"
23
+ } ],
24
+ "no-trailing-spaces": [ "error", {
25
+ "ignoreComments": true
26
+ } ]
14
27
  },
15
- globals: {
16
- Promise,
28
+ "globals": {
29
+ "Promise"
17
30
  }
18
31
  }
@@ -0,0 +1,17 @@
1
+ {
2
+ "source": {
3
+ "includePattern": ".+\\.js(doc|x)?$",
4
+ "excludePattern": "((^|\\/|\\\\)_|spec\\.js$)"
5
+ },
6
+ "opts": {
7
+ "template": "./.jsdoc",
8
+ "encoding": "utf8",
9
+ "destination": "./docs/"
10
+ },
11
+ "templates": {
12
+ "default": {
13
+ "includeDate": false,
14
+ "outputSourceFiles": false
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,195 @@
1
+ /* eslint-disable no-undefined, no-console */
2
+
3
+ function formatType (d, inTable = false) {
4
+ return d.names?.map(n => `\`${n}\``).join(inTable ? ' \\| ' : ' | ') || '';
5
+ }
6
+
7
+ function formatArguments (args) {
8
+ let inOpt = false;
9
+ let r = args.reduce((a, d, i) => {
10
+ if (d.name.includes('.')) {
11
+ return a;
12
+ }
13
+ if (inOpt && !d.optional) {
14
+ a += ']_';
15
+ inOpt = false;
16
+ }
17
+ if (i) {
18
+ a += ', ';
19
+ }
20
+ if (!inOpt && d.optional) {
21
+ a += '_[';
22
+ inOpt = true;
23
+ }
24
+ a += d.name;
25
+ if (d.defaultvalue) {
26
+ a += ' = `' + d.defaultvalue + '`';
27
+ }
28
+ return a;
29
+ }, '');
30
+ if (inOpt) {
31
+ r += ']_';
32
+ }
33
+ return r;
34
+ }
35
+
36
+ function repValue (v) {
37
+ if (v === undefined) {
38
+ return '';
39
+ }
40
+ if (v === "''") {
41
+ // FIXME: should transform all '' strings to "" style
42
+ v = '""';
43
+ }
44
+ return '`' + v + '`';
45
+ }
46
+
47
+ function repArgName (name, optional = false) {
48
+ const names = name.split('.');
49
+ const nPath = names.length > 1 ? '.' + names.slice(1).join('.') : '';
50
+ return (optional ? '_[' : '') + names[0] + (optional ? ']_' : '') + nPath;
51
+ }
52
+
53
+ function listToTable (args) {
54
+ const useDefaults = args.some(d => !!d.defaultvalue);
55
+ const table = useDefaults
56
+ ? [
57
+ '| Name | Type | Default | Description |',
58
+ '| ---- | ---- | ------- | ----------- |' ]
59
+ : [
60
+ '| Name | Type | Description |',
61
+ '| ---- | ---- | ----------- |' ];
62
+ args.forEach(d => {
63
+ const row = [ repArgName(d.name, d.optional) ];
64
+ row.push(formatType(d.type, true));
65
+ useDefaults && row.push(repValue(d.defaultvalue));
66
+ row.push(transformLinks(d.description));
67
+ table.push('| ' + row.join(' | ') + ' |');
68
+ });
69
+
70
+ return table.join('\n');
71
+ }
72
+
73
+ function adjustLineBreaks (md) {
74
+ return md.trim().replace(
75
+ /(?:```([^\0]*?)```|(\S)\n(?!\n))/g,
76
+ (a, b, c) => {
77
+ return (c ? (c + ' ') : a);
78
+ }
79
+ );
80
+ }
81
+
82
+ function formatSee (see) {
83
+ if (see) {
84
+ if (/^[a-z][a-z0-9]*$/i.test(see)) {
85
+ see = `{@link ${see}}`;
86
+ }
87
+ return `\n**See also:** ${transformLinks(see)}.\n`;
88
+ }
89
+ return '';
90
+ }
91
+
92
+ const re_links = /(?:\[([^\]]+)\])?\{@link(?:code|plain)? ([^} ]+)([^}]+)?\}/g;
93
+ function transformLinks (md) {
94
+ return md.replace(re_links, (a, preLabel, href, postLabel) => {
95
+ const hash = /^[a-z][a-z0-9]*$/i.test(href) ? '#' : '';
96
+ return `[${preLabel || postLabel || href}](${hash}${href})`;
97
+ });
98
+ }
99
+
100
+ function formatHeading (text, level = 2) {
101
+ return '#'.repeat(level || 1) + ' ' + text;
102
+ }
103
+
104
+ const format = {
105
+ function: d => {
106
+ return [
107
+ formatHeading(`<a name="${d.name}" href="#${d.name}">#</a> ${d.name}( ${formatArguments(d.params)} ) ⇒ ${formatType(d.returns[0].type)}`, 3),
108
+ adjustLineBreaks(transformLinks(d.description)),
109
+ formatSee(d.see),
110
+ formatHeading('Parameters', 4),
111
+ listToTable(d.params),
112
+ formatHeading('Returns', 4),
113
+ formatType(d.returns[0].type) + ' – ' + d.returns[0].description
114
+ ];
115
+ },
116
+ constant: d => {
117
+ return [
118
+ formatHeading(`<a name="${d.name}" href="#${d.name}">#</a> ${d.name} ⇒ ${formatType(d.type)}`, 3),
119
+ adjustLineBreaks(transformLinks(d.description)),
120
+ formatSee(d.see),
121
+ formatHeading('Properties', 4),
122
+ listToTable(d.properties)
123
+ ];
124
+ }
125
+ };
126
+
127
+ const categoryHeadings = {
128
+ package: null,
129
+ function: 'Functions',
130
+ constant: 'Constants'
131
+ };
132
+
133
+ exports.publish = (data, { destination }) => {
134
+ data({ undocumented: true }).remove();
135
+ const docs = data().get();
136
+
137
+ const test = '';
138
+ if (test) {
139
+ const s = docs.find(d => d.name === test);
140
+ const o = format[s.kind](s);
141
+ console.log(o.join('\n\n'));
142
+ return;
143
+ }
144
+
145
+ const categories = {};
146
+ docs.forEach(d => {
147
+ if (!categories[d.kind]) {
148
+ categories[d.kind] = [];
149
+ }
150
+ categories[d.kind].push(d);
151
+ });
152
+
153
+ let output = [];
154
+
155
+ Object.keys(categories)
156
+ .sort()
157
+ .forEach(kind => {
158
+ const heading = categoryHeadings[kind];
159
+ if (heading == null) { return; }
160
+ // emit category heading
161
+ output.push(formatHeading(heading, 2));
162
+ // emit all members belonging to that category
163
+ docs
164
+ .filter(d => d.kind === kind)
165
+ .filter(d => d.access !== 'private')
166
+ .sort((a, b) => {
167
+ if (a.name < b.name) { return -1; }
168
+ if (a.name > b.name) { return 1; }
169
+ return NaN;
170
+ })
171
+ .forEach((d, i) => {
172
+ if (i) {
173
+ output.push('---');
174
+ }
175
+ if (!format[d.kind]) {
176
+ console.error('Unsupported member type ' + d.kind);
177
+ process.exit(1);
178
+ }
179
+ output.push(...format[d.kind](d));
180
+ });
181
+ });
182
+
183
+ output = output
184
+ .map(d => d.trim())
185
+ .filter(Boolean);
186
+
187
+ // console.log(docs.map(d => d.name).sort());
188
+ if (destination === 'console') {
189
+ console.log(output.join('\n\n') + '\n');
190
+ }
191
+ else {
192
+ console.error('This template only supports output to the console. Use the option "-d console" when you run JSDoc.');
193
+ process.exit(1);
194
+ }
195
+ };
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # _fx_
2
2
 
3
- This is a collection utilities to work with Excel formula code, specifically syntax highlighting.
3
+ A tokenizer, parser, and other utilities to work with Excel formula code, specifically syntax highlighting.
4
+
5
+ This utility is partially developed as tooling for [GRID – The new face of spreadsheets](https://grid.is/), to which it owes a debt of gratitude.
4
6
 
5
- This utility is developed as tooling for [GRID – The new face of spreadsheets](https://grid.is/), to which it owes a debt of gratitude.
6
7
 
7
8
  ## Installing
8
9
 
@@ -10,319 +11,15 @@ The library is also provided as an ES6 module in an NPM package:
10
11
 
11
12
  $ npm install @borgar/fx
12
13
 
13
- ## API
14
-
15
- ### <a name="tokenize" href="#tokenize">#</a> **tokenize**( _formula [, options]_ )
16
-
17
- * `formula` should be a string (an Excel formula).
18
-
19
- * `options` are set as an object of keys: `tokenize(formula, { option: true })`. Supported options are:
20
-
21
- | name | default | effect |
22
- |- |- |-
23
- | `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.
24
- | `emitRanges` | `false` | Adds offset ranges on the tokens: `{ range: [ start, end ] }`
25
- | `mergeRanges` | `true` | Should ranges be returned as whole references (`Sheet1!A1:B2`) or as separate tokens for each part: (`Sheet1`,`!`,`A1`,`:`,`B2`). This is the same as calling [`mergeRanges`](#mergeRanges)
26
- | `negativeNumbers` | `true` | Merges unary minuses with their immediately following number tokens (`-`,`1`) => `-1`
27
- | `r1c1` | `false` | Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
28
-
29
- The returned output will be an array of objects representing the tokens:
30
-
31
- ```js
32
- [
33
- { type: FX_PREFIX, value: '=' },
34
- { type: FUNCTION, value: 'SUM' },
35
- { type: OPERATOR, value: '(' },
36
- { type: RANGE, value: 'A1:B2' },
37
- { type: OPERATOR, value: ')' }
38
- ]
39
- ```
40
-
41
- Token types may be found as an Object as the `tokenTypes` export on the package (`import {tokenTypes} from '@borgar/fx';`):
42
-
43
- ```js
44
- tokenTypes = {
45
- OPERATOR: "operator",
46
- BOOLEAN: "bool",
47
- ERROR: "error",
48
- NUMBER: "number",
49
- FUNCTION: "func",
50
- NEWLINE: "newline",
51
- WHITESPACE: "whitespace",
52
- STRING: "string",
53
- CONTEXT_QUOTE: "context_quote",
54
- CONTEXT: "context",
55
- RANGE: "range",
56
- RANGE_BEAM: "range_beam",
57
- RANGE_NAMED: "range_named",
58
- RANGE_TERNARY: "range_ternary",
59
- FX_PREFIX: "fx_prefix",
60
- UNKNOWN: "unknown"
61
- }
62
- ```
63
-
64
- To support syntax highlighting as you type, `STRING` tokens are allowed to be "unterminated". For example, the incomplete formula `="Hello world` would be tokenized as:
65
-
66
- ```js
67
- [
68
- { type: FX_PREFIX, value: '=' },
69
- { type: STRING, value: '"Hello world', unterminated: true },
70
- ]
71
- ```
72
-
73
- ### <a name="translateToA1" href="#translateToA1">#</a> **translateToA1**( _formula, anchorCell_ )
74
-
75
- Translates ranges in a formula or list of tokens from relative R1C1 syntax to absolute A1 syntax.
76
-
77
- * `formula` should be a string (an Excel formula) or a token list.
78
-
79
- * `anchorCell` should be a simple string reference to an A1 cell (`AF123` or `$C$5`).
80
-
81
- Returns the same formula with the ranges translated. If an array of tokens was supplied, then the same array is returned (be careful that `mergeRanges` must be false).
82
-
83
- ```js
84
- translateToA1("=SUM(RC[1],R2C5,Sheet!R3C5)", "D10");
85
- // => "=SUM(E10,$E$2,Sheet!$E$3)");
86
- ```
87
-
88
-
89
- ### <a name="translateToRC" href="#translateToRC">#</a> **translateToRC**( _formula, anchorCell_ )
90
-
91
- Translates ranges in a formula or list of tokens from absolute A1 syntax to relative R1C1 syntax.
92
-
93
- * `formula` should be a string (an Excel formula) or a token list.
94
-
95
- * `anchorCell` should be a simple string reference to an A1 cell (`AF123` or `$C$5`).
96
-
97
- Returns the same formula with the ranges translated. If an array of tokens was supplied, then the same array is returned (be careful that `mergeRanges` must be false).
98
-
99
- ```js
100
- translateToRC("=SUM(E10,$E$2,Sheet!$E$3)", "D10");
101
- // => "=SUM(RC[1],R2C5,Sheet!R3C5)");
102
- ```
103
-
104
-
105
- ### <a name="addMeta" href="#addMeta">#</a> **addMeta**( _tokenlist [, context]_ )
106
-
107
- Runs through a list of tokens and adds extra attributes such as matching parens and ranges.
108
-
109
- * `tokenlist` should be a token list (from `tokenize()`).
110
-
111
- * `context` should be an object containing default reference attributes: `{ workbookName: 'report.xlsx', sheetName: 'Sheet1' }`. If supplied, these are used to match `A1` to `Sheet1!A1`)
112
-
113
- All tokens will be tagged with a `.depth` number value to indicating the level of nesting in parentheses as well as an `.index` number indicating their zero based position in the list.
114
-
115
- The returned output will be the same array of tokens but the following properties will added to tokens (as applicable):
116
-
117
- #### Parentheses ( )
118
-
119
- Matching parens will be tagged with `.groupId` string identifier as well as a `.depth` number value (indicating the level of nesting).
120
-
121
- Closing parens without a counterpart will be tagged with `.error` (boolean true).
122
-
123
- #### Curly brackets { }
124
-
125
- Matching curly brackets will be tagged with `.groupId` string identifier. These may not be nested in Excel.
126
-
127
- Closing curly brackets without a counterpart will be tagged with `.error` (boolean `true`).
128
-
129
- #### Ranges (`RANGE` or `RANGE_BEAM` type tokens)
130
-
131
- All ranges will be tagged with `.groupId` string identifier regardless of the number of times they occur.
132
-
133
- #### Tokens of type `UNKNOWN`
134
-
135
- All will be tagged with `.error` (boolean `true`).
136
-
137
-
138
- ### <a name="mergeRanges" href="#mergeRanges">#</a> **mergeRanges**( _tokenlist_ )
139
-
140
- Given a tokenlist, returns a new list with ranges returned as whole references (`Sheet1!A1:B2`) rather than separate tokens for each part: (`Sheet1`,`!`,`A1`,`:`,`B2`).
141
-
142
-
143
- ### <a name="fixRanges" href="#fixRanges">#</a> **fixRanges**( _formula[, { addBounds: true } ]_ )
144
-
145
- Normalizes A1 style ranges in a formula or list of tokens so that the top and left coordinates of the range are on the left-hand side of a colon operator:
146
-
147
- * `B2:A1` → `A1:B2`
148
- * `1:A1` → `A1:1`
149
- * `A:A1` → `A1:A`
150
- * `B:A` → `A:B`
151
- * `2:1` → `1:2`
152
- * `A1:A1` → `A1`
153
-
154
- When `{ addBounds: true }` is passed as an option, the missing bounds are also added. This can be done to ensure Excel compatible ranges. The fixes then additionally include:
155
-
156
- * `1:A1` → `A1:1` → `1:1`
157
- * `A:A1` → `A1:A` → `A:A`
158
- * `A1:A` → `A:A`
159
- * `A1:1` → `A:1`
160
- * `B2:B` → `B2:1048576`
161
- * `B2:2` → `B2:XFD2`
162
-
163
- Returns the same formula with the ranges updated. If an array of tokens was supplied, then a new array is returned.
164
-
165
- ### <a name="isRange" href="#isRange">#</a> **isRange**( _token_ )
166
-
167
- Returns `true` if the input is a token that has a type of either RANGE (`A1` or `A1:B2`), RANGE_TERNARY (`A1:A`, `A1:1`, `1:A1`, or `A:A1`), or RANGE_BEAM (`A:A` or `1:1`). In all other cases `false` is returned.
168
-
169
-
170
- ### <a name="isReference" href="#isReference">#</a> **isReference**( _token_ )
171
-
172
- Returns `true` if the input is a token of type RANGE (`A1` or `A1:B2`), RANGE_TERNARY (`A1:A`, `A1:1`, `1:A1`, or `A:A1`), RANGE_BEAM (`A:A` or `1:1`), or RANGE_NAMED (`myrange`). In all other cases `false` is returned.
173
-
174
-
175
- ### .a1:
176
-
177
- An object of methods to interpret and manipulate A1 style references.
178
-
179
- #### <a name="a1.parse" href="#a1.parse">#</a> **.parse**( _refString[, { allowNamed: true, allowTernary: true } ]_ )
180
-
181
- Parse a string reference into an object representing it.
182
-
183
- ```js
184
- import { a1 } from '@borgar/fx';
185
- a1.parse('Sheet1!A$1:$B2');
186
- // => {
187
- // context: [ 'Sheet1' ],
188
- // range: {
189
- // top: 0,
190
- // left: 0,
191
- // bottom: 1,
192
- // right: 1
193
- // $top: true,
194
- // $left: false,
195
- // $bottom: false,
196
- // $right: true
197
- // }
198
- // }
199
- ```
200
-
201
- For A:A or A1:A style ranges, null will be used for any dimensions that the syntax does not specify:
202
-
203
- #### <a name="a1.stringify" href="#a1.stringify">#</a> **.stringify**( _refObject_ )
204
-
205
- Get a string representation of a reference object.
206
-
207
- ```js
208
- import { a1 } from '@borgar/fx';
209
- a1.stringify({
210
- context: [ 'Sheet1' ],
211
- range: {
212
- top: 0,
213
- left: 0,
214
- bottom: 1,
215
- right: 1,
216
- $top: true,
217
- $left: false,
218
- $bottom: false,
219
- $right: true
220
- }
221
- });
222
- // => 'Sheet1!A$1:$B2'
223
- ```
224
-
225
- #### <a name="a1.addBounds" href="#a1.addBounds">#</a> **.addBounds**( _refObject_ )
226
-
227
- Fill the any missing bounds in range objects. Top will be set to 0, bottom to 1048575, left to 0, and right to 16383, if they are `null` or `undefined`.
228
-
229
- ```js
230
- import { a1 } from '@borgar/fx';
231
- a1.addBounds({
232
- context: [ 'Sheet1' ],
233
- range: {
234
- top: 0,
235
- left: 0,
236
- bottom: 1,
237
- $top: true,
238
- $left: false,
239
- $bottom: false,
240
- }
241
- });
242
- // => {
243
- // context: [ 'Sheet1' ],
244
- // range: {
245
- // top: 0,
246
- // left: 0,
247
- // bottom: 1,
248
- // right: 16383,
249
- // $top: true,
250
- // $left: false,
251
- // $bottom: false,
252
- // $right: false
253
- // }
254
- // }
255
- ```
256
-
257
- #### <a name="a1.to" href="#a1.to">#</a> **.to**( _rangeObject_ )
258
-
259
- Stringify a range object ([see above](#a1.parse)) into A1 syntax.
260
-
261
- #### <a name="a1.from" href="#a1.from">#</a> **.from**( _rangeString_ )
262
-
263
- Parse a simple string reference to an A1 range into a range object ([see above](#a1.parse)). Will accept `A1`, `A2`, `A:A`, or `1:1`.
264
-
265
- #### <a name="a1.fromCol" href="#a1.fromCol">#</a> **.fromCol**( _columnString_ )
266
-
267
- Convert a column string representation to a 0 based offset number (`"C"` = `2`). The method expects a valid column identifier made up of _only_ A-Z letters, which may be either upper or lower case. Other input will return garbage.
268
-
269
- #### <a name="a1.toCol" href="#a1.toCol">#</a> **.toCol**( _columnNumber_ )
270
-
271
- Convert a 0 based offset number to a column string representation (`2` = `"C"`). The method expects a number between 0 and 16383. Other input will return garbage.
272
-
273
- ### .rc:
274
-
275
- An object of methods to interpret and manipulate R1C1 style references.
276
-
277
- #### <a name="rc.parse" href="#rc.parse">#</a> **.parse**( _refString[, { allowNamed: true, allowTernary: true } ]_ )
278
-
279
- Parse a string reference into an object representing it.
280
-
281
- ```js
282
- import { rc } from '@borgar/fx';
283
- rc.parse('Sheet1!R[9]C9:R[9]C9');
284
- // => {
285
- // context: [ 'Sheet1' ],
286
- // range: {
287
- // r0: 9,
288
- // c0: 8,
289
- // r1: 9,
290
- // c1: 8,
291
- // $c0: true,
292
- // $c1: true
293
- // $r0: false,
294
- // $r1: false
295
- // }
296
- // }
297
- ```
298
-
299
- #### <a name="rc.stringify" href="#rc.stringify">#</a> **.stringify**( _refObject_ )
300
14
 
301
- Get a string representation of a reference object.
15
+ ## Documentation
302
16
 
303
- ```js
304
- import { a1 } from '@borgar/fx';
305
- a1.stringify({
306
- context: [ 'Sheet1' ],
307
- range: {
308
- r0: 9,
309
- c0: 8,
310
- r1: 9,
311
- c1: 8,
312
- $c0: true,
313
- $c1: true
314
- $r0: false,
315
- $r1: false
316
- }
317
- });
318
- // => 'Sheet1!R[9]C9:R[9]C9'
319
- ```
17
+ Documentation can be found under [docs/](./docs/):
320
18
 
19
+ * The API is documented in [docs/API.md](./docs/API.md).
321
20
 
322
- #### <a name="rc.from" href="#rc.from">#</a> **.from**( _refString_ )
21
+ * The AST format the parser emits is specified in [docs/AST format.md](./docs/AST format.md).
323
22
 
324
- Parse a simple string reference to an R1C1 range into a range object ([see above](#rc.parse)).
23
+ * A primer/terminology definitions of Excel references and ranges can be found in [docs/References.md](./docs/References.md).
325
24
 
326
- #### <a name="rc.to" href="#rc.to">#</a> **.to**( _rangeObject_ )
327
25
 
328
- Stringify a range object ([see above](#rc.parse)) into R1C1 syntax.