The last time I was talking code (I mean, besides the little example usage snippet when I was announcing TGOpenIDLogin, in between my reporting back about the GeekDinner, the Western Cape Linux User Group  meeting, the Cape Town Python User Group meeting, and so forth), I was showing how I converted the Wordpress Sociable plugin to a TurboGears widget (innovatively named TGSociable).

The ultimate purpose of this was to make a plugin for Gibe (my little weblog engine) which would add the sociable icons with the correct URLs to blog entry pages, without having to put anything sociable-specific in the code.

When writing the comment format "plugin system" so that Gibe could use something other than the built-in TinyMCE, I used pkg_resources to create an "entry point" so that other packages could provide functionality to Gibe, and Gibe would know about them without anything but installing the package.

So, I started with the near-simplest possible Plugin class:

class Plugin(object):
    def post_top_widgets(self, post, widgets, context):
        # widgets to put above the post
        pass

I've been trying to make Gibe, my not-yet-released web log software, less and less about my needs and more about being able to cope with others' needs.  That means ripping out a lot of stuff that's custom to me, but somehow making it still available on my own web log.  I've made some progress with that - I've made the comment format customisable, as well as found a solution to add custom scripts like Google Analytics and a syntax highlighter.  It also means making it easy to add new stuff, which is what my experiment over the weekend was - to add a set of social bookmarking links to posts, so that people can post interesting links to places like Digg, Reddit, del.icio.us, and South Africa's Muti.

The sociable Wordpress plugin is one I've seen around, and I figured I might as well see how it was put together.  At its core, it is a list of social bookmarking sites and how to post to them:

$sociable_known_sites = Array(

        'blinkbits' => Array(
                'favicon' => 'blinkbits.png',
                'url' => 'http://www.blinkbits.com/bookmarklets/save.php?v=1&source_url=PERMALINK&title=TITLE&body=TITLE',
        ),

        'BlinkList' => Array(
                'favicon' => 'blinklist.png',
                'url' => 'http://www.blinklist.com/index.php?Action=Blink/addblink.php&Url=PERMALINK&Title=TITLE',
                'description' => 'Description',
        ),

        'BlogMemes' => Array(
                'favicon' => 'blogmemes.png',
                'url' => 'http://www.blogmemes.net/post.php?url=PERMALINK&title=TITLE',
        ),
        ...
);

First order of business was to convert that into a Python data structure.  Besides taking a detour via the itertools module just for fun, it didn't take too much effort:

#!/usr/bin/env python

import sys
import itertools
import re

if len(sys.argv) > 1:
    fp = open(sys.argv[1])
else:
    fp = sys.stdin

lines = fp.readlines()

beg = re.compile('^\$sociable_known_sites')
end = re.compile('^\);')
def find_beginning(item):
    return not beg.match(item)

def find_end(item):
    return not end.match(item)
    
def dropbefore(lines):
    for line in itertools.dropwhile(find_beginning, lines):
        yield line

def dropafter(lines):
    for line in itertools.takewhile(find_end, lines):
        yield line

start_item = re.compile(r"""^\t('[^']*') => Array\(""")
favicon_item = re.compile(r"""\t\t('favicon') => '([^']*)',""")
mid_item = re.compile(r"""\t\t('[^']*') => ('[^']*'),""")
end_item = re.compile(r"""\t\),""")

def start_item_handle(m):
    print "\t%s: {" % (m.groups()[0],)

def favicon_item_handle(m):
    k, v = m.groups()
    print "\t\t%s: turbogears.url('/tg_widgets/tgsociable/images/%s')," % (k, v)

def mid_item_handle(m):
    k, v = m.groups()
    v = v.replace("&", "&")
    print "\t\t%s: %s," % (k, v)

def end_item_handle(m):
    print "\t},"

handlers = [
    (start_item, start_item_handle),
    (favicon_item, favicon_item_handle),
    (mid_item, mid_item_handle),
    (end_item, end_item_handle),
]

def handle_lines(lines):
    for line in dropafter(dropbefore(lines)):
        line = line.rstrip()
        for regex, handler in handlers:
            m = regex.search(line)
            if m:
                handler(m)
                break

print "import turbogears"
print "all_sites = {"
handle_lines(lines)
print "}"

This outputs the expected Python code (using turbogears.url in a not-to-correct way...):

import turbogears
all_sites = {
        'blinkbits': {
                'favicon': turbogears.url('/tg_widgets/tgsociable/images/blinkbits.png'),
                'url': 'http://www.blinkbits.com/bookmarklets/save.php?v=1&source_url=PERMALINK&title=TITLE&body=TITLE',
        },
        'BlinkList': {
                'favicon': turbogears.url('/tg_widgets/tgsociable/images/blinklist.png'),
                'url': 'http://www.blinklist.com/index.php?Action=Blink/addblink.php&Url=PERMALINK&Title=TITLE',
        },
        ...
} 

Next, displaying the actual HTML.  At this point, the PHP is not conducive to programmatic Python conversion:

        $html .= "\n<div class=\"sociable\">\n<span class=\"sociable_tagline\">\n";
        $html .= get_option("sociable_tagline");
        $html .= "\n\t<span>" . __("These icons link to social bookmarking sites where readers can share and discover new web pages.", 'sociable') . "</span>";
        $html .= "\n</span>\n<ul>\n";

        foreach($display as $sitename) {
                // if they specify an unknown or inactive site, ignore it
                if (!in_array($sitename, $active_sites))
                        continue;

                $site = $sociable_known_sites[$sitename];
                $html .= "\t<li>";

                $url = $site['url'];
                $url = str_replace('PERMALINK', $permalink, $url);
                $url = str_replace('TITLE', $title, $url);
                $url = str_replace('RSS', $rss, $url);
                $url = str_replace('BLOGNAME', $blogname, $url);
                $url = str_replace('VERSION', $sociable_version, $url);

                $html .= "<a href=\"$url\" title=\"$sitename\"";
                if ($site['description'])
                    $html .= " onfocus=\"sociable_description_link(this, '{$site['description']}')\"";
                $html .= ">";
                $html .= "<img src=\"$imagepath{$site['favicon']}\" title=\"$sitename\" alt=\"$sitename\" class=\"sociable-hovers";
                if ($site['class'])
                    $html .= " sociable_{$site['class']}";
                $html .= "\" />";
                $html .= "</a></li>\n";
        }

        $html .= "</ul>\n</div>\n";

        return $html;

This turns out a lot prettier thanks to the templating engine (Kid, in this case):

class SociableWidget(widgets.Widget):
    template = """
<div class="sociable" xmlns:py="http://purl.org/kid/ns#">
  <span class="sociable_tagline">
    <strong py:content="sociable_tagline">get_option("sociable_tagline");</strong>
    <span py:content="sociable_tagline_description">_("These icons link to social bookmarking sites where readers can share and discover new web pages.", 'sociable')</span>
  </span>

  <ul>  
    <li py:for="site in sites">
      <a py:attrs="site['anchor_attrs']">
        <img py:attrs="site['img_attrs']" />
      </a>
    </li>
  </ul>
</div>
    """

There's a bit more work to make it automatically include the right CSS and Javascript, and to make it configurable (including allowing for extra sites to be added) from the caller of the widget:

    css = [
        widgets.CSSLink("tgsociable", "css/sociable.css"),
    ]
    javascript = [
        widgets.JSLink("tgsociable", "javascript/description_selection.js"),
    ]

    params_doc = {
        'active_sites' : 'Sites to display sociable icons for',
        'sociable_tagline' : 'Tag line heading',
        'sociable_tagline_description' : 'Tag line explanation',
        'extra_sites' : 'Sites not in the existing sites list that you want to use',
    }
    params = params_doc.keys()

    active_sites = ["Digg", "Reddit", "del.icio.us"]
    sociable_tagline = "Share and Enjoy:"
    sociable_tagline_description = "These icons link to social bookmarking sites where readers can share and discover new web pages."
    extra_sites = {
# Example:
#        'muti': {
#                'favicon': 'http://muti.co.za/images/favicon.ico',
#                'url': 'http://muti.co.za/submit?url=PERMALINK&title=TITLE',
#        },
    }

    def update_params(self, d):
        super(SociableWidget, self).update_params(d)
        active_sites = d['active_sites']
        d['sites'] = []

        my_all_sites = all_sites.copy()
        my_all_sites.update(d['extra_sites'])

        for sitename in active_sites:
            if sitename not in my_all_sites:
                continue

            site = my_all_sites[sitename]

            url = site['url'];
            url = url.replace('PERMALINK', d['post_url'])
            url = url.replace('TITLE', d['post_title'])
            url = url.replace('RSS', d['blog_rss'])
            url = url.replace('BLOGNAME', d['blog_name'])
            sociable_version = "2.0"
            url = url.replace('VERSION', sociable_version)

            anchor_attrs = {}
            anchor_attrs['href'] = url
            if 'description' in site:
                anchor_attrs['onfocus'] = "sociable_description_link(this, '%s')" % (site['description'],)
            img_attrs = {}
            img_attrs['src'] = site['favicon']
            img_attrs['title'] = sitename
            img_attrs['alt'] = sitename
            img_attrs['class'] = "sociable_hovers"
            if 'class' in site:
                img_attrs['class'] += " " + site['class']
            d['sites'].append(dict(anchor_attrs = anchor_attrs, img_attrs = img_attrs))

To use the widget is trivial. First, create the widget instance:

# No configuration - shows Digg, Reddit, and del.icio.us
widget = SociableWidget()

# Choose which sites to show:
widget = SociableWidget(active_sites = [#39;del.icio.us'])

# Add extra sites of your own
extra_sites = {
    'muti': {
    'favicon': 'http://muti.co.za/images/favicon.ico',
    'url': 'http://muti.co.za/submit?url=PERMALINK&title=TITLE',
}
widget = SociableWidget(extra_sites = extra_sites, active_sites = ['muti', 'del.icio.us'])

You can then pass this widget into a template. In the template, you need to provide the post URL, post title, RSS feed, and blog name when you display the widget - something like:

<span py:replace="ET(widget.display(post_url=tg.base_url, 
    post_title=post.title, blog_name = blog.name,
    blog_rss = tg.url_for('rss2.0.xml')))" />

At this point, this is just a plain TurboGears widget, that can be used in any TurboGears application (converting it to ToscaWidgets would be pretty trivial, and then it'll also be usable from Pylons and other platforms that are supported). Hooking it up so that it automatically gets displayed in Gibe posts took a bit more work, and I'll post about that later.

You can download the TGSociable widget on my TGSociable page.  You can also follow other posts about TGSociable here - I'll use the tag tgsociable.