summaryrefslogtreecommitdiff
path: root/docutils/parsers/rst/directives
diff options
context:
space:
mode:
authorwiemann <wiemann@929543f6-e4f2-0310-98a6-ba3bd3dd1d04>2006-01-09 20:44:25 +0000
committerwiemann <wiemann@929543f6-e4f2-0310-98a6-ba3bd3dd1d04>2006-01-09 20:44:25 +0000
commitd77fdfef70e08114f57cbef5d91707df8717ea9f (patch)
tree49444e3486c0c333cb7b33dfa721296c08ee4ece /docutils/parsers/rst/directives
parent53cd16ca6ca5f638cbe5956988e88f9339e355cf (diff)
parent3993c4097756e9885bcfbd07cb1cc1e4e95e50e4 (diff)
downloaddocutils-0.4.tar.gz
Release 0.4: tagging released revisiondocutils-0.4
git-svn-id: http://svn.code.sf.net/p/docutils/code/tags/docutils-0.4@4268 929543f6-e4f2-0310-98a6-ba3bd3dd1d04
Diffstat (limited to 'docutils/parsers/rst/directives')
-rw-r--r--docutils/parsers/rst/directives/__init__.py449
-rw-r--r--docutils/parsers/rst/directives/admonitions.py90
-rw-r--r--docutils/parsers/rst/directives/body.py196
-rw-r--r--docutils/parsers/rst/directives/html.py96
-rw-r--r--docutils/parsers/rst/directives/images.py152
-rw-r--r--docutils/parsers/rst/directives/misc.py408
-rw-r--r--docutils/parsers/rst/directives/parts.py126
-rw-r--r--docutils/parsers/rst/directives/references.py27
-rw-r--r--docutils/parsers/rst/directives/tables.py444
9 files changed, 1988 insertions, 0 deletions
diff --git a/docutils/parsers/rst/directives/__init__.py b/docutils/parsers/rst/directives/__init__.py
new file mode 100644
index 000000000..998c391e3
--- /dev/null
+++ b/docutils/parsers/rst/directives/__init__.py
@@ -0,0 +1,449 @@
+# Author: David Goodger
+# Contact: goodger@python.org
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+This package contains directive implementation modules.
+
+The interface for directive functions is as follows::
+
+ def directive_fn(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ code...
+
+ # Set function attributes:
+ directive_fn.arguments = ...
+ directive_fn.options = ...
+ direcitve_fn.content = ...
+
+Parameters:
+
+- ``name`` is the directive type or name (string).
+
+- ``arguments`` is a list of positional arguments (strings).
+
+- ``options`` is a dictionary mapping option names (strings) to values (type
+ depends on option conversion functions; see below).
+
+- ``content`` is a list of strings, the directive content.
+
+- ``lineno`` is the line number of the first line of the directive.
+
+- ``content_offset`` is the line offset of the first line of the content from
+ the beginning of the current input. Used when initiating a nested parse.
+
+- ``block_text`` is a string containing the entire directive. Include it as
+ the content of a literal block in a system message if there is a problem.
+
+- ``state`` is the state which called the directive function.
+
+- ``state_machine`` is the state machine which controls the state which called
+ the directive function.
+
+Function attributes, interpreted by the directive parser (which calls the
+directive function):
+
+- ``arguments``: A 3-tuple specifying the expected positional arguments, or
+ ``None`` if the directive has no arguments. The 3 items in the tuple are
+ ``(required, optional, whitespace OK in last argument)``:
+
+ 1. The number of required arguments.
+ 2. The number of optional arguments.
+ 3. A boolean, indicating if the final argument may contain whitespace.
+
+ Arguments are normally single whitespace-separated words. The final
+ argument may contain whitespace if the third item in the argument spec tuple
+ is 1/True. If the form of the arguments is more complex, specify only one
+ argument (either required or optional) and indicate that final whitespace is
+ OK; the client code must do any context-sensitive parsing.
+
+- ``options``: A dictionary, mapping known option names to conversion
+ functions such as `int` or `float`. ``None`` or an empty dict implies no
+ options to parse. Several directive option conversion functions are defined
+ in this module.
+
+ Option conversion functions take a single parameter, the option argument (a
+ string or ``None``), validate it and/or convert it to the appropriate form.
+ Conversion functions may raise ``ValueError`` and ``TypeError`` exceptions.
+
+- ``content``: A boolean; true if content is allowed. Client code must handle
+ the case where content is required but not supplied (an empty content list
+ will be supplied).
+
+Directive functions return a list of nodes which will be inserted into the
+document tree at the point where the directive was encountered (can be an
+empty list).
+
+See `Creating reStructuredText Directives`_ for more information.
+
+.. _Creating reStructuredText Directives:
+ http://docutils.sourceforge.net/docs/howto/rst-directives.html
+"""
+
+__docformat__ = 'reStructuredText'
+
+import re
+import codecs
+from docutils import nodes
+from docutils.parsers.rst.languages import en as _fallback_language_module
+
+
+_directive_registry = {
+ 'attention': ('admonitions', 'attention'),
+ 'caution': ('admonitions', 'caution'),
+ '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', 'line_block'),
+ 'parsed-literal': ('body', 'parsed_literal'),
+ 'rubric': ('body', 'rubric'),
+ 'epigraph': ('body', 'epigraph'),
+ 'highlights': ('body', 'highlights'),
+ 'pull-quote': ('body', 'pull_quote'),
+ 'compound': ('body', 'compound'),
+ 'container': ('body', 'container'),
+ #'questions': ('body', 'question_list'),
+ 'table': ('tables', 'table'),
+ 'csv-table': ('tables', 'csv_table'),
+ 'list-table': ('tables', 'list_table'),
+ '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', 'target_notes'),
+ 'meta': ('html', 'meta'),
+ #'imagemap': ('html', 'imagemap'),
+ 'raw': ('misc', 'raw'),
+ 'include': ('misc', 'include'),
+ 'replace': ('misc', 'replace'),
+ 'unicode': ('misc', 'unicode_directive'),
+ 'class': ('misc', 'class_directive'),
+ 'role': ('misc', 'role'),
+ 'default-role': ('misc', 'default_role'),
+ 'title': ('misc', 'title'),
+ 'date': ('misc', 'date'),
+ 'restructuredtext-test-directive': ('misc', 'directive_test_function'),}
+"""Mapping of directive name to (module name, function name). The directive
+name is canonical & must be lowercase. Language-dependent names are defined
+in the ``language`` subpackage."""
+
+_modules = {}
+"""Cache of imported directive modules."""
+
+_directives = {}
+"""Cache of imported directive functions."""
+
+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 _directives.has_key(normname):
+ return _directives[normname], messages
+ canonicalname = None
+ try:
+ canonicalname = language_module.directives[normname]
+ except AttributeError, 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, functionname = _directive_registry[canonicalname]
+ except KeyError:
+ # Error handling done by caller.
+ return None, messages
+ if _modules.has_key(modulename):
+ module = _modules[modulename]
+ else:
+ try:
+ module = __import__(modulename, globals(), locals())
+ except ImportError, 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:
+ function = getattr(module, functionname)
+ _directives[normname] = function
+ except AttributeError:
+ messages.append(document.reporter.error(
+ 'No function "%s" in module "%s" (directive "%s").'
+ % (functionname, modulename, directive_name),
+ line=document.current_line))
+ return None, messages
+ return function, messages
+
+def register_directive(name, directive_function):
+ """
+ Register a nonstandard application-defined directive function.
+ Language lookups are not needed for such functions.
+ """
+ _directives[name] = directive_function
+
+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
+
+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!
+
+def unchanged(argument):
+ """
+ Return the argument text, unchanged.
+ (Directive option conversion function.)
+
+ No argument implies empty string ("").
+ """
+ if argument is None:
+ return u''
+ else:
+ return argument # unchanged!
+
+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
+
+def uri(argument):
+ """
+ Return the URI argument with 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:
+ uri = ''.join(argument.split())
+ return uri
+
+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
+
+length_units = ['em', 'ex', 'px', 'in', 'cm', 'mm', 'pt', 'pc']
+
+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:
+ assert match is not None
+ float(match.group(1))
+ except (AssertionError, 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)
+
+def length_or_unitless(argument):
+ return get_measure(argument, length_units + [''])
+
+def length_or_percentage_or_unitless(argument):
+ return get_measure(argument, length_units + ['%', ''])
+
+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)
+
+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. ``&#x262E;``). Other text remains as-is.
+
+ Raise ValueError for illegal Unicode code values.
+ """
+ try:
+ if code.isdigit(): # decimal number
+ return unichr(int(code))
+ else:
+ match = unicode_pattern.match(code)
+ if match: # hex number
+ value = match.group(1) or match.group(2)
+ return unichr(int(value, 16))
+ else: # other text
+ return code
+ except OverflowError, detail:
+ raise ValueError('code too large (%s)' % detail)
+
+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
+
+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
+
+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
+
+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]
+
+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
+
+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)))
+
+def format_values(values):
+ return '%s, or "%s"' % (', '.join(['"%s"' % s for s in values[:-1]]),
+ values[-1])
diff --git a/docutils/parsers/rst/directives/admonitions.py b/docutils/parsers/rst/directives/admonitions.py
new file mode 100644
index 000000000..73ca18161
--- /dev/null
+++ b/docutils/parsers/rst/directives/admonitions.py
@@ -0,0 +1,90 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+Admonition directives.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from docutils.parsers.rst import states, directives
+from docutils import nodes
+
+
+def make_admonition(node_class, name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ if not content:
+ error = state_machine.reporter.error(
+ 'The "%s" admonition is empty; content required.' % (name),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ text = '\n'.join(content)
+ admonition_node = node_class(text)
+ if arguments:
+ title_text = arguments[0]
+ textnodes, messages = state.inline_text(title_text, lineno)
+ admonition_node += nodes.title(title_text, '', *textnodes)
+ admonition_node += messages
+ if options.has_key('class'):
+ classes = options['class']
+ else:
+ classes = ['admonition-' + nodes.make_id(title_text)]
+ admonition_node['classes'] += classes
+ state.nested_parse(content, content_offset, admonition_node)
+ return [admonition_node]
+
+def admonition(*args):
+ return make_admonition(nodes.admonition, *args)
+
+admonition.arguments = (1, 0, 1)
+admonition.options = {'class': directives.class_option}
+admonition.content = 1
+
+def attention(*args):
+ return make_admonition(nodes.attention, *args)
+
+attention.content = 1
+
+def caution(*args):
+ return make_admonition(nodes.caution, *args)
+
+caution.content = 1
+
+def danger(*args):
+ return make_admonition(nodes.danger, *args)
+
+danger.content = 1
+
+def error(*args):
+ return make_admonition(nodes.error, *args)
+
+error.content = 1
+
+def hint(*args):
+ return make_admonition(nodes.hint, *args)
+
+hint.content = 1
+
+def important(*args):
+ return make_admonition(nodes.important, *args)
+
+important.content = 1
+
+def note(*args):
+ return make_admonition(nodes.note, *args)
+
+note.content = 1
+
+def tip(*args):
+ return make_admonition(nodes.tip, *args)
+
+tip.content = 1
+
+def warning(*args):
+ return make_admonition(nodes.warning, *args)
+
+warning.content = 1
diff --git a/docutils/parsers/rst/directives/body.py b/docutils/parsers/rst/directives/body.py
new file mode 100644
index 000000000..2ff89e617
--- /dev/null
+++ b/docutils/parsers/rst/directives/body.py
@@ -0,0 +1,196 @@
+# Author: David Goodger
+# Contact: goodger@python.org
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+Directives for additional body elements.
+
+See `docutils.parsers.rst.directives` for API details.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import sys
+from docutils import nodes
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.roles import set_classes
+
+
+def topic(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine,
+ node_class=nodes.topic):
+ if not (state_machine.match_titles
+ or isinstance(state_machine.node, nodes.sidebar)):
+ error = state_machine.reporter.error(
+ 'The "%s" directive may not be used within topics '
+ 'or body elements.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ if not content:
+ warning = state_machine.reporter.warning(
+ 'Content block expected for the "%s" directive; none found.'
+ % name, nodes.literal_block(block_text, block_text),
+ line=lineno)
+ return [warning]
+ title_text = arguments[0]
+ textnodes, messages = state.inline_text(title_text, lineno)
+ titles = [nodes.title(title_text, '', *textnodes)]
+ # sidebar uses this code
+ if options.has_key('subtitle'):
+ textnodes, more_messages = state.inline_text(options['subtitle'],
+ lineno)
+ titles.append(nodes.subtitle(options['subtitle'], '', *textnodes))
+ messages.extend(more_messages)
+ text = '\n'.join(content)
+ node = node_class(text, *(titles + messages))
+ node['classes'] += options.get('class', [])
+ if text:
+ state.nested_parse(content, content_offset, node)
+ return [node]
+
+topic.arguments = (1, 0, 1)
+topic.options = {'class': directives.class_option}
+topic.content = 1
+
+def sidebar(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ if isinstance(state_machine.node, nodes.sidebar):
+ error = state_machine.reporter.error(
+ 'The "%s" directive may not be used within a sidebar element.'
+ % name, nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ return topic(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine,
+ node_class=nodes.sidebar)
+
+sidebar.arguments = (1, 0, 1)
+sidebar.options = {'subtitle': directives.unchanged_required,
+ 'class': directives.class_option}
+sidebar.content = 1
+
+def line_block(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ if not content:
+ warning = state_machine.reporter.warning(
+ 'Content block expected for the "%s" directive; none found.'
+ % name, nodes.literal_block(block_text, block_text), line=lineno)
+ return [warning]
+ block = nodes.line_block(classes=options.get('class', []))
+ node_list = [block]
+ for line_text in content:
+ text_nodes, messages = state.inline_text(line_text.strip(),
+ lineno + content_offset)
+ line = nodes.line(line_text, '', *text_nodes)
+ if line_text.strip():
+ line.indent = len(line_text) - len(line_text.lstrip())
+ block += line
+ node_list.extend(messages)
+ content_offset += 1
+ state.nest_line_block_lines(block)
+ return node_list
+
+line_block.options = {'class': directives.class_option}
+line_block.content = 1
+
+def parsed_literal(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ set_classes(options)
+ return block(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine,
+ node_class=nodes.literal_block)
+
+parsed_literal.options = {'class': directives.class_option}
+parsed_literal.content = 1
+
+def block(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine, node_class):
+ if not content:
+ warning = state_machine.reporter.warning(
+ 'Content block expected for the "%s" directive; none found.'
+ % name, nodes.literal_block(block_text, block_text), line=lineno)
+ return [warning]
+ text = '\n'.join(content)
+ text_nodes, messages = state.inline_text(text, lineno)
+ node = node_class(text, '', *text_nodes, **options)
+ node.line = content_offset + 1
+ return [node] + messages
+
+def rubric(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ rubric_text = arguments[0]
+ textnodes, messages = state.inline_text(rubric_text, lineno)
+ rubric = nodes.rubric(rubric_text, '', *textnodes, **options)
+ return [rubric] + messages
+
+rubric.arguments = (1, 0, 1)
+rubric.options = {'class': directives.class_option}
+
+def epigraph(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ block_quote, messages = state.block_quote(content, content_offset)
+ block_quote['classes'].append('epigraph')
+ return [block_quote] + messages
+
+epigraph.content = 1
+
+def highlights(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ block_quote, messages = state.block_quote(content, content_offset)
+ block_quote['classes'].append('highlights')
+ return [block_quote] + messages
+
+highlights.content = 1
+
+def pull_quote(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ block_quote, messages = state.block_quote(content, content_offset)
+ block_quote['classes'].append('pull-quote')
+ return [block_quote] + messages
+
+pull_quote.content = 1
+
+def compound(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ text = '\n'.join(content)
+ if not text:
+ error = state_machine.reporter.error(
+ 'The "%s" directive is empty; content required.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ node = nodes.compound(text)
+ node['classes'] += options.get('class', [])
+ state.nested_parse(content, content_offset, node)
+ return [node]
+
+compound.options = {'class': directives.class_option}
+compound.content = 1
+
+def container(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ text = '\n'.join(content)
+ if not text:
+ error = state_machine.reporter.error(
+ 'The "%s" directive is empty; content required.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ try:
+ if arguments:
+ classes = directives.class_option(arguments[0])
+ else:
+ classes = []
+ except ValueError:
+ error = state_machine.reporter.error(
+ 'Invalid class attribute value for "%s" directive: "%s".'
+ % (name, arguments[0]),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ node = nodes.container(text)
+ node['classes'].extend(classes)
+ state.nested_parse(content, content_offset, node)
+ return [node]
+
+container.arguments = (0, 1, 1)
+container.content = 1
diff --git a/docutils/parsers/rst/directives/html.py b/docutils/parsers/rst/directives/html.py
new file mode 100644
index 000000000..86e19dcfc
--- /dev/null
+++ b/docutils/parsers/rst/directives/html.py
@@ -0,0 +1,96 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+Directives for typically HTML-specific constructs.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import sys
+from docutils import nodes, utils
+from docutils.parsers.rst import states
+from docutils.transforms import components
+
+
+def meta(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ node = nodes.Element()
+ if content:
+ new_line_offset, blank_finish = state.nested_list_parse(
+ content, content_offset, node, initial_state='MetaBody',
+ blank_finish=1, state_machine_kwargs=metaSMkwargs)
+ if (new_line_offset - content_offset) != len(content):
+ # incomplete parse of block?
+ error = state_machine.reporter.error(
+ 'Invalid meta directive.',
+ nodes.literal_block(block_text, block_text), line=lineno)
+ node += error
+ else:
+ error = state_machine.reporter.error(
+ 'Empty meta directive.',
+ nodes.literal_block(block_text, block_text), line=lineno)
+ node += error
+ return node.children
+
+meta.content = 1
+
+def imagemap(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ return []
+
+
+class MetaBody(states.SpecializedBody):
+
+ class meta(nodes.Special, nodes.PreBibliographic, nodes.Element):
+ """HTML-specific "meta" element."""
+ pass
+
+ def field_marker(self, match, context, next_state):
+ """Meta element."""
+ node, blank_finish = self.parsemeta(match)
+ self.parent += node
+ return [], next_state, []
+
+ def parsemeta(self, match):
+ name = self.parse_field_marker(match)
+ indented, indent, line_offset, blank_finish = \
+ self.state_machine.get_first_known_indented(match.end())
+ node = self.meta()
+ pending = nodes.pending(components.Filter,
+ {'component': 'writer',
+ 'format': 'html',
+ 'nodes': [node]})
+ node['content'] = ' '.join(indented)
+ if not indented:
+ line = self.state_machine.line
+ msg = self.reporter.info(
+ 'No content for meta tag "%s".' % name,
+ nodes.literal_block(line, line),
+ line=self.state_machine.abs_line_number())
+ return msg, blank_finish
+ tokens = name.split()
+ try:
+ attname, val = utils.extract_name_value(tokens[0])[0]
+ node[attname.lower()] = val
+ except utils.NameValueError:
+ node['name'] = tokens[0]
+ for token in tokens[1:]:
+ try:
+ attname, val = utils.extract_name_value(token)[0]
+ node[attname.lower()] = val
+ except utils.NameValueError, detail:
+ line = self.state_machine.line
+ msg = self.reporter.error(
+ 'Error parsing meta tag attribute "%s": %s.'
+ % (token, detail), nodes.literal_block(line, line),
+ line=self.state_machine.abs_line_number())
+ return msg, blank_finish
+ self.document.note_pending(pending)
+ return pending, blank_finish
+
+
+metaSMkwargs = {'state_classes': (MetaBody,)}
diff --git a/docutils/parsers/rst/directives/images.py b/docutils/parsers/rst/directives/images.py
new file mode 100644
index 000000000..5aed4c01b
--- /dev/null
+++ b/docutils/parsers/rst/directives/images.py
@@ -0,0 +1,152 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+Directives for figures and simple images.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import sys
+from docutils import nodes, utils
+from docutils.parsers.rst import directives, states
+from docutils.nodes import fully_normalize_name, whitespace_normalize_name
+from docutils.parsers.rst.roles import set_classes
+
+try:
+ import Image # PIL
+except ImportError:
+ Image = None
+
+align_h_values = ('left', 'center', 'right')
+align_v_values = ('top', 'middle', 'bottom')
+align_values = align_v_values + align_h_values
+
+def align(argument):
+ return directives.choice(argument, align_values)
+
+def image(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ if options.has_key('align'):
+ # check for align_v values only
+ if isinstance(state, states.SubstitutionDef):
+ if options['align'] not in align_v_values:
+ error = state_machine.reporter.error(
+ 'Error in "%s" directive: "%s" is not a valid value for '
+ 'the "align" option within a substitution definition. '
+ 'Valid values for "align" are: "%s".'
+ % (name, options['align'], '", "'.join(align_v_values)),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ elif options['align'] not in align_h_values:
+ error = state_machine.reporter.error(
+ 'Error in "%s" directive: "%s" is not a valid value for '
+ 'the "align" option. Valid values for "align" are: "%s".'
+ % (name, options['align'], '", "'.join(align_h_values)),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ messages = []
+ reference = directives.uri(arguments[0])
+ options['uri'] = reference
+ reference_node = None
+ if options.has_key('target'):
+ block = states.escape2null(options['target']).splitlines()
+ block = [line for line in block]
+ target_type, data = state.parse_target(block, block_text, lineno)
+ if target_type == 'refuri':
+ reference_node = nodes.reference(refuri=data)
+ elif target_type == 'refname':
+ reference_node = nodes.reference(
+ refname=fully_normalize_name(data),
+ name=whitespace_normalize_name(data))
+ reference_node.indirect_reference_name = data
+ state.document.note_refname(reference_node)
+ else: # malformed target
+ messages.append(data) # data is a system message
+ del options['target']
+ set_classes(options)
+ image_node = nodes.image(block_text, **options)
+ if reference_node:
+ reference_node += image_node
+ return messages + [reference_node]
+ else:
+ return messages + [image_node]
+
+image.arguments = (1, 0, 1)
+image.options = {'alt': directives.unchanged,
+ 'height': directives.length_or_unitless,
+ 'width': directives.length_or_percentage_or_unitless,
+ 'scale': directives.nonnegative_int,
+ 'align': align,
+ 'target': directives.unchanged_required,
+ 'class': directives.class_option}
+
+def figure_align(argument):
+ return directives.choice(argument, align_h_values)
+
+def figure(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ figwidth = options.get('figwidth')
+ if figwidth:
+ del options['figwidth']
+ figclasses = options.get('figclass')
+ if figclasses:
+ del options['figclass']
+ align = options.get('align')
+ if align:
+ del options['align']
+ (image_node,) = image(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine)
+ if isinstance(image_node, nodes.system_message):
+ return [image_node]
+ figure_node = nodes.figure('', image_node)
+ if figwidth == 'image':
+ if Image and state.document.settings.file_insertion_enabled:
+ # PIL doesn't like Unicode paths:
+ try:
+ i = Image.open(str(image_node['uri']))
+ except (IOError, UnicodeError):
+ pass
+ else:
+ state.document.settings.record_dependencies.add(image_node['uri'])
+ figure_node['width'] = i.size[0]
+ elif figwidth is not None:
+ figure_node['width'] = figwidth
+ if figclasses:
+ figure_node['classes'] += figclasses
+ if align:
+ figure_node['align'] = align
+ if content:
+ node = nodes.Element() # anonymous container for parsing
+ state.nested_parse(content, content_offset, node)
+ first_node = node[0]
+ if isinstance(first_node, nodes.paragraph):
+ caption = nodes.caption(first_node.rawsource, '',
+ *first_node.children)
+ figure_node += caption
+ elif not (isinstance(first_node, nodes.comment)
+ and len(first_node) == 0):
+ error = state_machine.reporter.error(
+ 'Figure caption must be a paragraph or empty comment.',
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [figure_node, error]
+ if len(node) > 1:
+ figure_node += nodes.legend('', *node[1:])
+ return [figure_node]
+
+def figwidth_value(argument):
+ if argument.lower() == 'image':
+ return 'image'
+ else:
+ return directives.nonnegative_int(argument)
+
+figure.arguments = (1, 0, 1)
+figure.options = {'figwidth': figwidth_value,
+ 'figclass': directives.class_option}
+figure.options.update(image.options)
+figure.options['align'] = figure_align
+figure.content = 1
diff --git a/docutils/parsers/rst/directives/misc.py b/docutils/parsers/rst/directives/misc.py
new file mode 100644
index 000000000..42f642fee
--- /dev/null
+++ b/docutils/parsers/rst/directives/misc.py
@@ -0,0 +1,408 @@
+# Authors: David Goodger, Dethe Elza
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""Miscellaneous directives."""
+
+__docformat__ = 'reStructuredText'
+
+import sys
+import os.path
+import re
+import time
+from docutils import io, nodes, statemachine, utils
+from docutils.parsers.rst import directives, roles, states
+from docutils.transforms import misc
+
+try:
+ import urllib2
+except ImportError:
+ urllib2 = None
+
+
+standard_include_path = os.path.join(os.path.dirname(states.__file__),
+ 'include')
+
+def include(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """Include a reST file as part of the content of this reST file."""
+ if not state.document.settings.file_insertion_enabled:
+ warning = state_machine.reporter.warning(
+ '"%s" directive disabled.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [warning]
+ source = state_machine.input_lines.source(
+ lineno - state_machine.input_offset - 1)
+ source_dir = os.path.dirname(os.path.abspath(source))
+ path = directives.path(arguments[0])
+ if path.startswith('<') and path.endswith('>'):
+ path = os.path.join(standard_include_path, path[1:-1])
+ path = os.path.normpath(os.path.join(source_dir, path))
+ path = utils.relative_path(None, path)
+ encoding = options.get('encoding', state.document.settings.input_encoding)
+ try:
+ state.document.settings.record_dependencies.add(path)
+ include_file = io.FileInput(
+ source_path=path, encoding=encoding,
+ error_handler=state.document.settings.input_encoding_error_handler,
+ handle_io_errors=None)
+ except IOError, error:
+ severe = state_machine.reporter.severe(
+ 'Problems with "%s" directive path:\n%s: %s.'
+ % (name, error.__class__.__name__, error),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [severe]
+ try:
+ include_text = include_file.read()
+ except UnicodeError, error:
+ severe = state_machine.reporter.severe(
+ 'Problem with "%s" directive:\n%s: %s'
+ % (name, error.__class__.__name__, error),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [severe]
+ if options.has_key('literal'):
+ literal_block = nodes.literal_block(include_text, include_text,
+ source=path)
+ literal_block.line = 1
+ return literal_block
+ else:
+ include_lines = statemachine.string2lines(include_text,
+ convert_whitespace=1)
+ state_machine.insert_input(include_lines, path)
+ return []
+
+include.arguments = (1, 0, 1)
+include.options = {'literal': directives.flag,
+ 'encoding': directives.encoding}
+
+def raw(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """
+ Pass through content unchanged
+
+ Content is included in output based on type argument
+
+ Content may be included inline (content section of directive) or
+ imported from a file or url.
+ """
+ if ( not state.document.settings.raw_enabled
+ or (not state.document.settings.file_insertion_enabled
+ and (options.has_key('file') or options.has_key('url'))) ):
+ warning = state_machine.reporter.warning(
+ '"%s" directive disabled.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [warning]
+ attributes = {'format': ' '.join(arguments[0].lower().split())}
+ encoding = options.get('encoding', state.document.settings.input_encoding)
+ if content:
+ if options.has_key('file') or options.has_key('url'):
+ error = state_machine.reporter.error(
+ '"%s" directive may not both specify an external file and '
+ 'have content.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ text = '\n'.join(content)
+ elif options.has_key('file'):
+ if options.has_key('url'):
+ error = state_machine.reporter.error(
+ 'The "file" and "url" options may not be simultaneously '
+ 'specified for the "%s" directive.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ source_dir = os.path.dirname(
+ os.path.abspath(state.document.current_source))
+ path = os.path.normpath(os.path.join(source_dir, options['file']))
+ path = utils.relative_path(None, path)
+ try:
+ state.document.settings.record_dependencies.add(path)
+ raw_file = io.FileInput(
+ source_path=path, encoding=encoding,
+ error_handler=state.document.settings.input_encoding_error_handler,
+ handle_io_errors=None)
+ except IOError, error:
+ severe = state_machine.reporter.severe(
+ 'Problems with "%s" directive path:\n%s.' % (name, error),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [severe]
+ try:
+ text = raw_file.read()
+ except UnicodeError, error:
+ severe = state_machine.reporter.severe(
+ 'Problem with "%s" directive:\n%s: %s'
+ % (name, error.__class__.__name__, error),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [severe]
+ attributes['source'] = path
+ elif options.has_key('url'):
+ if not urllib2:
+ severe = state_machine.reporter.severe(
+ 'Problems with the "%s" directive and its "url" option: '
+ 'unable to access the required functionality (from the '
+ '"urllib2" module).' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [severe]
+ source = options['url']
+ try:
+ raw_text = urllib2.urlopen(source).read()
+ except (urllib2.URLError, IOError, OSError), error:
+ severe = state_machine.reporter.severe(
+ 'Problems with "%s" directive URL "%s":\n%s.'
+ % (name, options['url'], error),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [severe]
+ raw_file = io.StringInput(
+ source=raw_text, source_path=source, encoding=encoding,
+ error_handler=state.document.settings.input_encoding_error_handler)
+ try:
+ text = raw_file.read()
+ except UnicodeError, error:
+ severe = state_machine.reporter.severe(
+ 'Problem with "%s" directive:\n%s: %s'
+ % (name, error.__class__.__name__, error),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [severe]
+ attributes['source'] = source
+ else:
+ error = state_machine.reporter.warning(
+ 'The "%s" directive requires content; none supplied.' % (name),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ raw_node = nodes.raw('', text, **attributes)
+ return [raw_node]
+
+raw.arguments = (1, 0, 1)
+raw.options = {'file': directives.path,
+ 'url': directives.uri,
+ 'encoding': directives.encoding}
+raw.content = 1
+
+def replace(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ if not isinstance(state, states.SubstitutionDef):
+ error = state_machine.reporter.error(
+ 'Invalid context: the "%s" directive can only be used within a '
+ 'substitution definition.' % (name),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ text = '\n'.join(content)
+ element = nodes.Element(text)
+ if text:
+ state.nested_parse(content, content_offset, element)
+ if len(element) != 1 or not isinstance(element[0], nodes.paragraph):
+ messages = []
+ for node in element:
+ if isinstance(node, nodes.system_message):
+ node['backrefs'] = []
+ messages.append(node)
+ error = state_machine.reporter.error(
+ 'Error in "%s" directive: may contain a single paragraph '
+ 'only.' % (name), line=lineno)
+ messages.append(error)
+ return messages
+ else:
+ return element[0].children
+ else:
+ error = state_machine.reporter.error(
+ 'The "%s" directive is empty; content required.' % (name),
+ line=lineno)
+ return [error]
+
+replace.content = 1
+
+def unicode_directive(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ r"""
+ Convert Unicode character codes (numbers) to characters. 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. ``&#x262E;``). Text following ".." is a comment and is
+ ignored. Spaces are ignored, and any other text remains as-is.
+ """
+ if not isinstance(state, states.SubstitutionDef):
+ error = state_machine.reporter.error(
+ 'Invalid context: the "%s" directive can only be used within a '
+ 'substitution definition.' % (name),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ substitution_definition = state_machine.node
+ if options.has_key('trim'):
+ substitution_definition.attributes['ltrim'] = 1
+ substitution_definition.attributes['rtrim'] = 1
+ if options.has_key('ltrim'):
+ substitution_definition.attributes['ltrim'] = 1
+ if options.has_key('rtrim'):
+ substitution_definition.attributes['rtrim'] = 1
+ codes = unicode_comment_pattern.split(arguments[0])[0].split()
+ element = nodes.Element()
+ for code in codes:
+ try:
+ decoded = directives.unicode_code(code)
+ except ValueError, err:
+ error = state_machine.reporter.error(
+ 'Invalid character code: %s\n%s: %s'
+ % (code, err.__class__.__name__, err),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ element += nodes.Text(decoded)
+ return element.children
+
+unicode_directive.arguments = (1, 0, 1)
+unicode_directive.options = {'trim': directives.flag,
+ 'ltrim': directives.flag,
+ 'rtrim': directives.flag}
+unicode_comment_pattern = re.compile(r'( |\n|^)\.\. ')
+
+def class_directive(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """
+ Set a "class" attribute on the directive content or the next element.
+ When applied to the next element, a "pending" element is inserted, and a
+ transform does the work later.
+ """
+ try:
+ class_value = directives.class_option(arguments[0])
+ except ValueError:
+ error = state_machine.reporter.error(
+ 'Invalid class attribute value for "%s" directive: "%s".'
+ % (name, arguments[0]),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ node_list = []
+ if content:
+ container = nodes.Element()
+ state.nested_parse(content, content_offset, container)
+ for node in container:
+ node['classes'].extend(class_value)
+ node_list.extend(container.children)
+ else:
+ pending = nodes.pending(misc.ClassAttribute,
+ {'class': class_value, 'directive': name},
+ block_text)
+ state_machine.document.note_pending(pending)
+ node_list.append(pending)
+ return node_list
+
+class_directive.arguments = (1, 0, 1)
+class_directive.content = 1
+
+role_arg_pat = re.compile(r'(%s)\s*(\(\s*(%s)\s*\)\s*)?$'
+ % ((states.Inliner.simplename,) * 2))
+def role(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """Dynamically create and register a custom interpreted text role."""
+ if content_offset > lineno or not content:
+ error = state_machine.reporter.error(
+ '"%s" directive requires arguments on the first line.'
+ % name, nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ args = content[0]
+ match = role_arg_pat.match(args)
+ if not match:
+ error = state_machine.reporter.error(
+ '"%s" directive arguments not valid role names: "%s".'
+ % (name, args), nodes.literal_block(block_text, block_text),
+ line=lineno)
+ return [error]
+ new_role_name = match.group(1)
+ base_role_name = match.group(3)
+ messages = []
+ if base_role_name:
+ base_role, messages = roles.role(
+ base_role_name, state_machine.language, lineno, state.reporter)
+ if base_role is None:
+ error = state.reporter.error(
+ 'Unknown interpreted text role "%s".' % base_role_name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return messages + [error]
+ else:
+ base_role = roles.generic_custom_role
+ assert not hasattr(base_role, 'arguments'), (
+ 'Supplemental directive arguments for "%s" directive not supported'
+ '(specified by "%r" role).' % (name, base_role))
+ try:
+ (arguments, options, content, content_offset) = (
+ state.parse_directive_block(content[1:], content_offset, base_role,
+ option_presets={}))
+ except states.MarkupError, detail:
+ error = state_machine.reporter.error(
+ 'Error in "%s" directive:\n%s.' % (name, detail),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return messages + [error]
+ if not options.has_key('class'):
+ try:
+ options['class'] = directives.class_option(new_role_name)
+ except ValueError, detail:
+ error = state_machine.reporter.error(
+ 'Invalid argument for "%s" directive:\n%s.'
+ % (name, detail),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return messages + [error]
+ role = roles.CustomRole(new_role_name, base_role, options, content)
+ roles.register_local_role(new_role_name, role)
+ return messages
+
+role.content = 1
+
+def default_role(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """Set the default interpreted text role."""
+ if not arguments:
+ if roles._roles.has_key(''):
+ # restore the "default" default role
+ del roles._roles['']
+ return []
+ role_name = arguments[0]
+ role, messages = roles.role(
+ role_name, state_machine.language, lineno, state.reporter)
+ if role is None:
+ error = state.reporter.error(
+ 'Unknown interpreted text role "%s".' % role_name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return messages + [error]
+ roles._roles[''] = role
+ # @@@ should this be local to the document, not the parser?
+ return messages
+
+default_role.arguments = (0, 1, 0)
+
+def title(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ state_machine.document['title'] = arguments[0]
+ return []
+
+title.arguments = (1, 0, 1)
+
+def date(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ if not isinstance(state, states.SubstitutionDef):
+ error = state_machine.reporter.error(
+ 'Invalid context: the "%s" directive can only be used within a '
+ 'substitution definition.' % (name),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ format = '\n'.join(content) or '%Y-%m-%d'
+ text = time.strftime(format)
+ return [nodes.Text(text)]
+
+date.content = 1
+
+def directive_test_function(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """This directive is useful only for testing purposes."""
+ if content:
+ text = '\n'.join(content)
+ info = state_machine.reporter.info(
+ 'Directive processed. Type="%s", arguments=%r, options=%r, '
+ 'content:' % (name, arguments, options),
+ nodes.literal_block(text, text), line=lineno)
+ else:
+ info = state_machine.reporter.info(
+ 'Directive processed. Type="%s", arguments=%r, options=%r, '
+ 'content: None' % (name, arguments, options), line=lineno)
+ return [info]
+
+directive_test_function.arguments = (0, 1, 1)
+directive_test_function.options = {'option': directives.unchanged_required}
+directive_test_function.content = 1
diff --git a/docutils/parsers/rst/directives/parts.py b/docutils/parsers/rst/directives/parts.py
new file mode 100644
index 000000000..2a1a092a4
--- /dev/null
+++ b/docutils/parsers/rst/directives/parts.py
@@ -0,0 +1,126 @@
+# Author: David Goodger, Dmitry Jemerov
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+Directives for document parts.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from docutils import nodes, languages
+from docutils.transforms import parts
+from docutils.parsers.rst import directives
+
+
+backlinks_values = ('top', 'entry', 'none')
+
+def backlinks(arg):
+ value = directives.choice(arg, backlinks_values)
+ if value == 'none':
+ return None
+ else:
+ return value
+
+def contents(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """
+ Table of contents.
+
+ The table of contents is generated in two passes: initial parse and
+ transform. During the initial parse, a 'pending' element is generated
+ which acts as a placeholder, storing the TOC title and any options
+ internally. At a later stage in the processing, the 'pending' element is
+ replaced by a 'topic' element, a title and the table of contents proper.
+ """
+ if not (state_machine.match_titles
+ or isinstance(state_machine.node, nodes.sidebar)):
+ error = state_machine.reporter.error(
+ 'The "%s" directive may not be used within topics '
+ 'or body elements.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ document = state_machine.document
+ language = languages.get_language(document.settings.language_code)
+ if arguments:
+ title_text = arguments[0]
+ text_nodes, messages = state.inline_text(title_text, lineno)
+ title = nodes.title(title_text, '', *text_nodes)
+ else:
+ messages = []
+ if options.has_key('local'):
+ title = None
+ else:
+ title = nodes.title('', language.labels['contents'])
+ topic = nodes.topic(classes=['contents'])
+ topic['classes'] += options.get('class', [])
+ if options.has_key('local'):
+ topic['classes'].append('local')
+ if title:
+ name = title.astext()
+ topic += title
+ else:
+ name = language.labels['contents']
+ name = nodes.fully_normalize_name(name)
+ if not document.has_name(name):
+ topic['names'].append(name)
+ document.note_implicit_target(topic)
+ pending = nodes.pending(parts.Contents, rawsource=block_text)
+ pending.details.update(options)
+ document.note_pending(pending)
+ topic += pending
+ return [topic] + messages
+
+contents.arguments = (0, 1, 1)
+contents.options = {'depth': directives.nonnegative_int,
+ 'local': directives.flag,
+ 'backlinks': backlinks,
+ 'class': directives.class_option}
+
+def sectnum(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """Automatic section numbering."""
+ pending = nodes.pending(parts.SectNum)
+ pending.details.update(options)
+ state_machine.document.note_pending(pending)
+ return [pending]
+
+sectnum.options = {'depth': int,
+ 'start': int,
+ 'prefix': directives.unchanged_required,
+ 'suffix': directives.unchanged_required}
+
+def header_footer(node, name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """Contents of document header or footer."""
+ if not content:
+ warning = state_machine.reporter.warning(
+ 'Content block expected for the "%s" directive; none found.'
+ % name, nodes.literal_block(block_text, block_text),
+ line=lineno)
+ node.append(nodes.paragraph(
+ '', 'Problem with the "%s" directive: no content supplied.' % name))
+ return [warning]
+ text = '\n'.join(content)
+ state.nested_parse(content, content_offset, node)
+ return []
+
+def header(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ decoration = state_machine.document.get_decoration()
+ node = decoration.get_header()
+ return header_footer(node, name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine)
+
+header.content = 1
+
+def footer(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ decoration = state_machine.document.get_decoration()
+ node = decoration.get_footer()
+ return header_footer(node, name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine)
+
+footer.content = 1
diff --git a/docutils/parsers/rst/directives/references.py b/docutils/parsers/rst/directives/references.py
new file mode 100644
index 000000000..0406182b6
--- /dev/null
+++ b/docutils/parsers/rst/directives/references.py
@@ -0,0 +1,27 @@
+# Author: David Goodger, Dmitry Jemerov
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+Directives for references and targets.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from docutils import nodes
+from docutils.transforms import references
+from docutils.parsers.rst import directives
+
+
+def target_notes(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """Target footnote generation."""
+ pending = nodes.pending(references.TargetNotes)
+ pending.details.update(options)
+ state_machine.document.note_pending(pending)
+ nodelist = [pending]
+ return nodelist
+
+target_notes.options = {'class': directives.class_option}
diff --git a/docutils/parsers/rst/directives/tables.py b/docutils/parsers/rst/directives/tables.py
new file mode 100644
index 000000000..70a0de5ab
--- /dev/null
+++ b/docutils/parsers/rst/directives/tables.py
@@ -0,0 +1,444 @@
+# Authors: David Goodger, David Priest
+# Contact: goodger@python.org
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+Directives for table elements.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import sys
+import os.path
+from docutils import io, nodes, statemachine, utils
+from docutils.utils import SystemMessagePropagation
+from docutils.parsers.rst import directives
+
+try:
+ import csv # new in Python 2.3
+except ImportError:
+ csv = None
+
+try:
+ import urllib2
+except ImportError:
+ urllib2 = None
+
+try:
+ True
+except NameError: # Python 2.2 & 2.1 compatibility
+ True = not 0
+ False = not 1
+
+
+def table(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ if not content:
+ warning = state_machine.reporter.warning(
+ 'Content block expected for the "%s" directive; none found.'
+ % name, nodes.literal_block(block_text, block_text),
+ line=lineno)
+ return [warning]
+ title, messages = make_title(arguments, state, lineno)
+ node = nodes.Element() # anonymous container for parsing
+ state.nested_parse(content, content_offset, node)
+ if len(node) != 1 or not isinstance(node[0], nodes.table):
+ error = state_machine.reporter.error(
+ 'Error parsing content block for the "%s" directive: '
+ 'exactly one table expected.'
+ % name, nodes.literal_block(block_text, block_text),
+ line=lineno)
+ return [error]
+ table_node = node[0]
+ table_node['classes'] += options.get('class', [])
+ if title:
+ table_node.insert(0, title)
+ return [table_node] + messages
+
+table.arguments = (0, 1, 1)
+table.options = {'class': directives.class_option}
+table.content = 1
+
+def make_title(arguments, state, lineno):
+ if arguments:
+ title_text = arguments[0]
+ text_nodes, messages = state.inline_text(title_text, lineno)
+ title = nodes.title(title_text, '', *text_nodes)
+ else:
+ title = None
+ messages = []
+ return title, messages
+
+
+if csv:
+ class DocutilsDialect(csv.Dialect):
+
+ """CSV dialect for `csv_table` directive function."""
+
+ delimiter = ','
+ quotechar = '"'
+ doublequote = True
+ skipinitialspace = True
+ lineterminator = '\n'
+ quoting = csv.QUOTE_MINIMAL
+
+ def __init__(self, options):
+ if options.has_key('delim'):
+ self.delimiter = str(options['delim'])
+ if options.has_key('keepspace'):
+ self.skipinitialspace = False
+ if options.has_key('quote'):
+ self.quotechar = str(options['quote'])
+ if options.has_key('escape'):
+ self.doublequote = False
+ self.escapechar = str(options['escape'])
+ csv.Dialect.__init__(self)
+
+
+ class HeaderDialect(csv.Dialect):
+
+ """CSV dialect to use for the "header" option data."""
+
+ delimiter = ','
+ quotechar = '"'
+ escapechar = '\\'
+ doublequote = False
+ skipinitialspace = True
+ lineterminator = '\n'
+ quoting = csv.QUOTE_MINIMAL
+
+
+def csv_table(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ try:
+ if ( not state.document.settings.file_insertion_enabled
+ and (options.has_key('file') or options.has_key('url')) ):
+ warning = state_machine.reporter.warning(
+ 'File and URL access deactivated; ignoring "%s" directive.' %
+ name, nodes.literal_block(block_text,block_text), line=lineno)
+ return [warning]
+ check_requirements(name, lineno, block_text, state_machine)
+ title, messages = make_title(arguments, state, lineno)
+ csv_data, source = get_csv_data(
+ name, options, content, lineno, block_text, state, state_machine)
+ table_head, max_header_cols = process_header_option(
+ options, state_machine, lineno)
+ rows, max_cols = parse_csv_data_into_rows(
+ csv_data, DocutilsDialect(options), source, options)
+ max_cols = max(max_cols, max_header_cols)
+ header_rows = options.get('header-rows', 0) # default 0
+ stub_columns = options.get('stub-columns', 0) # default 0
+ check_table_dimensions(
+ rows, header_rows, stub_columns, name, lineno,
+ block_text, state_machine)
+ table_head.extend(rows[:header_rows])
+ table_body = rows[header_rows:]
+ col_widths = get_column_widths(
+ max_cols, name, options, lineno, block_text, state_machine)
+ extend_short_rows_with_empty_cells(max_cols, (table_head, table_body))
+ except SystemMessagePropagation, detail:
+ return [detail.args[0]]
+ except csv.Error, detail:
+ error = state_machine.reporter.error(
+ 'Error with CSV data in "%s" directive:\n%s' % (name, detail),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ table = (col_widths, table_head, table_body)
+ table_node = state.build_table(table, content_offset, stub_columns)
+ table_node['classes'] += options.get('class', [])
+ if title:
+ table_node.insert(0, title)
+ return [table_node] + messages
+
+csv_table.arguments = (0, 1, 1)
+csv_table.options = {'header-rows': directives.nonnegative_int,
+ 'stub-columns': directives.nonnegative_int,
+ 'header': directives.unchanged,
+ 'widths': directives.positive_int_list,
+ 'file': directives.path,
+ 'url': directives.uri,
+ 'encoding': directives.encoding,
+ 'class': directives.class_option,
+ # field delimiter char
+ 'delim': directives.single_char_or_whitespace_or_unicode,
+ # treat whitespace after delimiter as significant
+ 'keepspace': directives.flag,
+ # text field quote/unquote char:
+ 'quote': directives.single_char_or_unicode,
+ # char used to escape delim & quote as-needed:
+ 'escape': directives.single_char_or_unicode,}
+csv_table.content = 1
+
+def check_requirements(name, lineno, block_text, state_machine):
+ if not csv:
+ error = state_machine.reporter.error(
+ 'The "%s" directive is not compatible with this version of '
+ 'Python (%s). Requires the "csv" module, new in Python 2.3.'
+ % (name, sys.version.split()[0]),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+
+def get_csv_data(name, options, content, lineno, block_text,
+ state, state_machine):
+ """
+ CSV data can come from the directive content, from an external file, or
+ from a URL reference.
+ """
+ encoding = options.get('encoding', state.document.settings.input_encoding)
+ if content: # CSV data is from directive content
+ if options.has_key('file') or options.has_key('url'):
+ error = state_machine.reporter.error(
+ '"%s" directive may not both specify an external file and '
+ 'have content.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ source = content.source(0)
+ csv_data = content
+ elif options.has_key('file'): # CSV data is from an external file
+ if options.has_key('url'):
+ error = state_machine.reporter.error(
+ 'The "file" and "url" options may not be simultaneously '
+ 'specified for the "%s" directive.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ source_dir = os.path.dirname(
+ os.path.abspath(state.document.current_source))
+ source = os.path.normpath(os.path.join(source_dir, options['file']))
+ source = utils.relative_path(None, source)
+ try:
+ state.document.settings.record_dependencies.add(source)
+ csv_file = io.FileInput(
+ source_path=source, encoding=encoding,
+ error_handler
+ =state.document.settings.input_encoding_error_handler,
+ handle_io_errors=None)
+ csv_data = csv_file.read().splitlines()
+ except IOError, error:
+ severe = state_machine.reporter.severe(
+ 'Problems with "%s" directive path:\n%s.' % (name, error),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(severe)
+ elif options.has_key('url'): # CSV data is from a URL
+ if not urllib2:
+ severe = state_machine.reporter.severe(
+ 'Problems with the "%s" directive and its "url" option: '
+ 'unable to access the required functionality (from the '
+ '"urllib2" module).' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(severe)
+ source = options['url']
+ try:
+ csv_text = urllib2.urlopen(source).read()
+ except (urllib2.URLError, IOError, OSError, ValueError), error:
+ severe = state_machine.reporter.severe(
+ 'Problems with "%s" directive URL "%s":\n%s.'
+ % (name, options['url'], error),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(severe)
+ csv_file = io.StringInput(
+ source=csv_text, source_path=source, encoding=encoding,
+ error_handler=state.document.settings.input_encoding_error_handler)
+ csv_data = csv_file.read().splitlines()
+ else:
+ error = state_machine.reporter.warning(
+ 'The "%s" directive requires content; none supplied.' % (name),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ return csv_data, source
+
+def process_header_option(options, state_machine, lineno):
+ source = state_machine.get_source(lineno - 1)
+ table_head = []
+ max_header_cols = 0
+ if options.has_key('header'): # separate table header in option
+ rows, max_header_cols = parse_csv_data_into_rows(
+ options['header'].split('\n'), HeaderDialect(), source, options)
+ table_head.extend(rows)
+ return table_head, max_header_cols
+
+def parse_csv_data_into_rows(csv_data, dialect, source, options):
+ # csv.py doesn't do Unicode; encode temporarily as UTF-8
+ csv_reader = csv.reader([line.encode('utf-8') for line in csv_data],
+ dialect=dialect)
+ rows = []
+ max_cols = 0
+ for row in csv_reader:
+ row_data = []
+ for cell in row:
+ # decode UTF-8 back to Unicode
+ cell_text = unicode(cell, 'utf-8')
+ cell_data = (0, 0, 0, statemachine.StringList(
+ cell_text.splitlines(), source=source))
+ row_data.append(cell_data)
+ rows.append(row_data)
+ max_cols = max(max_cols, len(row))
+ return rows, max_cols
+
+def check_table_dimensions(rows, header_rows, stub_columns, name, lineno,
+ block_text, state_machine):
+ if len(rows) < header_rows:
+ error = state_machine.reporter.error(
+ '%s header row(s) specified but only %s row(s) of data supplied '
+ '("%s" directive).' % (header_rows, len(rows), name),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ if len(rows) == header_rows > 0:
+ error = state_machine.reporter.error(
+ 'Insufficient data supplied (%s row(s)); no data remaining for '
+ 'table body, required by "%s" directive.' % (len(rows), name),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ for row in rows:
+ if len(row) < stub_columns:
+ error = state_machine.reporter.error(
+ '%s stub column(s) specified but only %s columns(s) of data '
+ 'supplied ("%s" directive).' % (stub_columns, len(row), name),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ if len(row) == stub_columns > 0:
+ error = state_machine.reporter.error(
+ 'Insufficient data supplied (%s columns(s)); no data remaining '
+ 'for table body, required by "%s" directive.'
+ % (len(row), name),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+
+def get_column_widths(max_cols, name, options, lineno, block_text,
+ state_machine):
+ if options.has_key('widths'):
+ col_widths = options['widths']
+ if len(col_widths) != max_cols:
+ error = state_machine.reporter.error(
+ '"%s" widths do not match the number of columns in table (%s).'
+ % (name, max_cols),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ elif max_cols:
+ col_widths = [100 / max_cols] * max_cols
+ else:
+ error = state_machine.reporter.error(
+ 'No table data detected in CSV file.',
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ return col_widths
+
+def extend_short_rows_with_empty_cells(columns, parts):
+ for part in parts:
+ for row in part:
+ if len(row) < columns:
+ row.extend([(0, 0, 0, [])] * (columns - len(row)))
+
+def list_table(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """
+ Implement tables whose data is encoded as a uniform two-level bullet list.
+ For further ideas, see
+ http://docutils.sf.net/docs/dev/rst/alternatives.html#list-driven-tables
+ """
+ if not content:
+ error = state_machine.reporter.error(
+ 'The "%s" directive is empty; content required.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ title, messages = make_title(arguments, state, lineno)
+ node = nodes.Element() # anonymous container for parsing
+ state.nested_parse(content, content_offset, node)
+ try:
+ num_cols, col_widths = check_list_content(
+ node, name, options, content, lineno, block_text, state_machine)
+ table_data = [[item.children for item in row_list[0]]
+ for row_list in node[0]]
+ header_rows = options.get('header-rows', 0) # default 0
+ stub_columns = options.get('stub-columns', 0) # default 0
+ check_table_dimensions(
+ table_data, header_rows, stub_columns, name, lineno,
+ block_text, state_machine)
+ except SystemMessagePropagation, detail:
+ return [detail.args[0]]
+ table_node = build_table_from_list(table_data, col_widths,
+ header_rows, stub_columns)
+ table_node['classes'] += options.get('class', [])
+ if title:
+ table_node.insert(0, title)
+ return [table_node] + messages
+
+list_table.arguments = (0, 1, 1)
+list_table.options = {'header-rows': directives.nonnegative_int,
+ 'stub-columns': directives.nonnegative_int,
+ 'widths': directives.positive_int_list,
+ 'class': directives.class_option}
+list_table.content = 1
+
+def check_list_content(node, name, options, content, lineno, block_text,
+ state_machine):
+ if len(node) != 1 or not isinstance(node[0], nodes.bullet_list):
+ error = state_machine.reporter.error(
+ 'Error parsing content block for the "%s" directive: '
+ 'exactly one bullet list expected.' % name,
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ list_node = node[0]
+ # Check for a uniform two-level bullet list:
+ for item_index in range(len(list_node)):
+ item = list_node[item_index]
+ if len(item) != 1 or not isinstance(item[0], nodes.bullet_list):
+ error = state_machine.reporter.error(
+ 'Error parsing content block for the "%s" directive: '
+ 'two-level bullet list expected, but row %s does not contain '
+ 'a second-level bullet list.' % (name, item_index + 1),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ elif item_index:
+ # ATTN pychecker users: num_cols is guaranteed to be set in the
+ # "else" clause below for item_index==0, before this branch is
+ # triggered.
+ if len(item[0]) != num_cols:
+ error = state_machine.reporter.error(
+ 'Error parsing content block for the "%s" directive: '
+ 'uniform two-level bullet list expected, but row %s does '
+ 'not contain the same number of items as row 1 (%s vs %s).'
+ % (name, item_index + 1, len(item[0]), num_cols),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ else:
+ num_cols = len(item[0])
+ col_widths = get_column_widths(
+ num_cols, name, options, lineno, block_text, state_machine)
+ if len(col_widths) != num_cols:
+ error = state_machine.reporter.error(
+ 'Error parsing "widths" option of the "%s" directive: '
+ 'number of columns does not match the table data (%s vs %s).'
+ % (name, len(col_widths), num_cols),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ raise SystemMessagePropagation(error)
+ return num_cols, col_widths
+
+def build_table_from_list(table_data, col_widths, header_rows, stub_columns):
+ table = nodes.table()
+ tgroup = nodes.tgroup(cols=len(col_widths))
+ table += tgroup
+ for col_width in col_widths:
+ colspec = nodes.colspec(colwidth=col_width)
+ if stub_columns:
+ colspec.attributes['stub'] = 1
+ stub_columns -= 1
+ tgroup += colspec
+ rows = []
+ for row in table_data:
+ row_node = nodes.row()
+ for cell in row:
+ entry = nodes.entry()
+ entry += cell
+ row_node += entry
+ rows.append(row_node)
+ if header_rows:
+ thead = nodes.thead()
+ thead.extend(rows[:header_rows])
+ tgroup += thead
+ tbody = nodes.tbody()
+ tbody.extend(rows[header_rows:])
+ tgroup += tbody
+ return table