/**
* @module tag_utils
* @description A set of utilities for parsing and testing tags.
*/
const BLANK_STRING_REGEX = /^\s*$/;
const STRING_SPLITTER = /\s+|([\(\)])/g;
const cache = new Map();
/**
* Given a tag expression and a set of tags, evaluate whether the tags meet
* the expression or not.
*
* @example
* matcher('luxury | clothing', ['clothing'])
* => true
* matcher('luxury | clothing', ['alcohol'])
* => false
* matcher('(food | ammo) luxury', ['ammo'])
* => false
* matcher('(food | ammo) luxury', ['luxury', 'food'])
* => true
*
* @param {String} expression
* @param {Set} tags a set of tag strings to match against
* @returns boolean true if the tags match the expression, false otherwise
*/
export function matcher(expression, tags) {
if (typeof expression !== "string" || expression === "") {
return false;
}
if (expression === "*") {
return true;
}
let node = cache.get(expression);
if (!node) {
node = parser(expression);
cache.set(expression, node);
}
return node.evaluate(Array.from(tags));
}
// exported for tests
export function parser(string) {
if (typeof string === "undefined" || BLANK_STRING_REGEX.test(string)) {
return new True();
}
if (string instanceof Array) {
return _parse(string);
}
let tokens = string.split(STRING_SPLITTER).filter((el) => el);
return _parse(tokens);
}
function _parse(array) {
if (array.length === 0) {
return new True();
}
let someOf = new SomeOf();
let allOf = new AllOf();
for (let i = 0, len = array.length; i < len; i++) {
let element = array[i];
if (element === "(") {
let j = seek(array, i);
let subarray = array.slice(i + 1, j);
allOf.add(_parse(subarray));
i = j;
} else if (element === "|") {
someOf.add(allOf);
allOf = new AllOf();
} else {
if (element.substring(0, 1) === "-") {
allOf.add(new Not(element.substring(1)));
} else if (element !== "&") {
allOf.add(new Literal(element));
}
}
}
someOf.add(allOf);
return someOf.expressions.length === 1 ? someOf.expressions[0] : someOf;
}
function seek(array, i) {
let indent = 0;
for (let j = i; j < array.length; j++) {
if (array[j] === "(") {
indent++;
} else if (array[j] === ")") {
indent--;
}
if (i !== j && indent === 0) {
return j;
}
}
throw new Error("Mismatched parentheses at: " + i);
}
class SetBase {
constructor() {
this.expressions = [];
}
add(expr) {
if (expr instanceof SetBase && expr.expressions.length === 1) {
this.expressions.push(expr.expressions[0]);
} else {
this.expressions.push(expr);
}
}
}
class SomeOf extends SetBase {
evaluate(variables) {
return this.expressions.some((expr) => expr.evaluate(variables));
}
toString() {
return `(${this.expressions.join(" | ")})`;
}
}
class AllOf extends SetBase {
evaluate(variables) {
return this.expressions.every((expr) => expr.evaluate(variables));
}
toString() {
return `(${this.expressions.join(" & ")})`;
}
}
class Not {
constructor(token) {
this.token = token;
}
evaluate(variables) {
return variables.indexOf(this.token) === -1;
}
toString() {
return "-" + this.token;
}
}
class Literal {
constructor(token) {
this.token = token;
}
evaluate(variables) {
return variables.indexOf(this.token) !== -1;
}
toString() {
return this.token;
}
}
class True {
evaluate(variables) {
return true;
}
toString() {
return "true";
}
}