From d4043fc1f4d75a4b623f77811bef0788526141bc Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 1 Apr 2025 12:06:10 +0100 Subject: [PATCH 1/4] Add inspect operator --- core/modules/filters.js | 75 ++++++++++----- core/modules/filters/inspect.js | 69 ++++++++++++++ .../tests/data/filter-wrappers/Simple.tid | 92 +++++++++++++++++++ .../tiddlers/filters/inspect Operator.tid | 26 ++++++ 4 files changed, 240 insertions(+), 22 deletions(-) create mode 100644 core/modules/filters/inspect.js create mode 100644 editions/test/tiddlers/tests/data/filter-wrappers/Simple.tid create mode 100644 editions/tw5.com/tiddlers/filters/inspect Operator.tid diff --git a/core/modules/filters.js b/core/modules/filters.js index 321f5a211..2f541378b 100644 --- a/core/modules/filters.js +++ b/core/modules/filters.js @@ -220,13 +220,22 @@ exports.filterTiddlers = function(filterString,widget,source) { Compile a filter into a function with the signature fn(source,widget) where: source: an iterator function for the source tiddlers, called source(iterator), where iterator is called as iterator(tiddler,title) widget: an optional widget node for retrieving the current tiddler etc. + +Parameters: +filterString: the filter string to compile +options: includes: + +wrappers: a hashmap of wrapper functions to apply to the compiled filter function */ -exports.compileFilter = function(filterString) { +exports.compileFilter = function(filterString,options) { + options = options || {}; + var self = this; + var wrappers = options.wrappers || {}; if(!this.filterCache) { this.filterCache = Object.create(null); this.filterCacheCount = 0; } - if(this.filterCache[filterString] !== undefined) { + if(this.filterCache[filterString] !== undefined && !wrappers) { return this.filterCache[filterString]; } var filterParseTree; @@ -252,17 +261,18 @@ exports.compileFilter = function(filterString) { currTiddlerTitle = widget && widget.getVariable("currentTiddler"); $tw.utils.each(operation.operators,function(operator) { var operands = [], - operatorFunction; + operatorName,operatorFunction; if(!operator.operator) { // Use the "title" operator if no operator is specified - operatorFunction = filterOperators.title; + operatorName = "title"; } else if(!filterOperators[operator.operator]) { // 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]"]; + operatorName = "[unknown]"; } else { // Use the operator function - operatorFunction = filterOperators[operator.operator]; + operatorName = operator.operator; } + operatorFunction = filterOperators[operatorName]; $tw.utils.each(operator.operands,function(operand) { if(operand.indirect) { operand.value = self.getTextReference(operand.text,"",currTiddlerTitle); @@ -274,10 +284,14 @@ exports.compileFilter = function(filterString) { } operands.push(operand.value); }); - + // Wrap the filter operator module if required + if(wrappers.operator) { + operatorFunction = wrappers.operator.bind(self,operatorFunction); + } // Invoke the appropriate filteroperator module results = operatorFunction(accumulator,{ operator: operator.operator, + operatorName: operatorName, operand: operands.length > 0 ? operands[0] : undefined, operands: operands, prefix: operator.prefix, @@ -307,27 +321,44 @@ exports.compileFilter = function(filterString) { var filterRunPrefixes = self.getFilterRunPrefixes(); // Wrap the operator functions in a wrapper function that depends on the prefix operationFunctions.push((function() { - var options = {wiki: self, suffixes: operation.suffixes || []}; + var prefixName; switch(operation.prefix || "") { case "": // No prefix means that the operation is unioned into the result - return filterRunPrefixes["or"](operationSubFunction, options); + prefixName = "or"; + break; case "=": // The results of the operation are pushed into the result without deduplication - return filterRunPrefixes["all"](operationSubFunction, options); + prefixName = "all"; + break; case "-": // The results of this operation are removed from the main result - return filterRunPrefixes["except"](operationSubFunction, options); + prefixName = "except"; + break; case "+": // This operation is applied to the main results so far - return filterRunPrefixes["and"](operationSubFunction, options); + prefixName = "and"; + break; case "~": // This operation is unioned into the result only if the main result so far is empty - return filterRunPrefixes["else"](operationSubFunction, options); - default: - if(operation.namedPrefix && filterRunPrefixes[operation.namedPrefix]) { - return filterRunPrefixes[operation.namedPrefix](operationSubFunction, options); - } else { - return function(results,source,widget) { - results.clear(); - results.push($tw.language.getString("Error/FilterRunPrefix")); - }; - } + prefixName = "else"; + break; + default: + prefixName = operation.namedPrefix; + break; + } + if(prefixName && filterRunPrefixes[prefixName]) { + var options = { + wiki: self, + suffixes: operation.suffixes || [], + prefixName: prefixName + }, + filterRunPrefixFunction = filterRunPrefixes[prefixName]; + // Wrap the filter operator module if required + if(wrappers.prefix) { + filterRunPrefixFunction = wrappers.prefix.bind(self,filterRunPrefixFunction); + } + return filterRunPrefixFunction(operationSubFunction,options); + } else { + return function(results,source,widget) { + results.clear(); + results.push($tw.language.getString("Error/FilterRunPrefix")); + }; } })()); }); diff --git a/core/modules/filters/inspect.js b/core/modules/filters/inspect.js new file mode 100644 index 000000000..7e38f5c68 --- /dev/null +++ b/core/modules/filters/inspect.js @@ -0,0 +1,69 @@ +/*\ +title: $:/core/modules/filters/inspect.js +type: application/javascript +module-type: filteroperator + +Filter operator for inspecting the evaluation of a filter + +\*/ + +"use strict"; + +/* +Export our filter function +*/ +exports.inspect = function(source,operator,options) { + var self = this, + inputFilter = operator.operands[0] || "", + output = {input: [],runs: []}, + currentRun; + // Save the input + source(function(tiddler,title) { + output.input.push(title); + }); + // Compile the filter with wrapper functions to log the details + var compiledFilter = options.wiki.compileFilter(inputFilter,{ + wrappers: { + prefix: function(filterRunPrefixFunction,operationFunction,innerOptions) { + return function(results,innerSource,innerWidget) { + var details ={ + prefixName: innerOptions.prefixName, + operators: [] + }; + currentRun = details.operators; + var innerResults = filterRunPrefixFunction.call(this,operationFunction,innerOptions), + prefixOutput = new $tw.utils.LinkedList(); + innerResults(prefixOutput,innerSource,innerWidget); + var prefixOutputArray = prefixOutput.toArray(); + details.output = prefixOutputArray; + output.runs.push(details); + results.clear(); + $tw.utils.each(prefixOutputArray,function(title) { + results.push(title); + }); + }; + }, + operator: function(operatorFunction,innerSource,innerOperator,innerOptions) { + var details = { + operatorName: innerOperator.operatorName, + operands: innerOperator.operands, + prefix: innerOperator.prefix, + suffix: innerOperator.suffix, + suffixes: innerOperator.suffixes, + regexp: innerOperator.regexp, + input: [] + }, + innerResults = operatorFunction.apply(self,Array.prototype.slice.call(arguments,1)); + innerSource(function(tiddler,title) { + details.input.push(title); + }); + currentRun.push(details); + return innerResults; + } + } + }); + output.output = compiledFilter.call(this,source,options.widget); + var results = []; + results.push(JSON.stringify(output,null,4)); + return results; +}; diff --git a/editions/test/tiddlers/tests/data/filter-wrappers/Simple.tid b/editions/test/tiddlers/tests/data/filter-wrappers/Simple.tid new file mode 100644 index 000000000..7694292e7 --- /dev/null +++ b/editions/test/tiddlers/tests/data/filter-wrappers/Simple.tid @@ -0,0 +1,92 @@ +title: FiltersWrappers/Simple +description: Test filter inspection +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\whitespace trim + +\procedure test-filter() +1 2 3 +\end + +\function test-filter-wrapper() +[inspect] +\end + +<$text text=<>/> +- +<$text text=<>/> ++ +title: ExpectedResult + +

1 2 3-{ + "input": [ + "$:/core", + "ExpectedResult", + "Output" + ], + "runs": [ + { + "prefixName": "or", + "operators": [ + { + "operatorName": "title", + "operands": [ + "1" + ], + "input": [ + "$:/core", + "ExpectedResult", + "Output" + ] + } + ], + "output": [ + "1" + ] + }, + { + "prefixName": "or", + "operators": [ + { + "operatorName": "title", + "operands": [ + "2" + ], + "input": [ + "$:/core", + "ExpectedResult", + "Output" + ] + } + ], + "output": [ + "2" + ] + }, + { + "prefixName": "or", + "operators": [ + { + "operatorName": "title", + "operands": [ + "3" + ], + "input": [ + "$:/core", + "ExpectedResult", + "Output" + ] + } + ], + "output": [ + "3" + ] + } + ], + "output": [ + "3" + ] +}

\ No newline at end of file diff --git a/editions/tw5.com/tiddlers/filters/inspect Operator.tid b/editions/tw5.com/tiddlers/filters/inspect Operator.tid new file mode 100644 index 000000000..44fef6e3d --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/inspect Operator.tid @@ -0,0 +1,26 @@ +caption: inspect +created: 20250401094200994 +modified: 20250401094200994 +op-input: a [[selection of titles|Title Selection]] +op-output: a JSON object containing the input, output and intermediate results of evaluating the specified filter +op-parameter: the filter to be inspected +op-parameter-name: F +op-purpose: inspect the evaluation of a filter to aid debugging +tags: [[Filter Operators]] +title: inspect Operator +type: text/vnd.tiddlywiki + +<<.from-version "5.3.7">> The <<.op inspect>> operator evaluates a filter with the specified input titles and returns a JSON object containing the input, output and intermediate results of evaluating the specified filter. + +The JSON object contains the following properties: + +* `input`: the input titles passed to the filter +* `output`: the output titles resulting from evaluating the filter +* `runs`: an array of objects, each of which represents a single run of the filter. Each object contains the following properties: +** `prefixName`: the name of the prefix operator that was used in this run +** `operators`: an array of objects, each of which represents a single operator that was used in this run. Each object contains the following properties: +*** `operatorName`: the name of the operator +*** `operands`: an array of string operands passed to the operator +*** `input`: the input titles passed to the operator +** `output`: the output titles resulting from evaluating this run + From 27075acbc646d9c337f37d7f182a5f88b3f472e2 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Tue, 1 Apr 2025 12:43:13 +0100 Subject: [PATCH 2/4] Add "inspect" tab to advanced search and make the search box into a textarea --- core/language/en-GB/Search.multids | 2 + core/ui/AdvancedSearch/Filter.tid | 52 +++++-------------- .../AdvancedSearch/FilterResults/Inspect.tid | 12 +++++ .../AdvancedSearch/FilterResults/Results.tid | 13 +++++ core/wiki/config/AdvancedSearchDefaultTab.tid | 2 + .../config/AdvancedSearchFilterDefaultTab.tid | 2 + .../wiki/tags/AdvancedSearchFilterResults.tid | 2 + 7 files changed, 45 insertions(+), 40 deletions(-) create mode 100644 core/ui/AdvancedSearch/FilterResults/Inspect.tid create mode 100644 core/ui/AdvancedSearch/FilterResults/Results.tid create mode 100644 core/wiki/config/AdvancedSearchDefaultTab.tid create mode 100644 core/wiki/config/AdvancedSearchFilterDefaultTab.tid create mode 100644 core/wiki/tags/AdvancedSearchFilterResults.tid diff --git a/core/language/en-GB/Search.multids b/core/language/en-GB/Search.multids index f5aa478bf..bf055cf86 100644 --- a/core/language/en-GB/Search.multids +++ b/core/language/en-GB/Search.multids @@ -4,6 +4,8 @@ DefaultResults/Caption: List Filter/Caption: Filter Filter/Hint: Search via a [[filter expression|https://tiddlywiki.com/static/Filters.html]] Filter/Matches: //<> matches// +Filter/FilterResults/Results/Caption: Results +Filter/FilterResults/Inspect/Caption: Inspect Matches: //<> matches// Matches/All: All matches: Matches/NoMatch: //No match// diff --git a/core/ui/AdvancedSearch/Filter.tid b/core/ui/AdvancedSearch/Filter.tid index 7369e4c40..a451fa973 100644 --- a/core/ui/AdvancedSearch/Filter.tid +++ b/core/ui/AdvancedSearch/Filter.tid @@ -34,39 +34,11 @@ caption: {{$:/language/Search/Filter/Caption}} \end -\procedure input-accept-actions() -\whitespace trim -<$list filter="[{$:/config/Search/NavigateOnEnter/enable}match[yes]]"> - <$list-empty> - <$list filter="[get[text]!is[missing]] :else[get[text]is[shadow]]"> - <$action-navigate $to={{{ [get[text]] }}}/> - - <$/list-empty> - <$action-navigate $to={{{ [get[text]] }}}/> - -\end - -\procedure input-accept-variant-actions() -\whitespace trim -<$list filter="[{$:/config/Search/NavigateOnEnter/enable}match[yes]]"> - <$list-empty> - <$list filter="[get[text]!is[missing]] :else[get[text]is[shadow]]"> - <$list filter="[<__tiddler__>get[text]minlength[1]]"> - <$action-sendmessage $message="tm-edit-tiddler" $param={{{ [get[text]] }}}/> - - - - <$list filter="[get[text]minlength[1]]"> - <$action-sendmessage $message="tm-edit-tiddler" $param={{{ [get[text]] }}}/> - - -\end - \whitespace trim <> -