# -*- coding: utf-8 -*- """ sphinx.roles ~~~~~~~~~~~~ Handlers for additional ReST roles. :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re from docutils import nodes, utils from docutils.parsers.rst import roles from sphinx import addnodes from sphinx.locale import _ from sphinx.util import ws_re from sphinx.util.nodes import split_explicit_title, process_index_entry, \ set_role_source_info generic_docroles = { 'command' : nodes.strong, 'dfn' : nodes.emphasis, 'kbd' : nodes.literal, 'mailheader' : addnodes.literal_emphasis, 'makevar' : nodes.strong, 'manpage' : addnodes.literal_emphasis, 'mimetype' : addnodes.literal_emphasis, 'newsgroup' : addnodes.literal_emphasis, 'program' : nodes.strong, # XXX should be an x-ref 'regexp' : nodes.literal, } for rolename, nodeclass in generic_docroles.iteritems(): generic = roles.GenericRole(rolename, nodeclass) role = roles.CustomRole(rolename, generic, {'classes': [rolename]}) roles.register_local_role(rolename, role) # -- generic cross-reference role ---------------------------------------------- class XRefRole(object): """ A generic cross-referencing role. To create a callable that can be used as a role function, create an instance of this class. The general features of this role are: * Automatic creation of a reference and a content node. * Optional separation of title and target with `title `. * The implementation is a class rather than a function to make customization easier. Customization can be done in two ways: * Supplying constructor parameters: * `fix_parens` to normalize parentheses (strip from target, and add to title if configured) * `lowercase` to lowercase the target * `nodeclass` and `innernodeclass` select the node classes for the reference and the content node * Subclassing and overwriting `process_link()` and/or `result_nodes()`. """ nodeclass = addnodes.pending_xref innernodeclass = nodes.literal def __init__(self, fix_parens=False, lowercase=False, nodeclass=None, innernodeclass=None, warn_dangling=False): self.fix_parens = fix_parens self.lowercase = lowercase self.warn_dangling = warn_dangling if nodeclass is not None: self.nodeclass = nodeclass if innernodeclass is not None: self.innernodeclass = innernodeclass def _fix_parens(self, env, has_explicit_title, title, target): if not has_explicit_title: if title.endswith('()'): # remove parentheses title = title[:-2] if env.config.add_function_parentheses: # add them back to all occurrences if configured title += '()' # remove parentheses from the target too if target.endswith('()'): target = target[:-2] return title, target def __call__(self, typ, rawtext, text, lineno, inliner, options={}, content=[]): env = inliner.document.settings.env if not typ: typ = env.config.default_role else: typ = typ.lower() if ':' not in typ: domain, role = '', typ classes = ['xref', role] else: domain, role = typ.split(':', 1) classes = ['xref', domain, '%s-%s' % (domain, role)] # if the first character is a bang, don't cross-reference at all if text[0:1] == '!': text = utils.unescape(text)[1:] if self.fix_parens: text, tgt = self._fix_parens(env, False, text, "") innernode = self.innernodeclass(rawtext, text, classes=classes) return self.result_nodes(inliner.document, env, innernode, is_ref=False) # split title and target in role content has_explicit_title, title, target = split_explicit_title(text) title = utils.unescape(title) target = utils.unescape(target) # fix-up title and target if self.lowercase: target = target.lower() if self.fix_parens: title, target = self._fix_parens( env, has_explicit_title, title, target) # create the reference node refnode = self.nodeclass(rawtext, reftype=role, refdomain=domain, refexplicit=has_explicit_title) # we may need the line number for warnings set_role_source_info(inliner, lineno, refnode) title, target = self.process_link( env, refnode, has_explicit_title, title, target) # now that the target and title are finally determined, set them refnode['reftarget'] = target refnode += self.innernodeclass(rawtext, title, classes=classes) # we also need the source document refnode['refdoc'] = env.docname refnode['refwarn'] = self.warn_dangling # result_nodes allow further modification of return values return self.result_nodes(inliner.document, env, refnode, is_ref=True) # methods that can be overwritten def process_link(self, env, refnode, has_explicit_title, title, target): """Called after parsing title and target text, and creating the reference node (given in *refnode*). This method can alter the reference node and must return a new (or the same) ``(title, target)`` tuple. """ return title, ws_re.sub(' ', target) def result_nodes(self, document, env, node, is_ref): """Called before returning the finished nodes. *node* is the reference node if one was created (*is_ref* is then true), else the content node. This method can add other nodes and must return a ``(nodes, messages)`` tuple (the usual return value of a role function). """ return [node], [] def indexmarkup_role(typ, rawtext, etext, lineno, inliner, options={}, content=[]): """Role for PEP/RFC references that generate an index entry.""" env = inliner.document.settings.env if not typ: typ = env.config.default_role else: typ = typ.lower() text = utils.unescape(etext) targetid = 'index-%s' % env.new_serialno('index') indexnode = addnodes.index() targetnode = nodes.target('', '', ids=[targetid]) inliner.document.note_explicit_target(targetnode) if typ == 'pep': indexnode['entries'] = [ ('single', _('Python Enhancement Proposals; PEP %s') % text, targetid, '')] anchor = '' anchorindex = text.find('#') if anchorindex > 0: text, anchor = text[:anchorindex], text[anchorindex:] try: pepnum = int(text) except ValueError: msg = inliner.reporter.error('invalid PEP number %s' % text, line=lineno) prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] ref = inliner.document.settings.pep_base_url + 'pep-%04d' % pepnum sn = nodes.strong('PEP '+text, 'PEP '+text) rn = nodes.reference('', '', internal=False, refuri=ref+anchor, classes=[typ]) rn += sn return [indexnode, targetnode, rn], [] elif typ == 'rfc': indexnode['entries'] = [('single', 'RFC; RFC %s' % text, targetid, '')] anchor = '' anchorindex = text.find('#') if anchorindex > 0: text, anchor = text[:anchorindex], text[anchorindex:] try: rfcnum = int(text) except ValueError: msg = inliner.reporter.error('invalid RFC number %s' % text, line=lineno) prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] ref = inliner.document.settings.rfc_base_url + inliner.rfc_url % rfcnum sn = nodes.strong('RFC '+text, 'RFC '+text) rn = nodes.reference('', '', internal=False, refuri=ref+anchor, classes=[typ]) rn += sn return [indexnode, targetnode, rn], [] _amp_re = re.compile(r'(?', u'\N{TRIANGULAR BULLET}') spans = _amp_re.split(text) node = nodes.emphasis(rawtext=rawtext) for i, span in enumerate(spans): span = span.replace('&&', '&') if i == 0: if len(span) > 0: textnode = nodes.Text(span) node += textnode continue accel_node = nodes.inline() letter_node = nodes.Text(span[0]) accel_node += letter_node accel_node['classes'].append('accelerator') node += accel_node textnode = nodes.Text(span[1:]) node += textnode node['classes'].append(typ) return [node], [] _litvar_re = re.compile('{([^}]+)}') def emph_literal_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): text = utils.unescape(text) pos = 0 retnode = nodes.literal(role=typ.lower(), classes=[typ]) for m in _litvar_re.finditer(text): if m.start() > pos: txt = text[pos:m.start()] retnode += nodes.Text(txt, txt) retnode += nodes.emphasis(m.group(1), m.group(1)) pos = m.end() if pos < len(text): retnode += nodes.Text(text[pos:], text[pos:]) return [retnode], [] _abbr_re = re.compile('\((.*)\)$', re.S) def abbr_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): text = utils.unescape(text) m = _abbr_re.search(text) if m is None: return [addnodes.abbreviation(text, text)], [] abbr = text[:m.start()].strip() expl = m.group(1) return [addnodes.abbreviation(abbr, abbr, explanation=expl)], [] def index_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): # create new reference target env = inliner.document.settings.env targetid = 'index-%s' % env.new_serialno('index') targetnode = nodes.target('', '', ids=[targetid]) # split text and target in role content has_explicit_title, title, target = split_explicit_title(text) title = utils.unescape(title) target = utils.unescape(target) # if an explicit target is given, we can process it as a full entry if has_explicit_title: entries = process_index_entry(target, targetid) # otherwise we just create a "single" entry else: # but allow giving main entry main = '' if target.startswith('!'): target = target[1:] title = title[1:] main = 'main' entries = [('single', target, targetid, main)] indexnode = addnodes.index() indexnode['entries'] = entries set_role_source_info(inliner, lineno, indexnode) textnode = nodes.Text(title, title) return [indexnode, targetnode, textnode], [] specific_docroles = { # links to download references 'download': XRefRole(nodeclass=addnodes.download_reference), # links to documents 'doc': XRefRole(warn_dangling=True), 'pep': indexmarkup_role, 'rfc': indexmarkup_role, 'guilabel': menusel_role, 'menuselection': menusel_role, 'file': emph_literal_role, 'samp': emph_literal_role, 'abbr': abbr_role, 'index': index_role, } for rolename, func in specific_docroles.iteritems(): roles.register_local_role(rolename, func)