bengillies.net

a blog by Ben Gillies

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):

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)

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);

}

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
 

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!");
        }
}
};

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

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):

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)
 

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

BlogLayout

Name:BlogLayout
Description:adds a blog like view and tiddler summary view to TiddlyWiki
AuthorBenGillies
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
LicenseBSD 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);