UNPKG

@borgar/fx

Version:

Utilities for working with Excel formulas

1 lines 223 kB
{"version":3,"sources":["../../lib/xlsx/index.ts","../../lib/constants.ts","../../lib/mergeRefTokens.ts","../../lib/lexers/lexError.ts","../../lib/lexers/lexRangeTrim.ts","../../lib/lexers/lexOperator.ts","../../lib/lexers/lexBoolean.ts","../../lib/lexers/lexNewLine.ts","../../lib/lexers/lexWhitespace.ts","../../lib/lexers/lexString.ts","../../lib/lexers/lexContext.ts","../../lib/lexers/advRangeOp.ts","../../lib/lexers/canEndRange.ts","../../lib/lexers/lexRangeA1.ts","../../lib/lexers/lexRangeR1C1.ts","../../lib/lexers/lexRange.ts","../../lib/parseSRange.ts","../../lib/lexers/lexStructured.ts","../../lib/lexers/lexNumber.ts","../../lib/lexers/lexNamed.ts","../../lib/lexers/lexRefOp.ts","../../lib/lexers/lexNameFuncCntx.ts","../../lib/lexers/sets.ts","../../lib/isRCTokenValue.ts","../../lib/tokenize.ts","../../lib/isType.ts","../../lib/parse.ts","../../lib/stringifyPrefix.ts","../../lib/a1.ts","../../lib/stringifyR1C1Range.ts","../../lib/stringifyR1C1Ref.ts","../../lib/parseA1Range.ts","../../lib/stringifyTokens.ts","../../lib/cloneToken.ts","../../lib/parseRef.ts","../../lib/translateToR1C1.ts","../../lib/toCol.ts","../../lib/stringifyA1Range.ts","../../lib/stringifyA1Ref.ts","../../lib/parseR1C1Range.ts","../../lib/parseR1C1Ref.ts","../../lib/translateToA1.ts","../../lib/parseA1Ref.ts","../../lib/addA1RangeBounds.ts","../../lib/parseStructRef.ts","../../lib/stringifyStructRef.ts","../../lib/fixRanges.ts","../../lib/isNodeType.ts","../../lib/fromCol.ts","../../lib/tokenTypes.ts","../../lib/nodeTypes.ts","../../lib/addTokenMeta.ts"],"sourcesContent":["/**\n * A tokenizer, parser, and other utilities to work with Excel formula code.\n *\n * The xslx entry-point methods expect and return the variant of references that uses properties.\n * If you are not using xlsx files you should use the {@link fx} variant methods.\n *\n * See [Prefixes.md](./Prefixes.md) for documentation on how scopes work in Fx.\n *\n * @packageDocumentation\n * @module fx/xlsx\n */\n\nexport * from '../index.ts';\n\n// only exported here, not in the parent module (because it assumes xlsx tokens)\nexport { addTokenMeta } from '../addTokenMeta.ts';\n\nexport {\n fixTokenRangesXlsx as fixTokenRanges,\n fixFormulaRangesXlsx as fixFormulaRanges\n} from '../fixRanges.ts';\n\nexport { parseA1RefXlsx as parseA1Ref } from '../parseA1Ref.ts';\nexport { parseR1C1RefXlsx as parseR1C1Ref } from '../parseR1C1Ref.ts';\nexport { parseStructRefXlsx as parseStructRef } from '../parseStructRef.ts';\n\nexport { stringifyA1RefXlsx as stringifyA1Ref } from '../stringifyA1Ref.ts';\nexport { stringifyR1C1RefXlsx as stringifyR1C1Ref } from '../stringifyR1C1Ref.ts';\nexport { stringifyStructRefXlsx as stringifyStructRef } from '../stringifyStructRef.ts';\n\nexport { tokenizeXlsx as tokenize } from '../tokenize.ts';\n\n","export const OPERATOR = 'operator';\nexport const OPERATOR_TRIM = 'operator-trim'; // internal only\nexport const BOOLEAN = 'bool';\nexport const ERROR = 'error';\nexport const NUMBER = 'number';\nexport const FUNCTION = 'func';\nexport const NEWLINE = 'newline';\nexport const WHITESPACE = 'whitespace';\nexport const STRING = 'string';\nexport const CONTEXT_QUOTE = 'context_quote';\nexport const CONTEXT = 'context';\nexport const REF_RANGE = 'range';\nexport const REF_BEAM = 'range_beam';\nexport const REF_TERNARY = 'range_ternary';\nexport const REF_NAMED = 'range_named';\nexport const REF_STRUCT = 'structured';\n// TODO: in future, we should type the difference between A1:B1 (REF_RANGE) and\n// A1 (REF_CELL) but this will require a major version bump.\nexport const REF_CELL = 'cell'; // internal only\nexport const FX_PREFIX = 'fx_prefix';\nexport const UNKNOWN = 'unknown';\n\nexport const UNARY = 'UnaryExpression';\nexport const BINARY = 'BinaryExpression';\nexport const REFERENCE = 'ReferenceIdentifier';\nexport const LITERAL = 'Literal';\nexport const ERROR_LITERAL = 'ErrorLiteral';\nexport const CALL = 'CallExpression';\nexport const LAMBDA = 'LambdaExpression';\nexport const LET = 'LetExpression';\nexport const ARRAY = 'ArrayExpression';\nexport const IDENTIFIER = 'Identifier';\nexport const LET_DECL = 'LetDeclarator';\n\n/** The maximum number of columns a spreadsheet reference may hold (16383). */\nexport const MAX_COLS = (2 ** 14) - 1;\n\n/** The maximum number of rows a spreadsheet reference may hold (1048575). */\nexport const MAX_ROWS = (2 ** 20) - 1;\n","import { CONTEXT, CONTEXT_QUOTE, REF_RANGE, REF_NAMED, REF_BEAM, REF_TERNARY, OPERATOR, REF_STRUCT, REF_CELL } from './constants.ts';\nimport type { Token } from './types.ts';\n\nconst END = '$';\n\nconst validRunsMerge = [\n [ REF_CELL, ':', REF_CELL ],\n [ REF_CELL, '.:', REF_CELL ],\n [ REF_CELL, ':.', REF_CELL ],\n [ REF_CELL, '.:.', REF_CELL ],\n [ REF_RANGE ],\n [ REF_BEAM ],\n [ REF_TERNARY ],\n [ CONTEXT, '!', REF_CELL, ':', REF_CELL ],\n [ CONTEXT, '!', REF_CELL, '.:', REF_CELL ],\n [ CONTEXT, '!', REF_CELL, ':.', REF_CELL ],\n [ CONTEXT, '!', REF_CELL, '.:.', REF_CELL ],\n [ CONTEXT, '!', REF_CELL ],\n [ CONTEXT, '!', REF_RANGE ],\n [ CONTEXT, '!', REF_BEAM ],\n [ CONTEXT, '!', REF_TERNARY ],\n [ CONTEXT_QUOTE, '!', REF_CELL, ':', REF_CELL ],\n [ CONTEXT_QUOTE, '!', REF_CELL, '.:', REF_CELL ],\n [ CONTEXT_QUOTE, '!', REF_CELL, ':.', REF_CELL ],\n [ CONTEXT_QUOTE, '!', REF_CELL, '.:.', REF_CELL ],\n [ CONTEXT_QUOTE, '!', REF_CELL ],\n [ CONTEXT_QUOTE, '!', REF_RANGE ],\n [ CONTEXT_QUOTE, '!', REF_BEAM ],\n [ CONTEXT_QUOTE, '!', REF_TERNARY ],\n [ REF_NAMED ],\n [ CONTEXT, '!', REF_NAMED ],\n [ CONTEXT_QUOTE, '!', REF_NAMED ],\n [ REF_STRUCT ],\n [ REF_NAMED, REF_STRUCT ],\n [ CONTEXT, '!', REF_NAMED, REF_STRUCT ],\n [ CONTEXT_QUOTE, '!', REF_NAMED, REF_STRUCT ]\n];\n\ntype TypeNode = {\n [key: string]: TypeNode | boolean;\n};\n\n// valid token runs are converted to a tree structure\nconst refPartsTree: TypeNode = {};\nfunction packList (f: string[], node: TypeNode) {\n if (f.length) {\n const key = f[0];\n if (!node[key]) { node[key] = {}; }\n packList(f.slice(1), node[key] as TypeNode);\n }\n else {\n node[END] = true;\n }\n}\nvalidRunsMerge.forEach(run => packList(run.concat().reverse(), refPartsTree));\n\n// attempt to match a backwards run of tokens from a given point\n// to a path in the tree\nconst matcher = (tokens: Token[], currNode, anchorIndex, index = 0) => {\n let i = index;\n let node = currNode;\n const max = tokens.length - index;\n // keep walking as long as the next backward token matches a child key\n while (i <= max) {\n const token = tokens[anchorIndex - i];\n if (token) {\n const value = token.value;\n let key = (token.type === OPERATOR) ? value : token.type;\n // we need to prevent merging [\"A1:B2\" \":\" \"C3\"] as a range is only\n // allowed to contain a single \":\" operator even if \"A1:B2:C3\" is\n // valid Excel syntax\n if (key === REF_RANGE && !value.includes(':')) {\n key = REF_CELL;\n }\n if (key in node) {\n node = node[key];\n i += 1;\n continue;\n }\n }\n // can't advance further; accept only if current node is a terminal\n return node[END] ? i : 0;\n }\n};\n\n/**\n * Merges context with reference tokens as possible in a list of tokens.\n *\n * When given a tokenlist, this function returns a new list with ranges returned\n * as whole references (`Sheet1!A1:B2`) rather than separate tokens for each\n * part: (`Sheet1`,`!`,`A1`,`:`,`B2`).\n *\n * @param tokenlist An array of tokens.\n * @returns A new list of tokens with range parts merged.\n */\nexport function mergeRefTokens (tokenlist: Token[]): Token[] {\n const finalTokens = [];\n // this seeks backwards because it's really the range part\n // that controls what can be joined.\n for (let i = tokenlist.length - 1; i >= 0; i--) {\n let token = tokenlist[i];\n const type = token.type;\n // Quick check if token type could even start a valid run\n if (type === REF_RANGE || type === REF_BEAM || type === REF_TERNARY ||\n type === REF_NAMED || type === REF_STRUCT) {\n const valid = matcher(tokenlist, refPartsTree, i);\n if (valid > 1) {\n token = { ...token, value: '' };\n const start = i - valid + 1;\n for (let j = start; j <= i; j++) {\n token.value += tokenlist[j].value;\n }\n // adjust the offsets to include all the text\n if (token.loc && tokenlist[start].loc) {\n token.loc[0] = tokenlist[start].loc[0];\n }\n i -= valid - 1;\n }\n }\n finalTokens[finalTokens.length] = token;\n }\n return finalTokens.reverse();\n}\n","import { ERROR } from '../constants.ts';\nimport type { Token } from '../types.ts';\n\nconst re_ERROR = /#(?:NAME\\?|FIELD!|CALC!|VALUE!|REF!|DIV\\/0!|NULL!|NUM!|N\\/A|GETTING_DATA\\b|SPILL!|UNKNOWN!|SYNTAX\\?|ERROR!|CONNECT!|BLOCKED!|EXTERNAL!)/iy;\nconst HASH = 35;\n\nexport function lexError (str: string, pos: number): Token | undefined {\n if (str.charCodeAt(pos) === HASH) {\n re_ERROR.lastIndex = pos;\n const m = re_ERROR.exec(str);\n if (m) {\n return { type: ERROR, value: m[0] };\n }\n }\n}\n","import { OPERATOR_TRIM } from '../constants.ts';\nimport type { Token } from '../types.ts';\n\nconst PERIOD = 46;\nconst COLON = 58;\n\nexport function lexRangeTrim (str: string, pos: number): Token | undefined {\n const c0 = str.charCodeAt(pos);\n if (c0 === PERIOD || c0 === COLON) {\n const c1 = str.charCodeAt(pos + 1);\n if (c0 !== c1) {\n if (c1 === COLON) {\n return {\n type: OPERATOR_TRIM,\n value: str.slice(pos, pos + (str.charCodeAt(pos + 2) === PERIOD ? 3 : 2))\n };\n }\n else if (c1 === PERIOD) {\n return {\n type: OPERATOR_TRIM,\n value: str.slice(pos, pos + 2)\n };\n }\n }\n }\n}\n","import { OPERATOR } from '../constants.ts';\nimport type { Token } from '../types.ts';\n\nexport function lexOperator (str: string, pos: number): Token | undefined {\n const c0 = str.charCodeAt(pos);\n const c1 = str.charCodeAt(pos + 1);\n if (\n (c0 === 60 && c1 === 61) || // <=\n (c0 === 62 && c1 === 61) || // >=\n (c0 === 60 && c1 === 62) // <>\n ) {\n return { type: OPERATOR, value: str.slice(pos, pos + 2) };\n }\n if (\n // { } ! # % &\n c0 === 123 || c0 === 125 || c0 === 33 || c0 === 35 || c0 === 37 || c0 === 38 ||\n // ( ) * + , -\n (c0 >= 40 && c0 <= 45) ||\n // / : ; < = >\n c0 === 47 || (c0 >= 58 && c0 <= 62) ||\n // @ ^\n c0 === 64 || c0 === 94\n ) {\n return { type: OPERATOR, value: str[pos] };\n }\n}\n","import { BOOLEAN } from '../constants.ts';\nimport type { Token } from '../types.ts';\n\nfunction preventMatch (c: number) {\n return (\n (c >= 65 && c <= 90) || // A-Z\n (c >= 97 && c <= 122) || // a-z\n (c >= 48 && c <= 57) || // 0-9\n (c === 95) || // _\n (c === 92) || // \\\n (c === 40) || // (\n (c === 46) || // .\n (c === 63) || // ?\n (c > 0xA0) // \\u00a1-\\uffff\n );\n}\n\nexport function lexBoolean (str: string, pos: number): Token | undefined {\n // \"true\" (case insensitive)\n const c0 = str.charCodeAt(pos);\n if (c0 === 84 || c0 === 116) {\n const c1 = str.charCodeAt(pos + 1);\n if (c1 === 82 || c1 === 114) {\n const c2 = str.charCodeAt(pos + 2);\n if (c2 === 85 || c2 === 117) {\n const c3 = str.charCodeAt(pos + 3);\n if (c3 === 69 || c3 === 101) {\n const c4 = str.charCodeAt(pos + 4);\n if (!preventMatch(c4)) {\n return { type: BOOLEAN, value: str.slice(pos, pos + 4) };\n }\n }\n }\n }\n }\n // \"false\" (case insensitive)\n if (c0 === 70 || c0 === 102) {\n const c1 = str.charCodeAt(pos + 1);\n if (c1 === 65 || c1 === 97) {\n const c2 = str.charCodeAt(pos + 2);\n if (c2 === 76 || c2 === 108) {\n const c3 = str.charCodeAt(pos + 3);\n if (c3 === 83 || c3 === 115) {\n const c4 = str.charCodeAt(pos + 4);\n if (c4 === 69 || c4 === 101) {\n const c5 = str.charCodeAt(pos + 5);\n if (!preventMatch(c5)) {\n return { type: BOOLEAN, value: str.slice(pos, pos + 5) };\n }\n }\n }\n }\n }\n }\n}\n","import { NEWLINE } from '../constants.ts';\nimport type { Token } from '../types.ts';\n\nexport function lexNewLine (str: string, pos: number): Token | undefined {\n const start = pos;\n while (str.charCodeAt(pos) === 10) {\n pos++;\n }\n if (pos !== start) {\n return { type: NEWLINE, value: str.slice(start, pos) };\n }\n}\n","import { WHITESPACE } from '../constants.ts';\nimport type { Token } from '../types.ts';\n\nexport function isWS (c) {\n return (\n c === 0x9 ||\n c === 0xB ||\n c === 0xC ||\n c === 0xD ||\n c === 0x20 ||\n c === 0xA0 ||\n c === 0x1680 ||\n c === 0x2028 ||\n c === 0x2029 ||\n c === 0x202f ||\n c === 0x205f ||\n c === 0x3000 ||\n c === 0xfeff ||\n (c >= 0x2000 && c <= 0x200a)\n );\n}\n\nexport function lexWhitespace (str: string, pos: number): Token | undefined {\n const start = pos;\n while (isWS(str.charCodeAt(pos))) {\n pos++;\n }\n if (pos !== start) {\n return { type: WHITESPACE, value: str.slice(start, pos) };\n }\n}\n","import { STRING } from '../constants.ts';\nimport type { Token } from '../types.ts';\n\nconst QUOT = 34;\n\nexport function lexString (str: string, pos: number): Token | undefined {\n const start = pos;\n if (str.charCodeAt(pos) === QUOT) {\n pos++;\n while (pos < str.length) {\n const c = str.charCodeAt(pos);\n if (c === QUOT) {\n pos++;\n if (str.charCodeAt(pos) !== QUOT) {\n return { type: STRING, value: str.slice(start, pos) };\n }\n }\n pos++;\n }\n return { type: STRING, value: str.slice(start, pos), unterminated: true };\n }\n}\n","import { CONTEXT, CONTEXT_QUOTE } from '../constants.ts';\nimport type { Token } from '../types.ts';\n\nconst QUOT_SINGLE = 39; // '\nconst BR_OPEN = 91; // [\nconst BR_CLOSE = 93; // ]\nconst EXCL = 33; // !\n\n// xlsx xml uses a variant of the syntax that has external references in\n// bracets. Any of: [1]Sheet1!A1, '[1]Sheet one'!A1, [1]!named\nexport function lexContextQuoted (str: string, pos: number, options: { xlsx: boolean }): Token | undefined {\n const c0 = str.charCodeAt(pos);\n let br1: number;\n let br2: number;\n // quoted context: '(?:''|[^'])*('|$)(?=!)\n if (c0 === QUOT_SINGLE) {\n const start = pos;\n pos++;\n while (pos < str.length) {\n const c = str.charCodeAt(pos);\n if (c === BR_OPEN) {\n if (br1) { return; } // only 1 allowed\n br1 = pos;\n }\n else if (c === BR_CLOSE) {\n if (br2) { return; } // only 1 allowed\n br2 = pos;\n }\n else if (c === QUOT_SINGLE) {\n pos++;\n if (str.charCodeAt(pos) !== QUOT_SINGLE) {\n let valid = br1 == null && br2 == null;\n if (options.xlsx && (br1 === start + 1) && (br2 === pos - 2)) {\n valid = true;\n }\n if ((br1 >= start + 1) && (br2 < pos - 2) && (br2 > br1 + 1)) {\n valid = true;\n }\n if (valid && str.charCodeAt(pos) === EXCL) {\n return { type: CONTEXT_QUOTE, value: str.slice(start, pos) };\n }\n return;\n }\n }\n pos++;\n }\n }\n}\n\n// xlsx xml uses a variant of the syntax that has external references in\n// bracets. Any of: [1]Sheet1!A1, '[1]Sheet one'!A1, [1]!named\nexport function lexContextUnquoted (str: string, pos: number, options: { xlsx: boolean }): Token | undefined {\n const c0 = str.charCodeAt(pos);\n let br1: number;\n let br2: number;\n if (c0 !== QUOT_SINGLE && c0 !== EXCL) {\n const start = pos;\n while (pos < str.length) {\n const c = str.charCodeAt(pos);\n if (c === BR_OPEN) {\n if (br1) { return; } // only 1 allowed\n br1 = pos;\n }\n else if (c === BR_CLOSE) {\n if (br2) { return; } // only 1 allowed\n br2 = pos;\n }\n else if (c === EXCL) {\n let valid = br1 == null && br2 == null;\n if (options.xlsx && (br1 === start) && (br2 === pos - 1)) {\n valid = true;\n }\n if ((br1 >= start) && (br2 < pos - 1) && (br2 > br1 + 1)) {\n valid = true;\n }\n if (valid) {\n return { type: CONTEXT, value: str.slice(start, pos) };\n }\n }\n else if (\n (br1 == null || br2 != null) &&\n // [0-9A-Za-z._¡¤§¨ª\\u00ad¯-\\uffff]\n !(\n (c >= 65 && c <= 90) || // A-Z\n (c >= 97 && c <= 122) || // a-z\n (c >= 48 && c <= 57) || // 0-9\n (c === 46) || // .\n (c === 95) || // _\n (c === 161) || // ¡\n (c === 164) || // ¤\n (c === 167) || // §\n (c === 168) || // ¨\n (c === 170) || // ª\n (c === 173) || // \\u00ad\n (c >= 175) // ¯-\\uffff\n )\n ) {\n return;\n }\n // 0-9A-Za-z._¡¤§¨ª\\u00ad¯-\\uffff\n pos++;\n }\n }\n}\n","const PERIOD = 46;\nconst COLON = 58;\n\nexport function advRangeOp (str: string, pos: number): number {\n const c0 = str.charCodeAt(pos);\n if (c0 === PERIOD) {\n const c1 = str.charCodeAt(pos + 1);\n if (c1 === COLON) {\n return str.charCodeAt(pos + 2) === PERIOD ? 3 : 2;\n }\n }\n else if (c0 === COLON) {\n const c1 = str.charCodeAt(pos + 1);\n return c1 === PERIOD ? 2 : 1;\n }\n return 0;\n}\n\n","// regular: [A-Za-z0-9_\\u00a1-\\uffff]\nexport function canEndRange (str: string, pos: number): boolean {\n const c = str.charCodeAt(pos);\n return !(\n (c >= 65 && c <= 90) || // A-Z\n (c >= 97 && c <= 122) || // a-z\n (c >= 48 && c <= 57) || // 0-9\n (c === 95) || // _\n (c === 40) || // (\n (c > 0xA0) // \\u00a1-\\uffff\n );\n}\n\n// partial: [A-Za-z0-9_($.]\n// Also rejects \"!\" — a ternary range must not end where a sheet prefix\n// begins (e.g. \"F2:B\" in \"B!F2:B!F20\" is not a ternary range; the\n// trailing \"B\" is the start of the second sheet prefix \"B!\").\nexport function canEndPartialRange (str: string, pos: number): boolean {\n const c = str.charCodeAt(pos);\n return !(\n (c >= 65 && c <= 90) || // A-Z\n (c >= 97 && c <= 122) || // a-z\n (c >= 48 && c <= 57) || // 0-9\n (c === 95) || // _\n (c === 40) || // (\n (c === 36) || // $\n (c === 46) || // .\n (c === 33) // !\n );\n}\n","import { REF_RANGE, REF_BEAM, REF_TERNARY, MAX_COLS, MAX_ROWS } from '../constants.ts';\nimport type { Token } from '../types.ts';\nimport { advRangeOp } from './advRangeOp.ts';\nimport { canEndRange, canEndPartialRange } from './canEndRange.ts';\n\nfunction advA1Col (str: string, pos: number): number {\n // [A-Z]{1,3}\n const start = pos;\n if (str.charCodeAt(pos) === 36) { // $\n pos++;\n }\n const stop = pos + 3;\n let col = 0;\n do {\n const c = str.charCodeAt(pos);\n if (c >= 65 && c <= 90) { // A-Z\n col = 26 * col + c - 64;\n pos++;\n }\n else if (c >= 97 && c <= 122) { // a-z\n col = 26 * col + c - 96;\n pos++;\n }\n else {\n break;\n }\n }\n while (pos < stop && pos < str.length);\n return (col && col <= MAX_COLS + 1) ? pos - start : 0;\n}\n\nfunction advA1Row (str: string, pos: number): number {\n // [1-9][0-9]{0,6}\n const start = pos;\n if (str.charCodeAt(pos) === 36) { // $\n pos++;\n }\n const stop = pos + 7;\n let row = 0;\n let c = str.charCodeAt(pos);\n if (c >= 49 && c <= 57) { // 1-9\n row = row * 10 + c - 48;\n pos++;\n do {\n c = str.charCodeAt(pos);\n if (c >= 48 && c <= 57) { // 0-9\n row = row * 10 + c - 48;\n pos++;\n }\n else {\n break;\n }\n }\n while (pos < stop && pos < str.length);\n }\n return (row && row <= MAX_ROWS + 1) ? pos - start : 0;\n}\n\nexport function lexRangeA1 (\n str: string,\n pos: number,\n options: { mergeRefs: boolean, allowTernary: boolean }\n): Token | undefined {\n let p = pos;\n const left = advA1Col(str, p);\n let right = 0;\n let bottom = 0;\n if (left) {\n // TLBR: could be A1:A1\n // TL R: could be A1:A (if allowTernary)\n // TLB : could be A1:1 (if allowTernary)\n // LBR: could be A:A1 (if allowTernary)\n // L R: could be A:A\n p += left;\n const top = advA1Row(str, p);\n p += top;\n const op = advRangeOp(str, p);\n const preOp = p;\n if (op) {\n p += op;\n right = advA1Col(str, p);\n p += right;\n bottom = advA1Row(str, p);\n p += bottom;\n if (top && bottom && right) {\n if (canEndRange(str, p) && options.mergeRefs) {\n return { type: REF_RANGE, value: str.slice(pos, p) };\n }\n }\n else if (!top && !bottom) {\n if (canEndRange(str, p)) {\n return { type: REF_BEAM, value: str.slice(pos, p) };\n }\n }\n else if (options.allowTernary && (bottom || right)) {\n if (canEndPartialRange(str, p)) {\n return { type: REF_TERNARY, value: str.slice(pos, p) };\n }\n }\n }\n // LT : this is A1\n if (top && canEndRange(str, preOp) && str.charCodeAt(preOp) !== 33) { // 33 = \"!\"\n return { type: REF_RANGE, value: str.slice(pos, preOp) };\n }\n }\n else {\n // T B : could be 1:1\n // T BR: could be 1:A1 (if allowTernary)\n const top = advA1Row(str, p);\n if (top) {\n p += top;\n const op = advRangeOp(str, p);\n if (op) {\n p += op;\n right = advA1Col(str, p);\n if (right) {\n p += right;\n }\n bottom = advA1Row(str, p);\n p += bottom;\n if (right && bottom && options.allowTernary) {\n if (canEndPartialRange(str, p)) {\n return { type: REF_TERNARY, value: str.slice(pos, p) };\n }\n }\n if (!right && bottom) {\n if (canEndRange(str, p)) {\n return { type: REF_BEAM, value: str.slice(pos, p) };\n }\n }\n }\n }\n }\n}\n","import { REF_RANGE, REF_BEAM, REF_TERNARY, MAX_COLS, MAX_ROWS } from '../constants.ts';\nimport type { Token } from '../types.ts';\nimport { advRangeOp } from './advRangeOp.ts';\nimport { canEndRange } from './canEndRange.ts';\n\nconst BR_OPEN = 91; // [\nconst BR_CLOSE = 93; // ]\nconst UC_R = 82;\nconst LC_R = 114;\nconst UC_C = 67;\nconst LC_C = 99;\nconst PLUS = 43;\nconst MINUS = 45;\nconst EXCL = 33;\n\n// C\n// C\\[[+-]?\\d+\\]\n// C[1-9][0-9]{0,4}\n// R\n// R\\[[+-]?\\d+\\]\n// R[1-9][0-9]{0,6}\nfunction lexR1C1Part (str: string, pos: number, isRow: boolean = false): number {\n const start = pos;\n const c0 = str.charCodeAt(pos);\n if ((isRow ? c0 === UC_R || c0 === LC_R : c0 === UC_C || c0 === LC_C)) {\n pos++;\n let digits = 0;\n let value = 0;\n let stop = str.length;\n const c1 = str.charCodeAt(pos);\n let c;\n let sign = 1;\n const relative = c1 === BR_OPEN;\n if (relative) {\n stop = Math.min(stop, pos + (isRow ? 8 : 6));\n pos++;\n // allow +-\n c = str.charCodeAt(pos);\n if (c === PLUS || c === MINUS) {\n pos++;\n stop++;\n sign = c === MINUS ? -1 : 1;\n }\n }\n else if (c1 < 49 || c1 > 57 || isNaN(c1)) {\n // char must be 1-9, or part is either just \"R\" or \"C\"\n return 1;\n }\n\n do {\n const c = str.charCodeAt(pos);\n if (c >= 48 && c <= 57) { // 0-9\n value = value * 10 + c - 48;\n digits++;\n pos++;\n }\n else {\n break;\n }\n }\n while (pos < stop);\n\n const MAX = isRow ? MAX_ROWS : MAX_COLS;\n if (relative) {\n const c = str.charCodeAt(pos);\n if (c !== BR_CLOSE) {\n return 0;\n }\n // isRow: next char must not be a number!\n pos++;\n value *= sign;\n return (digits && (-MAX <= value) && (value <= MAX))\n ? pos - start\n : 0;\n }\n // isRow: next char must not be a number!\n return (digits && value <= (MAX + 1)) ? pos - start : 0;\n }\n return 0;\n}\n\nexport function lexRangeR1C1 (\n str: string,\n pos: number,\n options: { allowTernary: boolean }\n): Token | undefined {\n let p = pos;\n // C1\n // C1:C1\n // C1:R1C1 --partial\n // R1\n // R1:R1\n // R1:R1C1 --partial\n // R1C1\n // R1C1:C1 --partial\n // R1C1:R1 --partial\n const r1 = lexR1C1Part(str, p, true);\n p += r1;\n const c1 = lexR1C1Part(str, p);\n p += c1;\n if ((c1 || r1) && str.charCodeAt(p) !== EXCL) {\n const op = advRangeOp(str, p);\n const preOp = p;\n if (op) {\n p += op;\n const r2 = lexR1C1Part(str, p, true); // R1\n p += r2;\n const c2 = lexR1C1Part(str, p); // C1\n p += c2;\n\n // C1:R2C2 --partial\n // R1:R2C2 --partial\n // R1C1:C2 --partial\n // R1C1:R2 --partial\n if (\n (r1 && !c1 && r2 && c2) ||\n (!r1 && c1 && r2 && c2) ||\n (r1 && c1 && r2 && !c2) ||\n (r1 && c1 && !r2 && c2)\n ) {\n if (options.allowTernary && canEndRange(str, p)) {\n return { type: REF_TERNARY, value: str.slice(pos, p) };\n }\n }\n // C1:C2 -- beam\n // R1:R2 -- beam\n else if (\n (c1 && c2 && !r1 && !r2) ||\n (!c1 && !c2 && r1 && r2)\n ) {\n if (canEndRange(str, p)) {\n return { type: REF_BEAM, value: str.slice(pos, p) };\n }\n }\n // Note: we do not capture R1C1:R1C1, mergeRefTokens will join the parts\n }\n // R1\n // C1\n // R1C1\n if (canEndRange(str, preOp)) {\n return {\n type: (r1 && c1) ? REF_RANGE : REF_BEAM,\n value: str.slice(pos, preOp)\n };\n }\n }\n}\n","import type { Token } from '../types.ts';\nimport { lexRangeA1 } from './lexRangeA1.ts';\nimport { lexRangeR1C1 } from './lexRangeR1C1.ts';\n\ntype LexRangeOptions = {\n allowTernary: boolean,\n mergeRefs: boolean,\n r1c1: boolean\n};\n\nexport function lexRange (str: string, pos: number, options: LexRangeOptions): Token | undefined {\n return options.r1c1\n ? lexRangeR1C1(str, pos, options)\n : lexRangeA1(str, pos, options);\n}\n","import { isWS } from './lexers/lexWhitespace.ts';\n\nconst AT = 64; // @\nconst BR_CLOSE = 93; // ]\nconst BR_OPEN = 91; // [\nconst COLON = 58; // :\nconst COMMA = 44; // ,\nconst HASH = 35; // #\nconst QUOT_SINGLE = 39; // '\n\nconst keyTerms = {\n 'headers': 1,\n 'data': 2,\n 'totals': 4,\n 'all': 8,\n 'this row': 16,\n '@': 16\n};\n\n// only combinations allowed are: #data + (#headers | #totals | #data)\nconst fz = (...a: string[]) => Object.freeze(a);\nconst sectionMap = {\n // no terms\n 0: fz(),\n // single term\n 1: fz('headers'),\n 2: fz('data'),\n 4: fz('totals'),\n 8: fz('all'),\n 16: fz('this row'),\n // headers+data\n 3: fz('headers', 'data'),\n // totals+data\n 6: fz('data', 'totals')\n};\n\nfunction matchKeyword (str: string, pos: number): number {\n let p = pos;\n if (str.charCodeAt(p++) !== BR_OPEN) {\n return;\n }\n if (str.charCodeAt(p++) !== HASH) {\n return;\n }\n do {\n const c = str.charCodeAt(p);\n if (\n (c >= 65 && c <= 90) || // A-Z\n (c >= 97 && c <= 122) || // a-z\n (c === 32) // space\n ) {\n p++;\n }\n else {\n break;\n }\n }\n while (p < pos + 11); // max length: '[#this row'\n if (str.charCodeAt(p++) !== BR_CLOSE) {\n return;\n }\n return p - pos;\n}\n\nfunction skipWhitespace (str: string, pos: number): number {\n let p = pos;\n while (isWS(str.charCodeAt(p))) { p++; }\n return p - pos;\n}\n\nfunction matchColumn (str: string, pos: number, allowUnbraced = true): [ string, string ] {\n let p = pos;\n let column = '';\n if (str.charCodeAt(p) === BR_OPEN) {\n p++;\n let c;\n do {\n c = str.charCodeAt(p);\n if (c === QUOT_SINGLE) {\n p++;\n c = str.charCodeAt(p);\n // Allowed set: '#@[]\n if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN || c === BR_CLOSE) {\n column += String.fromCharCode(c);\n p++;\n }\n else {\n return;\n }\n }\n // Allowed set is all chars BUT: '#@[]\n else if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN) {\n return;\n }\n else if (c === BR_CLOSE) {\n p++;\n return [ str.slice(pos, p), column ];\n }\n else {\n column += String.fromCharCode(c);\n p++;\n }\n }\n while (p < str.length);\n }\n else if (allowUnbraced) {\n let c;\n do {\n c = str.charCodeAt(p);\n // Allowed set is all chars BUT: '#@[]:\n if (c === QUOT_SINGLE || c === HASH || c === AT || c === BR_OPEN || c === BR_CLOSE || c === COLON) {\n break;\n }\n else {\n column += String.fromCharCode(c);\n p++;\n }\n }\n while (p < str.length);\n if (p !== pos) {\n return [ column, column ];\n }\n }\n}\n\nexport type SRange = {\n columns: string[],\n sections: string[],\n length: number,\n token: string\n};\n\nexport function parseSRange (str: string, pos: number = 0): SRange {\n const columns: string[] = [];\n const start = pos;\n let m;\n let terms = 0;\n\n // structured refs start with a [\n if (str.charCodeAt(pos) !== BR_OPEN) {\n return;\n }\n\n // simple keyword: [#keyword]\n if ((m = matchKeyword(str, pos))) {\n const k = str.slice(pos + 2, pos + m - 1);\n pos += m;\n const term = keyTerms[k.toLowerCase()];\n if (!term) { return; }\n terms |= term;\n }\n // simple column: [column]\n else if ((m = matchColumn(str, pos, false))) {\n pos += m[0].length;\n if (m[1]) {\n columns.push(m[1]);\n }\n }\n // use the \"normal\" method\n // [[#keyword]]\n // [[column]]\n // [@]\n // [@column]\n // [@[column]]\n // [@column:column]\n // [@column:[column]]\n // [@[column]:column]\n // [@[column]:[column]]\n // [column:column]\n // [column:[column]]\n // [[column]:column]\n // [[column]:[column]]\n // [[#keyword],column]\n // [[#keyword],column:column]\n // [[#keyword],[#keyword],column:column]\n // ...\n else {\n let expect_more = true;\n pos++; // skip open brace\n pos += skipWhitespace(str, pos);\n // match keywords as we find them\n while (expect_more && (m = matchKeyword(str, pos))) {\n const k = str.slice(pos + 2, pos + m - 1);\n const term = keyTerms[k.toLowerCase()];\n if (!term) { return; }\n terms |= term;\n pos += m;\n pos += skipWhitespace(str, pos);\n expect_more = str.charCodeAt(pos) === COMMA;\n if (expect_more) {\n pos++;\n pos += skipWhitespace(str, pos);\n }\n }\n // is there an @ specifier?\n if (expect_more && (str.charCodeAt(pos) === AT)) {\n terms |= keyTerms['@'];\n pos += 1;\n expect_more = str.charCodeAt(pos) !== BR_CLOSE;\n }\n // not all keyword terms may be combined\n if (!sectionMap[terms]) {\n return;\n }\n // column definitions\n const leftCol = expect_more && matchColumn(str, pos, true);\n if (leftCol) {\n pos += leftCol[0].length;\n columns.push(leftCol[1]);\n if (str.charCodeAt(pos) === COLON) {\n pos++;\n const rightCol = matchColumn(str, pos, true);\n if (rightCol) {\n pos += rightCol[0].length;\n columns.push(rightCol[1]);\n }\n else {\n return;\n }\n }\n expect_more = false;\n }\n // advance ws\n pos += skipWhitespace(str, pos);\n // close the ref\n if (expect_more || str.charCodeAt(pos) !== BR_CLOSE) {\n return;\n }\n // step over the closing ]\n pos++;\n }\n\n const sections: string[] = sectionMap[terms];\n return {\n columns,\n sections: sections ? sections.concat() : sections,\n length: pos - start,\n token: str.slice(start, pos)\n };\n}\n","import { parseSRange } from '../parseSRange.ts';\nimport { REF_STRUCT } from '../constants.ts';\nimport { isWS } from './lexWhitespace.ts';\nimport type { Token } from '../types.ts';\n\nconst EXCL = 33; // !\n\nexport function lexStructured (str: string, pos: number): Token | undefined {\n const structData = parseSRange(str, pos);\n if (structData && structData.length) {\n // we have a match for a valid SR\n let i = structData.length;\n // skip tailing whitespace\n while (isWS(str.charCodeAt(pos + i))) {\n i++;\n }\n // and ensure that it isn't followed by a !\n if (str.charCodeAt(pos + i) !== EXCL) {\n return {\n type: REF_STRUCT,\n value: structData.token\n };\n }\n }\n}\n","import { NUMBER } from '../constants.ts';\nimport type { Token } from '../types.ts';\n\nfunction advDigits (str: string, pos: number): number {\n const start = pos;\n do {\n const c = str.charCodeAt(pos);\n if (c < 48 || c > 57) { // 0-9\n break;\n }\n pos++;\n }\n while (pos < str.length);\n return pos - start;\n}\n\n// \\d+(\\.\\d+)?(?:[eE][+-]?\\d+)?\nexport function lexNumber (str: string, pos: number): Token | undefined {\n const start = pos;\n\n // integer\n const lead = advDigits(str, pos);\n if (!lead) { return; }\n pos += lead;\n\n // optional fraction part\n const c0 = str.charCodeAt(pos);\n if (c0 === 46) { // .\n pos++;\n const frac = advDigits(str, pos);\n if (!frac) { return; }\n pos += frac;\n }\n // optional exponent part\n const c1 = str.charCodeAt(pos);\n if (c1 === 69 || c1 === 101) { // E e\n pos++;\n const sign = str.charCodeAt(pos);\n if (sign === 43 || sign === 45) { // + -\n pos++;\n }\n const exp = advDigits(str, pos);\n if (!exp) { return; }\n pos += exp;\n }\n\n return { type: NUMBER, value: str.slice(start, pos) };\n}\n","import { REF_NAMED } from '../constants.ts';\nimport type { Token } from '../types.ts';\n\n// The advertized named ranges rules are a bit off from what Excel seems to do.\n// In the \"extended range\" of chars, it looks like it allows most things above\n// U+00B0 with the range between U+00A0-U+00AF rather random:\n// /^[a-zA-Z\\\\_¡¤§¨ª\\u00ad¯\\u00b0-\\uffff][a-zA-Z0-9\\\\_.?¡¤§¨ª\\u00ad¯\\u00b0-\\uffff]{0,254}/\n//\n// I've simplified to allowing everything above U+00A1:\n// /^[a-zA-Z\\\\_\\u00a1-\\uffff][a-zA-Z0-9\\\\_.?\\u00a1-\\uffff]{0,254}/\nexport function lexNamed (str: string, pos: number): Token | undefined {\n const start = pos;\n // starts with: [a-zA-Z\\\\_\\u00a1-\\uffff]\n const s = str.charCodeAt(pos);\n if (\n (s >= 65 && s <= 90) || // A-Z\n (s >= 97 && s <= 122) || // a-z\n (s === 95) || // _\n (s === 92) || // \\\n (s > 0xA0) // \\u00a1-\\uffff\n ) {\n pos++;\n }\n else {\n return;\n }\n // has any number of: [a-zA-Z0-9\\\\_.?\\u00a1-\\uffff]\n let c: number;\n do {\n c = str.charCodeAt(pos);\n if (\n (c >= 65 && c <= 90) || // A-Z\n (c >= 97 && c <= 122) || // a-z\n (c >= 48 && c <= 57) || // 0-9\n (c === 95) || // _\n (c === 92) || // \\\n (c === 46) || // .\n (c === 63) || // ?\n (c > 0xA0) // \\u00a1-\\uffff\n ) {\n pos++;\n }\n else {\n break;\n }\n } while (isFinite(c));\n\n const len = pos - start;\n if (len && len < 255) {\n // names starting with \\ must be at least 3 char long\n if (s === 92 && len < 3) {\n return;\n }\n // single characters R and C are forbidden as names\n if (len === 1 && (s === 114 || s === 82 || s === 99 || s === 67)) {\n return;\n }\n return { type: REF_NAMED, value: str.slice(start, pos) };\n }\n}\n","import { OPERATOR } from '../constants.ts';\nimport type { Token } from '../types.ts';\nimport { advRangeOp } from './advRangeOp.ts';\n\nconst EXCL = 33; // !\n\nexport function lexRefOp (str: string, pos: number, opts: { r1c1: boolean }): Token | undefined {\n // in R1C1 mode we only allow [ '!' ]\n if (str.charCodeAt(pos) === EXCL) {\n return { type: OPERATOR, value: str[pos] };\n }\n if (!opts.r1c1) {\n // in A1 mode we allow [ '!' ] + [ ':', '.:', ':.', '.:.']\n const opLen = advRangeOp(str, pos);\n if (opLen) {\n return { type: OPERATOR, value: str.slice(pos, pos + opLen) };\n }\n }\n}\n","import { CONTEXT, FUNCTION, REF_NAMED, UNKNOWN } from '../constants.ts';\nimport type { Token } from '../types.ts';\nimport { lexContextUnquoted } from './lexContext.ts';\n\nconst BR_OPEN = 91; // [\nconst PAREN_OPEN = 40;\nconst EXCL = 33; // !\nconst OFFS = 32;\n\n// build a map of characters to allow-bitmasks\nconst ALLOWED = new Uint8Array(180 - OFFS);\nconst OK_NAME_0 = 0b000001;\nconst OK_FUNC_0 = 0b000010;\nconst OK_CNTX_0 = 0b000100;\nconst OK_NAME_N = 0b001000;\nconst OK_FUNC_N = 0b010000;\nconst OK_CNTX_N = 0b100000;\nconst OK_0 = OK_NAME_0 | OK_FUNC_0 | OK_CNTX_0;\nconst OK_N = OK_NAME_N | OK_FUNC_N | OK_CNTX_N;\nconst OK_HIGHCHAR = OK_NAME_0 | OK_NAME_N | OK_CNTX_0 | OK_CNTX_N;\nfor (let c = OFFS; c < 180; c++) {\n const char = String.fromCharCode(c);\n const n0 = /^[a-zA-Z_\\\\\\u00a1-\\uffff]$/.test(char);\n const f0 = /^[a-zA-Z_]$/.test(char);\n const nN = /^[a-zA-Z0-9_.\\\\?\\u00a1-\\uffff]$/.test(char);\n const fN = /^[a-zA-Z0-9_.]$/.test(char);\n const cX = /^[a-zA-Z0-9_.¡¤§¨ª\\u00ad¯-\\uffff]$/.test(char);\n ALLOWED[c - OFFS] = (\n (n0 ? OK_NAME_0 : 0) |\n (nN ? OK_NAME_N : 0) |\n (f0 ? OK_FUNC_0 : 0) |\n (fN ? OK_FUNC_N : 0) |\n (cX ? OK_CNTX_0 : 0) |\n (cX ? OK_CNTX_N : 0)\n );\n}\n\nfunction nameOrUnknown (str, s, start, pos, name) {\n const len = pos - start;\n if (name && len && len < 255) {\n // names starting with \\ must be at least 3 char long\n if (s === 92 && len < 3) {\n return;\n }\n // single characters R and C are forbidden as names\n if (len === 1 && (s === 114 || s === 82 || s === 99 || s === 67)) {\n return;\n }\n return { type: REF_NAMED, value: str.slice(start, pos) };\n }\n return { type: UNKNOWN, value: str.slice(start, pos) };\n}\n\nexport function lexNameFuncCntx (\n str: string,\n pos: number,\n opts: { xlsx: boolean }\n): Token | undefined {\n const start = pos;\n\n const s = str.charCodeAt(pos);\n const a = s > 180 ? OK_HIGHCHAR : ALLOWED[s - OFFS];\n // name: [a-zA-Z_\\\\\\u00a1-\\uffff]\n // func: [a-zA-Z_]\n // cntx: [a-zA-Z_0-9.¡¤§¨ª\\u00ad¯-\\uffff]\n if (((a & OK_CNTX_0) && !(a & OK_NAME_0) && !(a & OK_FUNC_0)) || s === BR_OPEN) {\n // its a context so delegate to that lexer\n return lexContextUnquoted(str, pos, opts);\n }\n if (!(a & OK_0)) {\n return;\n }\n let name = (a & OK_NAME_0) ? 1 : 0;\n let func = (a & OK_FUNC_0) ? 1 : 0;\n let cntx = (a & OK_CNTX_0) ? 1 : 0;\n pos++;\n\n let c: number;\n do {\n c = str.charCodeAt(pos);\n const a = s > 180 ? OK_HIGHCHAR : ALLOWED[c - OFFS] ?? 0;\n if (a & OK_N) {\n // name: [a-zA-Z_0-9.\\\\?\\u00a1-\\uffff]\n // func: [a-zA-Z_0-9.]\n // cntx: [a-zA-Z_0-9.¡¤§¨ª\\u00ad¯-\\uffff]\n if (name && !(a & OK_NAME_N)) {\n name = 0;\n }\n if (func && !(a & OK_FUNC_N)) {\n func = 0;\n }\n if (cntx && !(a & OK_CNTX_N)) {\n cntx = 0;\n }\n }\n else {\n if (c === PAREN_OPEN && func) {\n return { type: FUNCTION, value: str.slice(start, pos) };\n }\n else if (c === EXCL && cntx) {\n return { type: CONTEXT, value: str.slice(start, pos) };\n }\n return nameOrUnknown(str, s, start, pos, name);\n }\n pos++;\n }\n while ((name || func || cntx) && pos < str.length);\n\n if (start !== pos) {\n return nameOrUnknown(str, s, start, pos, name);\n }\n}\n","import { lexError } from './lexError.ts';\nimport { lexRangeTrim } from './lexRangeTrim.ts';\nimport { lexOperator } from './lexOperator.ts';\nimport { lexBoolean } from './lexBoolean.ts';\nimport { lexNewLine } from './lexNewLine.ts';\nimport { lexWhitespace } from './lexWhitespace.ts';\nimport { lexString } from './lexString.ts';\nimport { lexContextQuoted, lexContextUnquoted } from './lexContext.ts';\nimport { lexRange } from './lexRange.ts';\nimport { lexStructured } from './lexStructured.ts';\nimport { lexNumber } from './lexNumber.ts';\nimport { lexNamed } from './lexNamed.ts';\nimport { lexRefOp } from './lexRefOp.ts';\nimport { lexNameFuncCntx } from './lexNameFuncCntx.ts';\nimport type { Token } from '../types.ts';\n\nexport type PartLexer = (\n str: string,\n pos: number,\n options?: Partial<{\n xlsx: boolean,\n allowTerniary: boolean,\n allowTernary: boolean,\n mergeRefs: boolean,\n r1c1: boolean\n }>\n) => Token | undefined;\n\nexport const lexers: PartLexer[] = [\n lexError,\n lexRangeTrim,\n lexOperator,\n lexNewLine,\n lexWhitespace,\n lexString,\n lexRange,\n lexNumber,\n lexBoolean,\n lexContextQuoted,\n lexNameFuncCntx,\n lexStructured\n];\n\nexport const lexersRefs = [\n lexRefOp,\n lexContextQuoted,\n lexContextUnquoted,\n lexRange,\n lexStructured,\n lexNamed\n];\n","export function isRCTokenValue (value: string): boolean {\n return value === 'r' || value === 'R' || value === 'c' || value === 'C';\n}\n","import {\n FX_PREFIX,\n NEWLINE,\n NUMBER,\n OPERATOR,\n REF_NAMED,\n UNKNOWN,\n WHITESPACE,\n FUNCTION,\n OPERATOR_TRIM,\n REF_RANGE,\n REF_BEAM\n} from './constants.ts';\nimport { mergeRefTokens } from './mergeRefTokens.ts';\nimport { lexers, type PartLexer } from './lexers/sets.ts';\nimport type { Token } from './types.ts';\nimport { isRCTokenValue } from './isRCTokenValue.ts';\n\nconst reLetLambda = /^l(?:ambda|et)$/i;\nconst isType = (t: Token, type: string) => t && t.type === type;\nconst isTextTokenType = (tokenType: string) => tokenType === REF_NAMED || tokenType === FUNCTION;\n\nconst causesBinaryMinus = (token: Token) => {\n return !isType(token, OPERATOR) || (\n token.value === '%' ||\n token.value === '}' ||\n token.value === ')' ||\n token.value === '#'\n );\n};\n\nfunction fixRCNames (tokens: Token[], r1c1Mode?: boolean): Token[] {\n let withinCall = 0;\n let parenDepth = 0;\n let lastToken: Token;\n for (const token of tokens) {\n const tokenType = token.type;\n if (tokenType === OPERATOR) {\n if (token.value === '(') {\n parenDepth++;\n if (lastToken.type === FUNCTION) {\n if (reLetLambda.test(lastToken.value)) {\n withinCall = parenDepth;\n }\n }\n }\n else if (token.value === ')') {\n parenDepth--;\n if (parenDepth < withinCall) {\n withinCall = 0;\n }\n }\n }\n else if (withinCall && tokenType === UNKNOWN && isRCTokenValue(token.value)) {\n token.type = REF_NAMED;\n }\n else if (withinCall && r1c1Mode && tokenType === REF_BEAM && isRCTokenValue(token.value)) {\n token.type = REF_NAMED;\n }\n lastToken = token;\n }\n return tokens;\n}\n\ntype OptsGetTokens = {\n withLocation?: boolean,\n mergeRefs?: boolean,\n negativeNumbers?: boolean\n allowTernary?: boolean\n r1c1?: boolean\n xlsx?: boolean\n};\n\nexport function getTokens (fx: string, tokenHandlers: PartLexer[], options: OptsGetTokens = {}) {\n const {\n withLocation = false,\n mergeRefs = true,\n negativeNumbers = true\n } = options;\n const opts = {\n withLocation: withLocation,\n mergeRefs: mergeRefs,\n allowTernary: options.allowTernary ?? false,\n negativeNumbers: negativeNumbers,\n r1c1: options.r1c1 ?? false,\n xlsx: options.xlsx ?? false\n };\n\n const tokens = [];\n let pos = 0;\n let letOrLambda = 0;\n let unknownRC = 0;\n const trimOps = [];\n\n let tail0: Token; // last non-whitespace token\n let tail1: Token; // penultimate non-whitespace token\n let lastToken: Token; // last token\n const pushToken = (token: Token) => {\n let tokenType = token.type;\n const isCurrUnknown = tokenType === UNKNOWN;\n const isLastUnknown = lastToken && lastToken.type === UNKNOWN;\n if (lastToken && (\n (isCurrUnknown && isLastUnknown) ||\n (isCurrUnknown && isTextTokenType(lastToken.type)) ||\n (isLastUnknown && isTextTokenType(tokenType))\n )) {\n // UNKNOWN tokens \"contaminate\" sibling text tokens\n lastToken.value += token.value;\n lastToken.type = UNKNOWN;\n if (withLocation) {\n lastToken.loc[1] = token.loc[1];\n }\n }\n else {\n if (tokenType === OPERATOR_TRIM) {\n trimOps.push(tokens.length);\n tokenType = UNKNOWN;\n token.type = UNKNOWN;\n }\n // push token as normally\n tokens[tokens.length] = token;\n lastToken = token;\n if (tokenType !== WHITESPACE && tokenType !== NEWLINE) {\n tail1 = tail0;\n tail0 = token;\n }\n }\n };\n\n if (fx.startsWith('=')) {\n const token: Token = { type: FX_PREFIX, value: '=' };\n if (withLocation) {\n token.loc = [ 0, 1 ];\n }\n pos++;\n pushToken(token);\n }\n\n const numHandlers = tokenHandlers.length;\n while (pos < fx.length) {\n const startPos = pos;\n let token;\n for (let i = 0; i < numHandlers; i++) {\n token = tokenHandlers[i](fx, pos, opts);\n if (token) {\n pos += token.value.length;\n break;\n }\n }\n\n if (!token) {\n token = {\n type: UNKNOWN,\n value: fx[pos]\n };\n pos++;\n }\n if (withLocation) {\n token.loc = [ startPos, pos ];\n }\n\n // make a note if we found a let/lambda call\n if (lastToken && token.value === '(' && lastToken.type === FUNCTION) {\n if (reLetLambda.test(lastToken.value)) {\n letOrLambda++;\n }\n }\n // Make a note if we found a R or C unknown or REF_BEAM token in R1C1 mode.\n // It seemse unlikely that anyone does `F2 = LET(c,1,c+F:F)` as this is a\n // circular reference (and not a very useful one), so we're assuming that\n // all \"c\" or \"r\" tokens found within the LET are names.\n if (token.value.length === 1 && (token.type === UNKNOWN || (opts.r1c1 && token.type === REF_BEAM))) {\n unknownRC += isRCTokenValue(token.value) ? 1 : 0;\n }\n\n if (negativeNumbers && token.type === NUMBER) {\n const last1 = lastToken;\n // do we have a number preceded by a minus?\n if (last1?.type === OPERATOR && last1.value === '-') {\n // missing tail1 means we are at the start of the stream\n if (\n !tail1 ||\n tail1.type === FX_PREFIX ||\n !causesBinaryMinus(tail1)\n ) {\n const minus = tokens.pop();\n token.value = '-' + token.value;\n if (token.loc) {\n // ensure offsets are up to date\n token.loc[0] = minus.loc[0];\n }\n // next step tries to counter the screwing around with the tailing\n // it should be correct again once we pushToken()\n tail0 = tail1;\n lastToken = tokens[tokens.length - 1];\n }\n }\n }\n\n pushToken(token);\n }\n\n // if we encountered both a LAMBDA/LET call, and unknown 'r' or 'c' tokens\n // we'll turn the unknown tokens into names within the call.\n if (unknownRC && letOrLambda) {\n fixRCNames(tokens, opts.r1c1);\n }\n\n // Any OPERATOR_TRIM tokens have been indexed already, they now need to be\n // either turned into OPERATORs or UNKNOWNs. Trim operators are only allowed\n // between two REF_RANGE tokens as they are not valid in expressions as full\n // operators.\n for (const index of trimOps) {\n const before = tokens[index - 1];\n const after = tokens[index + 1];\n tokens[index].type = (before?.type === REF_RANGE && after?.type === REF_RANGE)\n ? OPERATOR\n : UNKNOWN;\n }\n\n if (mergeRefs) {\n return mergeRefTokens(tokens);\n }\n\n return tokens;\n}\n\n/**\n * Options for {@link tokenize}.\n */\nexport type OptsTokenize = {\n /**\n * Nodes will include source position offsets to the tokens: `{ loc: [ start, end ] }`\n * @defaultValue true\n */\n withLocation?: boolean,\n /**\n * Should ranges be returned as whole references (`Sheet1!A1:B2`) or as separate tokens for each\n * part: (`Sheet1`,`!`,`A1`,`:`,`B2`). This is the same as calling [`mergeRefTokens`](#mergeRefTokens)\n * @defaultValue true\n */\n mergeRefs?: boolean,\n /**\n * Merges unary minuses with their immediately following number tokens (`-`,`1`) => `-1`\n * (alternatively these will be unary operations in the tree).\n * @defaultValue true\n */\n negativeNumbers?: boolean\n /**\n * Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`. These are supported\n * by Google Sheets but not Excel. See: [References.md](./References.md).\n * @defaultValue false\n */\n allowTernary?: boolean\n /**\n * Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.\n * @defaultValue false\n */\n r1c1?: boolean\n};\n\n/**\n * Breaks a string formula into a list of tokens.\n *\n * The returned output will be an array of objects representing the tokens:\n *\n * ```js\n * [\n * { type: FX_PREFIX, value: '=' },\n * { type: FUNCTION, value: 'SUM' },\n * { type: OPERATOR, value: '(' },\n * { type: REF_RANGE, value: 'A1:B2' },\n * { type: OPERATOR, value: ')' }\n * ]\n * ```\n *\n * A collection of token types may be found as an object as the {@link tokenTypes}\n * export on the package.\n *\n * _Warning:_ To support syntax highlighting as you type, `STRING` tokens are allowed to be\n * \"unterminated\". For example, the incomplete formula `=\"Hello world` would be\n * tokenized as:\n *\n * ```js\n * [\n * { type: FX_PREFIX, value: '=' },\n * { type: STRING, value: '\"Hello world', unterminated: true },\n * ]\n * ```\n *\n * Parsers will ne