mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-07-07 12:34:22 +00:00
![Jermolene](/assets/img/avatar_default.png)
This is an experimental module to help us measure the performance of the refresh cycle and the filter mechanism. Not intended to replace the performance measurement features in browser developer tools, just to make it easier to automate performance checks cross-browser. The immediate purpose is to help in refactoring the filter mechanism. The recent change to encapsulate the wiki store “tiddlers” object has hurt the performance of filters, and it’s going to be helpful to have decent measurements while refactoring that code. I’m still not convinced that this stuff should be in the core, and may well end up removing it after the present refactoring cycle.
232 lines
7.1 KiB
JavaScript
232 lines
7.1 KiB
JavaScript
/*\
|
|
title: $:/core/modules/filters.js
|
|
type: application/javascript
|
|
module-type: wikimethod
|
|
|
|
Adds tiddler filtering to the $tw.Wiki object.
|
|
|
|
\*/
|
|
(function(){
|
|
|
|
/*jslint node: true, browser: true */
|
|
/*global $tw: false */
|
|
"use strict";
|
|
|
|
/*
|
|
Parses an operation within a filter string
|
|
results: Array of array of operator nodes into which results should be inserted
|
|
filterString: filter string
|
|
p: start position within the string
|
|
Returns the new start position, after the parsed operation
|
|
*/
|
|
function parseFilterOperation(operators,filterString,p) {
|
|
var operator, operand, bracketPos, curlyBracketPos;
|
|
// Skip the starting square bracket
|
|
if(filterString.charAt(p++) !== "[") {
|
|
throw "Missing [ in filter expression";
|
|
}
|
|
// Process each operator in turn
|
|
do {
|
|
operator = {};
|
|
// Check for an operator prefix
|
|
if(filterString.charAt(p) === "!") {
|
|
operator.prefix = filterString.charAt(p++);
|
|
}
|
|
// Get the operator name
|
|
var nextBracketPos = filterString.substring(p).search(/[\[\{\/]/);
|
|
if(nextBracketPos === -1) {
|
|
throw "Missing [ in filter expression";
|
|
}
|
|
nextBracketPos += p;
|
|
var bracket = filterString.charAt(nextBracketPos);
|
|
operator.operator = filterString.substring(p,nextBracketPos);
|
|
|
|
// Any suffix?
|
|
var colon = operator.operator.indexOf(':');
|
|
if(colon > -1) {
|
|
operator.suffix = operator.operator.substring(colon + 1);
|
|
operator.operator = operator.operator.substring(0,colon) || "field";
|
|
}
|
|
// Empty operator means: title
|
|
else if(operator.operator === "") {
|
|
operator.operator = "title";
|
|
}
|
|
|
|
p = nextBracketPos + 1;
|
|
switch (bracket) {
|
|
case '{': // Curly brackets
|
|
operator.indirect = true;
|
|
nextBracketPos = filterString.indexOf('}',p);
|
|
break;
|
|
case '[': // Square brackets
|
|
nextBracketPos = filterString.indexOf(']',p);
|
|
break;
|
|
case '/': // regexp brackets
|
|
var rex = /^((?:[^\\\/]*|\\.)*)\/(?:\(([mygi]+)\))?/g,
|
|
rexMatch = rex.exec(filterString.substring(p));
|
|
if(rexMatch) {
|
|
operator.regexp = new RegExp(rexMatch[1], rexMatch[2]);
|
|
nextBracketPos = p + rex.lastIndex - 1;
|
|
}
|
|
else {
|
|
throw "Unterminated regular expression in filter expression";
|
|
}
|
|
break;
|
|
}
|
|
|
|
if(nextBracketPos === -1) {
|
|
throw "Missing closing bracket in filter expression";
|
|
}
|
|
if(!operator.regexp) {
|
|
operator.operand = filterString.substring(p,nextBracketPos);
|
|
}
|
|
p = nextBracketPos + 1;
|
|
|
|
// Push this operator
|
|
operators.push(operator);
|
|
} while(filterString.charAt(p) !== "]");
|
|
// Skip the ending square bracket
|
|
if(filterString.charAt(p++) !== "]") {
|
|
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,
|
|
operandRegExp = /((?:\+|\-)?)(?:(\[)|("(?:[^"])*")|('(?:[^'])*')|([^\s\[\]]+))/mg;
|
|
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) {
|
|
throw "Syntax error in filter expression";
|
|
}
|
|
var operation = {
|
|
prefix: "",
|
|
operators: []
|
|
};
|
|
if(match[1]) {
|
|
operation.prefix = match[1];
|
|
p++;
|
|
}
|
|
if(match[2]) { // Opening square bracket
|
|
p = parseFilterOperation(operation.operators,filterString,p);
|
|
} else {
|
|
p = match.index + match[0].length;
|
|
}
|
|
if(match[3] || match[4] || match[5]) { // Double quoted string, single quoted string or unquoted title
|
|
operation.operators.push(
|
|
{operator: "title", operand: match[3] || match[4] || match[5]}
|
|
);
|
|
}
|
|
results.push(operation);
|
|
}
|
|
}
|
|
return results;
|
|
};
|
|
|
|
exports.getFilterOperators = function() {
|
|
if(!this.filterOperators) {
|
|
$tw.Wiki.prototype.filterOperators = {};
|
|
$tw.modules.applyMethods("filteroperator",this.filterOperators);
|
|
}
|
|
return this.filterOperators;
|
|
};
|
|
|
|
exports.filterTiddlers = function(filterString,currTiddlerTitle,tiddlerList) {
|
|
var fn = this.compileFilter(filterString);
|
|
return fn.call(this,tiddlerList,currTiddlerTitle);
|
|
};
|
|
|
|
exports.compileFilter = function(filterString) {
|
|
var filterParseTree;
|
|
try {
|
|
filterParseTree = this.parseFilter(filterString);
|
|
} catch(e) {
|
|
return function(source,currTiddlerTitle) {
|
|
return ["Filter error: " + e];
|
|
};
|
|
}
|
|
// 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
|
|
var operationSubFunction = function(source,currTiddlerTitle) {
|
|
var accumulator = source,
|
|
results = [];
|
|
$tw.utils.each(operation.operators,function(operator) {
|
|
var operatorFunction = filterOperators[operator.operator] || filterOperators.field || function(source,operator,operations) {
|
|
return ["Filter Error: unknown operator '" + operator.operator + "'"];
|
|
},
|
|
operand = operator.operand;
|
|
if(operator.indirect) {
|
|
operand = self.getTextReference(operator.operand,"",currTiddlerTitle);
|
|
}
|
|
results = operatorFunction(accumulator,{
|
|
operator: operator.operator,
|
|
operand: operand,
|
|
prefix: operator.prefix,
|
|
suffix: operator.suffix,
|
|
regexp: operator.regexp
|
|
},{
|
|
wiki: self,
|
|
currTiddlerTitle: currTiddlerTitle
|
|
});
|
|
accumulator = results;
|
|
});
|
|
return results;
|
|
};
|
|
// Wrap the operator functions in a wrapper function that depends on the prefix
|
|
operationFunctions.push((function() {
|
|
switch(operation.prefix || "") {
|
|
case "": // No prefix means that the operation is unioned into the result
|
|
return function(results,source,currTiddlerTitle) {
|
|
$tw.utils.pushTop(results,operationSubFunction(source,currTiddlerTitle));
|
|
};
|
|
case "-": // The results of this operation are removed from the main result
|
|
return function(results,source,currTiddlerTitle) {
|
|
$tw.utils.removeArrayEntries(results,operationSubFunction(source,currTiddlerTitle));
|
|
};
|
|
case "+": // This operation is applied to the main results so far
|
|
return function(results,source,currTiddlerTitle) {
|
|
// This replaces all the elements of the array, but keeps the actual array so that references to it are preserved
|
|
source = results.slice(0);
|
|
results.splice(0,results.length);
|
|
$tw.utils.pushTop(results,operationSubFunction(source,currTiddlerTitle));
|
|
};
|
|
}
|
|
})());
|
|
});
|
|
// Return a function that applies the operations to a source array/hashmap of tiddler titles
|
|
return $tw.perf.measure("filter",function filterFunction(source,currTiddlerTitle) {
|
|
source = source || self.getAllTitles();
|
|
var results = [];
|
|
$tw.utils.each(operationFunctions,function(operationFunction) {
|
|
operationFunction(results,source,currTiddlerTitle);
|
|
});
|
|
return results;
|
|
});
|
|
};
|
|
|
|
})();
|