mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-12-24 17:10:29 +00:00
Enhance search operator (#3502)
* Enhance search operator * Add support for searching all fields and also searching all fields except nominated fields. * Docs tweaks Thanks @pmario * Error message improvements * Improve error message formatting
This commit is contained in:
parent
d6a0b06f02
commit
6dcdc2049a
@ -40,12 +40,23 @@ function parseFilterOperation(operators,filterString,p) {
|
||||
nextBracketPos += p;
|
||||
var bracket = filterString.charAt(nextBracketPos);
|
||||
operator.operator = filterString.substring(p,nextBracketPos);
|
||||
|
||||
// Any suffix?
|
||||
var colon = operator.operator.indexOf(':');
|
||||
if(colon > -1) {
|
||||
// The raw suffix for older filters
|
||||
operator.suffix = operator.operator.substring(colon + 1);
|
||||
operator.operator = operator.operator.substring(0,colon) || "field";
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// Empty operator means: title
|
||||
else if(operator.operator === "") {
|
||||
@ -208,6 +219,7 @@ exports.compileFilter = function(filterString) {
|
||||
operand: operand,
|
||||
prefix: operator.prefix,
|
||||
suffix: operator.suffix,
|
||||
suffixes: operator.suffixes,
|
||||
regexp: operator.regexp
|
||||
},{
|
||||
wiki: self,
|
||||
|
@ -17,11 +17,32 @@ Export our filter function
|
||||
*/
|
||||
exports.search = function(source,operator,options) {
|
||||
var invert = operator.prefix === "!";
|
||||
if(operator.suffix) {
|
||||
if(operator.suffixes) {
|
||||
var hasFlag = function(flag) {
|
||||
return (operator.suffixes[1] || []).indexOf(flag) !== -1;
|
||||
},
|
||||
excludeFields = false,
|
||||
firstChar = operator.suffixes[0][0].charAt(0),
|
||||
fields;
|
||||
if(operator.suffixes[0][0].charAt(0) === "-") {
|
||||
fields = [operator.suffixes[0][0].slice(1)].concat(operator.suffixes[0].slice(1));
|
||||
excludeFields = true;
|
||||
} else if(operator.suffixes[0][0] === "*"){
|
||||
fields = [];
|
||||
excludeFields = true;
|
||||
} else {
|
||||
fields = operator.suffixes[0].slice(0);
|
||||
}
|
||||
return options.wiki.search(operator.operand,{
|
||||
source: source,
|
||||
invert: invert,
|
||||
field: operator.suffix
|
||||
field: fields,
|
||||
excludeField: excludeFields,
|
||||
caseSensitive: hasFlag("casesensitive"),
|
||||
literal: hasFlag("literal"),
|
||||
whitespace: hasFlag("whitespace"),
|
||||
regexp: hasFlag("regexp"),
|
||||
words: hasFlag("words")
|
||||
});
|
||||
} else {
|
||||
return options.wiki.search(operator.operand,{
|
||||
|
@ -1047,8 +1047,13 @@ Options available:
|
||||
exclude: An array of tiddler titles to exclude from the search
|
||||
invert: If true returns tiddlers that do not contain the specified string
|
||||
caseSensitive: If true forces a case sensitive search
|
||||
literal: If true, searches for literal string, rather than separate search terms
|
||||
field: If specified, restricts the search to the specified field
|
||||
field: If specified, restricts the search to the specified field, or an array of field names
|
||||
excludeField: If true, the field options are inverted to specify the fields that are not to be searched
|
||||
The search mode is determined by the first of these boolean flags to be true
|
||||
literal: searches for literal string
|
||||
whitespace: same as literal except runs of whitespace are treated as a single space
|
||||
regexp: treats the search term as a regular expression
|
||||
words: (default) treats search string as a list of tokens, and matches if all tokens are found, regardless of adjacency or ordering
|
||||
*/
|
||||
exports.search = function(text,options) {
|
||||
options = options || {};
|
||||
@ -1064,6 +1069,21 @@ exports.search = function(text,options) {
|
||||
} else {
|
||||
searchTermsRegExps = [new RegExp("(" + $tw.utils.escapeRegExp(text) + ")",flags)];
|
||||
}
|
||||
} else if(options.whitespace) {
|
||||
terms = [];
|
||||
$tw.utils.each(text.split(/\s+/g),function(term) {
|
||||
if(term) {
|
||||
terms.push($tw.utils.escapeRegExp(term));
|
||||
}
|
||||
});
|
||||
searchTermsRegExps = [new RegExp("(" + terms.join("\\s+") + ")",flags)];
|
||||
} else if(options.regexp) {
|
||||
try {
|
||||
searchTermsRegExps = [new RegExp("(" + text + ")",flags)];
|
||||
} catch(e) {
|
||||
searchTermsRegExps = null;
|
||||
console.log("Regexp error parsing /(" + text + ")/" + flags + ": ",e);
|
||||
}
|
||||
} else {
|
||||
terms = text.split(/ +/);
|
||||
if(terms.length === 1 && terms[0] === "") {
|
||||
@ -1075,6 +1095,23 @@ exports.search = function(text,options) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Accumulate the array of fields to be searched or excluded from the search
|
||||
var fields = [];
|
||||
if(options.field) {
|
||||
if($tw.utils.isArray(options.field)) {
|
||||
$tw.utils.each(options.field,function(fieldName) {
|
||||
fields.push(fieldName);
|
||||
});
|
||||
} else {
|
||||
fields.push(options.field);
|
||||
}
|
||||
}
|
||||
// Use default fields if none specified and we're not excluding fields (excluding fields with an empty field array is the same as searching all fields)
|
||||
if(fields.length === 0 && !options.excludeField) {
|
||||
fields.push("title");
|
||||
fields.push("tags");
|
||||
fields.push("text");
|
||||
}
|
||||
// Function to check a given tiddler for the search term
|
||||
var searchTiddler = function(title) {
|
||||
if(!searchTermsRegExps) {
|
||||
@ -1085,24 +1122,63 @@ exports.search = function(text,options) {
|
||||
tiddler = new $tw.Tiddler({title: title, text: "", type: "text/vnd.tiddlywiki"});
|
||||
}
|
||||
var contentTypeInfo = $tw.config.contentTypeInfo[tiddler.fields.type] || $tw.config.contentTypeInfo["text/vnd.tiddlywiki"],
|
||||
match;
|
||||
for(var t=0; t<searchTermsRegExps.length; t++) {
|
||||
match = false;
|
||||
if(options.field) {
|
||||
match = searchTermsRegExps[t].test(tiddler.getFieldString(options.field));
|
||||
searchFields;
|
||||
// Get the list of fields we're searching
|
||||
if(options.excludeField) {
|
||||
searchFields = Object.keys(tiddler.fields);
|
||||
$tw.utils.each(fields,function(fieldName) {
|
||||
var p = searchFields.indexOf(fieldName);
|
||||
if(p !== -1) {
|
||||
searchFields.splice(p,1);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Search title, tags and body
|
||||
if(contentTypeInfo.encoding === "utf8") {
|
||||
match = match || searchTermsRegExps[t].test(tiddler.fields.text);
|
||||
searchFields = fields;
|
||||
}
|
||||
var tags = tiddler.fields.tags ? tiddler.fields.tags.join("\0") : "";
|
||||
match = match || searchTermsRegExps[t].test(tags) || searchTermsRegExps[t].test(tiddler.fields.title);
|
||||
for(var fieldIndex=0; fieldIndex<searchFields.length; fieldIndex++) {
|
||||
// Don't search the text field if the content type is binary
|
||||
var fieldName = searchFields[fieldIndex];
|
||||
if(fieldName === "text" && contentTypeInfo.encoding !== "utf8") {
|
||||
break;
|
||||
}
|
||||
if(!match) {
|
||||
return false;
|
||||
var matches = true,
|
||||
str = tiddler.fields[fieldName],
|
||||
t;
|
||||
if(str) {
|
||||
if($tw.utils.isArray(str)) {
|
||||
// If the field value is an array, test each regexp against each field array entry and fail if each regexp doesn't match at least one field array entry
|
||||
for(t=0; t<searchTermsRegExps.length; t++) {
|
||||
var thisRegExpMatches = false
|
||||
for(var s=0; s<str.length; s++) {
|
||||
if(searchTermsRegExps[t].test(str[s])) {
|
||||
thisRegExpMatches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Bail if the current search expression doesn't match any entry in the current field array
|
||||
if(!thisRegExpMatches) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the field isn't an array, force it to a string and test each regexp against it and fail if any do not match
|
||||
str = tiddler.getFieldString(fieldName);
|
||||
for(t=0; t<searchTermsRegExps.length; t++) {
|
||||
if(!searchTermsRegExps[t].test(str)) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
matches = false;
|
||||
}
|
||||
if(matches) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
return false;
|
||||
};
|
||||
// Loop through all the tiddlers doing the search
|
||||
var results = [],
|
||||
|
@ -14,6 +14,26 @@ Tests the filtering mechanism.
|
||||
|
||||
describe("Filter tests", function() {
|
||||
|
||||
// Test filter parsing
|
||||
it("should parse new-style rich operator suffixes", function() {
|
||||
expect($tw.wiki.parseFilter("[search:: four, , five,, six [operand]]")).toEqual(
|
||||
[ { prefix : '', operators : [ { operator : 'search', suffix : ': four, , five,, six ', suffixes : [ [ ], [ 'four', 'five', 'six' ] ], operand : 'operand' } ] } ]
|
||||
);
|
||||
expect($tw.wiki.parseFilter("[search: one, two ,three :[operand]]")).toEqual(
|
||||
[ { prefix : '', operators : [ { operator : 'search', suffix : ' one, two ,three :', suffixes : [ [ 'one', 'two', 'three' ], [ ] ], operand : 'operand' } ] } ]
|
||||
);
|
||||
expect($tw.wiki.parseFilter("[search: one, two ,three :[operand]]")).toEqual(
|
||||
[ { prefix : '', operators : [ { operator : 'search', suffix : ' one, two ,three :', suffixes : [ [ 'one', 'two', 'three' ], [ ] ], operand : 'operand' } ] } ]
|
||||
);
|
||||
expect($tw.wiki.parseFilter("[search: one, two ,three : four, , five,, six [operand]]")).toEqual(
|
||||
[ { prefix : '', operators : [ { operator : 'search', suffix : ' one, two ,three : four, , five,, six ', suffixes : [ [ 'one', 'two', 'three' ], [ 'four', 'five', 'six' ] ], operand : 'operand' } ] } ]
|
||||
);
|
||||
expect($tw.wiki.parseFilter("[search: , : [operand]]")).toEqual(
|
||||
[ { prefix : '', operators : [ { operator : 'search', suffix : ' , : ', suffixes : [ [ ], [ ] ], operand : 'operand' } ] } ]
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// Create a wiki
|
||||
var wiki = new $tw.Wiki({
|
||||
shadowTiddlers: {
|
||||
@ -228,6 +248,13 @@ describe("Filter tests", function() {
|
||||
it("should handle the search operator", function() {
|
||||
expect(wiki.filterTiddlers("[search[the]sort[title]]").join(",")).toBe("$:/TiddlerTwo,a fourth tiddler,one,Tiddler Three,TiddlerOne");
|
||||
expect(wiki.filterTiddlers("[search{Tiddler8}sort[title]]").join(",")).toBe("$:/TiddlerTwo,a fourth tiddler,one,Tiddler Three,TiddlerOne");
|
||||
expect(wiki.filterTiddlers("[search:modifier[og]sort[title]]").join(",")).toBe("TiddlerOne");
|
||||
expect(wiki.filterTiddlers("[search:modifier,authors:casesensitive[Do]sort[title]]").join(",")).toBe("$:/TiddlerTwo,a fourth tiddler,one,Tiddler Three");
|
||||
expect(wiki.filterTiddlers("[search:modifier,authors:casesensitive[do]sort[title]]").join(",")).toBe("");
|
||||
expect(wiki.filterTiddlers("[search:authors:casesensitive,whitespace[John Doe]sort[title]]").join(",")).toBe("$:/TiddlerTwo");
|
||||
expect(wiki.filterTiddlers("[search:modifier:regexp[(d|bl)o(ggs|e)]sort[title]]").join(",")).toBe("$:/TiddlerTwo,a fourth tiddler,one,Tiddler Three,TiddlerOne");
|
||||
expect(wiki.filterTiddlers("[search:-modifier,authors:[g]sort[title]]").join(",")).toBe("Tiddler Three");
|
||||
expect(wiki.filterTiddlers("[search:*:[g]sort[title]]").join(",")).toBe("Tiddler Three,TiddlerOne");
|
||||
});
|
||||
|
||||
it("should handle the each operator", function() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
created: 20150124104508000
|
||||
modified: 20150124110256000
|
||||
modified: 20181025082022690
|
||||
tags: [[search Operator]] [[Operator Examples]]
|
||||
title: search Operator (Examples)
|
||||
type: text/vnd.tiddlywiki
|
||||
@ -7,4 +7,9 @@ type: text/vnd.tiddlywiki
|
||||
<$macrocall $name=".operator-example" n="1" eg="[!is[system]search[table]]" ie="non-system tiddlers containing the word <<.word table>>"/>
|
||||
<$macrocall $name=".operator-example" n="2" eg="[all[shadows]search[table]]" ie="shadow tiddlers containing the word <<.word table>>"/>
|
||||
<$macrocall $name=".operator-example" n="3" eg="[search:caption[arch]]" ie="tiddlers containing `arch` in their <<.field caption>> field"/>
|
||||
|
||||
<$macrocall $name=".operator-example" n="4" eg="[search:*[arch]]" ie="tiddlers containing `arch` in any field"/>
|
||||
<$macrocall $name=".operator-example" n="5" eg="[search:-title,caption[arch]]" ie="tiddlers containing `arch` in any field except <<.field title>> and <<.field caption>>"/>
|
||||
<$macrocall $name=".operator-example" n="6" eg="[!is[system]search[the first]]" ie="non-system tiddlers containing a case-insensitive match for both the <<.word 'the'>> and <<.word 'first'>>"/>
|
||||
<$macrocall $name=".operator-example" n="7" eg="[!is[system]search::literal[the first]]" ie="non-system tiddlers containing a case-insensitive match for the literal phrase <<.word 'the first'>>"/>
|
||||
<$macrocall $name=".operator-example" n="8" eg="[!is[system]search::literal,casesensitive[The first]]" ie="non-system tiddlers containing a case-sensitive match for the literal phrase <<.word 'The first'>>"/>
|
||||
<$macrocall $name=".operator-example" n="9" eg="[search:caption,description:casesensitive,words[arch]]" ie="any tiddlers containing a case-sensitive match for the word `arch` in their <<.field caption>> or <<.field description>> fields"/>
|
||||
|
@ -1,21 +1,44 @@
|
||||
created: 20140410103123179
|
||||
modified: 20150203191048000
|
||||
modified: 20181025082022690
|
||||
tags: [[Filter Operators]] [[Common Operators]] [[Field Operators]] [[Negatable Operators]]
|
||||
title: search Operator
|
||||
type: text/vnd.tiddlywiki
|
||||
caption: search
|
||||
op-purpose: filter the input by searching tiddler content
|
||||
op-input: a [[selection of titles|Title Selection]]
|
||||
op-suffix: optionally, the name of a [[field|TiddlerFields]]
|
||||
op-suffix-name: F
|
||||
op-parameter: one or more search terms, separated by spaces
|
||||
op-suffix: the <<.op search>> operator uses a rich suffix, see below for details
|
||||
op-parameter: one or more search terms, separated by spaces, or a literal search string
|
||||
op-output: those input tiddlers in which <<.em all>> of the search terms can be found in the value of field <<.place F>>
|
||||
op-neg-output: those input tiddlers in which <<.em not>> all of the search terms can be found
|
||||
|
||||
When used with a suffix, the <<.op search>> operator is similar to <<.olink regexp>> but less powerful.
|
||||
<<.from-version "5.1.18">> The search filter operator was significantly enhanced in 5.1.18. Earlier versions do not support the extended syntax and therefore do not permit searching multiple fields, or the ''literal'' or ''casesensitive'' options.
|
||||
|
||||
If the suffix is omitted, a tiddler is deemed to match if all the search terms appear in the combination of its <<.field tags>>, <<.field text>> and <<.field title>> fields.
|
||||
The <<.op search>> operator uses an extended syntax that permits multiple fields and flags to be passed:
|
||||
|
||||
The search ignores the difference between capital and lowercase letters.
|
||||
```
|
||||
[search:<field list>:<flag list>[<operand>]]
|
||||
```
|
||||
|
||||
* ''field list'': a comma delimited list of field names to restrict the search
|
||||
** defaults to <<.field tags>>, <<.field text>> and <<.field title>> if blank
|
||||
** an asterisk `*` instead of the field list causes the search to be performed across all fields available on each tiddler
|
||||
** preceding the list with a minus sign `-` reverses the order so that the search is performed on all fields except the listed fields
|
||||
* ''flag list'': a comma delimited list of flags (defaults to `words` if blank)
|
||||
* ''operand'': filter operand
|
||||
|
||||
This example searches the fields <<.field title>> and <<.field caption>> for a case-sensitive match for the literal string <<.op-word "The first">>:
|
||||
|
||||
```
|
||||
[search:title,caption:literal,casesensitive[The first]]
|
||||
```
|
||||
|
||||
The available flags are:
|
||||
|
||||
* Search mode - the first to be set of the following flags determines the type of search that is performed:
|
||||
** ''literal'': considers the search string to be a literal string, and requires an exact match
|
||||
** ''whitespace'': considers the search string to be a literal string, but will consider all runs of whitespace to be equivalent to a single space. Thus `A B` matches `A B`
|
||||
** ''regexp'': treats the search string as a regular expression. Note that the ''regexp'' option obviates the need for the older <<.olink regexp>> operator.
|
||||
** ''words'': (the default) treats the search string as a list of tokens separated by whitespace, and matches if all of the tokens appear in the string (regardless of ordering and whether there is other text in between)
|
||||
* ''casesensitive'': if present, this flag forces a case-sensitive match, where upper and lower case letters are considered different. By default, upper and lower case letters are considered identical for matching purposes.
|
||||
|
||||
<<.operator-examples "search">>
|
||||
|
Loading…
Reference in New Issue
Block a user