UNPKG

@qualifyze/airtable-formulator

Version:
331 lines 9.71 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.tokenize = exports.isTokenName = exports.operatorMatcher = void 0; const airtable_formula_reference_json_1 = require("../airtable-formula-reference.json"); function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } // convert operators to a regexp matcher exports.operatorMatcher = new RegExp(Object.keys(airtable_formula_reference_json_1.operators) .map((op) => escapeRegExp(op)) // Sort by length so that longer operators are matched first .sort((a, b) => b.length - a.length) .join("|")); const workingTokens = [ "escapedSingleQuote", "escapedDoubleQuote", "escapedBackslash", "openSingleQuote", "closeSingleQuote", "openDoubleQuote", "closeDoubleQuote", "singleQuotedString", "doubleQuotedString", "bracedReference", ]; const tokenNames = [ "space", "number", "string", "quoteMark", "operator", "openParenthesis", "closeParenthesis", "openBrace", "closeBrace", "reference", "argumentSeparator", "group", ]; function isWorkingTokenTypeName(type) { return [...tokenNames, ...workingTokens].includes(type); } function isTokenName(token) { return tokenNames.includes(token); } exports.isTokenName = isTokenName; function isOpening(token) { const { opens, closes, memberOf } = token; return (typeof opens === "string" && isWorkingTokenTypeName(opens) && !closes && !memberOf); } function isClosing(token) { const { opens, closes, memberOf } = token; return (typeof closes === "string" && isWorkingTokenTypeName(closes) && !opens && !memberOf); } function isMember(token) { const { opens, closes, memberOf } = token; return (typeof memberOf === "string" && isWorkingTokenTypeName(memberOf) && !opens && !closes); } function isEnclosed(token) { const { opens, closes, memberOf } = token; return typeof !memberOf && !opens && !closes; } const openingTokens = [ { type: "openDoubleQuote", finalType: "quoteMark", on: /"/, opens: "doubleQuotedString", onlyIn: "group", }, { type: "openSingleQuote", finalType: "quoteMark", on: /'/, opens: "singleQuotedString", onlyIn: "group", }, { type: "openParenthesis", on: /\(/, opens: "group", onlyIn: "group", }, { type: "openBrace", on: /\{/, opens: "bracedReference", onlyIn: "group", }, ]; const closingTokens = [ { type: "closeDoubleQuote", finalType: "quoteMark", on: /"/, closes: "doubleQuotedString", }, { type: "closeSingleQuote", finalType: "quoteMark", on: /'/, closes: "singleQuotedString", }, { type: "closeParenthesis", on: /\)/, closes: "group", }, { type: "closeBrace", on: /}/, closes: "bracedReference", }, ]; const memberTokens = [ { type: "escapedBackslash", finalType: "string", on: /(?:\\{2})+/, flatten: true, memberOf: "doubleQuotedString", }, { type: "escapedBackslash", finalType: "string", on: /(?:\\{2})+/, flatten: true, memberOf: "singleQuotedString", }, { type: "escapedDoubleQuote", finalType: "string", on: /\\"/, memberOf: "doubleQuotedString", flatten: true, }, { type: "escapedSingleQuote", finalType: "string", on: /\\''/, memberOf: "singleQuotedString", flatten: true, }, { type: "number", on: /\d+(?:\.\d+)?/, memberOf: "group", }, { type: "argumentSeparator", on: /,/, memberOf: "group", }, { type: "operator", on: exports.operatorMatcher, memberOf: "group", }, { type: "reference", // The Assumption is that a reference may not contain special characters // So that refs like `$myField` or `%myField` are not allowed, unless they // are braced with `{}`. on: /\b[a-z]\w*/i, memberOf: "group", }, { type: "space", on: /\s+/, memberOf: "group", }, ]; const enclosedTokens = [ { type: "bracedReference", finalType: "reference", on: /[^{}]+/, }, { type: "doubleQuotedString", finalType: "string", on: /[^"\\]+/, }, { type: "singleQuotedString", finalType: "string", on: /[^']+/, }, { type: "group", // eslint-disable-next-line no-empty-character-class on: /[]/, // Everything should have been matched by now. So anything else is an error. }, ]; const tokenTypes = [ ...openingTokens, ...closingTokens, ...memberTokens, ...enclosedTokens, ]; function finalizeToken({ type, opener, closer, members, ...rest }) { const typeDef = tokenTypes.find((t) => t.type === type); if (!typeDef) { throw new Error(`Unknown token type: ${type}`); } return { type: typeDef.finalType || typeDef.type, ...Object.fromEntries([ ["opener", opener && finalizeToken(opener)], ["closer", closer && finalizeToken(closer)], ["members", members && members.map(finalizeToken)], ].filter(([, value]) => value !== undefined)), ...rest, }; } function createTokenFromMatch(type, match, offset = 0) { return { type, start: offset + match.index, end: offset + match.index + match[0].length, value: match[0], }; } function isToken(obj) { const { type, start, end, value } = obj; return (typeof type === "string" && isTokenName(type) && typeof start === "number" && typeof end === "number" && typeof value === "string"); } function closeEnclosedToken(currentToken, closer) { return { ...currentToken, closer, end: closer.start, }; } function appendToken(group, member, addMember = true) { if (!group.end) { group.value += member.value; } if (addMember) { group.members.push(member); } } function getApplicableTypes(currentToken) { return tokenTypes.filter(({ type, closes, memberOf, onlyIn }) => type === currentToken.type || (closes && closes === currentToken.type) || (memberOf && memberOf === currentToken.type) || (onlyIn && onlyIn === currentToken.type)); } function findNextToken(applicableTypes, remaining, i, currentToken) { const matches = applicableTypes .map((tokenType) => ({ match: tokenType.on.exec(remaining), tokenType })) .filter(({ match }) => match?.index === 0); if (matches.length === 0) { throw new Error(`Syntax error at position ${i} for ${currentToken.type}:\n\tExpected: ${applicableTypes .map(({ type, on }) => `${type} (${on})`) .join(", ")}\n\tGot: "${remaining}"`); } const { match, tokenType } = matches.reduce((a, b) => a.match.index <= b.match.index ? a : b); return { match, tokenType }; } function tokenize(formula) { const rootToken = { start: 0, end: formula.length, type: "group", value: formula, members: [], }; let currentToken = rootToken; const stack = []; for (let i = 0; i < formula.length; i++) { const remaining = formula.slice(i); const applicableTypes = getApplicableTypes(currentToken); const { match, tokenType } = findNextToken(applicableTypes, remaining, i, currentToken); const matchedToken = createTokenFromMatch(tokenType.type, match, i); const invalidStr = formula.slice(i, matchedToken.start); if (invalidStr) { throw new Error(`Syntax Error: Invalid tokens at position ${i}: ${invalidStr}`); } if (isOpening(tokenType)) { const { opens } = tokenType; const openingToken = { type: opens, value: "", start: matchedToken.end, opener: matchedToken, members: [], }; stack.push(currentToken); currentToken = openingToken; } else if (isClosing(tokenType)) { const superToken = stack.pop(); if (!superToken) { throw new Error(`Syntax Error: Unexpected closing token ${match[0]} at ${i}`); } superToken.members.push(closeEnclosedToken(currentToken, matchedToken)); currentToken = superToken; } else if (isMember(tokenType) || isEnclosed(tokenType)) { const { flatten } = tokenType; appendToken(currentToken, matchedToken, isMember(tokenType) && !flatten); i = matchedToken.end - 1; } else { throw new Error(`Syntax Error: Unexpected token ${match[0]} at ${i}`); } } if (currentToken !== rootToken) { throw new Error(`SyntaxError: Unclosed token ${currentToken.type}, with ${currentToken.opener?.value} at position ${currentToken.opener?.start}, but no closing token at position ${currentToken.start + currentToken.value.length}`); } if (!isToken(rootToken)) { throw new Error("Internal error: Root Token is incomplete"); } return finalizeToken(rootToken); } exports.tokenize = tokenize; //# sourceMappingURL=tokenize.js.map