From 7cbc2e31bfef36ef3bb01b8c25dad82a7f26d638 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Mon, 21 Aug 2006 09:43:09 -0400 Subject: [PATCH] Theme support --- planet/__init__.py | 3 + planet/config.py | 57 ++- planet/reconstitute.py | 7 +- planet/splice.py | 90 +++- runtests.py | 7 +- splice.py | 5 +- tests/capture.py | 42 ++ tests/data/apply/config.ini | 17 + tests/data/apply/feed.xml | 215 +++++++++ tests/data/config/themed.ini | 14 + tests/data/spider/config.ini | 2 +- tests/data/splice/cache/example.com,3 | 2 +- tests/data/splice/cache/example.com,4 | 2 +- .../planet.intertwingly.net,2006,testfeed1,1 | 4 +- .../planet.intertwingly.net,2006,testfeed1,2 | 6 +- .../planet.intertwingly.net,2006,testfeed1,3 | 4 +- .../planet.intertwingly.net,2006,testfeed1,4 | 4 +- .../planet.intertwingly.net,2006,testfeed2,1 | 4 +- .../planet.intertwingly.net,2006,testfeed2,2 | 4 +- .../planet.intertwingly.net,2006,testfeed2,3 | 4 +- .../planet.intertwingly.net,2006,testfeed2,4 | 4 +- .../planet.intertwingly.net,2006,testfeed3,1 | 2 +- .../planet.intertwingly.net,2006,testfeed3,2 | 2 +- .../sources/tests,data,spider,testfeed1b.atom | 2 +- .../sources/tests,data,spider,testfeed2.atom | 2 +- tests/data/splice/config.ini | 1 - tests/test_apply.py | 33 ++ tests/test_config.py | 6 +- tests/test_spider.py | 11 +- tests/test_themes.py | 58 +++ themes/asf/config.ini | 19 + themes/asf/default.css | 429 ++++++++++++++++++ themes/asf/index.html.xslt | 153 +++++++ themes/asf/personalize.js | 220 +++++++++ themes/common/atom.xml.xslt | 25 + themes/common/foafroll.xml.xslt | 39 ++ themes/common/images/feed-icon-10x10.png | Bin 0 -> 469 bytes themes/common/images/foaf.png | Bin 0 -> 1393 bytes themes/common/images/opml.png | Bin 0 -> 804 bytes themes/common/images/planet.png | Bin 0 -> 426 bytes themes/common/opml.xml.xslt | 25 + 41 files changed, 1463 insertions(+), 61 deletions(-) create mode 100755 tests/capture.py create mode 100644 tests/data/apply/config.ini create mode 100644 tests/data/apply/feed.xml create mode 100644 tests/data/config/themed.ini create mode 100644 tests/test_apply.py create mode 100644 tests/test_themes.py create mode 100644 themes/asf/config.ini create mode 100644 themes/asf/default.css create mode 100644 themes/asf/index.html.xslt create mode 100644 themes/asf/personalize.js create mode 100644 themes/common/atom.xml.xslt create mode 100644 themes/common/foafroll.xml.xslt create mode 100644 themes/common/images/feed-icon-10x10.png create mode 100644 themes/common/images/foaf.png create mode 100644 themes/common/images/opml.png create mode 100644 themes/common/images/planet.png create mode 100644 themes/common/opml.xml.xslt diff --git a/planet/__init__.py b/planet/__init__.py index d66a958..8b9b982 100644 --- a/planet/__init__.py +++ b/planet/__init__.py @@ -2,6 +2,9 @@ xmlns = 'http://planet.intertwingly.net/' logger = None +import config +config.__init__() + def getLogger(level): """ get a logger with the specified log level """ global logger diff --git a/planet/config.py b/planet/config.py index 2e657af..2191d15 100644 --- a/planet/config.py +++ b/planet/config.py @@ -11,7 +11,7 @@ Usage: config.load('config.ini') # administrative / structural information - print config.templates() + print config.template_files() print config.feeds() # planet wide configuration @@ -37,6 +37,7 @@ def __init__(): """define the struture of an ini file""" import config + # underlying implementation def get(section, option, default): if section and parser.has_option(section, option): return parser.get(section, option) @@ -49,6 +50,10 @@ def __init__(): setattr(config, name, lambda default=default: get(None,name,default)) planet_predefined_options.append(name) + def define_planet_list(name): + setattr(config, name, lambda : get(None,name,'').split()) + planet_predefined_options.append(name) + def define_tmpl(name, default): setattr(config, name, lambda section, default=default: get(section,name,default)) @@ -63,25 +68,57 @@ def __init__(): define_planet('cache_directory', "cache") define_planet('log_level', "WARNING") define_planet('feed_timeout', 20) + define_planet('date_format', "%B %d, %Y %I:%M %p") + define_planet('generator', 'Venus') + define_planet('generator_uri', 'http://intertwingly.net/code/venus/') + define_planet('owner_name', 'Anonymous Coward') + define_planet('owner_email', '') + define_planet('output_theme', '') + define_planet('output_dir', 'output') + + define_planet_list('template_files') + define_planet_list('bill_of_materials') + define_planet_list('template_directories') # template options define_tmpl_int('days_per_page', 0) define_tmpl_int('items_per_page', 60) define_tmpl('encoding', 'utf-8') - # prevent re-initialization - setattr(config, '__init__', lambda: None) - -def load(file): +def load(config_file): """ initialize and load a configuration""" - __init__() global parser parser = ConfigParser() - parser.read(file) + parser.read(config_file) -def template_files(): - """ list the templates defined """ - return parser.get('Planet','template_files').split(' ') + if parser.has_option('Planet', 'output_theme'): + theme = parser.get('Planet', 'output_theme') + for path in ("", os.path.join(sys.path[0],'themes')): + theme_dir = os.path.join(path,theme) + theme_file = os.path.join(theme_dir,'config.ini') + if os.path.exists(theme_file): + # initial search list for theme directories + dirs = [theme_dir] + if parser.has_option('Planet', 'template_directories'): + dirs.insert(0,parser.get('Planet', 'template_directories')) + + # read in the theme + parser = ConfigParser() + parser.read(theme_file) + + # complete search list for theme directories + if parser.has_option('Planet', 'template_directories'): + dirs += [os.path.join(theme_dir,dir) for dir in + parser.get('Planet', 'template_directories').split()] + + # merge configurations, allowing current one to override theme + parser.read(config_file) + parser.set('Planet', 'template_directories', ' '.join(dirs)) + break + else: + import config, planet + log = planet.getLogger(config.log_level()) + log.error('Unable to find theme %s', theme) def cache_sources_directory(): if parser.has_option('Planet', 'cache_sources_directory'): diff --git a/planet/reconstitute.py b/planet/reconstitute.py index 2f57726..cc72054 100644 --- a/planet/reconstitute.py +++ b/planet/reconstitute.py @@ -18,7 +18,7 @@ from xml.sax.saxutils import escape from xml.dom import minidom from BeautifulSoup import BeautifulSoup from xml.parsers.expat import ExpatError -import planet +import planet, config illegal_xml_chars = re.compile("[\x01-\x08\x0B\x0C\x0E-\x1F]") @@ -29,6 +29,7 @@ def createTextElement(parent, name, value): xelement = xdoc.createElement(name) xelement.appendChild(xdoc.createTextNode(value)) parent.appendChild(xelement) + return xelement def invalidate(c): """ replace invalid characters """ @@ -98,7 +99,9 @@ def date(xentry, name, parsed): """ insert a date-formated element into the entry """ if not parsed: return formatted = time.strftime("%Y-%m-%dT%H:%M:%SZ", parsed) - createTextElement(xentry, name, formatted) + xdate = createTextElement(xentry, name, formatted) + formatted = time.strftime(config.date_format(), parsed) + xdate.setAttribute('planet:format', formatted) def author(xentry, name, detail): """ insert an author-like element into the entry """ diff --git a/planet/splice.py b/planet/splice.py index 55f9739..815326a 100644 --- a/planet/splice.py +++ b/planet/splice.py @@ -1,8 +1,8 @@ """ Splice together a planet from a cache of feed entries """ -import glob, os +import glob, os, time, shutil from xml.dom import minidom import planet, config, feedparser, reconstitute -from reconstitute import createTextElement +from reconstitute import createTextElement, date from spider import filename def splice(configFile): @@ -11,6 +11,7 @@ def splice(configFile): config.load(configFile) log = planet.getLogger(config.log_level()) + log.info("Loading cached data") cache = config.cache_directory() dir=[(os.stat(file).st_mtime,file) for file in glob.glob(cache+"/*") if not os.path.isdir(file)] @@ -18,17 +19,20 @@ def splice(configFile): dir.reverse() items=max([config.items_per_page(templ) - for templ in config.template_files()]) + for templ in config.template_files() or ['Planet']]) doc = minidom.parseString('') feed = doc.documentElement - # insert Google/LiveJournal's noindex - feed.setAttribute('indexing:index','no') - feed.setAttribute('xmlns:indexing','urn:atom-extension:indexing') - # insert feed information createTextElement(feed, 'title', config.name()) + date(feed, 'updated', time.gmtime()) + gen = createTextElement(feed, 'generator', config.generator()) + gen.setAttribute('uri', config.generator_uri()) + author = doc.createElement('author') + createTextElement(author, 'name', config.owner_name()) + createTextElement(author, 'email', config.owner_email()) + feed.appendChild(author) # insert entry information for mtime,file in dir[:items]: @@ -47,3 +51,75 @@ def splice(configFile): feed.appendChild(xdoc.documentElement) return doc + +def apply(doc): + output_dir = config.output_dir() + if not os.path.exists(output_dir): os.makedirs(output_dir) + log = planet.getLogger(config.log_level()) + + try: + # if available, use the python interface to libxslt + import libxml2 + import libxslt + dom = libxml2.parseDoc(doc) + docfile = None + except: + # otherwise, use the command line interface + dom = None + import warnings + warnings.simplefilter('ignore', RuntimeWarning) + docfile = os.tmpnam() + file = open(docfile,'w') + file.write(doc) + file.close() + + # Go-go-gadget-template + for template_file in config.template_files(): + for template_dir in config.template_directories(): + template_resolved = os.path.join(template_dir, template_file) + if os.path.exists(template_resolved): break + else: + log.error("Unable to locate template %s", template_file) + continue + + base,ext = os.path.splitext(os.path.basename(template_resolved)) + if ext != '.xslt': + log.warning("Skipping template %s", template_resolved) + continue + + log.info("Processing template %s", template_resolved) + output_file = os.path.join(output_dir, base) + if dom: + styledoc = libxml2.parseFile(template_resolved) + style = libxslt.parseStylesheetDoc(styledoc) + result = style.applyStylesheet(dom, None) + log.info("Writing %s", output_file) + style.saveResultToFilename(output_file, result, 0) + style.freeStylesheet() + result.freeDoc() + else: + log.info("Writing %s", output_file) + os.system('xsltproc %s %s > %s' % + (template_resolved, docfile, output_file)) + + if dom: dom.freeDoc() + if docfile: os.unlink(docfile) + + # Process bill of materials + for copy_file in config.bill_of_materials(): + dest = os.path.join(output_dir, copy_file) + for template_dir in config.template_directories(): + source = os.path.join(template_dir, copy_file) + if os.path.exists(source): break + else: + log.error('Unable to locate %s', copy_file) + continue + + mtime = os.stat(source).st_mtime + if not os.path.exists(dest) or os.stat(dest).st_mtime < mtime: + dest_dir = os.path.split(dest)[0] + if not os.path.exists(dest_dir): os.makedirs(dest_dir) + + log.info("Copying %s to %s", source, dest) + shutil.copyfile(source, dest) + shutil.copystat(source, dest) diff --git a/runtests.py b/runtests.py index 1c25c3d..55e22ae 100755 --- a/runtests.py +++ b/runtests.py @@ -1,8 +1,11 @@ #!/usr/bin/env python -import glob, trace, unittest +import glob, trace, unittest, os, sys + +# start in a consistent, predictable location +os.chdir(sys.path[0]) # find all of the planet test modules -modules = map(trace.fullmodname, glob.glob('tests/test_*.py')) +modules = map(trace.fullmodname, glob.glob(os.path.join('tests', 'test_*.py'))) # load all of the tests into a suite suite = unittest.TestLoader().loadTestsFromNames(modules) diff --git a/splice.py b/splice.py index e5ed424..580015b 100755 --- a/splice.py +++ b/splice.py @@ -13,10 +13,7 @@ if __name__ == '__main__': # at the moment, we don't have template support, so we cheat and # simply insert a XSLT processing instruction doc = splice.splice(sys.argv[1]) - pi = doc.createProcessingInstruction( - 'xml-stylesheet','type="text/xsl" href="planet.xslt"') - doc.insertBefore(pi, doc.firstChild) - print doc.toxml('utf-8') + splice.apply(doc.toxml('utf-8')) else: print "Usage:" print " python %s config.ini" % sys.argv[0] diff --git a/tests/capture.py b/tests/capture.py new file mode 100755 index 0000000..b71191d --- /dev/null +++ b/tests/capture.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +""" +While unit tests are intended to be independently executable, it often +is helpful to ensure that some downstream tasks can be run with the +exact output produced by upstream tasks. + +This script captures such output. It should be run whenever there is +a major change in the contract between stages +""" + +import shutil, os, sys + +# move up a directory +sys.path.insert(1, os.path.split(sys.path[0])[0]) +os.chdir(sys.path[1]) + +# copy spider output to splice input +from planet import spider +spider.spiderPlanet('tests/data/spider/config.ini') +if os.path.exists('tests/data/splice/cache'): + shutil.rmtree('tests/data/splice/cache') +shutil.move('tests/work/spider/cache', 'tests/data/splice/cache') + +source=open('tests/data/spider/config.ini') +dest1=open('tests/data/splice/config.ini', 'w') +dest1.write(source.read().replace('/work/spider/', '/data/splice/')) +dest1.close() + +source.seek(0) +dest2=open('tests/data/apply/config.ini', 'w') +dest2.write(source.read().replace('[Planet]', '''[Planet] +output_theme = asf +output_dir = tests/work/apply''')) +dest2.close() +source.close() + +# copy splice output to apply input +from planet import splice +file=open('tests/data/apply/feed.xml', 'w') +file.write(splice.splice('tests/data/splice/config.ini').toxml('utf-8')) +file.close() diff --git a/tests/data/apply/config.ini b/tests/data/apply/config.ini new file mode 100644 index 0000000..6256d2d --- /dev/null +++ b/tests/data/apply/config.ini @@ -0,0 +1,17 @@ +[Planet] +output_theme = asf +output_dir = tests/work/apply +name = test planet +cache_directory = tests/work/spider/cache + +[tests/data/spider/testfeed0.atom] +name = not found + +[tests/data/spider/testfeed1b.atom] +name = one + +[tests/data/spider/testfeed2.atom] +name = two + +[tests/data/spider/testfeed3.rss] +name = three diff --git a/tests/data/apply/feed.xml b/tests/data/apply/feed.xml new file mode 100644 index 0000000..f4029b2 --- /dev/null +++ b/tests/data/apply/feed.xml @@ -0,0 +1,215 @@ + +test planet2006-08-21T12:54:31ZVenusAnonymous Coward + tag:planet.intertwingly.net,2006:testfeed3/2 + + Venus + the Morning Star + 2006-08-21T12:54:31Z + + + + It’s just data + Sam Ruby + three + + + http://example.com/4 + + Mars + the Red Planet + 2006-08-21T12:54:31Z + + + + It’s just data + Sam Ruby + three + + + tag:planet.intertwingly.net,2006:testfeed1/2 + + Venus + the Jewel of the Sky + 2006-02-02T00:00:00Z + 2006-01-02T00:00:00Z + + tag:planet.intertwingly.net,2006:testfeed1 + + Sam Ruby + rubys@intertwingly.net + http://www.intertwingly.net/blog/ + + + + It’s just data + Sam Ruby + 2006-06-17T00:15:18Z + one + + + tag:planet.intertwingly.net,2006:testfeed2/4 + + Mars + the Red Planet + 2006-01-04T00:00:00Z + + tag:planet.intertwingly.net,2006:testfeed2 + + Sam Ruby + rubys@intertwingly.net + http://www.intertwingly.net/blog/ + + + + It’s just data + Sam Ruby + 2006-06-17T00:15:18Z + two + + + tag:planet.intertwingly.net,2006:testfeed1/4 + + Mars + the Red Planet + 2006-01-04T00:00:00Z + + tag:planet.intertwingly.net,2006:testfeed1 + + Sam Ruby + rubys@intertwingly.net + http://www.intertwingly.net/blog/ + + + + It’s just data + Sam Ruby + 2006-06-17T00:15:18Z + one + + + tag:planet.intertwingly.net,2006:testfeed2/3 + + Earth + the Blue Planet + 2006-01-03T00:00:00Z + + tag:planet.intertwingly.net,2006:testfeed2 + + Sam Ruby + rubys@intertwingly.net + http://www.intertwingly.net/blog/ + + + + It’s just data + Sam Ruby + 2006-06-17T00:15:18Z + two + + + tag:planet.intertwingly.net,2006:testfeed1/3 + + Earth + the Blue Planet + 2006-01-03T00:00:00Z + + tag:planet.intertwingly.net,2006:testfeed1 + + Sam Ruby + rubys@intertwingly.net + http://www.intertwingly.net/blog/ + + + + It’s just data + Sam Ruby + 2006-06-17T00:15:18Z + one + + + http://example.com/3 + + Earth + the Blue Planet + 2006-01-03T00:00:00Z + + + + It’s just data + Sam Ruby + three + + + tag:planet.intertwingly.net,2006:testfeed2/2 + + Venus + the Morning Star + 2006-01-02T00:00:00Z + + tag:planet.intertwingly.net,2006:testfeed2 + + Sam Ruby + rubys@intertwingly.net + http://www.intertwingly.net/blog/ + + + + It’s just data + Sam Ruby + 2006-06-17T00:15:18Z + two + + + tag:planet.intertwingly.net,2006:testfeed3/1 + + Mercury + Messenger of the Roman Gods + 2006-01-01T00:00:00Z + + + + It’s just data + Sam Ruby + three + + + tag:planet.intertwingly.net,2006:testfeed2/1 + + Mercury + Messenger of the Roman Gods + 2006-01-01T00:00:00Z + + tag:planet.intertwingly.net,2006:testfeed2 + + Sam Ruby + rubys@intertwingly.net + http://www.intertwingly.net/blog/ + + + + It’s just data + Sam Ruby + 2006-06-17T00:15:18Z + two + + + tag:planet.intertwingly.net,2006:testfeed1/1 + + Mercury + Messenger of the Roman Gods + 2006-01-01T00:00:00Z + + tag:planet.intertwingly.net,2006:testfeed1 + + Sam Ruby + rubys@intertwingly.net + http://www.intertwingly.net/blog/ + + + + It’s just data + Sam Ruby + 2006-06-17T00:15:18Z + one + +tag:planet.intertwingly.net,2006:testfeed2Sam Rubyrubys@intertwingly.nethttp://www.intertwingly.net/blog/It’s just dataSam Ruby2006-06-17T00:15:18ZtwoIt’s just dataSam Rubythreetag:planet.intertwingly.net,2006:testfeed1Sam Rubyrubys@intertwingly.nethttp://www.intertwingly.net/blog/It’s just dataSam Ruby2006-06-17T00:15:18Zone \ No newline at end of file diff --git a/tests/data/config/themed.ini b/tests/data/config/themed.ini new file mode 100644 index 0000000..7de933e --- /dev/null +++ b/tests/data/config/themed.ini @@ -0,0 +1,14 @@ +[Planet] +name = Test Configuration +output_theme = asf +items_per_page = 50 +template_directories = /foo /bar + +[index.html.xslt] +days_per_page = 7 + +[feed1] +name = one + +[feed2] +name = two diff --git a/tests/data/spider/config.ini b/tests/data/spider/config.ini index 7b38417..7a6c5e7 100644 --- a/tests/data/spider/config.ini +++ b/tests/data/spider/config.ini @@ -1,6 +1,6 @@ [Planet] +name = test planet cache_directory = tests/work/spider/cache -template_files = [tests/data/spider/testfeed0.atom] name = not found diff --git a/tests/data/splice/cache/example.com,3 b/tests/data/splice/cache/example.com,3 index df0943b..78c8001 100644 --- a/tests/data/splice/cache/example.com,3 +++ b/tests/data/splice/cache/example.com,3 @@ -4,7 +4,7 @@ Earth the Blue Planet - 2006-01-03T00:00:00Z + 2006-01-03T00:00:00Z diff --git a/tests/data/splice/cache/example.com,4 b/tests/data/splice/cache/example.com,4 index bc229ff..ee2f187 100644 --- a/tests/data/splice/cache/example.com,4 +++ b/tests/data/splice/cache/example.com,4 @@ -4,7 +4,7 @@ Mars the Red Planet - 2006-08-18T18:30:50Z + 2006-08-21T12:54:31Z diff --git a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,1 b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,1 index ee44d5b..3c3c8f3 100644 --- a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,1 +++ b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,1 @@ -4,7 +4,7 @@ Mercury Messenger of the Roman Gods - 2006-01-01T00:00:00Z + 2006-01-01T00:00:00Z tag:planet.intertwingly.net,2006:testfeed1 @@ -16,7 +16,7 @@ It’s just data Sam Ruby - 2006-06-17T00:15:18Z + 2006-06-17T00:15:18Z one diff --git a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,2 b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,2 index ee8ae2c..84d5fd4 100644 --- a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,2 +++ b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,2 @@ -4,8 +4,8 @@ Venus the Jewel of the Sky - 2006-02-02T00:00:00Z - 2006-01-02T00:00:00Z + 2006-02-02T00:00:00Z + 2006-01-02T00:00:00Z tag:planet.intertwingly.net,2006:testfeed1 @@ -17,7 +17,7 @@ It’s just data Sam Ruby - 2006-06-17T00:15:18Z + 2006-06-17T00:15:18Z one diff --git a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,3 b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,3 index e55d16a..f53e642 100644 --- a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,3 +++ b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,3 @@ -4,7 +4,7 @@ Earth the Blue Planet - 2006-01-03T00:00:00Z + 2006-01-03T00:00:00Z tag:planet.intertwingly.net,2006:testfeed1 @@ -16,7 +16,7 @@ It’s just data Sam Ruby - 2006-06-17T00:15:18Z + 2006-06-17T00:15:18Z one diff --git a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,4 b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,4 index 85b6a01..bd126fa 100644 --- a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,4 +++ b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed1,4 @@ -4,7 +4,7 @@ Mars the Red Planet - 2006-01-04T00:00:00Z + 2006-01-04T00:00:00Z tag:planet.intertwingly.net,2006:testfeed1 @@ -16,7 +16,7 @@ It’s just data Sam Ruby - 2006-06-17T00:15:18Z + 2006-06-17T00:15:18Z one diff --git a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,1 b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,1 index d143d18..4e387d0 100644 --- a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,1 +++ b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,1 @@ -4,7 +4,7 @@ Mercury Messenger of the Roman Gods - 2006-01-01T00:00:00Z + 2006-01-01T00:00:00Z tag:planet.intertwingly.net,2006:testfeed2 @@ -16,7 +16,7 @@ It’s just data Sam Ruby - 2006-06-17T00:15:18Z + 2006-06-17T00:15:18Z two diff --git a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,2 b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,2 index edeaeaa..2b3f94d 100644 --- a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,2 +++ b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,2 @@ -4,7 +4,7 @@ Venus the Morning Star - 2006-01-02T00:00:00Z + 2006-01-02T00:00:00Z tag:planet.intertwingly.net,2006:testfeed2 @@ -16,7 +16,7 @@ It’s just data Sam Ruby - 2006-06-17T00:15:18Z + 2006-06-17T00:15:18Z two diff --git a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,3 b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,3 index a724a82..576644e 100644 --- a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,3 +++ b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,3 @@ -4,7 +4,7 @@ Earth the Blue Planet - 2006-01-03T00:00:00Z + 2006-01-03T00:00:00Z tag:planet.intertwingly.net,2006:testfeed2 @@ -16,7 +16,7 @@ It’s just data Sam Ruby - 2006-06-17T00:15:18Z + 2006-06-17T00:15:18Z two diff --git a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,4 b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,4 index f28d7b9..aeaa78c 100644 --- a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,4 +++ b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed2,4 @@ -4,7 +4,7 @@ Mars the Red Planet - 2006-01-04T00:00:00Z + 2006-01-04T00:00:00Z tag:planet.intertwingly.net,2006:testfeed2 @@ -16,7 +16,7 @@ It’s just data Sam Ruby - 2006-06-17T00:15:18Z + 2006-06-17T00:15:18Z two diff --git a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed3,1 b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed3,1 index 5ca9f26..e19e044 100644 --- a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed3,1 +++ b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed3,1 @@ -4,7 +4,7 @@ Mercury Messenger of the Roman Gods - 2006-01-01T00:00:00Z + 2006-01-01T00:00:00Z diff --git a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed3,2 b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed3,2 index f5acd6b..a1262b1 100644 --- a/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed3,2 +++ b/tests/data/splice/cache/planet.intertwingly.net,2006,testfeed3,2 @@ -4,7 +4,7 @@ Venus the Morning Star - 2006-08-18T18:30:50Z + 2006-08-21T12:54:31Z diff --git a/tests/data/splice/cache/sources/tests,data,spider,testfeed1b.atom b/tests/data/splice/cache/sources/tests,data,spider,testfeed1b.atom index 8cb9e5c..d525b98 100644 --- a/tests/data/splice/cache/sources/tests,data,spider,testfeed1b.atom +++ b/tests/data/splice/cache/sources/tests,data,spider,testfeed1b.atom @@ -10,6 +10,6 @@ It’s just data Sam Ruby - 2006-06-17T00:15:18Z + 2006-06-17T00:15:18Z one diff --git a/tests/data/splice/cache/sources/tests,data,spider,testfeed2.atom b/tests/data/splice/cache/sources/tests,data,spider,testfeed2.atom index 6aeb0ab..df9da4d 100644 --- a/tests/data/splice/cache/sources/tests,data,spider,testfeed2.atom +++ b/tests/data/splice/cache/sources/tests,data,spider,testfeed2.atom @@ -10,6 +10,6 @@ It’s just data Sam Ruby - 2006-06-17T00:15:18Z + 2006-06-17T00:15:18Z two diff --git a/tests/data/splice/config.ini b/tests/data/splice/config.ini index 0ba74c3..9c8cf3e 100644 --- a/tests/data/splice/config.ini +++ b/tests/data/splice/config.ini @@ -1,7 +1,6 @@ [Planet] name = test planet cache_directory = tests/data/splice/cache -template_files = [tests/data/spider/testfeed0.atom] name = not found diff --git a/tests/test_apply.py b/tests/test_apply.py new file mode 100644 index 0000000..bdd0468 --- /dev/null +++ b/tests/test_apply.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +import unittest, os, shutil +from planet import config, splice + +workdir = 'tests/work/apply' +configfile = 'tests/data/apply/config.ini' +testfeed = 'tests/data/apply/feed.xml' + +class ApplyTest(unittest.TestCase): + def setUp(self): + try: + os.makedirs(workdir) + except: + self.tearDown() + os.makedirs(workdir) + + def tearDown(self): + shutil.rmtree(workdir) + os.removedirs(os.path.split(workdir)[0]) + + def test_apply(self): + testfile = open(testfeed) + feeddata = testfile.read() + testfile.close() + + config.load(configfile) + splice.apply(feeddata) + + for file in ['index.html', 'default.css', 'images/foaf.png']: + path = os.path.join(workdir, file) + self.assertTrue(os.path.exists(path)) + self.assertTrue(os.stat(path).st_size > 0) diff --git a/tests/test_config.py b/tests/test_config.py index eb3c79d..8773a46 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,8 +3,6 @@ import unittest from planet import config -workdir = 'tests/work/spider/cache' - class ConfigTest(unittest.TestCase): def setUp(self): config.load('tests/data/config/basic.ini') @@ -16,7 +14,9 @@ class ConfigTest(unittest.TestCase): config.template_files()) def test_feeds(self): - self.assertEqual(['feed1', 'feed2'], config.feeds()) + feeds = config.feeds() + feeds.sort() + self.assertEqual(['feed1', 'feed2'], feeds) # planet wide configuration diff --git a/tests/test_spider.py b/tests/test_spider.py index 1f945e8..067c602 100644 --- a/tests/test_spider.py +++ b/tests/test_spider.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -import unittest, os, glob, calendar +import unittest, os, glob, calendar, shutil from planet.spider import filename, spiderFeed, spiderPlanet from planet import feedparser, config @@ -17,13 +17,8 @@ class SpiderTest(unittest.TestCase): os.makedirs(workdir) def tearDown(self): - for file in glob.glob(workdir+"/sources/*"): - os.unlink(file) - if os.path.exists(workdir+"/sources"): - os.rmdir(workdir+"/sources") - for file in glob.glob(workdir+"/*"): - os.unlink(file) - os.removedirs(workdir) + shutil.rmtree(workdir) + os.removedirs(os.path.split(workdir)[0]) def test_filename(self): self.assertEqual('./example.com,index.html', diff --git a/tests/test_themes.py b/tests/test_themes.py new file mode 100644 index 0000000..b92eb41 --- /dev/null +++ b/tests/test_themes.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +import unittest +from planet import config +from os.path import split + +class ConfigTest(unittest.TestCase): + def setUp(self): + config.load('tests/data/config/themed.ini') + + # template directories + + def test_template_directories(self): + self.assertEqual(['foo', 'bar', 'asf', 'common'], + [split(dir)[1] for dir in config.template_directories()]) + + # administrivia + + def test_template(self): + self.assertTrue('index.html.xslt' in config.template_files()) + + def test_feeds(self): + feeds = config.feeds() + feeds.sort() + self.assertEqual(['feed1', 'feed2'], feeds) + + # planet wide configuration + + def test_name(self): + self.assertEqual('Test Configuration', config.name()) + + def test_link(self): + self.assertEqual('Unconfigured Planet', config.link()) + + # per template configuration + + def test_days_per_page(self): + self.assertEqual(7, config.days_per_page('index.html.xslt')) + self.assertEqual(0, config.days_per_page('atom.xml.xslt')) + + def test_items_per_page(self): + self.assertEqual(50, config.items_per_page('index.html.xslt')) + self.assertEqual(50, config.items_per_page('atom.xml.xslt')) + + def test_encoding(self): + self.assertEqual('utf-8', config.encoding('index.html.xslt')) + self.assertEqual('utf-8', config.encoding('atom.xml.xslt')) + + # dictionaries + + def test_feed_options(self): + self.assertEqual('one', config.feed_options('feed1')['name']) + self.assertEqual('two', config.feed_options('feed2')['name']) + + def test_template_options(self): + option = config.template_options('index.html.xslt') + self.assertEqual('7', option['days_per_page']) + self.assertEqual('50', option['items_per_page']) diff --git a/themes/asf/config.ini b/themes/asf/config.ini new file mode 100644 index 0000000..10f6a22 --- /dev/null +++ b/themes/asf/config.ini @@ -0,0 +1,19 @@ +# This template is based on the one originally developed by Stefano Mazzocci +# for planetapache.org, and modified by Sam Ruby for planet.intertwingly.net + +[Planet] +template_files: + atom.xml.xslt + foafroll.xml.xslt + index.html.xslt + opml.xml.xslt + +template_directories: + ../common + +bill_of_materials: + default.css + personalize.js + images/feed-icon-10x10.png + images/opml.png + images/foaf.png diff --git a/themes/asf/default.css b/themes/asf/default.css new file mode 100644 index 0000000..001c641 --- /dev/null +++ b/themes/asf/default.css @@ -0,0 +1,429 @@ +/* + * Written by Stefano Mazzocchi + */ + +/* ----------------------------- Global Definitions -------------------- */ + +body { + margin: 0px; + padding: 0px; + color: #222; + background-color: #fff; + quotes: "\201C" "\201E" "\2018" "\2019"; +} + +a:link { + color: #222; +} + +a:visited { + color: #555; +} + +a:hover { + color: #000; +} + +a:active { +} + +a:focus { +} + +h1 { + font-size: x-large; + text-transform: uppercase; + letter-spacing: 0.25em; + padding: 10px; + margin: 0px 0px 0px 0px; + color: #000; + font-weight: normal; + background-color: #eee; + border-bottom: 2px solid #bbb +} + +/* ----------------------------- Sidebar --------------------------- */ + +#sidebar { + float: right; + top: 150px; + right: 0px; + width: 210px; + background-color: white; + + padding: 0px 0px 20px 0px; + margin: 0px 0px 20px 20px; + border-left: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} + +#sidebar h2 { + letter-spacing: 0.15em; + text-transform: uppercase; + font-size: x-small; + color: #666; + font-weight: normal; + padding: 2px 0px 2px 4px; + margin: 15px 0px 5px 10px; + border-top: 1px solid #ccc; + border-left: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} + +#sidebar p { + font-size: x-small; + padding-left: 20px; + padding-right: 5px; +} + +#sidebar ul { + font-family: sans-serif; + margin-left: 5px; + padding-left: 25px; +} + +#sidebar li { + margin-left: 0px; + text-indent: -15px; + list-style-type: none; + font-size: x-small; +} + +#sidebar ul li a { + text-decoration: none; +} + +#sidebar ul li a:hover { + text-decoration: underline; +} + +#sidebar img { + border: 0; +} + +#sidebar dl { + font-size: x-small; + padding-left: 1.0em; +} + +#sidebar dl ul { + padding-left: 1em; +} + +#sidebar dt { + margin-top: 1em; + font-weight: bold; + padding-left: 1.0em; +} + +#sidebar dd { + margin-left: 2.5em; +} + +#sidebar .message { + cursor: help; + border-bottom: 1px dashed red; +} + +#sidebar a.message:hover { + cursor: help; + background-color: #ffD0D0; + border: 1px dashed red !important; + text-decoration: none !important; +} + +/* ----------------------------- Body ---------------------------- */ + +#body { + margin-top: 10px; +} + +.admin { + text-align: right; +} + +#body h2.date { + text-transform: none; + font-size: medium; + color: #333; + font-weight: bold; + text-align: right; + border-top: 1px solid #ccc; + background-color: #eee; + border-bottom: 1px solid #ccc; + padding: 1px 15px 1px 5px; + margin: 0; +} + +/* ----------------------------- News ---------------------------- */ + +.news { + margin: 30px 10px 30px 10px; + clear: left; +} + +.news > h3 { + text-indent: -10px; + margin: 12px; + padding: 0px; + font-size: medium; +} + +.news > h3 > a:first-child { + margin-left: 10px +} + +.news > h3 > a:first-child:before { + content: '⌘'; + color: #D70; + margin-left: -18px; + margin-right: 2px; + text-decoration: none; +} + +img.icon { + height: 16px; + width: 16px; + margin-left: -8px; + margin-bottom: -2px; + margin-right: 3px; +} + +.news .content { + margin: 5px 5px 5px 15px; + padding: 0px 5px 0px 5px; + border-left: 1px solid #ccc; + line-height: 1.2em; + font-size: small; + font-family: sans-serif; +} + +.news .links { +} + +.news .permalink { + text-align: right; +} + +/* ----------------------------- News Content ---------------------------- */ + +.news .content p { + line-height: 1.2em; +} + +.news .content img { + margin: 5px; +} + +.news .content blockquote { + margin: 10px 35px 10px 35px; + padding: 5px; +} + +.news .content pre { + font-family: monospace; + font-size: medium; + font-weight: bold; + border: 1px solid #ddd; + padding: 10px; + margin: 10px 20px 10px 20px; + background-color: #f8f8f8; + overflow: auto; +} + +.news .content ul, .news .content ol { + margin: 5px 35px 5px 35px; + padding: 5px; + counter-reset: item; +} + +.news .content ul > ul, .news .content ul > ol, .news .content ol > ul, .news .content ol > ol { + margin: 0px 0px 0px 35px; + padding: 0px; +} + +.news .content li { + padding: 1px; + line-height: 1.2em; +} + +.news code { + font-family: monospace; + font-size: medium; + font-weight: bold; +} + +.news .content a { + text-decoration: none; + color: #000; + border-bottom: 1px dotted #777; + margin: 0px 2px 0px 2px; + padding: 1px 1px 1px 1px; +} + +.news .content a:hover { + border: 1px dotted #000; + background-color: #eee; + padding: 1px 2px 1px 2px; + margin: 0px; +} + +.news .content a:active { + background-color: #ccc !important; + position: relative; + top: 1px; + left: 1px; + padding: 1px 2px 1px 2px; + margin: 0px; +} + +.news .content a:focus { + border: 1px solid #fff !important; + background-color: #ccc !important; + padding: 1px 2px 1px 2px; + margin: 0px; +} + +/* --------------------------- Accomodations ----------------------- */ + +/* boing boing */ +br { + clear: none !important; +} + +/* engadget */ +h6 { + clear: left !important; +} + +/* cadenhead */ +p.sourcecode { + font-family: monospace; + font-size: medium; + font-weight: bold; + border: 1px solid #ddd; + padding: 10px; + margin: 10px 20px 10px 20px; + background-color: #f8f8f8; + overflow: auto; +} + +/* cadenhead */ +span.sourcecode { + font-family: monospace; + font-size: medium; + font-weight: bold; + font-size: large; + background-color: #f8f8f8; +} + +/* hsivonen */ +ul p, ol p { + margin-top: 0.3em; + margin-bottom: 0.3em; +} + +/* programmableweb */ +.imgRight { + float: right; +} + +/* gizmodo */ +img.left { + float: left; +} + +/* gizmodo */ +img.right { + float: right; +} + +/* gizmodo */ +img.center { + display: block; + margin-left: auto; + margin-right: auto; +} + +/* wikipedia */ +table { + width: auto !important; +} + +/* del.icio.us */ +.delicious-tags { + font-size: x-small; + text-align: right; +} + +/* musings */ +img.mathlogo, img.svglogo { + float: right; + border: 0; +} + +math { + white-space: nowrap; +} + +math[display=block] { + overflow: auto; +} + +.eqno { + float: right; +} + +/* sutor */ +img.post-img-right { + float:right; +} + +/* niall */ +img.floatright { + float: right; +} + +/* jason kolb */ +.FeaturedPost > li { + list-style-type: none; + background-color: #f8f8f8; +} + +/* GigaOM */ +p img { + float: left; +} + +/* Tantek */ +ul.tags,ul.tags li,h4.tags { + display:inline; + font-size: x-small +} + +ul.tags a:link, ul.tags a:visited { + color:green +} + +/* DiveIntoMark */ +.framed { + float: none; +} + +/* ----------------------------- Footer ---------------------------- */ + +#footer { + padding: 0px; + margin: 30px 0px 50px 50px; +} + +#footer p { + padding: 2px 2px 2px 5px; + background-color: #ccc; + border-top: 1px solid #aaa; + border-bottom: 1px solid #aaa; + border-left: 1px solid #aaa; + letter-spacing: 0.15em; + text-transform: uppercase; + text-align: left; +} diff --git a/themes/asf/index.html.xslt b/themes/asf/index.html.xslt new file mode 100644 index 0000000..5665700 --- /dev/null +++ b/themes/asf/index.html.xslt @@ -0,0 +1,153 @@ + + + + + + + + + + <xsl:value-of select="atom:title"/> + + + + + + + + + + + +

+ + + + + + + +
+ + + + + + +

+
+ + +
+ + + + + + + +

+ + + + + + + + + + + + + + + + +

+ + + +
+ + + + + + + + + + + + + + +
+ + + + +
+ +
+
diff --git a/themes/asf/personalize.js b/themes/asf/personalize.js new file mode 100644 index 0000000..83db3a3 --- /dev/null +++ b/themes/asf/personalize.js @@ -0,0 +1,220 @@ +var entries = []; // list of news items + +var days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", + "Friday", "Saturday"]; +var months = ["January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December"]; + +// event complete: stop propagation of the event +function stopPropagation(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + } +} + +// scroll back to the previous article +function prevArticle(event) { + for (var i=entries.length; --i>=0;) { + if (entries[i].anchor.offsetTop < document.documentElement.scrollTop) { + window.location.hash=entries[i].anchor.id; + stopPropagation(event); + break; + } + } +} + +// advance to the next article +function nextArticle(event) { + for (var i=1; i document.documentElement.scrollTop) { + window.location.hash=entries[i].anchor.id; + stopPropagation(event); + break; + } + } +} + +// process keypresses +function navkey(event) { + var checkbox = document.getElementById('navkeys'); + if (!checkbox || !checkbox.checked) return; + + if (!event) event=window.event; + key=event.keyCode; + + if (!document.documentElement) return; + if (!entries[0].anchor || !entries[0].anchor.offsetTop) return; + + if (key == 'J'.charCodeAt(0)) nextArticle(event); + if (key == 'K'.charCodeAt(0)) prevArticle(event); +} + +// create (or reset) a cookie +function createCookie(name,value,days) { + if (days) { + var date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + var expires = "; expires="+date.toGMTString(); + } + else expires = ""; + document.cookie = name+"="+value+expires+"; path=/"; +} + +// read a cookie +function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; +} + +// each time the value of the option changes, update the cookie +function selectOption() { + var checkbox = document.getElementById('navkeys'); + if (!checkbox) return; + createCookie("navkeys", checkbox.checked?'true':'false', 365); +} + +// add navkeys option to sidebar +function addOption(event) { + if (entries.length > 1 && entries[entries.length-1].parent.offsetTop > 0) { + var sidebar = document.getElementById('sidebar'); + if (!sidebar) return; + + for (var i=entries.length; --i>=0;) { + var a = entries[i].anchor = document.createElement('a'); + a.id = "news-" + i; + entries[i].parent.insertBefore(a, entries[i].parent.firstChild); + } + + var h2 = document.createElement('h2'); + h2.appendChild(document.createTextNode('Options')); + sidebar.appendChild(h2); + + var form = document.createElement('form'); + var p = document.createElement('p'); + var input = document.createElement('input'); + input.type = "checkbox"; + input.id = "navkeys"; + p.appendChild(input); + var a = document.createElement('a'); + a.title = "Navigate entries"; + a.appendChild(document.createTextNode('Enable ')); + var code = document.createElement('code'); + code.appendChild(document.createTextNode('J')); + a.appendChild(code); + a.appendChild(document.createTextNode(' and ')); + code = document.createElement('code'); + code.appendChild(document.createTextNode('K')); + a.appendChild(code); + a.appendChild(document.createTextNode(' keys')); + p.appendChild(a); + form.appendChild(p); + sidebar.appendChild(form); + + var cookie = readCookie("navkeys"); + if (cookie && cookie == 'true') input.checked = true; + input.onclick = selectOption; + document.onkeydown = navkey; + } +} + +// convert date to local time +var localere = /^(\w+) (\d+) (\w+) \d+ 0?(\d\d?:\d\d):\d\d ([AP]M) (EST|EDT|CST|CDT|MST|MDT|PST|PDT)/; +function localizeDate(element) { + var date = new Date(); + date.setTime(Date.parse(element.innerHTML + " GMT")); + + var local = date.toLocaleString(); + var match = local.match(localere); + if (match) { + element.innerHTML = match[4] + ' ' + match[5].toLowerCase(); + element.title = match[6] + " \u2014 " + + match[1] + ', ' + match[3] + ' ' + match[2]; + return days[date.getDay()] + ', ' + months[date.getMonth()] + ' ' + + date.getDate() + ', ' + date.getFullYear(); + } else { + element.title = element.innerHTML + ' GMT'; + element.innerHTML = local; + return days[date.getDay()] + ', ' + date.getDate() + ' ' + + months[date.getMonth()] + ' ' + date.getFullYear(); + } + +} + +// find entries (and localizeDates) +function findEntries() { + + var span = document.getElementsByTagName('span'); + + for (var i=0; i + + + + + + + + no + + + + + + + + + + + + diff --git a/themes/common/foafroll.xml.xslt b/themes/common/foafroll.xml.xslt new file mode 100644 index 0000000..7781eac --- /dev/null +++ b/themes/common/foafroll.xml.xslt @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/themes/common/images/feed-icon-10x10.png b/themes/common/images/feed-icon-10x10.png new file mode 100644 index 0000000000000000000000000000000000000000..cc869bc61785f4db646fcbbcfc87aa3d20d99eba GIT binary patch literal 469 zcmV;`0V@89P)b#`aw-kX_Si^Jc1|2c;v&L+N%#GTkbr^vx_L@0?2ue1vae8uy9 zW>j2Fwi~;wnv#w|%)>D{wT>10d6+ znvjX&#K$e}$~4lwrKocpr#p$RZpK^viI-7mZAGBOZfK!ocn0%!gWCL!dO5}Fox+@M z<8LitMCck7=WdtW+z{sJ1iSwiD)WlBvkT!CKn3HAn`5Jatl8=pf zWMv()t_|gz0w^mJ$i`l18o*uui>ycxWHsvP8s|%U9u%*Cx=p;;psT*)T^`{*rxCTS zWX}(wG=ZN^W3ms(Xv}CQKedl?b7Aoq{>2_>AN82ZL&^z8{|hhxfn}MuvYci literal 0 HcmV?d00001 diff --git a/themes/common/images/foaf.png b/themes/common/images/foaf.png new file mode 100644 index 0000000000000000000000000000000000000000..0fea5ba5bc3daba7e3fca691f0db3c755934f685 GIT binary patch literal 1393 zcmd^7`%hX27{xmA?Q~Wi-s?&UP6=D9ncP*kxD~ZX5vLRhB~B3p17=HopqW`ZQIr^3 z%%ry}7rX?-W^+DoTdZ!&GGf$;6${PWGfUHq#geHsX|}rd?q>hRvXh*1@+IF7-#Ph) zYO49EN!dvx5-C+sSuQ3#n_yZ(ETQD!-)$sPEUBhSQn9eGAP@-V=H_~Pd#TjpWo2bJ z?!>j){y_}K5mqj%tWsH9E5R@fZ)q{jn!;fVe0^ggIDzYQmOGZg=|T6d8)Sk=0P%RV zIBrw-s{3a9hRiGaF}+Hq3M&-dJ#LRs==$EZ*W7IQ+z^St{r&yOHp1gK1pT?m3cEljZGlG=SQX?R*YI1*rqm0}u$>`TWJYx)XUP_VzxZ(J0i+%rMTb`wzKCNX*4>R^O9F0d6@;W*-#`h%msjsZs*vz-~5F+R9`PX zc@o`1ooLAG?c(uxhiY||NE3}(_jS4>t@gaIIutTSmCDXe0Xd$WaU8ItK@KP953Sbt znNgmbyT3mQLAiy6jIdIfk(zEa-E{xpPESsA?YiuC)s?GX8jK4RAWQmz^z>;41hFhj z7Aw4#pAXRiih0FM1M~9PTpO}Yp`;bk>EZA&au=DGNF4SZ;vaAvC-nb`?DWucBKb_G zxcUOo!+!>(XB96T5QoPoh5T~d6CYhAhQv#i7dr^A{Fmo8T1$zAT0!|)2`#9SVS}S< zyJM#AEgM%Jt$$h{mA5*3o356;VRHU@1w7*JY>=61CZ-fOiBoJPb<4}_9NXT5sb6>; zW8d;>tV~8TOcj*3(>6-p+{|u!c)ryfZ9OpSg#3hr$*tRszWJrjr_74gCx2zC)G4A@ zIj@OQ)X_7=zWaT28ZU12ntz&eyXK%a3xglSSk{I2bS=~eS?k{hinOpw+A~_vIAC79 z(cqd0G>_gN8JR{N&CqE}dNBF1`y1V}kw4+wBG#0Ra#Ma$w0UY#BlG#U3O$~WZjIdA ziz73W!##e#!aAZ5Ld)SDdRr!d6tz!1cM b*GVMbqsy7^M_-&FS|$l9s>^42ioSmU^%GgE literal 0 HcmV?d00001 diff --git a/themes/common/images/opml.png b/themes/common/images/opml.png new file mode 100644 index 0000000000000000000000000000000000000000..3f18190cdec1bf6ff9d956ac7246a9a27028c8a9 GIT binary patch literal 804 zcmV+<1Ka$GP)TX7h22X?t*x!#Eh+oz>EUT)-8DA8zP>XvGo+-X*J~-u#itoQJIu_?`S$kiud>~2Xw@PZ;h2}) z4FKgB4&yN?*araVSy$|kj^igB@4~+2As6qZrQ=Ua-$O&)A|TsyZs@nT*)c2Rdvxb} ze7Co^;5RefVO;5^rt;w6c?F(L4-t=s?r>vwkMYi!&U2ILBJ zWMt-0Qr8m>;6XXzfqw48#^!Tw-Z(biV^Zd=t?n5WqX5d z5}kX1OqWKIK#m!z7xJG2h(wJY=tj}Pwg61swk;o-+>Te*0ZM1AlTN+rhBwRj0qWbo zbn`}RGz#(wiOC(QWfK`6IJ%*d4tX>L!jcm`58UM=svx?stG#6WVt};X!j5IWd0001ZP)t-s6&4vG zAtEd*E;&0qKV`^${d~tAgh-qeVagus* zg>rMCdwqI?e3N&7tAT=(e1oHueuIjFjfswVrh|BujFOR(o065Ps+E + + + + + + + <xsl:value-of select="atom:title"/> + + + + + + + + + + + + +