@qualifyze/airtable-formulator
Version:
Airtable Formula Manipulator
331 lines • 9.71 kB
JavaScript
;
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