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