bengillies.net

a blog by Ben Gillies

Like.py

This is a Python plugin for TiddlyWeb that filters tiddlers, returning all tiddlers that have the specified string somewhere in the specified field (aka - partial matching on fields). It is available on GitHub at like.py.

The code is republished below for reference purposes (though any future updates will only appear on GitHub):

"""
Compare the given string to the supplied field and return everything that partially matches:
 
eg:
 
/bags/foo/tiddlers?like=title:bar
 
will return all tiddlers where bar is contained somewhere within the title
 
"""
 
from tiddlyweb.filters import FILTER_PARSERS
from tiddlyweb.filters.select import select_parse
 
 
 
 
 
def compare_text(source, test, negate=False):
    if source.lower() in test.lower():
        return True != negate
            
    return False != negate
 
def compare_tags(source, test, negate=False):
    count = 0
    for tag in test:
        if source.lower() in tag.lower():
            return True != negate
            
    return False != negate
 
def compare_fields(source, test, attribute, negate=False):
    try:
        if type(test[attribute]) == text:
            return compare_text(source[attribute], test[attribute])
    except KeyError:
        return False != negate
            
    return False != negate
                         
ATTRIBUTE_SELECTOR={
    'tags': compare_tags,
    }
 
 
def like(attribute, args, tiddlers, negate=False):
    for tiddler in tiddlers:
        try:
            test = getattr(tiddler, attribute)
            test_func = ATTRIBUTE_SELECTOR.get(attribute, compare_text)
            found = test_func(args, test, negate)
        except AttributeError:
            found = compare_fields(args, tiddler.fields, attribute, negate)
            
        if found:
            yield tiddler
    
    return
 
 
def like_parse(command):
    attribute, args = command.split(':', 1)
    
    if args.startswith('!'):
        args = args.replace('!', '', 1)
        def selector(tiddlers):
            return like(attribute, args, tiddlers, negate=True)
    else:
        def selector(tiddlers):
            return like(attribute, args, tiddlers)
            
    return selector
 
 
FILTER_PARSERS['like'] = like_parse
 
 
def init(config):
    pass
 

Filtering TiddlyWeb

Following on from my previous post about validators, I thought I'd talk about filters in TiddlyWeb and give a couple of examples about how they work.

Filters take a list of tiddlers, filter that list in some way, and return the filtered list. How it works behind the scenes is slightly more complex, but essentially the end result is that you can apply them to any list of tiddlers and you'll get a filtered list back in return, so whether that's a recipe, or a bag of tiddlers (or even a list of tiddlers in a custom python script) it doesn't matter: they can all be filtered.

Writing a custom filter, as you may expect with TiddlyWeb, is trivially easy: all it requires is a function that takes the filter itself, and returns another function that in turn, does the filtering on a list of tiddlers. If you're thinking about writing a filter yourself, this means that you'll need at least two functions, one to parse the filter into a usable format, and another to take that format, along with a list of tiddlers (technically in Python, this is a generator object) and return the filtered list.

So, onto my filters: One filter matches the tiddlers in a given list with a named tiddler, returning all those tiddlers that are related, ranked in order of most-relatedness; The other filter does partial matching on any field that you specify, returning all tiddlers that have a field that partially matches the given text.

You can use them as follows:

Related tiddlers:


/recipes/foo/tiddlers?related=bar:title,tags,bag

The above filter would take all tiddlers in the recipe "foo", and return all those related to the tiddler "bar" (where "bar" is in the same recipe). The related-ness in this example would be based on three fields: title; tags; and bag.

Like tiddlers:


/recipes/foo/tiddlers?like=tags:tiddly

This filter would take all tiddlers in the recipe "foo" and return all those where "tiddly" appears somewhere in one of the tags. In this particular example, this would be useful if you're looking for tiddlers about either TiddlyWiki, or TiddlyWeb.

Both of these are available on GitHub, where you'll find the most up to date version. I have also reproduced the code (as it is at the moment) on my blog. If you're interested, take a look:

Related.py - find all tiddlers related to the given tiddler
Like.py - compare the given string to the supplied field and return everything that partially matches

Related.py

This is a Python plugin for TiddlyWeb that filters tiddlers, returning all related tiddlers, ranked in order of related-ness. It is available on GitHub at related.py.

The code is republished below for reference purposes (though any future updates will only appear on GitHub):

"""
Compare the given tiddler with other tiddlers in the bag and return
anything that is related byt the supplied fields, sorted in order with most related first
 
eg:
 
/bags/foo/tiddlers?related=bar:title,tags
 
will return all tiddlers related (by title and tags) to the tiddler "bar", ranked in most related first order
 
"""
 
from tiddlyweb.filters import FILTER_PARSERS, parse_for_filters, recursive_filter
 
import logging
 
import re
 
 
def compare_text(source, test):
    source_words = re.split('\W',source)
    count = 0
    for word in source_words:
        if word.lower() in test.lower():
            count += 1
            
    return count
 
def compare_tags(source, test):
    count = 0
    for tag in source:
        if tag in test:
            count += 1
            
    return count
 
def compare_fields(source, test, match):
    count = 0
    try:
        if type(source[match]) == text:
            count = compare_text(source[match], test[match])
    except KeyError:
        pass
            
    return count
                         
ATTRIBUTE_SELECTOR={
    'tags': compare_tags,
    }
 
def match_related_articles(title, matches, tiddlers):
    def empty_generator(): return ;yield 'never'
    tiddlers = [tiddler for tiddler in tiddlers]
    try:
        source_tiddler = recursive_filter(parse_for_filters('select=title:%s' % title)[0], tiddlers).next()
    except StopIteration:
        #nothing to match on, so return an empty generator
        return empty_generator()
                         
    sort_set = []
    for tiddler in tiddlers:
        count = 0
        for match in matches:
            try:
                source = getattr(source_tiddler, match)
                test = getattr(tiddler, match)
                test_func = ATTRIBUTE_SELECTOR.get(match, compare_text)
                count += test_func(source, test)
            except AttributeError:
                count += compare_fields(source_tiddler.fields, tiddler.fields, match)
                            
        if count > 0 and source_tiddler.title != tiddler.title:
            sort_set.append([tiddler,count])
    
    def sort_function(a,b): return cmp(b[1],a[1])
    sort_set.sort(sort_function)
    
    result = (tiddler_set[0] for tiddler_set in sort_set)
    
    return result
 
 
 
def related_parse(command):
    
    attribute, args = command.split(':', 1)
    args = args.split(',')
    
    def relator(tiddlers):
        return match_related_articles(attribute, args, tiddlers)
    
    return relator
 
 
FILTER_PARSERS['related'] = related_parse
        
def init(config):
    pass