From aa4f8e2e436d6c49e1524a4a3fb164b28632d894 Mon Sep 17 00:00:00 2001 From: Chris Johns Date: Mon, 7 Aug 2017 21:58:52 +1000 Subject: Add the sphinxcontrib.bibtex extension to the repo. --- common/sphinxcontrib/__init__.py | 13 + common/sphinxcontrib/bibtex/__init__.py | 148 +++++++++++ common/sphinxcontrib/bibtex/cache.py | 406 ++++++++++++++++++++++++++++++ common/sphinxcontrib/bibtex/directives.py | 221 ++++++++++++++++ common/sphinxcontrib/bibtex/nodes.py | 17 ++ common/sphinxcontrib/bibtex/roles.py | 43 ++++ common/sphinxcontrib/bibtex/transforms.py | 127 ++++++++++ common/waf.py | 1 - 8 files changed, 975 insertions(+), 1 deletion(-) create mode 100644 common/sphinxcontrib/__init__.py create mode 100644 common/sphinxcontrib/bibtex/__init__.py create mode 100644 common/sphinxcontrib/bibtex/cache.py create mode 100644 common/sphinxcontrib/bibtex/directives.py create mode 100644 common/sphinxcontrib/bibtex/nodes.py create mode 100644 common/sphinxcontrib/bibtex/roles.py create mode 100644 common/sphinxcontrib/bibtex/transforms.py diff --git a/common/sphinxcontrib/__init__.py b/common/sphinxcontrib/__init__.py new file mode 100644 index 0000000..35d34fc --- /dev/null +++ b/common/sphinxcontrib/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +""" + sphinxcontrib + ~~~~~~~~~~~~~ + + This package is a namespace package that contains all extensions + distributed in the ``sphinx-contrib`` distribution. + + :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +__import__('pkg_resources').declare_namespace(__name__) diff --git a/common/sphinxcontrib/bibtex/__init__.py b/common/sphinxcontrib/bibtex/__init__.py new file mode 100644 index 0000000..d79e688 --- /dev/null +++ b/common/sphinxcontrib/bibtex/__init__.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +""" + Sphinx Interface + ~~~~~~~~~~~~~~~~ + + .. autofunction:: setup + .. autofunction:: init_bibtex_cache + .. autofunction:: purge_bibtex_cache + .. autofunction:: process_citations + .. autofunction:: process_citation_references + .. autofunction:: check_duplicate_labels +""" + +import docutils.nodes +import docutils.parsers.rst +from sphinxcontrib.bibtex.cache import Cache +from sphinxcontrib.bibtex.nodes import bibliography +from sphinxcontrib.bibtex.roles import CiteRole +from sphinxcontrib.bibtex.directives import BibliographyDirective +from sphinxcontrib.bibtex.transforms import BibliographyTransform +import six + + +def init_bibtex_cache(app): + """Create ``app.env.bibtex_cache`` if it does not exist yet. + + :param app: The sphinx application. + :type app: :class:`sphinx.application.Sphinx` + """ + if not hasattr(app.env, "bibtex_cache"): + app.env.bibtex_cache = Cache() + + +def purge_bibtex_cache(app, env, docname): + """Remove all information related to *docname* from the cache. + + :param app: The sphinx application. + :type app: :class:`sphinx.application.Sphinx` + :param env: The sphinx build environment. + :type env: :class:`sphinx.environment.BuildEnvironment` + """ + env.bibtex_cache.purge(docname) + + +def process_citations(app, doctree, docname): + """Replace labels of citation nodes by actual labels. + + :param app: The sphinx application. + :type app: :class:`sphinx.application.Sphinx` + :param doctree: The document tree. + :type doctree: :class:`docutils.nodes.document` + :param docname: The document name. + :type docname: :class:`str` + """ + for node in doctree.traverse(docutils.nodes.citation): + key = node[0].astext() + try: + label = app.env.bibtex_cache.get_label_from_key(key) + except KeyError: + app.warn("could not relabel citation [%s]" % key) + else: + node[0] = docutils.nodes.label('', label) + + +def process_citation_references(app, doctree, docname): + """Replace text of citation reference nodes by actual labels. + + :param app: The sphinx application. + :type app: :class:`sphinx.application.Sphinx` + :param doctree: The document tree. + :type doctree: :class:`docutils.nodes.document` + :param docname: The document name. + :type docname: :class:`str` + """ + # sphinx has already turned citation_reference nodes + # into reference nodes, so iterate over reference nodes + for node in doctree.traverse(docutils.nodes.reference): + # exclude sphinx [source] labels + if isinstance(node[0], docutils.nodes.Element): + if 'viewcode-link' in node[0]['classes']: + continue + text = node[0].astext() + if text.startswith('[') and text.endswith(']'): + key = text[1:-1] + try: + label = app.env.bibtex_cache.get_label_from_key(key) + except KeyError: + app.warn("could not relabel citation reference [%s]" % key) + else: + node[0] = docutils.nodes.Text('[' + label + ']') + + +def check_duplicate_labels(app, env): + """Check and warn about duplicate citation labels. + + :param app: The sphinx application. + :type app: :class:`sphinx.application.Sphinx` + :param env: The sphinx build environment. + :type env: :class:`sphinx.environment.BuildEnvironment` + """ + label_to_key = {} + for info in env.bibtex_cache.get_all_bibliography_caches(): + for key, label in six.iteritems(info.labels): + if label in label_to_key: + app.warn( + "duplicate label for keys %s and %s" + % (key, label_to_key[label])) + else: + label_to_key[label] = key + + +def setup(app): + """Set up the bibtex extension: + + * register config values + * register directives + * register nodes + * register roles + * register transforms + * connect events to functions + + :param app: The sphinx application. + :type app: :class:`sphinx.application.Sphinx` + """ + + app.add_config_value("bibtex_default_style", "alpha", "html") + app.connect("builder-inited", init_bibtex_cache) + app.connect("doctree-resolved", process_citations) + app.connect("doctree-resolved", process_citation_references) + app.connect("env-purge-doc", purge_bibtex_cache) + app.connect("env-updated", check_duplicate_labels) + + # docutils keeps state around during testing, so to avoid spurious + # warnings, we detect here whether the directives have already been + # registered... very ugly hack but no better solution so far + _directives = docutils.parsers.rst.directives._directives + if "bibliography" not in _directives: + app.add_directive("bibliography", BibliographyDirective) + app.add_role("cite", CiteRole()) + app.add_node(bibliography) + app.add_transform(BibliographyTransform) + else: + assert _directives["bibliography"] is BibliographyDirective + + # Parallel read is not safe at the moment: in the current design, + # the document that contains references must be read last for all + # references to be resolved. + return {'parallel_read_safe': False} diff --git a/common/sphinxcontrib/bibtex/cache.py b/common/sphinxcontrib/bibtex/cache.py new file mode 100644 index 0000000..aa9064f --- /dev/null +++ b/common/sphinxcontrib/bibtex/cache.py @@ -0,0 +1,406 @@ +# -*- coding: utf-8 -*- +""" + Cached Information + ~~~~~~~~~~~~~~~~~~ + + Classes and methods to maintain any information that is stored + outside the doctree. + + .. autoclass:: Cache + :members: + + .. autoclass:: BibfileCache + :members: + + .. autoclass:: BibliographyCache + :members: +""" + +import six +try: # pragma: no cover + from collections import OrderedDict +except ImportError: # pragma: no cover + from ordereddict import OrderedDict +import ast +import collections +import copy +from oset import oset +import re + + +def _raise_invalid_node(node): + """Helper method to raise an exception when an invalid node is + visited. + """ + raise ValueError("invalid node %s in filter expression" % node) + + +class _FilterVisitor(ast.NodeVisitor): + + """Visit the abstract syntax tree of a parsed filter expression.""" + + entry = None + """The bibliographic entry to which the filter must be applied.""" + + cited_docnames = False + """The documents where the entry is cited (empty if not cited).""" + + def __init__(self, entry, docname, cited_docnames): + self.entry = entry + self.docname = docname + self.cited_docnames = cited_docnames + + def visit_Module(self, node): + if len(node.body) != 1: + raise ValueError( + "filter expression cannot contain multiple expressions") + return self.visit(node.body[0]) + + def visit_Expr(self, node): + return self.visit(node.value) + + def visit_BoolOp(self, node): + outcomes = (self.visit(value) for value in node.values) + if isinstance(node.op, ast.And): + return all(outcomes) + elif isinstance(node.op, ast.Or): + return any(outcomes) + else: # pragma: no cover + # there are no other boolean operators + # so this code should never execute + assert False, "unexpected boolean operator %s" % node.op + + def visit_UnaryOp(self, node): + if isinstance(node.op, ast.Not): + return not self.visit(node.operand) + else: + _raise_invalid_node(node) + + def visit_BinOp(self, node): + left = self.visit(node.left) + op = node.op + right = self.visit(node.right) + if isinstance(op, ast.Mod): + # modulo operator is used for regular expression matching + if not isinstance(left, six.string_types): + raise ValueError( + "expected a string on left side of %s" % node.op) + if not isinstance(right, six.string_types): + raise ValueError( + "expected a string on right side of %s" % node.op) + return re.search(right, left, re.IGNORECASE) + elif isinstance(op, ast.BitOr): + return left | right + elif isinstance(op, ast.BitAnd): + return left & right + else: + _raise_invalid_node(node) + + def visit_Compare(self, node): + # keep it simple: binary comparators only + if len(node.ops) != 1: + raise ValueError("syntax for multiple comparators not supported") + left = self.visit(node.left) + op = node.ops[0] + right = self.visit(node.comparators[0]) + if isinstance(op, ast.Eq): + return left == right + elif isinstance(op, ast.NotEq): + return left != right + elif isinstance(op, ast.Lt): + return left < right + elif isinstance(op, ast.LtE): + return left <= right + elif isinstance(op, ast.Gt): + return left > right + elif isinstance(op, ast.GtE): + return left >= right + elif isinstance(op, ast.In): + return left in right + elif isinstance(op, ast.NotIn): + return left not in right + else: + # not used currently: ast.Is | ast.IsNot + _raise_invalid_node(op) + + def visit_Name(self, node): + """Calculate the value of the given identifier.""" + id_ = node.id + if id_ == 'type': + return self.entry.type.lower() + elif id_ == 'key': + return self.entry.key.lower() + elif id_ == 'cited': + return bool(self.cited_docnames) + elif id_ == 'docname': + return self.docname + elif id_ == 'docnames': + return self.cited_docnames + elif id_ == 'True': + return True + elif id_ == 'False': + return False + elif id_ == 'author' or id_ == 'editor': + if id_ in self.entry.persons: + return u' and '.join( + six.text_type(person) # XXX needs fix in pybtex? + for person in self.entry.persons[id_]) + else: + return u'' + else: + return self.entry.fields.get(id_, "") + + def visit_Set(self, node): + return frozenset(self.visit(elt) for elt in node.elts) + + def visit_Str(self, node): + return node.s + + # NameConstant is Python 3.4 only so do not insist on coverage + def visit_NameConstant(self, node): # pragma: no cover + return node.value + + def generic_visit(self, node): + _raise_invalid_node(node) + + +class Cache: + + """Global bibtex extension information cache. Stored in + ``app.env.bibtex_cache``, so must be picklable. + """ + + bibfiles = None + """A :class:`dict` mapping .bib file names (relative to the top + source folder) to :class:`BibfileCache` instances. + """ + + _bibliographies = None + """Each bibliography directive is assigned an id of the form + bibtex-bibliography-xxx. This :class:`dict` maps each docname + to another :class:`dict` which maps each id + to information about the bibliography directive, + :class:`BibliographyCache`. We need to store this extra + information separately because it cannot be stored in the + :class:`~sphinxcontrib.bibtex.nodes.bibliography` nodes + themselves. + """ + + _cited = None + """A :class:`dict` mapping each docname to a :class:`set` of + citation keys. + """ + + _enum_count = None + """A :class:`dict` mapping each docname to an :class:`int` + representing the current bibliography enumeration counter. + """ + + def __init__(self): + + self.bibfiles = {} + self._bibliographies = collections.defaultdict(dict) + self._cited = collections.defaultdict(oset) + self._enum_count = {} + + def purge(self, docname): + """Remove all information related to *docname*. + + :param docname: The document name. + :type docname: :class:`str` + """ + self._bibliographies.pop(docname, None) + self._cited.pop(docname, None) + self._enum_count.pop(docname, None) + + def inc_enum_count(self, docname): + """Increment enumeration list counter for document *docname*.""" + self._enum_count[docname] += 1 + + def set_enum_count(self, docname, value): + """Set enumeration list counter for document *docname* to *value*.""" + self._enum_count[docname] = value + + def get_enum_count(self, docname): + """Get enumeration list counter for document *docname*.""" + return self._enum_count[docname] + + def add_cited(self, key, docname): + """Add the given *key* to the set of cited keys for + *docname*. + + :param key: The citation key. + :type key: :class:`str` + :param docname: The document name. + :type docname: :class:`str` + """ + self._cited[docname].add(key) + + def get_cited_docnames(self, key): + """Return the *docnames* from which the given *key* is cited. + + :param key: The citation key. + :type key: :class:`str` + """ + return frozenset([ + docname for docname, keys in six.iteritems(self._cited) + if key in keys]) + + def get_label_from_key(self, key): + """Return label for the given key.""" + for bibcache in self.get_all_bibliography_caches(): + if key in bibcache.labels: + return bibcache.labels[key] + else: + raise KeyError("%s not found" % key) + + def get_all_cited_keys(self): + """Yield all citation keys, sorted first by document + (alphabetical), then by citation order in the document. + """ + for docname in sorted(self._cited): + for key in self._cited[docname]: + yield key + + def set_bibliography_cache(self, docname, id_, bibcache): + """Register *bibcache* (:class:`BibliographyCache`) + with id *id_* for document *docname*. + """ + assert id_ not in self._bibliographies[docname] + self._bibliographies[docname][id_] = bibcache + + def get_bibliography_cache(self, docname, id_): + """Return :class:`BibliographyCache` with id *id_* in + document *docname*. + """ + return self._bibliographies[docname][id_] + + def get_all_bibliography_caches(self): + """Return all bibliography caches.""" + for bibcaches in six.itervalues(self._bibliographies): + for bibcache in six.itervalues(bibcaches): + yield bibcache + + def _get_bibliography_entries(self, docname, id_, warn): + """Return filtered bibliography entries, sorted by occurence + in the bib file. + """ + # get the information of this bibliography node + bibcache = self.get_bibliography_cache(docname=docname, id_=id_) + # generate entries + for bibfile in bibcache.bibfiles: + data = self.bibfiles[bibfile].data + for entry in six.itervalues(data.entries): + # beware: the prefix is not stored in the data + # to allow reusing the data for multiple bibliographies + cited_docnames = self.get_cited_docnames( + bibcache.keyprefix + entry.key) + visitor = _FilterVisitor( + entry=entry, + docname=docname, + cited_docnames=cited_docnames) + try: + success = visitor.visit(bibcache.filter_) + except ValueError as err: + warn("syntax error in :filter: expression; %s" % err) + # recover by falling back to the default + success = bool(cited_docnames) + if success: + # entries are modified in an unpickable way + # when formatting, so fetch a deep copy + # and return this copy with prefixed key + # we do not deep copy entry.collection because that + # consumes enormous amounts of memory + entry.collection = None + entry2 = copy.deepcopy(entry) + entry2.key = bibcache.keyprefix + entry.key + entry2.collection = data + entry.collection = data + yield entry2 + + def get_bibliography_entries(self, docname, id_, warn): + """Return filtered bibliography entries, sorted by citation order.""" + # get entries, ordered by bib file occurrence + entries = OrderedDict( + (entry.key, entry) for entry in + self._get_bibliography_entries( + docname=docname, id_=id_, warn=warn)) + # order entries according to which were cited first + # first, we add all keys that were cited + # then, we add all remaining keys + sorted_entries = [] + for key in self.get_all_cited_keys(): + try: + entry = entries.pop(key) + except KeyError: + pass + else: + sorted_entries.append(entry) + sorted_entries += six.itervalues(entries) + return sorted_entries + + +class BibfileCache(collections.namedtuple('BibfileCache', 'mtime data')): + + """Contains information about a parsed .bib file. + + .. attribute:: mtime + + A :class:`float` representing the modification time of the .bib + file when it was last parsed. + + .. attribute:: data + + A :class:`pybtex.database.BibliographyData` containing the + parsed .bib file. + + """ + + +class BibliographyCache(collections.namedtuple( + 'BibliographyCache', + """bibfiles style encoding +list_ enumtype start labels labelprefix +filter_ curly_bracket_strip keyprefix +""")): + + """Contains information about a bibliography directive. + + .. attribute:: bibfiles + + A :class:`list` of :class:`str`\\ s containing the .bib file + names (relative to the top source folder) that contain the + references. + + .. attribute:: style + + The bibtex style. + + .. attribute:: list_ + + The list type. + + .. attribute:: enumtype + + The sequence type (only used for enumerated lists). + + .. attribute:: start + + The first ordinal of the sequence (only used for enumerated lists). + + .. attribute:: labels + + Maps citation keys to their final labels. + + .. attribute:: labelprefix + + This bibliography's string prefix for pybtex generated labels. + + .. attribute:: keyprefix + + This bibliography's string prefix for citation keys. + + .. attribute:: filter_ + + An :class:`ast.AST` node, containing the parsed filter expression. + """ diff --git a/common/sphinxcontrib/bibtex/directives.py b/common/sphinxcontrib/bibtex/directives.py new file mode 100644 index 0000000..af8e9db --- /dev/null +++ b/common/sphinxcontrib/bibtex/directives.py @@ -0,0 +1,221 @@ +""" + New Doctree Directives + ~~~~~~~~~~~~~~~~~~~~~~ + + .. autoclass:: BibliographyDirective + + .. automethod:: run + .. automethod:: process_bibfile + .. automethod:: update_bibfile_cache + .. automethod:: parse_bibfile + + .. autofunction:: process_start_option +""" + +import ast # parse(), used for filter +import os.path # getmtime() + +from docutils.parsers.rst import directives # for Directive.option_spec +from sphinx.util.compat import Directive +from sphinx.util.console import bold, standout + +from pybtex.database.input import bibtex +from pybtex.database import BibliographyData + +from sphinxcontrib.bibtex.cache import BibliographyCache, BibfileCache +from sphinxcontrib.bibtex.nodes import bibliography + +# register the latex codec +import latexcodec # noqa + + +def process_start_option(value): + """Process and validate the start option value + of a :rst:dir:`bibliography` directive. + If *value* is ``continue`` then this function returns -1, + otherwise *value* is converted into a positive integer. + """ + if value == "continue": + return -1 + else: + return directives.positive_int(value) + + +class BibliographyDirective(Directive): + + """Class for processing the :rst:dir:`bibliography` directive. + + Parses the bibliography files, and produces a + :class:`~sphinxcontrib.bibtex.nodes.bibliography` node. + + .. seealso:: + + Further processing of the resulting + :class:`~sphinxcontrib.bibtex.nodes.bibliography` node is done + by + :class:`~sphinxcontrib.bibtex.transforms.BibliographyTransform`. + """ + + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + has_content = False + option_spec = { + 'cited': directives.flag, + 'notcited': directives.flag, + 'all': directives.flag, + 'filter': directives.unchanged, + 'style': directives.unchanged, + 'list': directives.unchanged, + 'enumtype': directives.unchanged, + 'start': process_start_option, + 'encoding': directives.encoding, + 'disable-curly-bracket-strip': directives.flag, + 'labelprefix': directives.unchanged, + 'keyprefix': directives.unchanged, + } + + def run(self): + """Process .bib files, set file dependencies, and create a + node that is to be transformed to the entries of the + bibliography. + """ + env = self.state.document.settings.env + # create id and cache for this node + # this id will be stored with the node + # and is used to look up additional data in env.bibtex_cache + # (implementation note: new_serialno only guarantees unique + # ids within a single document, but we need the id to be + # unique across all documents, so we also include the docname + # in the id) + id_ = 'bibtex-bibliography-%s-%s' % ( + env.docname, env.new_serialno('bibtex')) + if "filter" in self.options: + if "all" in self.options: + env.app.warn(standout(":filter: overrides :all:")) + if "notcited" in self.options: + env.app.warn(standout(":filter: overrides :notcited:")) + if "cited" in self.options: + env.app.warn(standout(":filter: overrides :cited:")) + try: + filter_ = ast.parse(self.options["filter"]) + except SyntaxError: + env.app.warn( + standout("syntax error in :filter: expression") + + " (" + self.options["filter"] + "); " + "the option will be ignored" + ) + filter_ = ast.parse("cited") + elif "all" in self.options: + filter_ = ast.parse("True") + elif "notcited" in self.options: + filter_ = ast.parse("not cited") + else: + # the default filter: include only cited entries + filter_ = ast.parse("cited") + bibcache = BibliographyCache( + list_=self.options.get("list", "citation"), + enumtype=self.options.get("enumtype", "arabic"), + start=self.options.get("start", 1), + style=self.options.get( + "style", env.app.config.bibtex_default_style), + filter_=filter_, + encoding=self.options.get( + 'encoding', + 'latex+' + self.state.document.settings.input_encoding), + curly_bracket_strip=( + 'disable-curly-bracket-strip' not in self.options), + labelprefix=self.options.get("labelprefix", ""), + keyprefix=self.options.get("keyprefix", ""), + labels={}, + bibfiles=[], + ) + if (bibcache.list_ not in set(["bullet", "enumerated", "citation"])): + env.app.warn( + "unknown bibliography list type '{0}'.".format(bibcache.list_)) + for bibfile in self.arguments[0].split(): + # convert to normalized absolute path to ensure that the same file + # only occurs once in the cache + bibfile = os.path.normpath(env.relfn2path(bibfile.strip())[1]) + self.process_bibfile(bibfile, bibcache.encoding) + env.note_dependency(bibfile) + bibcache.bibfiles.append(bibfile) + env.bibtex_cache.set_bibliography_cache(env.docname, id_, bibcache) + return [bibliography('', ids=[id_])] + + def parse_bibfile(self, bibfile, encoding): + """Parse *bibfile*, and return parsed data. + + :param bibfile: The bib file name. + :type bibfile: ``str`` + :return: The parsed bibliography data. + :rtype: :class:`pybtex.database.BibliographyData` + """ + app = self.state.document.settings.env.app + parser = bibtex.Parser(encoding) + app.info( + bold("parsing bibtex file {0}... ".format(bibfile)), nonl=True) + parser.parse_file(bibfile) + app.info("parsed {0} entries" + .format(len(parser.data.entries))) + return parser.data + + def update_bibfile_cache(self, bibfile, mtime, encoding): + """Parse *bibfile* (see :meth:`parse_bibfile`), and store the + parsed data, along with modification time *mtime*, in the + bibtex cache. + + :param bibfile: The bib file name. + :type bibfile: ``str`` + :param mtime: The bib file's modification time. + :type mtime: ``float`` + :return: The parsed bibliography data. + :rtype: :class:`pybtex.database.BibliographyData` + """ + data = self.parse_bibfile(bibfile, encoding) + env = self.state.document.settings.env + env.bibtex_cache.bibfiles[bibfile] = BibfileCache( + mtime=mtime, + data=data) + return data + + def process_bibfile(self, bibfile, encoding): + """Check if ``env.bibtex_cache.bibfiles[bibfile]`` is still + up to date. If not, parse the *bibfile* (see + :meth:`update_bibfile_cache`), and store parsed data in the + bibtex cache. + + :param bibfile: The bib file name. + :type bibfile: ``str`` + :return: The parsed bibliography data. + :rtype: :class:`pybtex.database.BibliographyData` + """ + env = self.state.document.settings.env + cache = env.bibtex_cache.bibfiles + # get modification time of bibfile + try: + mtime = os.path.getmtime(bibfile) + except OSError: + env.app.warn( + standout("could not open bibtex file {0}.".format(bibfile))) + cache[bibfile] = BibfileCache( # dummy cache + mtime=-float("inf"), data=BibliographyData()) + return cache[bibfile].data + # get cache and check if it is still up to date + # if it is not up to date, parse the bibtex file + # and store it in the cache + env.app.info( + bold("checking for {0} in bibtex cache... ".format(bibfile)), + nonl=True) + try: + bibfile_cache = cache[bibfile] + except KeyError: + env.app.info("not found") + self.update_bibfile_cache(bibfile, mtime, encoding) + else: + if mtime != bibfile_cache.mtime: + env.app.info("out of date") + self.update_bibfile_cache(bibfile, mtime, encoding) + else: + env.app.info('up to date') + return cache[bibfile].data diff --git a/common/sphinxcontrib/bibtex/nodes.py b/common/sphinxcontrib/bibtex/nodes.py new file mode 100644 index 0000000..426aed9 --- /dev/null +++ b/common/sphinxcontrib/bibtex/nodes.py @@ -0,0 +1,17 @@ +""" + New Doctree Nodes + ~~~~~~~~~~~~~~~~~ + + .. autoclass:: bibliography +""" + +from docutils import nodes + + +class bibliography(nodes.General, nodes.Element): + + """Node for representing a bibliography. Replaced by a list of + citations by + :class:`~sphinxcontrib.bibtex.transforms.BibliographyTransform`. + """ + pass diff --git a/common/sphinxcontrib/bibtex/roles.py b/common/sphinxcontrib/bibtex/roles.py new file mode 100644 index 0000000..bbbd1f0 --- /dev/null +++ b/common/sphinxcontrib/bibtex/roles.py @@ -0,0 +1,43 @@ +""" + New Doctree Roles + ~~~~~~~~~~~~~~~~~ + + .. autoclass:: CiteRole + :show-inheritance: + + .. automethod:: result_nodes +""" + +from pybtex.plugin import find_plugin +import pybtex.database +from sphinx.roles import XRefRole # for :cite: + + +class CiteRole(XRefRole): + + """Class for processing the :rst:role:`cite` role.""" + backend = find_plugin('pybtex.backends', 'docutils')() + + def result_nodes(self, document, env, node, is_ref): + """Transform reference node into a citation reference, + and note that the reference was cited. + """ + keys = node['reftarget'].split(',') + # Note that at this point, usually, env.bibtex_cache.bibfiles + # is still empty because the bibliography directive may not + # have been processed yet, so we cannot get the actual entry. + # Instead, we simply fake an entry with the desired key, and + # fix the label at doctree-resolved time. This happens in + # process_citation_references. + refnodes = [ + self.backend.citation_reference(_fake_entry(key), document) + for key in keys] + for key in keys: + env.bibtex_cache.add_cited(key, env.docname) + return refnodes, [] + + +def _fake_entry(key): + entry = pybtex.database.Entry(type_="") + entry.key = key + return entry diff --git a/common/sphinxcontrib/bibtex/transforms.py b/common/sphinxcontrib/bibtex/transforms.py new file mode 100644 index 0000000..8e4cdcb --- /dev/null +++ b/common/sphinxcontrib/bibtex/transforms.py @@ -0,0 +1,127 @@ +""" + New Doctree Transforms + ~~~~~~~~~~~~~~~~~~~~~~ + + .. autoclass:: BibliographyTransform + :show-inheritance: + + .. autoattribute:: default_priority + .. automethod:: apply + + .. autofunction:: node_text_transform + + .. autofunction:: transform_curly_bracket_strip + + .. autofunction:: transform_url_command +""" + +import docutils.nodes +import docutils.transforms + +from pybtex.plugin import find_plugin + +from sphinxcontrib.bibtex.nodes import bibliography + + +def node_text_transform(node, transform): + """Apply transformation to all Text nodes within node.""" + for child in node.children: + if isinstance(child, docutils.nodes.Text): + node.replace(child, transform(child)) + else: + node_text_transform(child, transform) + + +def transform_curly_bracket_strip(textnode): + """Strip curly brackets from text.""" + text = textnode.astext() + if '{' in text or '}' in text: + text = text.replace('{', '').replace('}', '') + return docutils.nodes.Text(text) + else: + return textnode + + +def transform_url_command(textnode): + """Convert '\\\\url{...}' into a proper docutils hyperlink.""" + text = textnode.astext() + if '\\url' in text: + text1, _, text = text.partition('\\url') + text2, _, text3 = text.partition('}') + text2 = text2.lstrip(' {') + ref = docutils.nodes.reference(refuri=text2) + ref += docutils.nodes.Text(text2) + node = docutils.nodes.inline() + node += transform_url_command(docutils.nodes.Text(text1)) + node += ref + node += transform_url_command(docutils.nodes.Text(text3)) + return node + else: + return textnode + + +class BibliographyTransform(docutils.transforms.Transform): + + """A docutils transform to generate citation entries for + bibliography nodes. + """ + + # transform must be applied before references are resolved + default_priority = 10 + """Priority of the transform. See + http://docutils.sourceforge.net/docs/ref/transforms.html + """ + + def apply(self): + """Transform each + :class:`~sphinxcontrib.bibtex.nodes.bibliography` node into a + list of citations. + """ + env = self.document.settings.env + docname = env.docname + for bibnode in self.document.traverse(bibliography): + id_ = bibnode['ids'][0] + bibcache = env.bibtex_cache.get_bibliography_cache( + docname=docname, id_=id_) + entries = env.bibtex_cache.get_bibliography_entries( + docname=docname, id_=id_, warn=env.app.warn) + # locate and instantiate style and backend plugins + style = find_plugin('pybtex.style.formatting', bibcache.style)() + backend = find_plugin('pybtex.backends', 'docutils')() + # create citation nodes for all references + if bibcache.list_ == "enumerated": + nodes = docutils.nodes.enumerated_list() + nodes['enumtype'] = bibcache.enumtype + if bibcache.start >= 1: + nodes['start'] = bibcache.start + env.bibtex_cache.set_enum_count( + env.docname, bibcache.start) + else: + nodes['start'] = env.bibtex_cache.get_enum_count( + env.docname) + elif bibcache.list_ == "bullet": + nodes = docutils.nodes.bullet_list() + else: # "citation" + nodes = docutils.nodes.paragraph() + # remind: style.format_entries modifies entries in unpickable way + for entry in style.format_entries(entries): + if bibcache.list_ in ["enumerated", "bullet"]: + citation = docutils.nodes.list_item() + citation += backend.paragraph(entry) + else: # "citation" + citation = backend.citation(entry, self.document) + # backend.citation(...) uses entry.key as citation label + # we change it to entry.label later onwards + # but we must note the entry.label now; + # at this point, we also already prefix the label + key = citation[0].astext() + bibcache.labels[key] = bibcache.labelprefix + entry.label + node_text_transform(citation, transform_url_command) + if bibcache.curly_bracket_strip: + node_text_transform( + citation, + transform_curly_bracket_strip) + nodes += citation + if bibcache.list_ == "enumerated": + env.bibtex_cache.inc_enum_count(env.docname) + bibnode.replace_self(nodes) diff --git a/common/waf.py b/common/waf.py index 7947829..3ee4435 100644 --- a/common/waf.py +++ b/common/waf.py @@ -206,7 +206,6 @@ def cmd_configure(ctx): check_sphinx_extension(ctx, 'sphinx.ext.graphviz') check_sphinx_extension(ctx, 'sphinx.ext.intersphinx') check_sphinx_extension(ctx, 'sphinx.ext.mathjax') - check_sphinx_extension(ctx, 'sphinxcontrib.bibtex') # # Optional builds. -- cgit v1.2.3