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