2012-05-05 12:15:44 +00:00
|
|
|
/*\
|
2012-06-06 11:17:08 +00:00
|
|
|
title: $:/core/modules/filters.js
|
2012-05-05 12:15:44 +00:00
|
|
|
type: application/javascript
|
|
|
|
module-type: wikimethod
|
|
|
|
|
2014-04-03 19:49:16 +00:00
|
|
|
Adds tiddler filtering methods to the $tw.Wiki object.
|
2012-05-08 14:11:53 +00:00
|
|
|
|
2012-05-05 12:15:44 +00:00
|
|
|
\*/
|
|
|
|
(function(){
|
|
|
|
|
|
|
|
/*jslint node: true, browser: true */
|
|
|
|
/*global $tw: false */
|
|
|
|
"use strict";
|
|
|
|
|
2012-05-08 14:11:53 +00:00
|
|
|
/*
|
2015-01-14 20:09:15 +00:00
|
|
|
Parses an operation (i.e. a run) within a filter string
|
|
|
|
operators: Array of array of operator nodes into which results should be inserted
|
2012-05-08 14:11:53 +00:00
|
|
|
filterString: filter string
|
|
|
|
p: start position within the string
|
|
|
|
Returns the new start position, after the parsed operation
|
|
|
|
*/
|
|
|
|
function parseFilterOperation(operators,filterString,p) {
|
2016-10-08 12:32:14 +00:00
|
|
|
var nextBracketPos, operator;
|
2012-05-08 14:11:53 +00:00
|
|
|
// Skip the starting square bracket
|
2013-02-05 19:12:05 +00:00
|
|
|
if(filterString.charAt(p++) !== "[") {
|
2012-05-08 14:11:53 +00:00
|
|
|
throw "Missing [ in filter expression";
|
|
|
|
}
|
|
|
|
// Process each operator in turn
|
|
|
|
do {
|
|
|
|
operator = {};
|
|
|
|
// Check for an operator prefix
|
2013-02-05 19:12:05 +00:00
|
|
|
if(filterString.charAt(p) === "!") {
|
|
|
|
operator.prefix = filterString.charAt(p++);
|
2012-05-08 14:11:53 +00:00
|
|
|
}
|
|
|
|
// Get the operator name
|
2016-10-08 12:32:14 +00:00
|
|
|
nextBracketPos = filterString.substring(p).search(/[\[\{<\/]/);
|
2014-01-14 21:08:05 +00:00
|
|
|
if(nextBracketPos === -1) {
|
2012-05-08 14:11:53 +00:00
|
|
|
throw "Missing [ in filter expression";
|
|
|
|
}
|
2014-01-14 21:08:05 +00:00
|
|
|
nextBracketPos += p;
|
|
|
|
var bracket = filterString.charAt(nextBracketPos);
|
|
|
|
operator.operator = filterString.substring(p,nextBracketPos);
|
|
|
|
// Any suffix?
|
2014-01-14 15:19:34 +00:00
|
|
|
var colon = operator.operator.indexOf(':');
|
|
|
|
if(colon > -1) {
|
2018-10-30 17:39:18 +00:00
|
|
|
// The raw suffix for older filters
|
2014-01-24 19:15:27 +00:00
|
|
|
operator.suffix = operator.operator.substring(colon + 1);
|
2014-01-14 21:08:05 +00:00
|
|
|
operator.operator = operator.operator.substring(0,colon) || "field";
|
2018-10-30 17:39:18 +00:00
|
|
|
// The processed suffix for newer filters
|
|
|
|
operator.suffixes = [];
|
|
|
|
$tw.utils.each(operator.suffix.split(":"),function(subsuffix) {
|
|
|
|
operator.suffixes.push([]);
|
|
|
|
$tw.utils.each(subsuffix.split(","),function(entry) {
|
|
|
|
entry = $tw.utils.trim(entry);
|
|
|
|
if(entry) {
|
|
|
|
operator.suffixes[operator.suffixes.length - 1].push(entry);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2014-01-14 15:19:34 +00:00
|
|
|
}
|
2014-01-14 21:08:05 +00:00
|
|
|
// Empty operator means: title
|
|
|
|
else if(operator.operator === "") {
|
2012-05-08 14:11:53 +00:00
|
|
|
operator.operator = "title";
|
|
|
|
}
|
2020-11-07 09:47:08 +00:00
|
|
|
operator.operands = [];
|
2021-03-14 10:27:05 +00:00
|
|
|
var parseOperand = function(bracketType) {
|
2020-11-07 09:47:08 +00:00
|
|
|
var operand = {};
|
|
|
|
switch (bracketType) {
|
|
|
|
case "{": // Curly brackets
|
|
|
|
operand.indirect = true;
|
|
|
|
nextBracketPos = filterString.indexOf("}",p);
|
|
|
|
break;
|
|
|
|
case "[": // Square brackets
|
|
|
|
nextBracketPos = filterString.indexOf("]",p);
|
|
|
|
break;
|
|
|
|
case "<": // Angle brackets
|
|
|
|
operand.variable = true;
|
|
|
|
nextBracketPos = filterString.indexOf(">",p);
|
|
|
|
break;
|
|
|
|
case "/": // regexp brackets
|
2021-01-29 15:26:25 +00:00
|
|
|
var rex = /^((?:[^\\\/]|\\.)*)\/(?:\(([mygi]+)\))?/g,
|
2020-11-07 09:47:08 +00:00
|
|
|
rexMatch = rex.exec(filterString.substring(p));
|
|
|
|
if(rexMatch) {
|
|
|
|
operator.regexp = new RegExp(rexMatch[1], rexMatch[2]);
|
|
|
|
// DEPRECATION WARNING
|
|
|
|
console.log("WARNING: Filter",operator.operator,"has a deprecated regexp operand",operator.regexp);
|
|
|
|
nextBracketPos = p + rex.lastIndex - 1;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
throw "Unterminated regular expression in filter expression";
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2014-01-14 21:08:05 +00:00
|
|
|
|
2020-11-07 09:47:08 +00:00
|
|
|
if(nextBracketPos === -1) {
|
|
|
|
throw "Missing closing bracket in filter expression";
|
|
|
|
}
|
2022-02-07 16:39:29 +00:00
|
|
|
if(operator.regexp) {
|
|
|
|
operand.text = "";
|
|
|
|
} else {
|
2020-11-07 09:47:08 +00:00
|
|
|
operand.text = filterString.substring(p,nextBracketPos);
|
|
|
|
}
|
2022-02-07 16:39:29 +00:00
|
|
|
operator.operands.push(operand);
|
2020-11-07 09:47:08 +00:00
|
|
|
p = nextBracketPos + 1;
|
2012-05-08 14:11:53 +00:00
|
|
|
}
|
2021-05-30 18:20:17 +00:00
|
|
|
|
2014-01-14 21:08:05 +00:00
|
|
|
p = nextBracketPos + 1;
|
2020-11-07 09:47:08 +00:00
|
|
|
parseOperand(bracket);
|
2021-05-30 18:20:17 +00:00
|
|
|
|
2020-11-07 09:47:08 +00:00
|
|
|
// Check for multiple operands
|
|
|
|
while(filterString.charAt(p) === ",") {
|
|
|
|
p++;
|
|
|
|
if(/^[\[\{<\/]/.test(filterString.substring(p))) {
|
|
|
|
nextBracketPos = p;
|
|
|
|
p++;
|
|
|
|
parseOperand(filterString.charAt(nextBracketPos));
|
|
|
|
} else {
|
|
|
|
throw "Missing [ in filter expression";
|
|
|
|
}
|
|
|
|
}
|
2021-05-30 18:20:17 +00:00
|
|
|
|
2012-05-08 14:11:53 +00:00
|
|
|
// Push this operator
|
|
|
|
operators.push(operator);
|
2013-02-05 19:12:05 +00:00
|
|
|
} while(filterString.charAt(p) !== "]");
|
2012-05-08 14:11:53 +00:00
|
|
|
// Skip the ending square bracket
|
2013-02-05 19:12:05 +00:00
|
|
|
if(filterString.charAt(p++) !== "]") {
|
2012-05-08 14:11:53 +00:00
|
|
|
throw "Missing ] in filter expression";
|
|
|
|
}
|
|
|
|
// Return the parsing position
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Parse a filter string
|
|
|
|
*/
|
|
|
|
exports.parseFilter = function(filterString) {
|
|
|
|
filterString = filterString || "";
|
|
|
|
var results = [], // Array of arrays of operator nodes {operator:,operand:}
|
|
|
|
p = 0, // Current position in the filter string
|
|
|
|
match;
|
|
|
|
var whitespaceRegExp = /(\s+)/mg,
|
2021-04-25 18:37:47 +00:00
|
|
|
operandRegExp = /((?:\+|\-|~|=|\:(\w+)(?:\:([\w\:, ]*))?)?)(?:(\[)|(?:"([^"]*)")|(?:'([^']*)')|([^\s\[\]]+))/mg;
|
2012-05-08 14:11:53 +00:00
|
|
|
while(p < filterString.length) {
|
|
|
|
// Skip any whitespace
|
|
|
|
whitespaceRegExp.lastIndex = p;
|
|
|
|
match = whitespaceRegExp.exec(filterString);
|
|
|
|
if(match && match.index === p) {
|
|
|
|
p = p + match[0].length;
|
|
|
|
}
|
|
|
|
// Match the start of the operation
|
|
|
|
if(p < filterString.length) {
|
|
|
|
operandRegExp.lastIndex = p;
|
|
|
|
match = operandRegExp.exec(filterString);
|
|
|
|
if(!match || match.index !== p) {
|
2016-05-17 20:58:47 +00:00
|
|
|
throw $tw.language.getString("Error/FilterSyntax");
|
2012-05-08 14:11:53 +00:00
|
|
|
}
|
|
|
|
var operation = {
|
|
|
|
prefix: "",
|
|
|
|
operators: []
|
|
|
|
};
|
|
|
|
if(match[1]) {
|
|
|
|
operation.prefix = match[1];
|
2020-10-27 12:24:18 +00:00
|
|
|
p = p + operation.prefix.length;
|
|
|
|
if(match[2]) {
|
|
|
|
operation.namedPrefix = match[2];
|
|
|
|
}
|
2021-04-25 18:37:47 +00:00
|
|
|
if(match[3]) {
|
|
|
|
operation.suffixes = [];
|
|
|
|
$tw.utils.each(match[3].split(":"),function(subsuffix) {
|
|
|
|
operation.suffixes.push([]);
|
|
|
|
$tw.utils.each(subsuffix.split(","),function(entry) {
|
|
|
|
entry = $tw.utils.trim(entry);
|
|
|
|
if(entry) {
|
|
|
|
operation.suffixes[operation.suffixes.length -1].push(entry);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2012-05-08 14:11:53 +00:00
|
|
|
}
|
2021-04-25 18:37:47 +00:00
|
|
|
if(match[4]) { // Opening square bracket
|
2012-05-08 14:11:53 +00:00
|
|
|
p = parseFilterOperation(operation.operators,filterString,p);
|
|
|
|
} else {
|
|
|
|
p = match.index + match[0].length;
|
|
|
|
}
|
2021-04-25 18:37:47 +00:00
|
|
|
if(match[5] || match[6] || match[7]) { // Double quoted string, single quoted string or unquoted title
|
2012-05-08 14:11:53 +00:00
|
|
|
operation.operators.push(
|
2021-04-25 18:37:47 +00:00
|
|
|
{operator: "title", operands: [{text: match[5] || match[6] || match[7]}]}
|
2012-05-08 14:11:53 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
results.push(operation);
|
2012-05-05 12:15:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
};
|
|
|
|
|
2013-05-25 16:26:22 +00:00
|
|
|
exports.getFilterOperators = function() {
|
|
|
|
if(!this.filterOperators) {
|
|
|
|
$tw.Wiki.prototype.filterOperators = {};
|
|
|
|
$tw.modules.applyMethods("filteroperator",this.filterOperators);
|
|
|
|
}
|
|
|
|
return this.filterOperators;
|
|
|
|
};
|
|
|
|
|
2020-10-27 12:24:18 +00:00
|
|
|
exports.getFilterRunPrefixes = function() {
|
2020-11-25 13:58:54 +00:00
|
|
|
if(!this.filterRunPrefixes) {
|
2020-10-27 12:24:18 +00:00
|
|
|
$tw.Wiki.prototype.filterRunPrefixes = {};
|
|
|
|
$tw.modules.applyMethods("filterrunprefix",this.filterRunPrefixes);
|
|
|
|
}
|
|
|
|
return this.filterRunPrefixes;
|
|
|
|
}
|
|
|
|
|
2014-04-27 19:03:33 +00:00
|
|
|
exports.filterTiddlers = function(filterString,widget,source) {
|
2013-05-25 16:26:22 +00:00
|
|
|
var fn = this.compileFilter(filterString);
|
2014-04-27 19:03:33 +00:00
|
|
|
return fn.call(this,source,widget);
|
2013-05-25 16:26:22 +00:00
|
|
|
};
|
|
|
|
|
2014-04-03 19:49:16 +00:00
|
|
|
/*
|
2014-04-27 19:03:33 +00:00
|
|
|
Compile a filter into a function with the signature fn(source,widget) where:
|
2014-04-03 19:49:16 +00:00
|
|
|
source: an iterator function for the source tiddlers, called source(iterator), where iterator is called as iterator(tiddler,title)
|
2014-04-27 19:03:33 +00:00
|
|
|
widget: an optional widget node for retrieving the current tiddler etc.
|
2014-04-03 19:49:16 +00:00
|
|
|
*/
|
2013-05-25 16:26:22 +00:00
|
|
|
exports.compileFilter = function(filterString) {
|
2022-05-23 10:26:56 +00:00
|
|
|
if(!this.filterCache) {
|
|
|
|
this.filterCache = Object.create(null);
|
|
|
|
this.filterCacheCount = 0;
|
|
|
|
}
|
|
|
|
if(this.filterCache[filterString] !== undefined) {
|
|
|
|
return this.filterCache[filterString];
|
|
|
|
}
|
2013-11-21 08:52:41 +00:00
|
|
|
var filterParseTree;
|
|
|
|
try {
|
|
|
|
filterParseTree = this.parseFilter(filterString);
|
|
|
|
} catch(e) {
|
2022-05-23 10:26:56 +00:00
|
|
|
// We do not cache this result, so it adjusts along with localization changes
|
2014-04-27 19:03:33 +00:00
|
|
|
return function(source,widget) {
|
2016-05-17 20:58:47 +00:00
|
|
|
return [$tw.language.getString("Error/Filter") + ": " + e];
|
2013-11-21 08:52:41 +00:00
|
|
|
};
|
|
|
|
}
|
2013-05-25 16:26:22 +00:00
|
|
|
// Get the hashmap of filter operator functions
|
|
|
|
var filterOperators = this.getFilterOperators();
|
|
|
|
// Assemble array of functions, one for each operation
|
|
|
|
var operationFunctions = [];
|
|
|
|
// Step through the operations
|
|
|
|
var self = this;
|
|
|
|
$tw.utils.each(filterParseTree,function(operation) {
|
|
|
|
// Create a function for the chain of operators in the operation
|
2014-04-27 19:03:33 +00:00
|
|
|
var operationSubFunction = function(source,widget) {
|
2013-05-25 16:26:22 +00:00
|
|
|
var accumulator = source,
|
2014-04-27 19:03:33 +00:00
|
|
|
results = [],
|
|
|
|
currTiddlerTitle = widget && widget.getVariable("currentTiddler");
|
2013-05-25 16:26:22 +00:00
|
|
|
$tw.utils.each(operation.operators,function(operator) {
|
2020-11-07 09:47:08 +00:00
|
|
|
var operands = [],
|
2014-04-03 19:49:16 +00:00
|
|
|
operatorFunction;
|
|
|
|
if(!operator.operator) {
|
|
|
|
operatorFunction = filterOperators.title;
|
|
|
|
} else if(!filterOperators[operator.operator]) {
|
|
|
|
operatorFunction = filterOperators.field;
|
|
|
|
} else {
|
|
|
|
operatorFunction = filterOperators[operator.operator];
|
|
|
|
}
|
2021-05-30 18:20:17 +00:00
|
|
|
|
2020-11-07 09:47:08 +00:00
|
|
|
$tw.utils.each(operator.operands,function(operand) {
|
|
|
|
if(operand.indirect) {
|
|
|
|
operand.value = self.getTextReference(operand.text,"",currTiddlerTitle);
|
|
|
|
} else if(operand.variable) {
|
2021-06-29 21:21:39 +00:00
|
|
|
var varTree = $tw.utils.parseFilterVariable(operand.text);
|
|
|
|
operand.value = widget.getVariable(varTree.name,{params:varTree.params,defaultValue: ""});
|
2020-11-07 09:47:08 +00:00
|
|
|
} else {
|
|
|
|
operand.value = operand.text;
|
|
|
|
}
|
|
|
|
operands.push(operand.value);
|
|
|
|
});
|
|
|
|
|
2015-01-14 20:09:15 +00:00
|
|
|
// Invoke the appropriate filteroperator module
|
2013-05-27 16:57:37 +00:00
|
|
|
results = operatorFunction(accumulator,{
|
|
|
|
operator: operator.operator,
|
2020-11-07 09:47:08 +00:00
|
|
|
operand: operands.length > 0 ? operands[0] : undefined,
|
|
|
|
operands: operands,
|
2014-01-10 09:32:49 +00:00
|
|
|
prefix: operator.prefix,
|
2014-01-24 19:15:27 +00:00
|
|
|
suffix: operator.suffix,
|
2018-10-30 17:39:18 +00:00
|
|
|
suffixes: operator.suffixes,
|
2014-01-10 09:32:49 +00:00
|
|
|
regexp: operator.regexp
|
2013-05-27 16:57:37 +00:00
|
|
|
},{
|
|
|
|
wiki: self,
|
2014-04-27 19:03:33 +00:00
|
|
|
widget: widget
|
2013-05-27 16:57:37 +00:00
|
|
|
});
|
2014-04-30 21:50:17 +00:00
|
|
|
if($tw.utils.isArray(results)) {
|
|
|
|
accumulator = self.makeTiddlerIterator(results);
|
|
|
|
} else {
|
|
|
|
accumulator = results;
|
|
|
|
}
|
2013-05-25 16:26:22 +00:00
|
|
|
});
|
2014-04-30 21:50:17 +00:00
|
|
|
if($tw.utils.isArray(results)) {
|
|
|
|
return results;
|
|
|
|
} else {
|
|
|
|
var resultArray = [];
|
|
|
|
results(function(tiddler,title) {
|
|
|
|
resultArray.push(title);
|
|
|
|
});
|
|
|
|
return resultArray;
|
|
|
|
}
|
2013-05-25 16:26:22 +00:00
|
|
|
};
|
2020-10-27 12:24:18 +00:00
|
|
|
var filterRunPrefixes = self.getFilterRunPrefixes();
|
2013-05-25 16:26:22 +00:00
|
|
|
// Wrap the operator functions in a wrapper function that depends on the prefix
|
|
|
|
operationFunctions.push((function() {
|
2021-04-25 18:37:47 +00:00
|
|
|
var options = {wiki: self, suffixes: operation.suffixes || []};
|
2013-05-25 16:26:22 +00:00
|
|
|
switch(operation.prefix || "") {
|
|
|
|
case "": // No prefix means that the operation is unioned into the result
|
2020-12-05 16:12:40 +00:00
|
|
|
return filterRunPrefixes["or"](operationSubFunction, options);
|
2019-06-10 16:54:20 +00:00
|
|
|
case "=": // The results of the operation are pushed into the result without deduplication
|
2020-12-05 16:12:40 +00:00
|
|
|
return filterRunPrefixes["all"](operationSubFunction, options);
|
2013-05-25 16:26:22 +00:00
|
|
|
case "-": // The results of this operation are removed from the main result
|
2020-12-05 16:12:40 +00:00
|
|
|
return filterRunPrefixes["except"](operationSubFunction, options);
|
2013-05-25 16:26:22 +00:00
|
|
|
case "+": // This operation is applied to the main results so far
|
2020-12-05 16:12:40 +00:00
|
|
|
return filterRunPrefixes["and"](operationSubFunction, options);
|
2018-11-20 13:29:44 +00:00
|
|
|
case "~": // This operation is unioned into the result only if the main result so far is empty
|
2020-12-05 16:12:40 +00:00
|
|
|
return filterRunPrefixes["else"](operationSubFunction, options);
|
2020-10-27 12:24:18 +00:00
|
|
|
default:
|
|
|
|
if(operation.namedPrefix && filterRunPrefixes[operation.namedPrefix]) {
|
2020-12-05 16:12:40 +00:00
|
|
|
return filterRunPrefixes[operation.namedPrefix](operationSubFunction, options);
|
2020-10-27 12:24:18 +00:00
|
|
|
} else {
|
|
|
|
return function(results,source,widget) {
|
2020-12-10 18:25:53 +00:00
|
|
|
results.clear();
|
2020-10-27 12:24:18 +00:00
|
|
|
results.push($tw.language.getString("Error/FilterRunPrefix"));
|
|
|
|
};
|
|
|
|
}
|
2013-05-25 16:26:22 +00:00
|
|
|
}
|
|
|
|
})());
|
|
|
|
});
|
2014-04-03 19:49:16 +00:00
|
|
|
// Return a function that applies the operations to a source iterator of tiddler titles
|
2022-05-23 10:26:56 +00:00
|
|
|
var compiled = $tw.perf.measure("filter: " + filterString,function filterFunction(source,widget) {
|
2014-04-03 19:49:16 +00:00
|
|
|
if(!source) {
|
|
|
|
source = self.each;
|
|
|
|
} else if(typeof source === "object") { // Array or hashmap
|
|
|
|
source = self.makeTiddlerIterator(source);
|
|
|
|
}
|
2021-05-22 19:00:24 +00:00
|
|
|
if(!widget) {
|
|
|
|
widget = $tw.rootWidget;
|
|
|
|
}
|
2020-12-06 08:54:57 +00:00
|
|
|
var results = new $tw.utils.LinkedList();
|
2013-05-25 16:26:22 +00:00
|
|
|
$tw.utils.each(operationFunctions,function(operationFunction) {
|
2014-04-27 19:03:33 +00:00
|
|
|
operationFunction(results,source,widget);
|
2013-05-25 16:26:22 +00:00
|
|
|
});
|
2020-12-06 08:54:57 +00:00
|
|
|
return results.toArray();
|
2014-04-01 07:33:36 +00:00
|
|
|
});
|
2022-05-23 10:26:56 +00:00
|
|
|
if(this.filterCacheCount >= 2000) {
|
|
|
|
// To prevent memory leak, we maintain an upper limit for cache size.
|
|
|
|
// Reset if exceeded. This should give us 95% of the benefit
|
|
|
|
// that no cache limit would give us.
|
|
|
|
this.filterCache = Object.create(null);
|
|
|
|
this.filterCacheCount = 0;
|
|
|
|
}
|
|
|
|
this.filterCache[filterString] = compiled;
|
|
|
|
this.filterCacheCount++;
|
|
|
|
return compiled;
|
2013-05-25 16:26:22 +00:00
|
|
|
};
|
|
|
|
|
2012-05-05 12:15:44 +00:00
|
|
|
})();
|