2012-05-05 12:15:44 +00:00
/ * \
2012-06-06 11:17:08 +00:00
title : $ : / c o r e / m o d u l e s / f i l t e r s . j s
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" ;
2022-10-01 09:13:40 +00:00
/* Maximum permitted filter recursion depth */
var MAX _FILTER _DEPTH = 300 ;
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 ) {
2022-05-06 14:01:17 +00:00
// Use the "title" operator if no operator is specified
2014-04-03 19:49:16 +00:00
operatorFunction = filterOperators . title ;
} else if ( ! filterOperators [ operator . operator ] ) {
2022-05-11 13:52:25 +00:00
// Unknown operators treated as "[unknown]" - at run time we can distinguish between a custom operator and falling back to the default "field" operator
operatorFunction = filterOperators [ "[unknown]" ] ;
2014-04-03 19:49:16 +00:00
} else {
2022-05-06 14:01:17 +00:00
// Use the operator function
2014-04-03 19:49:16 +00:00
operatorFunction = filterOperators [ operator . operator ] ;
}
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 ) ;
2023-01-21 22:07:34 +00:00
operand . value = widget . evaluateVariable ( varTree . name , { params : varTree . params , source : source } ) [ 0 ] || "" ;
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-07-21 08:17:57 +00:00
var fnMeasured = $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 ( ) ;
2022-07-21 08:17:57 +00:00
self . filterRecursionCount = ( self . filterRecursionCount || 0 ) + 1 ;
2022-10-01 09:13:40 +00:00
if ( self . filterRecursionCount < MAX _FILTER _DEPTH ) {
2022-09-04 15:23:49 +00:00
$tw . utils . each ( operationFunctions , function ( operationFunction ) {
operationFunction ( results , source , widget ) ;
} ) ;
2022-07-21 08:17:57 +00:00
} else {
2022-09-04 15:26:47 +00:00
results . push ( "/**-- Excessive filter recursion --**/" ) ;
2022-07-21 08:17:57 +00:00
}
self . filterRecursionCount = self . filterRecursionCount - 1 ;
2022-09-04 15:23:49 +00:00
return results . toArray ( ) ;
} ) ;
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 ;
}
2022-09-04 15:23:49 +00:00
this . filterCache [ filterString ] = fnMeasured ;
2022-05-23 10:26:56 +00:00
this . filterCacheCount ++ ;
2022-09-04 15:23:49 +00:00
return fnMeasured ;
2013-05-25 16:26:22 +00:00
} ;
2012-05-05 12:15:44 +00:00
} ) ( ) ;