Genshi Themes

This commit is contained in:
Sam Ruby 2007-04-24 16:13:15 -04:00
parent 9d97a03d33
commit 1e3ca39733
8 changed files with 420 additions and 4 deletions

View File

@ -167,5 +167,18 @@ a <code>planet:format</code> attribute containing the referenced date
formatted according to the <code>[planet] date_format</code> specified
in the configuration</li>
</ul>
<h3>genshi</h3>
<p>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 <a href="http://feedparser.org/docs/reference.html">feedparser</a> values, with the following additions:</p>
<ul>
<li>In addition to a <code>feed</code> element which describes the feed
for your planet, there is also a <code>feeds</code> element which contains
the description for each subscription.</li>
<li>All <code>feed</code>, <code>feeds</code>, and <code>source</code> elements have a child <code>config</code> element which contains the config.ini entries associated with that feed.</li>
<li>All text construct detail elements (<code>subtitle</code>, <code>rights</code>, <code>title</code>, <code>summary</code>, <code>content</code>) also contain a <code>stream</code> element which contains the value as a Genshi stream.</li>
<li>Each of the <code>entries</code> has a <code>new_date</code> and <code>new_feed</code> value which indicates if this entry's date or feed differs from the preceeding entry.</li>
</ul>
</body>
</html>

View File

@ -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("<div>%s</div>" % 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:

View File

@ -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

View File

@ -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')

View File

@ -208,7 +208,7 @@ body > h1 {
text-align: right;
}
#body h2 {
#body > h2 {
text-transform: none;
font-size: medium;
color: #333;

View File

@ -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

View File

@ -0,0 +1,95 @@
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/">
<!--!
### Fancy Planet HTML template, converted to Genshi.
###
### When combined with the stylesheet and images in the output/ directory
### of the Planet source, this gives you a much prettier result than the
### default examples template and demonstrates how to use the config file
### to support things like faces
###
### For documentation on the more boring template elements, see
### http://www.intertwingly.net/code/venus/docs/templates.html
-->
<head>
<title>$feed.config.name</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="generator" content="$feed.generator"/>
<link rel="stylesheet" href="planet.css" type="text/css"/>
<link py:for="link in feed.links"
py:if="link.type in ['application/atom+xml','application/rss+xml']"
href="$link.href" rel="alternate" title="$link.title" type="$link.type"/>
</head>
<body>
<h1>$feed.config.name</h1>
<py:for each="entry in entries">
<div class="channelgroup" py:strip="not entry.new_date">
<h2 py:if="entry.new_date">$entry.new_date</h2>
<div class="entrygroup" py:strip="not entry.new_feed">
<h3 py:if="entry.new_feed"><a href="$entry.link" title="$entry.source.title">$entry.source.config.name</a></h3>
<img py:if="entry.new_feed and entry.source.config.face" class="face" src="images/$entry.source.config.face" width="$entry.source.config.facewidth" height="$entry.source.config.faceheight" alt=""/>
<h4 py:if="entry.title" lang="$entry.title_detail.language"><a href="$entry.link">$entry.title_detail.stream</a></h4>
<div class="entry">
<div class="content" py:choose="">
<py:when test="entry.content">${entry.content[0].stream}</py:when>
<py:when test="entry.summary_detail">${entry.summary_detail.stream}</py:when>
</div>
<p class="date"><py:if test="entry.author_detail and entry.author_detail.name">by $entry.author_detail.name at </py:if>$entry.updated</p>
</div>
</div>
</div>
</py:for>
<div class="sidebar">
<img src="images/logo.png" width="136" height="136" alt=""/>
<h2>Subscriptions</h2>
<ul>
<li py:for="feed in feeds">
<a py:for="link in feed.links" py:if="link.rel == 'self' and
link.type in ['application/atom+xml','application/rss+xml']"
href="$link.href" title="subscribe"><img src="images/feed-icon-10x10.png" alt="(feed)"/></a>
<py:choose>
<a py:when="feed.planet_message" href="$feed.link" class="message" title="$feed.planet_message">$feed.config.name</a>
<a py:otherwise="1" href="$feed.link" title="$feed.title">$feed.config.name</a>
</py:choose>
</li>
</ul>
<p>
<strong>Last updated:</strong><br/>
$feed.updated<br/>
<em>All times are UTC.</em><br/>
<br/>
Powered by:<br/>
<a href="http://intertwingly.net/code/venus/"><img src="images/venus.png" width="80" height="15" alt="Planet Venus" border="0"/></a>
</p>
<p>
<h2>Planetarium:</h2>
<ul>
<li><a href="http://www.planetapache.org/">Planet Apache</a></li>
<li><a href="http://planet.debian.net/">Planet Debian</a></li>
<li><a href="http://planet.freedesktop.org/">Planet freedesktop.org</a></li>
<li><a href="http://planet.gnome.org/">Planet GNOME</a></li>
<li><a href="http://planetsun.org/">Planet Sun</a></li>
<li><a href="http://fedora.linux.duke.edu/fedorapeople/">Fedora People</a></li>
<li><a href="http://www.planetplanet.org/">more...</a></li>
</ul>
</p>
</div>
</body>
</html>

View File

@ -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;
}