diff --git a/docs/templates.html b/docs/templates.html index 1eb9d7a..b9fd9c1 100644 --- a/docs/templates.html +++ b/docs/templates.html @@ -167,5 +167,18 @@ a planet:format attribute containing the referenced date formatted according to the [planet] date_format specified in the configuration + +

genshi

+

Genshi approaches the power of XSLT, but with a syntax that many Python +programmers find more natural, succinct and expressive. Genshi templates +have access to the full range of feedparser values, with the following additions:

+ diff --git a/planet/shell/_genshi.py b/planet/shell/_genshi.py index c08cd81..595d6e9 100644 --- a/planet/shell/_genshi.py +++ b/planet/shell/_genshi.py @@ -1,8 +1,51 @@ from StringIO import StringIO +from xml.sax.saxutils import escape -from genshi.input import XMLParser +from genshi.input import HTMLParser, XMLParser from genshi.template import Context, MarkupTemplate +subscriptions = [] +feed_types = [ + 'application/atom+xml', + 'application/rss+xml', + 'application/rdf+xml' +] + +def norm(value): + """ Convert to Unicode """ + if hasattr(value,'items'): + return dict([(norm(n),norm(v)) for n,v in value.items()]) + + try: + return value.decode('utf-8') + except: + return value.decode('iso-8859-1') + +def find_config(config, feed): + # match based on self link + for link in feed.links: + if link.has_key('rel') and link.rel=='self': + if link.has_key('type') and link.type in feed_types: + if link.has_key('href') and link.href in subscriptions: + return norm(dict(config.parser.items(link.href))) + + # match based on name + for sub in subscriptions: + if config.parser.has_option(sub, 'name') and \ + norm(config.parser.get(sub, 'name')) == feed.planet_name: + return norm(dict(config.parser.items(sub))) + + return {} + +def streamify(text,bozo): + """ add a .stream to a _detail textConstruct """ + if text.type == 'text/plain': + text.stream = HTMLParser(StringIO(escape(text.value))) + elif text.type == 'text/html' or bozo != 'false': + text.stream = HTMLParser(StringIO(text.value)) + else: + text.stream = XMLParser(StringIO("
%s
" % text.value)) + def run(script, doc, output_file=None, options={}): """ process an Genshi template """ @@ -12,7 +55,68 @@ def run(script, doc, output_file=None, options={}): tmpl = MarkupTemplate(tmpl_fileobj, script) tmpl_fileobj.close() - context.push({'input':XMLParser(StringIO(doc))}) + if not output_file: + # filter + context.push({'input':XMLParser(StringIO(doc))}) + else: + # template + import time + from planet import config,feedparser + from planet.spider import filename + + # gather a list of subscriptions, feeds + global subscriptions + feeds = [] + sources = config.cache_sources_directory() + for sub in config.subscriptions(): + data=feedparser.parse(filename(sources,sub)) + data.feed.config = norm(dict(config.parser.items(sub))) + if data.feed.has_key('link'): + feeds.append((data.feed.config.get('name',''),data.feed)) + subscriptions.append(norm(sub)) + feeds.sort() + + # annotate each entry + new_date_format = config.new_date_format() + vars = feedparser.parse(StringIO(doc)) + vars.feeds = [value for name,value in feeds] + last_feed = None + last_date = None + for entry in vars.entries: + entry.source.config = find_config(config, entry.source) + + # add new_feed and new_date fields + entry.new_feed = entry.source.id + entry.new_date = date = None + if entry.has_key('published_parsed'): date=entry.published_parsed + if entry.has_key('updated_parsed'): date=entry.updated_parsed + if date: entry.new_date = time.strftime(new_date_format, date) + + # remove new_feed and new_date fields if not "new" + if entry.new_date == last_date: + entry.new_date = None + if entry.new_feed == last_feed: + entry.new_feed = None + else: + last_feed = entry.new_feed + elif entry.new_date: + last_date = entry.new_date + last_feed = None + + # add streams for all text constructs + for key in entry.keys(): + if key.endswith("_detail") and entry[key].has_key('type') and \ + entry[key].has_key('value'): + streamify(entry[key],entry.source.planet_bozo) + if entry.has_key('content'): + for content in entry.content: + streamify(content,entry.source.planet_bozo) + + # add cumulative feed information to the Genshi context + vars.feed.config = dict(config.parser.items('Planet',True)) + context.push(vars) + + # apply template output=tmpl.generate(context).render('xml') if output_file: diff --git a/tests/data/apply/config-genshi.ini b/tests/data/apply/config-genshi.ini new file mode 100644 index 0000000..28c9c2f --- /dev/null +++ b/tests/data/apply/config-genshi.ini @@ -0,0 +1,21 @@ +[Planet] +output_theme = genshi_fancy +output_dir = tests/work/apply +name = test planet +cache_directory = tests/work/spider/cache + +bill_of_materials: + images/#{face} + +[tests/data/spider/testfeed0.atom] +name = not found + +[tests/data/spider/testfeed1b.atom] +name = one +face = jdub.png + +[tests/data/spider/testfeed2.atom] +name = two + +[tests/data/spider/testfeed3.rss] +name = three diff --git a/tests/test_apply.py b/tests/test_apply.py index e151fba..6ed8186 100644 --- a/tests/test_apply.py +++ b/tests/test_apply.py @@ -47,8 +47,15 @@ class ApplyTest(unittest.TestCase): self.assertEqual(12, content) self.assertEqual(3, lang) - def test_apply_fancy(self): + def test_apply_classic_fancy(self): config.load(configfile % 'fancy') + self.apply_fancy() + + def test_apply_genshi_fancy(self): + config.load(configfile % 'genshi') + self.apply_fancy() + + def apply_fancy(self): splice.apply(self.feeddata) # verify that selected files are there @@ -93,3 +100,9 @@ except ImportError: logger.warn("xsltproc is not available => can't test XSLT templates") for method in dir(ApplyTest): if method.startswith('test_'): delattr(ApplyTest,method) + +import test_filter_genshi +for method in dir(test_filter_genshi.GenshiFilterTests): + if method.startswith('test_'): break +else: + delattr(ApplyTest,'test_apply_genshi_fancy') diff --git a/themes/asf/default.css b/themes/asf/default.css index 18a3bf7..64d7076 100644 --- a/themes/asf/default.css +++ b/themes/asf/default.css @@ -208,7 +208,7 @@ body > h1 { text-align: right; } -#body h2 { +#body > h2 { text-transform: none; font-size: medium; color: #333; diff --git a/themes/genshi_fancy/config.ini b/themes/genshi_fancy/config.ini new file mode 100644 index 0000000..d5a127d --- /dev/null +++ b/themes/genshi_fancy/config.ini @@ -0,0 +1,20 @@ +# This theme reimplements the classic "fancy" htmltmpl using genshi + +[Planet] +template_files: + atom.xml.xslt + foafroll.xml.xslt + index.html.genshi + opml.xml.xslt + rss10.xml.tmpl + rss20.xml.tmpl + +template_directories: + ../common + ../classic_fancy + +bill_of_materials: + planet.css + images/feed-icon-10x10.png + images/logo.png + images/venus.png diff --git a/themes/genshi_fancy/index.html.genshi b/themes/genshi_fancy/index.html.genshi new file mode 100644 index 0000000..fe26934 --- /dev/null +++ b/themes/genshi_fancy/index.html.genshi @@ -0,0 +1,95 @@ + + + + + +$feed.config.name + + + + + + + +

$feed.config.name

+ + + +
+

$entry.new_date

+ +
+

$entry.source.config.name

+ + + +

$entry.title_detail.stream

+ +
+
+${entry.content[0].stream} +${entry.summary_detail.stream} +
+ +

by $entry.author_detail.name at $entry.updated

+
+ +
+
+ +
+ + + + + diff --git a/themes/genshi_fancy/planet.css b/themes/genshi_fancy/planet.css new file mode 100644 index 0000000..05653c0 --- /dev/null +++ b/themes/genshi_fancy/planet.css @@ -0,0 +1,150 @@ +body { + border-right: 1px solid black; + margin-right: 200px; + + padding-left: 20px; + padding-right: 20px; +} + +h1 { + margin-top: 0px; + padding-top: 20px; + + font-family: "Bitstream Vera Sans", sans-serif; + font-weight: normal; + letter-spacing: -2px; + text-transform: lowercase; + text-align: right; + + color: grey; +} + +.admin { + text-align: right; +} + +h2 { + font-family: "Bitstream Vera Sans", sans-serif; + font-weight: normal; + color: #200080; + + margin-left: -20px; +} + +h3 { + font-family: "Bitstream Vera Sans", sans-serif; + font-weight: normal; + + background-color: #a0c0ff; + border: 1px solid #5080b0; + + padding: 4px; +} + +h3 a { + text-decoration: none; + color: inherit; +} + +h4 { + font-family: "Bitstream Vera Sans", sans-serif; + font-weight: bold; +} + +h4 a { + text-decoration: none; + color: inherit; +} + +img.face { + float: right; + margin-top: -3em; +} + +.entry { + margin-bottom: 2em; +} + +.entry .date { + font-family: "Bitstream Vera Sans", sans-serif; + color: grey; +} + +.entry .date a { + text-decoration: none; + color: inherit; +} + +.sidebar { + position: absolute; + top: 0px; + right: 0px; + width: 200px; + + margin-left: 0px; + margin-right: 0px; + padding-right: 0px; + + padding-top: 20px; + padding-left: 0px; + + font-family: "Bitstream Vera Sans", sans-serif; + font-size: 85%; +} + +.sidebar h2 { + font-size: 110%; + font-weight: bold; + color: black; + + padding-left: 5px; + margin-left: 0px; +} + +.sidebar ul { + padding-left: 1em; + margin-left: 0px; + + list-style-type: none; +} + +.sidebar ul li:hover { + color: grey; +} + +.sidebar ul li a { + text-decoration: none; +} + +.sidebar ul li a:hover { + text-decoration: underline; +} + +.sidebar ul li a img { + border: 0; +} + +.sidebar p { + border-top: 1px solid grey; + margin-top: 30px; + padding-top: 10px; + + padding-left: 5px; +} + +.sidebar .message { + cursor: help; + border-bottom: 1px dashed red; +} + +.sidebar a.message:hover { + cursor: help; + background-color: #ff0000; + color: #ffffff !important; + text-decoration: none !important; +} + +a:hover { + text-decoration: underline !important; + color: blue !important; +}