1# $Id: __init__.py 7621 2013-03-04 13:20:49Z milde $
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 codecs
12import re
13import sys
14
15from docutils import nodes
16from docutils.parsers.rst.languages import en as _fallback_language_module
17
18if sys.version_info < (2,5):
19    from docutils._compat import __import__
20
21
22_directive_registry = {
23      'attention': ('admonitions', 'Attention'),
24      'caution': ('admonitions', 'Caution'),
25      'code': ('body', 'CodeBlock'),
26      'danger': ('admonitions', 'Danger'),
27      'error': ('admonitions', 'Error'),
28      'important': ('admonitions', 'Important'),
29      'note': ('admonitions', 'Note'),
30      'tip': ('admonitions', 'Tip'),
31      'hint': ('admonitions', 'Hint'),
32      'warning': ('admonitions', 'Warning'),
33      'admonition': ('admonitions', 'Admonition'),
34      'sidebar': ('body', 'Sidebar'),
35      'topic': ('body', 'Topic'),
36      'line-block': ('body', 'LineBlock'),
37      'parsed-literal': ('body', 'ParsedLiteral'),
38      'math': ('body', 'MathBlock'),
39      'rubric': ('body', 'Rubric'),
40      'epigraph': ('body', 'Epigraph'),
41      'highlights': ('body', 'Highlights'),
42      'pull-quote': ('body', 'PullQuote'),
43      'compound': ('body', 'Compound'),
44      'container': ('body', 'Container'),
45      #'questions': ('body', 'question_list'),
46      'table': ('tables', 'RSTTable'),
47      'csv-table': ('tables', 'CSVTable'),
48      'list-table': ('tables', 'ListTable'),
49      'image': ('images', 'Image'),
50      'figure': ('images', 'Figure'),
51      'contents': ('parts', 'Contents'),
52      'sectnum': ('parts', 'Sectnum'),
53      'header': ('parts', 'Header'),
54      'footer': ('parts', 'Footer'),
55      #'footnotes': ('parts', 'footnotes'),
56      #'citations': ('parts', 'citations'),
57      'target-notes': ('references', 'TargetNotes'),
58      'meta': ('html', 'Meta'),
59      #'imagemap': ('html', 'imagemap'),
60      'raw': ('misc', 'Raw'),
61      'include': ('misc', 'Include'),
62      'replace': ('misc', 'Replace'),
63      'unicode': ('misc', 'Unicode'),
64      'class': ('misc', 'Class'),
65      'role': ('misc', 'Role'),
66      'default-role': ('misc', 'DefaultRole'),
67      'title': ('misc', 'Title'),
68      'date': ('misc', 'Date'),
69      'restructuredtext-test-directive': ('misc', 'TestDirective'),}
70"""Mapping of directive name to (module name, class name).  The
71directive name is canonical & must be lowercase.  Language-dependent
72names are defined in the ``language`` subpackage."""
73
74_directives = {}
75"""Cache of imported directives."""
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__(modulename, globals(), locals(), level=1)
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
135def register_directive(name, directive):
136    """
137    Register a nonstandard application-defined directive function.
138    Language lookups are not needed for such functions.
139    """
140    _directives[name] = directive
141
142def flag(argument):
143    """
144    Check for a valid flag option (no argument) and return ``None``.
145    (Directive option conversion function.)
146
147    Raise ``ValueError`` if an argument is found.
148    """
149    if argument and argument.strip():
150        raise ValueError('no argument is allowed; "%s" supplied' % argument)
151    else:
152        return None
153
154def unchanged_required(argument):
155    """
156    Return the argument text, unchanged.
157    (Directive option conversion function.)
158
159    Raise ``ValueError`` if no argument is found.
160    """
161    if argument is None:
162        raise ValueError('argument required but none supplied')
163    else:
164        return argument  # unchanged!
165
166def unchanged(argument):
167    """
168    Return the argument text, unchanged.
169    (Directive option conversion function.)
170
171    No argument implies empty string ("").
172    """
173    if argument is None:
174        return ''
175    else:
176        return argument  # unchanged!
177
178def path(argument):
179    """
180    Return the path argument unwrapped (with newlines removed).
181    (Directive option conversion function.)
182
183    Raise ``ValueError`` if no argument is found.
184    """
185    if argument is None:
186        raise ValueError('argument required but none supplied')
187    else:
188        path = ''.join([s.strip() for s in argument.splitlines()])
189        return path
190
191def uri(argument):
192    """
193    Return the URI argument with whitespace removed.
194    (Directive option conversion function.)
195
196    Raise ``ValueError`` if no argument is found.
197    """
198    if argument is None:
199        raise ValueError('argument required but none supplied')
200    else:
201        uri = ''.join(argument.split())
202        return uri
203
204def nonnegative_int(argument):
205    """
206    Check for a nonnegative integer argument; raise ``ValueError`` if not.
207    (Directive option conversion function.)
208    """
209    value = int(argument)
210    if value < 0:
211        raise ValueError('negative value; must be positive or zero')
212    return value
213
214def percentage(argument):
215    """
216    Check for an integer percentage value with optional percent sign.
217    """
218    try:
219        argument = argument.rstrip(' %')
220    except AttributeError:
221        pass
222    return nonnegative_int(argument)
223
224length_units = ['em', 'ex', 'px', 'in', 'cm', 'mm', 'pt', 'pc']
225
226def get_measure(argument, units):
227    """
228    Check for a positive argument of one of the units and return a
229    normalized string of the form "<value><unit>" (without space in
230    between).
231
232    To be called from directive option conversion functions.
233    """
234    match = re.match(r'^([0-9.]+) *(%s)$' % '|'.join(units), argument)
235    try:
236        float(match.group(1))
237    except (AttributeError, ValueError):
238        raise ValueError(
239            'not a positive measure of one of the following units:\n%s'
240            % ' '.join(['"%s"' % i for i in units]))
241    return match.group(1) + match.group(2)
242
243def length_or_unitless(argument):
244    return get_measure(argument, length_units + [''])
245
246def length_or_percentage_or_unitless(argument, default=''):
247    """
248    Return normalized string of a length or percentage unit.
249
250    Add <default> if there is no unit. Raise ValueError if the argument is not
251    a positive measure of one of the valid CSS units (or without unit).
252
253    >>> length_or_percentage_or_unitless('3 pt')
254    '3pt'
255    >>> length_or_percentage_or_unitless('3%', 'em')
256    '3%'
257    >>> length_or_percentage_or_unitless('3')
258    '3'
259    >>> length_or_percentage_or_unitless('3', 'px')
260    '3px'
261    """
262    try:
263        return get_measure(argument, length_units + ['%'])
264    except ValueError:
265        try:
266            return get_measure(argument, ['']) + default
267        except ValueError:
268            # raise ValueError with list of valid units:
269            return get_measure(argument, length_units + ['%'])
270
271def class_option(argument):
272    """
273    Convert the argument into a list of ID-compatible strings and return it.
274    (Directive option conversion function.)
275
276    Raise ``ValueError`` if no argument is found.
277    """
278    if argument is None:
279        raise ValueError('argument required but none supplied')
280    names = argument.split()
281    class_names = []
282    for name in names:
283        class_name = nodes.make_id(name)
284        if not class_name:
285            raise ValueError('cannot make "%s" into a class name' % name)
286        class_names.append(class_name)
287    return class_names
288
289unicode_pattern = re.compile(
290    r'(?:0x|x|\\x|U\+?|\\u)([0-9a-f]+)$|&#x([0-9a-f]+);$', re.IGNORECASE)
291
292def unicode_code(code):
293    r"""
294    Convert a Unicode character code to a Unicode character.
295    (Directive option conversion function.)
296
297    Codes may be decimal numbers, hexadecimal numbers (prefixed by ``0x``,
298    ``x``, ``\x``, ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style
299    numeric character entities (e.g. ``&#x262E;``).  Other text remains as-is.
300
301    Raise ValueError for illegal Unicode code values.
302    """
303    try:
304        if code.isdigit():                  # decimal number
305            return chr(int(code))
306        else:
307            match = unicode_pattern.match(code)
308            if match:                       # hex number
309                value = match.group(1) or match.group(2)
310                return chr(int(value, 16))
311            else:                           # other text
312                return code
313    except OverflowError as detail:
314        raise ValueError('code too large (%s)' % detail)
315
316def single_char_or_unicode(argument):
317    """
318    A single character is returned as-is.  Unicode characters codes are
319    converted as in `unicode_code`.  (Directive option conversion function.)
320    """
321    char = unicode_code(argument)
322    if len(char) > 1:
323        raise ValueError('%r invalid; must be a single character or '
324                         'a Unicode code' % char)
325    return char
326
327def single_char_or_whitespace_or_unicode(argument):
328    """
329    As with `single_char_or_unicode`, but "tab" and "space" are also supported.
330    (Directive option conversion function.)
331    """
332    if argument == 'tab':
333        char = '\t'
334    elif argument == 'space':
335        char = ' '
336    else:
337        char = single_char_or_unicode(argument)
338    return char
339
340def positive_int(argument):
341    """
342    Converts the argument into an integer.  Raises ValueError for negative,
343    zero, or non-integer values.  (Directive option conversion function.)
344    """
345    value = int(argument)
346    if value < 1:
347        raise ValueError('negative or zero value; must be positive')
348    return value
349
350def positive_int_list(argument):
351    """
352    Converts a space- or comma-separated list of values into a Python list
353    of integers.
354    (Directive option conversion function.)
355
356    Raises ValueError for non-positive-integer values.
357    """
358    if ',' in argument:
359        entries = argument.split(',')
360    else:
361        entries = argument.split()
362    return [positive_int(entry) for entry in entries]
363
364def encoding(argument):
365    """
366    Verfies the encoding argument by lookup.
367    (Directive option conversion function.)
368
369    Raises ValueError for unknown encodings.
370    """
371    try:
372        codecs.lookup(argument)
373    except LookupError:
374        raise ValueError('unknown encoding: "%s"' % argument)
375    return argument
376
377def choice(argument, values):
378    """
379    Directive option utility function, supplied to enable options whose
380    argument must be a member of a finite set of possible values (must be
381    lower case).  A custom conversion function must be written to use it.  For
382    example::
383
384        from docutils.parsers.rst import directives
385
386        def yesno(argument):
387            return directives.choice(argument, ('yes', 'no'))
388
389    Raise ``ValueError`` if no argument is found or if the argument's value is
390    not valid (not an entry in the supplied list).
391    """
392    try:
393        value = argument.lower().strip()
394    except AttributeError:
395        raise ValueError('must supply an argument; choose from %s'
396                         % format_values(values))
397    if value in values:
398        return value
399    else:
400        raise ValueError('"%s" unknown; choose from %s'
401                         % (argument, format_values(values)))
402
403def format_values(values):
404    return '%s, or "%s"' % (', '.join(['"%s"' % s for s in values[:-1]]),
405                            values[-1])
406