html_validator.py
This is a Python plugin for TiddlyWeb that restricts HTML tags and attributes to a whitelist. It is available on GitHub at html_validator.py.
The code is republished below for reference purposes (though any future updates will only appear on GitHub):
The code is republished below for reference purposes (though any future updates will only appear on GitHub):
from tiddlyweb.web.validator import TIDDLER_VALIDATORS
from tiddlyweb.model.tiddler import Tiddler
import logging
from BeautifulSoup import BeautifulSoup, Comment
import re
"""
sanitise any input that has unauthorised javascript/html tags in it
Let through only tags and attributes in the whitelists ALLOWED_TAGS and ALLOWED_ATTRIBUTES
"""
ALLOWED_TAGS=[
'html',
'p',
'i',
'strong',
'b',
'u',
'a',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'pre',
'br',
'img',
'span',
'em',
'strike',
'sub',
'sup',
'address',
'font',
'table',
'tbody',
'tr',
'td',
'ol',
'ul',
'li',
'div'
]
ALLOWED_ATTRIBUTES=[
'href',
'src',
'alt',
'title'
]
def sanitise_html(value):
#match dangerous attribute values (eg javascript:) with regex
r = re.compile(r'[\s]*(&#x.{1,7})?'.join(list('javascript:')))
#turn the input into a BeautifulSoup parse tree
soup = BeautifulSoup(value)
#get all HTML comments (<!-- -->) and remove them
for comment in soup.findAll(text=lambda text: isinstance(text, Comment)):
comment.extract()
#Check all tags against the allowed set and remove any that are not found
for tag in soup.findAll(True):
if tag.name not in ALLOWED_TAGS:
tag.hidden = True
#check that attribute/value pairs against the allowed list and regex and cut any that are not allowed
tag.attrs = [(attr, r.sub('', val)) for attr, val in tag.attrs
if attr in ALLOWED_ATTRIBUTES]
return soup.renderContents().decode('utf8')
def sanitise_xss(tiddler,environ):
for field,value in tiddler.fields.iteritems():
tiddler.fields[field] = sanitise_html(value)
tiddler.text = sanitise_html(tiddler.text)
tiddler.tags = map(sanitise_html,tiddler.tags)
tiddler.title = sanitise_html(tiddler.title)
def init(config_in):
"""
init function
"""
TIDDLER_VALIDATORS.append(sanitise_xss)
from tiddlyweb.model.tiddler import Tiddler
import logging
from BeautifulSoup import BeautifulSoup, Comment
import re
"""
sanitise any input that has unauthorised javascript/html tags in it
Let through only tags and attributes in the whitelists ALLOWED_TAGS and ALLOWED_ATTRIBUTES
"""
ALLOWED_TAGS=[
'html',
'p',
'i',
'strong',
'b',
'u',
'a',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'pre',
'br',
'img',
'span',
'em',
'strike',
'sub',
'sup',
'address',
'font',
'table',
'tbody',
'tr',
'td',
'ol',
'ul',
'li',
'div'
]
ALLOWED_ATTRIBUTES=[
'href',
'src',
'alt',
'title'
]
def sanitise_html(value):
#match dangerous attribute values (eg javascript:) with regex
r = re.compile(r'[\s]*(&#x.{1,7})?'.join(list('javascript:')))
#turn the input into a BeautifulSoup parse tree
soup = BeautifulSoup(value)
#get all HTML comments (<!-- -->) and remove them
for comment in soup.findAll(text=lambda text: isinstance(text, Comment)):
comment.extract()
#Check all tags against the allowed set and remove any that are not found
for tag in soup.findAll(True):
if tag.name not in ALLOWED_TAGS:
tag.hidden = True
#check that attribute/value pairs against the allowed list and regex and cut any that are not allowed
tag.attrs = [(attr, r.sub('', val)) for attr, val in tag.attrs
if attr in ALLOWED_ATTRIBUTES]
return soup.renderContents().decode('utf8')
def sanitise_xss(tiddler,environ):
for field,value in tiddler.fields.iteritems():
tiddler.fields[field] = sanitise_html(value)
tiddler.text = sanitise_html(tiddler.text)
tiddler.tags = map(sanitise_html,tiddler.tags)
tiddler.title = sanitise_html(tiddler.title)
def init(config_in):
"""
init function
"""
TIDDLER_VALIDATORS.append(sanitise_xss)
Comments
ZPublishCleanup
// hijack function to handle 412 edit conflict response
config.extensions.ServerSideSavingPlugin.saveTiddlerCallback = function(context, userParams) {
var tiddler = context.tiddler;
if(context.status || context.httpStatus == 1223) {
handle204(context);
} else {
if(context.httpStatus == 412) {
handle412(context);
} else {
displayMessage(config.extensions.ServerSideSavingPlugin.locale.saveError.format([tiddler.title, context.statusText]));
}
}
};
function handle204(context) {
var tiddler = context.tiddler;
if(tiddler.fields.changecount == context.changecount) { //# check for changes since save was triggered
tiddler.clearChangeCount();
} else if(tiddler.fields.changecount > 0) {
tiddler.fields.changecount -= context.changecount;
}
displayMessage(config.extensions.ServerSideSavingPlugin.locale.saved.format([tiddler.title]));
store.setDirty(false);
// from here is a custom bit for the copyPlugin; you'd want to handle via a custom 204 handler passed into the ServerSideSavingPlugin
if (tiddler.fields["sourceworkspace"]) {
newWorkspace = tiddler.fields['server.workspace'];
originalworkspace = tiddler.fields["sourceworkspace"];
tiddler.fields["server.workspace"] = originalworkspace;
tiddler.fields["server.page.revision"] = tiddler.fields["revisionsidinsource"];
if (tiddler.fields["publishlevel"] == 'move') {
store.removeTiddler(tiddler.title);
autoSaveChanges();
}
delete tiddler.fields['sourceworkspace'];
delete tiddler.fields["revisionsidinsource"];
}
}
function handle412(context) {
var tiddler = context.tiddler;
try {
var adaptor = config.extensions.ServerSideSavingPlugin.getTiddlerServerAdaptor(tiddler);
} catch(ex) {
return false;
}
if (!adaptor.host){
adaptor.host = context.host;
}
var context = {workspace: tiddler.fields["server.workspace"]};
var req = adaptor.getTiddler(tiddler.title, context, {}, onGetTiddler);
return req ? tiddler : false;
}
function onGetTiddler(context) {
var destTiddler = context.tiddler;
var sourceTiddler = store.getTiddler(destTiddler.title);
sourceTiddler.fields['server.page.revision'] = destTiddler.fields['server.page.revision'];
sourceTiddler.fields['server.workspace'] = destTiddler.fields['server.workspace'];
store.saveTiddler(sourceTiddler.title);
autoSaveChanges(false);
}
config.extensions.ServerSideSavingPlugin.saveTiddlerCallback = function(context, userParams) {
var tiddler = context.tiddler;
if(context.status || context.httpStatus == 1223) {
handle204(context);
} else {
if(context.httpStatus == 412) {
handle412(context);
} else {
displayMessage(config.extensions.ServerSideSavingPlugin.locale.saveError.format([tiddler.title, context.statusText]));
}
}
};
function handle204(context) {
var tiddler = context.tiddler;
if(tiddler.fields.changecount == context.changecount) { //# check for changes since save was triggered
tiddler.clearChangeCount();
} else if(tiddler.fields.changecount > 0) {
tiddler.fields.changecount -= context.changecount;
}
displayMessage(config.extensions.ServerSideSavingPlugin.locale.saved.format([tiddler.title]));
store.setDirty(false);
// from here is a custom bit for the copyPlugin; you'd want to handle via a custom 204 handler passed into the ServerSideSavingPlugin
if (tiddler.fields["sourceworkspace"]) {
newWorkspace = tiddler.fields['server.workspace'];
originalworkspace = tiddler.fields["sourceworkspace"];
tiddler.fields["server.workspace"] = originalworkspace;
tiddler.fields["server.page.revision"] = tiddler.fields["revisionsidinsource"];
if (tiddler.fields["publishlevel"] == 'move') {
store.removeTiddler(tiddler.title);
autoSaveChanges();
}
delete tiddler.fields['sourceworkspace'];
delete tiddler.fields["revisionsidinsource"];
}
}
function handle412(context) {
var tiddler = context.tiddler;
try {
var adaptor = config.extensions.ServerSideSavingPlugin.getTiddlerServerAdaptor(tiddler);
} catch(ex) {
return false;
}
if (!adaptor.host){
adaptor.host = context.host;
}
var context = {workspace: tiddler.fields["server.workspace"]};
var req = adaptor.getTiddler(tiddler.title, context, {}, onGetTiddler);
return req ? tiddler : false;
}
function onGetTiddler(context) {
var destTiddler = context.tiddler;
var sourceTiddler = store.getTiddler(destTiddler.title);
sourceTiddler.fields['server.page.revision'] = destTiddler.fields['server.page.revision'];
sourceTiddler.fields['server.workspace'] = destTiddler.fields['server.workspace'];
store.saveTiddler(sourceTiddler.title);
autoSaveChanges(false);
}
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):
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
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
PublishCommand
config.commands.saveTiddler.old_handler = config.commands.saveTiddler.handler;
config.commands.saveTiddler.handler = function (event, src, title) {
var stored_tiddler = store.getTiddler(title);
if(stored_tiddler&&stored_tiddler.fields && stored_tiddler.fields['sourceworkspace']){
delete stored_tiddler.fields['sourceworkspace'];
}
config.commands.saveTiddler.old_handler(event,src,title);
};
config.commands.publishtiddler = {
text: "publish",
tooltip: "publish this so everyone can see it",
confirmMsg: "Are you sure you want to publish this? If so, it will become visible to everybody and you will no longer be able to edit it",
saveFirstMsg: "Please save this first!",
handler: function(event,src,title){
var t = store.getTiddler(title);
if(!t){
alert(this.saveFirstMsg);
}
if (!confirm(this.confirmMsg)) {
return false;
}
var fields = store.getTiddler(title).fields;
//********************CHANGE THIS BIT TO YOUR PREFERRED BAG***************
fields['publishtobag'] = "blog";
fields['publishlevel'] = "copy";
//********************************************************************************************
var publishToBag = fields.publishtobag;
var newWorkspace = "bags/"+publishToBag;
var publishLevel = fields.publishlevel;
if(publishLevel) {
fields['sourceworkspace'] = fields['server.workspace'];
fields['revisionsidinsource'] = fields['server.page.revision'];
fields['server.workspace'] = newWorkspace;
store.saveTiddler(title);
autoSaveChanges();
} else {
alert("no publish level set!");
}
}
};
config.commands.saveTiddler.handler = function (event, src, title) {
var stored_tiddler = store.getTiddler(title);
if(stored_tiddler&&stored_tiddler.fields && stored_tiddler.fields['sourceworkspace']){
delete stored_tiddler.fields['sourceworkspace'];
}
config.commands.saveTiddler.old_handler(event,src,title);
};
config.commands.publishtiddler = {
text: "publish",
tooltip: "publish this so everyone can see it",
confirmMsg: "Are you sure you want to publish this? If so, it will become visible to everybody and you will no longer be able to edit it",
saveFirstMsg: "Please save this first!",
handler: function(event,src,title){
var t = store.getTiddler(title);
if(!t){
alert(this.saveFirstMsg);
}
if (!confirm(this.confirmMsg)) {
return false;
}
var fields = store.getTiddler(title).fields;
//********************CHANGE THIS BIT TO YOUR PREFERRED BAG***************
fields['publishtobag'] = "blog";
fields['publishlevel'] = "copy";
//********************************************************************************************
var publishToBag = fields.publishtobag;
var newWorkspace = "bags/"+publishToBag;
var publishLevel = fields.publishlevel;
if(publishLevel) {
fields['sourceworkspace'] = fields['server.workspace'];
fields['revisionsidinsource'] = fields['server.page.revision'];
fields['server.workspace'] = newWorkspace;
store.saveTiddler(title);
autoSaveChanges();
} else {
alert("no publish level set!");
}
}
};
URL Handling
In a previous post on TiddlyWebPages, I mentioned URL handling functionality, and promised a later post detailing how to use it. Well, since then, I've done a considerable amount of extra work on it, simplifying the way URL's are stored, and adding a couple of extra features, which I'll detail here.
First, and most importantly though, it's no longer part of TiddlyWebPages. Instead, it has been packaged up as tiddlywebplugins.urls and can be installed by doing
Then you'll need to put 'tiddlywebplugins.urls' into both the 'server_plugins' and 'twanager_plugins' sections of tiddlywebconfig.py.
Once you've done that, you'll be able to add URLs with twanager. For example, loading up the "default" recipe as a TiddlyWiki at /mywiki, you'd write:
Where /mywiki is the url path you want to use, and /recipes/default/tiddlers.wiki is what you want to appear there.
If you wanted to load up any recipe as a wiki, and serve them up at /wikis/<recipe_name> then you could write:
In this example, {recipe:segment} specifies a variable within the URL called "recipe". The {{ recipe }} then get's replaced with whatever URL you eventually go to. You could then arrive at the /mywiki example above by pointing your browser at /wikis/default.
You can find further instructions about syntax for the URL path you are adding at http://lukearno.com/projects/selector/ though note that any variables are always embedded within the destination URL with double braces (eg - /recipes/{{ recipe }}/tiddlers.wiki).
tiddlywebplugins.urls also supports URL redirection. This can be used either internally to your site, or with any external link instead.
To use internally, just add --redirect to the twanager command (eg - twanager url --redirect /s3Rw /bags/common/tiddlers/MyTiddler would redirect anyone going to /s3Rw to /bags/common/tiddlers/MyTiddler).
To use with external sites, you just need to use the full URL. For example, you could redirect to Google with:
These URLs are all stored in the TiddlyWeb store with the Title being the URL you want to create, and the text being the URL you want to map/redirect to. The redirect option in the twanager command, relates directly to tagging the tiddler "redirect".
By default, this will be stored in a bag called urls, with a strict policy to stop other users modifying your URLs. This means that, with sufficient permissions, you could manage all your URLs from within TiddlyWebWiki itself.
Just one final note to finish off with, Currently, this only supports GET requests. This means that you cannot send PUT, POST or DELETE requests to these URLs. Saying that though, TiddlyWebWiki should still work regardless, just bear in mind that you should currently be PUTting to the standard set of URLs if you're writing any custom Javascript yourself.
Source code: http://github.com/bengillies/tiddlywebplugins.urls
Package: http://pypi.python.org/pypi/tiddlywebplugins.urls
Guide to Selector: http://lukearno.com/projects/selector/
First, and most importantly though, it's no longer part of TiddlyWebPages. Instead, it has been packaged up as tiddlywebplugins.urls and can be installed by doing
pip install -U tiddlywebplugins.urls
Then you'll need to put 'tiddlywebplugins.urls' into both the 'server_plugins' and 'twanager_plugins' sections of tiddlywebconfig.py.
Once you've done that, you'll be able to add URLs with twanager. For example, loading up the "default" recipe as a TiddlyWiki at /mywiki, you'd write:
twanager url /mywiki /recipes/default/tiddlers.wiki
Where /mywiki is the url path you want to use, and /recipes/default/tiddlers.wiki is what you want to appear there.
If you wanted to load up any recipe as a wiki, and serve them up at /wikis/<recipe_name> then you could write:
twanager url /wikis/{recipe:segment} "/recipes/{{ recipe }}/tiddlers.wiki"
In this example, {recipe:segment} specifies a variable within the URL called "recipe". The {{ recipe }} then get's replaced with whatever URL you eventually go to. You could then arrive at the /mywiki example above by pointing your browser at /wikis/default.
You can find further instructions about syntax for the URL path you are adding at http://lukearno.com/projects/selector/ though note that any variables are always embedded within the destination URL with double braces (eg - /recipes/{{ recipe }}/tiddlers.wiki).
Redirection
tiddlywebplugins.urls also supports URL redirection. This can be used either internally to your site, or with any external link instead.
To use internally, just add --redirect to the twanager command (eg - twanager url --redirect /s3Rw /bags/common/tiddlers/MyTiddler would redirect anyone going to /s3Rw to /bags/common/tiddlers/MyTiddler).
To use with external sites, you just need to use the full URL. For example, you could redirect to Google with:
twanager url /google http://www.google.com
URL storage and Modification
These URLs are all stored in the TiddlyWeb store with the Title being the URL you want to create, and the text being the URL you want to map/redirect to. The redirect option in the twanager command, relates directly to tagging the tiddler "redirect".
By default, this will be stored in a bag called urls, with a strict policy to stop other users modifying your URLs. This means that, with sufficient permissions, you could manage all your URLs from within TiddlyWebWiki itself.
Just one final note to finish off with, Currently, this only supports GET requests. This means that you cannot send PUT, POST or DELETE requests to these URLs. Saying that though, TiddlyWebWiki should still work regardless, just bear in mind that you should currently be PUTting to the standard set of URLs if you're writing any custom Javascript yourself.
Useful Links
Source code: http://github.com/bengillies/tiddlywebplugins.urls
Package: http://pypi.python.org/pypi/tiddlywebplugins.urls
Guide to Selector: http://lukearno.com/projects/selector/
tiddlywiki_validator.py
This is a Python plugin for TiddlyWeb that filters out specific tiddlers base on titles matched with a blacklist. It also removes the systemConfig tag, if present and is designed to be used speficially to validate TiddlyWebWiki. It is available on GitHub at tiddlywiki_validator.py.
The code is republished below for reference purposes (though any future updates will only appear on GitHub):
The code is republished below for reference purposes (though any future updates will only appear on GitHub):
from tiddlyweb.web.validator import TIDDLER_VALIDATORS
from tiddlyweb.model.tiddler import Tiddler
"""
sanitise all tiddlywiki input by dissallowing reserved names, and clearing systemConfig tags.
Any tiddler in RESERVED_TITLES will be dissallowed.
"""
RESERVED_TITLES=[
'MarkupPreHead',
'MarkupPostBody'
]
def validate_tiddlywiki(tiddler,environ):
if tiddler.title in RESERVED_TITLES:
raise Exception('Reserved name')
if 'systemConfig' in tiddler.tags:
tiddler.tags.remove('systemConfig')
def init(config_in):
"""
init function
"""
TIDDLER_VALIDATORS.append(validate_tiddlywiki)
from tiddlyweb.model.tiddler import Tiddler
"""
sanitise all tiddlywiki input by dissallowing reserved names, and clearing systemConfig tags.
Any tiddler in RESERVED_TITLES will be dissallowed.
"""
RESERVED_TITLES=[
'MarkupPreHead',
'MarkupPostBody'
]
def validate_tiddlywiki(tiddler,environ):
if tiddler.title in RESERVED_TITLES:
raise Exception('Reserved name')
if 'systemConfig' in tiddler.tags:
tiddler.tags.remove('systemConfig')
def init(config_in):
"""
init function
"""
TIDDLER_VALIDATORS.append(validate_tiddlywiki)
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):
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
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
BlogLayout
| Name: | BlogLayout |
| Description: | adds a blog like view and tiddler summary view to TiddlyWiki |
| Author | BenGillies |
| CodeRepository: | http://svn.tiddlywiki.org/Trunk/contributors/BenGillies/plugins/BlogLayout.js |
| Version: | 1.0 |
| Comments: | Please make comments at http://groups.google.co.uk/group/TiddlyWikiDev |
| License | BSD License |
| CoreVersion: | 2.5 |
Usage
Set POST_TAG_NAME to the tag that you want to load by default. This will then automatically order all tags most recent first.
All posts longer than MAX_HEIGHT will be shortened and a "Read More..." link appended to the bottom.
You can additionally call
<<collapseThisTiddler default_height>>
At the end of any tiddler to provide a similarly shortened view with a "Read More..." link at the bottom. default_height is optional and provides a default to set the height to if setCollapseHeightHere has not been called within the tiddler (see below). Set default_height to -1 to set a default of not shortening tiddlers. This can be placed in the ViewTemplate tiddler (AUTO_SUMMARISE_FRONT_PAGE should be turned off if you are doing this) right after the .viewer div, as follows:
<div macro="collapseThisTiddler default_height"></div>;
You can also set a custom height from within the tiddler. If you do this, it will take precedent over all other default height settings and is the recommended method of setting height as it allows you to fine tune how short each tiddler can be. To use, call:
<<setCollapseHeightHere turn_off>>
This will set the height of the shortened tiddler to wherever you place the macro. turn_off should be -1 if you wish the tiddler to always appear in full. Otherwise, leave blank.
You can link to a blog-like page/layout (as per the page ouy get on first load) by putting:
<<recentByTagLink link_name tag_name max_posts collapse_posts default_height>>
in place of any link, where:
link_name = the text you want the link to read
tag_name = the name of the tag you want to filter by (aka POST_TAG_NAME)
max_posts = the maximum number of posts to display
collapse_posts = this can be 1 or 0. If 1 it will shorten posts, adding the Read More link. Default is AUTO_SUMMARISE_FRONT_PAGE.
default_height = the default height of shortened posts. Set to -1 to turn off by default.
Note - It is assumed that when a user clicks on a link specifically, they want to read the whole tiddler. If you want tiddlers to appear shortened when they are clicked on, you will need to edit the ViewTemplate tiddler.
Code
if(!version.extensions.BlogLayout)
{ //# ensure that the plugin is only installed once
version.extensions.BlogLayout = { installed: true }
};
(function($) { //set up alias for jQuery
config.macros.BlogLayout =
{
//*******collapseTiddlers variables********//
AUTO_SUMMARISE_FRONT_PAGE: true, //collapse all default tiddlers on first load (other tiddlers are unaffected)
MAX_HEIGHT: 200, //max height of tiddler content in pixels (default value)
//*******recentPosts variables*************//
POST_DISPLAY_COUNT: 5, //maximum number of posts to display
POST_TAG_NAME: "blog" //all posts that you want displayed in date order need to be tagged with this.
}
config.macros.BlogLayout.collapseMe = function(tiddlerRoot,defaultHeight)
//collapse tiddlerRoot
{
if (!store.getTiddler($(tiddlerRoot).attr("tiddler")))
{
return;
}
custHeight = store.getTiddler($(tiddlerRoot).attr("tiddler")).fields["collapseHeight"] || defaultHeight || this.MAX_HEIGHT;
customHeight = parseInt(custHeight);
//if the post is too big
if (($(tiddlerRoot).children('.viewer').height() > customHeight)&&(customHeight != -1))
{
//limit height of tiddler
$(tiddlerRoot).children('.viewer').css('overflow','hidden').css('height',customHeight);
//create a link
myLink = document.createElement("a");
myLink.href = "javascript:;";
myLink.onclick = function() {return config.macros.BlogLayout.expandClick(tiddlerRoot);};
myLink.innerHTML = "Read More...";
myLink.className = "button";
$("<div />").addClass('readMore').append(myLink).css("margin-top","3px").appendTo($(tiddlerRoot));
}
}
config.macros.BlogLayout.collapseTiddlers = function(defaultHeight)
//collapse all currently open tiddlers
{
$(".tiddler").each(
function() {
if(this.style.display == "none")
{
$(this).attr("collapseMeLater",(defaultHeight)?(defaultHeight+""):"null");
}
else
{
return config.macros.BlogLayout.collapseMe($(this),defaultHeight)
}
}
)
}
config.macros.BlogLayout.expandClick = function(tiddlerToExpand)
{
$(tiddlerToExpand).children(".readMore").css('display','none');
$(tiddlerToExpand).children(".viewer").css('overflow','visible').css('height','');
}
config.macros.BlogLayout.showNextTiddlers = function(clickedLink)
{
var divs = clickedLink.nextSibling;
$(clickedLink).hide();
$(clickedLink).remove();
var stopping = false;
while((!stopping)&&(divs))
{
$(divs).show();
if (divs.className == "showMorePosts")
{
stopping = true;
break;
}
else if ($(divs).attr("collapseMeLater"))
{
if ($(divs).attr("collapseMeLater") == "null")
{
this.collapseMe($(divs));
}
else
{
this.collapseMe($(divs),$(divs).attr("collapseMeLater"));
}
$(divs).removeAttr("collapseMeLater");
}
divs = divs.nextSibling;
}
}
config.macros.BlogLayout.recentTiddlersByTag = function(tagName,maxPosts)
//view all tiddlers with tagName by date order
{
story.closeAllTiddlers(); //clear screen ready for display
$(".showMorePosts").remove();
tiddlers = store.filterTiddlers("[tag["+tagName+"]][sort[-created]]");
var count = 0;
var currMax = maxPosts;
var justChanged = false;
while (count < tiddlers.length)
{
if (count == currMax)
{
$("<div />").addClass("showMorePosts").text("More Posts...").css("display","none").click(function(){return config.macros.BlogLayout.showNextTiddlers(this);}).appendTo("#tiddlerDisplay");
currMax += maxPosts;
}
story.displayTiddler("bottom",tiddlers[count].title,DEFAULT_VIEW_TEMPLATE,false,false);
if (count >= maxPosts)
{
//hide the tiddler
$(story.getTiddler(tiddlers[count].title)).css("display","none");
}
count += 1;
}
//hide all but the first More Posts...
if ($(".showMorePosts").length > 0)
{
$(".showMorePosts")[0].style.display = "block";
}
}
config.macros.BlogLayout.autoRecentTiddlers = function()
{
if(!window.location.hash)
{
this.recentTiddlersByTag(this.POST_TAG_NAME,this.POST_DISPLAY_COUNT);
}
}
config.shadowTiddlers['DefaultTiddlers'] = "[tag["+config.macros.BlogLayout+"]][sort[-created]]";
window.original_restart = window.restart;
window.restart = function()
{
window.original_restart();
if (config.macros.BlogLayout.POST_DISPLAY_COUNT != -1)
{
config.macros.BlogLayout.autoRecentTiddlers(); //call this to ensure number of posts is limited
}
if ((config.macros.BlogLayout.AUTO_SUMMARISE_FRONT_PAGE)&&(!window.location.hash))
{
$(document).ready(function() {config.macros.BlogLayout.collapseTiddlers()});
}
}
//$(document).ready(config.macros.BlogLayout.collapseTiddlers());
config.macros.setCollapseHeightHere ={
handler: function(place,macroName,params,wikifier,paramString,tiddler)
{
dontCollapse = params[0];
if (dontCollapse)
{
tiddler.fields['collapseHeight'] = -1;
}
else
{
tiddler.fields['collapseHeight'] = (place.clientHeight > 0)?(place.clientHeight - 4):(place.offsetHeight - 4);
tiddler.fields['collapseHeight'] += "";
}
}
}
config.macros.collapseThisTiddler ={
handler: function(place,macroName,params,wikifier,paramString,tiddler)
{
if ((params[0])&&(!tiddler.fields['collapseHeight']))
{
tiddler.fields['collapseHeight'] = params[0];
}
config.macros.BlogLayout.collapseMe($(story.getTiddler(tiddler.title)));
}
}
config.macros.BlogLayout.collapseRecentByTag = function(tagName,maxPosts,collapsePosts,defaultHeight)
{
this.recentTiddlersByTag(tagName,maxPosts);
if (collapsePosts)
{
this.collapseTiddlers(defaultHeight);
}
}
//params[0] = name of link
//params[1] = tagName
//params[2] = maxPosts
//params[3] = collapse posts. Values are true/false. default is true.
//params[4] = default value for collapsing posts by
config.macros.recentByTagLink ={
handler: function(place,macroName,params,wikifier,paramString,tiddler)
{
//check parameters supplied
var tagName, maxPosts, collapsePosts, linkName,defaultHeight;
tagName = params[1] || config.macros.BlogLayout.POST_TAG_NAME;
maxPosts = params[2] || config.macros.BlogLayout.POST_DISPLAY_COUNT;
collapsePosts = params[3] || (config.macros.BlogLayout.AUTO_SUMMARISE_FRONT_PAGE?1:0);
defaultHeight = params[4] || config.macros.BlogLayout.MAX_HEIGHT;
linkName = params[0] || tagName;
collapse = (collapsePosts == 1)?true:false;
var tagLink = document.createElement("a");
tagLink.href = "javascript:;";
tagLink.onclick = function() {return config.macros.BlogLayout.collapseRecentByTag(tagName,maxPosts,collapse,defaultHeight);};
tagLink.innerHTML = linkName;
$(place).append(tagLink);
}
}
})(jQuery)
config.shadowTiddlers.StylesheetBlogLayout = ".showMorePosts {margin: 5px 5px 20px 5px; cursor: pointer; width: 100%; text-align: center; border: 1px solid #c0c0c0; }\n" +
".readMore {}\n" +
".readMore .button {}";
store.addNotification("StylesheetBlogLayout",refreshStyles);
{ //# ensure that the plugin is only installed once
version.extensions.BlogLayout = { installed: true }
};
(function($) { //set up alias for jQuery
config.macros.BlogLayout =
{
//*******collapseTiddlers variables********//
AUTO_SUMMARISE_FRONT_PAGE: true, //collapse all default tiddlers on first load (other tiddlers are unaffected)
MAX_HEIGHT: 200, //max height of tiddler content in pixels (default value)
//*******recentPosts variables*************//
POST_DISPLAY_COUNT: 5, //maximum number of posts to display
POST_TAG_NAME: "blog" //all posts that you want displayed in date order need to be tagged with this.
}
config.macros.BlogLayout.collapseMe = function(tiddlerRoot,defaultHeight)
//collapse tiddlerRoot
{
if (!store.getTiddler($(tiddlerRoot).attr("tiddler")))
{
return;
}
custHeight = store.getTiddler($(tiddlerRoot).attr("tiddler")).fields["collapseHeight"] || defaultHeight || this.MAX_HEIGHT;
customHeight = parseInt(custHeight);
//if the post is too big
if (($(tiddlerRoot).children('.viewer').height() > customHeight)&&(customHeight != -1))
{
//limit height of tiddler
$(tiddlerRoot).children('.viewer').css('overflow','hidden').css('height',customHeight);
//create a link
myLink = document.createElement("a");
myLink.href = "javascript:;";
myLink.onclick = function() {return config.macros.BlogLayout.expandClick(tiddlerRoot);};
myLink.innerHTML = "Read More...";
myLink.className = "button";
$("<div />").addClass('readMore').append(myLink).css("margin-top","3px").appendTo($(tiddlerRoot));
}
}
config.macros.BlogLayout.collapseTiddlers = function(defaultHeight)
//collapse all currently open tiddlers
{
$(".tiddler").each(
function() {
if(this.style.display == "none")
{
$(this).attr("collapseMeLater",(defaultHeight)?(defaultHeight+""):"null");
}
else
{
return config.macros.BlogLayout.collapseMe($(this),defaultHeight)
}
}
)
}
config.macros.BlogLayout.expandClick = function(tiddlerToExpand)
{
$(tiddlerToExpand).children(".readMore").css('display','none');
$(tiddlerToExpand).children(".viewer").css('overflow','visible').css('height','');
}
config.macros.BlogLayout.showNextTiddlers = function(clickedLink)
{
var divs = clickedLink.nextSibling;
$(clickedLink).hide();
$(clickedLink).remove();
var stopping = false;
while((!stopping)&&(divs))
{
$(divs).show();
if (divs.className == "showMorePosts")
{
stopping = true;
break;
}
else if ($(divs).attr("collapseMeLater"))
{
if ($(divs).attr("collapseMeLater") == "null")
{
this.collapseMe($(divs));
}
else
{
this.collapseMe($(divs),$(divs).attr("collapseMeLater"));
}
$(divs).removeAttr("collapseMeLater");
}
divs = divs.nextSibling;
}
}
config.macros.BlogLayout.recentTiddlersByTag = function(tagName,maxPosts)
//view all tiddlers with tagName by date order
{
story.closeAllTiddlers(); //clear screen ready for display
$(".showMorePosts").remove();
tiddlers = store.filterTiddlers("[tag["+tagName+"]][sort[-created]]");
var count = 0;
var currMax = maxPosts;
var justChanged = false;
while (count < tiddlers.length)
{
if (count == currMax)
{
$("<div />").addClass("showMorePosts").text("More Posts...").css("display","none").click(function(){return config.macros.BlogLayout.showNextTiddlers(this);}).appendTo("#tiddlerDisplay");
currMax += maxPosts;
}
story.displayTiddler("bottom",tiddlers[count].title,DEFAULT_VIEW_TEMPLATE,false,false);
if (count >= maxPosts)
{
//hide the tiddler
$(story.getTiddler(tiddlers[count].title)).css("display","none");
}
count += 1;
}
//hide all but the first More Posts...
if ($(".showMorePosts").length > 0)
{
$(".showMorePosts")[0].style.display = "block";
}
}
config.macros.BlogLayout.autoRecentTiddlers = function()
{
if(!window.location.hash)
{
this.recentTiddlersByTag(this.POST_TAG_NAME,this.POST_DISPLAY_COUNT);
}
}
config.shadowTiddlers['DefaultTiddlers'] = "[tag["+config.macros.BlogLayout+"]][sort[-created]]";
window.original_restart = window.restart;
window.restart = function()
{
window.original_restart();
if (config.macros.BlogLayout.POST_DISPLAY_COUNT != -1)
{
config.macros.BlogLayout.autoRecentTiddlers(); //call this to ensure number of posts is limited
}
if ((config.macros.BlogLayout.AUTO_SUMMARISE_FRONT_PAGE)&&(!window.location.hash))
{
$(document).ready(function() {config.macros.BlogLayout.collapseTiddlers()});
}
}
//$(document).ready(config.macros.BlogLayout.collapseTiddlers());
config.macros.setCollapseHeightHere ={
handler: function(place,macroName,params,wikifier,paramString,tiddler)
{
dontCollapse = params[0];
if (dontCollapse)
{
tiddler.fields['collapseHeight'] = -1;
}
else
{
tiddler.fields['collapseHeight'] = (place.clientHeight > 0)?(place.clientHeight - 4):(place.offsetHeight - 4);
tiddler.fields['collapseHeight'] += "";
}
}
}
config.macros.collapseThisTiddler ={
handler: function(place,macroName,params,wikifier,paramString,tiddler)
{
if ((params[0])&&(!tiddler.fields['collapseHeight']))
{
tiddler.fields['collapseHeight'] = params[0];
}
config.macros.BlogLayout.collapseMe($(story.getTiddler(tiddler.title)));
}
}
config.macros.BlogLayout.collapseRecentByTag = function(tagName,maxPosts,collapsePosts,defaultHeight)
{
this.recentTiddlersByTag(tagName,maxPosts);
if (collapsePosts)
{
this.collapseTiddlers(defaultHeight);
}
}
//params[0] = name of link
//params[1] = tagName
//params[2] = maxPosts
//params[3] = collapse posts. Values are true/false. default is true.
//params[4] = default value for collapsing posts by
config.macros.recentByTagLink ={
handler: function(place,macroName,params,wikifier,paramString,tiddler)
{
//check parameters supplied
var tagName, maxPosts, collapsePosts, linkName,defaultHeight;
tagName = params[1] || config.macros.BlogLayout.POST_TAG_NAME;
maxPosts = params[2] || config.macros.BlogLayout.POST_DISPLAY_COUNT;
collapsePosts = params[3] || (config.macros.BlogLayout.AUTO_SUMMARISE_FRONT_PAGE?1:0);
defaultHeight = params[4] || config.macros.BlogLayout.MAX_HEIGHT;
linkName = params[0] || tagName;
collapse = (collapsePosts == 1)?true:false;
var tagLink = document.createElement("a");
tagLink.href = "javascript:;";
tagLink.onclick = function() {return config.macros.BlogLayout.collapseRecentByTag(tagName,maxPosts,collapse,defaultHeight);};
tagLink.innerHTML = linkName;
$(place).append(tagLink);
}
}
})(jQuery)
config.shadowTiddlers.StylesheetBlogLayout = ".showMorePosts {margin: 5px 5px 20px 5px; cursor: pointer; width: 100%; text-align: center; border: 1px solid #c0c0c0; }\n" +
".readMore {}\n" +
".readMore .button {}";
store.addNotification("StylesheetBlogLayout",refreshStyles);