# -*- 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 <document>").
        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)
