# -*- coding: utf-8 -*- """ sphinx.directives ~~~~~~~~~~~~~~~~~ Handlers for additional ReST directives. :copyright: 2007-2008 by Georg Brandl. :license: BSD. """ import re import sys import string import posixpath from os import path from docutils import nodes from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.roles import caption_ref_re from sphinx.util.compat import make_admonition ws_re = re.compile(r'\s+') # ------ index markup -------------------------------------------------------------- entrytypes = [ 'single', 'pair', 'triple', 'module', 'keyword', 'operator', 'object', 'exception', 'statement', 'builtin', ] def index_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): arguments = arguments[0].split('\n') env = state.document.settings.env targetid = 'index-%s' % env.index_num env.index_num += 1 targetnode = nodes.target('', '', ids=[targetid]) state.document.note_explicit_target(targetnode) indexnode = addnodes.index() indexnode['entries'] = ne = [] for entry in arguments: entry = entry.strip() for type in entrytypes: if entry.startswith(type+':'): value = entry[len(type)+1:].strip() env.note_index_entry(type, value, targetid, value) ne.append((type, value, targetid, value)) break # shorthand notation for single entries else: for value in entry.split(','): env.note_index_entry('single', value.strip(), targetid, value.strip()) ne.append(('single', value.strip(), targetid, value.strip())) return [indexnode, targetnode] index_directive.arguments = (1, 0, 1) directives.register_directive('index', index_directive) # ------ information units --------------------------------------------------------- def desc_index_text(desctype, currmodule, name): if desctype == 'function': if not currmodule: return '%s() (built-in function)' % name return '%s() (in module %s)' % (name, currmodule) elif desctype == 'data': if not currmodule: return '%s (built-in variable)' % name return '%s (in module %s)' % (name, currmodule) elif desctype == 'class': return '%s (class in %s)' % (name, currmodule) elif desctype == 'exception': return name elif desctype == 'method': try: clsname, methname = name.rsplit('.', 1) except: if currmodule: return '%s() (in module %s)' % (name, currmodule) else: return '%s()' % name if currmodule: return '%s() (%s.%s method)' % (methname, currmodule, clsname) else: return '%s() (%s method)' % (methname, clsname) elif desctype == 'attribute': try: clsname, attrname = name.rsplit('.', 1) except: if currmodule: return '%s (in module %s)' % (name, currmodule) else: return name if currmodule: return '%s (%s.%s attribute)' % (attrname, currmodule, clsname) else: return '%s (%s attribute)' % (attrname, clsname) elif desctype == 'opcode': return '%s (opcode)' % name elif desctype == 'cfunction': return '%s (C function)' % name elif desctype == 'cmember': return '%s (C member)' % name elif desctype == 'cmacro': return '%s (C macro)' % name elif desctype == 'ctype': return '%s (C type)' % name elif desctype == 'cvar': return '%s (C variable)' % name else: raise ValueError("unhandled descenv: %s" % desctype) # ------ functions to parse a Python or C signature and create desc_* nodes. py_sig_re = re.compile(r'''^([\w.]*\.)? # class names (\w+) \s* # thing name (?: \((.*)\) )? $ # optionally arguments ''', re.VERBOSE) py_paramlist_re = re.compile(r'([\[\],])') # split at '[', ']' and ',' def parse_py_signature(signode, sig, desctype, env): """ Transform a python signature into RST nodes. Return (fully qualified name of the thing, classname if any). If inside a class, the current class name is handled intelligently: * it is stripped from the displayed name if present * it is added to the full name (return value) if not present """ m = py_sig_re.match(sig) if m is None: raise ValueError classname, name, arglist = m.groups() add_module = True if env.currclass: if classname and classname.startswith(env.currclass): fullname = classname + name # class name is given again in the signature classname = classname[len(env.currclass):].lstrip('.') add_module = False elif classname: # class name is given in the signature, but different fullname = env.currclass + '.' + classname + name else: # class name is not given in the signature fullname = env.currclass + '.' + name add_module = False else: fullname = classname and classname + name or name if classname: signode += addnodes.desc_classname(classname, classname) # exceptions are a special case, since they are documented in the # 'exceptions' module. elif add_module and env.config.add_module_names and \ env.currmodule and env.currmodule != 'exceptions': nodetext = env.currmodule + '.' signode += addnodes.desc_classname(nodetext, nodetext) signode += addnodes.desc_name(name, name) if not arglist: if desctype in ('function', 'method'): # for callables, add an empty parameter list signode += addnodes.desc_parameterlist() return fullname, classname signode += addnodes.desc_parameterlist() stack = [signode[-1]] for token in py_paramlist_re.split(arglist): if token == '[': opt = addnodes.desc_optional() stack[-1] += opt stack.append(opt) elif token == ']': try: stack.pop() except IndexError: raise ValueError elif not token or token == ',' or token.isspace(): pass else: token = token.strip() stack[-1] += addnodes.desc_parameter(token, token) if len(stack) != 1: raise ValueError return fullname, classname c_sig_re = re.compile( r'''^([^(]*?) # return type ([\w:]+) \s* # thing name (colon allowed for C++ class names) (?: \((.*)\) )? $ # optionally arguments ''', re.VERBOSE) c_funcptr_sig_re = re.compile( r'''^([^(]+?) # return type (\( [^()]+ \)) \s* # name in parentheses \( (.*) \) $ # arguments ''', re.VERBOSE) c_funcptr_name_re = re.compile(r'^\(\s*\*\s*(.*?)\s*\)$') # RE to split at word boundaries wsplit_re = re.compile(r'(\W+)') # These C types aren't described in the reference, so don't try to create # a cross-reference to them stopwords = set(('const', 'void', 'char', 'int', 'long', 'FILE', 'struct')) def parse_c_type(node, ctype): # add cross-ref nodes for all words for part in filter(None, wsplit_re.split(ctype)): tnode = nodes.Text(part, part) if part[0] in string.letters+'_' and part not in stopwords: pnode = addnodes.pending_xref( '', reftype='ctype', reftarget=part, modname=None, classname=None) pnode += tnode node += pnode else: node += tnode def parse_c_signature(signode, sig, desctype): """Transform a C (or C++) signature into RST nodes.""" # first try the function pointer signature regex, it's more specific m = c_funcptr_sig_re.match(sig) if m is None: m = c_sig_re.match(sig) if m is None: raise ValueError('no match') rettype, name, arglist = m.groups() signode += addnodes.desc_type("", "") parse_c_type(signode[-1], rettype) signode += addnodes.desc_name(name, name) # clean up parentheses from canonical name m = c_funcptr_name_re.match(name) if m: name = m.group(1) if not arglist: if desctype == 'cfunction': # for functions, add an empty parameter list signode += addnodes.desc_parameterlist() return name paramlist = addnodes.desc_parameterlist() arglist = arglist.replace('`', '').replace('\\ ', '') # remove markup # this messes up function pointer types, but not too badly ;) args = arglist.split(',') for arg in args: arg = arg.strip() param = addnodes.desc_parameter('', '', noemph=True) try: ctype, argname = arg.rsplit(' ', 1) except ValueError: # no argument name given, only the type parse_c_type(param, arg) else: parse_c_type(param, ctype) param += nodes.emphasis(' '+argname, ' '+argname) paramlist += param signode += paramlist return name opcode_sig_re = re.compile(r'(\w+(?:\+\d)?)\s*\((.*)\)') def parse_opcode_signature(signode, sig): """Transform an opcode signature into RST nodes.""" m = opcode_sig_re.match(sig) if m is None: raise ValueError opname, arglist = m.groups() signode += addnodes.desc_name(opname, opname) paramlist = addnodes.desc_parameterlist() signode += paramlist paramlist += addnodes.desc_parameter(arglist, arglist) return opname.strip() option_desc_re = re.compile( r'(/|-|--)([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)') def parse_option_desc(signode, sig): """Transform an option description into RST nodes.""" count = 0 firstname = '' for m in option_desc_re.finditer(sig): prefix, optname, args = m.groups() if count: signode += addnodes.desc_classname(', ', ', ') signode += addnodes.desc_name(prefix+optname, prefix+optname) signode += addnodes.desc_classname(args, args) if not count: firstname = optname count += 1 if not firstname: raise ValueError return firstname def desc_directive(desctype, arguments, options, content, lineno, content_offset, block_text, state, state_machine): env = state.document.settings.env node = addnodes.desc() node['desctype'] = desctype noindex = ('noindex' in options) node['noindex'] = noindex # remove backslashes to support (dummy) escapes; helps Vim's highlighting signatures = map(lambda s: s.strip().replace('\\', ''), arguments[0].split('\n')) names = [] clsname = None for i, sig in enumerate(signatures): # add a signature node for each signature in the current unit # and add a reference target for it sig = sig.strip() signode = addnodes.desc_signature(sig, '') signode['first'] = False node.append(signode) try: if desctype in ('function', 'data', 'class', 'exception', 'method', 'attribute'): name, clsname = parse_py_signature(signode, sig, desctype, env) elif desctype in ('cfunction', 'cmember', 'cmacro', 'ctype', 'cvar'): name = parse_c_signature(signode, sig, desctype) elif desctype == 'opcode': name = parse_opcode_signature(signode, sig) elif desctype == 'cmdoption': optname = parse_option_desc(signode, sig) if not noindex: targetname = 'cmdoption-' + optname signode['ids'].append(targetname) state.document.note_explicit_target(signode) env.note_index_entry('pair', 'command line option; %s' % sig, targetname, targetname) env.note_reftarget('option', optname, targetname) continue elif desctype == 'describe': signode.clear() signode += addnodes.desc_name(sig, sig) continue else: # another registered generic x-ref directive rolename, indextemplate, parse_node = additional_xref_types[desctype] if parse_node: fullname = parse_node(env, sig, signode) else: signode.clear() signode += addnodes.desc_name(sig, sig) # normalize whitespace like xfileref_role does fullname = ws_re.sub('', sig) if not noindex: targetname = '%s-%s' % (rolename, fullname) signode['ids'].append(targetname) state.document.note_explicit_target(signode) if indextemplate: indexentry = indextemplate % (fullname,) indextype = 'single' colon = indexentry.find(':') if colon != -1: indextype = indexentry[:colon].strip() indexentry = indexentry[colon+1:].strip() env.note_index_entry(indextype, indexentry, targetname, targetname) env.note_reftarget(rolename, fullname, targetname) # don't use object indexing below continue except ValueError, err: # signature parsing failed signode.clear() signode += addnodes.desc_name(sig, sig) continue # we don't want an index entry here # only add target and index entry if this is the first description of the # function name in this desc block if not noindex and name not in names: fullname = (env.currmodule and env.currmodule + '.' or '') + name # note target if fullname not in state.document.ids: signode['names'].append(fullname) signode['ids'].append(fullname) signode['first'] = (not names) state.document.note_explicit_target(signode) env.note_descref(fullname, desctype, lineno) names.append(name) env.note_index_entry('single', desc_index_text(desctype, env.currmodule, name), fullname, fullname) subnode = addnodes.desc_content() # needed for automatic qualification of members clsname_set = False if desctype in ('class', 'exception') and names: env.currclass = names[0] clsname_set = True elif desctype in ('method', 'attribute') and clsname and not env.currclass: env.currclass = clsname.strip('.') clsname_set = True # needed for association of version{added,changed} directives if names: env.currdesc = names[0] state.nested_parse(content, content_offset, subnode) if clsname_set: env.currclass = None env.currdesc = None node.append(subnode) return [node] desc_directive.content = 1 desc_directive.arguments = (1, 0, 1) desc_directive.options = {'noindex': directives.flag} desctypes = [ # the Python ones 'function', 'data', 'class', 'method', 'attribute', 'exception', # the C ones 'cfunction', 'cmember', 'cmacro', 'ctype', 'cvar', # the odd one 'opcode', # for command line options 'cmdoption', # the generic one 'describe', 'envvar', ] for _name in desctypes: directives.register_directive(_name, desc_directive) # Generic cross-reference types; they can be registered in the application; # the directives are either desc_directive or target_directive additional_xref_types = { # directive name: (role name, index text, function to parse the desc node) 'envvar': ('envvar', 'environment variable; %s', None), } # ------ target -------------------------------------------------------------------- def target_directive(targettype, arguments, options, content, lineno, content_offset, block_text, state, state_machine): """Generic target for user-defined cross-reference types.""" env = state.document.settings.env rolename, indextemplate, _ = additional_xref_types[targettype] # normalize whitespace in fullname like xfileref_role does fullname = ws_re.sub('', arguments[0].strip()) targetname = '%s-%s' % (rolename, fullname) node = nodes.target('', '', ids=[targetname]) state.document.note_explicit_target(node) if indextemplate: indexentry = indextemplate % (fullname,) indextype = 'single' colon = indexentry.find(':') if colon != -1: indextype = indexentry[:colon].strip() indexentry = indexentry[colon+1:].strip() env.note_index_entry(indextype, indexentry, targetname, targetname) env.note_reftarget(rolename, fullname, targetname) return [node] target_directive.content = 0 target_directive.arguments = (1, 0, 1) # ------ versionadded/versionchanged ----------------------------------------------- def version_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): node = addnodes.versionmodified() node['type'] = name node['version'] = arguments[0] if len(arguments) == 2: inodes, messages = state.inline_text(arguments[1], lineno+1) node.extend(inodes) if content: state.nested_parse(content, content_offset, node) ret = [node] + messages else: ret = [node] env = state.document.settings.env env.note_versionchange(node['type'], node['version'], node, lineno) return ret version_directive.arguments = (1, 1, 1) version_directive.content = 1 directives.register_directive('deprecated', version_directive) directives.register_directive('versionadded', version_directive) directives.register_directive('versionchanged', version_directive) # ------ see also ------------------------------------------------------------------ def seealso_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): rv = make_admonition( addnodes.seealso, name, ['See also'], options, content, lineno, content_offset, block_text, state, state_machine) return rv seealso_directive.content = 1 seealso_directive.arguments = (0, 0, 0) directives.register_directive('seealso', seealso_directive) # ------ production list (for the reference) --------------------------------------- token_re = re.compile('`([a-z_]+)`') def token_xrefs(text, env): retnodes = [] pos = 0 for m in token_re.finditer(text): if m.start() > pos: txt = text[pos:m.start()] retnodes.append(nodes.Text(txt, txt)) refnode = addnodes.pending_xref(m.group(1)) refnode['reftype'] = 'token' refnode['reftarget'] = m.group(1) refnode['modname'] = env.currmodule refnode['classname'] = env.currclass refnode += nodes.literal(m.group(1), m.group(1), classes=['xref']) retnodes.append(refnode) pos = m.end() if pos < len(text): retnodes.append(nodes.Text(text[pos:], text[pos:])) return retnodes def productionlist_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): env = state.document.settings.env node = addnodes.productionlist() messages = [] i = 0 for rule in arguments[0].split('\n'): if i == 0 and ':' not in rule: # production group continue i += 1 try: name, tokens = rule.split(':', 1) except ValueError: break subnode = addnodes.production() subnode['tokenname'] = name.strip() if subnode['tokenname']: idname = 'grammar-token-%s' % subnode['tokenname'] if idname not in state.document.ids: subnode['ids'].append(idname) state.document.note_implicit_target(subnode, subnode) env.note_reftarget('token', subnode['tokenname'], idname) subnode.extend(token_xrefs(tokens, env)) node.append(subnode) return [node] + messages productionlist_directive.content = 0 productionlist_directive.arguments = (1, 0, 1) directives.register_directive('productionlist', productionlist_directive) # ------ section metadata ---------------------------------------------------------- def module_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): env = state.document.settings.env modname = arguments[0].strip() env.currmodule = modname env.note_module(modname, options.get('synopsis', ''), options.get('platform', ''), 'deprecated' in options) modulenode = addnodes.module() modulenode['modname'] = modname modulenode['synopsis'] = options.get('synopsis', '') targetnode = nodes.target('', '', ids=['module-' + modname]) state.document.note_explicit_target(targetnode) ret = [modulenode, targetnode] if 'platform' in options: modulenode['platform'] = options['platform'] node = nodes.paragraph() node += nodes.emphasis('Platforms: ', 'Platforms: ') node += nodes.Text(options['platform'], options['platform']) ret.append(node) # the synopsis isn't printed; in fact, it is only used in the modindex currently env.note_index_entry('single', '%s (module)' % modname, 'module-' + modname, modname) return ret module_directive.arguments = (1, 0, 0) module_directive.options = {'platform': lambda x: x, 'synopsis': lambda x: x, 'deprecated': directives.flag} directives.register_directive('module', module_directive) def currentmodule_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): # This directive is just to tell people that we're documenting # stuff in module foo, but links to module foo won't lead here. env = state.document.settings.env modname = arguments[0].strip() env.currmodule = modname return [] currentmodule_directive.arguments = (1, 0, 0) directives.register_directive('currentmodule', currentmodule_directive) def author_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): # Show authors only if the show_authors option is on env = state.document.settings.env if not env.config.show_authors: return [] para = nodes.paragraph() emph = nodes.emphasis() para += emph if name == 'sectionauthor': text = 'Section author: ' elif name == 'moduleauthor': text = 'Module author: ' else: text = 'Author: ' emph += nodes.Text(text, text) inodes, messages = state.inline_text(arguments[0], lineno) emph.extend(inodes) return [para] + messages author_directive.arguments = (1, 0, 1) directives.register_directive('sectionauthor', author_directive) directives.register_directive('moduleauthor', author_directive) # ------ toctree directive --------------------------------------------------------- def toctree_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): env = state.document.settings.env suffix = env.config.source_suffix dirname = posixpath.dirname(env.docname) ret = [] subnode = addnodes.toctree() includefiles = [] includetitles = {} for docname in content: if not docname: continue # look for explicit titles and documents ("Some Title "). m = caption_ref_re.match(docname) if m: docname = m.group(2) includetitles[docname] = m.group(1) # absolutize filenames, remove suffixes if docname.endswith(suffix): docname = docname[:-len(suffix)] docname = posixpath.normpath(posixpath.join(dirname, docname)) if docname not in env.found_docs: ret.append(state.document.reporter.warning( 'toctree references unknown document %r' % docname, line=lineno)) else: includefiles.append(docname) subnode['includefiles'] = includefiles subnode['includetitles'] = includetitles subnode['maxdepth'] = options.get('maxdepth', -1) ret.append(subnode) return ret toctree_directive.content = 1 toctree_directive.options = {'maxdepth': int} directives.register_directive('toctree', toctree_directive) # ------ centered directive --------------------------------------------------------- def centered_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): if not arguments: return [] subnode = addnodes.centered() inodes, messages = state.inline_text(arguments[0], lineno) subnode.extend(inodes) return [subnode] + messages centered_directive.arguments = (1, 0, 1) directives.register_directive('centered', centered_directive) # ------ highlight directive -------------------------------------------------------- def highlightlang_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): if 'linenothreshold' in options: try: linenothreshold = int(options['linenothreshold']) except Exception: linenothreshold = 10 else: linenothreshold = sys.maxint return [addnodes.highlightlang(lang=arguments[0].strip(), linenothreshold=linenothreshold)] highlightlang_directive.content = 0 highlightlang_directive.arguments = (1, 0, 0) highlightlang_directive.options = {'linenothreshold': directives.unchanged} directives.register_directive('highlight', highlightlang_directive) directives.register_directive('highlightlang', highlightlang_directive) # old name # ------ code-block directive ------------------------------------------------------- def codeblock_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): code = u'\n'.join(content) literal = nodes.literal_block(code, code) literal['language'] = arguments[0] literal['linenos'] = 'linenos' in options return [literal] codeblock_directive.content = 1 codeblock_directive.arguments = (1, 0, 0) codeblock_directive.options = {'linenos': directives.flag} directives.register_directive('code-block', codeblock_directive) directives.register_directive('sourcecode', codeblock_directive) # ------ literalinclude directive --------------------------------------------------- def literalinclude_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): """Like .. include:: :literal:, but only warns if the include file is not found.""" if not state.document.settings.file_insertion_enabled: return [state.document.reporter.warning('File insertion disabled', line=lineno)] env = state.document.settings.env rel_fn = arguments[0] source_dir = path.dirname(path.abspath(state_machine.input_lines.source( lineno - state_machine.input_offset - 1))) fn = path.normpath(path.join(source_dir, rel_fn)) try: f = open(fn) text = f.read() f.close() except (IOError, OSError): retnode = state.document.reporter.warning( 'Include file %r not found or reading it failed' % arguments[0], line=lineno) else: retnode = nodes.literal_block(text, text, source=fn) retnode.line = 1 if options.get('language', ''): retnode['language'] = options['language'] if 'linenos' in options: retnode['linenos'] = True state.document.settings.env.note_dependency(rel_fn) return [retnode] literalinclude_directive.options = {'linenos': directives.flag, 'language': directives.unchanged} literalinclude_directive.content = 0 literalinclude_directive.arguments = (1, 0, 0) directives.register_directive('literalinclude', literalinclude_directive) # ------ glossary directive --------------------------------------------------------- def glossary_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): """Glossary with cross-reference targets for :dfn: roles.""" env = state.document.settings.env node = addnodes.glossary() state.nested_parse(content, content_offset, node) # the content should be definition lists dls = [child for child in node if isinstance(child, nodes.definition_list)] # now, extract definition terms to enable cross-reference creation for dl in dls: dl['classes'].append('glossary') for li in dl.children: if not li.children or not isinstance(li[0], nodes.term): continue termtext = li.children[0].astext() new_id = 'term-' + nodes.make_id(termtext) if new_id in env.gloss_entries: new_id = 'term-' + str(len(env.gloss_entries)) env.gloss_entries.add(new_id) li[0]['names'].append(new_id) li[0]['ids'].append(new_id) state.document.settings.env.note_reftarget('term', termtext.lower(), new_id) return [node] glossary_directive.content = 1 glossary_directive.arguments = (0, 0, 0) directives.register_directive('glossary', glossary_directive) # ------ acks directive ------------------------------------------------------------- def acks_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): node = addnodes.acks() state.nested_parse(content, content_offset, node) if len(node.children) != 1 or not isinstance(node.children[0], nodes.bullet_list): return [state.document.reporter.warning('.. acks content is not a list', line=lineno)] return [node] acks_directive.content = 1 acks_directive.arguments = (0, 0, 0) directives.register_directive('acks', acks_directive)