@borgar/fx
Advanced tools
+3
-0
@@ -17,2 +17,5 @@ export const OPERATOR = 'operator'; | ||
| export const REF_STRUCT = 'structured'; | ||
| // TODO: in future, we should type the difference between A1:B1 (REF_RANGE) and | ||
| // A1 (REF_CELL) but this will require a major version bump. | ||
| export const REF_CELL = 'cell'; // internal only | ||
| export const FX_PREFIX = 'fx_prefix'; | ||
@@ -19,0 +22,0 @@ export const UNKNOWN = 'unknown'; |
@@ -69,2 +69,16 @@ import { describe, test, expect } from 'vitest'; | ||
| describe('fixRanges prefixes', () => { | ||
| test('Quotes prefixes as needed', () => { | ||
| isFixed('=Sch1!B2', "='Sch1'!B2"); | ||
| isFixed('=[Foo]Ab12x!B2', '=[Foo]Ab12x!B2'); | ||
| isFixed('=[Foo]Ab12!B2', "='[Foo]Ab12'!B2"); | ||
| isFixed('=ABC123!B2', "='ABC123'!B2"); | ||
| isFixed('=abc123!B2', "='abc123'!B2"); | ||
| isFixed('=C!B2', "='C'!B2"); | ||
| isFixed('=R!B2', "='R'!B2"); | ||
| isFixed('=RC!B2', "='RC'!B2"); | ||
| isFixed('=CR!B2', '=CR!B2'); | ||
| }); | ||
| }); | ||
| describe('fixRanges A1', () => { | ||
@@ -71,0 +85,0 @@ const opt = { allowTernary: true }; |
@@ -102,3 +102,3 @@ import { REF_RANGE, REF_BEAM, REF_TERNARY, MAX_COLS, MAX_ROWS } from '../constants.ts'; | ||
| // LT : this is A1 | ||
| if (top && canEndRange(str, preOp)) { | ||
| if (top && canEndRange(str, preOp) && str.charCodeAt(preOp) !== 33) { // 33 = "!" | ||
| return { type: REF_RANGE, value: str.slice(pos, preOp) }; | ||
@@ -105,0 +105,0 @@ } |
@@ -14,2 +14,3 @@ import { REF_RANGE, REF_BEAM, REF_TERNARY, MAX_COLS, MAX_ROWS } from '../constants.ts'; | ||
| const MINUS = 45; | ||
| const EXCL = 33; | ||
@@ -101,3 +102,3 @@ // C | ||
| p += c1; | ||
| if (c1 || r1) { | ||
| if ((c1 || r1) && str.charCodeAt(p) !== EXCL) { | ||
| const op = advRangeOp(str, p); | ||
@@ -104,0 +105,0 @@ const preOp = p; |
+23
-14
@@ -1,2 +0,2 @@ | ||
| import { CONTEXT, CONTEXT_QUOTE, REF_RANGE, REF_NAMED, REF_BEAM, REF_TERNARY, OPERATOR, REF_STRUCT } from './constants.ts'; | ||
| import { CONTEXT, CONTEXT_QUOTE, REF_RANGE, REF_NAMED, REF_BEAM, REF_TERNARY, OPERATOR, REF_STRUCT, REF_CELL } from './constants.ts'; | ||
| import type { Token } from './types.ts'; | ||
@@ -7,20 +7,22 @@ | ||
| const validRunsMerge = [ | ||
| [ REF_RANGE, ':', REF_RANGE ], | ||
| [ REF_RANGE, '.:', REF_RANGE ], | ||
| [ REF_RANGE, ':.', REF_RANGE ], | ||
| [ REF_RANGE, '.:.', REF_RANGE ], | ||
| [ REF_CELL, ':', REF_CELL ], | ||
| [ REF_CELL, '.:', REF_CELL ], | ||
| [ REF_CELL, ':.', REF_CELL ], | ||
| [ REF_CELL, '.:.', REF_CELL ], | ||
| [ REF_RANGE ], | ||
| [ REF_BEAM ], | ||
| [ REF_TERNARY ], | ||
| [ CONTEXT, '!', REF_RANGE, ':', REF_RANGE ], | ||
| [ CONTEXT, '!', REF_RANGE, '.:', REF_RANGE ], | ||
| [ CONTEXT, '!', REF_RANGE, ':.', REF_RANGE ], | ||
| [ CONTEXT, '!', REF_RANGE, '.:.', REF_RANGE ], | ||
| [ CONTEXT, '!', REF_CELL, ':', REF_CELL ], | ||
| [ CONTEXT, '!', REF_CELL, '.:', REF_CELL ], | ||
| [ CONTEXT, '!', REF_CELL, ':.', REF_CELL ], | ||
| [ CONTEXT, '!', REF_CELL, '.:.', REF_CELL ], | ||
| [ CONTEXT, '!', REF_CELL ], | ||
| [ CONTEXT, '!', REF_RANGE ], | ||
| [ CONTEXT, '!', REF_BEAM ], | ||
| [ CONTEXT, '!', REF_TERNARY ], | ||
| [ CONTEXT_QUOTE, '!', REF_RANGE, ':', REF_RANGE ], | ||
| [ CONTEXT_QUOTE, '!', REF_RANGE, '.:', REF_RANGE ], | ||
| [ CONTEXT_QUOTE, '!', REF_RANGE, ':.', REF_RANGE ], | ||
| [ CONTEXT_QUOTE, '!', REF_RANGE, '.:.', REF_RANGE ], | ||
| [ CONTEXT_QUOTE, '!', REF_CELL, ':', REF_CELL ], | ||
| [ CONTEXT_QUOTE, '!', REF_CELL, '.:', REF_CELL ], | ||
| [ CONTEXT_QUOTE, '!', REF_CELL, ':.', REF_CELL ], | ||
| [ CONTEXT_QUOTE, '!', REF_CELL, '.:.', REF_CELL ], | ||
| [ CONTEXT_QUOTE, '!', REF_CELL ], | ||
| [ CONTEXT_QUOTE, '!', REF_RANGE ], | ||
@@ -66,3 +68,10 @@ [ CONTEXT_QUOTE, '!', REF_BEAM ], | ||
| if (token) { | ||
| const key = (token.type === OPERATOR) ? token.value : token.type; | ||
| const value = token.value; | ||
| let key = (token.type === OPERATOR) ? value : token.type; | ||
| // we need to prevent merging ["A1:B2" ":" "C3"] as a range is only | ||
| // allowed to contain a single ":" operator even if "A1:B2:C3" is | ||
| // valid Excel syntax | ||
| if (key === REF_RANGE && !value.includes(':')) { | ||
| key = REF_CELL; | ||
| } | ||
| if (key in node) { | ||
@@ -69,0 +78,0 @@ node = node[key]; |
@@ -338,1 +338,8 @@ /* eslint-disable @stylistic/object-property-newline */ | ||
| }); | ||
| describe('Sheet name that looks like an A1 ref', () => { | ||
| test('parse correctly', () => { | ||
| isA1Equal("'Sch1'!B2", { context: [ 'Sch1' ], range: { top: 1, left: 1, bottom: 1, right: 1 } }); | ||
| isA1Equal('Sch1!B2', { context: [ 'Sch1' ], range: { top: 1, left: 1, bottom: 1, right: 1 } }); | ||
| }); | ||
| }); |
@@ -33,2 +33,8 @@ import { describe, test, expect } from 'vitest'; | ||
| }); | ||
| test('should quote prefixes that look like A1 ranges', () => { | ||
| expect(stringifyA1Ref({ context: [ 'Ab12' ], range: rangeA1 })).toBe("'Ab12'!A1"); | ||
| expect(stringifyA1Ref({ context: [ 'Sch1' ], range: rangeA1 })).toBe("'Sch1'!A1"); | ||
| expect(stringifyA1Ref({ context: [ 'Foo12345' ], range: rangeA1 })).toBe("'Foo12345'!A1"); | ||
| }); | ||
| }); | ||
@@ -65,2 +71,14 @@ | ||
| }); | ||
| test('should quote prefixes that look like ranges', () => { | ||
| expect(stringifyA1RefXlsx({ sheetName: 'C', range: rangeA1 })).toBe("'C'!A1"); | ||
| expect(stringifyA1RefXlsx({ sheetName: 'R', range: rangeA1 })).toBe("'R'!A1"); | ||
| expect(stringifyA1RefXlsx({ sheetName: 'RC', range: rangeA1 })).toBe("'RC'!A1"); | ||
| expect(stringifyA1RefXlsx({ sheetName: 'Ab12', range: rangeA1 })).toBe("'Ab12'!A1"); | ||
| expect(stringifyA1RefXlsx({ sheetName: 'Sch1', range: rangeA1 })).toBe("'Sch1'!A1"); | ||
| expect(stringifyA1RefXlsx({ sheetName: 'Foo12345', range: rangeA1 })).toBe("'Foo12345'!A1"); | ||
| expect(stringifyA1RefXlsx({ workbookName: 'Ab12', range: rangeA1 })).toBe("'[Ab12]'!A1"); | ||
| expect(stringifyA1RefXlsx({ workbookName: 'Sch1', range: rangeA1 })).toBe("'[Sch1]'!A1"); | ||
| expect(stringifyA1RefXlsx({ workbookName: 'Foo12345', range: rangeA1 })).toBe("'[Foo12345]'!A1"); | ||
| }); | ||
| }); |
@@ -13,3 +13,24 @@ import type { | ||
| const reBannedChars = /[^0-9A-Za-z._¡¤§¨ª\u00ad¯-\uffff]/; | ||
| // A1-XFD1048575 | R | C | RC | ||
| const reIsRangelike = /^(R|C|RC|[A-Z]{1,3}\d{1,7})$/i; | ||
| export function needQuotes (scope: string, yesItDoes = 0): number { | ||
| if (yesItDoes) { | ||
| return 1; | ||
| } | ||
| if (scope) { | ||
| if (reBannedChars.test(scope)) { | ||
| return 1; | ||
| } | ||
| if (reIsRangelike.test(scope)) { | ||
| return 1; | ||
| } | ||
| } | ||
| return 0; | ||
| } | ||
| export function quotePrefix (prefix) { | ||
| return "'" + prefix.replace(/'/g, "''") + "'"; | ||
| } | ||
| export function stringifyPrefix ( | ||
@@ -27,3 +48,3 @@ ref: ReferenceA1 | ReferenceName | ReferenceStruct | ReferenceR1C1 | ||
| pre = part + pre; | ||
| quote += +reBannedChars.test(scope); | ||
| quote += needQuotes(scope, quote); | ||
| nth++; | ||
@@ -33,3 +54,3 @@ } | ||
| if (quote) { | ||
| pre = "'" + pre.replace(/'/g, "''") + "'"; | ||
| pre = quotePrefix(pre); | ||
| } | ||
@@ -47,12 +68,12 @@ return pre ? pre + '!' : pre; | ||
| pre += '[' + workbookName + ']'; | ||
| quote += +reBannedChars.test(workbookName); | ||
| quote += needQuotes(workbookName); | ||
| } | ||
| if (sheetName) { | ||
| pre += sheetName; | ||
| quote += +reBannedChars.test(sheetName); | ||
| quote += needQuotes(sheetName); | ||
| } | ||
| if (quote) { | ||
| pre = "'" + pre.replace(/'/g, "''") + "'"; | ||
| pre = quotePrefix(pre); | ||
| } | ||
| return pre ? pre + '!' : pre; | ||
| } |
@@ -103,4 +103,3 @@ import { stringifyR1C1RefXlsx } from './stringifyR1C1Ref.ts'; | ||
| const tokenValue = token.value; | ||
| // We can get away with using the xlsx ref-parser here because it is more permissive | ||
| // and we will end up with the same prefix after serialization anyway: | ||
| // We can get away with using the xlsx ref-parser here because it is more permissive: | ||
| const ref = quickParseA1(tokenValue); | ||
@@ -107,0 +106,0 @@ if (ref) { |
+7
-7
| { | ||
| "name": "@borgar/fx", | ||
| "version": "5.0.1", | ||
| "version": "5.0.2", | ||
| "description": "Utilities for working with Excel formulas", | ||
@@ -71,9 +71,9 @@ "type": "module", | ||
| "typescript": "~5.9.3", | ||
| "typescript-eslint": "~8.46.2", | ||
| "vitest": "~4.0.6", | ||
| "globals": "~16.5.0", | ||
| "typedoc": "~0.28.14", | ||
| "typedoc-plugin-markdown": "~4.9.0", | ||
| "tsup": "~8.5.0" | ||
| "typescript-eslint": "~8.55.0", | ||
| "vitest": "~4.0.18", | ||
| "globals": "~17.3.0", | ||
| "typedoc": "~0.28.17", | ||
| "typedoc-plugin-markdown": "~4.10.0", | ||
| "tsup": "~8.5.1" | ||
| } | ||
| } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
1823107
0.66%25969
0.75%76
5.56%