1 # -*- coding: utf-8 -*-
2 from fsdict import FSDict
4 from urllib import quote_plus
11 #constant unlikely to occur in a docname and legal as a filename
12 MAGIC_SEPARATOR = '---###---'
14 def parse_date(datestring):
16 parser = parse_date.parser
17 except AttributeError:
18 import dateutil.parser
19 parser = dateutil.parser.parser()
20 parse_date.parser = parser
21 return parser.parse(datestring)
25 see: http://sphinx.pocoo.org/ext/appapi.html
26 this is the primary extension point for Sphinx
28 from sphinx.application import Sphinx
29 if not isinstance(app, Sphinx): return
30 app.add_config_value('feed_base_url', '', 'html')
31 app.add_config_value('feed_description', '', 'html')
32 app.add_config_value('feed_filename', 'rss.xml', 'html')
33 app.add_config_value('feed_title', '', 'html')
34 app.add_config_value('feed_subtitle', '', 'html')
35 app.add_config_value('feed_author_name', '', 'html')
36 app.add_config_value('feed_author_email', '', 'html')
37 app.add_config_value('feed_categories', [], 'html')
38 app.add_config_value('feed_variants',
39 {'all': {'filename': 'rss.xml', 'categories': None}},
41 app.connect('html-page-context', create_feed_item)
42 app.connect('build-finished', emit_feed)
43 app.connect('builder-inited', create_feed_container)
44 app.connect('env-purge-doc', remove_dead_feed_item)
46 def create_feed_container(app):
48 create lazy filesystem stash for keeping RSS entry fragments, since we
49 don't want to store the entire site in the environment (in fact, even if
50 we did, it wasn't persisting for some reason.)
53 rss_fragment_path = os.path.realpath(os.path.join(app.outdir, '..', 'rss_entry_fragments'))
54 feed_entries = FSDict(work_dir=rss_fragment_path)
55 app.builder.env.feed_url = app.config.feed_base_url + '/' + \
56 app.config.feed_filename
58 def inject_feed_url(app, pagename, templatename, ctx, doctree):
59 #We like to provide our templates with a way to link to the rss output file
60 ctx['rss_link'] = app.builder.env.feed_url #app.config.feed_base_url + '/' + app.config.feed_filename
63 def create_feed_item(app, pagename, templatename, ctx, doctree):
65 Here we have access to nice HTML fragments to use in, say, an RSS feed.
66 We serialize them to disk so that we get them preserved across builds.
68 We also inject useful metadata into the context here.
71 from absolutify_urls import absolutify
72 metadata = app.builder.env.metadata.get(pagename, {})
74 if 'date' not in metadata:
75 return #don't index dateless articles
77 pub_date = parse_date(metadata['date'])
78 app.builder.env.metadata.get(pagename, {})
79 except ValueError, exc:
80 #probably a nonsensical date
81 app.builder.warn('date parse error: ' + str(exc) + ' in ' + pagename)
84 # RSS item attributes, w/defaults:
85 # title, link, description, author_email=None,
86 # author_name=None, author_link=None, pubdate=None, comments=None,
87 # unique_id=None, enclosure=None, categories=(), item_copyright=None,
89 link = app.config.feed_base_url + '/' + ctx['current_page_name'] + ctx['file_suffix']
90 # bring main body of the feed item into shape
91 body = ctx.get('body')
92 # remove all header links (they make ugly characters in feed readers)
93 body = re.sub('\<a class\="headerlink".*\>.</a\>', '', body)
96 'title': ctx.get('title'),
99 'description': absolutify(body, link),
103 if 'tags' in metadata:
104 item['categories'] = metadata['tags'].split(",")
105 if 'author' in metadata:
106 item['author_name'] = metadata['author']
108 item['author_name'] = app.config.feed_author_name
109 if 'author_email' in metadata:
110 item['author_email'] = metadata['author_email']
112 item['author_email'] = app.config.feed_author_email
113 feed_entries[nice_name(pagename, pub_date)] = item
115 #Now, useful variables to keep in context
116 ctx['rss_link'] = app.builder.env.feed_url
117 ctx['pub_date'] = pub_date
119 def remove_dead_feed_item(app, env, docname):
125 munged_name = ''.join([MAGIC_SEPARATOR,quote_plus(docname)])
126 for name in feed_entries:
127 if name.endswith(munged_name):
128 del(feed_entries[name])
130 def emit_feed(app, exc):
134 title = app.config.feed_title
136 title = app.config.project
140 'subtitle': app.config.feed_subtitle,
141 'link': app.config.feed_base_url,
142 'feed_url': app.config.feed_base_url,
143 'description': app.config.feed_description,
144 'categories': app.config.feed_categories,
145 'author_name': app.config.feed_author_name,
146 'author_email': app.config.feed_author_email
148 if app.config.language:
149 feed_dict['language'] = app.config.language
150 if app.config.copyright:
151 feed_dict['feed_copyright'] = app.config.copyright
153 ordered_keys = feed_entries.keys()
154 ordered_keys.sort(reverse=True)
155 # loop over all feed variants
156 for feedvar in app.config.feed_variants:
157 feedvar_settings = app.config.feed_variants[feedvar]
158 feed = feedgenerator.Rss201rev2Feed(**feed_dict)
159 app.builder.env.feed_feed = feed
160 for key in ordered_keys:
161 item = feed_entries[key]
162 # only take the ones that should be in this feed
163 if feedvar_settings['tag'] is None \
164 or feedvar_settings['tag'] in item['categories']:
165 feed.add_item(**feed_entries[key])
166 outfilename = os.path.join(app.builder.outdir,
167 feedvar_settings['filename'])
168 # make sure the directory exists
169 feed_dir = os.path.dirname(outfilename)
170 if feed_dir and not os.path.exists(feed_dir):
171 os.makedirs(os.path.dirname(outfilename))
172 fp = open(outfilename, 'w')
173 feed.write(fp, 'utf-8')
176 def nice_name(docname, date):
178 we need convenient filenames which incorporate dates for ease of sorting and
179 guid for uniqueness, plus will work in the FS without inconvenient
180 characters. NB, at the moment, hour of publication is ignored.
182 return quote_plus(MAGIC_SEPARATOR.join([date.isoformat(), docname]))