| # -*- coding: utf-8 -*- |
| """ |
| sphinx.roles |
| ~~~~~~~~~~~~ |
| |
| Handlers for additional ReST roles. |
| |
| :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. |
| :license: BSD, see LICENSE for details. |
| """ |
| |
| import re |
| |
| from six import iteritems |
| 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' : addnodes.literal_strong, |
| 'dfn' : nodes.emphasis, |
| 'kbd' : nodes.literal, |
| 'mailheader' : addnodes.literal_emphasis, |
| 'makevar' : addnodes.literal_strong, |
| 'manpage' : addnodes.literal_emphasis, |
| 'mimetype' : addnodes.literal_emphasis, |
| 'newsgroup' : addnodes.literal_emphasis, |
| 'program' : addnodes.literal_strong, # XXX should be an x-ref |
| 'regexp' : nodes.literal, |
| } |
| |
| for rolename, nodeclass in iteritems(generic_docroles): |
| 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 <target>`. |
| * 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, text, 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() |
| has_explicit_title, title, target = split_explicit_title(text) |
| title = utils.unescape(title) |
| target = utils.unescape(target) |
| 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') % target, |
| targetid, '')] |
| anchor = '' |
| anchorindex = target.find('#') |
| if anchorindex > 0: |
| target, anchor = target[:anchorindex], target[anchorindex:] |
| if not has_explicit_title: |
| title = "PEP " + utils.unescape(title) |
| try: |
| pepnum = int(target) |
| except ValueError: |
| msg = inliner.reporter.error('invalid PEP number %s' % target, |
| line=lineno) |
| prb = inliner.problematic(rawtext, rawtext, msg) |
| return [prb], [msg] |
| ref = inliner.document.settings.pep_base_url + 'pep-%04d' % pepnum |
| sn = nodes.strong(title, title) |
| 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' % target, targetid, '')] |
| anchor = '' |
| anchorindex = target.find('#') |
| if anchorindex > 0: |
| target, anchor = target[:anchorindex], target[anchorindex:] |
| if not has_explicit_title: |
| title = "RFC " + utils.unescape(title) |
| try: |
| rfcnum = int(target) |
| except ValueError: |
| msg = inliner.reporter.error('invalid RFC number %s' % target, |
| 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(title, title) |
| rn = nodes.reference('', '', internal=False, refuri=ref+anchor, |
| classes=[typ]) |
| rn += sn |
| return [indexnode, targetnode, rn], [] |
| |
| |
| _amp_re = re.compile(r'(?<!&)&(?![&\s])') |
| |
| def menusel_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): |
| text = utils.unescape(text) |
| if typ == 'menuselection': |
| text = text.replace('-->', 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, **options)], [] |
| abbr = text[:m.start()].strip() |
| expl = m.group(1) |
| options = options.copy() |
| options['explanation'] = expl |
| return [addnodes.abbreviation(abbr, abbr, **options)], [] |
| |
| |
| 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 iteritems(specific_docroles): |
| roles.register_local_role(rolename, func) |