diff --git a/core/modules/filters.js b/core/modules/filters.js index 592cb7b97..831ae5780 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -32,32 +32,57 @@ function parseFilterOperation(operators,filterString,p) { if(filterString.charAt(p) === "!") { operator.prefix = filterString.charAt(p++); } + // Get the operator name - bracketPos = filterString.indexOf("[",p); - curlyBracketPos = filterString.indexOf("{",p); - if((bracketPos === -1) && (curlyBracketPos === -1)) { + var nextBracketPos = filterString.substring(p).search(/[\[\{\/]/); + if(nextBracketPos === -1) { throw "Missing [ in filter expression"; } - if(bracketPos === -1 || (curlyBracketPos !== -1 && curlyBracketPos < bracketPos)) { - // Curly brackets - operator.indirect = true; - operator.operator = filterString.substring(p,curlyBracketPos); - p = curlyBracketPos + 1; - } else { - // Square brackets - operator.operator = filterString.substring(p,bracketPos); - p = bracketPos + 1; + nextBracketPos += p; + var bracket = filterString.charAt(nextBracketPos); + operator.operator = filterString.substring(p,nextBracketPos); + + // Any suffix? + var colon = operator.operator.indexOf(':'); + if(colon > -1) { + operator.field = operator.operator.substring(colon+1); + operator.operator = operator.operator.substring(0,colon) || "field"; } - if(operator.operator === "") { + // Empty operator means: title + else if(operator.operator === "") { operator.operator = "title"; } - // Get the operand - bracketPos = filterString.indexOf(operator.indirect ? "}" : "]",p); - if(bracketPos === -1) { + + 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"; } - operator.operand = filterString.substring(p,bracketPos); - p = bracketPos + 1; + if(!operator.regexp) { + operator.operand = filterString.substring(p,nextBracketPos); + } + p = nextBracketPos + 1; + // Push this operator operators.push(operator); } while(filterString.charAt(p) !== "]"); @@ -161,7 +186,9 @@ exports.compileFilter = function(filterString) { results = operatorFunction(accumulator,{ operator: operator.operator, operand: operand, - prefix: operator.prefix + prefix: operator.prefix, + field: operator.field, + regexp: operator.regexp },{ wiki: self, currTiddlerTitle: currTiddlerTitle diff --git a/core/modules/filters/field.js b/core/modules/filters/field.js index 8fff9114b..b3b2d2787 100644 --- a/core/modules/filters/field.js +++ b/core/modules/filters/field.js @@ -21,7 +21,14 @@ exports.field = function(source,operator,options) { function checkTiddler(title) { var tiddler = options.wiki.getTiddler(title); if(tiddler) { - var match = tiddler.getFieldString(operator.operator) === operator.operand; + var match, + text = tiddler.getFieldString(operator.field); + if(operator.regexp) { + match = !! operator.regexp.exec(text); + } + else { + match = text === operator.operand; + } if(operator.prefix === "!") { match = !match; } @@ -31,6 +38,10 @@ exports.field = function(source,operator,options) { } } // Iterate through the source tiddlers + if(!operator.field) { + operator.field = operator.operator; + } + operator.field.toLowerCase(); if($tw.utils.isArray(source)) { $tw.utils.each(source,function(title) { checkTiddler(title); diff --git a/editions/tw5.com/tiddlers/concepts/TiddlerFilters.tid b/editions/tw5.com/tiddlers/concepts/TiddlerFilters.tid index 1df0da137..6813c5c08 100644 --- a/editions/tw5.com/tiddlers/concepts/TiddlerFilters.tid +++ b/editions/tw5.com/tiddlers/concepts/TiddlerFilters.tid @@ -44,7 +44,8 @@ A filter string consists of one or more runs of filter operators that each look * ''prefix'': tests whether a tiddlers title starts with the prefix specified in the operand * ''limit'': limits the number of subresults to the integer specified in the operand * ''tag'': tests whether a given tag is (`[tag[mytag]]`) or is not (`[!tag[mytag]]`) present on the tiddler -* ''{field}'': tests whether a tiddler field has a specified value (`[modifier[Jeremy]]`) or not (`[!modifier[Jeremy]]`) +* ''field:{field}'': or +* ''{field}'': tests whether a tiddler field has a specified value (`[modifier[Jeremy]]` or `[field:modifier[Jeremy]]`) or not (`[!modifier[Jeremy]]`) * ''tags'': selects the tags on the currently selected tiddlers * ''tagging'': selects the tiddlers tagged with the currently selected tiddlers * ''untagged'': selects the any of the selected tiddlers that do not have at least one tag @@ -79,6 +80,16 @@ If a filter operator is written with curly brackets around the operand then it i ''[search{$:/temp/search}]'': selects all tiddlers containing the string contained in the tiddler titled ''$:/temp/search''. +! Regular Expression Filters + +The field-filter also accepts regular expressions in the form `/regexp/(modifier)`. Please refer to you favourite JavaScript documentation to learn more about regular expressions and modifiers. + +In the easiest form, regular expressions allow you do do a search on substrings for every field: + +* `field:title/example/`: searches for all tiddlers having "example" in its title. +* `field:title:/example$/`: `$` is an "anchor" for the end of the text. So "example" has to be the end of the title. +* `field:text/jeremy|ruston/(i)`: Searches for tiddlers containing Jeremy's first or last name, ignoring the case. + ! Runs Operators are combined into runs that function as logically ANDed expressions by bashing them together and merging the square brackets: