Source code for sphinxcontrib.bibtex.transforms

"""
    New Doctree Transforms
    ~~~~~~~~~~~~~~~~~~~~~~

    .. autoclass:: BibliographyTransform
        :show-inheritance:

        .. autoattribute:: default_priority
        .. automethod:: apply

    .. autoclass:: FilterVisitor
        :members: entry, is_cited
        :show-inheritance:

    .. autofunction:: node_text_transform

    .. autofunction:: transform_curly_bracket_strip

    .. autofunction:: transform_url_command
"""

import sys
if sys.version_info < (2, 7):  # pragma: no cover
    from ordereddict import OrderedDict
else:                          # pragma: no cover
    from collections import OrderedDict

import ast
import re
import copy
import docutils.nodes
import docutils.transforms

from pybtex.plugin import find_plugin

from sphinxcontrib.bibtex.nodes import bibliography


[docs]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)
[docs]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
[docs]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
[docs]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.""" is_cited = False """Whether the entry is cited.""" def _raise_invalid_node(self, node): """Helper method to raise an exception when an invalid node is visited. """ raise ValueError("invalid node %s in filter expression" % node) def __init__(self, entry, is_cited): self.entry = entry self.is_cited = is_cited 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: self._raise_invalid_node(node) def visit_BinOp(self, node): if isinstance(node.op, ast.Mod): # modulo operator is used for regular expression matching name = self.visit(node.left) regexp = self.visit(node.right) if not isinstance(name, basestring): raise ValueError( "expected a string on left side of %s" % node.op) if not isinstance(regexp, basestring): raise ValueError( "expected a string on right side of %s" % node.op) return re.search(regexp, name, re.IGNORECASE) else: self._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 else: # not used currently: ast.Is | ast.IsNot | ast.In | ast.NotIn self._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 self.is_cited 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( unicode(person) for person in self.entry.persons[id_]) else: return u'' else: return self.entry.fields.get(id_, "") def visit_Str(self, node): return node.s def generic_visit(self, node): self._raise_invalid_node(node)
[docs]class BibliographyTransform(docutils.transforms.Transform): # transform must be applied before references are resolved default_priority = 10 """Priority of the transform. See http://docutils.sourceforge.net/docs/ref/transforms.html """
[docs] def apply(self): """Transform each :class:`~sphinxcontrib.bibtex.nodes.bibliography` node into a list of citations. """ env = self.document.settings.env for bibnode in self.document.traverse(bibliography): # get the information of this bibliography node # by looking up its id in the bibliography cache id_ = bibnode['ids'][0] infos = [info for other_id, info in env.bibtex_cache.bibliographies.iteritems() if other_id == id_ and info.docname == env.docname] assert infos, "no bibliography id '%s' in %s" % ( id_, env.docname) assert len(infos) == 1, "duplicate bibliography ids '%s' in %s" % ( id_, env.docname) info = infos[0] # generate entries entries = OrderedDict() for bibfile in info.bibfiles: # XXX entries are modified below in an unpickable way # XXX so fetch a deep copy data = env.bibtex_cache.bibfiles[bibfile].data for entry in data.entries.itervalues(): visitor = FilterVisitor( entry=entry, is_cited=env.bibtex_cache.is_cited(entry.key)) try: ok = visitor.visit(info.filter_) except ValueError as e: env.app.warn( "syntax error in :filter: expression; %s" % e) # recover by falling back to the default ok = env.bibtex_cache.is_cited(entry.key) if ok: entries[entry.key] = copy.deepcopy(entry) # 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 env.bibtex_cache.get_all_cited_keys(): try: entry = entries.pop(key) except KeyError: pass else: sorted_entries.append(entry) sorted_entries += entries.itervalues() # locate and instantiate style and backend plugins style = find_plugin('pybtex.style.formatting', info.style)() backend = find_plugin('pybtex.backends', 'docutils')() # create citation nodes for all references if info.list_ == "enumerated": nodes = docutils.nodes.enumerated_list() nodes['enumtype'] = info.enumtype if info.start >= 1: nodes['start'] = info.start env.bibtex_cache.set_enum_count(env.docname, info.start) else: nodes['start'] = env.bibtex_cache.get_enum_count( env.docname) elif info.list_ == "bullet": nodes = docutils.nodes.bullet_list() else: # "citation" nodes = docutils.nodes.paragraph() # XXX style.format_entries modifies entries in unpickable way for entry in style.format_entries(sorted_entries): if info.list_ == "enumerated" or info.list_ == "bullet": citation = docutils.nodes.list_item() citation += entry.text.render(backend) 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() info.labels[key] = info.labelprefix + entry.label node_text_transform(citation, transform_url_command) if info.curly_bracket_strip: node_text_transform( citation, transform_curly_bracket_strip) nodes += citation if info.list_ == "enumerated": env.bibtex_cache.inc_enum_count(env.docname) bibnode.replace_self(nodes)

Project Versions

This Page