1# $Id$ 
    2# Author: David Goodger <goodger@python.org> 
    3# Copyright: This module has been placed in the public domain. 
    4 
    5""" 
    6This package contains directive implementation modules. 
    7""" 
    8 
    9__docformat__ = 'reStructuredText' 
    10 
    11import re 
    12import codecs 
    13from importlib import import_module 
    14 
    15from docutils import nodes, parsers 
    16from docutils.utils import split_escaped_whitespace, escape2null 
    17from docutils.parsers.rst.languages import en as _fallback_language_module 
    18 
    19 
    20_directive_registry = { 
    21      'attention': ('admonitions', 'Attention'), 
    22      'caution': ('admonitions', 'Caution'), 
    23      'code': ('body', 'CodeBlock'), 
    24      'danger': ('admonitions', 'Danger'), 
    25      'error': ('admonitions', 'Error'), 
    26      'important': ('admonitions', 'Important'), 
    27      'note': ('admonitions', 'Note'), 
    28      'tip': ('admonitions', 'Tip'), 
    29      'hint': ('admonitions', 'Hint'), 
    30      'warning': ('admonitions', 'Warning'), 
    31      'admonition': ('admonitions', 'Admonition'), 
    32      'sidebar': ('body', 'Sidebar'), 
    33      'topic': ('body', 'Topic'), 
    34      'line-block': ('body', 'LineBlock'), 
    35      'parsed-literal': ('body', 'ParsedLiteral'), 
    36      'math': ('body', 'MathBlock'), 
    37      'rubric': ('body', 'Rubric'), 
    38      'epigraph': ('body', 'Epigraph'), 
    39      'highlights': ('body', 'Highlights'), 
    40      'pull-quote': ('body', 'PullQuote'), 
    41      'compound': ('body', 'Compound'), 
    42      'container': ('body', 'Container'), 
    43      # 'questions': ('body', 'question_list'), 
    44      'table': ('tables', 'RSTTable'), 
    45      'csv-table': ('tables', 'CSVTable'), 
    46      'list-table': ('tables', 'ListTable'), 
    47      'image': ('images', 'Image'), 
    48      'figure': ('images', 'Figure'), 
    49      'contents': ('parts', 'Contents'), 
    50      'sectnum': ('parts', 'Sectnum'), 
    51      'header': ('parts', 'Header'), 
    52      'footer': ('parts', 'Footer'), 
    53      # 'footnotes': ('parts', 'footnotes'), 
    54      # 'citations': ('parts', 'citations'), 
    55      'target-notes': ('references', 'TargetNotes'), 
    56      'meta': ('misc', 'Meta'), 
    57      # 'imagemap': ('html', 'imagemap'), 
    58      'raw': ('misc', 'Raw'), 
    59      'include': ('misc', 'Include'), 
    60      'replace': ('misc', 'Replace'), 
    61      'unicode': ('misc', 'Unicode'), 
    62      'class': ('misc', 'Class'), 
    63      'role': ('misc', 'Role'), 
    64      'default-role': ('misc', 'DefaultRole'), 
    65      'title': ('misc', 'Title'), 
    66      'date': ('misc', 'Date'), 
    67      'restructuredtext-test-directive': ('misc', 'TestDirective'), 
    68      } 
    69"""Mapping of directive name to (module name, class name).  The 
    70directive name is canonical & must be lowercase.  Language-dependent 
    71names are defined in the ``language`` subpackage.""" 
    72 
    73_directives = {} 
    74"""Cache of imported directives.""" 
    75 
    76 
    77def directive(directive_name, language_module, document): 
    78    """ 
    79    Locate and return a directive function from its language-dependent name. 
    80    If not found in the current language, check English.  Return None if the 
    81    named directive cannot be found. 
    82    """ 
    83    normname = directive_name.lower() 
    84    messages = [] 
    85    msg_text = [] 
    86    if normname in _directives: 
    87        return _directives[normname], messages 
    88    canonicalname = None 
    89    try: 
    90        canonicalname = language_module.directives[normname] 
    91    except AttributeError as error: 
    92        msg_text.append('Problem retrieving directive entry from language ' 
    93                        'module %r: %s.' % (language_module, error)) 
    94    except KeyError: 
    95        msg_text.append('No directive entry for "%s" in module "%s".' 
    96                        % (directive_name, language_module.__name__)) 
    97    if not canonicalname: 
    98        try: 
    99            canonicalname = _fallback_language_module.directives[normname] 
    100            msg_text.append('Using English fallback for directive "%s".' 
    101                            % directive_name) 
    102        except KeyError: 
    103            msg_text.append('Trying "%s" as canonical directive name.' 
    104                            % directive_name) 
    105            # The canonical name should be an English name, but just in case: 
    106            canonicalname = normname 
    107    if msg_text: 
    108        message = document.reporter.info( 
    109            '\n'.join(msg_text), line=document.current_line) 
    110        messages.append(message) 
    111    try: 
    112        modulename, classname = _directive_registry[canonicalname] 
    113    except KeyError: 
    114        # Error handling done by caller. 
    115        return None, messages 
    116    try: 
    117        module = import_module('docutils.parsers.rst.directives.'+modulename) 
    118    except ImportError as detail: 
    119        messages.append(document.reporter.error( 
    120            'Error importing directive module "%s" (directive "%s"):\n%s' 
    121            % (modulename, directive_name, detail), 
    122            line=document.current_line)) 
    123        return None, messages 
    124    try: 
    125        directive = getattr(module, classname) 
    126        _directives[normname] = directive 
    127    except AttributeError: 
    128        messages.append(document.reporter.error( 
    129            'No directive class "%s" in module "%s" (directive "%s").' 
    130            % (classname, modulename, directive_name), 
    131            line=document.current_line)) 
    132        return None, messages 
    133    return directive, messages 
    134 
    135 
    136def register_directive(name, directive): 
    137    """ 
    138    Register a nonstandard application-defined directive function. 
    139    Language lookups are not needed for such functions. 
    140    """ 
    141    _directives[name] = directive 
    142 
    143 
    144# conversion functions for `Directive.option_spec` 
    145# ------------------------------------------------ 
    146# 
    147# see also `parsers.rst.Directive` in ../__init__.py. 
    148 
    149 
    150def flag(argument): 
    151    """ 
    152    Check for a valid flag option (no argument) and return ``None``. 
    153    (Directive option conversion function.) 
    154 
    155    Raise ``ValueError`` if an argument is found. 
    156    """ 
    157    if argument and argument.strip(): 
    158        raise ValueError('no argument is allowed; "%s" supplied' % argument) 
    159    else: 
    160        return None 
    161 
    162 
    163def unchanged_required(argument): 
    164    """ 
    165    Return the argument text, unchanged. 
    166    (Directive option conversion function.) 
    167 
    168    Raise ``ValueError`` if no argument is found. 
    169    """ 
    170    if argument is None: 
    171        raise ValueError('argument required but none supplied') 
    172    else: 
    173        return argument  # unchanged! 
    174 
    175 
    176def unchanged(argument): 
    177    """ 
    178    Return the argument text, unchanged. 
    179    (Directive option conversion function.) 
    180 
    181    No argument implies empty string (""). 
    182    """ 
    183    if argument is None: 
    184        return '' 
    185    else: 
    186        return argument  # unchanged! 
    187 
    188 
    189def path(argument): 
    190    """ 
    191    Return the path argument unwrapped (with newlines removed). 
    192    (Directive option conversion function.) 
    193 
    194    Raise ``ValueError`` if no argument is found. 
    195    """ 
    196    if argument is None: 
    197        raise ValueError('argument required but none supplied') 
    198    else: 
    199        return ''.join(s.strip() for s in argument.splitlines()) 
    200 
    201 
    202def uri(argument): 
    203    """ 
    204    Return the URI argument with unescaped whitespace removed. 
    205    (Directive option conversion function.) 
    206 
    207    Raise ``ValueError`` if no argument is found. 
    208    """ 
    209    if argument is None: 
    210        raise ValueError('argument required but none supplied') 
    211    else: 
    212        parts = split_escaped_whitespace(escape2null(argument)) 
    213        return ' '.join(''.join(nodes.unescape(part).split()) 
    214                        for part in parts) 
    215 
    216 
    217def nonnegative_int(argument): 
    218    """ 
    219    Check for a nonnegative integer argument; raise ``ValueError`` if not. 
    220    (Directive option conversion function.) 
    221    """ 
    222    value = int(argument) 
    223    if value < 0: 
    224        raise ValueError('negative value; must be positive or zero') 
    225    return value 
    226 
    227 
    228def percentage(argument): 
    229    """ 
    230    Check for an integer percentage value with optional percent sign. 
    231    (Directive option conversion function.) 
    232    """ 
    233    try: 
    234        argument = argument.rstrip(' %') 
    235    except AttributeError: 
    236        pass 
    237    return nonnegative_int(argument) 
    238 
    239 
    240length_units = ['em', 'ex', 'px', 'in', 'cm', 'mm', 'pt', 'pc'] 
    241 
    242 
    243def get_measure(argument, units): 
    244    """ 
    245    Check for a positive argument of one of the units and return a 
    246    normalized string of the form "<value><unit>" (without space in 
    247    between). 
    248    (Directive option conversion function.) 
    249 
    250    To be called from directive option conversion functions. 
    251    """ 
    252    match = re.match(r'^([0-9.]+) *(%s)$' % '|'.join(units), argument) 
    253    try: 
    254        float(match.group(1)) 
    255    except (AttributeError, ValueError): 
    256        raise ValueError( 
    257            'not a positive measure of one of the following units:\n%s' 
    258            % ' '.join('"%s"' % i for i in units)) 
    259    return match.group(1) + match.group(2) 
    260 
    261 
    262def length_or_unitless(argument): 
    263    return get_measure(argument, length_units + ['']) 
    264 
    265 
    266def length_or_percentage_or_unitless(argument, default=''): 
    267    """ 
    268    Return normalized string of a length or percentage unit. 
    269    (Directive option conversion function.) 
    270 
    271    Add <default> if there is no unit. Raise ValueError if the argument is not 
    272    a positive measure of one of the valid CSS units (or without unit). 
    273 
    274    >>> length_or_percentage_or_unitless('3 pt') 
    275    '3pt' 
    276    >>> length_or_percentage_or_unitless('3%', 'em') 
    277    '3%' 
    278    >>> length_or_percentage_or_unitless('3') 
    279    '3' 
    280    >>> length_or_percentage_or_unitless('3', 'px') 
    281    '3px' 
    282    """ 
    283    try: 
    284        return get_measure(argument, length_units + ['%']) 
    285    except ValueError: 
    286        try: 
    287            return get_measure(argument, ['']) + default 
    288        except ValueError: 
    289            # raise ValueError with list of valid units: 
    290            return get_measure(argument, length_units + ['%']) 
    291 
    292 
    293def class_option(argument): 
    294    """ 
    295    Convert the argument into a list of ID-compatible strings and return it. 
    296    (Directive option conversion function.) 
    297 
    298    Raise ``ValueError`` if no argument is found. 
    299    """ 
    300    if argument is None: 
    301        raise ValueError('argument required but none supplied') 
    302    names = argument.split() 
    303    class_names = [] 
    304    for name in names: 
    305        class_name = nodes.make_id(name) 
    306        if not class_name: 
    307            raise ValueError('cannot make "%s" into a class name' % name) 
    308        class_names.append(class_name) 
    309    return class_names 
    310 
    311 
    312unicode_pattern = re.compile( 
    313    r'(?:0x|x|\\x|U\+?|\\u)([0-9a-f]+)$|&#x([0-9a-f]+);$', re.IGNORECASE) 
    314 
    315 
    316def unicode_code(code): 
    317    r""" 
    318    Convert a Unicode character code to a Unicode character. 
    319    (Directive option conversion function.) 
    320 
    321    Codes may be decimal numbers, hexadecimal numbers (prefixed by ``0x``, 
    322    ``x``, ``\x``, ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style 
    323    numeric character entities (e.g. ``☮``).  Other text remains as-is. 
    324 
    325    Raise ValueError for illegal Unicode code values. 
    326    """ 
    327    try: 
    328        if code.isdigit():                  # decimal number 
    329            return chr(int(code)) 
    330        else: 
    331            match = unicode_pattern.match(code) 
    332            if match:                       # hex number 
    333                value = match.group(1) or match.group(2) 
    334                return chr(int(value, 16)) 
    335            else:                           # other text 
    336                return code 
    337    except OverflowError as detail: 
    338        raise ValueError('code too large (%s)' % detail) 
    339 
    340 
    341def single_char_or_unicode(argument): 
    342    """ 
    343    A single character is returned as-is.  Unicode character codes are 
    344    converted as in `unicode_code`.  (Directive option conversion function.) 
    345    """ 
    346    char = unicode_code(argument) 
    347    if len(char) > 1: 
    348        raise ValueError('%r invalid; must be a single character or ' 
    349                         'a Unicode code' % char) 
    350    return char 
    351 
    352 
    353def single_char_or_whitespace_or_unicode(argument): 
    354    """ 
    355    As with `single_char_or_unicode`, but "tab" and "space" are also supported. 
    356    (Directive option conversion function.) 
    357    """ 
    358    if argument == 'tab': 
    359        char = '\t' 
    360    elif argument == 'space': 
    361        char = ' ' 
    362    else: 
    363        char = single_char_or_unicode(argument) 
    364    return char 
    365 
    366 
    367def positive_int(argument): 
    368    """ 
    369    Converts the argument into an integer.  Raises ValueError for negative, 
    370    zero, or non-integer values.  (Directive option conversion function.) 
    371    """ 
    372    value = int(argument) 
    373    if value < 1: 
    374        raise ValueError('negative or zero value; must be positive') 
    375    return value 
    376 
    377 
    378def positive_int_list(argument): 
    379    """ 
    380    Converts a space- or comma-separated list of values into a Python list 
    381    of integers. 
    382    (Directive option conversion function.) 
    383 
    384    Raises ValueError for non-positive-integer values. 
    385    """ 
    386    if ',' in argument: 
    387        entries = argument.split(',') 
    388    else: 
    389        entries = argument.split() 
    390    return [positive_int(entry) for entry in entries] 
    391 
    392 
    393def encoding(argument): 
    394    """ 
    395    Verifies the encoding argument by lookup. 
    396    (Directive option conversion function.) 
    397 
    398    Raises ValueError for unknown encodings. 
    399    """ 
    400    try: 
    401        codecs.lookup(argument) 
    402    except LookupError: 
    403        raise ValueError('unknown encoding: "%s"' % argument) 
    404    return argument 
    405 
    406 
    407def choice(argument, values): 
    408    """ 
    409    Directive option utility function, supplied to enable options whose 
    410    argument must be a member of a finite set of possible values (must be 
    411    lower case).  A custom conversion function must be written to use it.  For 
    412    example:: 
    413 
    414        from docutils.parsers.rst import directives 
    415 
    416        def yesno(argument): 
    417            return directives.choice(argument, ('yes', 'no')) 
    418 
    419    Raise ``ValueError`` if no argument is found or if the argument's value is 
    420    not valid (not an entry in the supplied list). 
    421    """ 
    422    try: 
    423        value = argument.lower().strip() 
    424    except AttributeError: 
    425        raise ValueError('must supply an argument; choose from %s' 
    426                         % format_values(values)) 
    427    if value in values: 
    428        return value 
    429    else: 
    430        raise ValueError('"%s" unknown; choose from %s' 
    431                         % (argument, format_values(values))) 
    432 
    433 
    434def format_values(values): 
    435    return '%s, or "%s"' % (', '.join('"%s"' % s for s in values[:-1]), 
    436                            values[-1]) 
    437 
    438 
    439def value_or(values, other): 
    440    """ 
    441    Directive option conversion function. 
    442 
    443    The argument can be any of `values` or `argument_type`. 
    444    """ 
    445    def auto_or_other(argument): 
    446        if argument in values: 
    447            return argument 
    448        else: 
    449            return other(argument) 
    450    return auto_or_other 
    451 
    452 
    453def parser_name(argument): 
    454    """ 
    455    Return a docutils parser whose name matches the argument. 
    456    (Directive option conversion function.) 
    457 
    458    Return `None`, if the argument evaluates to `False`. 
    459    Raise `ValueError` if importing the parser module fails. 
    460    """ 
    461    if not argument: 
    462        return None 
    463    try: 
    464        return parsers.get_parser_class(argument) 
    465    except ImportError as err: 
    466        raise ValueError(str(err))