Commits

DasIch  committed 57e54a1 Merge
  • Participants
  • Parent commits 6db3a54, e3ab6b1

Comments (0)

Files changed (42)

 b494009dccf19d0df894a4ce50754e866a099808 1.0b2
 dc883adf6424f1265e6b6d3c95c23e7c562e1206 1.0
 8c458e9894f29458a0507e747e0c3bab396a9b8a 1.0.1
+3fcdbfa3afb181d3bec533be76ab1fcb446416bb 1.0.2
 * Added Python 3.x support.
 
 
-Release 1.0.2 (in development)
-==============================
+Release 1.0.2 (Aug 14, 2010)
+============================
+
+* #490: Fix cross-references to objects of types added by the
+  :func:`~.Sphinx.add_object_type` API function.
+
+* Fix handling of doc field types for different directive types.
 
 * Allow breaking long signatures, continuing with backlash-escaped
   newlines.

File CHANGES.jacobmason

+May 30: Added files builders/websupport.py, writers/websupport.py,
+websupport/api.py, and websupport/document.api. Provides a rudimentary 
+method of building websupport data, and rendering it as html.
+
+May 31-June 10: Continued changing way web support data is represented
+and accessed.
+
+June 14 - June 17: Continued making improvements to the web support package 
+and demo web application. Included sidebars, navlinks etc...
+
+June 21 - June 26: Implement server side search with two search adapters,
+one for Xapian and one for Whoosh
+
+June 28 - July 12: Implement voting system on the backend, and created a
+jQuery script to handle voting on the frontend.
+
+July 13 - July 19: Added documentation for the web support package.
+
+July 20 - July 27: Added a system to allow user's to propose changes to
+documentation along with comments.
+
+July 28 - August 3: Added tests for the web support package. Refactored
+sqlalchemy storage to be more efficient.
+
+August 4 - August 7: Added comment moderation system. Added more 
+documentation. General code cleanup.
+
 
 include babel.cfg
 include Makefile
-include setup_distribute.py
+include distribute_setup.py
 include sphinx-autogen.py
 include sphinx-build.py
 include sphinx-quickstart.py

File doc/contents.rst

    theming
    templating
    extensions
+   websupport
 
    faq
    glossary

File doc/ext/autodoc.rst

      will document all non-private member functions and properties (that is,
      those whose name doesn't start with ``_``).
 
+     For modules, ``__all__`` will be respected when looking for members; the
+     order of the members will also be the order in ``__all__``.
+
      You can also give an explicit list of members; only these will then be
      documented::
 

File doc/web/api.rst

+.. _websupportapi:
+
+.. currentmodule:: sphinx.websupport
+
+The WebSupport Class
+====================
+
+.. class:: WebSupport
+
+    The main API class for the web support package. All interactions
+    with the web support package should occur through this class.
+
+    The class takes the following keyword arguments:
+
+    srcdir
+        The directory containing reStructuredText source files.
+
+    builddir
+        The directory that build data and static files should be placed in.
+        This should be used when creating a :class:`WebSupport` object that
+        will be used to build data.
+
+    datadir:
+        The directory that the web support data is in. This should be used
+        when creating a :class:`WebSupport` object that will be used to
+        retrieve data.
+
+    search:
+        This may contain either a string (e.g. 'xapian') referencing a
+        built-in search adapter to use, or an instance of a subclass of
+        :class:`~sphinx.websupport.search.BaseSearch`.
+
+    storage:
+        This may contain either a string representing a database uri, or an
+        instance of a subclass of
+        :class:`~sphinx.websupport.storage.StorageBackend`. If this is not
+        provided a new sqlite database will be created.
+
+    moderation_callback:
+        A callable to be called when a new comment is added that is not
+        displayed. It must accept one argument: a dict representing the
+        comment that was added.
+
+    staticdir:
+        If static files are served from a location besides "/static", this
+        should be a string with the name of that location
+        (e.g. '/static_files').
+
+    docroot:
+        If the documentation is not served from the base path of a URL, this
+        should be a string specifying that path (e.g. 'docs')
+
+Methods
+~~~~~~~
+
+.. automethod:: sphinx.websupport.WebSupport.build
+
+.. automethod:: sphinx.websupport.WebSupport.get_document
+
+.. automethod:: sphinx.websupport.WebSupport.get_data
+
+.. automethod:: sphinx.websupport.WebSupport.add_comment
+
+.. automethod:: sphinx.websupport.WebSupport.process_vote
+
+.. automethod:: sphinx.websupport.WebSupport.get_search_results

File doc/web/quickstart.rst

+.. _websupportquickstart:
+
+Web Support Quick Start
+=======================
+
+Building Documentation Data
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To make use of the web support package in your application you'll
+need to build the data it uses. This data includes pickle files representing
+documents, search indices, and node data that is used to track where
+comments and other things are in a document. To do this you will need
+to create an instance of the :class:`~sphinx.websupport.WebSupport`
+class and call it's :meth:`~sphinx.websupport.WebSupport.build` method::
+
+    from sphinx.websupport import WebSupport
+
+    support = WebSupport(srcdir='/path/to/rst/sources/',
+                         builddir='/path/to/build/outdir',
+                         search='xapian')
+
+    support.build()
+
+This will read reStructuredText sources from `srcdir` and place the
+necessary data in `builddir`. The `builddir` will contain two
+sub-directories. One named "data" that contains all the data needed
+to display documents, search through documents, and add comments to
+documents. The other directory will be called "static" and contains static
+files that should be served from "/static".
+
+.. note::
+
+    If you wish to serve static files from a path other than "/static", you
+    can do so by providing the *staticdir* keyword argument when creating
+    the :class:`~sphinx.websupport.api.WebSupport` object.
+
+Integrating Sphinx Documents Into Your Webapp
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now that the data is built, it's time to do something useful with it.
+Start off by creating a :class:`~sphinx.websupport.WebSupport` object
+for your application::
+
+    from sphinx.websupport import WebSupport
+
+    support = WebSupport(datadir='/path/to/the/data',
+                         search='xapian')
+
+You'll only need one of these for each set of documentation you will be
+working with. You can then call it's
+:meth:`~sphinx.websupport.WebSupport.get_document` method to access
+individual documents::
+
+    contents = support.get_document('contents')
+
+This will return a dictionary containing the following items:
+
+* **body**: The main body of the document as HTML
+* **sidebar**: The sidebar of the document as HTML
+* **relbar**: A div containing links to related documents
+* **title**: The title of the document
+* **css**: Links to css files used by Sphinx
+* **js**: Javascript containing comment options
+
+This dict can then be used as context for templates. The goal is to be
+easy to integrate with your existing templating system. An example using
+`Jinja2 <http://jinja.pocoo.org/2/>`_ is:
+
+.. sourcecode:: html+jinja
+
+    {%- extends "layout.html" %}
+
+    {%- block title %}
+        {{ document.title }}
+    {%- endblock %}
+
+    {% block css %}
+        {{ super() }}
+        {{ document.css|safe }}
+        <link rel="stylesheet" href="/static/websupport-custom.css" type="text/css">
+    {% endblock %}
+
+    {%- block js %}
+        {{ super() }}
+        {{ document.js|safe }}
+    {%- endblock %}
+
+    {%- block relbar %}
+        {{ document.relbar|safe }}
+    {%- endblock %}
+
+    {%- block body %}
+        {{ document.body|safe }}
+    {%- endblock %}
+
+    {%- block sidebar %}
+        {{ document.sidebar|safe }}
+    {%- endblock %}
+
+Authentication
+--------------
+
+To use certain features such as voting it must be possible to authenticate
+users. The details of the authentication are left to your application.
+Once a user has been authenticated you can pass the user's details to certain
+:class:`~sphinx.websupport.WebSupport` methods using the *username* and
+*moderator* keyword arguments. The web support package will store the
+username with comments and votes. The only caveat is that if you allow users
+to change their username you must update the websupport package's data::
+
+    support.update_username(old_username, new_username)
+
+*username* should be a unique string which identifies a user, and *moderator*
+should be a boolean representing whether the user has moderation
+privilieges. The default value for *moderator* is *False*.
+
+An example `Flask <http://flask.pocoo.org/>`_ function that checks whether
+a user is logged in and then retrieves a document is::
+
+    from sphinx.websupport.errors import *
+
+    @app.route('/<path:docname>')
+    def doc(docname):
+        username = g.user.name if g.user else ''
+        moderator = g.user.moderator if g.user else False
+        try:
+            document = support.get_document(docname, username, moderator)
+        except DocumentNotFoundError:
+            abort(404)
+        return render_template('doc.html', document=document)
+
+The first thing to notice is that the *docname* is just the request path.
+This makes accessing the correct document easy from a single view.
+If the user is authenticated then the username and moderation status are
+passed along with the docname to
+:meth:`~sphinx.websupport.WebSupport.get_document`. The web support package
+will then add this data to the COMMENT_OPTIONS that are used in the template.
+
+.. note::
+
+    This only works works if your documentation is served from your
+    document root. If it is served from another directory, you will
+    need to prefix the url route with that directory, and give the `docroot`
+    keyword argument when creating the web support object::
+
+        support = WebSupport(..., docroot='docs')
+
+        @app.route('/docs/<path:docname>')
+
+Performing Searches
+~~~~~~~~~~~~~~~~~~~
+
+To use the search form built-in to the Sphinx sidebar, create a function
+to handle requests to the url 'search' relative to the documentation root.
+The user's search query will be in the GET parameters, with the key `q`.
+Then use the :meth:`~sphinx.websupport.WebSupport.get_search_results` method
+to retrieve search results. In `Flask <http://flask.pocoo.org/>`_ that
+would be like this::
+
+    @app.route('/search')
+    def search():
+        q = request.args.get('q')
+        document = support.get_search_results(q)
+        return render_template('doc.html', document=document)
+
+Note that we used the same template to render our search results as we
+did to render our documents. That's because
+:meth:`~sphinx.websupport.WebSupport.get_search_results` returns a context
+dict in the same format that
+:meth:`~sphinx.websupport.WebSupport.get_document` does.
+
+Comments & Proposals
+~~~~~~~~~~~~~~~~~~~~
+
+Now that this is done it's time to define the functions that handle
+the AJAX calls from the script. You will need three functions. The first
+function is used to add a new comment, and will call the web support method
+:meth:`~sphinx.websupport.WebSupport.add_comment`::
+
+    @app.route('/docs/add_comment', methods=['POST'])
+    def add_comment():
+        parent_id = request.form.get('parent', '')
+        node_id = request.form.get('node', '')
+        text = request.form.get('text', '')
+        proposal = request.form.get('proposal', '')
+        username = g.user.name if g.user is not None else 'Anonymous'
+        comment = support.add_comment(text, node_id='node_id',
+                                      parent_id='parent_id',
+                                      username=username, proposal=proposal)
+        return jsonify(comment=comment)
+
+You'll notice that both a `parent_id` and `node_id` are sent with the
+request. If the comment is being attached directly to a node, `parent_id`
+will be empty. If the comment is a child of another comment, then `node_id`
+will be empty. Then next function handles the retrieval of comments for a
+specific node, and is aptly named
+:meth:`~sphinx.websupport.WebSupport.get_data`::
+
+    @app.route('/docs/get_comments')
+    def get_comments():
+        username = g.user.name if g.user else None
+        moderator = g.user.moderator if g.user else False
+        node_id = request.args.get('node', '')
+        data = support.get_data(parent_id, user_id)
+        return jsonify(**data)
+
+The final function that is needed will call
+:meth:`~sphinx.websupport.WebSupport.process_vote`, and will handle user
+votes on comments::
+
+    @app.route('/docs/process_vote', methods=['POST'])
+    def process_vote():
+        if g.user is None:
+            abort(401)
+        comment_id = request.form.get('comment_id')
+        value = request.form.get('value')
+        if value is None or comment_id is None:
+            abort(400)
+        support.process_vote(comment_id, g.user.id, value)
+        return "success"
+
+Comment Moderation
+~~~~~~~~~~~~~~~~~~
+
+By default all comments added through
+:meth:`~sphinx.websupport.WebSupport.add_comment` are automatically
+displayed. If you wish to have some form of moderation, you can pass
+the `displayed` keyword argument::
+
+    comment = support.add_comment(text, node_id='node_id',
+                                  parent_id='parent_id',
+                                  username=username, proposal=proposal,
+                                  displayed=False)
+
+You can then create two new views to handle the moderation of comments. The
+first will be called when a moderator decides a comment should be accepted
+and displayed::
+
+    @app.route('/docs/accept_comment', methods=['POST'])
+    def accept_comment():
+        moderator = g.user.moderator if g.user else False
+        comment_id = request.form.get('id')
+        support.accept_comment(comment_id, moderator=moderator)
+        return 'OK'
+
+The next is very similar, but used when rejecting a comment::
+
+    @app.route('/docs/reject_comment', methods=['POST'])
+    def reject_comment():
+        moderator = g.user.moderator if g.user else False
+        comment_id = request.form.get('id')
+        support.reject_comment(comment_id, moderator=moderator)
+        return 'OK'
+
+To perform a custom action (such as emailing a moderator) when a new comment
+is added but not displayed, you can pass callable to the
+:class:`~sphinx.websupport.WebSupport` class when instantiating your support
+object::
+
+    def moderation_callback(comment):
+        """Do something..."""
+
+    support = WebSupport(..., moderation_callback=moderation_callback)
+
+The moderation callback must take one argument, which will be the same
+comment dict that is returned by add_comment.

File doc/web/searchadapters.rst

+.. _searchadapters:
+
+.. currentmodule:: sphinx.websupport.search
+
+Search Adapters
+===============
+
+To create a custom search adapter you will need to subclass the
+:class:`~BaseSearch` class. Then create an instance of the new class
+and pass that as the `search` keyword argument when you create the
+:class:`~sphinx.websupport.WebSupport` object::
+
+    support = Websupport(srcdir=srcdir,
+                         builddir=builddir,
+                         search=MySearch())
+
+For more information about creating a custom search adapter, please see
+the documentation of the :class:`BaseSearch` class below.
+
+.. class:: BaseSearch
+
+    Defines an interface for search adapters.
+
+BaseSearch Methods
+~~~~~~~~~~~~~~~~~~
+
+    The following methods are defined in the BaseSearch class. Some methods
+    do not need to be overridden, but some (
+    :meth:`~sphinx.websupport.search.BaseSearch.add_document` and
+    :meth:`~sphinx.websupport.search.BaseSearch.handle_query`) must be
+    overridden in your subclass. For a working example, look at the
+    built-in adapter for whoosh.
+
+.. automethod:: sphinx.websupport.search.BaseSearch.init_indexing
+
+.. automethod:: sphinx.websupport.search.BaseSearch.finish_indexing
+
+.. automethod:: sphinx.websupport.search.BaseSearch.feed
+
+.. automethod:: sphinx.websupport.search.BaseSearch.add_document
+
+.. automethod:: sphinx.websupport.search.BaseSearch.query
+
+.. automethod:: sphinx.websupport.search.BaseSearch.handle_query
+
+.. automethod:: sphinx.websupport.search.BaseSearch.extract_context

File doc/web/storagebackends.rst

+.. _storagebackends:
+
+.. currentmodule:: sphinx.websupport.storage
+
+Storage Backends
+================
+
+To create a custom storage backend you will need to subclass the
+:class:`~StorageBackend` class. Then create an instance of the new class
+and pass that as the `storage` keyword argument when you create the
+:class:`~sphinx.websupport.WebSupport` object::
+
+    support = Websupport(srcdir=srcdir,
+                         builddir=builddir,
+                         storage=MyStorage())
+
+For more information about creating a custom storage backend, please see
+the documentation of the :class:`StorageBackend` class below.
+
+.. class:: StorageBackend
+
+    Defines an interface for storage backends.
+
+StorageBackend Methods
+~~~~~~~~~~~~~~~~~~~~~~
+
+.. automethod:: sphinx.websupport.storage.StorageBackend.pre_build
+
+.. automethod:: sphinx.websupport.storage.StorageBackend.add_node
+
+.. automethod:: sphinx.websupport.storage.StorageBackend.post_build
+
+.. automethod:: sphinx.websupport.storage.StorageBackend.add_comment
+
+.. automethod:: sphinx.websupport.storage.StorageBackend.delete_comment
+
+.. automethod:: sphinx.websupport.storage.StorageBackend.get_data
+
+.. automethod:: sphinx.websupport.storage.StorageBackend.process_vote
+
+.. automethod:: sphinx.websupport.storage.StorageBackend.update_username
+
+.. automethod:: sphinx.websupport.storage.StorageBackend.accept_comment
+
+.. automethod:: sphinx.websupport.storage.StorageBackend.reject_comment

File doc/websupport.rst

+.. _websupport:
+
+Sphinx Web Support
+==================
+
+Sphinx provides a way to easily integrate Sphinx documentation
+into your web application. To learn more read the
+:ref:`websupportquickstart`.
+
+.. toctree::
+
+    web/quickstart
+    web/api
+    web/searchadapters
+    web/storagebackends

File sphinx/__init__.py

File contents unchanged.

File sphinx/builders/__init__.py

     'man':        ('manpage', 'ManualPageBuilder'),
     'changes':    ('changes', 'ChangesBuilder'),
     'linkcheck':  ('linkcheck', 'CheckExternalLinksBuilder'),
+    'websupport': ('websupport', 'WebSupportBuilder'),
 }

File sphinx/builders/websupport.py

+# -*- coding: utf-8 -*-
+"""
+    sphinx.builders.websupport
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Builder for the web support package.
+
+    :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import cPickle as pickle
+from os import path
+from cgi import escape
+import posixpath
+import shutil
+
+from docutils.io import StringOutput
+
+from sphinx.util.osutil import os_path, relative_uri, ensuredir, copyfile
+from sphinx.util.jsonimpl import dumps as dump_json
+from sphinx.util.websupport import is_commentable
+from sphinx.builders.html import StandaloneHTMLBuilder
+from sphinx.builders.versioning import VersioningBuilderMixin
+from sphinx.writers.websupport import WebSupportTranslator
+
+
+class WebSupportBuilder(StandaloneHTMLBuilder, VersioningBuilderMixin):
+    """
+    Builds documents for the web support package.
+    """
+    name = 'websupport'
+    out_suffix = '.fpickle'
+
+    def init(self):
+        StandaloneHTMLBuilder.init(self)
+        VersioningBuilderMixin.init(self)
+
+    def init_translator_class(self):
+        self.translator_class = WebSupportTranslator
+
+    def write_doc(self, docname, doctree):
+        destination = StringOutput(encoding='utf-8')
+        doctree.settings = self.docsettings
+
+        self.handle_versioning(docname, doctree, is_commentable)
+
+        self.cur_docname = docname
+        self.secnumbers = self.env.toc_secnumbers.get(docname, {})
+        self.imgpath = '/' + posixpath.join(self.app.staticdir, '_images')
+        self.post_process_images(doctree)
+        self.dlpath = '/' + posixpath.join(self.app.staticdir, '_downloads')
+        self.docwriter.write(doctree, destination)
+        self.docwriter.assemble_parts()
+        body = self.docwriter.parts['fragment']
+        metatags = self.docwriter.clean_meta
+
+        ctx = self.get_doc_context(docname, body, metatags)
+        self.index_page(docname, doctree, ctx.get('title', ''))
+        self.handle_page(docname, ctx, event_arg=doctree)
+
+    def get_target_uri(self, docname, typ=None):
+        return docname
+
+    def load_indexer(self, docnames):
+        self.indexer = self.app.search
+        self.indexer.init_indexing(changed=docnames)
+
+    def handle_page(self, pagename, addctx, templatename='page.html',
+                    outfilename=None, event_arg=None):
+        # This is mostly copied from StandaloneHTMLBuilder. However, instead
+        # of rendering the template and saving the html, create a context
+        # dict and pickle it.
+        ctx = self.globalcontext.copy()
+        ctx['pagename'] = pagename
+
+        def pathto(otheruri, resource=False,
+                   baseuri=self.get_target_uri(pagename)):
+            if not resource:
+                otheruri = self.get_target_uri(otheruri)
+                return relative_uri(baseuri, otheruri) or '#'
+            else:
+                return '/' + posixpath.join(self.app.staticdir, otheruri)
+        ctx['pathto'] = pathto
+        ctx['hasdoc'] = lambda name: name in self.env.all_docs
+        ctx['encoding'] = encoding = self.config.html_output_encoding
+        ctx['toctree'] = lambda **kw: self._get_local_toctree(pagename, **kw)
+        self.add_sidebars(pagename, ctx)
+        ctx.update(addctx)
+
+        self.app.emit('html-page-context', pagename, templatename,
+                      ctx, event_arg)
+
+        # Create a dict that will be pickled and used by webapps.
+        css = '<link rel="stylesheet" href="%s" type=text/css />' % \
+            pathto('_static/pygments.css', 1)
+        doc_ctx = {'body': ctx.get('body', ''),
+                   'title': ctx.get('title', ''),
+                   'css': css,
+                   'js': self._make_js(ctx)}
+        # Partially render the html template to proved a more useful ctx.
+        template = self.templates.environment.get_template(templatename)
+        template_module = template.make_module(ctx)
+        if hasattr(template_module, 'sidebar'):
+            doc_ctx['sidebar'] = template_module.sidebar()
+        if hasattr(template_module, 'relbar'):
+            doc_ctx['relbar'] = template_module.relbar()
+
+        if not outfilename:
+            outfilename = path.join(self.outdir, 'pickles',
+                                    os_path(pagename) + self.out_suffix)
+
+        ensuredir(path.dirname(outfilename))
+        f = open(outfilename, 'wb')
+        try:
+            pickle.dump(doc_ctx, f, pickle.HIGHEST_PROTOCOL)
+        finally:
+            f.close()
+
+        # if there is a source file, copy the source file for the
+        # "show source" link
+        if ctx.get('sourcename'):
+            source_name = path.join(self.app.builddir, self.app.staticdir,
+                                    '_sources',  os_path(ctx['sourcename']))
+            ensuredir(path.dirname(source_name))
+            copyfile(self.env.doc2path(pagename), source_name)
+
+    def handle_finish(self):
+        StandaloneHTMLBuilder.handle_finish(self)
+        VersioningBuilderMixin.finish(self)
+        directories = ['_images', '_static']
+        for directory in directories:
+            src = path.join(self.outdir, directory)
+            dst = path.join(self.app.builddir, self.app.staticdir, directory)
+            if path.isdir(src):
+                if path.isdir(dst):
+                    shutil.rmtree(dst)
+                shutil.move(src, dst)
+
+    def dump_search_index(self):
+        self.indexer.finish_indexing()
+
+    def _make_js(self, ctx):
+        def make_script(file):
+            path = ctx['pathto'](file, 1)
+            return '<script type="text/javascript" src="%s"></script>' % path
+
+        opts = {
+            'URL_ROOT': ctx.get('url_root', ''),
+            'VERSION': ctx['release'],
+            'COLLAPSE_INDEX': False,
+            'FILE_SUFFIX': '',
+            'HAS_SOURCE': ctx['has_source']
+        }
+        scripts = [make_script(file) for file in ctx['script_files']]
+        scripts.append(make_script('_static/websupport.js'))
+        return '\n'.join([
+            '<script type="text/javascript">'
+            'var DOCUMENTATION_OPTIONS = %s;' % dump_json(opts),
+            '</script>'
+        ] + scripts)

File sphinx/directives/__init__.py

             self.env.temp_data['object'] = self.names[0]
         self.before_content()
         self.state.nested_parse(self.content, self.content_offset, contentnode)
-        #self.handle_doc_fields(contentnode)
         DocFieldTransformer(self).transform_all(contentnode)
         self.env.temp_data['object'] = None
         self.after_content()

File sphinx/domains/std.py

                 return make_refnode(builder, fromdocname, docname,
                                     labelid, contnode)
         else:
-            docname, labelid = self.data['objects'].get((typ, target), ('', ''))
+            objtypes = self.objtypes_for_role(typ) or []
+            for objtype in objtypes:
+                if (objtype, target) in self.data['objects']:
+                    docname, labelid = self.data['objects'][objtype, target]
+                    break
+            else:
+                docname, labelid = '', ''
             if not docname:
                 if typ == 'term':
                     env.warn(node.get('refdoc', fromdocname),

File sphinx/themes/basic/searchresults.html

+{#
+    basic/searchresults.html
+    ~~~~~~~~~~~~~~~~~
+
+    Template for the body of the search results page.
+
+    :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+#}
+<h1 id="search-documentation">Search</h1>
+<p>
+  From here you can search these documents. Enter your search
+  words into the box below and click "search".
+</p>
+<form action="" method="get">
+  <input type="text" name="q" value="" />
+  <input type="submit" value="search" />
+  <span id="search-progress" style="padding-left: 10px"></span>
+</form>
+{% if search_performed %}
+<h2>Search Results</h2>
+{% if not search_results %}
+<p>Your search did not match any results.</p>
+{% endif %}
+{% endif %}
+<div id="search-results">
+  {% if search_results %}
+  <ul class="search">
+    {% for href, caption, context in search_results %}
+    <li><a href="{{ href }}?highlight={{ q }}">{{ caption }}</a>
+      <div class="context">{{ context|e }}</div>
+    </li>
+    {% endfor %}
+  </ul>
+  {% endif %}
+</div>

File sphinx/themes/basic/static/ajax-loader.gif

Added
New image

File sphinx/themes/basic/static/comment-bright.png

Added
New image

File sphinx/themes/basic/static/comment.png

Added
New image

File sphinx/themes/basic/static/down-pressed.png

Added
New image

File sphinx/themes/basic/static/down.png

Added
New image

File sphinx/themes/basic/static/up-pressed.png

Added
New image

File sphinx/themes/basic/static/up.png

Added
New image

File sphinx/themes/basic/static/websupport.js

+(function($) {
+  $.fn.autogrow = function(){
+    return this.each(function(){
+    var textarea = this;
+
+    $.fn.autogrow.resize(textarea);
+
+    $(textarea)
+      .focus(function() {
+        textarea.interval = setInterval(function() {
+          $.fn.autogrow.resize(textarea);
+        }, 500);
+      })
+      .blur(function() {
+        clearInterval(textarea.interval);
+      });
+    });
+  };
+
+  $.fn.autogrow.resize = function(textarea) {
+    var lineHeight = parseInt($(textarea).css('line-height'));
+    var lines = textarea.value.split('\n');
+    var columns = textarea.cols;
+    var lineCount = 0;
+    $.each(lines, function() {
+      lineCount += Math.ceil(this.length / columns) || 1;
+    });
+    var height = lineHeight * (lineCount + 1);
+    $(textarea).css('height', height);
+  };
+})(jQuery);
+
+(function($) {
+  var commentListEmpty, popup, comp;
+
+  function init() {
+    initTemplates();
+    initEvents();
+    initComparator();
+  };
+
+  function initEvents() {
+    $('a#comment_close').click(function(event) {
+      event.preventDefault();
+      hide();
+    });
+    $('form#comment_form').submit(function(event) {
+      event.preventDefault();
+      addComment($('form#comment_form'));
+    });
+    $('.vote').live("click", function() {
+      handleVote($(this));
+      return false;
+    });
+    $('a.reply').live("click", function() {
+      openReply($(this).attr('id').substring(2));
+      return false;
+    });
+    $('a.close_reply').live("click", function() {
+      closeReply($(this).attr('id').substring(2));
+      return false;
+    });
+    $('a.sort_option').click(function(event) {
+      event.preventDefault();
+      handleReSort($(this));
+    });
+    $('a.show_proposal').live("click", function() {
+      showProposal($(this).attr('id').substring(2));
+      return false;
+    });
+    $('a.hide_proposal').live("click", function() {
+      hideProposal($(this).attr('id').substring(2));
+      return false;
+    });
+    $('a.show_propose_change').live("click", function() {
+      showProposeChange($(this).attr('id').substring(2));
+      return false;
+    });
+    $('a.hide_propose_change').live("click", function() {
+      hideProposeChange($(this).attr('id').substring(2));
+      return false;
+    });
+    $('a.accept_comment').live("click", function() {
+      acceptComment($(this).attr('id').substring(2));
+      return false;
+    });
+    $('a.reject_comment').live("click", function() {
+      rejectComment($(this).attr('id').substring(2));
+      return false;
+    });
+    $('a.delete_comment').live("click", function() {
+      deleteComment($(this).attr('id').substring(2));
+      return false;
+    });
+  };
+
+  function initTemplates() {
+    // Create our popup div, the same div is recycled each time comments
+    // are displayed.
+    popup = $(renderTemplate(popupTemplate, opts));
+    // Setup autogrow on the textareas
+    popup.find('textarea').autogrow();
+    $('body').append(popup);
+  };
+
+  /*
+   Create a comp function. If the user has preferences stored in
+   the sortBy cookie, use those, otherwise use the default.
+  */
+  function initComparator() {
+    var by = 'rating'; // Default to sort by rating.
+    // If the sortBy cookie is set, use that instead.
+    if (document.cookie.length > 0) {
+      var start = document.cookie.indexOf('sortBy=');
+      if (start != -1) {
+        start = start + 7;
+        var end = document.cookie.indexOf(";", start);
+        if (end == -1)
+          end = document.cookie.length;
+          by = unescape(document.cookie.substring(start, end));
+        }
+    }
+    setComparator(by);
+  };
+
+  /*
+   Show the comments popup window.
+  */
+  function show(nodeId) {
+    var id = nodeId.substring(1);
+
+    // Reset the main comment form, and set the value of the parent input.
+    $('form#comment_form')
+      .find('textarea,input')
+      .removeAttr('disabled').end()
+      .find('input[name="node"]')
+      .val(id).end()
+      .find('textarea[name="proposal"]')
+      .val('')
+      .hide();
+
+    // Position the popup and show it.
+    var clientWidth = document.documentElement.clientWidth;
+    var popupWidth = $('div.popup_comment').width();
+    $('div#focuser').fadeIn('fast');
+    $('div.popup_comment')
+      .css({
+        'top': 100 + $(window).scrollTop(),
+        'left': clientWidth / 2 - popupWidth / 2,
+        'position': 'absolute'
+      })
+      .fadeIn('fast', function() {
+        getComments(id);
+      });
+  };
+
+  /*
+   Hide the comments popup window.
+  */
+  function hide() {
+    $('div#focuser').fadeOut('fast');
+    $('div.popup_comment').fadeOut('fast', function() {
+      $('ul#comment_ul').empty();
+      $('h3#comment_notification').show();
+      $('form#comment_form').find('textarea')
+        .val('').end()
+        .find('textarea, input')
+        .removeAttr('disabled');
+    });
+  };
+
+  /*
+   Perform an ajax request to get comments for a node
+   and insert the comments into the comments tree.
+  */
+  function getComments(id) {
+    $.ajax({
+     type: 'GET',
+     url: opts.getCommentsURL,
+     data: {node: id},
+     success: function(data, textStatus, request) {
+       var ul = $('ul#comment_ul').hide();
+       $('form#comment_form')
+         .find('textarea[name="proposal"]')
+         .data('source', data.source);
+
+       if (data.comments.length == 0) {
+         ul.html('<li>No comments yet.</li>');
+         commentListEmpty = true;
+         var speed = 100;
+       } else {
+         // If there are comments, sort them and put them in the list.
+         var comments = sortComments(data.comments);
+         var speed = data.comments.length * 100;
+         appendComments(comments, ul);
+         commentListEmpty = false;
+       }
+       $('h3#comment_notification').slideUp(speed + 200);
+       ul.slideDown(speed);
+     },
+     error: function(request, textStatus, error) {
+       showError('Oops, there was a problem retrieving the comments.');
+     },
+     dataType: 'json'
+    });
+  };
+
+  /*
+   Add a comment via ajax and insert the comment into the comment tree.
+  */
+  function addComment(form) {
+    // Disable the form that is being submitted.
+    form.find('textarea,input').attr('disabled', 'disabled');
+    var node_id = form.find('input[name="node"]').val();
+
+    // Send the comment to the server.
+    $.ajax({
+      type: "POST",
+      url: opts.addCommentURL,
+      dataType: 'json',
+      data: {
+        node: node_id,
+        parent: form.find('input[name="parent"]').val(),
+        text: form.find('textarea[name="comment"]').val(),
+        proposal: form.find('textarea[name="proposal"]').val()
+      },
+      success: function(data, textStatus, error) {
+        // Reset the form.
+        if (node_id) {
+          hideProposeChange(node_id);
+        }
+        form.find('textarea')
+          .val('')
+          .add(form.find('input'))
+          .removeAttr('disabled');
+        if (commentListEmpty) {
+          $('ul#comment_ul').empty();
+          commentListEmpty = false;
+        }
+        insertComment(data.comment);
+      },
+      error: function(request, textStatus, error) {
+        form.find('textarea,input').removeAttr('disabled');
+        showError('Oops, there was a problem adding the comment.');
+      }
+    });
+  };
+
+  /*
+   Recursively append comments to the main comment list and children
+   lists, creating the comment tree.
+  */
+  function appendComments(comments, ul) {
+    $.each(comments, function() {
+      var div = createCommentDiv(this);
+      ul.append($(document.createElement('li')).html(div));
+      appendComments(this.children, div.find('ul.children'));
+      // To avoid stagnating data, don't store the comments children in data.
+      this.children = null;
+      div.data('comment', this);
+    });
+  };
+
+  /*
+   After adding a new comment, it must be inserted in the correct
+   location in the comment tree.
+  */
+  function insertComment(comment) {
+    var div = createCommentDiv(comment);
+
+    // To avoid stagnating data, don't store the comments children in data.
+    comment.children = null;
+    div.data('comment', comment);
+
+    if (comment.node != null) {
+      var ul = $('ul#comment_ul');
+      var siblings = getChildren(ul);
+    } else {
+      var ul = $('#cl' + comment.parent);
+      var siblings = getChildren(ul);
+    }
+
+    var li = $(document.createElement('li'));
+    li.hide();
+
+    // Determine where in the parents children list to insert this comment.
+    for(i=0; i < siblings.length; i++) {
+      if (comp(comment, siblings[i]) <= 0) {
+        $('#cd' + siblings[i].id)
+          .parent()
+          .before(li.html(div));
+        li.slideDown('fast');
+        return;
+      }
+    }
+
+    // If we get here, this comment rates lower than all the others,
+    // or it is the only comment in the list.
+    ul.append(li.html(div));
+    li.slideDown('fast');
+  };
+
+  function acceptComment(id) {
+    $.ajax({
+      type: 'POST',
+      url: opts.acceptCommentURL,
+      data: {id: id},
+      success: function(data, textStatus, request) {
+        $('#cm' + id).fadeOut('fast');
+      },
+      error: function(request, textStatus, error) {
+        showError("Oops, there was a problem accepting the comment.");
+      },
+    });
+  };
+
+  function rejectComment(id) {
+    $.ajax({
+      type: 'POST',
+      url: opts.rejectCommentURL,
+      data: {id: id},
+      success: function(data, textStatus, request) {
+        var div = $('#cd' + id);
+        div.slideUp('fast', function() {
+        div.remove();
+        });
+      },
+      error: function(request, textStatus, error) {
+        showError("Oops, there was a problem rejecting the comment.");
+      },
+    });
+  };
+
+  function deleteComment(id) {
+    $.ajax({
+      type: 'POST',
+      url: opts.deleteCommentURL,
+      data: {id: id},
+      success: function(data, textStatus, request) {
+        var div = $('#cd' + id);
+        div
+          .find('span.user_id:first')
+          .text('[deleted]').end()
+          .find('p.comment_text:first')
+          .text('[deleted]').end()
+          .find('#cm' + id + ', #dc' + id + ', #ac' + id + ', #rc' + id +
+                ', #sp' + id + ', #hp' + id + ', #cr' + id + ', #rl' + id)
+          .remove();
+        var comment = div.data('comment');
+        comment.username = '[deleted]';
+        comment.text = '[deleted]';
+        div.data('comment', comment);
+      },
+      error: function(request, textStatus, error) {
+        showError("Oops, there was a problem deleting the comment.");
+      },
+    });
+  };
+
+  function showProposal(id) {
+    $('#sp' + id).hide();
+    $('#hp' + id).show();
+    $('#pr' + id).slideDown('fast');
+  };
+
+  function hideProposal(id) {
+    $('#hp' + id).hide();
+    $('#sp' + id).show();
+    $('#pr' + id).slideUp('fast');
+  };
+
+  function showProposeChange(id) {
+    $('a.show_propose_change').hide();
+    $('a.hide_propose_change').show();
+    var textarea = $('textarea[name="proposal"]');
+    textarea.val(textarea.data('source'));
+    $.fn.autogrow.resize(textarea[0]);
+    textarea.slideDown('fast');
+  };
+
+  function hideProposeChange(id) {
+    $('a.hide_propose_change').hide();
+    $('a.show_propose_change').show();
+    var textarea = $('textarea[name="proposal"]');
+    textarea.val('').removeAttr('disabled');
+    textarea.slideUp('fast');
+  };
+
+  /*
+   Handle when the user clicks on a sort by link.
+  */
+  function handleReSort(link) {
+    setComparator(link.attr('id'));
+    // Save/update the sortBy cookie.
+    var expiration = new Date();
+    expiration.setDate(expiration.getDate() + 365);
+    document.cookie= 'sortBy=' + escape(link.attr('id')) +
+                     ';expires=' + expiration.toUTCString();
+    var comments = getChildren($('ul#comment_ul'), true);
+    comments = sortComments(comments);
+
+    appendComments(comments, $('ul#comment_ul').empty());
+  };
+
+  /*
+   Function to process a vote when a user clicks an arrow.
+  */
+  function handleVote(link) {
+    if (!opts.voting) {
+      showError("You'll need to login to vote.");
+      return;
+    }
+
+    var id = link.attr('id');
+    // If it is an unvote, the new vote value is 0,
+    // Otherwise it's 1 for an upvote, or -1 for a downvote.
+    if (id.charAt(1) == 'u') {
+      var value = 0;
+    } else {
+      var value = id.charAt(0) == 'u' ? 1 : -1;
+    }
+    // The data to be sent to the server.
+    var d = {
+      comment_id: id.substring(2),
+      value: value
+    };
+
+    // Swap the vote and unvote links.
+    link.hide();
+    $('#' + id.charAt(0) + (id.charAt(1) == 'u' ? 'v' : 'u') + d.comment_id)
+      .show();
+
+    // The div the comment is displayed in.
+    var div = $('div#cd' + d.comment_id);
+    var data = div.data('comment');
+
+    // If this is not an unvote, and the other vote arrow has
+    // already been pressed, unpress it.
+    if ((d.value != 0) && (data.vote == d.value * -1)) {
+      $('#' + (d.value == 1 ? 'd' : 'u') + 'u' + d.comment_id).hide();
+      $('#' + (d.value == 1 ? 'd' : 'u') + 'v' + d.comment_id).show();
+    }
+
+    // Update the comments rating in the local data.
+    data.rating += (data.vote == 0) ? d.value : (d.value - data.vote);
+    data.vote = d.value;
+    div.data('comment', data);
+
+    // Change the rating text.
+    div.find('.rating:first')
+      .text(data.rating + ' point' + (data.rating == 1 ? '' : 's'));
+
+    // Send the vote information to the server.
+    $.ajax({
+      type: "POST",
+      url: opts.processVoteURL,
+      data: d,
+      error: function(request, textStatus, error) {
+        showError("Oops, there was a problem casting that vote.");
+      }
+    });
+  };
+
+  /*
+   Open a reply form used to reply to an existing comment.
+  */
+  function openReply(id) {
+    // Swap out the reply link for the hide link
+    $('#rl' + id).hide();
+    $('#cr' + id).show();
+
+    // Add the reply li to the children ul.
+    var div = $(renderTemplate(replyTemplate, {id: id})).hide();
+    $('#cl' + id)
+      .prepend(div)
+      // Setup the submit handler for the reply form.
+      .find('#rf' + id)
+      .submit(function(event) {
+        event.preventDefault();
+        addComment($('#rf' + id));
+        closeReply(id);
+      });
+    div.slideDown('fast');
+  };
+
+  /*
+   Close the reply form opened with openReply.
+  */
+  function closeReply(id) {
+    // Remove the reply div from the DOM.
+    $('#rd' + id).slideUp('fast', function() {
+      $(this).remove();
+    });
+
+    // Swap out the hide link for the reply link
+    $('#cr' + id).hide();
+    $('#rl' + id).show();
+  };
+
+  /*
+   Recursively sort a tree of comments using the comp comparator.
+  */
+  function sortComments(comments) {
+    comments.sort(comp);
+    $.each(comments, function() {
+      this.children = sortComments(this.children);
+    });
+    return comments;
+  };
+
+  /*
+   Set comp, which is a comparator function used for sorting and
+   inserting comments into the list.
+  */
+  function setComparator(by) {
+    // If the first three letters are "asc", sort in ascending order
+    // and remove the prefix.
+    if (by.substring(0,3) == 'asc') {
+      var i = by.substring(3);
+      comp = function(a, b) { return a[i] - b[i]; }
+    } else {
+      // Otherwise sort in descending order.
+      comp = function(a, b) { return b[by] - a[by]; }
+    }
+
+    // Reset link styles and format the selected sort option.
+    $('a.sel').attr('href', '#').removeClass('sel');
+    $('#' + by).removeAttr('href').addClass('sel');
+  };
+
+  /*
+   Get the children comments from a ul. If recursive is true,
+   recursively include childrens' children.
+  */
+  function getChildren(ul, recursive) {
+    var children = [];
+    ul.children().children("[id^='cd']")
+      .each(function() {
+        var comment = $(this).data('comment');
+        if (recursive) {
+          comment.children = getChildren($(this).find('#cl' + comment.id), true);
+        }
+        children.push(comment);
+      });
+    return children;
+  };
+
+  /*
+   Create a div to display a comment in.
+  */
+  function createCommentDiv(comment) {
+    // Prettify the comment rating.
+    comment.pretty_rating = comment.rating + ' point' +
+    (comment.rating == 1 ? '' : 's');
+    // Create a div for this comment.
+    var context = $.extend({}, opts, comment);
+    var div = $(renderTemplate(commentTemplate, context));
+
+    // If the user has voted on this comment, highlight the correct arrow.
+    if (comment.vote) {
+      var direction = (comment.vote == 1) ? 'u' : 'd';
+      div.find('#' + direction + 'v' + comment.id).hide();
+      div.find('#' + direction + 'u' + comment.id).show();
+    }
+
+    if (comment.text != '[deleted]') {
+      div.find('a.reply').show();
+      if (comment.proposal_diff) {
+        div.find('#sp' + comment.id).show();
+      }
+      if (opts.moderator && !comment.displayed) {
+        div.find('#cm' + comment.id).show();
+      }
+      if (opts.moderator || (opts.username == comment.username)) {
+        div.find('#dc' + comment.id).show();
+      }
+    }
+    return div;
+  }
+
+  /*
+   A simple template renderer. Placeholders such as <%id%> are replaced
+   by context['id']. Items are always escaped.
+  */
+  function renderTemplate(template, context) {
+    var esc = $(document.createElement('div'));
+
+    function handle(ph, escape) {
+      var cur = context;
+      $.each(ph.split('.'), function() {
+        cur = cur[this];
+      });
+      return escape ? esc.text(cur || "").html() : cur;
+    }
+
+    return template.replace(/<([%#])([\w\.]*)\1>/g, function(){
+      return handle(arguments[2], arguments[1] == '%' ? true : false);
+    });
+  };
+
+  function showError(message) {
+    $(document.createElement('div')).attr({class: 'popup_error'})
+      .append($(document.createElement('h1')).text(message))
+      .appendTo('body')
+      .fadeIn("slow")
+      .delay(2000)
+      .fadeOut("slow");
+  };
+
+  /*
+   Add a link the user uses to open the comments popup.
+  */
+  $.fn.comment = function() {
+    return this.each(function() {
+      var id = $(this).attr('id').substring(1);
+      var count = COMMENT_METADATA[id]
+      var title = count + ' comment' + (count == 1 ? '' : 's');
+      var image = count > 0 ? opts.commentBrightImage : opts.commentImage;
+      $(this).append(
+        $(document.createElement('a')).attr({href: '#', class: 'sphinx_comment'})
+          .append($(document.createElement('img')).attr({
+            src: image,
+            alt: 'comment',
+            title: title
+          }))
+          .click(function(event) {
+            event.preventDefault();
+            show($(this).parent().attr('id'));
+          })
+      );
+    });
+  };
+
+  var opts = jQuery.extend({
+    processVoteURL: '/process_vote',
+    addCommentURL: '/add_comment',
+    getCommentsURL: '/get_comments',
+    acceptCommentURL: '/accept_comment',
+    rejectCommentURL: '/reject_comment',
+    rejectCommentURL: '/delete_comment',
+    commentImage: '/static/_static/comment.png',
+    loadingImage: '/static/_static/ajax-loader.gif',
+    commentBrightImage: '/static/_static/comment-bright.png',
+    upArrow: '/static/_static/up.png',
+    downArrow: '/static/_static/down.png',
+    upArrowPressed: '/static/_static/up-pressed.png',
+    downArrowPressed: '/static/_static/down-pressed.png',
+    voting: false,
+    moderator: false
+  }, COMMENT_OPTIONS);
+
+  var replyTemplate = '\
+    <li>\
+      <div class="reply_div" id="rd<%id%>">\
+        <form id="rf<%id%>">\
+          <textarea name="comment" cols="80"></textarea>\
+          <input type="submit" value="add reply" />\
+          <input type="hidden" name="parent" value="<%id%>" />\
+          <input type="hidden" name="node" value="" />\
+        </form>\
+      </div>\
+    </li>';
+
+  var commentTemplate = '\
+    <div  id="cd<%id%>" class="spxcdiv">\
+      <div class="vote">\
+        <div class="arrow">\
+          <a href="#" id="uv<%id%>" class="vote">\
+            <img src="<%upArrow%>" />\
+          </a>\
+          <a href="#" id="uu<%id%>" class="un vote">\
+            <img src="<%upArrowPressed%>" />\
+          </a>\
+        </div>\
+        <div class="arrow">\
+          <a href="#" id="dv<%id%>" class="vote">\
+            <img src="<%downArrow%>" id="da<%id%>" />\
+          </a>\
+          <a href="#" id="du<%id%>" class="un vote">\
+            <img src="<%downArrowPressed%>" />\
+          </a>\
+        </div>\
+      </div>\
+      <div class="comment_content">\
+        <p class="tagline comment">\
+          <span class="user_id"><%username%></span>\
+          <span class="rating"><%pretty_rating%></span>\
+          <span class="delta"><%time.delta%></span>\
+        </p>\
+        <p class="comment_text comment"><%text%></p>\
+        <p class="comment_opts comment">\
+          <a href="#" class="reply hidden" id="rl<%id%>">reply &#9657;</a>\
+          <a href="#" class="close_reply" id="cr<%id%>">reply &#9663;</a>\
+          <a href="#" id="sp<%id%>" class="show_proposal">\
+            proposal &#9657;\
+          </a>\
+          <a href="#" id="hp<%id%>" class="hide_proposal">\
+            proposal &#9663;\
+          </a>\
+          <a href="#" id="dc<%id%>" class="delete_comment hidden">\
+            delete\
+          </a>\
+          <span id="cm<%id%>" class="moderation hidden">\
+            <a href="#" id="ac<%id%>" class="accept_comment">accept</a>\
+            <a href="#" id="rc<%id%>" class="reject_comment">reject</a>\
+          </span>\
+        </p>\
+        <pre class="proposal" id="pr<%id%>">\
+<#proposal_diff#>\
+        </pre>\
+          <ul class="children" id="cl<%id%>"></ul>\
+        </div>\
+        <div class="clearleft"></div>\
+      </div>\
+    </div>';
+
+  var popupTemplate = '\
+    <div id="popup_template">\
+      <div class="popup_comment">\
+        <a id="comment_close" href="#">x</a>\
+        <h1>Comments</h1>\
+        <form method="post" id="comment_form" action="/docs/add_comment">\
+          <textarea name="comment" cols="80"></textarea>\
+          <p class="propose_button">\
+            <a href="#" class="show_propose_change">\
+              Propose a change &#9657;\
+            </a>\
+            <a href="#" class="hide_propose_change">\
+              Propose a change &#9663;\
+            </a>\
+          </p>\
+          <textarea name="proposal" cols="80" spellcheck="false"></textarea>\
+          <input type="submit" value="add comment" id="comment_button" />\
+          <input type="hidden" name="node" />\
+          <input type="hidden" name="parent" value="" />\
+          <p class="sort_options">\
+            Sort by:\
+            <a href="#" class="sort_option" id="rating">top</a>\
+            <a href="#" class="sort_option" id="ascage">newest</a>\
+            <a href="#" class="sort_option" id="age">oldest</a>\
+          </p>\
+        </form>\
+        <h3 id="comment_notification">loading comments... <img src="' +
+          opts.loadingImage + '" alt="" /></h3>\
+        <ul id="comment_ul"></ul>\
+      </div>\
+    </div>\
+    <div id="focuser"></div>';
+
+
+  $(document).ready(function() {
+    init();
+  });
+})(jQuery);
+
+$(document).ready(function() {
+  $('.spxcmt').comment();
+
+  /** Highlight search words in search results. */
+  $("div.context").each(function() {
+    var params = $.getQueryParameters();
+    var terms = (params.q) ? params.q[0].split(/\s+/) : [];
+    var result = $(this);
+    $.each(terms, function() {
+      result.highlightText(this.toLowerCase(), 'highlighted');
+    });
+  });
+});

File sphinx/util/docfields.py

 
     def __init__(self, directive):
         self.domain = directive.domain
-        if not hasattr(directive, '_doc_field_type_map'):
+        if '_doc_field_type_map' not in directive.__class__.__dict__:
             directive.__class__._doc_field_type_map = \
                 self.preprocess_fieldtypes(directive.__class__.doc_field_types)
         self.typemap = directive._doc_field_type_map

File sphinx/util/smartypants.py

 # Constants for quote education.
 
 punct_class = r"""[!"#\$\%'()*+,-.\/:;<=>?\@\[\\\]\^_`{|}~]"""
+end_of_word_class = r"""[\s.,;:!?)]"""
 close_class = r"""[^\ \t\r\n\[\{\(\-]"""
 dec_dashes = r"""&#8211;|&#8212;"""
 
 closing_double_quotes_regex = re.compile(r"""
                 #(%s)?   # character that indicates the quote should be closing
                 "
-                (?=\s)
-                """ % (close_class,), re.VERBOSE)
+                (?=%s)
+                """ % (close_class, end_of_word_class), re.VERBOSE)
 
 closing_double_quotes_regex_2 = re.compile(r"""
                 (%s)   # character that indicates the quote should be closing

File sphinx/util/websupport.py

+# -*- coding: utf-8 -*-
+"""
+    sphinx.util.websupport
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+def is_commentable(node):
+    return node.__class__.__name__ in ('paragraph', 'literal_block')

File sphinx/websupport/__init__.py

+# -*- coding: utf-8 -*-
+"""
+    sphinx.websupport
+    ~~~~~~~~~~~~~~~~~
+
+    Base Module for web support functions.
+
+    :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+import sys
+import cPickle as pickle
+import posixpath
+from os import path
+from datetime import datetime
+
+from jinja2 import Environment, FileSystemLoader
+
+from sphinx.application import Sphinx
+from sphinx.util.osutil import ensuredir
+from sphinx.util.jsonimpl import dumps as dump_json
+from sphinx.websupport.search import BaseSearch, search_adapters
+from sphinx.websupport.storage import StorageBackend
+from sphinx.websupport.errors import *
+
+class WebSupportApp(Sphinx):
+    def __init__(self, *args, **kwargs):
+        self.staticdir = kwargs.pop('staticdir', None)
+        self.builddir = kwargs.pop('builddir', None)
+        self.search = kwargs.pop('search', None)
+        self.storage = kwargs.pop('storage', None)
+        Sphinx.__init__(self, *args, **kwargs)
+
+class WebSupport(object):
+    """The main API class for the web support package. All interactions
+    with the web support package should occur through this class.
+    """
+    def __init__(self, srcdir='', builddir='', datadir='', search=None,
+                 storage=None, status=sys.stdout, warning=sys.stderr,
+                 moderation_callback=None, staticdir='static',
+                 docroot=''):
+        self.srcdir = srcdir
+        self.builddir = builddir
+        self.outdir = path.join(builddir, 'data')
+        self.datadir = datadir or self.outdir
+        self.staticdir = staticdir.strip('/')
+        self.docroot = docroot.strip('/')
+        self.status = status
+        self.warning = warning
+        self.moderation_callback = moderation_callback
+
+        self._init_templating()
+        self._init_search(search)
+        self._init_storage(storage)
+
+        self._make_base_comment_options()
+
+    def _init_storage(self, storage):
+        if isinstance(storage, StorageBackend):
+            self.storage = storage
+        else:
+            # If a StorageBackend isn't provided, use the default
+            # SQLAlchemy backend.
+            from sphinx.websupport.storage.sqlalchemystorage \
+                import SQLAlchemyStorage
+            from sqlalchemy import create_engine
+            db_path = path.join(self.datadir, 'db', 'websupport.db')
+            ensuredir(path.dirname(db_path))
+            uri = storage or 'sqlite:///%s' % db_path
+            engine = create_engine(uri)
+            self.storage = SQLAlchemyStorage(engine)
+
+    def _init_templating(self):
+        import sphinx
+        template_path = path.join(path.dirname(sphinx.__file__),
+                                  'themes', 'basic')
+        loader = FileSystemLoader(template_path)
+        self.template_env = Environment(loader=loader)
+
+    def _init_search(self, search):
+        if isinstance(search, BaseSearch):
+            self.search = search
+        else:
+            mod, cls = search_adapters[search or 'null']
+            mod = 'sphinx.websupport.search.' + mod
+            SearchClass = getattr(__import__(mod, None, None, [cls]), cls)
+            search_path = path.join(self.datadir, 'search')
+            self.search = SearchClass(search_path)
+        self.results_template = \
+            self.template_env.get_template('searchresults.html')
+
+    def build(self):
+        """Build the documentation. Places the data into the `outdir`
+        directory. Use it like this::
+
+            support = WebSupport(srcdir, builddir, search='xapian')
+            support.build()
+
+        This will read reStructured text files from `srcdir`. Then it will
+        build the pickles and search index, placing them into `builddir`.
+        It will also save node data to the database.
+        """
+        if not self.srcdir:
+            raise SrcdirNotSpecifiedError( \
+                'No srcdir associated with WebSupport object')
+        doctreedir = path.join(self.outdir, 'doctrees')
+        app = WebSupportApp(self.srcdir, self.srcdir,
+                            self.outdir, doctreedir, 'websupport',
+                            search=self.search, status=self.status,
+                            warning=self.warning, storage=self.storage,
+                            staticdir=self.staticdir, builddir=self.builddir)
+
+        self.storage.pre_build()
+        app.build()
+        self.storage.post_build()
+
+    def get_document(self, docname, username='', moderator=False):
+        """Load and return a document from a pickle. The document will
+        be a dict object which can be used to render a template::
+
+            support = WebSupport(datadir=datadir)
+            support.get_document('index', username, moderator)
+
+        In most cases `docname` will be taken from the request path and
+        passed directly to this function. In Flask, that would be something
+        like this::
+
+            @app.route('/<path:docname>')
+            def index(docname):
+                username = g.user.name if g.user else ''
+                moderator = g.user.moderator if g.user else False
+                try:
+                    document = support.get_document(docname, username,
+                                                    moderator)
+                except DocumentNotFoundError:
+                    abort(404)
+                render_template('doc.html', document=document)
+
+        The document dict that is returned contains the following items
+        to be used during template rendering.
+
+        * **body**: The main body of the document as HTML
+        * **sidebar**: The sidebar of the document as HTML
+        * **relbar**: A div containing links to related documents
+        * **title**: The title of the document
+        * **css**: Links to css files used by Sphinx
+        * **js**: Javascript containing comment options
+
+        This raises :class:`~sphinx.websupport.errors.DocumentNotFoundError`
+        if a document matching `docname` is not found.
+
+        :param docname: the name of the document to load.
+        """
+        infilename = path.join(self.datadir, 'pickles', docname + '.fpickle')
+
+        try:
+            f = open(infilename, 'rb')
+        except IOError:
+            raise DocumentNotFoundError(
+                'The document "%s" could not be found' % docname)
+
+        document = pickle.load(f)
+        comment_opts = self._make_comment_options(username, moderator)
+        comment_metadata = self.storage.get_metadata(docname, moderator)
+
+        document['js'] = '\n'.join([comment_opts,
+                                    self._make_metadata(comment_metadata),
+                                    document['js']])
+        return document
+
+    def get_search_results(self, q):
+        """Perform a search for the query `q`, and create a set
+        of search results. Then render the search results as html and
+        return a context dict like the one created by
+        :meth:`get_document`::
+
+            document = support.get_search_results(q)
+
+        :param q: the search query
+        """
+        results = self.search.query(q)
+        ctx = {'search_performed': True,
+               'search_results': results,
+               'q': q}
+        document = self.get_document('search')
+        document['body'] = self.results_template.render(ctx)
+        document['title'] = 'Search Results'
+        return document
+
+    def get_data(self, node_id, username=None, moderator=False):
+        """Get the comments and source associated with `node_id`. If
+        `username` is given vote information will be included with the
+        returned comments. The default CommentBackend returns a dict with
+        two keys, *source*, and *comments*. *source* is raw source of the
+        node and is used as the starting point for proposals a user can
+        add. *comments* is a list of dicts that represent a comment, each
+        having the following items:
+
+        ============= ======================================================
+        Key           Contents
+        ============= ======================================================
+        text          The comment text.
+        username      The username that was stored with the comment.
+        id            The comment's unique identifier.
+        rating        The comment's current rating.
+        age           The time in seconds since the comment was added.
+        time          A dict containing time information. It contains the
+                      following keys: year, month, day, hour, minute, second,
+                      iso, and delta. `iso` is the time formatted in ISO
+                      8601 format. `delta` is a printable form of how old
+                      the comment is (e.g. "3 hours ago").
+        vote          If `user_id` was given, this will be an integer
+                      representing the vote. 1 for an upvote, -1 for a
+                      downvote, or 0 if unvoted.
+        node          The id of the node that the comment is attached to.
+                      If the comment's parent is another comment rather than
+                      a node, this will be null.
+        parent        The id of the comment that this comment is attached
+                      to if it is not attached to a node.
+        children      A list of all children, in this format.
+        proposal_diff An HTML representation of the differences between the
+                      the current source and the user's proposed source.
+        ============= ======================================================
+
+        :param node_id: the id of the node to get comments for.
+        :param username: the username of the user viewing the comments.
+        :param moderator: whether the user is a moderator.
+        """
+        return self.storage.get_data(node_id, username, moderator)
+
+    def delete_comment(self, comment_id, username='', moderator=False):
+        """Delete a comment. Doesn't actually delete the comment, but
+        instead replaces the username and text files with "[deleted]" so
+        as not to leave any comments orphaned.
+
+        If `moderator` is True, the comment will always be deleted. If
+        `moderator` is False, the comment will only be deleted if the
+        `username` matches the `username` on the comment.
+
+        This raises :class:`~sphinx.websupport.errors.UserNotAuthorizedError`
+        if moderator is False and `username` doesn't match username on the
+        comment.
+
+        :param comment_id: the id of the comment to delete.
+        :param username: the username requesting the deletion.
+        :param moderator: whether the requestor is a moderator.
+        """
+        self.storage.delete_comment(comment_id, username, moderator)
+
+    def add_comment(self, text, node_id='', parent_id='', displayed=True,
+                    username=None, time=None, proposal=None,
+                    moderator=False):
+        """Add a comment to a node or another comment. Returns the comment
+        in the same format as :meth:`get_comments`. If the comment is being
+        attached to a node, pass in the node's id (as a string) with the
+        node keyword argument::
+
+            comment = support.add_comment(text, node_id=node_id)
+
+        If the comment is the child of another comment, provide the parent's
+        id (as a string) with the parent keyword argument::
+
+            comment = support.add_comment(text, parent_id=parent_id)
+
+        If you would like to store a username with the comment, pass
+        in the optional `username` keyword argument::
+
+            comment = support.add_comment(text, node=node_id,
+                                          username=username)
+
+        :param parent_id: the prefixed id of the comment's parent.
+        :param text: the text of the comment.
+        :param displayed: for moderation purposes
+        :param username: the username of the user making the comment.
+        :param time: the time the comment was created, defaults to now.
+        """
+        comment = self.storage.add_comment(text, displayed, username,
+                                           time, proposal, node_id,
+                                           parent_id, moderator)
+        if not displayed and self.moderation_callback:
+            self.moderation_callback(comment)
+        return comment
+
+    def process_vote(self, comment_id, username, value):
+        """Process a user's vote. The web support package relies
+        on the API user to perform authentication. The API user will
+        typically receive a comment_id and value from a form, and then
+        make sure the user is authenticated. A unique username  must be
+        passed in, which will also be used to retrieve the user's past
+        voting data. An example, once again in Flask::
+
+            @app.route('/docs/process_vote', methods=['POST'])
+            def process_vote():
+                if g.user is None:
+                    abort(401)
+                comment_id = request.form.get('comment_id')
+                value = request.form.get('value')
+                if value is None or comment_id is None:
+                    abort(400)
+                support.process_vote(comment_id, g.user.name, value)
+                return "success"
+
+        :param comment_id: the comment being voted on
+        :param username: the unique username of the user voting
+        :param value: 1 for an upvote, -1 for a downvote, 0 for an unvote.
+        """
+        value = int(value)
+        if not -1 <= value <= 1:
+            raise ValueError('vote value %s out of range (-1, 1)' % value)
+        self.storage.process_vote(comment_id, username, value)
+
+    def update_username(self, old_username, new_username):
+        """To remain decoupled from a webapp's authentication system, the
+        web support package stores a user's username with each of their
+        comments and votes. If the authentication system allows a user to
+        change their username, this can lead to stagnate data in the web
+        support system. To avoid this, each time a username is changed, this
+        method should be called.
+
+        :param old_username: The original username.
+        :param new_username: The new username.
+        """
+        self.storage.update_username(old_username, new_username)
+
+    def accept_comment(self, comment_id, moderator=False):
+        """Accept a comment that is pending moderation.
+
+        This raises :class:`~sphinx.websupport.errors.UserNotAuthorizedError`
+        if moderator is False.
+
+        :param comment_id: The id of the comment that was accepted.
+        :param moderator: Whether the user making the request is a moderator.
+        """
+        if not moderator:
+            raise UserNotAuthorizedError()