@cucumber/tag-expressions
Version:
Cucumber Tag Expression parser
227 lines • 7.07 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = parse;
var OPERAND = 'operand';
var OPERATOR = 'operator';
var PREC = {
'(': -2,
')': -1,
or: 0,
and: 1,
not: 2,
};
var ASSOC = {
or: 'left',
and: 'left',
not: 'right',
};
/**
* Parses infix boolean expression (using Dijkstra's Shunting Yard algorithm)
* and builds a tree of expressions. The root node of the expression is returned.
*
* This expression can be evaluated by passing in an array of literals that resolve to true
*/
function parse(infix) {
var tokens = tokenize(infix);
if (tokens.length === 0) {
return new True();
}
var expressions = [];
var operators = [];
var expectedTokenType = OPERAND;
tokens.forEach(function (token) {
if (isUnary(token)) {
check(expectedTokenType, OPERAND);
operators.push(token);
expectedTokenType = OPERAND;
}
else if (isBinary(token)) {
check(expectedTokenType, OPERATOR);
while (operators.length > 0 &&
isOp(peek(operators)) &&
((ASSOC[token] === 'left' && PREC[token] <= PREC[peek(operators)]) ||
(ASSOC[token] === 'right' && PREC[token] < PREC[peek(operators)]))) {
pushExpr(pop(operators), expressions);
}
operators.push(token);
expectedTokenType = OPERAND;
}
else if ('(' === token) {
check(expectedTokenType, OPERAND);
operators.push(token);
expectedTokenType = OPERAND;
}
else if (')' === token) {
check(expectedTokenType, OPERATOR);
while (operators.length > 0 && peek(operators) !== '(') {
pushExpr(pop(operators), expressions);
}
if (operators.length === 0) {
throw new Error("Tag expression \"".concat(infix, "\" could not be parsed because of syntax error: Unmatched )."));
}
if (peek(operators) === '(') {
pop(operators);
}
expectedTokenType = OPERATOR;
}
else {
check(expectedTokenType, OPERAND);
pushExpr(token, expressions);
expectedTokenType = OPERATOR;
}
});
while (operators.length > 0) {
if (peek(operators) === '(') {
throw new Error("Tag expression \"".concat(infix, "\" could not be parsed because of syntax error: Unmatched (."));
}
pushExpr(pop(operators), expressions);
}
return pop(expressions);
function check(expectedTokenType, tokenType) {
if (expectedTokenType !== tokenType) {
throw new Error("Tag expression \"".concat(infix, "\" could not be parsed because of syntax error: Expected ").concat(expectedTokenType, "."));
}
}
}
function tokenize(expr) {
var tokens = [];
var isEscaped = false;
var token = [];
for (var i = 0; i < expr.length; i++) {
var c = expr.charAt(i);
if (isEscaped) {
if (c === '(' || c === ')' || c === '\\' || /\s/.test(c)) {
token.push(c);
isEscaped = false;
}
else {
throw new Error("Tag expression \"".concat(expr, "\" could not be parsed because of syntax error: Illegal escape before \"").concat(c, "\"."));
}
}
else if (c === '\\') {
isEscaped = true;
}
else if (c === '(' || c === ')' || /\s/.test(c)) {
if (token.length > 0) {
tokens.push(token.join(''));
token = [];
}
if (!/\s/.test(c)) {
tokens.push(c);
}
}
else {
token.push(c);
}
}
if (token.length > 0) {
tokens.push(token.join(''));
}
return tokens;
}
function isUnary(token) {
return 'not' === token;
}
function isBinary(token) {
return 'or' === token || 'and' === token;
}
function isOp(token) {
return ASSOC[token] !== undefined;
}
function peek(stack) {
return stack[stack.length - 1];
}
function pop(stack) {
if (stack.length === 0) {
throw new Error('empty stack');
}
return stack.pop();
}
function pushExpr(token, stack) {
if (token === 'and') {
var rightAndExpr = pop(stack);
stack.push(new And(pop(stack), rightAndExpr));
}
else if (token === 'or') {
var rightOrExpr = pop(stack);
stack.push(new Or(pop(stack), rightOrExpr));
}
else if (token === 'not') {
stack.push(new Not(pop(stack)));
}
else {
stack.push(new Literal(token));
}
}
var Literal = /** @class */ (function () {
function Literal(value) {
this.value = value;
}
Literal.prototype.evaluate = function (variables) {
return variables.indexOf(this.value) !== -1;
};
Literal.prototype.toString = function () {
return this.value
.replace(/\\/g, '\\\\')
.replace(/\(/g, '\\(')
.replace(/\)/g, '\\)')
.replace(/\s/g, '\\ ');
};
return Literal;
}());
var Or = /** @class */ (function () {
function Or(leftExpr, rightExpr) {
this.leftExpr = leftExpr;
this.rightExpr = rightExpr;
}
Or.prototype.evaluate = function (variables) {
return this.leftExpr.evaluate(variables) || this.rightExpr.evaluate(variables);
};
Or.prototype.toString = function () {
return '( ' + this.leftExpr.toString() + ' or ' + this.rightExpr.toString() + ' )';
};
return Or;
}());
var And = /** @class */ (function () {
function And(leftExpr, rightExpr) {
this.leftExpr = leftExpr;
this.rightExpr = rightExpr;
}
And.prototype.evaluate = function (variables) {
return this.leftExpr.evaluate(variables) && this.rightExpr.evaluate(variables);
};
And.prototype.toString = function () {
return '( ' + this.leftExpr.toString() + ' and ' + this.rightExpr.toString() + ' )';
};
return And;
}());
var Not = /** @class */ (function () {
function Not(expr) {
this.expr = expr;
}
Not.prototype.evaluate = function (variables) {
return !this.expr.evaluate(variables);
};
Not.prototype.toString = function () {
if (this.expr instanceof And || this.expr instanceof Or) {
// -- HINT: Binary Operators already have already '( ... )'.
return 'not ' + this.expr.toString();
}
// -- OTHERWISE:
return 'not ( ' + this.expr.toString() + ' )';
};
return Not;
}());
var True = /** @class */ (function () {
function True() {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
True.prototype.evaluate = function (variables) {
return true;
};
True.prototype.toString = function () {
return 'true';
};
return True;
}());
//# sourceMappingURL=index.js.map