Commits

x...@resolvent.net  committed 1e41477

Renamed to `sphinxcontrib-newsfeed`; renamed some directives.

  • Participants
  • Parent commits 2f4fa2c

Comments (0)

Files changed (10)

-*****************************************************
-  ``sphinxcontrib-blog`` -- A Sphinx Blog Extension
-*****************************************************
+****************************************************************
+  ``sphinxcontrib-newsfeed`` -- News Feed extension for Sphinx
+****************************************************************
 
 Overview
 ========
 
-``sphinxcontrib-blog`` is a extension for adding a simple *Blog*, *News*
-or *Announcements*  section to a Sphinx_ website.
+``sphinxcontrib-newsfeed`` is a extension for adding a simple *Blog*,
+*News* or *Announcements*  section to a Sphinx_ website.
 
 Features:
 
-* Makes blog posts from Sphinx documents.
-* Generates a list of blog posts with teasers and an RSS feed.
+* Makes feed entries from Sphinx documents.
+* Generates a list of entries with teasers.
+* Writes the feed in RSS/Atom format.
 * Supports comments via Disqus_.
 
 You can see this extension in action at http://htsql.org/blog/.
 Usage
 =====
 
-To use this extension with your Sphinx website, add the following line
-to ``conf.py``::
+To enable this extension, add the following line to ``conf.py``::
 
-    extensions.append('sphinxcontrib.blog')
+    extensions.append('sphinxcontrib.newsfeed')
 
-To enable comments, you also need to specify the Disqus_ website
-identifier::
+To add a comment form to news entries, you also need to specify the
+Disqus_ website identifier::
 
     disqus_shortname = '...'
 
-Now you can make a blog post from any Sphinx document by adding
-directive ``blogpost``.  For example::
+Now you can convert any Sphinx document to a news entry by using
+directive ``feed-entry``.  For example::
 
-    First Post!!!
-    =============
+    Welcome!!!
+    ==========
 
-    .. blogpost::
-       :date: 2013-01-01
+    .. feed-entry::
+       :date: 2012-01-01
 
-    Welcome to my new blog!!!
+    Welcome to the news feed of **Elvensense**.  Here we will post
+    release announcements and other project news.
 
-Use ``blogcut`` directive to separate the post teaser from the content::
+Use ``cut`` directive to separate the entry teaser from the content::
 
-    Post with a Teaser
-    ==================
+    Elvensense 96 is released
+    =========================
 
-    .. blogpost::
-       :date: 2013-02-02
+    .. feed-entry::
+       :date: 2012-12-31
 
-    Place summary here.
+    We are proud to announce a new release of **Elvensense**.
 
-    .. blogcut::
+    .. cut::
 
-    Here is the main body of the post.
+    Specific changes since the last release:
 
-To make a list of blog posts and generate an RSS feed, use ``blogtree``
-directive::
+    * An exciting feature was added.
+    * An annoying bug was fixed.
 
-    .. blogtree::
+
+To make a list of news entries and generate an RSS or Atom feed, use
+``feed`` directive::
+
+    .. feed::
        :rss: index.rss
-       :title: My Project News
+       :atom: index.atom
+       :title: Elvensense News
 
-       with-teaser
-       first-post
+       release
+       welcome
 
-The ``blogtree`` body should contain a list of documents (just like
-``toctree``).
+The body of ``feed`` directive must list documents containing news
+entries (similar to ``toctree``).  The options of ``feed`` directive
+define the location of RSS and Atom files and describe the feed
+metadata.
 
-The ``blogtree`` options specify the name of the RSS feed file and RSS
-metadata.  You need to manually add a link to the RSS feed to your
-templates::
+You need to manually update your HTML templates to add links to RSS and
+Atom feeds:
 
       <link rel="alternate"
             type="application/rss+xml"
-            title="My Project News"
+            title="Elvensense News"
             href="/index.rss" />
+      <link rel="alternate"
+            type="application/atom+xml"
+            title="Elvensense News"
+            href="/index.atom" />
 
 
 Reference
 Directives
 ----------
 
-``blogpost``
-    Inserts the post metadata.
+``feed-entry``
+    Specifies an entry metadata.
 
     This directive has no body.
 
     ``date``
         The date of the post in ``YYYY-MM-DD`` format.
 
-``blogtree``
-    Inserts a list of blog posts with teasers at the current location.
+``feed``
+    Inserts a list of entries with teasers at the current location.
 
     This directive should contain a list of document names (similar to
-    ``toctree``).
+    ``toctree``).  This directive adds the documents to the hierarchy,
+    so that you don't need to add the to ``toctree``.
 
     Options:
 
     ``rss``
         Where to write the RSS feed (optional).
+    ``atom``
+        Where to write the Atom feed (optional).
     ``title``
-        The name of the blog (for RSS metadata).
+        The name of the feed (for RSS/Atom metadata).
     ``description``
-        The description of the blog (for RSS metadata).
+        The description of the feed (for RSS/Atom metadata).
     ``link``
-        The URL of the blog (for RSS metadata).
+        The URL of the feed (for RSS/Atom metadata).
 
-``blogcut``
-    Separates the post teaser from the main body of the post.
+``cut``
+    Separates the entry teaser from the main body.
 
     This directive has no options and no body.
 
 
     Normally you don't need to use this directive since if
     ``disqus_shortname`` parameter is set, Disqus comments are included
-    automatically with every blog post.  This directive allows you to
+    automatically with every feed entry.  This directive allows you to
     use Disqus with regular Sphinx documents.
 
     Options:
 CSS classes
 -----------
 
-``blog-meta``
+``feed-meta``
     Wraps for the post metadata block.
 
-``blog-author``
+``feed-author``
     Wraps the author name.
 
-``blog-date``
+``feed-date``
     Wraps the post date.
 
-``blog-disqus``
+``feed-disqus``
     Wraps the Disqus comment widget.
 
-``blog-ref``
+``feed-ref``
     Wraps the post title in the list of posts.
 
-``blog-more``
+``feed-more``
     Wraps the *Read more...* link.
 
 

File demo/conf.py

 
-# Enable `sphinxcontrib-blog` extension.
-extensions = ['sphinxcontrib.blog']
+# Enable `sphinxcontrib-newsfeed` extension.
+extensions = ['sphinxcontrib.newsfeed']
 
 # Standard configuration.
-project = u'A Project'
+project = u'Elvensense'
 master_doc = 'index'
 html_theme = 'sphinxdoc'
 highlight_language = 'rest'

File demo/first-post.rst

-First Post!!!
-=============
-
-.. blogpost::
-   :date: 2013-01-01
-
-Welcome to my new blog!!!
-

File demo/index.rst

-My Project News
+Elvensense News
 ===============
 
-This is a small demo for ``sphinxcontrib-blog`` extension.
-What follows is the output of ``blogtree`` directive.
+This is a small demo for ``sphinxcontrib-newsfeed`` extension.
+What follows is the output of ``feed`` directive.
 
-.. blogtree::
+.. feed::
    :rss: index.rss
-   :title: My Project News
+   :atom: index.atom
+   :title: Elvensense News
 
-   with-teaser
-   first-post
+   release
+   welcome
 

File demo/release.rst

+Elvensense 96 is released
+=========================
+
+.. feed-entry::
+   :date: 2012-12-31
+
+We are proud to announce a new release of **Elvensense**.
+
+.. cut::
+
+Specific changes since the last release:
+
+* An exciting feature was added.
+* An annoying bug was fixed.
+

File demo/welcome.rst

+Welcome!!!
+==========
+
+.. feed-entry::
+   :date: 2012-01-01
+
+Welcome to the news feed of **Elvensense**.  Here we will post
+release announcements and other project news.
+

File demo/with-teaser.rst

-Post with a Teaser
-==================
-
-.. blogpost::
-   :date: 2013-02-02
-
-Place summary here.
-
-.. blogcut::
-
-Here is the main body of the post.
-
 from setuptools import setup
 
 
-NAME = "sphinxcontrib-blog"
-VERSION = "0.0.1"
-DESCRIPTION = "A Sphinx Blog Extension"
+NAME = "sphinxcontrib-newsfeed"
+VERSION = "0.1"
+DESCRIPTION = "News Feed extension for Sphinx"
 LONG_DESCRIPTION = open('README').read()
 AUTHOR = "Kirill Simonov (Prometheus Research, LLC)"
 AUTHOR_EMAIL = "xi@resolvent.net"
 LICENSE = "BSD"
-URL = "http://bitbucket.org/prometheus/sphinxcontrib-blog"
-DOWNLOAD_URL = "http://pypi.python.org/pypi/sphinxcontrib-blog"
+URL = "http://bitbucket.org/prometheus/sphinxcontrib-newsfeed"
+DOWNLOAD_URL = "http://pypi.python.org/pypi/sphinxcontrib-newsfeed"
 CLASSIFIERS = [
         'Development Status :: 4 - Beta',
         'Intended Audience :: Developers',

File sphinxcontrib/blog.py

-#
-# Copyright (c) 2013, Prometheus Research, LLC
-#
-
-
-from docutils import nodes
-from docutils.parsers.rst import Directive, directives
-from sphinx import addnodes
-from sphinx.util import docname_join
-import os.path, datetime, re
-import PyRSS2Gen
-
-
-class BlogTreeDirective(Directive):
-
-    has_content = True
-    option_spec = {
-            'rss': directives.unchanged,
-            'title': directives.unchanged,
-            'link': directives.unchanged,
-            'description': directives.unchanged,
-    }
-
-    def run(self):
-        env = self.state.document.settings.env
-        output = []
-        entries = []
-        includefiles = []
-        for entry in self.content:
-            if not entry:
-                continue
-            docname = docname_join(env.docname, entry)
-            if docname not in env.found_docs:
-                output.append(self.state.document.reporter.warning(
-                    'blogtree contains reference to nonexisting '
-                    'document %r' % docname, line=self.lineno))
-                env.note_reread()
-            else:
-                entries.append((None, docname))
-                includefiles.append(docname)
-        subnode = addnodes.toctree()
-        subnode['parent'] = env.docname
-        subnode['entries'] = entries
-        subnode['includefiles'] = includefiles
-        subnode['maxdepth'] = 1
-        subnode['glob'] = False
-        subnode['hidden'] = True
-        subnode['numbered'] = False
-        subnode['titlesonly'] = False
-        wrappernode = nodes.compound(classes=['toctree-wrapper'])
-        wrappernode.append(subnode)
-        output.append(wrappernode)
-        subnode = blogtree()
-        subnode['posts'] = includefiles
-        subnode['rss'] = self.options.get('rss')
-        subnode['title'] = self.options.get('title', '')
-        subnode['link'] = self.options.get('link', '')
-        subnode['description'] = self.options.get('description', '')
-        output.append(subnode)
-        return output
-
-
-class BlogPostDirective(Directive):
-
-    option_spec = {
-            'author': directives.unchanged,
-            'date': directives.unchanged_required,
-    }
-
-    def run(self):
-        author = self.options.get('author')
-        date = self.options.get('date')
-        for format in ["%Y-%m-%d", "%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M:%S"]:
-            try:
-                date = datetime.datetime.strptime(date, format)
-            except ValueError:
-                pass
-            else:
-                break
-        else:
-            return [doc.reporter.error("invalid date `%s`" % date,
-                                       lineno=self.lineno)]
-        meta_node = blogmeta(classes=['blog-meta'])
-        meta_node += nodes.Text(u'Published')
-        if author:
-            meta_node += nodes.Text(u' by ')
-            author_node = nodes.emphasis(classes=['blog-author'])
-            author_node += nodes.Text(author)
-            meta_node += author_node
-        if date:
-            meta_node += nodes.Text(u' on ')
-            date_node = nodes.emphasis(classes=['blog-date'])
-            if date.time():
-                date_node += nodes.Text(date)
-            else:
-                date_node += nodes.Text(date.date())
-            meta_node += date_node
-        meta_node['author'] = author
-        meta_node['date'] = date
-        return [meta_node]
-
-
-class BlogCutDirective(Directive):
-
-    def run(self):
-        return [blogcut()]
-
-
-class DisqusDirective(Directive):
-
-    option_spec = {
-            'shortname': directives.unchanged,
-            'identifier': directives.unchanged,
-            'title': directives.unchanged,
-    }
-
-    def run(self):
-        doc = self.state.document
-        env = doc.settings.env
-        node = disqus(classes=['blog-disqus'])
-        if 'shortname' in self.options:
-            node['shortname'] = self.options['shortname']
-        else:
-            if not env.config.disqus_shortname:
-                return [doc.reporter.error("config option `disqus_shortname`"
-                                           " is not set", lineno=self.lineno)]
-            node['shortname'] = env.config.disqus_shortname
-        if 'identifier' in self.options:
-            node['identifier'] = self.options['identifier']
-        else:
-            node['identifier'] = "/%s" % env.docname
-        node['title'] = self.options.get('title')
-        node['developer'] = env.config.disqus_developer
-        return [node]
-
-
-class blogtree(nodes.General, nodes.Element):
-    pass
-
-
-class blogmeta(nodes.paragraph):
-    pass
-
-
-class blogcut(nodes.General, nodes.Element):
-    pass
-
-
-class disqus(nodes.General, nodes.Element):
-    pass
-
-
-def visit_blogmeta(self, node):
-    self.visit_paragraph(node)
-
-
-def depart_blogmeta(self, node):
-    self.depart_paragraph(node)
-
-
-def visit_blogcut(self, node):
-    raise nodes.SkipNode
-
-
-DISQUS_TEMPLATE = """\
-<div id="disqus_thread"></div>
-<script type="text/javascript">
-%(config)s
-(function() {
-    var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
-    dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
-    (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
-})();
-</script>
-<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
-<a href="http://disqus.com" class="dsq-brlink">blog comments powered by <span class="logo-disqus">Disqus</span></a>
-"""
-
-
-def visit_disqus(self, node):
-    shortname = node['shortname']
-    identifier = node['identifier']
-    title = node['title']
-    developer = node['developer']
-    config = []
-    if shortname:
-        config.append(('disqus_shortname', shortname))
-    if identifier:
-        config.append(('disqus_identifier', identifier))
-    if title:
-        config.append(('disqus_title', title))
-    if developer:
-        config.append(('disqus_developer', '1'))
-    config = "".join("var %s = \"%s\";\n"
-                     % (name, value.replace('\\', '\\\\')
-                                   .replace('\"', '\\"'))
-                     for name, value in config)
-    html = DISQUS_TEMPLATE % vars()
-    self.body.append(self.starttag(node, 'div'))
-    self.body.append(html)
-    self.body.append("</div>")
-    raise nodes.SkipNode
-
-
-def process_blogtree(app, doctree, fromdocname):
-    env = app.builder.env
-    if env.config.disqus_shortname and doctree.traverse(blogmeta):
-        node = disqus(classes=['blog-disqus'])
-        node['shortname'] = env.config.disqus_shortname
-        node['identifier'] = "/%s" % fromdocname
-        node['title'] = env.titles[fromdocname][0]
-        node['developer'] = env.config.disqus_developer
-        doctree += node
-    for node in doctree.traverse(blogtree):
-        rss_output = node['rss']
-        rss_title = node['title']
-        rss_link = node['link']
-        rss_description = node['description']
-        rss_items = []
-        replacement = []
-        for docname in node['posts']:
-            blogpost = env.get_doctree(docname)
-            for meta in blogpost.traverse(blogmeta):
-                section_node = nodes.section()
-                title = env.titles[docname]
-                section_node['ids'] = blogpost[0]['ids']
-                title_node = nodes.title()
-                ref_node = nodes.reference(classes=['blog-ref'])
-                ref_node['internal'] = True
-                ref_node['refdocname'] = docname
-                ref_node['refuri'] = \
-                        app.builder.get_relative_uri(fromdocname, docname)
-                ref_node['refuri'] += '#' + section_node['ids'][0]
-                ref_node += title[0]
-                title_node += ref_node
-                section_node += title_node
-                for subnode in blogpost[0]:
-                    if isinstance(subnode, (nodes.title, disqus)):
-                        continue
-                    if isinstance(subnode, blogcut):
-                        para_node = nodes.paragraph()
-                        ref_node = nodes.reference(classes=['blog-more'])
-                        ref_node['internal'] = True
-                        ref_node['refdocname'] = docname
-                        ref_node['refuri'] = \
-                                app.builder.get_relative_uri(fromdocname, docname)
-                        ref_node['refuri'] += '#' + section_node['ids'][0]
-                        ref_node += nodes.Text(u'Read more\u2026')
-                        para_node += ref_node
-                        section_node += para_node
-                        break
-                    section_node += subnode.deepcopy()
-                env.resolve_references(section_node, fromdocname, app.builder)
-                replacement.append(section_node)
-                if rss_output:
-                    rss_item_title = title[0]
-                    rss_item_link = rss_link+app.builder.get_target_uri(docname)
-                    rss_item_description = nodes.compound()
-                    for subnode in blogpost[0]:
-                        if isinstance(subnode, (nodes.title, blogmeta, disqus)):
-                            continue
-                        if isinstance(subnode, blogcut):
-                            break
-                        rss_item_description += subnode.deepcopy()
-                    env.resolve_references(rss_item_description, docname,
-                                           app.builder)
-                    rss_item_description = app.builder.render_partial(
-                                                    rss_item_description)['body']
-                    rss_item_date = meta['date']
-                    rss_item = PyRSS2Gen.RSSItem(
-                            title = rss_item_title,
-                            link = rss_item_link,
-                            description = rss_item_description,
-                            guid = PyRSS2Gen.Guid(rss_item_link),
-                            pubDate = rss_item_date)
-                    rss_items.append(rss_item)
-        node.replace_self(replacement)
-        if rss_output:
-            rss_path = os.path.join(app.builder.outdir, rss_output)
-            rss = PyRSS2Gen.RSS2(
-                    title = rss_title,
-                    link = rss_link,
-                    description = rss_description,
-                    lastBuildDate = datetime.datetime.utcnow(),
-                    items = rss_items)
-            rss.write_xml(open(rss_path, "w"), encoding="utf-8")
-
-
-def setup(app):
-    app.add_config_value('disqus_shortname', None, 'env')
-    app.add_config_value('disqus_developer', False, 'env')
-    app.add_directive('blogtree', BlogTreeDirective)
-    app.add_directive('blogpost', BlogPostDirective)
-    app.add_directive('blogcut', BlogCutDirective)
-    app.add_directive('disqus', DisqusDirective)
-    app.add_node(blogtree)
-    app.add_node(blogmeta,
-                 html=(visit_blogmeta, depart_blogmeta))
-    app.add_node(blogcut,
-                 html=(visit_blogcut, None))
-    app.add_node(disqus,
-                 html=(visit_disqus, None))
-    app.connect('doctree-resolved', process_blogtree)
-
-

File sphinxcontrib/newsfeed.py

+#
+# Copyright (c) 2013, Prometheus Research, LLC
+#
+
+
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+from sphinx import addnodes
+from sphinx.util import docname_join
+import os.path, datetime, re
+import PyRSS2Gen
+
+
+class FeedDirective(Directive):
+
+    has_content = True
+    option_spec = {
+            'rss': directives.unchanged,
+            'atom': directives.unchanged,
+            'title': directives.unchanged,
+            'link': directives.unchanged,
+            'description': directives.unchanged,
+    }
+
+    def run(self):
+        env = self.state.document.settings.env
+        output = []
+        entries = []
+        includefiles = []
+        for entry in self.content:
+            if not entry:
+                continue
+            docname = docname_join(env.docname, entry)
+            if docname not in env.found_docs:
+                output.append(self.state.document.reporter.warning(
+                    'feed contains a reference to nonexisting '
+                    'document %r' % docname, line=self.lineno))
+                env.note_reread()
+            else:
+                entries.append((None, docname))
+                includefiles.append(docname)
+        subnode = addnodes.toctree()
+        subnode['parent'] = env.docname
+        subnode['entries'] = entries
+        subnode['includefiles'] = includefiles
+        subnode['maxdepth'] = 1
+        subnode['glob'] = False
+        subnode['hidden'] = True
+        subnode['numbered'] = False
+        subnode['titlesonly'] = False
+        wrappernode = nodes.compound(classes=['toctree-wrapper'])
+        wrappernode.append(subnode)
+        output.append(wrappernode)
+        subnode = feed()
+        subnode['entries'] = includefiles
+        subnode['rss'] = self.options.get('rss')
+        subnode['atom'] = self.options.get('atom')
+        subnode['title'] = self.options.get('title', '')
+        subnode['link'] = self.options.get('link', '')
+        subnode['description'] = self.options.get('description', '')
+        output.append(subnode)
+        return output
+
+
+class FeedEntryDirective(Directive):
+
+    option_spec = {
+            'author': directives.unchanged,
+            'date': directives.unchanged_required,
+    }
+
+    def run(self):
+        author = self.options.get('author')
+        date = self.options.get('date')
+        for format in ["%Y-%m-%d", "%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M:%S"]:
+            try:
+                date = datetime.datetime.strptime(date, format)
+            except ValueError:
+                pass
+            else:
+                break
+        else:
+            return [doc.reporter.error("invalid date `%s`" % date,
+                                       lineno=self.lineno)]
+        meta_node = entrymeta(classes=['feed-meta'])
+        meta_node += nodes.Text(u'Published')
+        if author:
+            meta_node += nodes.Text(u' by ')
+            author_node = nodes.emphasis(classes=['feed-author'])
+            author_node += nodes.Text(author)
+            meta_node += author_node
+        if date:
+            meta_node += nodes.Text(u' on ')
+            date_node = nodes.emphasis(classes=['feed-date'])
+            if date.time():
+                date_node += nodes.Text(date)
+            else:
+                date_node += nodes.Text(date.date())
+            meta_node += date_node
+        meta_node['author'] = author
+        meta_node['date'] = date
+        return [meta_node]
+
+
+class CutDirective(Directive):
+
+    def run(self):
+        return [entrycut()]
+
+
+class DisqusDirective(Directive):
+
+    option_spec = {
+            'shortname': directives.unchanged,
+            'identifier': directives.unchanged,
+            'title': directives.unchanged,
+    }
+
+    def run(self):
+        doc = self.state.document
+        env = doc.settings.env
+        node = disqus(classes=['feed-disqus'])
+        if 'shortname' in self.options:
+            node['shortname'] = self.options['shortname']
+        else:
+            if not env.config.disqus_shortname:
+                return [doc.reporter.error("config option `disqus_shortname`"
+                                           " is not set", lineno=self.lineno)]
+            node['shortname'] = env.config.disqus_shortname
+        if 'identifier' in self.options:
+            node['identifier'] = self.options['identifier']
+        else:
+            node['identifier'] = "/%s" % env.docname
+        node['title'] = self.options.get('title')
+        node['developer'] = env.config.disqus_developer
+        return [node]
+
+
+class feed(nodes.General, nodes.Element):
+    pass
+
+
+class entrymeta(nodes.paragraph):
+    pass
+
+
+class entrycut(nodes.General, nodes.Element):
+    pass
+
+
+class disqus(nodes.General, nodes.Element):
+    pass
+
+
+def visit_entrymeta(self, node):
+    self.visit_paragraph(node)
+
+
+def depart_entrymeta(self, node):
+    self.depart_paragraph(node)
+
+
+def visit_entrycut(self, node):
+    raise nodes.SkipNode
+
+
+DISQUS_TEMPLATE = """\
+<div id="disqus_thread"></div>
+<script type="text/javascript">
+%(config)s
+(function() {
+    var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
+    dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
+    (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
+})();
+</script>
+<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
+<a href="http://disqus.com" class="dsq-brlink">blog comments powered by <span class="logo-disqus">Disqus</span></a>
+"""
+
+
+def visit_disqus(self, node):
+    shortname = node['shortname']
+    identifier = node['identifier']
+    title = node['title']
+    developer = node['developer']
+    config = []
+    if shortname:
+        config.append(('disqus_shortname', shortname))
+    if identifier:
+        config.append(('disqus_identifier', identifier))
+    if title:
+        config.append(('disqus_title', title))
+    if developer:
+        config.append(('disqus_developer', '1'))
+    config = "".join("var %s = \"%s\";\n"
+                     % (name, value.replace('\\', '\\\\')
+                                   .replace('\"', '\\"'))
+                     for name, value in config)
+    html = DISQUS_TEMPLATE % vars()
+    self.body.append(self.starttag(node, 'div'))
+    self.body.append(html)
+    self.body.append("</div>")
+    raise nodes.SkipNode
+
+
+def process_feed(app, doctree, fromdocname):
+    env = app.builder.env
+    if env.config.disqus_shortname and doctree.traverse(entrymeta):
+        node = disqus(classes=['feed-disqus'])
+        node['shortname'] = env.config.disqus_shortname
+        node['identifier'] = "/%s" % fromdocname
+        node['title'] = env.titles[fromdocname][0]
+        node['developer'] = env.config.disqus_developer
+        doctree += node
+    for node in doctree.traverse(feed):
+        rss_output = node['rss']
+        rss_title = node['title']
+        rss_link = node['link']
+        rss_description = node['description']
+        rss_items = []
+        replacement = []
+        for docname in node['entries']:
+            entry = env.get_doctree(docname)
+            for meta in entry.traverse(entrymeta):
+                section_node = nodes.section()
+                title = env.titles[docname]
+                section_node['ids'] = entry[0]['ids']
+                title_node = nodes.title()
+                ref_node = nodes.reference(classes=['feed-ref'])
+                ref_node['internal'] = True
+                ref_node['refdocname'] = docname
+                ref_node['refuri'] = \
+                        app.builder.get_relative_uri(fromdocname, docname)
+                ref_node['refuri'] += '#' + section_node['ids'][0]
+                ref_node += title[0]
+                title_node += ref_node
+                section_node += title_node
+                for subnode in entry[0]:
+                    if isinstance(subnode, (nodes.title, disqus)):
+                        continue
+                    if isinstance(subnode, entrycut):
+                        para_node = nodes.paragraph()
+                        ref_node = nodes.reference(classes=['feed-more'])
+                        ref_node['internal'] = True
+                        ref_node['refdocname'] = docname
+                        ref_node['refuri'] = \
+                                app.builder.get_relative_uri(fromdocname, docname)
+                        ref_node['refuri'] += '#' + section_node['ids'][0]
+                        ref_node += nodes.Text(u'Read more\u2026')
+                        para_node += ref_node
+                        section_node += para_node
+                        break
+                    section_node += subnode.deepcopy()
+                env.resolve_references(section_node, fromdocname, app.builder)
+                replacement.append(section_node)
+                if rss_output:
+                    rss_item_title = title[0]
+                    rss_item_link = rss_link+app.builder.get_target_uri(docname)
+                    rss_item_description = nodes.compound()
+                    for subnode in entry[0]:
+                        if isinstance(subnode, (nodes.title, entrymeta, disqus)):
+                            continue
+                        if isinstance(subnode, entrycut):
+                            break
+                        rss_item_description += subnode.deepcopy()
+                    env.resolve_references(rss_item_description, docname,
+                                           app.builder)
+                    rss_item_description = app.builder.render_partial(
+                                                    rss_item_description)['body']
+                    rss_item_date = meta['date']
+                    rss_item = PyRSS2Gen.RSSItem(
+                            title = rss_item_title,
+                            link = rss_item_link,
+                            description = rss_item_description,
+                            guid = PyRSS2Gen.Guid(rss_item_link),
+                            pubDate = rss_item_date)
+                    rss_items.append(rss_item)
+        node.replace_self(replacement)
+        if rss_output:
+            rss_path = os.path.join(app.builder.outdir, rss_output)
+            rss = PyRSS2Gen.RSS2(
+                    title = rss_title,
+                    link = rss_link,
+                    description = rss_description,
+                    lastBuildDate = datetime.datetime.utcnow(),
+                    items = rss_items)
+            rss.write_xml(open(rss_path, "w"), encoding="utf-8")
+
+
+def setup(app):
+    app.add_config_value('disqus_shortname', None, 'env')
+    app.add_config_value('disqus_developer', False, 'env')
+    app.add_directive('feed', FeedDirective)
+    app.add_directive('feed-entry', FeedEntryDirective)
+    app.add_directive('cut', CutDirective)
+    app.add_directive('disqus', DisqusDirective)
+    app.add_node(feed)
+    app.add_node(entrymeta,
+                 html=(visit_entrymeta, depart_entrymeta))
+    app.add_node(entrycut,
+                 html=(visit_entrycut, None))
+    app.add_node(disqus,
+                 html=(visit_disqus, None))
+    app.connect('doctree-resolved', process_feed)
+
+