summaryrefslogtreecommitdiff
path: root/docutils/parsers
diff options
context:
space:
mode:
authorwiemann <wiemann@929543f6-e4f2-0310-98a6-ba3bd3dd1d04>2005-08-17 13:59:12 +0000
committerwiemann <wiemann@929543f6-e4f2-0310-98a6-ba3bd3dd1d04>2005-08-17 13:59:12 +0000
commita4bc0d09f3d1f633a5780976d1f913e083c9564a (patch)
tree1777475e88463b08438f0719be97cc087554eadd /docutils/parsers
parent60c69f8cf97125135948a3d35b7698ee80921137 (diff)
parent2818db9a234cc37008c5f2bb0e821f9b1fa92de2 (diff)
downloaddocutils-0.3.9.tar.gz
re-added docutils-0.3.9 tag one level deeper (without web/ and sandboxes/)docutils-0.3.9
git-svn-id: http://svn.code.sf.net/p/docutils/code/tags/docutils-0.3.9@3815 929543f6-e4f2-0310-98a6-ba3bd3dd1d04
Diffstat (limited to 'docutils/parsers')
-rw-r--r--docutils/parsers/__init__.py49
-rw-r--r--docutils/parsers/rst/__init__.py155
-rw-r--r--docutils/parsers/rst/directives/__init__.py421
-rw-r--r--docutils/parsers/rst/directives/admonitions.py90
-rw-r--r--docutils/parsers/rst/directives/body.py169
-rw-r--r--docutils/parsers/rst/directives/html.py96
-rw-r--r--docutils/parsers/rst/directives/images.py147
-rw-r--r--docutils/parsers/rst/directives/misc.py349
-rw-r--r--docutils/parsers/rst/directives/parts.py124
-rw-r--r--docutils/parsers/rst/directives/references.py23
-rw-r--r--docutils/parsers/rst/directives/tables.py441
-rw-r--r--docutils/parsers/rst/languages/__init__.py27
-rw-r--r--docutils/parsers/rst/languages/af.py100
-rw-r--r--docutils/parsers/rst/languages/ca.py119
-rw-r--r--docutils/parsers/rst/languages/cs.py102
-rw-r--r--docutils/parsers/rst/languages/de.py93
-rw-r--r--docutils/parsers/rst/languages/en.py102
-rw-r--r--docutils/parsers/rst/languages/eo.py111
-rw-r--r--docutils/parsers/rst/languages/es.py117
-rw-r--r--docutils/parsers/rst/languages/fi.py91
-rw-r--r--docutils/parsers/rst/languages/fr.py97
-rw-r--r--docutils/parsers/rst/languages/it.py90
-rw-r--r--docutils/parsers/rst/languages/nl.py106
-rw-r--r--docutils/parsers/rst/languages/pt_br.py102
-rw-r--r--docutils/parsers/rst/languages/ru.py101
-rw-r--r--docutils/parsers/rst/languages/sk.py89
-rw-r--r--docutils/parsers/rst/languages/sv.py88
-rw-r--r--docutils/parsers/rst/languages/zh_tw.py102
-rw-r--r--docutils/parsers/rst/roles.py347
-rw-r--r--docutils/parsers/rst/states.py2922
-rw-r--r--docutils/parsers/rst/tableparser.py522
31 files changed, 7492 insertions, 0 deletions
diff --git a/docutils/parsers/__init__.py b/docutils/parsers/__init__.py
new file mode 100644
index 000000000..027c25a86
--- /dev/null
+++ b/docutils/parsers/__init__.py
@@ -0,0 +1,49 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+This package contains Docutils parser modules.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from docutils import Component
+
+
+class Parser(Component):
+
+ component_type = 'parser'
+ config_section = 'parsers'
+
+ def parse(self, inputstring, document):
+ """Override to parse `inputstring` into document tree `document`."""
+ raise NotImplementedError('subclass must override this method')
+
+ def setup_parse(self, inputstring, document):
+ """Initial parse setup. Call at start of `self.parse()`."""
+ self.inputstring = inputstring
+ self.document = document
+ document.reporter.attach_observer(document.note_parse_message)
+
+ def finish_parse(self):
+ """Finalize parse details. Call at end of `self.parse()`."""
+ self.document.reporter.detach_observer(
+ self.document.note_parse_message)
+
+
+_parser_aliases = {
+ 'restructuredtext': 'rst',
+ 'rest': 'rst',
+ 'restx': 'rst',
+ 'rtxt': 'rst',}
+
+def get_parser_class(parser_name):
+ """Return the Parser class from the `parser_name` module."""
+ parser_name = parser_name.lower()
+ if _parser_aliases.has_key(parser_name):
+ parser_name = _parser_aliases[parser_name]
+ module = __import__(parser_name, globals(), locals())
+ return module.Parser
diff --git a/docutils/parsers/rst/__init__.py b/docutils/parsers/rst/__init__.py
new file mode 100644
index 000000000..30825d7c4
--- /dev/null
+++ b/docutils/parsers/rst/__init__.py
@@ -0,0 +1,155 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+This is ``docutils.parsers.rst`` package. It exports a single class, `Parser`,
+the reStructuredText parser.
+
+
+Usage
+=====
+
+1. Create a parser::
+
+ parser = docutils.parsers.rst.Parser()
+
+ Several optional arguments may be passed to modify the parser's behavior.
+ Please see `Customizing the Parser`_ below for details.
+
+2. Gather input (a multi-line string), by reading a file or the standard
+ input::
+
+ input = sys.stdin.read()
+
+3. Create a new empty `docutils.nodes.document` tree::
+
+ document = docutils.utils.new_document(source, settings)
+
+ See `docutils.utils.new_document()` for parameter details.
+
+4. Run the parser, populating the document tree::
+
+ parser.parse(input, document)
+
+
+Parser Overview
+===============
+
+The reStructuredText parser is implemented as a state machine, examining its
+input one line at a time. To understand how the parser works, please first
+become familiar with the `docutils.statemachine` module, then see the
+`states` module.
+
+
+Customizing the Parser
+----------------------
+
+Anything that isn't already customizable is that way simply because that type
+of customizability hasn't been implemented yet. Patches welcome!
+
+When instantiating an object of the `Parser` class, two parameters may be
+passed: ``rfc2822`` and ``inliner``. Pass ``rfc2822=1`` to enable an initial
+RFC-2822 style header block, parsed as a "field_list" element (with "class"
+attribute set to "rfc2822"). Currently this is the only body-level element
+which is customizable without subclassing. (Tip: subclass `Parser` and change
+its "state_classes" and "initial_state" attributes to refer to new classes.
+Contact the author if you need more details.)
+
+The ``inliner`` parameter takes an instance of `states.Inliner` or a subclass.
+It handles inline markup recognition. A common extension is the addition of
+further implicit hyperlinks, like "RFC 2822". This can be done by subclassing
+`states.Inliner`, adding a new method for the implicit markup, and adding a
+``(pattern, method)`` pair to the "implicit_dispatch" attribute of the
+subclass. See `states.Inliner.implicit_inline()` for details. Explicit
+inline markup can be customized in a `states.Inliner` subclass via the
+``patterns.initial`` and ``dispatch`` attributes (and new methods as
+appropriate).
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import docutils.parsers
+import docutils.statemachine
+from docutils.parsers.rst import states
+from docutils import frontend
+
+
+class Parser(docutils.parsers.Parser):
+
+ """The reStructuredText parser."""
+
+ supported = ('restructuredtext', 'rst', 'rest', 'restx', 'rtxt', 'rstx')
+ """Aliases this parser supports."""
+
+ settings_spec = (
+ 'reStructuredText Parser Options',
+ None,
+ (('Recognize and link to standalone PEP references (like "PEP 258").',
+ ['--pep-references'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ ('Base URL for PEP references '
+ '(default "http://www.python.org/peps/").',
+ ['--pep-base-url'],
+ {'metavar': '<URL>', 'default': 'http://www.python.org/peps/',
+ 'validator': frontend.validate_url_trailing_slash}),
+ ('Recognize and link to standalone RFC references (like "RFC 822").',
+ ['--rfc-references'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ ('Base URL for RFC references (default "http://www.faqs.org/rfcs/").',
+ ['--rfc-base-url'],
+ {'metavar': '<URL>', 'default': 'http://www.faqs.org/rfcs/',
+ 'validator': frontend.validate_url_trailing_slash}),
+ ('Set number of spaces for tab expansion (default 8).',
+ ['--tab-width'],
+ {'metavar': '<width>', 'type': 'int', 'default': 8}),
+ ('Remove spaces before footnote references.',
+ ['--trim-footnote-reference-space'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ ('Leave spaces before footnote references.',
+ ['--leave-footnote-reference-space'],
+ {'action': 'store_false', 'dest': 'trim_footnote_reference_space',
+ 'validator': frontend.validate_boolean}),
+ ('Disable directives that insert the contents of external file '
+ '("include" & "raw"); replaced with a "warning" system message.',
+ ['--no-file-insertion'],
+ {'action': 'store_false', 'default': 1,
+ 'dest': 'file_insertion_enabled'}),
+ ('Enable directives that insert the contents of external file '
+ '("include" & "raw"). Enabled by default.',
+ ['--file-insertion-enabled'],
+ {'action': 'store_true', 'dest': 'file_insertion_enabled'}),
+ ('Disable the "raw" directives; replaced with a "warning" '
+ 'system message.',
+ ['--no-raw'],
+ {'action': 'store_false', 'default': 1, 'dest': 'raw_enabled'}),
+ ('Enable the "raw" directive. Enabled by default.',
+ ['--raw-enabled'],
+ {'action': 'store_true', 'dest': 'raw_enabled'}),))
+
+ config_section = 'restructuredtext parser'
+ config_section_dependencies = ('parsers',)
+
+ def __init__(self, rfc2822=None, inliner=None):
+ if rfc2822:
+ self.initial_state = 'RFC2822Body'
+ else:
+ self.initial_state = 'Body'
+ self.state_classes = states.state_classes
+ self.inliner = inliner
+
+ def parse(self, inputstring, document):
+ """Parse `inputstring` and populate `document`, a document tree."""
+ self.setup_parse(inputstring, document)
+ self.statemachine = states.RSTStateMachine(
+ state_classes=self.state_classes,
+ initial_state=self.initial_state,
+ debug=document.reporter.debug_flag)
+ inputlines = docutils.statemachine.string2lines(
+ inputstring, tab_width=document.settings.tab_width,
+ convert_whitespace=1)
+ self.statemachine.run(inputlines, document, inliner=self.inliner)
+ self.finish_parse()
diff --git a/docutils/parsers/rst/directives/__init__.py b/docutils/parsers/rst/directives/__init__.py
new file mode 100644
index 000000000..8e9f42284
--- /dev/null
+++ b/docutils/parsers/rst/directives/__init__.py
@@ -0,0 +1,421 @@
+# 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'),
+ #'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'),
+ '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:
+ messages.append(document.reporter.error(
+ 'Directive "%s" not registered (canonical name "%s").'
+ % (directive_name, canonicalname), line=document.current_line))
+ 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
+
+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..28682328a
--- /dev/null
+++ b/docutils/parsers/rst/directives/body.py
@@ -0,0 +1,169 @@
+# 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
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..670f969a0
--- /dev/null
+++ b/docutils/parsers/rst/directives/images.py
@@ -0,0 +1,147 @@
+# 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
+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=data,
+ name=fully_normalize_name(options['target']))
+ 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.nonnegative_int,
+ 'width': directives.nonnegative_int,
+ '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.setdefault('figwidth')
+ figclasses = options.setdefault('figclass')
+ align = options.setdefault('align')
+ del options['figwidth']
+ del options['figclass']
+ 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..7bae832d2
--- /dev/null
+++ b/docutils/parsers/rst/directives/misc.py
@@ -0,0 +1,349 @@
+# 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
+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
+
+
+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])
+ 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 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]
+ pending = nodes.pending(misc.ClassAttribute,
+ {'class': class_value, 'directive': name},
+ block_text)
+ state_machine.document.note_pending(pending)
+ return [pending]
+
+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 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..37a32a963
--- /dev/null
+++ b/docutils/parsers/rst/directives/parts.py
@@ -0,0 +1,124 @@
+# 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 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..92966140f
--- /dev/null
+++ b/docutils/parsers/rst/directives/references.py
@@ -0,0 +1,23 @@
+# 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
+
+
+def target_notes(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """Target footnote generation."""
+ pending = nodes.pending(references.TargetNotes)
+ state_machine.document.note_pending(pending)
+ nodelist = [pending]
+ return nodelist
diff --git a/docutils/parsers/rst/directives/tables.py b/docutils/parsers/rst/directives/tables.py
new file mode 100644
index 000000000..87131277d
--- /dev/null
+++ b/docutils/parsers/rst/directives/tables.py
@@ -0,0 +1,441 @@
+# 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(
+ '"%s" directive disabled.' % 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:
+ 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
diff --git a/docutils/parsers/rst/languages/__init__.py b/docutils/parsers/rst/languages/__init__.py
new file mode 100644
index 000000000..be6feb644
--- /dev/null
+++ b/docutils/parsers/rst/languages/__init__.py
@@ -0,0 +1,27 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# Internationalization details are documented in
+# <http://docutils.sf.net/docs/howto/i18n.html>.
+
+"""
+This package contains modules for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+_languages = {}
+
+def get_language(language_code):
+ if _languages.has_key(language_code):
+ return _languages[language_code]
+ try:
+ module = __import__(language_code, globals(), locals())
+ except ImportError:
+ return None
+ _languages[language_code] = module
+ return module
diff --git a/docutils/parsers/rst/languages/af.py b/docutils/parsers/rst/languages/af.py
new file mode 100644
index 000000000..9dddae716
--- /dev/null
+++ b/docutils/parsers/rst/languages/af.py
@@ -0,0 +1,100 @@
+# Author: Jannie Hofmeyr
+# Contact: jhsh@sun.ac.za
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Afrikaans-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ 'aandag': 'attention',
+ 'versigtig': 'caution',
+ 'gevaar': 'danger',
+ 'fout': 'error',
+ 'wenk': 'hint',
+ 'belangrik': 'important',
+ 'nota': 'note',
+ 'tip': 'tip', # hint and tip both have the same translation: wenk
+ 'waarskuwing': 'warning',
+ 'vermaning': 'admonition',
+ 'kantstreep': 'sidebar',
+ 'onderwerp': 'topic',
+ 'lynblok': 'line-block',
+ 'parsed-literal (translation required)': 'parsed-literal',
+ 'rubriek': 'rubric',
+ 'epigraaf': 'epigraph',
+ 'hoogtepunte': 'highlights',
+ 'pull-quote (translation required)': 'pull-quote',
+ u'compound (translation required)': 'compound',
+ #'vrae': 'questions',
+ #'qa': 'questions',
+ #'faq': 'questions',
+ 'table (translation required)': 'table',
+ 'csv-table (translation required)': 'csv-table',
+ 'list-table (translation required)': 'list-table',
+ 'meta': 'meta',
+ #'beeldkaart': 'imagemap',
+ 'beeld': 'image',
+ 'figuur': 'figure',
+ 'insluiting': 'include',
+ 'rou': 'raw',
+ 'vervang': 'replace',
+ 'unicode': 'unicode', # should this be translated? unikode
+ 'klas': 'class',
+ 'role (translation required)': 'role',
+ 'inhoud': 'contents',
+ 'sectnum': 'sectnum',
+ 'section-numbering': 'sectnum',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',
+ #'voetnote': 'footnotes',
+ #'aanhalings': 'citations',
+ 'teikennotas': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Afrikaans name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ 'afkorting': 'abbreviation',
+ 'ab': 'abbreviation',
+ 'akroniem': 'acronym',
+ 'ac': 'acronym',
+ 'indeks': 'index',
+ 'i': 'index',
+ 'voetskrif': 'subscript',
+ 'sub': 'subscript',
+ 'boskrif': 'superscript',
+ 'sup': 'superscript',
+ 'titelverwysing': 'title-reference',
+ 'titel': 'title-reference',
+ 't': 'title-reference',
+ 'pep-verwysing': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'rfc-verwysing': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'nadruk': 'emphasis',
+ 'sterk': 'strong',
+ 'literal (translation required)': 'literal',
+ 'benoemde verwysing': 'named-reference',
+ 'anonieme verwysing': 'anonymous-reference',
+ 'voetnootverwysing': 'footnote-reference',
+ 'aanhalingverwysing': 'citation-reference',
+ 'vervangingsverwysing': 'substitution-reference',
+ 'teiken': 'target',
+ 'uri-verwysing': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'rou': 'raw',}
+"""Mapping of Afrikaans role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/ca.py b/docutils/parsers/rst/languages/ca.py
new file mode 100644
index 000000000..9fdce400d
--- /dev/null
+++ b/docutils/parsers/rst/languages/ca.py
@@ -0,0 +1,119 @@
+# Author: Ivan Vilata i Balaguer
+# Contact: ivan@selidor.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Catalan-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ u'atenci\u00F3': 'attention',
+ u'compte': 'caution',
+ u'perill': 'danger',
+ u'error': 'error',
+ u'suggeriment': 'hint',
+ u'important': 'important',
+ u'nota': 'note',
+ u'consell': 'tip',
+ u'av\u00EDs': 'warning',
+ u'advertiment': 'admonition',
+ u'nota-al-marge': 'sidebar',
+ u'nota-marge': 'sidebar',
+ u'tema': 'topic',
+ u'bloc-de-l\u00EDnies': 'line-block',
+ u'bloc-l\u00EDnies': 'line-block',
+ u'literal-analitzat': 'parsed-literal',
+ u'r\u00FAbrica': 'rubric',
+ u'ep\u00EDgraf': 'epigraph',
+ u'sumari': 'highlights',
+ u'cita-destacada': 'pull-quote',
+ u'compost': 'compound',
+ #'questions': 'questions',
+ u'taula': 'table',
+ u'taula-csv': 'csv-table',
+ u'taula-llista': 'list-table',
+ #'qa': 'questions',
+ #'faq': 'questions',
+ u'meta': 'meta',
+ #'imagemap': 'imagemap',
+ u'imatge': 'image',
+ u'figura': 'figure',
+ u'inclou': 'include',
+ u'incloure': 'include',
+ u'cru': 'raw',
+ u'reempla\u00E7a': 'replace',
+ u'reempla\u00E7ar': 'replace',
+ u'unicode': 'unicode',
+ u'classe': 'class',
+ u'rol': 'role',
+ u'contingut': 'contents',
+ u'numsec': 'sectnum',
+ u'numeraci\u00F3-de-seccions': 'sectnum',
+ u'numeraci\u00F3-seccions': 'sectnum',
+ u'cap\u00E7alera': 'header',
+ u'peu-de-p\u00E0gina': 'footer',
+ u'peu-p\u00E0gina': 'footer',
+ #'footnotes': 'footnotes',
+ #'citations': 'citations',
+ u'notes-amb-destinacions': 'target-notes',
+ u'notes-destinacions': 'target-notes',
+ u'directiva-de-prova-de-restructuredtext': 'restructuredtext-test-directive'}
+"""Catalan name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ u'abreviatura': 'abbreviation',
+ u'abreviaci\u00F3': 'abbreviation',
+ u'abrev': 'abbreviation',
+ u'ab': 'abbreviation',
+ u'acr\u00F2nim': 'acronym',
+ u'ac': 'acronym',
+ u'\u00EDndex': 'index',
+ u'i': 'index',
+ u'sub\u00EDndex': 'subscript',
+ u'sub': 'subscript',
+ u'super\u00EDndex': 'superscript',
+ u'sup': 'superscript',
+ u'refer\u00E8ncia-a-t\u00EDtol': 'title-reference',
+ u'refer\u00E8ncia-t\u00EDtol': 'title-reference',
+ u't\u00EDtol': 'title-reference',
+ u't': 'title-reference',
+ u'refer\u00E8ncia-a-pep': 'pep-reference',
+ u'refer\u00E8ncia-pep': 'pep-reference',
+ u'pep': 'pep-reference',
+ u'refer\u00E8ncia-a-rfc': 'rfc-reference',
+ u'refer\u00E8ncia-rfc': 'rfc-reference',
+ u'rfc': 'rfc-reference',
+ u'\u00E8mfasi': 'emphasis',
+ u'destacat': 'strong',
+ u'literal': 'literal',
+ u'refer\u00E8ncia-amb-nom': 'named-reference',
+ u'refer\u00E8ncia-nom': 'named-reference',
+ u'refer\u00E8ncia-an\u00F2nima': 'anonymous-reference',
+ u'refer\u00E8ncia-a-nota-al-peu': 'footnote-reference',
+ u'refer\u00E8ncia-nota-al-peu': 'footnote-reference',
+ u'refer\u00E8ncia-a-cita': 'citation-reference',
+ u'refer\u00E8ncia-cita': 'citation-reference',
+ u'refer\u00E8ncia-a-substituci\u00F3': 'substitution-reference',
+ u'refer\u00E8ncia-substituci\u00F3': 'substitution-reference',
+ u'destinaci\u00F3': 'target',
+ u'refer\u00E8ncia-a-uri': 'uri-reference',
+ u'refer\u00E8ncia-uri': 'uri-reference',
+ u'uri': 'uri-reference',
+ u'url': 'uri-reference',
+ u'cru': 'raw',}
+"""Mapping of Catalan role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/cs.py b/docutils/parsers/rst/languages/cs.py
new file mode 100644
index 000000000..7163cbf8f
--- /dev/null
+++ b/docutils/parsers/rst/languages/cs.py
@@ -0,0 +1,102 @@
+# Author: Marek Blaha
+# Contact: mb@dat.cz
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Czech-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ u'pozor': 'attention',
+ u'caution (translation required)': 'caution', # jak rozlisit caution a warning?
+ u'nebezpe\u010D\u00ED': 'danger',
+ u'chyba': 'error',
+ u'rada': 'hint',
+ u'd\u016Fle\u017Eit\u00E9': 'important',
+ u'pozn\u00E1mka': 'note',
+ u'tip (translation required)': 'tip',
+ u'varov\u00E1n\u00ED': 'warning',
+ u'admonition (translation required)': 'admonition',
+ u'sidebar (translation required)': 'sidebar',
+ u't\u00E9ma': 'topic',
+ u'line-block (translation required)': 'line-block',
+ u'parsed-literal (translation required)': 'parsed-literal',
+ u'odd\u00EDl': 'rubric',
+ u'moto': 'epigraph',
+ u'highlights (translation required)': 'highlights',
+ u'pull-quote (translation required)': 'pull-quote',
+ u'compound (translation required)': 'compound',
+ #'questions': 'questions',
+ #'qa': 'questions',
+ #'faq': 'questions',
+ u'table (translation required)': 'table',
+ u'csv-table (translation required)': 'csv-table',
+ u'list-table (translation required)': 'list-table',
+ u'meta (translation required)': 'meta',
+ #'imagemap': 'imagemap',
+ u'image (translation required)': 'image', # obrazek
+ u'figure (translation required)': 'figure', # a tady?
+ u'include (translation required)': 'include',
+ u'raw (translation required)': 'raw',
+ u'replace (translation required)': 'replace',
+ u'unicode (translation required)': 'unicode',
+ u't\u0159\u00EDda': 'class',
+ u'role (translation required)': 'role',
+ u'obsah': 'contents',
+ u'sectnum (translation required)': 'sectnum',
+ u'section-numbering (translation required)': 'sectnum',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',
+ #'footnotes': 'footnotes',
+ #'citations': 'citations',
+ u'target-notes (translation required)': 'target-notes',
+ u'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Czech name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ u'abbreviation (translation required)': 'abbreviation',
+ u'ab (translation required)': 'abbreviation',
+ u'acronym (translation required)': 'acronym',
+ u'ac (translation required)': 'acronym',
+ u'index (translation required)': 'index',
+ u'i (translation required)': 'index',
+ u'subscript (translation required)': 'subscript',
+ u'sub (translation required)': 'subscript',
+ u'superscript (translation required)': 'superscript',
+ u'sup (translation required)': 'superscript',
+ u'title-reference (translation required)': 'title-reference',
+ u'title (translation required)': 'title-reference',
+ u't (translation required)': 'title-reference',
+ u'pep-reference (translation required)': 'pep-reference',
+ u'pep (translation required)': 'pep-reference',
+ u'rfc-reference (translation required)': 'rfc-reference',
+ u'rfc (translation required)': 'rfc-reference',
+ u'emphasis (translation required)': 'emphasis',
+ u'strong (translation required)': 'strong',
+ u'literal (translation required)': 'literal',
+ u'named-reference (translation required)': 'named-reference',
+ u'anonymous-reference (translation required)': 'anonymous-reference',
+ u'footnote-reference (translation required)': 'footnote-reference',
+ u'citation-reference (translation required)': 'citation-reference',
+ u'substitution-reference (translation required)': 'substitution-reference',
+ u'target (translation required)': 'target',
+ u'uri-reference (translation required)': 'uri-reference',
+ u'uri (translation required)': 'uri-reference',
+ u'url (translation required)': 'uri-reference',
+ u'raw (translation required)': 'raw',}
+"""Mapping of Czech role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/de.py b/docutils/parsers/rst/languages/de.py
new file mode 100644
index 000000000..9431f0e9d
--- /dev/null
+++ b/docutils/parsers/rst/languages/de.py
@@ -0,0 +1,93 @@
+# Authors: Engelbert Gruber; Felix Wiemann
+# Contact: grubert@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+German-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ 'achtung': 'attention',
+ 'vorsicht': 'caution',
+ 'gefahr': 'danger',
+ 'fehler': 'error',
+ 'hinweis': 'hint',
+ 'wichtig': 'important',
+ 'notiz': 'note',
+ 'tipp': 'tip',
+ 'warnung': 'warning',
+ 'ermahnung': 'admonition',
+ 'kasten': 'sidebar',
+ 'seitenkasten': 'sidebar',
+ 'thema': 'topic',
+ 'zeilen-block': 'line-block',
+ 'parsed-literal (translation required)': 'parsed-literal',
+ 'rubrik': 'rubric',
+ 'epigraph': 'epigraph',
+ 'highlights (translation required)': 'highlights',
+ 'pull-quote (translation required)': 'pull-quote', # kasten too ?
+ 'zusammengesetzt': 'compound',
+ 'verbund': 'compound',
+ #'fragen': 'questions',
+ 'tabelle': 'table',
+ 'csv-tabelle': 'csv-table',
+ 'list-table (translation required)': 'list-table',
+ 'meta': 'meta',
+ #'imagemap': 'imagemap',
+ 'bild': 'image',
+ 'abbildung': 'figure',
+ u'unver\xe4ndert': 'raw',
+ u'roh': 'raw',
+ u'einf\xfcgen': 'include',
+ 'ersetzung': 'replace',
+ 'ersetzen': 'replace',
+ 'ersetze': 'replace',
+ 'unicode': 'unicode',
+ 'klasse': 'class',
+ 'rolle': 'role',
+ 'inhalt': 'contents',
+ 'kapitel-nummerierung': 'sectnum',
+ 'abschnitts-nummerierung': 'sectnum',
+ u'linkziel-fu\xdfnoten': 'target-notes',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',
+ #u'fu\xdfnoten': 'footnotes',
+ #'zitate': 'citations',
+ }
+"""German name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ u'abk\xfcrzung': 'abbreviation',
+ 'akronym': 'acronym',
+ 'index': 'index',
+ 'tiefgestellt': 'subscript',
+ 'hochgestellt': 'superscript',
+ 'titel-referenz': 'title-reference',
+ 'pep-referenz': 'pep-reference',
+ 'rfc-referenz': 'rfc-reference',
+ 'betonung': 'emphasis',
+ 'fett': 'strong',
+ u'w\xf6rtlich': 'literal',
+ 'benannte-referenz': 'named-reference',
+ 'unbenannte-referenz': 'anonymous-reference',
+ u'fu\xdfnoten-referenz': 'footnote-reference',
+ 'zitat-referenz': 'citation-reference',
+ 'ersetzungs-referenz': 'substitution-reference',
+ 'ziel': 'target',
+ 'uri-referenz': 'uri-reference',
+ u'unver\xe4ndert': 'raw',
+ u'roh': 'raw',}
+"""Mapping of German role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/en.py b/docutils/parsers/rst/languages/en.py
new file mode 100644
index 000000000..63ddcb1b9
--- /dev/null
+++ b/docutils/parsers/rst/languages/en.py
@@ -0,0 +1,102 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+English-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'attention': 'attention',
+ 'caution': 'caution',
+ 'danger': 'danger',
+ 'error': 'error',
+ 'hint': 'hint',
+ 'important': 'important',
+ 'note': 'note',
+ 'tip': 'tip',
+ 'warning': 'warning',
+ 'admonition': 'admonition',
+ 'sidebar': 'sidebar',
+ 'topic': 'topic',
+ 'line-block': 'line-block',
+ 'parsed-literal': 'parsed-literal',
+ 'rubric': 'rubric',
+ 'epigraph': 'epigraph',
+ 'highlights': 'highlights',
+ 'pull-quote': 'pull-quote',
+ 'compound': 'compound',
+ #'questions': 'questions',
+ 'table': 'table',
+ 'csv-table': 'csv-table',
+ 'list-table': 'list-table',
+ #'qa': 'questions',
+ #'faq': 'questions',
+ 'meta': 'meta',
+ #'imagemap': 'imagemap',
+ 'image': 'image',
+ 'figure': 'figure',
+ 'include': 'include',
+ 'raw': 'raw',
+ 'replace': 'replace',
+ 'unicode': 'unicode',
+ 'class': 'class',
+ 'role': 'role',
+ 'contents': 'contents',
+ 'sectnum': 'sectnum',
+ 'section-numbering': 'sectnum',
+ 'header': 'header',
+ 'footer': 'footer',
+ #'footnotes': 'footnotes',
+ #'citations': 'citations',
+ 'target-notes': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""English name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'abbreviation': 'abbreviation',
+ 'ab': 'abbreviation',
+ 'acronym': 'acronym',
+ 'ac': 'acronym',
+ 'index': 'index',
+ 'i': 'index',
+ 'subscript': 'subscript',
+ 'sub': 'subscript',
+ 'superscript': 'superscript',
+ 'sup': 'superscript',
+ 'title-reference': 'title-reference',
+ 'title': 'title-reference',
+ 't': 'title-reference',
+ 'pep-reference': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'rfc-reference': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'emphasis': 'emphasis',
+ 'strong': 'strong',
+ 'literal': 'literal',
+ 'named-reference': 'named-reference',
+ 'anonymous-reference': 'anonymous-reference',
+ 'footnote-reference': 'footnote-reference',
+ 'citation-reference': 'citation-reference',
+ 'substitution-reference': 'substitution-reference',
+ 'target': 'target',
+ 'uri-reference': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'raw': 'raw',}
+"""Mapping of English role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/eo.py b/docutils/parsers/rst/languages/eo.py
new file mode 100644
index 000000000..069fea45e
--- /dev/null
+++ b/docutils/parsers/rst/languages/eo.py
@@ -0,0 +1,111 @@
+# Author: Marcelo Huerta San Martin
+# Contact: richieadler@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Esperanto-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ u'atentu': 'attention',
+ u'zorgu': 'caution',
+ u'dangxero': 'danger',
+ u'dan\u011dero': 'danger',
+ u'eraro': 'error',
+ u'spuro': 'hint',
+ u'grava': 'important',
+ u'noto': 'note',
+ u'helpeto': 'tip',
+ u'averto': 'warning',
+ u'admono': 'admonition',
+ u'flankteksto': 'sidebar',
+ u'temo': 'topic',
+ u'linea-bloko': 'line-block',
+ u'analizota-literalo': 'parsed-literal',
+ u'rubriko': 'rubric',
+ u'epigrafo': 'epigraph',
+ u'elstarajxoj': 'highlights',
+ u'elstara\u0135oj': 'highlights',
+ u'ekstera-citajxo': 'pull-quote',
+ u'ekstera-cita\u0135o': 'pull-quote',
+ u'kombinajxo': 'compound',
+ u'kombina\u0135o': 'compound',
+ #'questions': 'questions',
+ #'qa': 'questions',
+ #'faq': 'questions',
+ u'tabelo': 'table',
+ u'tabelo-vdk': 'csv-table', # "valoroj disigitaj per komoj"
+ u'tabelo-csv': 'csv-table',
+ u'tabelo-lista': 'list-table',
+ u'meta': 'meta',
+ #'imagemap': 'imagemap',
+ u'bildo': 'image',
+ u'figuro': 'figure',
+ u'inkludi': 'include',
+ u'senanaliza': 'raw',
+ u'anstatauxi': 'replace',
+ u'anstata\u016di': 'replace',
+ u'unicode': 'unicode',
+ u'klaso': 'class',
+ u'rolo': 'role',
+ u'enhavo': 'contents',
+ u'seknum': 'sectnum',
+ u'sekcia-numerado': 'sectnum',
+ u'kapsekcio': 'header',
+ u'piedsekcio': 'footer',
+ #'footnotes': 'footnotes',
+ #'citations': 'citations',
+ u'celaj-notoj': 'target-notes',
+ u'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Esperanto name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ u'mallongigo': 'abbreviation',
+ u'mall': 'abbreviation',
+ u'komenclitero': 'acronym',
+ u'kl': 'acronym',
+ u'indekso': 'index',
+ u'i': 'index',
+ u'subskribo': 'subscript',
+ u'sub': 'subscript',
+ u'supraskribo': 'superscript',
+ u'sup': 'superscript',
+ u'titola-referenco': 'title-reference',
+ u'titolo': 'title-reference',
+ u't': 'title-reference',
+ u'pep-referenco': 'pep-reference',
+ u'pep': 'pep-reference',
+ u'rfc-referenco': 'rfc-reference',
+ u'rfc': 'rfc-reference',
+ u'emfazo': 'emphasis',
+ u'forta': 'strong',
+ u'litera': 'literal',
+ u'nomita-referenco': 'named-reference',
+ u'nenomita-referenco': 'anonymous-reference',
+ u'piednota-referenco': 'footnote-reference',
+ u'citajxo-referenco': 'citation-reference',
+ u'cita\u0135o-referenco': 'citation-reference',
+ u'anstatauxa-referenco': 'substitution-reference',
+ u'anstata\u016da-referenco': 'substitution-reference',
+ u'celo': 'target',
+ u'uri-referenco': 'uri-reference',
+ u'uri': 'uri-reference',
+ u'url': 'uri-reference',
+ u'senanaliza': 'raw',
+}
+"""Mapping of Esperanto role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/es.py b/docutils/parsers/rst/languages/es.py
new file mode 100644
index 000000000..5f0a094da
--- /dev/null
+++ b/docutils/parsers/rst/languages/es.py
@@ -0,0 +1,117 @@
+# -*- coding: iso-8859-1 -*-
+# Author: Marcelo Huerta San Martín
+# Contact: richieadler@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Spanish-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ u'atenci\u00f3n': 'attention',
+ u'atencion': 'attention',
+ u'precauci\u00f3n': 'caution',
+ u'precaucion': 'caution',
+ u'peligro': 'danger',
+ u'error': 'error',
+ u'sugerencia': 'hint',
+ u'importante': 'important',
+ u'nota': 'note',
+ u'consejo': 'tip',
+ u'advertencia': 'warning',
+ u'exhortacion': 'admonition',
+ u'exhortaci\u00f3n': 'admonition',
+ u'nota-al-margen': 'sidebar',
+ u'tema': 'topic',
+ u'bloque-de-lineas': 'line-block',
+ u'bloque-de-l\u00edneas': 'line-block',
+ u'literal-evaluado': 'parsed-literal',
+ u'firma': 'rubric',
+ u'ep\u00edgrafe': 'epigraph',
+ u'epigrafe': 'epigraph',
+ u'destacado': 'highlights',
+ u'cita-destacada': 'pull-quote',
+ u'combinacion': 'compound',
+ u'combinaci\u00f3n': 'compound',
+ #'questions': 'questions',
+ #'qa': 'questions',
+ #'faq': 'questions',
+ u'tabla': 'table',
+ u'tabla-vsc': 'csv-table',
+ u'tabla-csv': 'csv-table',
+ u'tabla-lista': 'list-table',
+ u'meta': 'meta',
+ #'imagemap': 'imagemap',
+ u'imagen': 'image',
+ u'figura': 'figure',
+ u'incluir': 'include',
+ u'sin-analisis': 'raw',
+ u'sin-an\u00e1lisis': 'raw',
+ u'reemplazar': 'replace',
+ u'unicode': 'unicode',
+ u'clase': 'class',
+ u'rol': 'role',
+ u'contenido': 'contents',
+ u'numseccion': 'sectnum',
+ u'numsecci\u00f3n': 'sectnum',
+ u'numeracion-seccion': 'sectnum',
+ u'numeraci\u00f3n-secci\u00f3n': 'sectnum',
+ u'notas-destino': 'target-notes',
+ u'cabecera': 'header',
+ u'pie': 'footer',
+ #'footnotes': 'footnotes',
+ #'citations': 'citations',
+ u'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Spanish name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ u'abreviatura': 'abbreviation',
+ u'ab': 'abbreviation',
+ u'acronimo': 'acronym',
+ u'acronimo': 'acronym',
+ u'ac': 'acronym',
+ u'indice': 'index',
+ u'i': 'index',
+ u'subindice': 'subscript',
+ u'sub\u00edndice': 'subscript',
+ u'superindice': 'superscript',
+ u'super\u00edndice': 'superscript',
+ u'referencia-titulo': 'title-reference',
+ u'titulo': 'title-reference',
+ u't': 'title-reference',
+ u'referencia-pep': 'pep-reference',
+ u'pep': 'pep-reference',
+ u'referencia-rfc': 'rfc-reference',
+ u'rfc': 'rfc-reference',
+ u'enfasis': 'emphasis',
+ u'\u00e9nfasis': 'emphasis',
+ u'destacado': 'strong',
+ u'literal': 'literal', # "literal" is also a word in Spanish :-)
+ u'referencia-con-nombre': 'named-reference',
+ u'referencia-anonima': 'anonymous-reference',
+ u'referencia-an\u00f3nima': 'anonymous-reference',
+ u'referencia-nota-al-pie': 'footnote-reference',
+ u'referencia-cita': 'citation-reference',
+ u'referencia-sustitucion': 'substitution-reference',
+ u'referencia-sustituci\u00f3n': 'substitution-reference',
+ u'destino': 'target',
+ u'referencia-uri': 'uri-reference',
+ u'uri': 'uri-reference',
+ u'url': 'uri-reference',
+ u'sin-analisis': 'raw',
+ u'sin-an\u00e1lisis': 'raw',
+}
+"""Mapping of Spanish role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/fi.py b/docutils/parsers/rst/languages/fi.py
new file mode 100644
index 000000000..c60444e3f
--- /dev/null
+++ b/docutils/parsers/rst/languages/fi.py
@@ -0,0 +1,91 @@
+# Author: Asko Soukka
+# Contact: asko.soukka@iki.fi
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Finnish-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ u'huomio': u'attention',
+ u'varo': u'caution',
+ u'vaara': u'danger',
+ u'virhe': u'error',
+ u'vihje': u'hint',
+ u't\u00e4rke\u00e4\u00e4': u'important',
+ u'huomautus': u'note',
+ u'neuvo': u'tip',
+ u'varoitus': u'warning',
+ u'kehotus': u'admonition',
+ u'sivupalkki': u'sidebar',
+ u'aihe': u'topic',
+ u'rivi': u'line-block',
+ u'tasalevyinen': u'parsed-literal',
+ u'ohje': u'rubric',
+ u'epigraafi': u'epigraph',
+ u'kohokohdat': u'highlights',
+ u'lainaus': u'pull-quote',
+ u'taulukko': u'table',
+ u'csv-taulukko': u'csv-table',
+ u'list-table (translation required)': 'list-table',
+ u'compound (translation required)': 'compound',
+ #u'kysymykset': u'questions',
+ u'meta': u'meta',
+ #u'kuvakartta': u'imagemap',
+ u'kuva': u'image',
+ u'kaavio': u'figure',
+ u'sis\u00e4llyt\u00e4': u'include',
+ u'raaka': u'raw',
+ u'korvaa': u'replace',
+ u'unicode': u'unicode',
+ u'luokka': u'class',
+ u'rooli': u'role',
+ u'sis\u00e4llys': u'contents',
+ u'kappale': u'sectnum',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',
+ #u'alaviitteet': u'footnotes',
+ #u'viitaukset': u'citations',
+ u'target-notes (translation required)': u'target-notes'}
+"""Finnish name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ u'lyhennys': u'abbreviation',
+ u'akronyymi': u'acronym',
+ u'kirjainsana': u'acronym',
+ u'hakemisto': u'index',
+ u'luettelo': u'index',
+ u'alaindeksi': u'subscript',
+ u'indeksi': u'subscript',
+ u'yl\u00e4indeksi': u'superscript',
+ u'title-reference (translation required)': u'title-reference',
+ u'title (translation required)': u'title-reference',
+ u'pep-reference (translation required)': u'pep-reference',
+ u'rfc-reference (translation required)': u'rfc-reference',
+ u'korostus': u'emphasis',
+ u'vahvistus': u'strong',
+ u'tasalevyinen': u'literal',
+ u'named-reference (translation required)': u'named-reference',
+ u'anonymous-reference (translation required)': u'anonymous-reference',
+ u'footnote-reference (translation required)': u'footnote-reference',
+ u'citation-reference (translation required)': u'citation-reference',
+ u'substitution-reference (translation required)': u'substitution-reference',
+ u'kohde': u'target',
+ u'uri-reference (translation required)': u'uri-reference',
+ u'raw (translation required)': 'raw',}
+"""Mapping of Finnish role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/fr.py b/docutils/parsers/rst/languages/fr.py
new file mode 100644
index 000000000..8b9cb04de
--- /dev/null
+++ b/docutils/parsers/rst/languages/fr.py
@@ -0,0 +1,97 @@
+# Authors: David Goodger; William Dode
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+French-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ u'attention': 'attention',
+ u'pr\u00E9caution': 'caution',
+ u'danger': 'danger',
+ u'erreur': 'error',
+ u'conseil': 'hint',
+ u'important': 'important',
+ u'note': 'note',
+ u'astuce': 'tip',
+ u'avertissement': 'warning',
+ u'admonition': 'admonition',
+ u'encadr\u00E9': 'sidebar',
+ u'sujet': 'topic',
+ u'bloc-textuel': 'line-block',
+ u'bloc-interpr\u00E9t\u00E9': 'parsed-literal',
+ u'code-interpr\u00E9t\u00E9': 'parsed-literal',
+ u'intertitre': 'rubric',
+ u'exergue': 'epigraph',
+ u'\u00E9pigraphe': 'epigraph',
+ u'chapeau': 'highlights',
+ u'accroche': 'pull-quote',
+ u'compound (translation required)': 'compound',
+ #u'questions': 'questions',
+ #u'qr': 'questions',
+ #u'faq': 'questions',
+ u'tableau': 'table',
+ u'csv-table (translation required)': 'csv-table',
+ u'list-table (translation required)': 'list-table',
+ u'm\u00E9ta': 'meta',
+ #u'imagemap (translation required)': 'imagemap',
+ u'image': 'image',
+ u'figure': 'figure',
+ u'inclure': 'include',
+ u'brut': 'raw',
+ u'remplacer': 'replace',
+ u'remplace': 'replace',
+ u'unicode': 'unicode',
+ u'classe': 'class',
+ u'role (translation required)': 'role',
+ u'sommaire': 'contents',
+ u'table-des-mati\u00E8res': 'contents',
+ u'sectnum': 'sectnum',
+ u'section-num\u00E9rot\u00E9e': 'sectnum',
+ u'liens': 'target-notes',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',
+ #u'footnotes (translation required)': 'footnotes',
+ #u'citations (translation required)': 'citations',
+ }
+"""French name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ u'abr\u00E9viation': 'abbreviation',
+ u'acronyme': 'acronym',
+ u'sigle': 'acronym',
+ u'index': 'index',
+ u'indice': 'subscript',
+ u'ind': 'subscript',
+ u'exposant': 'superscript',
+ u'exp': 'superscript',
+ u'titre-r\u00E9f\u00E9rence': 'title-reference',
+ u'titre': 'title-reference',
+ u'pep-r\u00E9f\u00E9rence': 'pep-reference',
+ u'rfc-r\u00E9f\u00E9rence': 'rfc-reference',
+ u'emphase': 'emphasis',
+ u'fort': 'strong',
+ u'litt\u00E9ral': 'literal',
+ u'nomm\u00E9e-r\u00E9f\u00E9rence': 'named-reference',
+ u'anonyme-r\u00E9f\u00E9rence': 'anonymous-reference',
+ u'note-r\u00E9f\u00E9rence': 'footnote-reference',
+ u'citation-r\u00E9f\u00E9rence': 'citation-reference',
+ u'substitution-r\u00E9f\u00E9rence': 'substitution-reference',
+ u'lien': 'target',
+ u'uri-r\u00E9f\u00E9rence': 'uri-reference',
+ u'brut': 'raw',}
+"""Mapping of French role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/it.py b/docutils/parsers/rst/languages/it.py
new file mode 100644
index 000000000..ee31b42ae
--- /dev/null
+++ b/docutils/parsers/rst/languages/it.py
@@ -0,0 +1,90 @@
+# Author: Nicola Larosa, Lele Gaifax
+# Contact: docutils@tekNico.net, lele@seldati.it
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# Beware: the italian translation of the reStructuredText documentation
+# at http://docit.bice.dyndns.org/static/ReST, in particular
+# http://docit.bice.dyndns.org/static/ReST/ref/rst/directives.html, needs
+# to be synced with the content of this file.
+
+"""
+Italian-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ 'attenzione': 'attention',
+ 'cautela': 'caution',
+ 'pericolo': 'danger',
+ 'errore': 'error',
+ 'suggerimento': 'hint',
+ 'importante': 'important',
+ 'nota': 'note',
+ 'consiglio': 'tip',
+ 'avvertenza': 'warning',
+ 'ammonizione': 'admonition',
+ 'riquadro': 'sidebar',
+ 'argomento': 'topic',
+ 'blocco-di-righe': 'line-block',
+ 'blocco-interpretato': 'parsed-literal',
+ 'rubrica': 'rubric',
+ 'epigrafe': 'epigraph',
+ 'punti-salienti': 'highlights',
+ 'estratto-evidenziato': 'pull-quote',
+ 'composito': 'compound',
+ #'questions': 'questions',
+ #'qa': 'questions',
+ #'faq': 'questions',
+ 'tabella': 'table',
+ 'tabella-csv': 'csv-table',
+ 'tabella-elenco': 'list-table',
+ 'meta': 'meta',
+ #'imagemap': 'imagemap',
+ 'immagine': 'image',
+ 'figura': 'figure',
+ 'includi': 'include',
+ 'grezzo': 'raw',
+ 'sostituisci': 'replace',
+ 'unicode': 'unicode',
+ 'classe': 'class',
+ 'ruolo': 'role',
+ 'indice': 'contents',
+ 'contenuti': 'contents',
+ 'seznum': 'sectnum',
+ 'sezioni-autonumerate': 'sectnum',
+ 'annota-riferimenti-esterni': 'target-notes',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',
+ #'footnotes': 'footnotes',
+ #'citations': 'citations',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Italian name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ 'abbreviazione': 'abbreviation',
+ 'acronimo': 'acronym',
+ 'indice': 'index',
+ 'deponente': 'subscript',
+ 'esponente': 'superscript',
+ 'riferimento-titolo': 'title-reference',
+ 'riferimento-pep': 'pep-reference',
+ 'riferimento-rfc': 'rfc-reference',
+ 'enfasi': 'emphasis',
+ 'forte': 'strong',
+ 'letterale': 'literal',
+ 'riferimento-con-nome': 'named-reference',
+ 'riferimento-anonimo': 'anonymous-reference',
+ 'riferimento-nota': 'footnote-reference',
+ 'riferimento-citazione': 'citation-reference',
+ 'riferimento-sostituzione': 'substitution-reference',
+ 'destinazione': 'target',
+ 'riferimento-uri': 'uri-reference',
+ 'grezzo': 'raw',}
+"""Mapping of Italian role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/nl.py b/docutils/parsers/rst/languages/nl.py
new file mode 100644
index 000000000..35942ca05
--- /dev/null
+++ b/docutils/parsers/rst/languages/nl.py
@@ -0,0 +1,106 @@
+# Author: Martijn Pieters
+# Contact: mjpieters@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Dutch-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'attentie': 'attention',
+ 'let-op': 'caution',
+ 'gevaar': 'danger',
+ 'fout': 'error',
+ 'hint': 'hint',
+ 'belangrijk': 'important',
+ 'opmerking': 'note',
+ 'tip': 'tip',
+ 'waarschuwing': 'warning',
+ 'aanmaning': 'admonition',
+ 'katern': 'sidebar',
+ 'onderwerp': 'topic',
+ 'lijn-blok': 'line-block',
+ 'letterlijk-ontleed': 'parsed-literal',
+ 'rubriek': 'rubric',
+ 'opschrift': 'epigraph',
+ 'hoogtepunten': 'highlights',
+ 'pull-quote': 'pull-quote', # Dutch printers use the english term
+ 'samenstelling': 'compound',
+ 'verbinding': 'compound',
+ #'vragen': 'questions',
+ 'tabel': 'table',
+ 'csv-tabel': 'csv-table',
+ 'lijst-tabel': 'list-table',
+ #'veelgestelde-vragen': 'questions',
+ 'meta': 'meta',
+ #'imagemap': 'imagemap',
+ 'beeld': 'image',
+ 'figuur': 'figure',
+ 'opnemen': 'include',
+ 'onbewerkt': 'raw',
+ 'vervang': 'replace',
+ 'vervanging': 'replace',
+ 'unicode': 'unicode',
+ 'klasse': 'class',
+ 'rol': 'role',
+ 'inhoud': 'contents',
+ 'sectnum': 'sectnum',
+ 'sectie-nummering': 'sectnum',
+ 'hoofdstuk-nummering': 'sectnum',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',
+ #'voetnoten': 'footnotes',
+ #'citaten': 'citations',
+ 'verwijzing-voetnoten': 'target-notes',
+ 'restructuredtext-test-instructie': 'restructuredtext-test-directive'}
+"""Dutch name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'afkorting': 'abbreviation',
+ # 'ab': 'abbreviation',
+ 'acroniem': 'acronym',
+ 'ac': 'acronym',
+ 'index': 'index',
+ 'i': 'index',
+ 'inferieur': 'subscript',
+ 'inf': 'subscript',
+ 'superieur': 'superscript',
+ 'sup': 'superscript',
+ 'titel-referentie': 'title-reference',
+ 'titel': 'title-reference',
+ 't': 'title-reference',
+ 'pep-referentie': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'rfc-referentie': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'nadruk': 'emphasis',
+ 'extra': 'strong',
+ 'extra-nadruk': 'strong',
+ 'vet': 'strong',
+ 'letterlijk': 'literal',
+ 'benoemde-referentie': 'named-reference',
+ 'anonieme-referentie': 'anonymous-reference',
+ 'voetnoot-referentie': 'footnote-reference',
+ 'citaat-referentie': 'citation-reference',
+ 'substitie-reference': 'substitution-reference',
+ 'verwijzing': 'target',
+ 'uri-referentie': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'onbewerkt': 'raw',}
+"""Mapping of Dutch role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/pt_br.py b/docutils/parsers/rst/languages/pt_br.py
new file mode 100644
index 000000000..6f5d4cc6b
--- /dev/null
+++ b/docutils/parsers/rst/languages/pt_br.py
@@ -0,0 +1,102 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Brazilian Portuguese-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ u'aten\u00E7\u00E3o': 'attention',
+ 'cuidado': 'caution',
+ 'perigo': 'danger',
+ 'erro': 'error',
+ u'sugest\u00E3o': 'hint',
+ 'importante': 'important',
+ 'nota': 'note',
+ 'dica': 'tip',
+ 'aviso': 'warning',
+ u'exorta\u00E7\u00E3o': 'admonition',
+ 'barra-lateral': 'sidebar',
+ u't\u00F3pico': 'topic',
+ 'bloco-de-linhas': 'line-block',
+ 'literal-interpretado': 'parsed-literal',
+ 'rubrica': 'rubric',
+ u'ep\u00EDgrafo': 'epigraph',
+ 'destaques': 'highlights',
+ u'cita\u00E7\u00E3o-destacada': 'pull-quote',
+ u'compound (translation required)': 'compound',
+ #'perguntas': 'questions',
+ #'qa': 'questions',
+ #'faq': 'questions',
+ u'table (translation required)': 'table',
+ u'csv-table (translation required)': 'csv-table',
+ u'list-table (translation required)': 'list-table',
+ 'meta': 'meta',
+ #'imagemap': 'imagemap',
+ 'imagem': 'image',
+ 'figura': 'figure',
+ u'inclus\u00E3o': 'include',
+ 'cru': 'raw',
+ u'substitui\u00E7\u00E3o': 'replace',
+ 'unicode': 'unicode',
+ 'classe': 'class',
+ 'role (translation required)': 'role',
+ u'\u00EDndice': 'contents',
+ 'numsec': 'sectnum',
+ u'numera\u00E7\u00E3o-de-se\u00E7\u00F5es': 'sectnum',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',
+ #u'notas-de-rorap\u00E9': 'footnotes',
+ #u'cita\u00E7\u00F5es': 'citations',
+ u'links-no-rodap\u00E9': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Brazilian Portuguese name to registered (in directives/__init__.py)
+directive name mapping."""
+
+roles = {
+ # language-dependent: fixed
+ u'abbrevia\u00E7\u00E3o': 'abbreviation',
+ 'ab': 'abbreviation',
+ u'acr\u00F4nimo': 'acronym',
+ 'ac': 'acronym',
+ u'\u00EDndice-remissivo': 'index',
+ 'i': 'index',
+ 'subscrito': 'subscript',
+ 'sub': 'subscript',
+ 'sobrescrito': 'superscript',
+ 'sob': 'superscript',
+ u'refer\u00EAncia-a-t\u00EDtulo': 'title-reference',
+ u't\u00EDtulo': 'title-reference',
+ 't': 'title-reference',
+ u'refer\u00EAncia-a-pep': 'pep-reference',
+ 'pep': 'pep-reference',
+ u'refer\u00EAncia-a-rfc': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ u'\u00EAnfase': 'emphasis',
+ 'forte': 'strong',
+ 'literal': 'literal', # translation required?
+ u'refer\u00EAncia-por-nome': 'named-reference',
+ u'refer\u00EAncia-an\u00F4nima': 'anonymous-reference',
+ u'refer\u00EAncia-a-nota-de-rodap\u00E9': 'footnote-reference',
+ u'refer\u00EAncia-a-cita\u00E7\u00E3o': 'citation-reference',
+ u'refer\u00EAncia-a-substitui\u00E7\u00E3o': 'substitution-reference',
+ 'alvo': 'target',
+ u'refer\u00EAncia-a-uri': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'cru': 'raw',}
+"""Mapping of Brazilian Portuguese role names to canonical role names
+for interpreted text."""
diff --git a/docutils/parsers/rst/languages/ru.py b/docutils/parsers/rst/languages/ru.py
new file mode 100644
index 000000000..4ccca658c
--- /dev/null
+++ b/docutils/parsers/rst/languages/ru.py
@@ -0,0 +1,101 @@
+# Author: Roman Suzi
+# Contact: rnd@onego.ru
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Russian-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+directives = {
+ u'\u0431\u043b\u043e\u043a-\u0441\u0442\u0440\u043e\u043a': u'line-block',
+ u'meta': u'meta',
+ u'\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0439-\u043b\u0438\u0442\u0435\u0440\u0430\u043b':
+ u'parsed-literal',
+ u'\u0432\u044b\u0434\u0435\u043b\u0435\u043d\u043d\u0430\u044f-\u0446\u0438\u0442\u0430\u0442\u0430':
+ u'pull-quote',
+ u'compound (translation required)': 'compound',
+ u'table (translation required)': 'table',
+ u'csv-table (translation required)': 'csv-table',
+ u'list-table (translation required)': 'list-table',
+ u'\u0441\u044b\u0440\u043e\u0439': u'raw',
+ u'\u0437\u0430\u043c\u0435\u043d\u0430': u'replace',
+ u'\u0442\u0435\u0441\u0442\u043e\u0432\u0430\u044f-\u0434\u0438\u0440\u0435\u043a\u0442\u0438\u0432\u0430-restructuredtext':
+ u'restructuredtext-test-directive',
+ u'\u0446\u0435\u043b\u0435\u0432\u044b\u0435-\u0441\u043d\u043e\u0441\u043a\u0438':
+ u'target-notes',
+ u'unicode': u'unicode',
+ u'\u0431\u043e\u043a\u043e\u0432\u0430\u044f-\u043f\u043e\u043b\u043e\u0441\u0430':
+ u'sidebar',
+ u'\u0432\u0430\u0436\u043d\u043e': u'important',
+ u'\u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c': u'include',
+ u'\u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435': u'attention',
+ u'\u0432\u044b\u0434\u0435\u043b\u0435\u043d\u0438\u0435': u'highlights',
+ u'\u0437\u0430\u043c\u0435\u0447\u0430\u043d\u0438\u0435': u'admonition',
+ u'\u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435':
+ u'image',
+ u'\u043a\u043b\u0430\u0441\u0441': u'class',
+ u'role (translation required)': 'role',
+ u'\u043d\u043e\u043c\u0435\u0440-\u0440\u0430\u0437\u0434\u0435\u043b\u0430':
+ u'sectnum',
+ u'\u043d\u0443\u043c\u0435\u0440\u0430\u0446\u0438\u044f-\u0440\u0430\u0437'
+ u'\u0434\u0435\u043b\u043e\u0432': u'sectnum',
+ u'\u043e\u043f\u0430\u0441\u043d\u043e': u'danger',
+ u'\u043e\u0441\u0442\u043e\u0440\u043e\u0436\u043d\u043e': u'caution',
+ u'\u043e\u0448\u0438\u0431\u043a\u0430': u'error',
+ u'\u043f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430': u'tip',
+ u'\u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d'
+ u'\u0438\u0435': u'warning',
+ u'\u043f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u0435': u'note',
+ u'\u0440\u0438\u0441\u0443\u043d\u043e\u043a': u'figure',
+ u'\u0440\u0443\u0431\u0440\u0438\u043a\u0430': u'rubric',
+ u'\u0441\u043e\u0432\u0435\u0442': u'hint',
+ u'\u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435': u'contents',
+ u'\u0442\u0435\u043c\u0430': u'topic',
+ u'\u044d\u043f\u0438\u0433\u0440\u0430\u0444': u'epigraph',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',}
+"""Russian name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ u'\u0430\u043a\u0440\u043e\u043d\u0438\u043c': 'acronym',
+ u'\u0430\u043d\u043e\u043d\u0438\u043c\u043d\u0430\u044f-\u0441\u0441\u044b\u043b\u043a\u0430':
+ 'anonymous-reference',
+ u'\u0431\u0443\u043a\u0432\u0430\u043b\u044c\u043d\u043e': 'literal',
+ u'\u0432\u0435\u0440\u0445\u043d\u0438\u0439-\u0438\u043d\u0434\u0435\u043a\u0441':
+ 'superscript',
+ u'\u0432\u044b\u0434\u0435\u043b\u0435\u043d\u0438\u0435': 'emphasis',
+ u'\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u043d\u0430\u044f-\u0441\u0441\u044b\u043b\u043a\u0430':
+ 'named-reference',
+ u'\u0438\u043d\u0434\u0435\u043a\u0441': 'index',
+ u'\u043d\u0438\u0436\u043d\u0438\u0439-\u0438\u043d\u0434\u0435\u043a\u0441':
+ 'subscript',
+ u'\u0441\u0438\u043b\u044c\u043d\u043e\u0435-\u0432\u044b\u0434\u0435\u043b\u0435\u043d\u0438\u0435':
+ 'strong',
+ u'\u0441\u043e\u043a\u0440\u0430\u0449\u0435\u043d\u0438\u0435':
+ 'abbreviation',
+ u'\u0441\u0441\u044b\u043b\u043a\u0430-\u0437\u0430\u043c\u0435\u043d\u0430':
+ 'substitution-reference',
+ u'\u0441\u0441\u044b\u043b\u043a\u0430-\u043d\u0430-pep': 'pep-reference',
+ u'\u0441\u0441\u044b\u043b\u043a\u0430-\u043d\u0430-rfc': 'rfc-reference',
+ u'\u0441\u0441\u044b\u043b\u043a\u0430-\u043d\u0430-uri': 'uri-reference',
+ u'\u0441\u0441\u044b\u043b\u043a\u0430-\u043d\u0430-\u0437\u0430\u0433\u043b\u0430\u0432\u0438\u0435':
+ 'title-reference',
+ u'\u0441\u0441\u044b\u043b\u043a\u0430-\u043d\u0430-\u0441\u043d\u043e\u0441\u043a\u0443':
+ 'footnote-reference',
+ u'\u0446\u0438\u0442\u0430\u0442\u043d\u0430\u044f-\u0441\u0441\u044b\u043b\u043a\u0430':
+ 'citation-reference',
+ u'\u0446\u0435\u043b\u044c': 'target',
+ u'raw (translation required)': 'raw',}
+"""Mapping of Russian role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/sk.py b/docutils/parsers/rst/languages/sk.py
new file mode 100644
index 000000000..2ba849700
--- /dev/null
+++ b/docutils/parsers/rst/languages/sk.py
@@ -0,0 +1,89 @@
+# Author: Miroslav Vasko
+# Contact: zemiak@zoznam.sk
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Slovak-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ u'pozor': 'attention',
+ u'opatrne': 'caution',
+ u'nebezpe\xe8enstvo': 'danger',
+ u'chyba': 'error',
+ u'rada': 'hint',
+ u'd\xf4le\x9eit\xe9': 'important',
+ u'pozn\xe1mka': 'note',
+ u'tip (translation required)': 'tip',
+ u'varovanie': 'warning',
+ u'admonition (translation required)': 'admonition',
+ u'sidebar (translation required)': 'sidebar',
+ u't\xe9ma': 'topic',
+ u'blok-riadkov': 'line-block',
+ u'parsed-literal': 'parsed-literal',
+ u'rubric (translation required)': 'rubric',
+ u'epigraph (translation required)': 'epigraph',
+ u'highlights (translation required)': 'highlights',
+ u'pull-quote (translation required)': 'pull-quote',
+ u'compound (translation required)': 'compound',
+ #u'questions': 'questions',
+ #u'qa': 'questions',
+ #u'faq': 'questions',
+ u'table (translation required)': 'table',
+ u'csv-table (translation required)': 'csv-table',
+ u'list-table (translation required)': 'list-table',
+ u'meta': 'meta',
+ #u'imagemap': 'imagemap',
+ u'obr\xe1zok': 'image',
+ u'tvar': 'figure',
+ u'vlo\x9ei\x9d': 'include',
+ u'raw (translation required)': 'raw',
+ u'nahradi\x9d': 'replace',
+ u'unicode': 'unicode',
+ u'class (translation required)': 'class',
+ u'role (translation required)': 'role',
+ u'obsah': 'contents',
+ u'\xe8as\x9d': 'sectnum',
+ u'\xe8as\x9d-\xe8\xedslovanie': 'sectnum',
+ u'cie\xbeov\xe9-pozn\xe1mky': 'target-notes',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',
+ #u'footnotes': 'footnotes',
+ #u'citations': 'citations',
+ }
+"""Slovak name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ u'abbreviation (translation required)': 'abbreviation',
+ u'acronym (translation required)': 'acronym',
+ u'index (translation required)': 'index',
+ u'subscript (translation required)': 'subscript',
+ u'superscript (translation required)': 'superscript',
+ u'title-reference (translation required)': 'title-reference',
+ u'pep-reference (translation required)': 'pep-reference',
+ u'rfc-reference (translation required)': 'rfc-reference',
+ u'emphasis (translation required)': 'emphasis',
+ u'strong (translation required)': 'strong',
+ u'literal (translation required)': 'literal',
+ u'named-reference (translation required)': 'named-reference',
+ u'anonymous-reference (translation required)': 'anonymous-reference',
+ u'footnote-reference (translation required)': 'footnote-reference',
+ u'citation-reference (translation required)': 'citation-reference',
+ u'substitution-reference (translation required)': 'substitution-reference',
+ u'target (translation required)': 'target',
+ u'uri-reference (translation required)': 'uri-reference',
+ u'raw (translation required)': 'raw',}
+"""Mapping of Slovak role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/sv.py b/docutils/parsers/rst/languages/sv.py
new file mode 100644
index 000000000..d6f6fba46
--- /dev/null
+++ b/docutils/parsers/rst/languages/sv.py
@@ -0,0 +1,88 @@
+# Author: Adam Chodorowski
+# Contact: chodorowski@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Swedish language mappings for language-dependent features of reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ u'observera': 'attention',
+ u'caution (translation required)': 'caution',
+ u'fara': 'danger',
+ u'fel': 'error',
+ u'v\u00e4gledning': 'hint',
+ u'viktigt': 'important',
+ u'notera': 'note',
+ u'tips': 'tip',
+ u'varning': 'warning',
+ u'admonition (translation required)': 'admonition',
+ u'sidebar (translation required)': 'sidebar',
+ u'\u00e4mne': 'topic',
+ u'line-block (translation required)': 'line-block',
+ u'parsed-literal (translation required)': 'parsed-literal',
+ u'mellanrubrik': 'rubric',
+ u'epigraph (translation required)': 'epigraph',
+ u'highlights (translation required)': 'highlights',
+ u'pull-quote (translation required)': 'pull-quote',
+ u'compound (translation required)': 'compound',
+ # u'fr\u00e5gor': 'questions',
+ # NOTE: A bit long, but recommended by http://www.nada.kth.se/dataterm/:
+ # u'fr\u00e5gor-och-svar': 'questions',
+ # u'vanliga-fr\u00e5gor': 'questions',
+ u'table (translation required)': 'table',
+ u'csv-table (translation required)': 'csv-table',
+ u'list-table (translation required)': 'list-table',
+ u'meta': 'meta',
+ # u'bildkarta': 'imagemap', # FIXME: Translation might be too literal.
+ u'bild': 'image',
+ u'figur': 'figure',
+ u'inkludera': 'include',
+ u'r\u00e5': 'raw', # FIXME: Translation might be too literal.
+ u'ers\u00e4tt': 'replace',
+ u'unicode': 'unicode',
+ u'class (translation required)': 'class',
+ u'role (translation required)': 'role',
+ u'inneh\u00e5ll': 'contents',
+ u'sektionsnumrering': 'sectnum',
+ u'target-notes (translation required)': 'target-notes',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',
+ # u'fotnoter': 'footnotes',
+ # u'citeringar': 'citations',
+ }
+"""Swedish name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ u'abbreviation (translation required)': 'abbreviation',
+ u'acronym (translation required)': 'acronym',
+ u'index (translation required)': 'index',
+ u'subscript (translation required)': 'subscript',
+ u'superscript (translation required)': 'superscript',
+ u'title-reference (translation required)': 'title-reference',
+ u'pep-reference (translation required)': 'pep-reference',
+ u'rfc-reference (translation required)': 'rfc-reference',
+ u'emphasis (translation required)': 'emphasis',
+ u'strong (translation required)': 'strong',
+ u'literal (translation required)': 'literal',
+ u'named-reference (translation required)': 'named-reference',
+ u'anonymous-reference (translation required)': 'anonymous-reference',
+ u'footnote-reference (translation required)': 'footnote-reference',
+ u'citation-reference (translation required)': 'citation-reference',
+ u'substitution-reference (translation required)': 'substitution-reference',
+ u'target (translation required)': 'target',
+ u'uri-reference (translation required)': 'uri-reference',
+ u'r\u00e5': 'raw',}
+"""Mapping of Swedish role names to canonical role names for interpreted text.
+"""
diff --git a/docutils/parsers/rst/languages/zh_tw.py b/docutils/parsers/rst/languages/zh_tw.py
new file mode 100644
index 000000000..15f253114
--- /dev/null
+++ b/docutils/parsers/rst/languages/zh_tw.py
@@ -0,0 +1,102 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Traditional Chinese language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'attention (translation required)': 'attention',
+ 'caution (translation required)': 'caution',
+ 'danger (translation required)': 'danger',
+ 'error (translation required)': 'error',
+ 'hint (translation required)': 'hint',
+ 'important (translation required)': 'important',
+ 'note (translation required)': 'note',
+ 'tip (translation required)': 'tip',
+ 'warning (translation required)': 'warning',
+ 'admonition (translation required)': 'admonition',
+ 'sidebar (translation required)': 'sidebar',
+ 'topic (translation required)': 'topic',
+ 'line-block (translation required)': 'line-block',
+ 'parsed-literal (translation required)': 'parsed-literal',
+ 'rubric (translation required)': 'rubric',
+ 'epigraph (translation required)': 'epigraph',
+ 'highlights (translation required)': 'highlights',
+ 'pull-quote (translation required)': 'pull-quote',
+ 'compound (translation required)': 'compound',
+ #'questions (translation required)': 'questions',
+ 'table (translation required)': 'table',
+ 'csv-table (translation required)': 'csv-table',
+ 'list-table (translation required)': 'list-table',
+ #'qa (translation required)': 'questions',
+ #'faq (translation required)': 'questions',
+ 'meta (translation required)': 'meta',
+ #'imagemap (translation required)': 'imagemap',
+ 'image (translation required)': 'image',
+ 'figure (translation required)': 'figure',
+ 'include (translation required)': 'include',
+ 'raw (translation required)': 'raw',
+ 'replace (translation required)': 'replace',
+ 'unicode (translation required)': 'unicode',
+ 'class (translation required)': 'class',
+ 'role (translation required)': 'role',
+ 'contents (translation required)': 'contents',
+ 'sectnum (translation required)': 'sectnum',
+ 'section-numbering (translation required)': 'sectnum',
+ u'header (translation required)': 'header',
+ u'footer (translation required)': 'footer',
+ #'footnotes (translation required)': 'footnotes',
+ #'citations (translation required)': 'citations',
+ 'target-notes (translation required)': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Traditional Chinese name to registered (in directives/__init__.py)
+directive name mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'abbreviation (translation required)': 'abbreviation',
+ 'ab (translation required)': 'abbreviation',
+ 'acronym (translation required)': 'acronym',
+ 'ac (translation required)': 'acronym',
+ 'index (translation required)': 'index',
+ 'i (translation required)': 'index',
+ 'subscript (translation required)': 'subscript',
+ 'sub (translation required)': 'subscript',
+ 'superscript (translation required)': 'superscript',
+ 'sup (translation required)': 'superscript',
+ 'title-reference (translation required)': 'title-reference',
+ 'title (translation required)': 'title-reference',
+ 't (translation required)': 'title-reference',
+ 'pep-reference (translation required)': 'pep-reference',
+ 'pep (translation required)': 'pep-reference',
+ 'rfc-reference (translation required)': 'rfc-reference',
+ 'rfc (translation required)': 'rfc-reference',
+ 'emphasis (translation required)': 'emphasis',
+ 'strong (translation required)': 'strong',
+ 'literal (translation required)': 'literal',
+ 'named-reference (translation required)': 'named-reference',
+ 'anonymous-reference (translation required)': 'anonymous-reference',
+ 'footnote-reference (translation required)': 'footnote-reference',
+ 'citation-reference (translation required)': 'citation-reference',
+ 'substitution-reference (translation required)': 'substitution-reference',
+ 'target (translation required)': 'target',
+ 'uri-reference (translation required)': 'uri-reference',
+ 'uri (translation required)': 'uri-reference',
+ 'url (translation required)': 'uri-reference',
+ 'raw (translation required)': 'raw',}
+"""Mapping of Traditional Chinese role names to canonical role names for
+interpreted text."""
diff --git a/docutils/parsers/rst/roles.py b/docutils/parsers/rst/roles.py
new file mode 100644
index 000000000..554e1f441
--- /dev/null
+++ b/docutils/parsers/rst/roles.py
@@ -0,0 +1,347 @@
+# Author: Edward Loper
+# Contact: edloper@gradient.cis.upenn.edu
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+This module defines standard interpreted text role functions, a registry for
+interpreted text roles, and an API for adding to and retrieving from the
+registry.
+
+The interface for interpreted role functions is as follows::
+
+ def role_fn(name, rawtext, text, lineno, inliner,
+ options={}, content=[]):
+ code...
+
+ # Set function attributes for customization:
+ role_fn.options = ...
+ role_fn.content = ...
+
+Parameters:
+
+- ``name`` is the local name of the interpreted text role, the role name
+ actually used in the document.
+
+- ``rawtext`` is a string containing the entire interpreted text construct.
+ Return it as a ``problematic`` node linked to a system message if there is a
+ problem.
+
+- ``text`` is the interpreted text content, with backslash escapes converted
+ to nulls (``\x00``).
+
+- ``lineno`` is the line number where the interpreted text beings.
+
+- ``inliner`` is the Inliner object that called the role function.
+ It defines the following useful attributes: ``reporter``,
+ ``problematic``, ``memo``, ``parent``, ``document``.
+
+- ``options``: A dictionary of directive options for customization, to be
+ interpreted by the role function. Used for additional attributes for the
+ generated elements and other functionality.
+
+- ``content``: A list of strings, the directive content for customization
+ ("role" directive). To be interpreted by the role function.
+
+Function attributes for customization, interpreted by the "role" directive:
+
+- ``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 the `directives` module.
+
+ All role functions implicitly support the "class" option, unless disabled
+ with an explicit ``{'class': None}``.
+
+- ``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).
+
+Note that unlike directives, the "arguments" function attribute is not
+supported for role customization. Directive arguments are handled by the
+"role" directive itself.
+
+Interpreted role functions return a tuple of two values:
+
+- A list of nodes which will be inserted into the document tree at the
+ point where the interpreted role was encountered (can be an empty
+ list).
+
+- A list of system messages, which will be inserted into the document tree
+ immediately after the end of the current inline block (can also be empty).
+"""
+
+__docformat__ = 'reStructuredText'
+
+from docutils import nodes, utils
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.languages import en as _fallback_language_module
+
+DEFAULT_INTERPRETED_ROLE = 'title-reference'
+"""
+The canonical name of the default interpreted role. This role is used
+when no role is specified for a piece of interpreted text.
+"""
+
+_role_registry = {}
+"""Mapping of canonical role names to role functions. Language-dependent role
+names are defined in the ``language`` subpackage."""
+
+_roles = {}
+"""Mapping of local or language-dependent interpreted text role names to role
+functions."""
+
+def role(role_name, language_module, lineno, reporter):
+ """
+ Locate and return a role function from its language-dependent name, along
+ with a list of system messages. If the role is not found in the current
+ language, check English. Return a 2-tuple: role function (``None`` if the
+ named role cannot be found) and a list of system messages.
+ """
+ normname = role_name.lower()
+ messages = []
+ msg_text = []
+
+ if _roles.has_key(normname):
+ return _roles[normname], messages
+
+ if role_name:
+ canonicalname = None
+ try:
+ canonicalname = language_module.roles[normname]
+ except AttributeError, error:
+ msg_text.append('Problem retrieving role entry from language '
+ 'module %r: %s.' % (language_module, error))
+ except KeyError:
+ msg_text.append('No role entry for "%s" in module "%s".'
+ % (role_name, language_module.__name__))
+ else:
+ canonicalname = DEFAULT_INTERPRETED_ROLE
+
+ # If we didn't find it, try English as a fallback.
+ if not canonicalname:
+ try:
+ canonicalname = _fallback_language_module.roles[normname]
+ msg_text.append('Using English fallback for role "%s".'
+ % role_name)
+ except KeyError:
+ msg_text.append('Trying "%s" as canonical role name.'
+ % role_name)
+ # The canonical name should be an English name, but just in case:
+ canonicalname = normname
+
+ # Collect any messages that we generated.
+ if msg_text:
+ message = reporter.info('\n'.join(msg_text), line=lineno)
+ messages.append(message)
+
+ # Look the role up in the registry, and return it.
+ if _role_registry.has_key(canonicalname):
+ role_fn = _role_registry[canonicalname]
+ register_local_role(normname, role_fn)
+ return role_fn, messages
+ else:
+ return None, messages # Error message will be generated by caller.
+
+def register_canonical_role(name, role_fn):
+ """
+ Register an interpreted text role by its canonical name.
+
+ :Parameters:
+ - `name`: The canonical name of the interpreted role.
+ - `role_fn`: The role function. See the module docstring.
+ """
+ set_implicit_options(role_fn)
+ _role_registry[name] = role_fn
+
+def register_local_role(name, role_fn):
+ """
+ Register an interpreted text role by its local or language-dependent name.
+
+ :Parameters:
+ - `name`: The local or language-dependent name of the interpreted role.
+ - `role_fn`: The role function. See the module docstring.
+ """
+ set_implicit_options(role_fn)
+ _roles[name] = role_fn
+
+def set_implicit_options(role_fn):
+ """
+ Add customization options to role functions, unless explicitly set or
+ disabled.
+ """
+ if not hasattr(role_fn, 'options') or role_fn.options is None:
+ role_fn.options = {'class': directives.class_option}
+ elif not role_fn.options.has_key('class'):
+ role_fn.options['class'] = directives.class_option
+
+def register_generic_role(canonical_name, node_class):
+ """For roles which simply wrap a given `node_class` around the text."""
+ role = GenericRole(canonical_name, node_class)
+ register_canonical_role(canonical_name, role)
+
+
+class GenericRole:
+
+ """
+ Generic interpreted text role, where the interpreted text is simply
+ wrapped with the provided node class.
+ """
+
+ def __init__(self, role_name, node_class):
+ self.name = role_name
+ self.node_class = node_class
+
+ def __call__(self, role, rawtext, text, lineno, inliner,
+ options={}, content=[]):
+ set_classes(options)
+ return [self.node_class(rawtext, utils.unescape(text), **options)], []
+
+
+class CustomRole:
+
+ """
+ Wrapper for custom interpreted text roles.
+ """
+
+ def __init__(self, role_name, base_role, options={}, content=[]):
+ self.name = role_name
+ self.base_role = base_role
+ self.options = None
+ if hasattr(base_role, 'options'):
+ self.options = base_role.options
+ self.content = None
+ if hasattr(base_role, 'content'):
+ self.content = base_role.content
+ self.supplied_options = options
+ self.supplied_content = content
+
+ def __call__(self, role, rawtext, text, lineno, inliner,
+ options={}, content=[]):
+ opts = self.supplied_options.copy()
+ opts.update(options)
+ cont = list(self.supplied_content)
+ if cont and content:
+ cont += '\n'
+ cont.extend(content)
+ return self.base_role(role, rawtext, text, lineno, inliner,
+ options=opts, content=cont)
+
+
+def generic_custom_role(role, rawtext, text, lineno, inliner,
+ options={}, content=[]):
+ """"""
+ # Once nested inline markup is implemented, this and other methods should
+ # recursively call inliner.nested_parse().
+ set_classes(options)
+ return [nodes.inline(rawtext, utils.unescape(text), **options)], []
+
+generic_custom_role.options = {'class': directives.class_option}
+
+
+######################################################################
+# Define and register the standard roles:
+######################################################################
+
+register_generic_role('abbreviation', nodes.abbreviation)
+register_generic_role('acronym', nodes.acronym)
+register_generic_role('emphasis', nodes.emphasis)
+register_generic_role('literal', nodes.literal)
+register_generic_role('strong', nodes.strong)
+register_generic_role('subscript', nodes.subscript)
+register_generic_role('superscript', nodes.superscript)
+register_generic_role('title-reference', nodes.title_reference)
+
+def pep_reference_role(role, rawtext, text, lineno, inliner,
+ options={}, content=[]):
+ try:
+ pepnum = int(text)
+ if pepnum < 0 or pepnum > 9999:
+ raise ValueError
+ except ValueError:
+ msg = inliner.reporter.error(
+ 'PEP number must be a number from 0 to 9999; "%s" is invalid.'
+ % text, line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+ # Base URL mainly used by inliner.pep_reference; so this is correct:
+ ref = inliner.document.settings.pep_base_url + inliner.pep_url % pepnum
+ set_classes(options)
+ return [nodes.reference(rawtext, 'PEP ' + utils.unescape(text), refuri=ref,
+ **options)], []
+
+register_canonical_role('pep-reference', pep_reference_role)
+
+def rfc_reference_role(role, rawtext, text, lineno, inliner,
+ options={}, content=[]):
+ try:
+ rfcnum = int(text)
+ if rfcnum <= 0:
+ raise ValueError
+ except ValueError:
+ msg = inliner.reporter.error(
+ 'RFC number must be a number greater than or equal to 1; '
+ '"%s" is invalid.' % text, line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+ # Base URL mainly used by inliner.rfc_reference, so this is correct:
+ ref = inliner.document.settings.rfc_base_url + inliner.rfc_url % rfcnum
+ set_classes(options)
+ node = nodes.reference(rawtext, 'RFC ' + utils.unescape(text), refuri=ref,
+ **options)
+ return [node], []
+
+register_canonical_role('rfc-reference', rfc_reference_role)
+
+def raw_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
+ if not options.has_key('format'):
+ msg = inliner.reporter.error(
+ 'No format (Writer name) is associated with this role: "%s".\n'
+ 'The "raw" role cannot be used directly.\n'
+ 'Instead, use the "role" directive to create a new role with '
+ 'an associated format.' % role, line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+ set_classes(options)
+ node = nodes.raw(rawtext, utils.unescape(text, 1), **options)
+ return [node], []
+
+raw_role.options = {'format': directives.unchanged}
+
+register_canonical_role('raw', raw_role)
+
+
+######################################################################
+# Register roles that are currently unimplemented.
+######################################################################
+
+def unimplemented_role(role, rawtext, text, lineno, inliner, attributes={}):
+ msg = inliner.reporter.error(
+ 'Interpreted text role "%s" not implemented.' % role, line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+
+register_canonical_role('index', unimplemented_role)
+register_canonical_role('named-reference', unimplemented_role)
+register_canonical_role('anonymous-reference', unimplemented_role)
+register_canonical_role('uri-reference', unimplemented_role)
+register_canonical_role('footnote-reference', unimplemented_role)
+register_canonical_role('citation-reference', unimplemented_role)
+register_canonical_role('substitution-reference', unimplemented_role)
+register_canonical_role('target', unimplemented_role)
+
+# This should remain unimplemented, for testing purposes:
+register_canonical_role('restructuredtext-unimplemented-role',
+ unimplemented_role)
+
+
+def set_classes(options):
+ """
+ Auxiliary function to set options['classes'] and delete
+ options['class'].
+ """
+ if options.has_key('class'):
+ assert not options.has_key('classes')
+ options['classes'] = options['class']
+ del options['class']
diff --git a/docutils/parsers/rst/states.py b/docutils/parsers/rst/states.py
new file mode 100644
index 000000000..1ee9dc6f2
--- /dev/null
+++ b/docutils/parsers/rst/states.py
@@ -0,0 +1,2922 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+This is the ``docutils.parsers.restructuredtext.states`` module, the core of
+the reStructuredText parser. It defines the following:
+
+:Classes:
+ - `RSTStateMachine`: reStructuredText parser's entry point.
+ - `NestedStateMachine`: recursive StateMachine.
+ - `RSTState`: reStructuredText State superclass.
+ - `Inliner`: For parsing inline markup.
+ - `Body`: Generic classifier of the first line of a block.
+ - `SpecializedBody`: Superclass for compound element members.
+ - `BulletList`: Second and subsequent bullet_list list_items
+ - `DefinitionList`: Second+ definition_list_items.
+ - `EnumeratedList`: Second+ enumerated_list list_items.
+ - `FieldList`: Second+ fields.
+ - `OptionList`: Second+ option_list_items.
+ - `RFC2822List`: Second+ RFC2822-style fields.
+ - `ExtensionOptions`: Parses directive option fields.
+ - `Explicit`: Second+ explicit markup constructs.
+ - `SubstitutionDef`: For embedded directives in substitution definitions.
+ - `Text`: Classifier of second line of a text block.
+ - `SpecializedText`: Superclass for continuation lines of Text-variants.
+ - `Definition`: Second line of potential definition_list_item.
+ - `Line`: Second line of overlined section title or transition marker.
+ - `Struct`: An auxiliary collection class.
+
+:Exception classes:
+ - `MarkupError`
+ - `ParserError`
+ - `MarkupMismatch`
+
+:Functions:
+ - `escape2null()`: Return a string, escape-backslashes converted to nulls.
+ - `unescape()`: Return a string, nulls removed or restored to backslashes.
+
+:Attributes:
+ - `state_classes`: set of State classes used with `RSTStateMachine`.
+
+Parser Overview
+===============
+
+The reStructuredText parser is implemented as a recursive state machine,
+examining its input one line at a time. To understand how the parser works,
+please first become familiar with the `docutils.statemachine` module. In the
+description below, references are made to classes defined in this module;
+please see the individual classes for details.
+
+Parsing proceeds as follows:
+
+1. The state machine examines each line of input, checking each of the
+ transition patterns of the state `Body`, in order, looking for a match.
+ The implicit transitions (blank lines and indentation) are checked before
+ any others. The 'text' transition is a catch-all (matches anything).
+
+2. The method associated with the matched transition pattern is called.
+
+ A. Some transition methods are self-contained, appending elements to the
+ document tree (`Body.doctest` parses a doctest block). The parser's
+ current line index is advanced to the end of the element, and parsing
+ continues with step 1.
+
+ B. Other transition methods trigger the creation of a nested state machine,
+ whose job is to parse a compound construct ('indent' does a block quote,
+ 'bullet' does a bullet list, 'overline' does a section [first checking
+ for a valid section header], etc.).
+
+ - In the case of lists and explicit markup, a one-off state machine is
+ created and run to parse contents of the first item.
+
+ - A new state machine is created and its initial state is set to the
+ appropriate specialized state (`BulletList` in the case of the
+ 'bullet' transition; see `SpecializedBody` for more detail). This
+ state machine is run to parse the compound element (or series of
+ explicit markup elements), and returns as soon as a non-member element
+ is encountered. For example, the `BulletList` state machine ends as
+ soon as it encounters an element which is not a list item of that
+ bullet list. The optional omission of inter-element blank lines is
+ enabled by this nested state machine.
+
+ - The current line index is advanced to the end of the elements parsed,
+ and parsing continues with step 1.
+
+ C. The result of the 'text' transition depends on the next line of text.
+ The current state is changed to `Text`, under which the second line is
+ examined. If the second line is:
+
+ - Indented: The element is a definition list item, and parsing proceeds
+ similarly to step 2.B, using the `DefinitionList` state.
+
+ - A line of uniform punctuation characters: The element is a section
+ header; again, parsing proceeds as in step 2.B, and `Body` is still
+ used.
+
+ - Anything else: The element is a paragraph, which is examined for
+ inline markup and appended to the parent element. Processing
+ continues with step 1.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import sys
+import re
+import roman
+from types import TupleType
+from docutils import nodes, statemachine, utils, urischemes
+from docutils import ApplicationError, DataError
+from docutils.statemachine import StateMachineWS, StateWS
+from docutils.nodes import fully_normalize_name as normalize_name
+from docutils.nodes import whitespace_normalize_name
+from docutils.utils import escape2null, unescape
+from docutils.parsers.rst import directives, languages, tableparser, roles
+from docutils.parsers.rst.languages import en as _fallback_language_module
+
+
+class MarkupError(DataError): pass
+class UnknownInterpretedRoleError(DataError): pass
+class InterpretedRoleNotImplementedError(DataError): pass
+class ParserError(ApplicationError): pass
+class MarkupMismatch(Exception): pass
+
+
+class Struct:
+
+ """Stores data attributes for dotted-attribute access."""
+
+ def __init__(self, **keywordargs):
+ self.__dict__.update(keywordargs)
+
+
+class RSTStateMachine(StateMachineWS):
+
+ """
+ reStructuredText's master StateMachine.
+
+ The entry point to reStructuredText parsing is the `run()` method.
+ """
+
+ def run(self, input_lines, document, input_offset=0, match_titles=1,
+ inliner=None):
+ """
+ Parse `input_lines` and modify the `document` node in place.
+
+ Extend `StateMachineWS.run()`: set up parse-global data and
+ run the StateMachine.
+ """
+ self.language = languages.get_language(
+ document.settings.language_code)
+ self.match_titles = match_titles
+ if inliner is None:
+ inliner = Inliner()
+ inliner.init_customizations(document.settings)
+ self.memo = Struct(document=document,
+ reporter=document.reporter,
+ language=self.language,
+ title_styles=[],
+ section_level=0,
+ section_bubble_up_kludge=0,
+ inliner=inliner)
+ self.document = document
+ self.attach_observer(document.note_source)
+ self.reporter = self.memo.reporter
+ self.node = document
+ results = StateMachineWS.run(self, input_lines, input_offset,
+ input_source=document['source'])
+ assert results == [], 'RSTStateMachine.run() results should be empty!'
+ self.node = self.memo = None # remove unneeded references
+
+
+class NestedStateMachine(StateMachineWS):
+
+ """
+ StateMachine run from within other StateMachine runs, to parse nested
+ document structures.
+ """
+
+ def run(self, input_lines, input_offset, memo, node, match_titles=1):
+ """
+ Parse `input_lines` and populate a `docutils.nodes.document` instance.
+
+ Extend `StateMachineWS.run()`: set up document-wide data.
+ """
+ self.match_titles = match_titles
+ self.memo = memo
+ self.document = memo.document
+ self.attach_observer(self.document.note_source)
+ self.reporter = memo.reporter
+ self.language = memo.language
+ self.node = node
+ results = StateMachineWS.run(self, input_lines, input_offset)
+ assert results == [], ('NestedStateMachine.run() results should be '
+ 'empty!')
+ return results
+
+
+class RSTState(StateWS):
+
+ """
+ reStructuredText State superclass.
+
+ Contains methods used by all State subclasses.
+ """
+
+ nested_sm = NestedStateMachine
+
+ def __init__(self, state_machine, debug=0):
+ self.nested_sm_kwargs = {'state_classes': state_classes,
+ 'initial_state': 'Body'}
+ StateWS.__init__(self, state_machine, debug)
+
+ def runtime_init(self):
+ StateWS.runtime_init(self)
+ memo = self.state_machine.memo
+ self.memo = memo
+ self.reporter = memo.reporter
+ self.inliner = memo.inliner
+ self.document = memo.document
+ self.parent = self.state_machine.node
+
+ def goto_line(self, abs_line_offset):
+ """
+ Jump to input line `abs_line_offset`, ignoring jumps past the end.
+ """
+ try:
+ self.state_machine.goto_line(abs_line_offset)
+ except EOFError:
+ pass
+
+ def no_match(self, context, transitions):
+ """
+ Override `StateWS.no_match` to generate a system message.
+
+ This code should never be run.
+ """
+ self.reporter.severe(
+ 'Internal error: no transition pattern match. State: "%s"; '
+ 'transitions: %s; context: %s; current line: %r.'
+ % (self.__class__.__name__, transitions, context,
+ self.state_machine.line),
+ line=self.state_machine.abs_line_number())
+ return context, None, []
+
+ def bof(self, context):
+ """Called at beginning of file."""
+ return [], []
+
+ def nested_parse(self, block, input_offset, node, match_titles=0,
+ state_machine_class=None, state_machine_kwargs=None):
+ """
+ Create a new StateMachine rooted at `node` and run it over the input
+ `block`.
+ """
+ if state_machine_class is None:
+ state_machine_class = self.nested_sm
+ if state_machine_kwargs is None:
+ state_machine_kwargs = self.nested_sm_kwargs
+ block_length = len(block)
+ state_machine = state_machine_class(debug=self.debug,
+ **state_machine_kwargs)
+ state_machine.run(block, input_offset, memo=self.memo,
+ node=node, match_titles=match_titles)
+ state_machine.unlink()
+ new_offset = state_machine.abs_line_offset()
+ # No `block.parent` implies disconnected -- lines aren't in sync:
+ if block.parent and (len(block) - block_length) != 0:
+ # Adjustment for block if modified in nested parse:
+ self.state_machine.next_line(len(block) - block_length)
+ return new_offset
+
+ def nested_list_parse(self, block, input_offset, node, initial_state,
+ blank_finish,
+ blank_finish_state=None,
+ extra_settings={},
+ match_titles=0,
+ state_machine_class=None,
+ state_machine_kwargs=None):
+ """
+ Create a new StateMachine rooted at `node` and run it over the input
+ `block`. Also keep track of optional intermediate blank lines and the
+ required final one.
+ """
+ if state_machine_class is None:
+ state_machine_class = self.nested_sm
+ if state_machine_kwargs is None:
+ state_machine_kwargs = self.nested_sm_kwargs.copy()
+ state_machine_kwargs['initial_state'] = initial_state
+ state_machine = state_machine_class(debug=self.debug,
+ **state_machine_kwargs)
+ if blank_finish_state is None:
+ blank_finish_state = initial_state
+ state_machine.states[blank_finish_state].blank_finish = blank_finish
+ for key, value in extra_settings.items():
+ setattr(state_machine.states[initial_state], key, value)
+ state_machine.run(block, input_offset, memo=self.memo,
+ node=node, match_titles=match_titles)
+ blank_finish = state_machine.states[blank_finish_state].blank_finish
+ state_machine.unlink()
+ return state_machine.abs_line_offset(), blank_finish
+
+ def section(self, title, source, style, lineno, messages):
+ """Check for a valid subsection and create one if it checks out."""
+ if self.check_subsection(source, style, lineno):
+ self.new_subsection(title, lineno, messages)
+
+ def check_subsection(self, source, style, lineno):
+ """
+ Check for a valid subsection header. Return 1 (true) or None (false).
+
+ When a new section is reached that isn't a subsection of the current
+ section, back up the line count (use ``previous_line(-x)``), then
+ ``raise EOFError``. The current StateMachine will finish, then the
+ calling StateMachine can re-examine the title. This will work its way
+ back up the calling chain until the correct section level isreached.
+
+ @@@ Alternative: Evaluate the title, store the title info & level, and
+ back up the chain until that level is reached. Store in memo? Or
+ return in results?
+
+ :Exception: `EOFError` when a sibling or supersection encountered.
+ """
+ memo = self.memo
+ title_styles = memo.title_styles
+ mylevel = memo.section_level
+ try: # check for existing title style
+ level = title_styles.index(style) + 1
+ except ValueError: # new title style
+ if len(title_styles) == memo.section_level: # new subsection
+ title_styles.append(style)
+ return 1
+ else: # not at lowest level
+ self.parent += self.title_inconsistent(source, lineno)
+ return None
+ if level <= mylevel: # sibling or supersection
+ memo.section_level = level # bubble up to parent section
+ if len(style) == 2:
+ memo.section_bubble_up_kludge = 1
+ # back up 2 lines for underline title, 3 for overline title
+ self.state_machine.previous_line(len(style) + 1)
+ raise EOFError # let parent section re-evaluate
+ if level == mylevel + 1: # immediate subsection
+ return 1
+ else: # invalid subsection
+ self.parent += self.title_inconsistent(source, lineno)
+ return None
+
+ def title_inconsistent(self, sourcetext, lineno):
+ error = self.reporter.severe(
+ 'Title level inconsistent:', nodes.literal_block('', sourcetext),
+ line=lineno)
+ return error
+
+ def new_subsection(self, title, lineno, messages):
+ """Append new subsection to document tree. On return, check level."""
+ memo = self.memo
+ mylevel = memo.section_level
+ memo.section_level += 1
+ section_node = nodes.section()
+ self.parent += section_node
+ textnodes, title_messages = self.inline_text(title, lineno)
+ titlenode = nodes.title(title, '', *textnodes)
+ name = normalize_name(titlenode.astext())
+ section_node['names'].append(name)
+ section_node += titlenode
+ section_node += messages
+ section_node += title_messages
+ self.document.note_implicit_target(section_node, section_node)
+ offset = self.state_machine.line_offset + 1
+ absoffset = self.state_machine.abs_line_offset() + 1
+ newabsoffset = self.nested_parse(
+ self.state_machine.input_lines[offset:], input_offset=absoffset,
+ node=section_node, match_titles=1)
+ self.goto_line(newabsoffset)
+ if memo.section_level <= mylevel: # can't handle next section?
+ raise EOFError # bubble up to supersection
+ # reset section_level; next pass will detect it properly
+ memo.section_level = mylevel
+
+ def paragraph(self, lines, lineno):
+ """
+ Return a list (paragraph & messages) & a boolean: literal_block next?
+ """
+ data = '\n'.join(lines).rstrip()
+ if data[-2:] == '::':
+ if len(data) == 2:
+ return [], 1
+ elif data[-3] in ' \n':
+ text = data[:-3].rstrip()
+ else:
+ text = data[:-1]
+ literalnext = 1
+ else:
+ text = data
+ literalnext = 0
+ textnodes, messages = self.inline_text(text, lineno)
+ p = nodes.paragraph(data, '', *textnodes)
+ p.line = lineno
+ return [p] + messages, literalnext
+
+ def inline_text(self, text, lineno):
+ """
+ Return 2 lists: nodes (text and inline elements), and system_messages.
+ """
+ return self.inliner.parse(text, lineno, self.memo, self.parent)
+
+ def unindent_warning(self, node_name):
+ return self.reporter.warning(
+ '%s ends without a blank line; unexpected unindent.' % node_name,
+ line=(self.state_machine.abs_line_number() + 1))
+
+
+def build_regexp(definition, compile=1):
+ """
+ Build, compile and return a regular expression based on `definition`.
+
+ :Parameter: `definition`: a 4-tuple (group name, prefix, suffix, parts),
+ where "parts" is a list of regular expressions and/or regular
+ expression definitions to be joined into an or-group.
+ """
+ name, prefix, suffix, parts = definition
+ part_strings = []
+ for part in parts:
+ if type(part) is TupleType:
+ part_strings.append(build_regexp(part, None))
+ else:
+ part_strings.append(part)
+ or_group = '|'.join(part_strings)
+ regexp = '%(prefix)s(?P<%(name)s>%(or_group)s)%(suffix)s' % locals()
+ if compile:
+ return re.compile(regexp, re.UNICODE)
+ else:
+ return regexp
+
+
+class Inliner:
+
+ """
+ Parse inline markup; call the `parse()` method.
+ """
+
+ def __init__(self, roles=None):
+ """
+ `roles` is a mapping of canonical role name to role function or bound
+ method, which enables additional interpreted text roles.
+ """
+
+ self.implicit_dispatch = [(self.patterns.uri, self.standalone_uri),]
+ """List of (pattern, bound method) tuples, used by
+ `self.implicit_inline`."""
+
+ def init_customizations(self, settings):
+ """Setting-based customizations; run when parsing begins."""
+ if settings.pep_references:
+ self.implicit_dispatch.append((self.patterns.pep,
+ self.pep_reference))
+ if settings.rfc_references:
+ self.implicit_dispatch.append((self.patterns.rfc,
+ self.rfc_reference))
+
+ def parse(self, text, lineno, memo, parent):
+ # Needs to be refactored for nested inline markup.
+ # Add nested_parse() method?
+ """
+ Return 2 lists: nodes (text and inline elements), and system_messages.
+
+ Using `self.patterns.initial`, a pattern which matches start-strings
+ (emphasis, strong, interpreted, phrase reference, literal,
+ substitution reference, and inline target) and complete constructs
+ (simple reference, footnote reference), search for a candidate. When
+ one is found, check for validity (e.g., not a quoted '*' character).
+ If valid, search for the corresponding end string if applicable, and
+ check it for validity. If not found or invalid, generate a warning
+ and ignore the start-string. Implicit inline markup (e.g. standalone
+ URIs) is found last.
+ """
+ self.reporter = memo.reporter
+ self.document = memo.document
+ self.language = memo.language
+ self.parent = parent
+ pattern_search = self.patterns.initial.search
+ dispatch = self.dispatch
+ remaining = escape2null(text)
+ processed = []
+ unprocessed = []
+ messages = []
+ while remaining:
+ match = pattern_search(remaining)
+ if match:
+ groups = match.groupdict()
+ method = dispatch[groups['start'] or groups['backquote']
+ or groups['refend'] or groups['fnend']]
+ before, inlines, remaining, sysmessages = method(self, match,
+ lineno)
+ unprocessed.append(before)
+ messages += sysmessages
+ if inlines:
+ processed += self.implicit_inline(''.join(unprocessed),
+ lineno)
+ processed += inlines
+ unprocessed = []
+ else:
+ break
+ remaining = ''.join(unprocessed) + remaining
+ if remaining:
+ processed += self.implicit_inline(remaining, lineno)
+ return processed, messages
+
+ openers = '\'"([{<'
+ closers = '\'")]}>'
+ start_string_prefix = (r'((?<=^)|(?<=[-/: \n%s]))' % re.escape(openers))
+ end_string_suffix = (r'((?=$)|(?=[-/:.,;!? \n\x00%s]))'
+ % re.escape(closers))
+ non_whitespace_before = r'(?<![ \n])'
+ non_whitespace_escape_before = r'(?<![ \n\x00])'
+ non_whitespace_after = r'(?![ \n])'
+ # Alphanumerics with isolated internal [-._] chars (i.e. not 2 together):
+ simplename = r'(?:(?!_)\w)+(?:[-._](?:(?!_)\w)+)*'
+ # Valid URI characters (see RFC 2396 & RFC 2732);
+ # final \x00 allows backslash escapes in URIs:
+ uric = r"""[-_.!~*'()[\];/:@&=+$,%a-zA-Z0-9\x00]"""
+ # Delimiter indicating the end of a URI (not part of the URI):
+ uri_end_delim = r"""[>]"""
+ # Last URI character; same as uric but no punctuation:
+ urilast = r"""[_~*/=+a-zA-Z0-9]"""
+ # End of a URI (either 'urilast' or 'uric followed by a
+ # uri_end_delim'):
+ uri_end = r"""(?:%(urilast)s|%(uric)s(?=%(uri_end_delim)s))""" % locals()
+ emailc = r"""[-_!~*'{|}/#?^`&=+$%a-zA-Z0-9\x00]"""
+ email_pattern = r"""
+ %(emailc)s+(?:\.%(emailc)s+)* # name
+ (?<!\x00)@ # at
+ %(emailc)s+(?:\.%(emailc)s*)* # host
+ %(uri_end)s # final URI char
+ """
+ parts = ('initial_inline', start_string_prefix, '',
+ [('start', '', non_whitespace_after, # simple start-strings
+ [r'\*\*', # strong
+ r'\*(?!\*)', # emphasis but not strong
+ r'``', # literal
+ r'_`', # inline internal target
+ r'\|(?!\|)'] # substitution reference
+ ),
+ ('whole', '', end_string_suffix, # whole constructs
+ [# reference name & end-string
+ r'(?P<refname>%s)(?P<refend>__?)' % simplename,
+ ('footnotelabel', r'\[', r'(?P<fnend>\]_)',
+ [r'[0-9]+', # manually numbered
+ r'\#(%s)?' % simplename, # auto-numbered (w/ label?)
+ r'\*', # auto-symbol
+ r'(?P<citationlabel>%s)' % simplename] # citation reference
+ )
+ ]
+ ),
+ ('backquote', # interpreted text or phrase reference
+ '(?P<role>(:%s:)?)' % simplename, # optional role
+ non_whitespace_after,
+ ['`(?!`)'] # but not literal
+ )
+ ]
+ )
+ patterns = Struct(
+ initial=build_regexp(parts),
+ emphasis=re.compile(non_whitespace_escape_before
+ + r'(\*)' + end_string_suffix),
+ strong=re.compile(non_whitespace_escape_before
+ + r'(\*\*)' + end_string_suffix),
+ interpreted_or_phrase_ref=re.compile(
+ r"""
+ %(non_whitespace_escape_before)s
+ (
+ `
+ (?P<suffix>
+ (?P<role>:%(simplename)s:)?
+ (?P<refend>__?)?
+ )
+ )
+ %(end_string_suffix)s
+ """ % locals(), re.VERBOSE | re.UNICODE),
+ embedded_uri=re.compile(
+ r"""
+ (
+ (?:[ \n]+|^) # spaces or beginning of line/string
+ < # open bracket
+ %(non_whitespace_after)s
+ ([^<>\x00]+) # anything but angle brackets & nulls
+ %(non_whitespace_before)s
+ > # close bracket w/o whitespace before
+ )
+ $ # end of string
+ """ % locals(), re.VERBOSE),
+ literal=re.compile(non_whitespace_before + '(``)'
+ + end_string_suffix),
+ target=re.compile(non_whitespace_escape_before
+ + r'(`)' + end_string_suffix),
+ substitution_ref=re.compile(non_whitespace_escape_before
+ + r'(\|_{0,2})'
+ + end_string_suffix),
+ email=re.compile(email_pattern % locals() + '$', re.VERBOSE),
+ uri=re.compile(
+ (r"""
+ %(start_string_prefix)s
+ (?P<whole>
+ (?P<absolute> # absolute URI
+ (?P<scheme> # scheme (http, ftp, mailto)
+ [a-zA-Z][a-zA-Z0-9.+-]*
+ )
+ :
+ (
+ ( # either:
+ (//?)? # hierarchical URI
+ %(uric)s* # URI characters
+ %(uri_end)s # final URI char
+ )
+ ( # optional query
+ \?%(uric)s*
+ %(uri_end)s
+ )?
+ ( # optional fragment
+ \#%(uric)s*
+ %(uri_end)s
+ )?
+ )
+ )
+ | # *OR*
+ (?P<email> # email address
+ """ + email_pattern + r"""
+ )
+ )
+ %(end_string_suffix)s
+ """) % locals(), re.VERBOSE),
+ pep=re.compile(
+ r"""
+ %(start_string_prefix)s
+ (
+ (pep-(?P<pepnum1>\d+)(.txt)?) # reference to source file
+ |
+ (PEP\s+(?P<pepnum2>\d+)) # reference by name
+ )
+ %(end_string_suffix)s""" % locals(), re.VERBOSE),
+ rfc=re.compile(
+ r"""
+ %(start_string_prefix)s
+ (RFC(-|\s+)?(?P<rfcnum>\d+))
+ %(end_string_suffix)s""" % locals(), re.VERBOSE))
+
+ def quoted_start(self, match):
+ """Return 1 if inline markup start-string is 'quoted', 0 if not."""
+ string = match.string
+ start = match.start()
+ end = match.end()
+ if start == 0: # start-string at beginning of text
+ return 0
+ prestart = string[start - 1]
+ try:
+ poststart = string[end]
+ if self.openers.index(prestart) \
+ == self.closers.index(poststart): # quoted
+ return 1
+ except IndexError: # start-string at end of text
+ return 1
+ except ValueError: # not quoted
+ pass
+ return 0
+
+ def inline_obj(self, match, lineno, end_pattern, nodeclass,
+ restore_backslashes=0):
+ string = match.string
+ matchstart = match.start('start')
+ matchend = match.end('start')
+ if self.quoted_start(match):
+ return (string[:matchend], [], string[matchend:], [], '')
+ endmatch = end_pattern.search(string[matchend:])
+ if endmatch and endmatch.start(1): # 1 or more chars
+ text = unescape(endmatch.string[:endmatch.start(1)],
+ restore_backslashes)
+ textend = matchend + endmatch.end(1)
+ rawsource = unescape(string[matchstart:textend], 1)
+ return (string[:matchstart], [nodeclass(rawsource, text)],
+ string[textend:], [], endmatch.group(1))
+ msg = self.reporter.warning(
+ 'Inline %s start-string without end-string.'
+ % nodeclass.__name__, line=lineno)
+ text = unescape(string[matchstart:matchend], 1)
+ rawsource = unescape(string[matchstart:matchend], 1)
+ prb = self.problematic(text, rawsource, msg)
+ return string[:matchstart], [prb], string[matchend:], [msg], ''
+
+ def problematic(self, text, rawsource, message):
+ msgid = self.document.set_id(message, self.parent)
+ problematic = nodes.problematic(rawsource, text, refid=msgid)
+ prbid = self.document.set_id(problematic)
+ message.add_backref(prbid)
+ return problematic
+
+ def emphasis(self, match, lineno):
+ before, inlines, remaining, sysmessages, endstring = self.inline_obj(
+ match, lineno, self.patterns.emphasis, nodes.emphasis)
+ return before, inlines, remaining, sysmessages
+
+ def strong(self, match, lineno):
+ before, inlines, remaining, sysmessages, endstring = self.inline_obj(
+ match, lineno, self.patterns.strong, nodes.strong)
+ return before, inlines, remaining, sysmessages
+
+ def interpreted_or_phrase_ref(self, match, lineno):
+ end_pattern = self.patterns.interpreted_or_phrase_ref
+ string = match.string
+ matchstart = match.start('backquote')
+ matchend = match.end('backquote')
+ rolestart = match.start('role')
+ role = match.group('role')
+ position = ''
+ if role:
+ role = role[1:-1]
+ position = 'prefix'
+ elif self.quoted_start(match):
+ return (string[:matchend], [], string[matchend:], [])
+ endmatch = end_pattern.search(string[matchend:])
+ if endmatch and endmatch.start(1): # 1 or more chars
+ textend = matchend + endmatch.end()
+ if endmatch.group('role'):
+ if role:
+ msg = self.reporter.warning(
+ 'Multiple roles in interpreted text (both '
+ 'prefix and suffix present; only one allowed).',
+ line=lineno)
+ text = unescape(string[rolestart:textend], 1)
+ prb = self.problematic(text, text, msg)
+ return string[:rolestart], [prb], string[textend:], [msg]
+ role = endmatch.group('suffix')[1:-1]
+ position = 'suffix'
+ escaped = endmatch.string[:endmatch.start(1)]
+ rawsource = unescape(string[matchstart:textend], 1)
+ if rawsource[-1:] == '_':
+ if role:
+ msg = self.reporter.warning(
+ 'Mismatch: both interpreted text role %s and '
+ 'reference suffix.' % position, line=lineno)
+ text = unescape(string[rolestart:textend], 1)
+ prb = self.problematic(text, text, msg)
+ return string[:rolestart], [prb], string[textend:], [msg]
+ return self.phrase_ref(string[:matchstart], string[textend:],
+ rawsource, escaped, unescape(escaped))
+ else:
+ rawsource = unescape(string[rolestart:textend], 1)
+ nodelist, messages = self.interpreted(rawsource, escaped, role,
+ lineno)
+ return (string[:rolestart], nodelist,
+ string[textend:], messages)
+ msg = self.reporter.warning(
+ 'Inline interpreted text or phrase reference start-string '
+ 'without end-string.', line=lineno)
+ text = unescape(string[matchstart:matchend], 1)
+ prb = self.problematic(text, text, msg)
+ return string[:matchstart], [prb], string[matchend:], [msg]
+
+ def phrase_ref(self, before, after, rawsource, escaped, text):
+ match = self.patterns.embedded_uri.search(escaped)
+ if match:
+ text = unescape(escaped[:match.start(0)])
+ uri_text = match.group(2)
+ uri = ''.join(uri_text.split())
+ uri = self.adjust_uri(uri)
+ if uri:
+ target = nodes.target(match.group(1), refuri=uri)
+ else:
+ raise ApplicationError('problem with URI: %r' % uri_text)
+ if not text:
+ text = uri
+ else:
+ target = None
+ refname = normalize_name(text)
+ reference = nodes.reference(rawsource, text,
+ name=whitespace_normalize_name(text))
+ node_list = [reference]
+ if rawsource[-2:] == '__':
+ if target:
+ reference['refuri'] = uri
+ else:
+ reference['anonymous'] = 1
+ self.document.note_anonymous_ref(reference)
+ else:
+ if target:
+ reference['refuri'] = uri
+ target['names'].append(refname)
+ self.document.note_external_target(target)
+ self.document.note_explicit_target(target, self.parent)
+ node_list.append(target)
+ else:
+ reference['refname'] = refname
+ self.document.note_refname(reference)
+ return before, node_list, after, []
+
+ def adjust_uri(self, uri):
+ match = self.patterns.email.match(uri)
+ if match:
+ return 'mailto:' + uri
+ else:
+ return uri
+
+ def interpreted(self, rawsource, text, role, lineno):
+ role_fn, messages = roles.role(role, self.language, lineno,
+ self.reporter)
+ if role_fn:
+ nodes, messages2 = role_fn(role, rawsource, text, lineno, self)
+ return nodes, messages + messages2
+ else:
+ msg = self.reporter.error(
+ 'Unknown interpreted text role "%s".' % role,
+ line=lineno)
+ return ([self.problematic(rawsource, rawsource, msg)],
+ messages + [msg])
+
+ def literal(self, match, lineno):
+ before, inlines, remaining, sysmessages, endstring = self.inline_obj(
+ match, lineno, self.patterns.literal, nodes.literal,
+ restore_backslashes=1)
+ return before, inlines, remaining, sysmessages
+
+ def inline_internal_target(self, match, lineno):
+ before, inlines, remaining, sysmessages, endstring = self.inline_obj(
+ match, lineno, self.patterns.target, nodes.target)
+ if inlines and isinstance(inlines[0], nodes.target):
+ assert len(inlines) == 1
+ target = inlines[0]
+ name = normalize_name(target.astext())
+ target['names'].append(name)
+ self.document.note_explicit_target(target, self.parent)
+ return before, inlines, remaining, sysmessages
+
+ def substitution_reference(self, match, lineno):
+ before, inlines, remaining, sysmessages, endstring = self.inline_obj(
+ match, lineno, self.patterns.substitution_ref,
+ nodes.substitution_reference)
+ if len(inlines) == 1:
+ subref_node = inlines[0]
+ if isinstance(subref_node, nodes.substitution_reference):
+ subref_text = subref_node.astext()
+ self.document.note_substitution_ref(subref_node, subref_text)
+ if endstring[-1:] == '_':
+ reference_node = nodes.reference(
+ '|%s%s' % (subref_text, endstring), '')
+ if endstring[-2:] == '__':
+ reference_node['anonymous'] = 1
+ self.document.note_anonymous_ref(
+ reference_node)
+ else:
+ reference_node['refname'] = normalize_name(subref_text)
+ self.document.note_refname(reference_node)
+ reference_node += subref_node
+ inlines = [reference_node]
+ return before, inlines, remaining, sysmessages
+
+ def footnote_reference(self, match, lineno):
+ """
+ Handles `nodes.footnote_reference` and `nodes.citation_reference`
+ elements.
+ """
+ label = match.group('footnotelabel')
+ refname = normalize_name(label)
+ string = match.string
+ before = string[:match.start('whole')]
+ remaining = string[match.end('whole'):]
+ if match.group('citationlabel'):
+ refnode = nodes.citation_reference('[%s]_' % label,
+ refname=refname)
+ refnode += nodes.Text(label)
+ self.document.note_citation_ref(refnode)
+ else:
+ refnode = nodes.footnote_reference('[%s]_' % label)
+ if refname[0] == '#':
+ refname = refname[1:]
+ refnode['auto'] = 1
+ self.document.note_autofootnote_ref(refnode)
+ elif refname == '*':
+ refname = ''
+ refnode['auto'] = '*'
+ self.document.note_symbol_footnote_ref(
+ refnode)
+ else:
+ refnode += nodes.Text(label)
+ if refname:
+ refnode['refname'] = refname
+ self.document.note_footnote_ref(refnode)
+ if utils.get_trim_footnote_ref_space(self.document.settings):
+ before = before.rstrip()
+ return (before, [refnode], remaining, [])
+
+ def reference(self, match, lineno, anonymous=None):
+ referencename = match.group('refname')
+ refname = normalize_name(referencename)
+ referencenode = nodes.reference(
+ referencename + match.group('refend'), referencename,
+ name=whitespace_normalize_name(referencename))
+ if anonymous:
+ referencenode['anonymous'] = 1
+ self.document.note_anonymous_ref(referencenode)
+ else:
+ referencenode['refname'] = refname
+ self.document.note_refname(referencenode)
+ string = match.string
+ matchstart = match.start('whole')
+ matchend = match.end('whole')
+ return (string[:matchstart], [referencenode], string[matchend:], [])
+
+ def anonymous_reference(self, match, lineno):
+ return self.reference(match, lineno, anonymous=1)
+
+ def standalone_uri(self, match, lineno):
+ if not match.group('scheme') or urischemes.schemes.has_key(
+ match.group('scheme').lower()):
+ if match.group('email'):
+ addscheme = 'mailto:'
+ else:
+ addscheme = ''
+ text = match.group('whole')
+ unescaped = unescape(text, 0)
+ return [nodes.reference(unescape(text, 1), unescaped,
+ refuri=addscheme + unescaped)]
+ else: # not a valid scheme
+ raise MarkupMismatch
+
+ pep_url = 'pep-%04d.html'
+
+ def pep_reference(self, match, lineno):
+ text = match.group(0)
+ if text.startswith('pep-'):
+ pepnum = int(match.group('pepnum1'))
+ elif text.startswith('PEP'):
+ pepnum = int(match.group('pepnum2'))
+ else:
+ raise MarkupMismatch
+ ref = self.document.settings.pep_base_url + self.pep_url % pepnum
+ unescaped = unescape(text, 0)
+ return [nodes.reference(unescape(text, 1), unescaped, refuri=ref)]
+
+ rfc_url = 'rfc%d.html'
+
+ def rfc_reference(self, match, lineno):
+ text = match.group(0)
+ if text.startswith('RFC'):
+ rfcnum = int(match.group('rfcnum'))
+ ref = self.document.settings.rfc_base_url + self.rfc_url % rfcnum
+ else:
+ raise MarkupMismatch
+ unescaped = unescape(text, 0)
+ return [nodes.reference(unescape(text, 1), unescaped, refuri=ref)]
+
+ def implicit_inline(self, text, lineno):
+ """
+ Check each of the patterns in `self.implicit_dispatch` for a match,
+ and dispatch to the stored method for the pattern. Recursively check
+ the text before and after the match. Return a list of `nodes.Text`
+ and inline element nodes.
+ """
+ if not text:
+ return []
+ for pattern, method in self.implicit_dispatch:
+ match = pattern.search(text)
+ if match:
+ try:
+ # Must recurse on strings before *and* after the match;
+ # there may be multiple patterns.
+ return (self.implicit_inline(text[:match.start()], lineno)
+ + method(match, lineno) +
+ self.implicit_inline(text[match.end():], lineno))
+ except MarkupMismatch:
+ pass
+ return [nodes.Text(unescape(text), rawsource=unescape(text, 1))]
+
+ dispatch = {'*': emphasis,
+ '**': strong,
+ '`': interpreted_or_phrase_ref,
+ '``': literal,
+ '_`': inline_internal_target,
+ ']_': footnote_reference,
+ '|': substitution_reference,
+ '_': reference,
+ '__': anonymous_reference}
+
+
+class Body(RSTState):
+
+ """
+ Generic classifier of the first line of a block.
+ """
+
+ enum = Struct()
+ """Enumerated list parsing information."""
+
+ enum.formatinfo = {
+ 'parens': Struct(prefix='(', suffix=')', start=1, end=-1),
+ 'rparen': Struct(prefix='', suffix=')', start=0, end=-1),
+ 'period': Struct(prefix='', suffix='.', start=0, end=-1)}
+ enum.formats = enum.formatinfo.keys()
+ enum.sequences = ['arabic', 'loweralpha', 'upperalpha',
+ 'lowerroman', 'upperroman'] # ORDERED!
+ enum.sequencepats = {'arabic': '[0-9]+',
+ 'loweralpha': '[a-z]',
+ 'upperalpha': '[A-Z]',
+ 'lowerroman': '[ivxlcdm]+',
+ 'upperroman': '[IVXLCDM]+',}
+ enum.converters = {'arabic': int,
+ 'loweralpha':
+ lambda s, zero=(ord('a')-1): ord(s) - zero,
+ 'upperalpha':
+ lambda s, zero=(ord('A')-1): ord(s) - zero,
+ 'lowerroman':
+ lambda s: roman.fromRoman(s.upper()),
+ 'upperroman': roman.fromRoman}
+
+ enum.sequenceregexps = {}
+ for sequence in enum.sequences:
+ enum.sequenceregexps[sequence] = re.compile(
+ enum.sequencepats[sequence] + '$')
+
+ grid_table_top_pat = re.compile(r'\+-[-+]+-\+ *$')
+ """Matches the top (& bottom) of a full table)."""
+
+ simple_table_top_pat = re.compile('=+( +=+)+ *$')
+ """Matches the top of a simple table."""
+
+ simple_table_border_pat = re.compile('=+[ =]*$')
+ """Matches the bottom & header bottom of a simple table."""
+
+ pats = {}
+ """Fragments of patterns used by transitions."""
+
+ pats['nonalphanum7bit'] = '[!-/:-@[-`{-~]'
+ pats['alpha'] = '[a-zA-Z]'
+ pats['alphanum'] = '[a-zA-Z0-9]'
+ pats['alphanumplus'] = '[a-zA-Z0-9_-]'
+ pats['enum'] = ('(%(arabic)s|%(loweralpha)s|%(upperalpha)s|%(lowerroman)s'
+ '|%(upperroman)s|#)' % enum.sequencepats)
+ pats['optname'] = '%(alphanum)s%(alphanumplus)s*' % pats
+ # @@@ Loosen up the pattern? Allow Unicode?
+ pats['optarg'] = '(%(alpha)s%(alphanumplus)s*|<[^<>]+>)' % pats
+ pats['shortopt'] = r'(-|\+)%(alphanum)s( ?%(optarg)s)?' % pats
+ pats['longopt'] = r'(--|/)%(optname)s([ =]%(optarg)s)?' % pats
+ pats['option'] = r'(%(shortopt)s|%(longopt)s)' % pats
+
+ for format in enum.formats:
+ pats[format] = '(?P<%s>%s%s%s)' % (
+ format, re.escape(enum.formatinfo[format].prefix),
+ pats['enum'], re.escape(enum.formatinfo[format].suffix))
+
+ patterns = {
+ 'bullet': r'[-+*]( +|$)',
+ 'enumerator': r'(%(parens)s|%(rparen)s|%(period)s)( +|$)' % pats,
+ 'field_marker': r':[^: ]([^:]*[^: ])?:( +|$)',
+ 'option_marker': r'%(option)s(, %(option)s)*( +| ?$)' % pats,
+ 'doctest': r'>>>( +|$)',
+ 'line_block': r'\|( +|$)',
+ 'grid_table_top': grid_table_top_pat,
+ 'simple_table_top': simple_table_top_pat,
+ 'explicit_markup': r'\.\.( +|$)',
+ 'anonymous': r'__( +|$)',
+ 'line': r'(%(nonalphanum7bit)s)\1* *$' % pats,
+ 'text': r''}
+ initial_transitions = (
+ 'bullet',
+ 'enumerator',
+ 'field_marker',
+ 'option_marker',
+ 'doctest',
+ 'line_block',
+ 'grid_table_top',
+ 'simple_table_top',
+ 'explicit_markup',
+ 'anonymous',
+ 'line',
+ 'text')
+
+ def indent(self, match, context, next_state):
+ """Block quote."""
+ indented, indent, line_offset, blank_finish = \
+ self.state_machine.get_indented()
+ blockquote, messages = self.block_quote(indented, line_offset)
+ self.parent += blockquote
+ self.parent += messages
+ if not blank_finish:
+ self.parent += self.unindent_warning('Block quote')
+ return context, next_state, []
+
+ def block_quote(self, indented, line_offset):
+ blockquote_lines, attribution_lines, attribution_offset = \
+ self.check_attribution(indented, line_offset)
+ blockquote = nodes.block_quote()
+ self.nested_parse(blockquote_lines, line_offset, blockquote)
+ messages = []
+ if attribution_lines:
+ attribution, messages = self.parse_attribution(attribution_lines,
+ attribution_offset)
+ blockquote += attribution
+ return blockquote, messages
+
+ # u'\u2014' is an em-dash:
+ attribution_pattern = re.compile(ur'(---?(?!-)|\u2014) *(?=[^ \n])')
+
+ def check_attribution(self, indented, line_offset):
+ """
+ Check for an attribution in the last contiguous block of `indented`.
+
+ * First line after last blank line must begin with "--" (etc.).
+ * Every line after that must have consistent indentation.
+
+ Return a 3-tuple: (block quote lines, attribution lines,
+ attribution offset).
+ """
+ blank = None
+ nonblank_seen = None
+ indent = 0
+ for i in range(len(indented) - 1, 0, -1): # don't check first line
+ this_line_blank = not indented[i].strip()
+ if nonblank_seen and this_line_blank:
+ match = self.attribution_pattern.match(indented[i + 1])
+ if match:
+ blank = i
+ break
+ elif not this_line_blank:
+ nonblank_seen = 1
+ if blank and len(indented) - blank > 2: # multi-line attribution
+ indent = (len(indented[blank + 2])
+ - len(indented[blank + 2].lstrip()))
+ for j in range(blank + 3, len(indented)):
+ if indent != (len(indented[j])
+ - len(indented[j].lstrip())): # bad shape
+ blank = None
+ break
+ if blank:
+ a_lines = indented[blank + 1:]
+ a_lines.trim_left(match.end(), end=1)
+ a_lines.trim_left(indent, start=1)
+ return (indented[:blank], a_lines, line_offset + blank + 1)
+ else:
+ return (indented, None, None)
+
+ def parse_attribution(self, indented, line_offset):
+ text = '\n'.join(indented).rstrip()
+ lineno = self.state_machine.abs_line_number() + line_offset
+ textnodes, messages = self.inline_text(text, lineno)
+ node = nodes.attribution(text, '', *textnodes)
+ node.line = lineno
+ return node, messages
+
+ def bullet(self, match, context, next_state):
+ """Bullet list item."""
+ bulletlist = nodes.bullet_list()
+ self.parent += bulletlist
+ bulletlist['bullet'] = match.string[0]
+ i, blank_finish = self.list_item(match.end())
+ bulletlist += i
+ offset = self.state_machine.line_offset + 1 # next line
+ new_line_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=bulletlist, initial_state='BulletList',
+ blank_finish=blank_finish)
+ self.goto_line(new_line_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Bullet list')
+ return [], next_state, []
+
+ def list_item(self, indent):
+ indented, line_offset, blank_finish = \
+ self.state_machine.get_known_indented(indent)
+ listitem = nodes.list_item('\n'.join(indented))
+ if indented:
+ self.nested_parse(indented, input_offset=line_offset,
+ node=listitem)
+ return listitem, blank_finish
+
+ def enumerator(self, match, context, next_state):
+ """Enumerated List Item"""
+ format, sequence, text, ordinal = self.parse_enumerator(match)
+ if not self.is_enumerated_list_item(ordinal, sequence, format):
+ raise statemachine.TransitionCorrection('text')
+ enumlist = nodes.enumerated_list()
+ self.parent += enumlist
+ if sequence == '#':
+ enumlist['enumtype'] = 'arabic'
+ else:
+ enumlist['enumtype'] = sequence
+ enumlist['prefix'] = self.enum.formatinfo[format].prefix
+ enumlist['suffix'] = self.enum.formatinfo[format].suffix
+ if ordinal != 1:
+ enumlist['start'] = ordinal
+ msg = self.reporter.info(
+ 'Enumerated list start value not ordinal-1: "%s" (ordinal %s)'
+ % (text, ordinal), line=self.state_machine.abs_line_number())
+ self.parent += msg
+ listitem, blank_finish = self.list_item(match.end())
+ enumlist += listitem
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=enumlist, initial_state='EnumeratedList',
+ blank_finish=blank_finish,
+ extra_settings={'lastordinal': ordinal,
+ 'format': format,
+ 'auto': sequence == '#'})
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Enumerated list')
+ return [], next_state, []
+
+ def parse_enumerator(self, match, expected_sequence=None):
+ """
+ Analyze an enumerator and return the results.
+
+ :Return:
+ - the enumerator format ('period', 'parens', or 'rparen'),
+ - the sequence used ('arabic', 'loweralpha', 'upperroman', etc.),
+ - the text of the enumerator, stripped of formatting, and
+ - the ordinal value of the enumerator ('a' -> 1, 'ii' -> 2, etc.;
+ ``None`` is returned for invalid enumerator text).
+
+ The enumerator format has already been determined by the regular
+ expression match. If `expected_sequence` is given, that sequence is
+ tried first. If not, we check for Roman numeral 1. This way,
+ single-character Roman numerals (which are also alphabetical) can be
+ matched. If no sequence has been matched, all sequences are checked in
+ order.
+ """
+ groupdict = match.groupdict()
+ sequence = ''
+ for format in self.enum.formats:
+ if groupdict[format]: # was this the format matched?
+ break # yes; keep `format`
+ else: # shouldn't happen
+ raise ParserError('enumerator format not matched')
+ text = groupdict[format][self.enum.formatinfo[format].start
+ :self.enum.formatinfo[format].end]
+ if text == '#':
+ sequence = '#'
+ elif expected_sequence:
+ try:
+ if self.enum.sequenceregexps[expected_sequence].match(text):
+ sequence = expected_sequence
+ except KeyError: # shouldn't happen
+ raise ParserError('unknown enumerator sequence: %s'
+ % sequence)
+ elif text == 'i':
+ sequence = 'lowerroman'
+ elif text == 'I':
+ sequence = 'upperroman'
+ if not sequence:
+ for sequence in self.enum.sequences:
+ if self.enum.sequenceregexps[sequence].match(text):
+ break
+ else: # shouldn't happen
+ raise ParserError('enumerator sequence not matched')
+ if sequence == '#':
+ ordinal = 1
+ else:
+ try:
+ ordinal = self.enum.converters[sequence](text)
+ except roman.InvalidRomanNumeralError:
+ ordinal = None
+ return format, sequence, text, ordinal
+
+ def is_enumerated_list_item(self, ordinal, sequence, format):
+ """
+ Check validity based on the ordinal value and the second line.
+
+ Return true iff the ordinal is valid and the second line is blank,
+ indented, or starts with the next enumerator or an auto-enumerator.
+ """
+ if ordinal is None:
+ return None
+ try:
+ next_line = self.state_machine.next_line()
+ except EOFError: # end of input lines
+ self.state_machine.previous_line()
+ return 1
+ else:
+ self.state_machine.previous_line()
+ if not next_line[:1].strip(): # blank or indented
+ return 1
+ next_enumerator, auto_enumerator = self.make_enumerator(
+ ordinal + 1, sequence, format)
+ try:
+ if ( next_line.startswith(next_enumerator) or
+ next_line.startswith(auto_enumerator) ):
+ return 1
+ except TypeError:
+ pass
+ return None
+
+ def make_enumerator(self, ordinal, sequence, format):
+ """
+ Construct and return the next enumerated list item marker, and an
+ auto-enumerator ("#" instead of the regular enumerator).
+
+ Return ``None`` for invalid (out of range) ordinals.
+ """ #"
+ if sequence == '#':
+ enumerator = '#'
+ elif sequence == 'arabic':
+ enumerator = str(ordinal)
+ else:
+ if sequence.endswith('alpha'):
+ if ordinal > 26:
+ return None
+ enumerator = chr(ordinal + ord('a') - 1)
+ elif sequence.endswith('roman'):
+ try:
+ enumerator = roman.toRoman(ordinal)
+ except roman.RomanError:
+ return None
+ else: # shouldn't happen
+ raise ParserError('unknown enumerator sequence: "%s"'
+ % sequence)
+ if sequence.startswith('lower'):
+ enumerator = enumerator.lower()
+ elif sequence.startswith('upper'):
+ enumerator = enumerator.upper()
+ else: # shouldn't happen
+ raise ParserError('unknown enumerator sequence: "%s"'
+ % sequence)
+ formatinfo = self.enum.formatinfo[format]
+ next_enumerator = (formatinfo.prefix + enumerator + formatinfo.suffix
+ + ' ')
+ auto_enumerator = formatinfo.prefix + '#' + formatinfo.suffix + ' '
+ return next_enumerator, auto_enumerator
+
+ def field_marker(self, match, context, next_state):
+ """Field list item."""
+ field_list = nodes.field_list()
+ self.parent += field_list
+ field, blank_finish = self.field(match)
+ field_list += field
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=field_list, initial_state='FieldList',
+ blank_finish=blank_finish)
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Field list')
+ return [], next_state, []
+
+ def field(self, match):
+ name = self.parse_field_marker(match)
+ lineno = self.state_machine.abs_line_number()
+ indented, indent, line_offset, blank_finish = \
+ self.state_machine.get_first_known_indented(match.end())
+ field_node = nodes.field()
+ field_node.line = lineno
+ name_nodes, name_messages = self.inline_text(name, lineno)
+ field_node += nodes.field_name(name, '', *name_nodes)
+ field_body = nodes.field_body('\n'.join(indented), *name_messages)
+ field_node += field_body
+ if indented:
+ self.parse_field_body(indented, line_offset, field_body)
+ return field_node, blank_finish
+
+ def parse_field_marker(self, match):
+ """Extract & return field name from a field marker match."""
+ field = match.string[1:] # strip off leading ':'
+ field = field[:field.find(':')] # strip off trailing ':' etc.
+ return field
+
+ def parse_field_body(self, indented, offset, node):
+ self.nested_parse(indented, input_offset=offset, node=node)
+
+ def option_marker(self, match, context, next_state):
+ """Option list item."""
+ optionlist = nodes.option_list()
+ try:
+ listitem, blank_finish = self.option_list_item(match)
+ except MarkupError, (message, lineno):
+ # This shouldn't happen; pattern won't match.
+ msg = self.reporter.error(
+ 'Invalid option list marker: %s' % message, line=lineno)
+ self.parent += msg
+ indented, indent, line_offset, blank_finish = \
+ self.state_machine.get_first_known_indented(match.end())
+ blockquote, messages = self.block_quote(indented, line_offset)
+ self.parent += blockquote
+ self.parent += messages
+ if not blank_finish:
+ self.parent += self.unindent_warning('Option list')
+ return [], next_state, []
+ self.parent += optionlist
+ optionlist += listitem
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=optionlist, initial_state='OptionList',
+ blank_finish=blank_finish)
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Option list')
+ return [], next_state, []
+
+ def option_list_item(self, match):
+ offset = self.state_machine.abs_line_offset()
+ options = self.parse_option_marker(match)
+ indented, indent, line_offset, blank_finish = \
+ self.state_machine.get_first_known_indented(match.end())
+ if not indented: # not an option list item
+ self.goto_line(offset)
+ raise statemachine.TransitionCorrection('text')
+ option_group = nodes.option_group('', *options)
+ description = nodes.description('\n'.join(indented))
+ option_list_item = nodes.option_list_item('', option_group,
+ description)
+ if indented:
+ self.nested_parse(indented, input_offset=line_offset,
+ node=description)
+ return option_list_item, blank_finish
+
+ def parse_option_marker(self, match):
+ """
+ Return a list of `node.option` and `node.option_argument` objects,
+ parsed from an option marker match.
+
+ :Exception: `MarkupError` for invalid option markers.
+ """
+ optlist = []
+ optionstrings = match.group().rstrip().split(', ')
+ for optionstring in optionstrings:
+ tokens = optionstring.split()
+ delimiter = ' '
+ firstopt = tokens[0].split('=')
+ if len(firstopt) > 1:
+ # "--opt=value" form
+ tokens[:1] = firstopt
+ delimiter = '='
+ elif (len(tokens[0]) > 2
+ and ((tokens[0].startswith('-')
+ and not tokens[0].startswith('--'))
+ or tokens[0].startswith('+'))):
+ # "-ovalue" form
+ tokens[:1] = [tokens[0][:2], tokens[0][2:]]
+ delimiter = ''
+ if len(tokens) > 1 and (tokens[1].startswith('<')
+ and tokens[-1].endswith('>')):
+ # "-o <value1 value2>" form; join all values into one token
+ tokens[1:] = [' '.join(tokens[1:])]
+ if 0 < len(tokens) <= 2:
+ option = nodes.option(optionstring)
+ option += nodes.option_string(tokens[0], tokens[0])
+ if len(tokens) > 1:
+ option += nodes.option_argument(tokens[1], tokens[1],
+ delimiter=delimiter)
+ optlist.append(option)
+ else:
+ raise MarkupError(
+ 'wrong number of option tokens (=%s), should be 1 or 2: '
+ '"%s"' % (len(tokens), optionstring),
+ self.state_machine.abs_line_number() + 1)
+ return optlist
+
+ def doctest(self, match, context, next_state):
+ data = '\n'.join(self.state_machine.get_text_block())
+ self.parent += nodes.doctest_block(data, data)
+ return [], next_state, []
+
+ def line_block(self, match, context, next_state):
+ """First line of a line block."""
+ block = nodes.line_block()
+ self.parent += block
+ lineno = self.state_machine.abs_line_number()
+ line, messages, blank_finish = self.line_block_line(match, lineno)
+ block += line
+ self.parent += messages
+ if not blank_finish:
+ offset = self.state_machine.line_offset + 1 # next line
+ new_line_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=block, initial_state='LineBlock',
+ blank_finish=0)
+ self.goto_line(new_line_offset)
+ if not blank_finish:
+ self.parent += self.reporter.warning(
+ 'Line block ends without a blank line.',
+ line=(self.state_machine.abs_line_number() + 1))
+ if len(block):
+ if block[0].indent is None:
+ block[0].indent = 0
+ self.nest_line_block_lines(block)
+ return [], next_state, []
+
+ def line_block_line(self, match, lineno):
+ """Return one line element of a line_block."""
+ indented, indent, line_offset, blank_finish = \
+ self.state_machine.get_first_known_indented(match.end(),
+ until_blank=1)
+ text = u'\n'.join(indented)
+ text_nodes, messages = self.inline_text(text, lineno)
+ line = nodes.line(text, '', *text_nodes)
+ if match.string.rstrip() != '|': # not empty
+ line.indent = len(match.group(1)) - 1
+ return line, messages, blank_finish
+
+ def nest_line_block_lines(self, block):
+ for index in range(1, len(block)):
+ if block[index].indent is None:
+ block[index].indent = block[index - 1].indent
+ self.nest_line_block_segment(block)
+
+ def nest_line_block_segment(self, block):
+ indents = [item.indent for item in block]
+ least = min(indents)
+ new_items = []
+ new_block = nodes.line_block()
+ for item in block:
+ if item.indent > least:
+ new_block.append(item)
+ else:
+ if len(new_block):
+ self.nest_line_block_segment(new_block)
+ new_items.append(new_block)
+ new_block = nodes.line_block()
+ new_items.append(item)
+ if len(new_block):
+ self.nest_line_block_segment(new_block)
+ new_items.append(new_block)
+ block[:] = new_items
+
+ def grid_table_top(self, match, context, next_state):
+ """Top border of a full table."""
+ return self.table_top(match, context, next_state,
+ self.isolate_grid_table,
+ tableparser.GridTableParser)
+
+ def simple_table_top(self, match, context, next_state):
+ """Top border of a simple table."""
+ return self.table_top(match, context, next_state,
+ self.isolate_simple_table,
+ tableparser.SimpleTableParser)
+
+ def table_top(self, match, context, next_state,
+ isolate_function, parser_class):
+ """Top border of a generic table."""
+ nodelist, blank_finish = self.table(isolate_function, parser_class)
+ self.parent += nodelist
+ if not blank_finish:
+ msg = self.reporter.warning(
+ 'Blank line required after table.',
+ line=self.state_machine.abs_line_number() + 1)
+ self.parent += msg
+ return [], next_state, []
+
+ def table(self, isolate_function, parser_class):
+ """Parse a table."""
+ block, messages, blank_finish = isolate_function()
+ if block:
+ try:
+ parser = parser_class()
+ tabledata = parser.parse(block)
+ tableline = (self.state_machine.abs_line_number() - len(block)
+ + 1)
+ table = self.build_table(tabledata, tableline)
+ nodelist = [table] + messages
+ except tableparser.TableMarkupError, detail:
+ nodelist = self.malformed_table(
+ block, ' '.join(detail.args)) + messages
+ else:
+ nodelist = messages
+ return nodelist, blank_finish
+
+ def isolate_grid_table(self):
+ messages = []
+ blank_finish = 1
+ try:
+ block = self.state_machine.get_text_block(flush_left=1)
+ except statemachine.UnexpectedIndentationError, instance:
+ block, source, lineno = instance.args
+ messages.append(self.reporter.error('Unexpected indentation.',
+ source=source, line=lineno))
+ blank_finish = 0
+ block.disconnect()
+ width = len(block[0].strip())
+ for i in range(len(block)):
+ block[i] = block[i].strip()
+ if block[i][0] not in '+|': # check left edge
+ blank_finish = 0
+ self.state_machine.previous_line(len(block) - i)
+ del block[i:]
+ break
+ if not self.grid_table_top_pat.match(block[-1]): # find bottom
+ blank_finish = 0
+ # from second-last to third line of table:
+ for i in range(len(block) - 2, 1, -1):
+ if self.grid_table_top_pat.match(block[i]):
+ self.state_machine.previous_line(len(block) - i + 1)
+ del block[i+1:]
+ break
+ else:
+ messages.extend(self.malformed_table(block))
+ return [], messages, blank_finish
+ for i in range(len(block)): # check right edge
+ if len(block[i]) != width or block[i][-1] not in '+|':
+ messages.extend(self.malformed_table(block))
+ return [], messages, blank_finish
+ return block, messages, blank_finish
+
+ def isolate_simple_table(self):
+ start = self.state_machine.line_offset
+ lines = self.state_machine.input_lines
+ limit = len(lines) - 1
+ toplen = len(lines[start].strip())
+ pattern_match = self.simple_table_border_pat.match
+ found = 0
+ found_at = None
+ i = start + 1
+ while i <= limit:
+ line = lines[i]
+ match = pattern_match(line)
+ if match:
+ if len(line.strip()) != toplen:
+ self.state_machine.next_line(i - start)
+ messages = self.malformed_table(
+ lines[start:i+1], 'Bottom/header table border does '
+ 'not match top border.')
+ return [], messages, i == limit or not lines[i+1].strip()
+ found += 1
+ found_at = i
+ if found == 2 or i == limit or not lines[i+1].strip():
+ end = i
+ break
+ i += 1
+ else: # reached end of input_lines
+ if found:
+ extra = ' or no blank line after table bottom'
+ self.state_machine.next_line(found_at - start)
+ block = lines[start:found_at+1]
+ else:
+ extra = ''
+ self.state_machine.next_line(i - start - 1)
+ block = lines[start:]
+ messages = self.malformed_table(
+ block, 'No bottom table border found%s.' % extra)
+ return [], messages, not extra
+ self.state_machine.next_line(end - start)
+ block = lines[start:end+1]
+ return block, [], end == limit or not lines[end+1].strip()
+
+ def malformed_table(self, block, detail=''):
+ data = '\n'.join(block)
+ message = 'Malformed table.'
+ lineno = self.state_machine.abs_line_number() - len(block) + 1
+ if detail:
+ message += '\n' + detail
+ error = self.reporter.error(message, nodes.literal_block(data, data),
+ line=lineno)
+ return [error]
+
+ def build_table(self, tabledata, tableline, stub_columns=0):
+ colwidths, headrows, bodyrows = tabledata
+ table = nodes.table()
+ tgroup = nodes.tgroup(cols=len(colwidths))
+ table += tgroup
+ for colwidth in colwidths:
+ colspec = nodes.colspec(colwidth=colwidth)
+ if stub_columns:
+ colspec.attributes['stub'] = 1
+ stub_columns -= 1
+ tgroup += colspec
+ if headrows:
+ thead = nodes.thead()
+ tgroup += thead
+ for row in headrows:
+ thead += self.build_table_row(row, tableline)
+ tbody = nodes.tbody()
+ tgroup += tbody
+ for row in bodyrows:
+ tbody += self.build_table_row(row, tableline)
+ return table
+
+ def build_table_row(self, rowdata, tableline):
+ row = nodes.row()
+ for cell in rowdata:
+ if cell is None:
+ continue
+ morerows, morecols, offset, cellblock = cell
+ attributes = {}
+ if morerows:
+ attributes['morerows'] = morerows
+ if morecols:
+ attributes['morecols'] = morecols
+ entry = nodes.entry(**attributes)
+ row += entry
+ if ''.join(cellblock):
+ self.nested_parse(cellblock, input_offset=tableline+offset,
+ node=entry)
+ return row
+
+
+ explicit = Struct()
+ """Patterns and constants used for explicit markup recognition."""
+
+ explicit.patterns = Struct(
+ target=re.compile(r"""
+ (
+ _ # anonymous target
+ | # *OR*
+ (?P<quote>`?) # optional open quote
+ (?![ `]) # first char. not space or
+ # backquote
+ (?P<name> # reference name
+ .+?
+ )
+ %(non_whitespace_escape_before)s
+ (?P=quote) # close quote if open quote used
+ )
+ %(non_whitespace_escape_before)s
+ [ ]? # optional space
+ : # end of reference name
+ ([ ]+|$) # followed by whitespace
+ """ % vars(Inliner), re.VERBOSE),
+ reference=re.compile(r"""
+ (
+ (?P<simple>%(simplename)s)_
+ | # *OR*
+ ` # open backquote
+ (?![ ]) # not space
+ (?P<phrase>.+?) # hyperlink phrase
+ %(non_whitespace_escape_before)s
+ `_ # close backquote,
+ # reference mark
+ )
+ $ # end of string
+ """ % vars(Inliner), re.VERBOSE | re.UNICODE),
+ substitution=re.compile(r"""
+ (
+ (?![ ]) # first char. not space
+ (?P<name>.+?) # substitution text
+ %(non_whitespace_escape_before)s
+ \| # close delimiter
+ )
+ ([ ]+|$) # followed by whitespace
+ """ % vars(Inliner), re.VERBOSE),)
+
+ def footnote(self, match):
+ lineno = self.state_machine.abs_line_number()
+ indented, indent, offset, blank_finish = \
+ self.state_machine.get_first_known_indented(match.end())
+ label = match.group(1)
+ name = normalize_name(label)
+ footnote = nodes.footnote('\n'.join(indented))
+ footnote.line = lineno
+ if name[0] == '#': # auto-numbered
+ name = name[1:] # autonumber label
+ footnote['auto'] = 1
+ if name:
+ footnote['names'].append(name)
+ self.document.note_autofootnote(footnote)
+ elif name == '*': # auto-symbol
+ name = ''
+ footnote['auto'] = '*'
+ self.document.note_symbol_footnote(footnote)
+ else: # manually numbered
+ footnote += nodes.label('', label)
+ footnote['names'].append(name)
+ self.document.note_footnote(footnote)
+ if name:
+ self.document.note_explicit_target(footnote, footnote)
+ else:
+ self.document.set_id(footnote, footnote)
+ if indented:
+ self.nested_parse(indented, input_offset=offset, node=footnote)
+ return [footnote], blank_finish
+
+ def citation(self, match):
+ lineno = self.state_machine.abs_line_number()
+ indented, indent, offset, blank_finish = \
+ self.state_machine.get_first_known_indented(match.end())
+ label = match.group(1)
+ name = normalize_name(label)
+ citation = nodes.citation('\n'.join(indented))
+ citation.line = lineno
+ citation += nodes.label('', label)
+ citation['names'].append(name)
+ self.document.note_citation(citation)
+ self.document.note_explicit_target(citation, citation)
+ if indented:
+ self.nested_parse(indented, input_offset=offset, node=citation)
+ return [citation], blank_finish
+
+ def hyperlink_target(self, match):
+ pattern = self.explicit.patterns.target
+ lineno = self.state_machine.abs_line_number()
+ block, indent, offset, blank_finish = \
+ self.state_machine.get_first_known_indented(
+ match.end(), until_blank=1, strip_indent=0)
+ blocktext = match.string[:match.end()] + '\n'.join(block)
+ block = [escape2null(line) for line in block]
+ escaped = block[0]
+ blockindex = 0
+ while 1:
+ targetmatch = pattern.match(escaped)
+ if targetmatch:
+ break
+ blockindex += 1
+ try:
+ escaped += block[blockindex]
+ except IndexError:
+ raise MarkupError('malformed hyperlink target.', lineno)
+ del block[:blockindex]
+ block[0] = (block[0] + ' ')[targetmatch.end()-len(escaped)-1:].strip()
+ target = self.make_target(block, blocktext, lineno,
+ targetmatch.group('name'))
+ return [target], blank_finish
+
+ def make_target(self, block, block_text, lineno, target_name):
+ target_type, data = self.parse_target(block, block_text, lineno)
+ if target_type == 'refname':
+ target = nodes.target(block_text, '', refname=normalize_name(data))
+ self.add_target(target_name, '', target, lineno)
+ self.document.note_indirect_target(target)
+ return target
+ elif target_type == 'refuri':
+ target = nodes.target(block_text, '')
+ self.add_target(target_name, data, target, lineno)
+ return target
+ else:
+ return data
+
+ def parse_target(self, block, block_text, lineno):
+ """
+ Determine the type of reference of a target.
+
+ :Return: A 2-tuple, one of:
+
+ - 'refname' and the indirect reference name
+ - 'refuri' and the URI
+ - 'malformed' and a system_message node
+ """
+ if block and block[-1].strip()[-1:] == '_': # possible indirect target
+ reference = ' '.join([line.strip() for line in block])
+ refname = self.is_reference(reference)
+ if refname:
+ return 'refname', refname
+ reference = ''.join([''.join(line.split()) for line in block])
+ return 'refuri', unescape(reference)
+
+ def is_reference(self, reference):
+ match = self.explicit.patterns.reference.match(
+ whitespace_normalize_name(reference))
+ if not match:
+ return None
+ return unescape(match.group('simple') or match.group('phrase'))
+
+ def add_target(self, targetname, refuri, target, lineno):
+ target.line = lineno
+ if targetname:
+ name = normalize_name(unescape(targetname))
+ target['names'].append(name)
+ if refuri:
+ uri = self.inliner.adjust_uri(refuri)
+ if uri:
+ target['refuri'] = uri
+ self.document.note_external_target(target)
+ else:
+ raise ApplicationError('problem with URI: %r' % refuri)
+ else:
+ self.document.note_internal_target(target)
+ self.document.note_explicit_target(target, self.parent)
+ else: # anonymous target
+ if refuri:
+ target['refuri'] = refuri
+ else:
+ self.document.note_internal_target(target)
+ target['anonymous'] = 1
+ self.document.note_anonymous_target(target)
+
+ def substitution_def(self, match):
+ pattern = self.explicit.patterns.substitution
+ lineno = self.state_machine.abs_line_number()
+ block, indent, offset, blank_finish = \
+ self.state_machine.get_first_known_indented(match.end(),
+ strip_indent=0)
+ blocktext = (match.string[:match.end()] + '\n'.join(block))
+ block.disconnect()
+ escaped = escape2null(block[0].rstrip())
+ blockindex = 0
+ while 1:
+ subdefmatch = pattern.match(escaped)
+ if subdefmatch:
+ break
+ blockindex += 1
+ try:
+ escaped = escaped + ' ' + escape2null(block[blockindex].strip())
+ except IndexError:
+ raise MarkupError('malformed substitution definition.',
+ lineno)
+ del block[:blockindex] # strip out the substitution marker
+ block[0] = (block[0].strip() + ' ')[subdefmatch.end()-len(escaped)-1:-1]
+ if not block[0]:
+ del block[0]
+ offset += 1
+ while block and not block[-1].strip():
+ block.pop()
+ subname = subdefmatch.group('name')
+ substitution_node = nodes.substitution_definition(blocktext)
+ substitution_node.line = lineno
+ self.document.note_substitution_def(
+ substitution_node,subname, self.parent)
+ if block:
+ block[0] = block[0].strip()
+ new_abs_offset, blank_finish = self.nested_list_parse(
+ block, input_offset=offset, node=substitution_node,
+ initial_state='SubstitutionDef', blank_finish=blank_finish)
+ i = 0
+ for node in substitution_node[:]:
+ if not (isinstance(node, nodes.Inline) or
+ isinstance(node, nodes.Text)):
+ self.parent += substitution_node[i]
+ del substitution_node[i]
+ else:
+ i += 1
+ if len(substitution_node) == 0:
+ msg = self.reporter.warning(
+ 'Substitution definition "%s" empty or invalid.'
+ % subname,
+ nodes.literal_block(blocktext, blocktext), line=lineno)
+ return [msg], blank_finish
+ else:
+ return [substitution_node], blank_finish
+ else:
+ msg = self.reporter.warning(
+ 'Substitution definition "%s" missing contents.' % subname,
+ nodes.literal_block(blocktext, blocktext), line=lineno)
+ return [msg], blank_finish
+
+ def directive(self, match, **option_presets):
+ """Returns a 2-tuple: list of nodes, and a "blank finish" boolean."""
+ type_name = match.group(1)
+ directive_function, messages = directives.directive(
+ type_name, self.memo.language, self.document)
+ self.parent += messages
+ if directive_function:
+ return self.run_directive(
+ directive_function, match, type_name, option_presets)
+ else:
+ return self.unknown_directive(type_name)
+
+ def run_directive(self, directive_fn, match, type_name, option_presets):
+ """
+ Parse a directive then run its directive function.
+
+ Parameters:
+
+ - `directive_fn`: The function implementing the directive. Uses
+ function attributes ``arguments``, ``options``, and/or ``content``
+ if present.
+
+ - `match`: A regular expression match object which matched the first
+ line of the directive.
+
+ - `type_name`: The directive name, as used in the source text.
+
+ - `option_presets`: A dictionary of preset options, defaults for the
+ directive options. Currently, only an "alt" option is passed by
+ substitution definitions (value: the substitution name), which may
+ be used by an embedded image directive.
+
+ Returns a 2-tuple: list of nodes, and a "blank finish" boolean.
+ """
+ lineno = self.state_machine.abs_line_number()
+ initial_line_offset = self.state_machine.line_offset
+ indented, indent, line_offset, blank_finish \
+ = self.state_machine.get_first_known_indented(match.end(),
+ strip_top=0)
+ block_text = '\n'.join(self.state_machine.input_lines[
+ initial_line_offset : self.state_machine.line_offset + 1])
+ try:
+ arguments, options, content, content_offset = (
+ self.parse_directive_block(indented, line_offset,
+ directive_fn, option_presets))
+ except MarkupError, detail:
+ error = self.reporter.error(
+ 'Error in "%s" directive:\n%s.' % (type_name,
+ ' '.join(detail.args)),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error], blank_finish
+ result = directive_fn(type_name, arguments, options, content, lineno,
+ content_offset, block_text, self,
+ self.state_machine)
+ return (result,
+ blank_finish or self.state_machine.is_next_line_blank())
+
+ def parse_directive_block(self, indented, line_offset, directive_fn,
+ option_presets):
+ arguments = []
+ options = {}
+ argument_spec = getattr(directive_fn, 'arguments', None)
+ if argument_spec and argument_spec[:2] == (0, 0):
+ argument_spec = None
+ option_spec = getattr(directive_fn, 'options', None)
+ content_spec = getattr(directive_fn, 'content', None)
+ if indented and not indented[0].strip():
+ indented.trim_start()
+ line_offset += 1
+ while indented and not indented[-1].strip():
+ indented.trim_end()
+ if indented and (argument_spec or option_spec):
+ for i in range(len(indented)):
+ if not indented[i].strip():
+ break
+ else:
+ i += 1
+ arg_block = indented[:i]
+ content = indented[i+1:]
+ content_offset = line_offset + i + 1
+ else:
+ content = indented
+ content_offset = line_offset
+ arg_block = []
+ while content and not content[0].strip():
+ content.trim_start()
+ content_offset += 1
+ if option_spec:
+ options, arg_block = self.parse_directive_options(
+ option_presets, option_spec, arg_block)
+ if arg_block and not argument_spec:
+ raise MarkupError('no arguments permitted; blank line '
+ 'required before content block')
+ if argument_spec:
+ arguments = self.parse_directive_arguments(
+ argument_spec, arg_block)
+ if content and not content_spec:
+ raise MarkupError('no content permitted')
+ return (arguments, options, content, content_offset)
+
+ def parse_directive_options(self, option_presets, option_spec, arg_block):
+ options = option_presets.copy()
+ for i in range(len(arg_block)):
+ if arg_block[i][:1] == ':':
+ opt_block = arg_block[i:]
+ arg_block = arg_block[:i]
+ break
+ else:
+ opt_block = []
+ if opt_block:
+ success, data = self.parse_extension_options(option_spec,
+ opt_block)
+ if success: # data is a dict of options
+ options.update(data)
+ else: # data is an error string
+ raise MarkupError(data)
+ return options, arg_block
+
+ def parse_directive_arguments(self, argument_spec, arg_block):
+ required, optional, last_whitespace = argument_spec
+ arg_text = '\n'.join(arg_block)
+ arguments = arg_text.split()
+ if len(arguments) < required:
+ raise MarkupError('%s argument(s) required, %s supplied'
+ % (required, len(arguments)))
+ elif len(arguments) > required + optional:
+ if last_whitespace:
+ arguments = arg_text.split(None, required + optional - 1)
+ else:
+ raise MarkupError(
+ 'maximum %s argument(s) allowed, %s supplied'
+ % (required + optional, len(arguments)))
+ return arguments
+
+ def parse_extension_options(self, option_spec, datalines):
+ """
+ Parse `datalines` for a field list containing extension options
+ matching `option_spec`.
+
+ :Parameters:
+ - `option_spec`: a mapping of option name to conversion
+ function, which should raise an exception on bad input.
+ - `datalines`: a list of input strings.
+
+ :Return:
+ - Success value, 1 or 0.
+ - An option dictionary on success, an error string on failure.
+ """
+ node = nodes.field_list()
+ newline_offset, blank_finish = self.nested_list_parse(
+ datalines, 0, node, initial_state='ExtensionOptions',
+ blank_finish=1)
+ if newline_offset != len(datalines): # incomplete parse of block
+ return 0, 'invalid option block'
+ try:
+ options = utils.extract_extension_options(node, option_spec)
+ except KeyError, detail:
+ return 0, ('unknown option: "%s"' % detail.args[0])
+ except (ValueError, TypeError), detail:
+ return 0, ('invalid option value: %s' % ' '.join(detail.args))
+ except utils.ExtensionOptionError, detail:
+ return 0, ('invalid option data: %s' % ' '.join(detail.args))
+ if blank_finish:
+ return 1, options
+ else:
+ return 0, 'option data incompletely parsed'
+
+ def unknown_directive(self, type_name):
+ lineno = self.state_machine.abs_line_number()
+ indented, indent, offset, blank_finish = \
+ self.state_machine.get_first_known_indented(0, strip_indent=0)
+ text = '\n'.join(indented)
+ error = self.reporter.error(
+ 'Unknown directive type "%s".' % type_name,
+ nodes.literal_block(text, text), line=lineno)
+ return [error], blank_finish
+
+ def comment(self, match):
+ if not match.string[match.end():].strip() \
+ and self.state_machine.is_next_line_blank(): # an empty comment?
+ return [nodes.comment()], 1 # "A tiny but practical wart."
+ indented, indent, offset, blank_finish = \
+ self.state_machine.get_first_known_indented(match.end())
+ while indented and not indented[-1].strip():
+ indented.trim_end()
+ text = '\n'.join(indented)
+ return [nodes.comment(text, text)], blank_finish
+
+ explicit.constructs = [
+ (footnote,
+ re.compile(r"""
+ \.\.[ ]+ # explicit markup start
+ \[
+ ( # footnote label:
+ [0-9]+ # manually numbered footnote
+ | # *OR*
+ \# # anonymous auto-numbered footnote
+ | # *OR*
+ \#%s # auto-number ed?) footnote label
+ | # *OR*
+ \* # auto-symbol footnote
+ )
+ \]
+ ([ ]+|$) # whitespace or end of line
+ """ % Inliner.simplename, re.VERBOSE | re.UNICODE)),
+ (citation,
+ re.compile(r"""
+ \.\.[ ]+ # explicit markup start
+ \[(%s)\] # citation label
+ ([ ]+|$) # whitespace or end of line
+ """ % Inliner.simplename, re.VERBOSE | re.UNICODE)),
+ (hyperlink_target,
+ re.compile(r"""
+ \.\.[ ]+ # explicit markup start
+ _ # target indicator
+ (?![ ]|$) # first char. not space or EOL
+ """, re.VERBOSE)),
+ (substitution_def,
+ re.compile(r"""
+ \.\.[ ]+ # explicit markup start
+ \| # substitution indicator
+ (?![ ]|$) # first char. not space or EOL
+ """, re.VERBOSE)),
+ (directive,
+ re.compile(r"""
+ \.\.[ ]+ # explicit markup start
+ (%s) # directive name
+ [ ]? # optional space
+ :: # directive delimiter
+ ([ ]+|$) # whitespace or end of line
+ """ % Inliner.simplename, re.VERBOSE | re.UNICODE))]
+
+ def explicit_markup(self, match, context, next_state):
+ """Footnotes, hyperlink targets, directives, comments."""
+ nodelist, blank_finish = self.explicit_construct(match)
+ self.parent += nodelist
+ self.explicit_list(blank_finish)
+ return [], next_state, []
+
+ def explicit_construct(self, match):
+ """Determine which explicit construct this is, parse & return it."""
+ errors = []
+ for method, pattern in self.explicit.constructs:
+ expmatch = pattern.match(match.string)
+ if expmatch:
+ try:
+ return method(self, expmatch)
+ except MarkupError, (message, lineno): # never reached?
+ errors.append(self.reporter.warning(message, line=lineno))
+ break
+ nodelist, blank_finish = self.comment(match)
+ return nodelist + errors, blank_finish
+
+ def explicit_list(self, blank_finish):
+ """
+ Create a nested state machine for a series of explicit markup
+ constructs (including anonymous hyperlink targets).
+ """
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=self.parent, initial_state='Explicit',
+ blank_finish=blank_finish,
+ match_titles=self.state_machine.match_titles)
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Explicit markup')
+
+ def anonymous(self, match, context, next_state):
+ """Anonymous hyperlink targets."""
+ nodelist, blank_finish = self.anonymous_target(match)
+ self.parent += nodelist
+ self.explicit_list(blank_finish)
+ return [], next_state, []
+
+ def anonymous_target(self, match):
+ lineno = self.state_machine.abs_line_number()
+ block, indent, offset, blank_finish \
+ = self.state_machine.get_first_known_indented(match.end(),
+ until_blank=1)
+ blocktext = match.string[:match.end()] + '\n'.join(block)
+ block = [escape2null(line) for line in block]
+ target = self.make_target(block, blocktext, lineno, '')
+ return [target], blank_finish
+
+ def line(self, match, context, next_state):
+ """Section title overline or transition marker."""
+ if self.state_machine.match_titles:
+ return [match.string], 'Line', []
+ elif match.string.strip() == '::':
+ raise statemachine.TransitionCorrection('text')
+ elif len(match.string.strip()) < 4:
+ msg = self.reporter.info(
+ 'Unexpected possible title overline or transition.\n'
+ "Treating it as ordinary text because it's so short.",
+ line=self.state_machine.abs_line_number())
+ self.parent += msg
+ raise statemachine.TransitionCorrection('text')
+ else:
+ blocktext = self.state_machine.line
+ msg = self.reporter.severe(
+ 'Unexpected section title or transition.',
+ nodes.literal_block(blocktext, blocktext),
+ line=self.state_machine.abs_line_number())
+ self.parent += msg
+ return [], next_state, []
+
+ def text(self, match, context, next_state):
+ """Titles, definition lists, paragraphs."""
+ return [match.string], 'Text', []
+
+
+class RFC2822Body(Body):
+
+ """
+ RFC2822 headers are only valid as the first constructs in documents. As
+ soon as anything else appears, the `Body` state should take over.
+ """
+
+ patterns = Body.patterns.copy() # can't modify the original
+ patterns['rfc2822'] = r'[!-9;-~]+:( +|$)'
+ initial_transitions = [(name, 'Body')
+ for name in Body.initial_transitions]
+ initial_transitions.insert(-1, ('rfc2822', 'Body')) # just before 'text'
+
+ def rfc2822(self, match, context, next_state):
+ """RFC2822-style field list item."""
+ fieldlist = nodes.field_list(classes=['rfc2822'])
+ self.parent += fieldlist
+ field, blank_finish = self.rfc2822_field(match)
+ fieldlist += field
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=fieldlist, initial_state='RFC2822List',
+ blank_finish=blank_finish)
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning(
+ 'RFC2822-style field list')
+ return [], next_state, []
+
+ def rfc2822_field(self, match):
+ name = match.string[:match.string.find(':')]
+ indented, indent, line_offset, blank_finish = \
+ self.state_machine.get_first_known_indented(match.end(),
+ until_blank=1)
+ fieldnode = nodes.field()
+ fieldnode += nodes.field_name(name, name)
+ fieldbody = nodes.field_body('\n'.join(indented))
+ fieldnode += fieldbody
+ if indented:
+ self.nested_parse(indented, input_offset=line_offset,
+ node=fieldbody)
+ return fieldnode, blank_finish
+
+
+class SpecializedBody(Body):
+
+ """
+ Superclass for second and subsequent compound element members. Compound
+ elements are lists and list-like constructs.
+
+ All transition methods are disabled (redefined as `invalid_input`).
+ Override individual methods in subclasses to re-enable.
+
+ For example, once an initial bullet list item, say, is recognized, the
+ `BulletList` subclass takes over, with a "bullet_list" node as its
+ container. Upon encountering the initial bullet list item, `Body.bullet`
+ calls its ``self.nested_list_parse`` (`RSTState.nested_list_parse`), which
+ starts up a nested parsing session with `BulletList` as the initial state.
+ Only the ``bullet`` transition method is enabled in `BulletList`; as long
+ as only bullet list items are encountered, they are parsed and inserted
+ into the container. The first construct which is *not* a bullet list item
+ triggers the `invalid_input` method, which ends the nested parse and
+ closes the container. `BulletList` needs to recognize input that is
+ invalid in the context of a bullet list, which means everything *other
+ than* bullet list items, so it inherits the transition list created in
+ `Body`.
+ """
+
+ def invalid_input(self, match=None, context=None, next_state=None):
+ """Not a compound element member. Abort this state machine."""
+ self.state_machine.previous_line() # back up so parent SM can reassess
+ raise EOFError
+
+ indent = invalid_input
+ bullet = invalid_input
+ enumerator = invalid_input
+ field_marker = invalid_input
+ option_marker = invalid_input
+ doctest = invalid_input
+ line_block = invalid_input
+ grid_table_top = invalid_input
+ simple_table_top = invalid_input
+ explicit_markup = invalid_input
+ anonymous = invalid_input
+ line = invalid_input
+ text = invalid_input
+
+
+class BulletList(SpecializedBody):
+
+ """Second and subsequent bullet_list list_items."""
+
+ def bullet(self, match, context, next_state):
+ """Bullet list item."""
+ if match.string[0] != self.parent['bullet']:
+ # different bullet: new list
+ self.invalid_input()
+ listitem, blank_finish = self.list_item(match.end())
+ self.parent += listitem
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+
+class DefinitionList(SpecializedBody):
+
+ """Second and subsequent definition_list_items."""
+
+ def text(self, match, context, next_state):
+ """Definition lists."""
+ return [match.string], 'Definition', []
+
+
+class EnumeratedList(SpecializedBody):
+
+ """Second and subsequent enumerated_list list_items."""
+
+ def enumerator(self, match, context, next_state):
+ """Enumerated list item."""
+ format, sequence, text, ordinal = self.parse_enumerator(
+ match, self.parent['enumtype'])
+ if ( format != self.format
+ or (sequence != '#' and (sequence != self.parent['enumtype']
+ or self.auto
+ or ordinal != (self.lastordinal + 1)))
+ or not self.is_enumerated_list_item(ordinal, sequence, format)):
+ # different enumeration: new list
+ self.invalid_input()
+ if sequence == '#':
+ self.auto = 1
+ listitem, blank_finish = self.list_item(match.end())
+ self.parent += listitem
+ self.blank_finish = blank_finish
+ self.lastordinal = ordinal
+ return [], next_state, []
+
+
+class FieldList(SpecializedBody):
+
+ """Second and subsequent field_list fields."""
+
+ def field_marker(self, match, context, next_state):
+ """Field list field."""
+ field, blank_finish = self.field(match)
+ self.parent += field
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+
+class OptionList(SpecializedBody):
+
+ """Second and subsequent option_list option_list_items."""
+
+ def option_marker(self, match, context, next_state):
+ """Option list item."""
+ try:
+ option_list_item, blank_finish = self.option_list_item(match)
+ except MarkupError, (message, lineno):
+ self.invalid_input()
+ self.parent += option_list_item
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+
+class RFC2822List(SpecializedBody, RFC2822Body):
+
+ """Second and subsequent RFC2822-style field_list fields."""
+
+ patterns = RFC2822Body.patterns
+ initial_transitions = RFC2822Body.initial_transitions
+
+ def rfc2822(self, match, context, next_state):
+ """RFC2822-style field list item."""
+ field, blank_finish = self.rfc2822_field(match)
+ self.parent += field
+ self.blank_finish = blank_finish
+ return [], 'RFC2822List', []
+
+ blank = SpecializedBody.invalid_input
+
+
+class ExtensionOptions(FieldList):
+
+ """
+ Parse field_list fields for extension options.
+
+ No nested parsing is done (including inline markup parsing).
+ """
+
+ def parse_field_body(self, indented, offset, node):
+ """Override `Body.parse_field_body` for simpler parsing."""
+ lines = []
+ for line in list(indented) + ['']:
+ if line.strip():
+ lines.append(line)
+ elif lines:
+ text = '\n'.join(lines)
+ node += nodes.paragraph(text, text)
+ lines = []
+
+
+class LineBlock(SpecializedBody):
+
+ """Second and subsequent lines of a line_block."""
+
+ blank = SpecializedBody.invalid_input
+
+ def line_block(self, match, context, next_state):
+ """New line of line block."""
+ lineno = self.state_machine.abs_line_number()
+ line, messages, blank_finish = self.line_block_line(match, lineno)
+ self.parent += line
+ self.parent.parent += messages
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+
+class Explicit(SpecializedBody):
+
+ """Second and subsequent explicit markup construct."""
+
+ def explicit_markup(self, match, context, next_state):
+ """Footnotes, hyperlink targets, directives, comments."""
+ nodelist, blank_finish = self.explicit_construct(match)
+ self.parent += nodelist
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+ def anonymous(self, match, context, next_state):
+ """Anonymous hyperlink targets."""
+ nodelist, blank_finish = self.anonymous_target(match)
+ self.parent += nodelist
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+ blank = SpecializedBody.invalid_input
+
+
+class SubstitutionDef(Body):
+
+ """
+ Parser for the contents of a substitution_definition element.
+ """
+
+ patterns = {
+ 'embedded_directive': re.compile(r'(%s)::( +|$)'
+ % Inliner.simplename, re.UNICODE),
+ 'text': r''}
+ initial_transitions = ['embedded_directive', 'text']
+
+ def embedded_directive(self, match, context, next_state):
+ nodelist, blank_finish = self.directive(match,
+ alt=self.parent['names'][0])
+ self.parent += nodelist
+ if not self.state_machine.at_eof():
+ self.blank_finish = blank_finish
+ raise EOFError
+
+ def text(self, match, context, next_state):
+ if not self.state_machine.at_eof():
+ self.blank_finish = self.state_machine.is_next_line_blank()
+ raise EOFError
+
+
+class Text(RSTState):
+
+ """
+ Classifier of second line of a text block.
+
+ Could be a paragraph, a definition list item, or a title.
+ """
+
+ patterns = {'underline': Body.patterns['line'],
+ 'text': r''}
+ initial_transitions = [('underline', 'Body'), ('text', 'Body')]
+
+ def blank(self, match, context, next_state):
+ """End of paragraph."""
+ paragraph, literalnext = self.paragraph(
+ context, self.state_machine.abs_line_number() - 1)
+ self.parent += paragraph
+ if literalnext:
+ self.parent += self.literal_block()
+ return [], 'Body', []
+
+ def eof(self, context):
+ if context:
+ self.blank(None, context, None)
+ return []
+
+ def indent(self, match, context, next_state):
+ """Definition list item."""
+ definitionlist = nodes.definition_list()
+ definitionlistitem, blank_finish = self.definition_list_item(context)
+ definitionlist += definitionlistitem
+ self.parent += definitionlist
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=definitionlist, initial_state='DefinitionList',
+ blank_finish=blank_finish, blank_finish_state='Definition')
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Definition list')
+ return [], 'Body', []
+
+ def underline(self, match, context, next_state):
+ """Section title."""
+ lineno = self.state_machine.abs_line_number()
+ title = context[0].rstrip()
+ underline = match.string.rstrip()
+ source = title + '\n' + underline
+ messages = []
+ if len(title) > len(underline):
+ if len(underline) < 4:
+ if self.state_machine.match_titles:
+ msg = self.reporter.info(
+ 'Possible title underline, too short for the title.\n'
+ "Treating it as ordinary text because it's so short.",
+ line=lineno)
+ self.parent += msg
+ raise statemachine.TransitionCorrection('text')
+ else:
+ blocktext = context[0] + '\n' + self.state_machine.line
+ msg = self.reporter.warning(
+ 'Title underline too short.',
+ nodes.literal_block(blocktext, blocktext), line=lineno)
+ messages.append(msg)
+ if not self.state_machine.match_titles:
+ blocktext = context[0] + '\n' + self.state_machine.line
+ msg = self.reporter.severe(
+ 'Unexpected section title.',
+ nodes.literal_block(blocktext, blocktext), line=lineno)
+ self.parent += messages
+ self.parent += msg
+ return [], next_state, []
+ style = underline[0]
+ context[:] = []
+ self.section(title, source, style, lineno - 1, messages)
+ return [], next_state, []
+
+ def text(self, match, context, next_state):
+ """Paragraph."""
+ startline = self.state_machine.abs_line_number() - 1
+ msg = None
+ try:
+ block = self.state_machine.get_text_block(flush_left=1)
+ except statemachine.UnexpectedIndentationError, instance:
+ block, source, lineno = instance.args
+ msg = self.reporter.error('Unexpected indentation.',
+ source=source, line=lineno)
+ lines = context + list(block)
+ paragraph, literalnext = self.paragraph(lines, startline)
+ self.parent += paragraph
+ self.parent += msg
+ if literalnext:
+ try:
+ self.state_machine.next_line()
+ except EOFError:
+ pass
+ self.parent += self.literal_block()
+ return [], next_state, []
+
+ def literal_block(self):
+ """Return a list of nodes."""
+ indented, indent, offset, blank_finish = \
+ self.state_machine.get_indented()
+ while indented and not indented[-1].strip():
+ indented.trim_end()
+ if not indented:
+ return self.quoted_literal_block()
+ data = '\n'.join(indented)
+ literal_block = nodes.literal_block(data, data)
+ literal_block.line = offset + 1
+ nodelist = [literal_block]
+ if not blank_finish:
+ nodelist.append(self.unindent_warning('Literal block'))
+ return nodelist
+
+ def quoted_literal_block(self):
+ abs_line_offset = self.state_machine.abs_line_offset()
+ offset = self.state_machine.line_offset
+ parent_node = nodes.Element()
+ new_abs_offset = self.nested_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=abs_line_offset, node=parent_node, match_titles=0,
+ state_machine_kwargs={'state_classes': (QuotedLiteralBlock,),
+ 'initial_state': 'QuotedLiteralBlock'})
+ self.goto_line(new_abs_offset)
+ return parent_node.children
+
+ def definition_list_item(self, termline):
+ indented, indent, line_offset, blank_finish = \
+ self.state_machine.get_indented()
+ definitionlistitem = nodes.definition_list_item(
+ '\n'.join(termline + list(indented)))
+ lineno = self.state_machine.abs_line_number() - 1
+ definitionlistitem.line = lineno
+ termlist, messages = self.term(termline, lineno)
+ definitionlistitem += termlist
+ definition = nodes.definition('', *messages)
+ definitionlistitem += definition
+ if termline[0][-2:] == '::':
+ definition += self.reporter.info(
+ 'Blank line missing before literal block (after the "::")? '
+ 'Interpreted as a definition list item.', line=line_offset+1)
+ self.nested_parse(indented, input_offset=line_offset, node=definition)
+ return definitionlistitem, blank_finish
+
+ classifier_delimiter = re.compile(' +: +')
+
+ def term(self, lines, lineno):
+ """Return a definition_list's term and optional classifiers."""
+ assert len(lines) == 1
+ text_nodes, messages = self.inline_text(lines[0], lineno)
+ term_node = nodes.term()
+ node_list = [term_node]
+ for i in range(len(text_nodes)):
+ node = text_nodes[i]
+ if isinstance(node, nodes.Text):
+ parts = self.classifier_delimiter.split(node.rawsource)
+ if len(parts) == 1:
+ node_list[-1] += node
+ else:
+
+ node_list[-1] += nodes.Text(parts[0].rstrip())
+ for part in parts[1:]:
+ classifier_node = nodes.classifier('', part)
+ node_list.append(classifier_node)
+ else:
+ node_list[-1] += node
+ return node_list, messages
+
+
+class SpecializedText(Text):
+
+ """
+ Superclass for second and subsequent lines of Text-variants.
+
+ All transition methods are disabled. Override individual methods in
+ subclasses to re-enable.
+ """
+
+ def eof(self, context):
+ """Incomplete construct."""
+ return []
+
+ def invalid_input(self, match=None, context=None, next_state=None):
+ """Not a compound element member. Abort this state machine."""
+ raise EOFError
+
+ blank = invalid_input
+ indent = invalid_input
+ underline = invalid_input
+ text = invalid_input
+
+
+class Definition(SpecializedText):
+
+ """Second line of potential definition_list_item."""
+
+ def eof(self, context):
+ """Not a definition."""
+ self.state_machine.previous_line(2) # so parent SM can reassess
+ return []
+
+ def indent(self, match, context, next_state):
+ """Definition list item."""
+ definitionlistitem, blank_finish = self.definition_list_item(context)
+ self.parent += definitionlistitem
+ self.blank_finish = blank_finish
+ return [], 'DefinitionList', []
+
+
+class Line(SpecializedText):
+
+ """
+ Second line of over- & underlined section title or transition marker.
+ """
+
+ eofcheck = 1 # @@@ ???
+ """Set to 0 while parsing sections, so that we don't catch the EOF."""
+
+ def eof(self, context):
+ """Transition marker at end of section or document."""
+ marker = context[0].strip()
+ if self.memo.section_bubble_up_kludge:
+ self.memo.section_bubble_up_kludge = 0
+ elif len(marker) < 4:
+ self.state_correction(context)
+ if self.eofcheck: # ignore EOFError with sections
+ lineno = self.state_machine.abs_line_number() - 1
+ transition = nodes.transition(rawsource=context[0])
+ transition.line = lineno
+ self.parent += transition
+ self.eofcheck = 1
+ return []
+
+ def blank(self, match, context, next_state):
+ """Transition marker."""
+ lineno = self.state_machine.abs_line_number() - 1
+ marker = context[0].strip()
+ if len(marker) < 4:
+ self.state_correction(context)
+ transition = nodes.transition(rawsource=marker)
+ transition.line = lineno
+ self.parent += transition
+ return [], 'Body', []
+
+ def text(self, match, context, next_state):
+ """Potential over- & underlined title."""
+ lineno = self.state_machine.abs_line_number() - 1
+ overline = context[0]
+ title = match.string
+ underline = ''
+ try:
+ underline = self.state_machine.next_line()
+ except EOFError:
+ blocktext = overline + '\n' + title
+ if len(overline.rstrip()) < 4:
+ self.short_overline(context, blocktext, lineno, 2)
+ else:
+ msg = self.reporter.severe(
+ 'Incomplete section title.',
+ nodes.literal_block(blocktext, blocktext), line=lineno)
+ self.parent += msg
+ return [], 'Body', []
+ source = '%s\n%s\n%s' % (overline, title, underline)
+ overline = overline.rstrip()
+ underline = underline.rstrip()
+ if not self.transitions['underline'][0].match(underline):
+ blocktext = overline + '\n' + title + '\n' + underline
+ if len(overline.rstrip()) < 4:
+ self.short_overline(context, blocktext, lineno, 2)
+ else:
+ msg = self.reporter.severe(
+ 'Missing matching underline for section title overline.',
+ nodes.literal_block(source, source), line=lineno)
+ self.parent += msg
+ return [], 'Body', []
+ elif overline != underline:
+ blocktext = overline + '\n' + title + '\n' + underline
+ if len(overline.rstrip()) < 4:
+ self.short_overline(context, blocktext, lineno, 2)
+ else:
+ msg = self.reporter.severe(
+ 'Title overline & underline mismatch.',
+ nodes.literal_block(source, source), line=lineno)
+ self.parent += msg
+ return [], 'Body', []
+ title = title.rstrip()
+ messages = []
+ if len(title) > len(overline):
+ blocktext = overline + '\n' + title + '\n' + underline
+ if len(overline.rstrip()) < 4:
+ self.short_overline(context, blocktext, lineno, 2)
+ else:
+ msg = self.reporter.warning(
+ 'Title overline too short.',
+ nodes.literal_block(source, source), line=lineno)
+ messages.append(msg)
+ style = (overline[0], underline[0])
+ self.eofcheck = 0 # @@@ not sure this is correct
+ self.section(title.lstrip(), source, style, lineno + 1, messages)
+ self.eofcheck = 1
+ return [], 'Body', []
+
+ indent = text # indented title
+
+ def underline(self, match, context, next_state):
+ overline = context[0]
+ blocktext = overline + '\n' + self.state_machine.line
+ lineno = self.state_machine.abs_line_number() - 1
+ if len(overline.rstrip()) < 4:
+ self.short_overline(context, blocktext, lineno, 1)
+ msg = self.reporter.error(
+ 'Invalid section title or transition marker.',
+ nodes.literal_block(blocktext, blocktext), line=lineno)
+ self.parent += msg
+ return [], 'Body', []
+
+ def short_overline(self, context, blocktext, lineno, lines=1):
+ msg = self.reporter.info(
+ 'Possible incomplete section title.\nTreating the overline as '
+ "ordinary text because it's so short.", line=lineno)
+ self.parent += msg
+ self.state_correction(context, lines)
+
+ def state_correction(self, context, lines=1):
+ self.state_machine.previous_line(lines)
+ context[:] = []
+ raise statemachine.StateCorrection('Body', 'text')
+
+
+class QuotedLiteralBlock(RSTState):
+
+ """
+ Nested parse handler for quoted (unindented) literal blocks.
+
+ Special-purpose. Not for inclusion in `state_classes`.
+ """
+
+ patterns = {'initial_quoted': r'(%(nonalphanum7bit)s)' % Body.pats,
+ 'text': r''}
+ initial_transitions = ('initial_quoted', 'text')
+
+ def __init__(self, state_machine, debug=0):
+ RSTState.__init__(self, state_machine, debug)
+ self.messages = []
+ self.initial_lineno = None
+
+ def blank(self, match, context, next_state):
+ if context:
+ raise EOFError
+ else:
+ return context, next_state, []
+
+ def eof(self, context):
+ if context:
+ text = '\n'.join(context)
+ literal_block = nodes.literal_block(text, text)
+ literal_block.line = self.initial_lineno
+ self.parent += literal_block
+ else:
+ self.parent += self.reporter.warning(
+ 'Literal block expected; none found.',
+ line=self.state_machine.abs_line_number())
+ self.state_machine.previous_line()
+ self.parent += self.messages
+ return []
+
+ def indent(self, match, context, next_state):
+ assert context, ('QuotedLiteralBlock.indent: context should not '
+ 'be empty!')
+ self.messages.append(
+ self.reporter.error('Unexpected indentation.',
+ line=self.state_machine.abs_line_number()))
+ self.state_machine.previous_line()
+ raise EOFError
+
+ def initial_quoted(self, match, context, next_state):
+ """Match arbitrary quote character on the first line only."""
+ self.remove_transition('initial_quoted')
+ quote = match.string[0]
+ pattern = re.compile(re.escape(quote))
+ # New transition matches consistent quotes only:
+ self.add_transition('quoted',
+ (pattern, self.quoted, self.__class__.__name__))
+ self.initial_lineno = self.state_machine.abs_line_number()
+ return [match.string], next_state, []
+
+ def quoted(self, match, context, next_state):
+ """Match consistent quotes on subsequent lines."""
+ context.append(match.string)
+ return context, next_state, []
+
+ def text(self, match, context, next_state):
+ if context:
+ self.messages.append(
+ self.reporter.error('Inconsistent literal block quoting.',
+ line=self.state_machine.abs_line_number()))
+ self.state_machine.previous_line()
+ raise EOFError
+
+
+state_classes = (Body, BulletList, DefinitionList, EnumeratedList, FieldList,
+ OptionList, LineBlock, ExtensionOptions, Explicit, Text,
+ Definition, Line, SubstitutionDef, RFC2822Body, RFC2822List)
+"""Standard set of State classes used to start `RSTStateMachine`."""
diff --git a/docutils/parsers/rst/tableparser.py b/docutils/parsers/rst/tableparser.py
new file mode 100644
index 000000000..8529a65b0
--- /dev/null
+++ b/docutils/parsers/rst/tableparser.py
@@ -0,0 +1,522 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision$
+# Date: $Date$
+# Copyright: This module has been placed in the public domain.
+
+"""
+This module defines table parser classes,which parse plaintext-graphic tables
+and produce a well-formed data structure suitable for building a CALS table.
+
+:Classes:
+ - `GridTableParser`: Parse fully-formed tables represented with a grid.
+ - `SimpleTableParser`: Parse simple tables, delimited by top & bottom
+ borders.
+
+:Exception class: `TableMarkupError`
+
+:Function:
+ `update_dict_of_lists()`: Merge two dictionaries containing list values.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import re
+import sys
+from docutils import DataError
+
+
+class TableMarkupError(DataError): pass
+
+
+class TableParser:
+
+ """
+ Abstract superclass for the common parts of the syntax-specific parsers.
+ """
+
+ head_body_separator_pat = None
+ """Matches the row separator between head rows and body rows."""
+
+ def parse(self, block):
+ """
+ Analyze the text `block` and return a table data structure.
+
+ Given a plaintext-graphic table in `block` (list of lines of text; no
+ whitespace padding), parse the table, construct and return the data
+ necessary to construct a CALS table or equivalent.
+
+ Raise `TableMarkupError` if there is any problem with the markup.
+ """
+ self.setup(block)
+ self.find_head_body_sep()
+ self.parse_table()
+ structure = self.structure_from_cells()
+ return structure
+
+ def find_head_body_sep(self):
+ """Look for a head/body row separator line; store the line index."""
+ for i in range(len(self.block)):
+ line = self.block[i]
+ if self.head_body_separator_pat.match(line):
+ if self.head_body_sep:
+ raise TableMarkupError(
+ 'Multiple head/body row separators in table (at line '
+ 'offset %s and %s); only one allowed.'
+ % (self.head_body_sep, i))
+ else:
+ self.head_body_sep = i
+ self.block[i] = line.replace('=', '-')
+ if self.head_body_sep == 0 or self.head_body_sep == (len(self.block)
+ - 1):
+ raise TableMarkupError('The head/body row separator may not be '
+ 'the first or last line of the table.')
+
+
+class GridTableParser(TableParser):
+
+ """
+ Parse a grid table using `parse()`.
+
+ Here's an example of a grid table::
+
+ +------------------------+------------+----------+----------+
+ | Header row, column 1 | Header 2 | Header 3 | Header 4 |
+ +========================+============+==========+==========+
+ | body row 1, column 1 | column 2 | column 3 | column 4 |
+ +------------------------+------------+----------+----------+
+ | body row 2 | Cells may span columns. |
+ +------------------------+------------+---------------------+
+ | body row 3 | Cells may | - Table cells |
+ +------------------------+ span rows. | - contain |
+ | body row 4 | | - body elements. |
+ +------------------------+------------+---------------------+
+
+ Intersections use '+', row separators use '-' (except for one optional
+ head/body row separator, which uses '='), and column separators use '|'.
+
+ Passing the above table to the `parse()` method will result in the
+ following data structure::
+
+ ([24, 12, 10, 10],
+ [[(0, 0, 1, ['Header row, column 1']),
+ (0, 0, 1, ['Header 2']),
+ (0, 0, 1, ['Header 3']),
+ (0, 0, 1, ['Header 4'])]],
+ [[(0, 0, 3, ['body row 1, column 1']),
+ (0, 0, 3, ['column 2']),
+ (0, 0, 3, ['column 3']),
+ (0, 0, 3, ['column 4'])],
+ [(0, 0, 5, ['body row 2']),
+ (0, 2, 5, ['Cells may span columns.']),
+ None,
+ None],
+ [(0, 0, 7, ['body row 3']),
+ (1, 0, 7, ['Cells may', 'span rows.', '']),
+ (1, 1, 7, ['- Table cells', '- contain', '- body elements.']),
+ None],
+ [(0, 0, 9, ['body row 4']), None, None, None]])
+
+ The first item is a list containing column widths (colspecs). The second
+ item is a list of head rows, and the third is a list of body rows. Each
+ row contains a list of cells. Each cell is either None (for a cell unused
+ because of another cell's span), or a tuple. A cell tuple contains four
+ items: the number of extra rows used by the cell in a vertical span
+ (morerows); the number of extra columns used by the cell in a horizontal
+ span (morecols); the line offset of the first line of the cell contents;
+ and the cell contents, a list of lines of text.
+ """
+
+ head_body_separator_pat = re.compile(r'\+=[=+]+=\+ *$')
+
+ def setup(self, block):
+ self.block = block[:] # make a copy; it may be modified
+ self.block.disconnect() # don't propagate changes to parent
+ self.bottom = len(block) - 1
+ self.right = len(block[0]) - 1
+ self.head_body_sep = None
+ self.done = [-1] * len(block[0])
+ self.cells = []
+ self.rowseps = {0: [0]}
+ self.colseps = {0: [0]}
+
+ def parse_table(self):
+ """
+ Start with a queue of upper-left corners, containing the upper-left
+ corner of the table itself. Trace out one rectangular cell, remember
+ it, and add its upper-right and lower-left corners to the queue of
+ potential upper-left corners of further cells. Process the queue in
+ top-to-bottom order, keeping track of how much of each text column has
+ been seen.
+
+ We'll end up knowing all the row and column boundaries, cell positions
+ and their dimensions.
+ """
+ corners = [(0, 0)]
+ while corners:
+ top, left = corners.pop(0)
+ if top == self.bottom or left == self.right \
+ or top <= self.done[left]:
+ continue
+ result = self.scan_cell(top, left)
+ if not result:
+ continue
+ bottom, right, rowseps, colseps = result
+ update_dict_of_lists(self.rowseps, rowseps)
+ update_dict_of_lists(self.colseps, colseps)
+ self.mark_done(top, left, bottom, right)
+ cellblock = self.block.get_2D_block(top + 1, left + 1,
+ bottom, right)
+ cellblock.disconnect() # lines in cell can't sync with parent
+ self.cells.append((top, left, bottom, right, cellblock))
+ corners.extend([(top, right), (bottom, left)])
+ corners.sort()
+ if not self.check_parse_complete():
+ raise TableMarkupError('Malformed table; parse incomplete.')
+
+ def mark_done(self, top, left, bottom, right):
+ """For keeping track of how much of each text column has been seen."""
+ before = top - 1
+ after = bottom - 1
+ for col in range(left, right):
+ assert self.done[col] == before
+ self.done[col] = after
+
+ def check_parse_complete(self):
+ """Each text column should have been completely seen."""
+ last = self.bottom - 1
+ for col in range(self.right):
+ if self.done[col] != last:
+ return None
+ return 1
+
+ def scan_cell(self, top, left):
+ """Starting at the top-left corner, start tracing out a cell."""
+ assert self.block[top][left] == '+'
+ result = self.scan_right(top, left)
+ return result
+
+ def scan_right(self, top, left):
+ """
+ Look for the top-right corner of the cell, and make note of all column
+ boundaries ('+').
+ """
+ colseps = {}
+ line = self.block[top]
+ for i in range(left + 1, self.right + 1):
+ if line[i] == '+':
+ colseps[i] = [top]
+ result = self.scan_down(top, left, i)
+ if result:
+ bottom, rowseps, newcolseps = result
+ update_dict_of_lists(colseps, newcolseps)
+ return bottom, i, rowseps, colseps
+ elif line[i] != '-':
+ return None
+ return None
+
+ def scan_down(self, top, left, right):
+ """
+ Look for the bottom-right corner of the cell, making note of all row
+ boundaries.
+ """
+ rowseps = {}
+ for i in range(top + 1, self.bottom + 1):
+ if self.block[i][right] == '+':
+ rowseps[i] = [right]
+ result = self.scan_left(top, left, i, right)
+ if result:
+ newrowseps, colseps = result
+ update_dict_of_lists(rowseps, newrowseps)
+ return i, rowseps, colseps
+ elif self.block[i][right] != '|':
+ return None
+ return None
+
+ def scan_left(self, top, left, bottom, right):
+ """
+ Noting column boundaries, look for the bottom-left corner of the cell.
+ It must line up with the starting point.
+ """
+ colseps = {}
+ line = self.block[bottom]
+ for i in range(right - 1, left, -1):
+ if line[i] == '+':
+ colseps[i] = [bottom]
+ elif line[i] != '-':
+ return None
+ if line[left] != '+':
+ return None
+ result = self.scan_up(top, left, bottom, right)
+ if result is not None:
+ rowseps = result
+ return rowseps, colseps
+ return None
+
+ def scan_up(self, top, left, bottom, right):
+ """
+ Noting row boundaries, see if we can return to the starting point.
+ """
+ rowseps = {}
+ for i in range(bottom - 1, top, -1):
+ if self.block[i][left] == '+':
+ rowseps[i] = [left]
+ elif self.block[i][left] != '|':
+ return None
+ return rowseps
+
+ def structure_from_cells(self):
+ """
+ From the data collected by `scan_cell()`, convert to the final data
+ structure.
+ """
+ rowseps = self.rowseps.keys() # list of row boundaries
+ rowseps.sort()
+ rowindex = {}
+ for i in range(len(rowseps)):
+ rowindex[rowseps[i]] = i # row boundary -> row number mapping
+ colseps = self.colseps.keys() # list of column boundaries
+ colseps.sort()
+ colindex = {}
+ for i in range(len(colseps)):
+ colindex[colseps[i]] = i # column boundary -> col number map
+ colspecs = [(colseps[i] - colseps[i - 1] - 1)
+ for i in range(1, len(colseps))] # list of column widths
+ # prepare an empty table with the correct number of rows & columns
+ onerow = [None for i in range(len(colseps) - 1)]
+ rows = [onerow[:] for i in range(len(rowseps) - 1)]
+ # keep track of # of cells remaining; should reduce to zero
+ remaining = (len(rowseps) - 1) * (len(colseps) - 1)
+ for top, left, bottom, right, block in self.cells:
+ rownum = rowindex[top]
+ colnum = colindex[left]
+ assert rows[rownum][colnum] is None, (
+ 'Cell (row %s, column %s) already used.'
+ % (rownum + 1, colnum + 1))
+ morerows = rowindex[bottom] - rownum - 1
+ morecols = colindex[right] - colnum - 1
+ remaining -= (morerows + 1) * (morecols + 1)
+ # write the cell into the table
+ rows[rownum][colnum] = (morerows, morecols, top + 1, block)
+ assert remaining == 0, 'Unused cells remaining.'
+ if self.head_body_sep: # separate head rows from body rows
+ numheadrows = rowindex[self.head_body_sep]
+ headrows = rows[:numheadrows]
+ bodyrows = rows[numheadrows:]
+ else:
+ headrows = []
+ bodyrows = rows
+ return (colspecs, headrows, bodyrows)
+
+
+class SimpleTableParser(TableParser):
+
+ """
+ Parse a simple table using `parse()`.
+
+ Here's an example of a simple table::
+
+ ===== =====
+ col 1 col 2
+ ===== =====
+ 1 Second column of row 1.
+ 2 Second column of row 2.
+ Second line of paragraph.
+ 3 - Second column of row 3.
+
+ - Second item in bullet
+ list (row 3, column 2).
+ 4 is a span
+ ------------
+ 5
+ ===== =====
+
+ Top and bottom borders use '=', column span underlines use '-', column
+ separation is indicated with spaces.
+
+ Passing the above table to the `parse()` method will result in the
+ following data structure, whose interpretation is the same as for
+ `GridTableParser`::
+
+ ([5, 25],
+ [[(0, 0, 1, ['col 1']),
+ (0, 0, 1, ['col 2'])]],
+ [[(0, 0, 3, ['1']),
+ (0, 0, 3, ['Second column of row 1.'])],
+ [(0, 0, 4, ['2']),
+ (0, 0, 4, ['Second column of row 2.',
+ 'Second line of paragraph.'])],
+ [(0, 0, 6, ['3']),
+ (0, 0, 6, ['- Second column of row 3.',
+ '',
+ '- Second item in bullet',
+ ' list (row 3, column 2).'])],
+ [(0, 1, 10, ['4 is a span'])],
+ [(0, 0, 12, ['5']),
+ (0, 0, 12, [''])]])
+ """
+
+ head_body_separator_pat = re.compile('=[ =]*$')
+ span_pat = re.compile('-[ -]*$')
+
+ def setup(self, block):
+ self.block = block[:] # make a copy; it will be modified
+ self.block.disconnect() # don't propagate changes to parent
+ # Convert top & bottom borders to column span underlines:
+ self.block[0] = self.block[0].replace('=', '-')
+ self.block[-1] = self.block[-1].replace('=', '-')
+ self.head_body_sep = None
+ self.columns = []
+ self.border_end = None
+ self.table = []
+ self.done = [-1] * len(block[0])
+ self.rowseps = {0: [0]}
+ self.colseps = {0: [0]}
+
+ def parse_table(self):
+ """
+ First determine the column boundaries from the top border, then
+ process rows. Each row may consist of multiple lines; accumulate
+ lines until a row is complete. Call `self.parse_row` to finish the
+ job.
+ """
+ # Top border must fully describe all table columns.
+ self.columns = self.parse_columns(self.block[0], 0)
+ self.border_end = self.columns[-1][1]
+ firststart, firstend = self.columns[0]
+ offset = 1 # skip top border
+ start = 1
+ text_found = None
+ while offset < len(self.block):
+ line = self.block[offset]
+ if self.span_pat.match(line):
+ # Column span underline or border; row is complete.
+ self.parse_row(self.block[start:offset], start,
+ (line.rstrip(), offset))
+ start = offset + 1
+ text_found = None
+ elif line[firststart:firstend].strip():
+ # First column not blank, therefore it's a new row.
+ if text_found and offset != start:
+ self.parse_row(self.block[start:offset], start)
+ start = offset
+ text_found = 1
+ elif not text_found:
+ start = offset + 1
+ offset += 1
+
+ def parse_columns(self, line, offset):
+ """
+ Given a column span underline, return a list of (begin, end) pairs.
+ """
+ cols = []
+ end = 0
+ while 1:
+ begin = line.find('-', end)
+ end = line.find(' ', begin)
+ if begin < 0:
+ break
+ if end < 0:
+ end = len(line)
+ cols.append((begin, end))
+ if self.columns:
+ if cols[-1][1] != self.border_end:
+ raise TableMarkupError('Column span incomplete at line '
+ 'offset %s.' % offset)
+ # Allow for an unbounded rightmost column:
+ cols[-1] = (cols[-1][0], self.columns[-1][1])
+ return cols
+
+ def init_row(self, colspec, offset):
+ i = 0
+ cells = []
+ for start, end in colspec:
+ morecols = 0
+ try:
+ assert start == self.columns[i][0]
+ while end != self.columns[i][1]:
+ i += 1
+ morecols += 1
+ except (AssertionError, IndexError):
+ raise TableMarkupError('Column span alignment problem at '
+ 'line offset %s.' % (offset + 1))
+ cells.append([0, morecols, offset, []])
+ i += 1
+ return cells
+
+ def parse_row(self, lines, start, spanline=None):
+ """
+ Given the text `lines` of a row, parse it and append to `self.table`.
+
+ The row is parsed according to the current column spec (either
+ `spanline` if provided or `self.columns`). For each column, extract
+ text from each line, and check for text in column margins. Finally,
+ adjust for insigificant whitespace.
+ """
+ if not (lines or spanline):
+ # No new row, just blank lines.
+ return
+ if spanline:
+ columns = self.parse_columns(*spanline)
+ span_offset = spanline[1]
+ else:
+ columns = self.columns[:]
+ span_offset = start
+ self.check_columns(lines, start, columns)
+ row = self.init_row(columns, start)
+ for i in range(len(columns)):
+ start, end = columns[i]
+ cellblock = lines.get_2D_block(0, start, len(lines), end)
+ cellblock.disconnect() # lines in cell can't sync with parent
+ row[i][3] = cellblock
+ self.table.append(row)
+
+ def check_columns(self, lines, first_line, columns):
+ """
+ Check for text in column margins and text overflow in the last column.
+ Raise TableMarkupError if anything but whitespace is in column margins.
+ Adjust the end value for the last column if there is text overflow.
+ """
+ # "Infinite" value for a dummy last column's beginning, used to
+ # check for text overflow:
+ columns.append((sys.maxint, None))
+ lastcol = len(columns) - 2
+ for i in range(len(columns) - 1):
+ start, end = columns[i]
+ nextstart = columns[i+1][0]
+ offset = 0
+ for line in lines:
+ if i == lastcol and line[end:].strip():
+ text = line[start:].rstrip()
+ new_end = start + len(text)
+ columns[i] = (start, new_end)
+ main_start, main_end = self.columns[-1]
+ if new_end > main_end:
+ self.columns[-1] = (main_start, new_end)
+ elif line[end:nextstart].strip():
+ raise TableMarkupError('Text in column margin at line '
+ 'offset %s.' % (first_line + offset))
+ offset += 1
+ columns.pop()
+
+ def structure_from_cells(self):
+ colspecs = [end - start for start, end in self.columns]
+ first_body_row = 0
+ if self.head_body_sep:
+ for i in range(len(self.table)):
+ if self.table[i][0][2] > self.head_body_sep:
+ first_body_row = i
+ break
+ return (colspecs, self.table[:first_body_row],
+ self.table[first_body_row:])
+
+
+def update_dict_of_lists(master, newdata):
+ """
+ Extend the list values of `master` with those from `newdata`.
+
+ Both parameters must be dictionaries containing list values.
+ """
+ for key, values in newdata.items():
+ master.setdefault(key, []).extend(values)