# $Id: __init__.py 8024 2017-02-06 00:41:48Z goodger $
# Author: David Goodger <goodger@python.org>
# Copyright: This module has been placed in the public domain.
"""
This package contains directive implementation modules.
"""
__docformat__ = 'reStructuredText'
import re
import codecs
import sys
from docutils import nodes
from docutils.utils import split_escaped_whitespace, escape2null, unescape
from docutils.parsers.rst.languages import en as _fallback_language_module
if sys.version_info < (2,5):
from docutils._compat import __import__
_directive_registry = {
'attention': ('admonitions', 'Attention'),
'caution': ('admonitions', 'Caution'),
'code': ('body', 'CodeBlock'),
'danger': ('admonitions', 'Danger'),
'error': ('admonitions', 'Error'),
'important': ('admonitions', 'Important'),
'note': ('admonitions', 'Note'),
'tip': ('admonitions', 'Tip'),
'hint': ('admonitions', 'Hint'),
'warning': ('admonitions', 'Warning'),
'admonition': ('admonitions', 'Admonition'),
'sidebar': ('body', 'Sidebar'),
'topic': ('body', 'Topic'),
'line-block': ('body', 'LineBlock'),
'parsed-literal': ('body', 'ParsedLiteral'),
'math': ('body', 'MathBlock'),
'rubric': ('body', 'Rubric'),
'epigraph': ('body', 'Epigraph'),
'highlights': ('body', 'Highlights'),
'pull-quote': ('body', 'PullQuote'),
'compound': ('body', 'Compound'),
'container': ('body', 'Container'),
#'questions': ('body', 'question_list'),
'table': ('tables', 'RSTTable'),
'csv-table': ('tables', 'CSVTable'),
'list-table': ('tables', 'ListTable'),
'image': ('images', 'Image'),
'figure': ('images', 'Figure'),
'contents': ('parts', 'Contents'),
'sectnum': ('parts', 'Sectnum'),
'header': ('parts', 'Header'),
'footer': ('parts', 'Footer'),
#'footnotes': ('parts', 'footnotes'),
#'citations': ('parts', 'citations'),
'target-notes': ('references', 'TargetNotes'),
'meta': ('html', 'Meta'),
#'imagemap': ('html', 'imagemap'),
'raw': ('misc', 'Raw'),
'include': ('misc', 'Include'),
'replace': ('misc', 'Replace'),
'unicode': ('misc', 'Unicode'),
'class': ('misc', 'Class'),
'role': ('misc', 'Role'),
'default-role': ('misc', 'DefaultRole'),
'title': ('misc', 'Title'),
'date': ('misc', 'Date'),
'restructuredtext-test-directive': ('misc', 'TestDirective'),}
"""Mapping of directive name to (module name, class name). The
directive name is canonical & must be lowercase. Language-dependent
names are defined in the ``language`` subpackage."""
_directives = {}
"""Cache of imported directives."""
def directive(directive_name, language_module, document):
"""
Locate and return a directive function from its language-dependent name.
If not found in the current language, check English. Return None if the
named directive cannot be found.
"""
normname = directive_name.lower()
messages = []
msg_text = []
if normname in _directives:
return _directives[normname], messages
canonicalname = None
try:
canonicalname = language_module.directives[normname]
except AttributeError as error:
msg_text.append('Problem retrieving directive entry from language '
'module %r: %s.' % (language_module, error))
except KeyError:
msg_text.append('No directive entry for "%s" in module "%s".'
% (directive_name, language_module.__name__))
if not canonicalname:
try:
canonicalname = _fallback_language_module.directives[normname]
msg_text.append('Using English fallback for directive "%s".'
% directive_name)
except KeyError:
msg_text.append('Trying "%s" as canonical directive name.'
% directive_name)
# The canonical name should be an English name, but just in case:
canonicalname = normname
if msg_text:
message = document.reporter.info(
'\n'.join(msg_text), line=document.current_line)
messages.append(message)
try:
modulename, classname = _directive_registry[canonicalname]
except KeyError:
# Error handling done by caller.
return None, messages
try:
module = __import__(modulename, globals(), locals(), level=1)
except ImportError as detail:
messages.append(document.reporter.error(
'Error importing directive module "%s" (directive "%s"):\n%s'
% (modulename, directive_name, detail),
line=document.current_line))
return None, messages
try:
directive = getattr(module, classname)
_directives[normname] = directive
except AttributeError:
messages.append(document.reporter.error(
'No directive class "%s" in module "%s" (directive "%s").'
% (classname, modulename, directive_name),
line=document.current_line))
return None, messages
return directive, messages
[docs]def register_directive(name, directive):
"""
Register a nonstandard application-defined directive function.
Language lookups are not needed for such functions.
"""
_directives[name] = directive
[docs]def flag(argument):
"""
Check for a valid flag option (no argument) and return ``None``.
(Directive option conversion function.)
Raise ``ValueError`` if an argument is found.
"""
if argument and argument.strip():
raise ValueError('no argument is allowed; "%s" supplied' % argument)
else:
return None
[docs]def unchanged_required(argument):
"""
Return the argument text, unchanged.
(Directive option conversion function.)
Raise ``ValueError`` if no argument is found.
"""
if argument is None:
raise ValueError('argument required but none supplied')
else:
return argument # unchanged!
[docs]def unchanged(argument):
"""
Return the argument text, unchanged.
(Directive option conversion function.)
No argument implies empty string ("").
"""
if argument is None:
return ''
else:
return argument # unchanged!
[docs]def path(argument):
"""
Return the path argument unwrapped (with newlines removed).
(Directive option conversion function.)
Raise ``ValueError`` if no argument is found.
"""
if argument is None:
raise ValueError('argument required but none supplied')
else:
path = ''.join([s.strip() for s in argument.splitlines()])
return path
[docs]def uri(argument):
"""
Return the URI argument with unescaped whitespace removed.
(Directive option conversion function.)
Raise ``ValueError`` if no argument is found.
"""
if argument is None:
raise ValueError('argument required but none supplied')
else:
parts = split_escaped_whitespace(escape2null(argument))
uri = ' '.join(''.join(unescape(part).split()) for part in parts)
return uri
[docs]def nonnegative_int(argument):
"""
Check for a nonnegative integer argument; raise ``ValueError`` if not.
(Directive option conversion function.)
"""
value = int(argument)
if value < 0:
raise ValueError('negative value; must be positive or zero')
return value
[docs]def percentage(argument):
"""
Check for an integer percentage value with optional percent sign.
"""
try:
argument = argument.rstrip(' %')
except AttributeError:
pass
return nonnegative_int(argument)
length_units = ['em', 'ex', 'px', 'in', 'cm', 'mm', 'pt', 'pc']
[docs]def get_measure(argument, units):
"""
Check for a positive argument of one of the units and return a
normalized string of the form "<value><unit>" (without space in
between).
To be called from directive option conversion functions.
"""
match = re.match(r'^([0-9.]+) *(%s)$' % '|'.join(units), argument)
try:
float(match.group(1))
except (AttributeError, ValueError):
raise ValueError(
'not a positive measure of one of the following units:\n%s'
% ' '.join(['"%s"' % i for i in units]))
return match.group(1) + match.group(2)
[docs]def length_or_unitless(argument):
return get_measure(argument, length_units + [''])
[docs]def length_or_percentage_or_unitless(argument, default=''):
"""
Return normalized string of a length or percentage unit.
Add <default> if there is no unit. Raise ValueError if the argument is not
a positive measure of one of the valid CSS units (or without unit).
>>> length_or_percentage_or_unitless('3 pt')
'3pt'
>>> length_or_percentage_or_unitless('3%', 'em')
'3%'
>>> length_or_percentage_or_unitless('3')
'3'
>>> length_or_percentage_or_unitless('3', 'px')
'3px'
"""
try:
return get_measure(argument, length_units + ['%'])
except ValueError:
try:
return get_measure(argument, ['']) + default
except ValueError:
# raise ValueError with list of valid units:
return get_measure(argument, length_units + ['%'])
[docs]def class_option(argument):
"""
Convert the argument into a list of ID-compatible strings and return it.
(Directive option conversion function.)
Raise ``ValueError`` if no argument is found.
"""
if argument is None:
raise ValueError('argument required but none supplied')
names = argument.split()
class_names = []
for name in names:
class_name = nodes.make_id(name)
if not class_name:
raise ValueError('cannot make "%s" into a class name' % name)
class_names.append(class_name)
return class_names
unicode_pattern = re.compile(
r'(?:0x|x|\\x|U\+?|\\u)([0-9a-f]+)$|&#x([0-9a-f]+);$', re.IGNORECASE)
[docs]def unicode_code(code):
r"""
Convert a Unicode character code to a Unicode character.
(Directive option conversion function.)
Codes may be decimal numbers, hexadecimal numbers (prefixed by ``0x``,
``x``, ``\x``, ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style
numeric character entities (e.g. ``☮``). Other text remains as-is.
Raise ValueError for illegal Unicode code values.
"""
try:
if code.isdigit(): # decimal number
return chr(int(code))
else:
match = unicode_pattern.match(code)
if match: # hex number
value = match.group(1) or match.group(2)
return chr(int(value, 16))
else: # other text
return code
except OverflowError as detail:
raise ValueError('code too large (%s)' % detail)
[docs]def single_char_or_unicode(argument):
"""
A single character is returned as-is. Unicode characters codes are
converted as in `unicode_code`. (Directive option conversion function.)
"""
char = unicode_code(argument)
if len(char) > 1:
raise ValueError('%r invalid; must be a single character or '
'a Unicode code' % char)
return char
[docs]def single_char_or_whitespace_or_unicode(argument):
"""
As with `single_char_or_unicode`, but "tab" and "space" are also supported.
(Directive option conversion function.)
"""
if argument == 'tab':
char = '\t'
elif argument == 'space':
char = ' '
else:
char = single_char_or_unicode(argument)
return char
[docs]def positive_int(argument):
"""
Converts the argument into an integer. Raises ValueError for negative,
zero, or non-integer values. (Directive option conversion function.)
"""
value = int(argument)
if value < 1:
raise ValueError('negative or zero value; must be positive')
return value
[docs]def positive_int_list(argument):
"""
Converts a space- or comma-separated list of values into a Python list
of integers.
(Directive option conversion function.)
Raises ValueError for non-positive-integer values.
"""
if ',' in argument:
entries = argument.split(',')
else:
entries = argument.split()
return [positive_int(entry) for entry in entries]
[docs]def encoding(argument):
"""
Verfies the encoding argument by lookup.
(Directive option conversion function.)
Raises ValueError for unknown encodings.
"""
try:
codecs.lookup(argument)
except LookupError:
raise ValueError('unknown encoding: "%s"' % argument)
return argument
[docs]def choice(argument, values):
"""
Directive option utility function, supplied to enable options whose
argument must be a member of a finite set of possible values (must be
lower case). A custom conversion function must be written to use it. For
example::
from docutils.parsers.rst import directives
def yesno(argument):
return directives.choice(argument, ('yes', 'no'))
Raise ``ValueError`` if no argument is found or if the argument's value is
not valid (not an entry in the supplied list).
"""
try:
value = argument.lower().strip()
except AttributeError:
raise ValueError('must supply an argument; choose from %s'
% format_values(values))
if value in values:
return value
else:
raise ValueError('"%s" unknown; choose from %s'
% (argument, format_values(values)))
[docs]def value_or(values, other):
"""
The argument can be any of `values` or `argument_type`.
"""
def auto_or_other(argument):
if argument in values:
return argument
else:
return other(argument)
return auto_or_other