Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kin filter: Recursively looking for kinship between tiddler titles #3511

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f5790f0
Filter operator that gathering "family" of tiddler
bimlas Oct 17, 2018
fff1ca0
Remove unneeded code
bimlas Oct 19, 2018
c8dee83
Do not use global `$tw`
bimlas Oct 19, 2018
5890ea9
Use as a real filter
bimlas Oct 19, 2018
304937d
List the kindred of input tiddles if there is no operand
bimlas Oct 24, 2018
de6bf92
Add negation option
bimlas Oct 26, 2018
55a28ef
Use to/with/from instead of up/both/down
bimlas Oct 25, 2018
bb0aa4e
Add documentation
bimlas Oct 27, 2018
7d3834a
Rename from `kindred` to `kin`
bimlas Oct 27, 2018
fdfe6de
Visualization of the filter results in the examples
bimlas Oct 31, 2018
05b9a53
Solve the problem with cyclical connections
bimlas Nov 1, 2018
f31afea
Modify examples, clarify documentation
bimlas Nov 1, 2018
ccc4d87
Change indentation, make it more readable
bimlas Nov 6, 2018
8a84087
Modify to suit to coding style
bimlas Nov 6, 2018
1d71b34
Merge remote-tracking branch 'upstream/master' into kin-filter
bimlas Nov 7, 2018
0ae7bf2
Use the new, builtin syntax for suffixes
bimlas Nov 7, 2018
dbc2d75
Prepare code to allow additional suffix options
bimlas Nov 7, 2018
152c903
Add `depth` option to specify max depth (0 means no limit)
bimlas Nov 7, 2018
2814191
Update documentation, be more verbose
bimlas Nov 7, 2018
3750994
Flip arrows on the picture; purpose of trees in examples
bimlas Nov 8, 2018
6437124
Use "older" standards; add tests
bimlas Nov 9, 2018
d6ae19b
Rename `findListingsOfTiddler` to `findTiddlersByField`
bimlas Nov 9, 2018
1832e43
Drop `describe` block of `kin` test, `it` is enough
bimlas Nov 9, 2018
ea5ab1e
Found a usage of `includes()`, changed to `indexOf()`
bimlas Nov 9, 2018
881bc32
Additional tests
bimlas Nov 9, 2018
e37596a
Accidentally changed DefaultTiddlers
bimlas Nov 20, 2018
1906752
Increment in a separate statement
bimlas Nov 20, 2018
cbbc88d
Keep `findListingsOfTiddler()` as alias of `findTiddlersByField()`
bimlas Nov 20, 2018
717e6f5
Use cache mechanism
bimlas Nov 20, 2018
660b4d3
Revert accidentally changed whitespace
bimlas Dec 10, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions core/modules/filters/kin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*\
title: $:/core/modules/filters/kin.js
type: application/javascript
module-type: filteroperator

Finds out where a tiddler originates from and what other tiddlers originate from it

\*/
(function() {

/*jslint node: true, browser: true */
/*global $tw: true */
"use strict";

function collectTitlesRecursively(baseTiddler,baseTitle,options) {
var cacheName = "kin-filter-" + baseTitle + "-" + options.fieldName + "-",
titlesPointingFromBase = {},
titlesPointingToBase = {},
resultsFrom = [],
resultsTo = [];

function addToResultsIfNotFoundAlready(alreadyFound,title,depth) {
if(title in alreadyFound) {
return false;
}
alreadyFound[title] = depth;
return true
}

function collectTitlesPointingFrom(tiddler,title,currentDepth) {
if(addToResultsIfNotFoundAlready(titlesPointingFromBase,title,currentDepth)) {
currentDepth += 1;
if(tiddler) {
$tw.utils.each(tiddler.getFieldList(options.fieldName),function(targetTitle) {
collectTitlesPointingFrom(options.wiki.getTiddler(targetTitle),targetTitle,currentDepth);
});
}
}
}

function collectTitlesPointingTo(title,currentDepth) {
if(addToResultsIfNotFoundAlready(titlesPointingToBase,title,currentDepth)) {
currentDepth += 1;
$tw.utils.each(options.wiki.findTiddlersByField(title,options.fieldName),function(targetTitle) {
collectTitlesPointingTo(targetTitle,currentDepth);
});
}
}

function getResultsInGivenDepth(cachedData) {
if(options.depth) {
return $tw.utils.getObjectKeysByExpression(cachedData,function(value) {
return value <= options.depth;
})
} else {
return Object.keys(cachedData);
}
}

if((options.direction === "from") || (options.direction === "with")) {
resultsFrom = $tw.wiki.getGlobalCache(cacheName + "from",function() {
collectTitlesPointingFrom(baseTiddler,baseTitle,0);
return titlesPointingFromBase;
});
resultsFrom = getResultsInGivenDepth(resultsFrom);
}
if((options.direction === "to") || (options.direction === "with")) {
resultsTo = $tw.wiki.getGlobalCache(cacheName + "to",function() {
collectTitlesPointingTo(baseTitle,0);
return titlesPointingToBase;
});
resultsTo = getResultsInGivenDepth(resultsTo);
}
return $tw.utils.pushTop(resultsFrom,resultsTo);
}

/*
Export our filter function
*/
exports.kin = function(source,operator,options) {
var results = [],
needsExclusion = operator.prefix === "!",
suffixes = operator.suffixes || [],
filterOptions = {
wiki: options.wiki,
fieldName: ((suffixes[0] || [])[0] || "tags").toLowerCase(),
direction: ((suffixes[1] || [])[0] || "with").toLowerCase(),
depth: Number((suffixes[2] || [])[0]),
};

if((operator.operand === "") && (needsExclusion)) {
return [];
}

if(operator.operand !== "") {
var baseTitle = operator.operand,
baseTiddler = options.wiki.getTiddler(baseTitle),
foundTitles = collectTitlesRecursively(baseTiddler,baseTitle,filterOptions);

source(function(tiddler,title) {
if(needsExclusion === (foundTitles.indexOf(title) === -1)) {
results.push(title);
}
});
} else {
source(function(tiddler,title) {
results = $tw.utils.pushTop(results,collectTitlesRecursively(tiddler,title,filterOptions));
});
}

return results;
}
})();
2 changes: 1 addition & 1 deletion core/modules/filters/listed.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ exports.listed = function(source,operator,options) {
var field = operator.operand || "list",
results = [];
source(function(tiddler,title) {
$tw.utils.pushTop(results,options.wiki.findListingsOfTiddler(title,field));
$tw.utils.pushTop(results,options.wiki.findTiddlersByField(title,field));
});
return results;
};
Expand Down
14 changes: 14 additions & 0 deletions core/modules/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,20 @@ exports.removeArrayEntries = function(array,value) {
}
};

/*
Collect keys of object (probably hash) where callback yields to true
*/
exports.getObjectKeysByExpression = function(object,callback) {
var key,
results = [];
for (key in object) {
if (object.hasOwnProperty(key) && callback(object[key])) {
results.push(key);
}
}
return results;
};

/*
Check whether any members of a hashmap are present in another hashmap
*/
Expand Down
6 changes: 5 additions & 1 deletion core/modules/wiki.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ exports.getTagMap = function() {
/*
Lookup a given tiddler and return a list of all the tiddlers that include it in the specified list field
*/
exports.findListingsOfTiddler = function(targetTitle,fieldName) {
exports.findTiddlersByField = function(targetTitle,fieldName) {
fieldName = fieldName || "list";
var titles = [];
this.each(function(tiddler,title) {
Expand All @@ -534,6 +534,10 @@ exports.findListingsOfTiddler = function(targetTitle,fieldName) {
return titles;
};

exports.findListingsOfTiddler = function(targetTitle,fieldName) {
return this.findTiddlersByField(targetTitle, fieldName)
};

/*
Sorts an array of tiddler titles according to an ordered list
*/
Expand Down
42 changes: 42 additions & 0 deletions editions/test/tiddlers/tests/test-filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,48 @@ describe("Filter tests", function() {
expect(wiki.filterTiddlers("[!untagged[]sort[title]]").join(",")).toBe("$:/TiddlerTwo,Tiddler Three,TiddlerOne");
});

it("should handle the kin operator",function() {
// It needs a tree-like wiki to test recursion.
var treeWiki = new $tw.Wiki();
treeWiki.addTiddler({
title: "A",
tags: ["B"],
list: "C"
});
treeWiki.addTiddler({
title: "B",
tags: ["C"],
});
treeWiki.addTiddler({
title: "C",
tags: ["A"],
list: ["E"]
});
treeWiki.addTiddler({
title: "D",
tags: ["B"],
});
treeWiki.addTiddler({
title: "E",
tags: ["D"],
});
treeWiki.addTiddler({
title: "F",
tags: ["E"],
});
treeWiki.addTiddler({
title: "G",
tags: ["D"],
});

expect(treeWiki.filterTiddlers("[kin[A]sort[title]]").join(",")).toBe("A,B,C,D,E,F,G");
expect(treeWiki.filterTiddlers("[kin::from[A]sort[title]]").join(",")).toBe("A,B,C");
expect(treeWiki.filterTiddlers("[kin::to[A]sort[title]]").join(",")).toBe("A,B,C,D,E,F,G");
expect(treeWiki.filterTiddlers("[kin[A]!kin::to[D]sort[title]]").join(",")).toBe("A,B,C");
expect(treeWiki.filterTiddlers("[kin::from:2[F]sort[title]]").join(",")).toBe("D,E,F");
expect(treeWiki.filterTiddlers("[kin:list[C]]").join(",")).toBe("A,C,E");
});

it("should handle the links operator", function() {
expect(wiki.filterTiddlers("[!is[shadow]links[]sort[title]]").join(",")).toBe("$:/TiddlerTwo,a fourth tiddler,one,Tiddler Three,TiddlerSix,TiddlerZero");
expect(wiki.filterTiddlers("[all[shadows]links[]sort[title]]").join(",")).toBe("TiddlerOne");
Expand Down
63 changes: 63 additions & 0 deletions editions/tw5.com/tiddlers/filters/examples/kin.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
created: 20181026114434874
modified: 20181108070317997
tags: [[Operator Examples]] [[kin Operator]]
title: kin Operator (Examples)
type: text/vnd.tiddlywiki

\define item-class(highlightfilter) <$list filter="""[$highlightfilter$is[current]first[]]""">highlighted-toc-item</$list>

\define each-level(tiddlername, highlightfilter)
<li>
<$wikify name="transcluded-item-class" text=<<item-class """$highlightfilter$""">> >
<$link class=<<transcluded-item-class """$highlightfilter$""">> >[[$(currentTiddler)$]]</$link>
</$wikify>
<ul>
<$list filter="""$tiddlername$ +[kin[]tag[$(currentTiddler)$]]""">
<<each-level """$tiddlername$""" """$highlightfilter$""">>
</$list>
</ul>
</li>
\end

\define kin-toc(highlightfilter)
<div class="tc-table-of-contents">
<$tiddler tiddler="TableOfContents">
<ul>
<<each-level "[[Filter Syntax]] [[kin Operator]]" """$highlightfilter$""">>
</ul>
</$tiddler>
</div>
\end

\define kin-example-with-toc(number filter comment)
<<.operator-example """$number$""" """[$filter$]""" """$comment$""">>
<$reveal type="nomatch" state="""$:/state/kin-example-with-toc-$number$""" text="show">
<$button set="""$:/state/kin-example-with-toc-$number$""" setTo="show">Show tree</$button>
</$reveal>
<$reveal type="match" state="""$:/state/kin-example-with-toc-$number$""" text="show">
<$button set="""$:/state/kin-example-with-toc-$number$""" setTo="hide">Hide tree</$button>
<<kin-toc """$filter$""">>
</$reveal>
\end

<style>
.highlighted-toc-item a {
color: red !important;
}
</style>

Family tree of [[Filter Syntax]] and [[kin Operator]] (to really understand, look at the [[TableOfContents]])

<<kin-toc "!is[current]">>

''The "Show tree" button below the examples only helps in understanding the filter, it's not part of the output.''

<<kin-example-with-toc 1 "kin[Filter Syntax]" "input titles which are family members of the parameter title">>
<<kin-example-with-toc 2 "kin[Filter Syntax]kin[kin Operator]" "common family members of each of the specified titles (intersection)">>
<<kin-example-with-toc 3 "[Filter Syntax]] [[kin Operator]] +[kin[]" "collected titles which are family members of any of the input tiddlers">>
<<kin-example-with-toc 4 "kin::to[Filters]" "successors of the given tiddler">>
<<kin-example-with-toc 5 "kin::from[Filter Expression]kin::to[Filters]" "subset of the family tree">>
<<kin-example-with-toc 6 "kin::from:2[Filter Expression]" "ancestors of the given tiddler until the given depth">>
<<kin-example-with-toc 7 "kin:tags:from[Filter Syntax]" "ancestors of tiddler based on `tags` field (`tags` points to parents)">>
<<kin-example-with-toc 8 "kin:list:to[Filter Syntax]" "ancestors of tiddler based on `list` field (`list` points to children)<br>[[Reference]] listing [[Concepts]], but [[Concepts]] does not listing [[Filters]] so they do not belong to the same family based on `list`">>
<<.operator-example 9 "[!is[system]type[text/vnd.tiddlywiki]!kin[TableOfContents]first[10]]" "first 10 tiddlers which are not related to [[TableOfContents]] (by `tags`)">>
40 changes: 40 additions & 0 deletions editions/tw5.com/tiddlers/filters/kin.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
caption: kin
created: 20181025133804171
modified: 20181107224931131
op-input: a [[selection of titles|Title Selection]]
op-neg-output: ''with parameter <<.place B>>''<br>&raquo; those input titles which are ''<<.em not>> kin with <<.place B>>''<br>''without parameter <<.place B>>''<br>&raquo; ignored
op-output: ''with parameter <<.place B>>''<br>&raquo; those input titles which are ''kin with <<.place B>>'' <br>''without <<.place B>>''<br>&raquo; ''all'' tiddler titles which are ''kin with input titles'' (treat input titles as base tiddlers)
op-parameter: base tiddler title or nothing
op-parameter-name: B
op-purpose: recursively looking for kinship between tiddler titles
op-suffix: the <<.op kin>> operator uses a rich suffix, see below for details
tags: [[Negatable Operators]] [[Tag Operators]] [[Filter Operators]] [[Field Operators]]
title: kin Operator
type: text/vnd.tiddlywiki

The purpose of the <<.op {{!!caption}}>> operator with examples:

* Finds out where base tiddler originates and what other elements originate from it
* Finds the ancestors and successors of a family member
* Finds the "leaves" of the branch of the base tiddler in a tree-like structure (where the base tiddler is a leaf)
* Finds the super- and subsets / groups of a mathematical set (where the base tiddler is a set)

[img[kin Operator.svg]]

The <<.op {{!!caption}}>> operator uses an extended syntax that permits multiple fields and flags to be passed:

```
[kin:<field>:<direction>:<depth>[<operand>]]
```

* ''field'': name of the [[field|TiddlerFields]] which connecting tiddlers (assumed to be a [[title list|Title List]], defaulting to <<.field tags>>)
* ''direction'': collect the tiddler titles in this direction relative to the base tiddler
** ''from'': collect kins of base tiddler pointing from it (including the base tiddler title itself)
** ''to'': collect kins of base tiddler pointing to it (including the base tiddler title itself)
** ''with'': (the default) union of the aboves
* ''depth'': maximum depth of the collected labels in the tree structure relative to the base tiddler (a positive number, not limited by default)
* ''operand'': filter operand, the base tiddler

<<.op {{!!caption}}>> is a [[modifier|Selection Constructors]], but without <<.place B>> parameter is a [[constructor|Selection Constructors]].

<<.operator-examples "kin">>
1 change: 1 addition & 0 deletions editions/tw5.com/tiddlers/images/kin Operator.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions editions/tw5.com/tiddlers/images/kin Operator.svg.meta
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
tags: picture
title: kin Operator.svg
type: image/svg+xml