diff --git a/docutils/__init__.py b/docutils/__init__.py new file mode 100644 index 000000000..9f1798144 --- /dev/null +++ b/docutils/__init__.py @@ -0,0 +1,214 @@ +# $Id: __init__.py 7446 2012-06-17 20:47:10Z grubert $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +This is the Docutils (Python Documentation Utilities) package. + +Package Structure +================= + +Modules: + +- __init__.py: Contains component base classes, exception classes, and + Docutils version information. + +- core.py: Contains the ``Publisher`` class and ``publish_*()`` convenience + functions. + +- frontend.py: Runtime settings (command-line interface, configuration files) + processing, for Docutils front-ends. + +- io.py: Provides a uniform API for low-level input and output. + +- nodes.py: Docutils document tree (doctree) node class library. + +- statemachine.py: A finite state machine specialized for + regular-expression-based text filters. + +- urischemes.py: Contains a complete mapping of known URI addressing + scheme names to descriptions. + +Subpackages: + +- languages: Language-specific mappings of terms. + +- parsers: Syntax-specific input parser modules or packages. + +- readers: Context-specific input handlers which understand the data + source and manage a parser. + +- transforms: Modules used by readers and writers to modify DPS + doctrees. + +- utils: Contains the ``Reporter`` system warning class and miscellaneous + utilities used by readers, writers, and transforms. + +- writers: Format-specific output translators. +""" + +__docformat__ = 'reStructuredText' + +__version__ = '0.9.1' +"""``major.minor.micro`` version number. The micro number is bumped for API +changes, for new functionality, and for interim project releases. The minor +number is bumped whenever there is a significant project release. The major +number will be bumped when the project is feature-complete, and perhaps if +there is a major change in the design.""" + +__version_details__ = 'release' +"""Extra version details (e.g. 'snapshot 2005-05-29, r3410', 'repository', +'release'), modified automatically & manually.""" + +import sys + +class ApplicationError(StandardError): + # Workaround: + # In Python < 2.6, unicode() calls `str` on the + # arg and therefore, e.g., unicode(StandardError(u'\u234')) fails + # with UnicodeDecodeError. + if sys.version_info < (2,6): + def __unicode__(self): + return u', '.join(self.args) + +class DataError(ApplicationError): pass + + +class SettingsSpec: + + """ + Runtime setting specification base class. + + SettingsSpec subclass objects used by `docutils.frontend.OptionParser`. + """ + + settings_spec = () + """Runtime settings specification. Override in subclasses. + + Defines runtime settings and associated command-line options, as used by + `docutils.frontend.OptionParser`. This is a tuple of: + + - Option group title (string or `None` which implies no group, just a list + of single options). + + - Description (string or `None`). + + - A sequence of option tuples. Each consists of: + + - Help text (string) + + - List of option strings (e.g. ``['-Q', '--quux']``). + + - Dictionary of keyword arguments sent to the OptionParser/OptionGroup + ``add_option`` method. + + Runtime setting names are derived implicitly from long option names + ('--a-setting' becomes ``settings.a_setting``) or explicitly from the + 'dest' keyword argument. + + Most settings will also have a 'validator' keyword & function. The + validator function validates setting values (from configuration files + and command-line option arguments) and converts them to appropriate + types. For example, the ``docutils.frontend.validate_boolean`` + function, **required by all boolean settings**, converts true values + ('1', 'on', 'yes', and 'true') to 1 and false values ('0', 'off', + 'no', 'false', and '') to 0. Validators need only be set once per + setting. See the `docutils.frontend.validate_*` functions. + + See the optparse docs for more details. + + - More triples of group title, description, options, as many times as + needed. Thus, `settings_spec` tuples can be simply concatenated. + """ + + settings_defaults = None + """A dictionary of defaults for settings not in `settings_spec` (internal + settings, intended to be inaccessible by command-line and config file). + Override in subclasses.""" + + settings_default_overrides = None + """A dictionary of auxiliary defaults, to override defaults for settings + defined in other components. Override in subclasses.""" + + relative_path_settings = () + """Settings containing filesystem paths. Override in subclasses. + Settings listed here are to be interpreted relative to the current working + directory.""" + + config_section = None + """The name of the config file section specific to this component + (lowercase, no brackets). Override in subclasses.""" + + config_section_dependencies = None + """A list of names of config file sections that are to be applied before + `config_section`, in order (from general to specific). In other words, + the settings in `config_section` are to be overlaid on top of the settings + from these sections. The "general" section is assumed implicitly. + Override in subclasses.""" + + +class TransformSpec: + + """ + Runtime transform specification base class. + + TransformSpec subclass objects used by `docutils.transforms.Transformer`. + """ + + def get_transforms(self): + """Transforms required by this class. Override in subclasses.""" + if self.default_transforms != (): + import warnings + warnings.warn('default_transforms attribute deprecated.\n' + 'Use get_transforms() method instead.', + DeprecationWarning) + return list(self.default_transforms) + return [] + + # Deprecated; for compatibility. + default_transforms = () + + unknown_reference_resolvers = () + """List of functions to try to resolve unknown references. Unknown + references have a 'refname' attribute which doesn't correspond to any + target in the document. Called when the transforms in + `docutils.tranforms.references` are unable to find a correct target. The + list should contain functions which will try to resolve unknown + references, with the following signature:: + + def reference_resolver(node): + '''Returns boolean: true if resolved, false if not.''' + + If the function is able to resolve the reference, it should also remove + the 'refname' attribute and mark the node as resolved:: + + del node['refname'] + node.resolved = 1 + + Each function must have a "priority" attribute which will affect the order + the unknown_reference_resolvers are run:: + + reference_resolver.priority = 100 + + Override in subclasses.""" + + +class Component(SettingsSpec, TransformSpec): + + """Base class for Docutils components.""" + + component_type = None + """Name of the component type ('reader', 'parser', 'writer'). Override in + subclasses.""" + + supported = () + """Names for this component. Override in subclasses.""" + + def supports(self, format): + """ + Is `format` supported by this component? + + To be used by transforms to ask the dependent component if it supports + a certain input context or output format. + """ + return format in self.supported diff --git a/docutils/_compat.py b/docutils/_compat.py new file mode 100644 index 000000000..99f346666 --- /dev/null +++ b/docutils/_compat.py @@ -0,0 +1,37 @@ +# $Id: _compat.py 7316 2012-01-19 11:31:58Z milde $ +# Author: Georg Brandl +# Copyright: This module has been placed in the public domain. + +""" +Python 2/3 compatibility definitions. + +This module currently provides the following helper symbols: + +* bytes (name of byte string type; str in 2.x, bytes in 3.x) +* b (function converting a string literal to an ASCII byte string; + can be also used to convert a Unicode string into a byte string) +* u_prefix (unicode repr prefix: 'u' in 2.x, '' in 3.x) + (Required in docutils/test/test_publisher.py) +* BytesIO (a StringIO class that works with bytestrings) +""" + +import sys + +if sys.version_info < (3,0): + b = bytes = str + u_prefix = 'u' + from StringIO import StringIO as BytesIO +else: + import builtins + bytes = builtins.bytes + u_prefix = '' + def b(s): + if isinstance(s, str): + return s.encode('latin1') + elif isinstance(s, bytes): + return s + else: + raise TypeError("Invalid argument %r for b()" % (s,)) + # using this hack since 2to3 "fixes" the relative import + # when using ``from io import BytesIO`` + BytesIO = __import__('io').BytesIO diff --git a/docutils/_string_template_compat.py b/docutils/_string_template_compat.py new file mode 100644 index 000000000..38929c296 --- /dev/null +++ b/docutils/_string_template_compat.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +# string_template_compat.py: string.Template for Python <= 2.4 +# ===================================================== + +# This is just an excerpt of the standard string module to provide backwards +# compatibility. + +import re as _re + +class _multimap: + """Helper class for combining multiple mappings. + + Used by .{safe_,}substitute() to combine the mapping and keyword + arguments. + """ + def __init__(self, primary, secondary): + self._primary = primary + self._secondary = secondary + + def __getitem__(self, key): + try: + return self._primary[key] + except KeyError: + return self._secondary[key] + + +class _TemplateMetaclass(type): + pattern = r""" + %(delim)s(?: + (?P%(delim)s) | # Escape sequence of two delimiters + (?P%(id)s) | # delimiter and a Python identifier + {(?P%(id)s)} | # delimiter and a braced identifier + (?P) # Other ill-formed delimiter exprs + ) + """ + + def __init__(cls, name, bases, dct): + super(_TemplateMetaclass, cls).__init__(name, bases, dct) + if 'pattern' in dct: + pattern = cls.pattern + else: + pattern = _TemplateMetaclass.pattern % { + 'delim' : _re.escape(cls.delimiter), + 'id' : cls.idpattern, + } + cls.pattern = _re.compile(pattern, _re.IGNORECASE | _re.VERBOSE) + + +class Template: + """A string class for supporting $-substitutions.""" + __metaclass__ = _TemplateMetaclass + + delimiter = '$' + idpattern = r'[_a-z][_a-z0-9]*' + + def __init__(self, template): + self.template = template + + # Search for $$, $identifier, ${identifier}, and any bare $'s + + def _invalid(self, mo): + i = mo.start('invalid') + lines = self.template[:i].splitlines(True) + if not lines: + colno = 1 + lineno = 1 + else: + colno = i - len(''.join(lines[:-1])) + lineno = len(lines) + raise ValueError('Invalid placeholder in string: line %d, col %d' % + (lineno, colno)) + + def substitute(self, *args, **kws): + if len(args) > 1: + raise TypeError('Too many positional arguments') + if not args: + mapping = kws + elif kws: + mapping = _multimap(kws, args[0]) + else: + mapping = args[0] + # Helper function for .sub() + def convert(mo): + # Check the most common path first. + named = mo.group('named') or mo.group('braced') + if named is not None: + val = mapping[named] + # We use this idiom instead of str() because the latter will + # fail if val is a Unicode containing non-ASCII characters. + return '%s' % (val,) + if mo.group('escaped') is not None: + return self.delimiter + if mo.group('invalid') is not None: + self._invalid(mo) + raise ValueError('Unrecognized named group in pattern', + self.pattern) + return self.pattern.sub(convert, self.template) + + def safe_substitute(self, *args, **kws): + if len(args) > 1: + raise TypeError('Too many positional arguments') + if not args: + mapping = kws + elif kws: + mapping = _multimap(kws, args[0]) + else: + mapping = args[0] + # Helper function for .sub() + def convert(mo): + named = mo.group('named') + if named is not None: + try: + # We use this idiom instead of str() because the latter + # will fail if val is a Unicode containing non-ASCII + return '%s' % (mapping[named],) + except KeyError: + return self.delimiter + named + braced = mo.group('braced') + if braced is not None: + try: + return '%s' % (mapping[braced],) + except KeyError: + return self.delimiter + '{' + braced + '}' + if mo.group('escaped') is not None: + return self.delimiter + if mo.group('invalid') is not None: + return self.delimiter + raise ValueError('Unrecognized named group in pattern', + self.pattern) + return self.pattern.sub(convert, self.template) + diff --git a/docutils/core.py b/docutils/core.py new file mode 100644 index 000000000..0dea4ffc4 --- /dev/null +++ b/docutils/core.py @@ -0,0 +1,667 @@ +# $Id: core.py 7384 2012-03-19 22:59:09Z milde $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +Calling the ``publish_*`` convenience functions (or instantiating a +`Publisher` object) with component names will result in default +behavior. For custom behavior (setting component options), create +custom component objects first, and pass *them* to +``publish_*``/`Publisher`. See `The Docutils Publisher`_. + +.. _The Docutils Publisher: http://docutils.sf.net/docs/api/publisher.html +""" + +__docformat__ = 'reStructuredText' + +import sys +import pprint +from docutils import __version__, __version_details__, SettingsSpec +from docutils import frontend, io, utils, readers, writers +from docutils.frontend import OptionParser +from docutils.transforms import Transformer +from docutils.error_reporting import ErrorOutput, ErrorString +import docutils.readers.doctree + +class Publisher: + + """ + A facade encapsulating the high-level logic of a Docutils system. + """ + + def __init__(self, reader=None, parser=None, writer=None, + source=None, source_class=io.FileInput, + destination=None, destination_class=io.FileOutput, + settings=None): + """ + Initial setup. If any of `reader`, `parser`, or `writer` are not + specified, the corresponding ``set_...`` method should be called with + a component name (`set_reader` sets the parser as well). + """ + + self.document = None + """The document tree (`docutils.nodes` objects).""" + + self.reader = reader + """A `docutils.readers.Reader` instance.""" + + self.parser = parser + """A `docutils.parsers.Parser` instance.""" + + self.writer = writer + """A `docutils.writers.Writer` instance.""" + + for component in 'reader', 'parser', 'writer': + assert not isinstance(getattr(self, component), str), ( + 'passed string "%s" as "%s" parameter; pass an instance, ' + 'or use the "%s_name" parameter instead (in ' + 'docutils.core.publish_* convenience functions).' + % (getattr(self, component), component, component)) + + self.source = source + """The source of input data, a `docutils.io.Input` instance.""" + + self.source_class = source_class + """The class for dynamically created source objects.""" + + self.destination = destination + """The destination for docutils output, a `docutils.io.Output` + instance.""" + + self.destination_class = destination_class + """The class for dynamically created destination objects.""" + + self.settings = settings + """An object containing Docutils settings as instance attributes. + Set by `self.process_command_line()` or `self.get_settings()`.""" + + self._stderr = ErrorOutput() + + def set_reader(self, reader_name, parser, parser_name): + """Set `self.reader` by name.""" + reader_class = readers.get_reader_class(reader_name) + self.reader = reader_class(parser, parser_name) + self.parser = self.reader.parser + + def set_writer(self, writer_name): + """Set `self.writer` by name.""" + writer_class = writers.get_writer_class(writer_name) + self.writer = writer_class() + + def set_components(self, reader_name, parser_name, writer_name): + if self.reader is None: + self.set_reader(reader_name, self.parser, parser_name) + if self.parser is None: + if self.reader.parser is None: + self.reader.set_parser(parser_name) + self.parser = self.reader.parser + if self.writer is None: + self.set_writer(writer_name) + + def setup_option_parser(self, usage=None, description=None, + settings_spec=None, config_section=None, + **defaults): + if config_section: + if not settings_spec: + settings_spec = SettingsSpec() + settings_spec.config_section = config_section + parts = config_section.split() + if len(parts) > 1 and parts[-1] == 'application': + settings_spec.config_section_dependencies = ['applications'] + #@@@ Add self.source & self.destination to components in future? + option_parser = OptionParser( + components=(self.parser, self.reader, self.writer, settings_spec), + defaults=defaults, read_config_files=True, + usage=usage, description=description) + return option_parser + + def get_settings(self, usage=None, description=None, + settings_spec=None, config_section=None, **defaults): + """ + Set and return default settings (overrides in `defaults` dict). + + Set components first (`self.set_reader` & `self.set_writer`). + Explicitly setting `self.settings` disables command line option + processing from `self.publish()`. + """ + option_parser = self.setup_option_parser( + usage, description, settings_spec, config_section, **defaults) + self.settings = option_parser.get_default_values() + return self.settings + + def process_programmatic_settings(self, settings_spec, + settings_overrides, + config_section): + if self.settings is None: + defaults = (settings_overrides or {}).copy() + # Propagate exceptions by default when used programmatically: + defaults.setdefault('traceback', True) + self.get_settings(settings_spec=settings_spec, + config_section=config_section, + **defaults) + + def process_command_line(self, argv=None, usage=None, description=None, + settings_spec=None, config_section=None, + **defaults): + """ + Pass an empty list to `argv` to avoid reading `sys.argv` (the + default). + + Set components first (`self.set_reader` & `self.set_writer`). + """ + option_parser = self.setup_option_parser( + usage, description, settings_spec, config_section, **defaults) + if argv is None: + argv = sys.argv[1:] + # converting to Unicode (Python 3 does this automatically): + if sys.version_info < (3,0): + # TODO: make this failsafe and reversible? + argv_encoding = (frontend.locale_encoding or 'ascii') + argv = [a.decode(argv_encoding) for a in argv] + self.settings = option_parser.parse_args(argv) + + def set_io(self, source_path=None, destination_path=None): + if self.source is None: + self.set_source(source_path=source_path) + if self.destination is None: + self.set_destination(destination_path=destination_path) + + def set_source(self, source=None, source_path=None): + if source_path is None: + source_path = self.settings._source + else: + self.settings._source = source_path + # Raise IOError instead of system exit with `tracback == True` + # TODO: change io.FileInput's default behaviour and remove this hack + try: + self.source = self.source_class( + source=source, source_path=source_path, + encoding=self.settings.input_encoding, + handle_io_errors=False) + except TypeError: + self.source = self.source_class( + source=source, source_path=source_path, + encoding=self.settings.input_encoding) + + def set_destination(self, destination=None, destination_path=None): + if destination_path is None: + destination_path = self.settings._destination + else: + self.settings._destination = destination_path + self.destination = self.destination_class( + destination=destination, destination_path=destination_path, + encoding=self.settings.output_encoding, + error_handler=self.settings.output_encoding_error_handler) + # Raise IOError instead of system exit with `tracback == True` + # TODO: change io.FileInput's default behaviour and remove this hack + self.destination.handle_io_errors=False + + def apply_transforms(self): + self.document.transformer.populate_from_components( + (self.source, self.reader, self.reader.parser, self.writer, + self.destination)) + self.document.transformer.apply_transforms() + + def publish(self, argv=None, usage=None, description=None, + settings_spec=None, settings_overrides=None, + config_section=None, enable_exit_status=False): + """ + Process command line options and arguments (if `self.settings` not + already set), run `self.reader` and then `self.writer`. Return + `self.writer`'s output. + """ + exit = None + try: + if self.settings is None: + self.process_command_line( + argv, usage, description, settings_spec, config_section, + **(settings_overrides or {})) + self.set_io() + self.document = self.reader.read(self.source, self.parser, + self.settings) + self.apply_transforms() + output = self.writer.write(self.document, self.destination) + self.writer.assemble_parts() + except SystemExit, error: + exit = 1 + exit_status = error.code + except Exception, error: + if not self.settings: # exception too early to report nicely + raise + if self.settings.traceback: # Propagate exceptions? + self.debugging_dumps() + raise + self.report_Exception(error) + exit = True + exit_status = 1 + self.debugging_dumps() + if (enable_exit_status and self.document + and (self.document.reporter.max_level + >= self.settings.exit_status_level)): + sys.exit(self.document.reporter.max_level + 10) + elif exit: + sys.exit(exit_status) + return output + + def debugging_dumps(self): + if not self.document: + return + if self.settings.dump_settings: + print >>self._stderr, '\n::: Runtime settings:' + print >>self._stderr, pprint.pformat(self.settings.__dict__) + if self.settings.dump_internals: + print >>self._stderr, '\n::: Document internals:' + print >>self._stderr, pprint.pformat(self.document.__dict__) + if self.settings.dump_transforms: + print >>self._stderr, '\n::: Transforms applied:' + print >>self._stderr, (' (priority, transform class, ' + 'pending node details, keyword args)') + print >>self._stderr, pprint.pformat( + [(priority, '%s.%s' % (xclass.__module__, xclass.__name__), + pending and pending.details, kwargs) + for priority, xclass, pending, kwargs + in self.document.transformer.applied]) + if self.settings.dump_pseudo_xml: + print >>self._stderr, '\n::: Pseudo-XML:' + print >>self._stderr, self.document.pformat().encode( + 'raw_unicode_escape') + + def report_Exception(self, error): + if isinstance(error, utils.SystemMessage): + self.report_SystemMessage(error) + elif isinstance(error, UnicodeEncodeError): + self.report_UnicodeError(error) + elif isinstance(error, io.InputError): + self._stderr.write(u'Unable to open source file for reading:\n' + u' %s\n' % ErrorString(error)) + elif isinstance(error, io.OutputError): + self._stderr.write( + u'Unable to open destination file for writing:\n' + u' %s\n' % ErrorString(error)) + else: + print >>self._stderr, u'%s' % ErrorString(error) + print >>self._stderr, ("""\ +Exiting due to error. Use "--traceback" to diagnose. +Please report errors to . +Include "--traceback" output, Docutils version (%s [%s]), +Python version (%s), your OS type & version, and the +command line used.""" % (__version__, __version_details__, + sys.version.split()[0])) + + def report_SystemMessage(self, error): + print >>self._stderr, ('Exiting due to level-%s (%s) system message.' + % (error.level, + utils.Reporter.levels[error.level])) + + def report_UnicodeError(self, error): + data = error.object[error.start:error.end] + self._stderr.write( + '%s\n' + '\n' + 'The specified output encoding (%s) cannot\n' + 'handle all of the output.\n' + 'Try setting "--output-encoding-error-handler" to\n' + '\n' + '* "xmlcharrefreplace" (for HTML & XML output);\n' + ' the output will contain "%s" and should be usable.\n' + '* "backslashreplace" (for other output formats);\n' + ' look for "%s" in the output.\n' + '* "replace"; look for "?" in the output.\n' + '\n' + '"--output-encoding-error-handler" is currently set to "%s".\n' + '\n' + 'Exiting due to error. Use "--traceback" to diagnose.\n' + 'If the advice above doesn\'t eliminate the error,\n' + 'please report it to .\n' + 'Include "--traceback" output, Docutils version (%s),\n' + 'Python version (%s), your OS type & version, and the\n' + 'command line used.\n' + % (ErrorString(error), + self.settings.output_encoding, + data.encode('ascii', 'xmlcharrefreplace'), + data.encode('ascii', 'backslashreplace'), + self.settings.output_encoding_error_handler, + __version__, sys.version.split()[0])) + +default_usage = '%prog [options] [ []]' +default_description = ('Reads from (default is stdin) and writes to ' + ' (default is stdout). See ' + ' for ' + 'the full reference.') + +def publish_cmdline(reader=None, reader_name='standalone', + parser=None, parser_name='restructuredtext', + writer=None, writer_name='pseudoxml', + settings=None, settings_spec=None, + settings_overrides=None, config_section=None, + enable_exit_status=True, argv=None, + usage=default_usage, description=default_description): + """ + Set up & run a `Publisher` for command-line-based file I/O (input and + output file paths taken automatically from the command line). Return the + encoded string output also. + + Parameters: see `publish_programmatically` for the remainder. + + - `argv`: Command-line argument list to use instead of ``sys.argv[1:]``. + - `usage`: Usage string, output if there's a problem parsing the command + line. + - `description`: Program description, output for the "--help" option + (along with command-line option descriptions). + """ + pub = Publisher(reader, parser, writer, settings=settings) + pub.set_components(reader_name, parser_name, writer_name) + output = pub.publish( + argv, usage, description, settings_spec, settings_overrides, + config_section=config_section, enable_exit_status=enable_exit_status) + return output + +def publish_file(source=None, source_path=None, + destination=None, destination_path=None, + reader=None, reader_name='standalone', + parser=None, parser_name='restructuredtext', + writer=None, writer_name='pseudoxml', + settings=None, settings_spec=None, settings_overrides=None, + config_section=None, enable_exit_status=False): + """ + Set up & run a `Publisher` for programmatic use with file-like I/O. + Return the encoded string output also. + + Parameters: see `publish_programmatically`. + """ + output, pub = publish_programmatically( + source_class=io.FileInput, source=source, source_path=source_path, + destination_class=io.FileOutput, + destination=destination, destination_path=destination_path, + reader=reader, reader_name=reader_name, + parser=parser, parser_name=parser_name, + writer=writer, writer_name=writer_name, + settings=settings, settings_spec=settings_spec, + settings_overrides=settings_overrides, + config_section=config_section, + enable_exit_status=enable_exit_status) + return output + +def publish_string(source, source_path=None, destination_path=None, + reader=None, reader_name='standalone', + parser=None, parser_name='restructuredtext', + writer=None, writer_name='pseudoxml', + settings=None, settings_spec=None, + settings_overrides=None, config_section=None, + enable_exit_status=False): + """ + Set up & run a `Publisher` for programmatic use with string I/O. Return + the encoded string or Unicode string output. + + For encoded string output, be sure to set the 'output_encoding' setting to + the desired encoding. Set it to 'unicode' for unencoded Unicode string + output. Here's one way:: + + publish_string(..., settings_overrides={'output_encoding': 'unicode'}) + + Similarly for Unicode string input (`source`):: + + publish_string(..., settings_overrides={'input_encoding': 'unicode'}) + + Parameters: see `publish_programmatically`. + """ + output, pub = publish_programmatically( + source_class=io.StringInput, source=source, source_path=source_path, + destination_class=io.StringOutput, + destination=None, destination_path=destination_path, + reader=reader, reader_name=reader_name, + parser=parser, parser_name=parser_name, + writer=writer, writer_name=writer_name, + settings=settings, settings_spec=settings_spec, + settings_overrides=settings_overrides, + config_section=config_section, + enable_exit_status=enable_exit_status) + return output + +def publish_parts(source, source_path=None, source_class=io.StringInput, + destination_path=None, + reader=None, reader_name='standalone', + parser=None, parser_name='restructuredtext', + writer=None, writer_name='pseudoxml', + settings=None, settings_spec=None, + settings_overrides=None, config_section=None, + enable_exit_status=False): + """ + Set up & run a `Publisher`, and return a dictionary of document parts. + Dictionary keys are the names of parts, and values are Unicode strings; + encoding is up to the client. For programmatic use with string I/O. + + For encoded string input, be sure to set the 'input_encoding' setting to + the desired encoding. Set it to 'unicode' for unencoded Unicode string + input. Here's how:: + + publish_parts(..., settings_overrides={'input_encoding': 'unicode'}) + + Parameters: see `publish_programmatically`. + """ + output, pub = publish_programmatically( + source=source, source_path=source_path, source_class=source_class, + destination_class=io.StringOutput, + destination=None, destination_path=destination_path, + reader=reader, reader_name=reader_name, + parser=parser, parser_name=parser_name, + writer=writer, writer_name=writer_name, + settings=settings, settings_spec=settings_spec, + settings_overrides=settings_overrides, + config_section=config_section, + enable_exit_status=enable_exit_status) + return pub.writer.parts + +def publish_doctree(source, source_path=None, + source_class=io.StringInput, + reader=None, reader_name='standalone', + parser=None, parser_name='restructuredtext', + settings=None, settings_spec=None, + settings_overrides=None, config_section=None, + enable_exit_status=False): + """ + Set up & run a `Publisher` for programmatic use with string I/O. + Return the document tree. + + For encoded string input, be sure to set the 'input_encoding' setting to + the desired encoding. Set it to 'unicode' for unencoded Unicode string + input. Here's one way:: + + publish_doctree(..., settings_overrides={'input_encoding': 'unicode'}) + + Parameters: see `publish_programmatically`. + """ + pub = Publisher(reader=reader, parser=parser, writer=None, + settings=settings, + source_class=source_class, + destination_class=io.NullOutput) + pub.set_components(reader_name, parser_name, 'null') + pub.process_programmatic_settings( + settings_spec, settings_overrides, config_section) + pub.set_source(source, source_path) + pub.set_destination(None, None) + output = pub.publish(enable_exit_status=enable_exit_status) + return pub.document + +def publish_from_doctree(document, destination_path=None, + writer=None, writer_name='pseudoxml', + settings=None, settings_spec=None, + settings_overrides=None, config_section=None, + enable_exit_status=False): + """ + Set up & run a `Publisher` to render from an existing document + tree data structure, for programmatic use with string I/O. Return + the encoded string output. + + Note that document.settings is overridden; if you want to use the settings + of the original `document`, pass settings=document.settings. + + Also, new document.transformer and document.reporter objects are + generated. + + For encoded string output, be sure to set the 'output_encoding' setting to + the desired encoding. Set it to 'unicode' for unencoded Unicode string + output. Here's one way:: + + publish_from_doctree( + ..., settings_overrides={'output_encoding': 'unicode'}) + + Parameters: `document` is a `docutils.nodes.document` object, an existing + document tree. + + Other parameters: see `publish_programmatically`. + """ + reader = docutils.readers.doctree.Reader(parser_name='null') + pub = Publisher(reader, None, writer, + source=io.DocTreeInput(document), + destination_class=io.StringOutput, settings=settings) + if not writer and writer_name: + pub.set_writer(writer_name) + pub.process_programmatic_settings( + settings_spec, settings_overrides, config_section) + pub.set_destination(None, destination_path) + return pub.publish(enable_exit_status=enable_exit_status) + +def publish_cmdline_to_binary(reader=None, reader_name='standalone', + parser=None, parser_name='restructuredtext', + writer=None, writer_name='pseudoxml', + settings=None, settings_spec=None, + settings_overrides=None, config_section=None, + enable_exit_status=True, argv=None, + usage=default_usage, description=default_description, + destination=None, destination_class=io.BinaryFileOutput + ): + """ + Set up & run a `Publisher` for command-line-based file I/O (input and + output file paths taken automatically from the command line). Return the + encoded string output also. + + This is just like publish_cmdline, except that it uses + io.BinaryFileOutput instead of io.FileOutput. + + Parameters: see `publish_programmatically` for the remainder. + + - `argv`: Command-line argument list to use instead of ``sys.argv[1:]``. + - `usage`: Usage string, output if there's a problem parsing the command + line. + - `description`: Program description, output for the "--help" option + (along with command-line option descriptions). + """ + pub = Publisher(reader, parser, writer, settings=settings, + destination_class=destination_class) + pub.set_components(reader_name, parser_name, writer_name) + output = pub.publish( + argv, usage, description, settings_spec, settings_overrides, + config_section=config_section, enable_exit_status=enable_exit_status) + return output + +def publish_programmatically(source_class, source, source_path, + destination_class, destination, destination_path, + reader, reader_name, + parser, parser_name, + writer, writer_name, + settings, settings_spec, + settings_overrides, config_section, + enable_exit_status): + """ + Set up & run a `Publisher` for custom programmatic use. Return the + encoded string output and the Publisher object. + + Applications should not need to call this function directly. If it does + seem to be necessary to call this function directly, please write to the + Docutils-develop mailing list + . + + Parameters: + + * `source_class` **required**: The class for dynamically created source + objects. Typically `io.FileInput` or `io.StringInput`. + + * `source`: Type depends on `source_class`: + + - If `source_class` is `io.FileInput`: Either a file-like object + (must have 'read' and 'close' methods), or ``None`` + (`source_path` is opened). If neither `source` nor + `source_path` are supplied, `sys.stdin` is used. + + - If `source_class` is `io.StringInput` **required**: The input + string, either an encoded 8-bit string (set the + 'input_encoding' setting to the correct encoding) or a Unicode + string (set the 'input_encoding' setting to 'unicode'). + + * `source_path`: Type depends on `source_class`: + + - `io.FileInput`: Path to the input file, opened if no `source` + supplied. + + - `io.StringInput`: Optional. Path to the file or object that produced + `source`. Only used for diagnostic output. + + * `destination_class` **required**: The class for dynamically created + destination objects. Typically `io.FileOutput` or `io.StringOutput`. + + * `destination`: Type depends on `destination_class`: + + - `io.FileOutput`: Either a file-like object (must have 'write' and + 'close' methods), or ``None`` (`destination_path` is opened). If + neither `destination` nor `destination_path` are supplied, + `sys.stdout` is used. + + - `io.StringOutput`: Not used; pass ``None``. + + * `destination_path`: Type depends on `destination_class`: + + - `io.FileOutput`: Path to the output file. Opened if no `destination` + supplied. + + - `io.StringOutput`: Path to the file or object which will receive the + output; optional. Used for determining relative paths (stylesheets, + source links, etc.). + + * `reader`: A `docutils.readers.Reader` object. + + * `reader_name`: Name or alias of the Reader class to be instantiated if + no `reader` supplied. + + * `parser`: A `docutils.parsers.Parser` object. + + * `parser_name`: Name or alias of the Parser class to be instantiated if + no `parser` supplied. + + * `writer`: A `docutils.writers.Writer` object. + + * `writer_name`: Name or alias of the Writer class to be instantiated if + no `writer` supplied. + + * `settings`: A runtime settings (`docutils.frontend.Values`) object, for + dotted-attribute access to runtime settings. It's the end result of the + `SettingsSpec`, config file, and option processing. If `settings` is + passed, it's assumed to be complete and no further setting/config/option + processing is done. + + * `settings_spec`: A `docutils.SettingsSpec` subclass or object. Provides + extra application-specific settings definitions independently of + components. In other words, the application becomes a component, and + its settings data is processed along with that of the other components. + Used only if no `settings` specified. + + * `settings_overrides`: A dictionary containing application-specific + settings defaults that override the defaults of other components. + Used only if no `settings` specified. + + * `config_section`: A string, the name of the configuration file section + for this application. Overrides the ``config_section`` attribute + defined by `settings_spec`. Used only if no `settings` specified. + + * `enable_exit_status`: Boolean; enable exit status at end of processing? + """ + pub = Publisher(reader, parser, writer, settings=settings, + source_class=source_class, + destination_class=destination_class) + pub.set_components(reader_name, parser_name, writer_name) + pub.process_programmatic_settings( + settings_spec, settings_overrides, config_section) + pub.set_source(source, source_path) + pub.set_destination(destination, destination_path) + output = pub.publish(enable_exit_status=enable_exit_status) + return output, pub diff --git a/docutils/docutils.conf b/docutils/docutils.conf new file mode 100644 index 000000000..cdce8d629 --- /dev/null +++ b/docutils/docutils.conf @@ -0,0 +1,5 @@ +# This configuration file is to prevent tools/buildhtml.py from +# processing text files in and below this directory. + +[buildhtml application] +prune: . diff --git a/docutils/error_reporting.py b/docutils/error_reporting.py new file mode 100644 index 000000000..797dc41c0 --- /dev/null +++ b/docutils/error_reporting.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +# :Id: $Id: error_reporting.py 7423 2012-05-03 11:01:54Z milde $ +# :Copyright: © 2011 Günter Milde. +# :License: Released under the terms of the `2-Clause BSD license`_, in short: +# +# Copying and distribution of this file, with or without modification, +# are permitted in any medium without royalty provided the copyright +# notice and this notice are preserved. +# This file is offered as-is, without any warranty. +# +# .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause + +""" +Error reporting should be safe from encoding/decoding errors. +However, implicit conversions of strings and exceptions like + +>>> u'%s world: %s' % ('H\xe4llo', Exception(u'H\xe4llo') + +fail in some Python versions: + +* In Python <= 2.6, ``unicode()`` uses + `__str__` and fails with non-ASCII chars in`unicode` arguments. + (work around http://bugs.python.org/issue2517): + +* In Python 2, unicode() fails, with non-ASCII + chars in arguments. (Use case: in some locales, the errstr + argument of IOError contains non-ASCII chars.) + +* In Python 2, str() fails, with non-ASCII chars + in `unicode` arguments. + +The `SafeString`, `ErrorString` and `ErrorOutput` classes handle +common exceptions. +""" + +import sys, codecs + +# Guess the locale's encoding. +# If no valid guess can be made, locale_encoding is set to `None`: +try: + import locale # module missing in Jython +except ImportError: + locale_encoding = None +else: + locale_encoding = locale.getlocale()[1] or locale.getdefaultlocale()[1] + # locale.getpreferredencoding([do_setlocale=True|False]) + # has side-effects | might return a wrong guess. + # (cf. Update 1 in http://stackoverflow.com/questions/4082645/using-python-2-xs-locale-module-to-format-numbers-and-currency) + try: + codecs.lookup(locale_encoding or '') # None -> '' + except LookupError: + locale_encoding = None + + + +class SafeString(object): + """ + A wrapper providing robust conversion to `str` and `unicode`. + """ + + def __init__(self, data, encoding=None, encoding_errors='backslashreplace', + decoding_errors='replace'): + self.data = data + self.encoding = (encoding or getattr(data, 'encoding', None) or + locale_encoding or 'ascii') + self.encoding_errors = encoding_errors + self.decoding_errors = decoding_errors + + + def __str__(self): + try: + return str(self.data) + except UnicodeEncodeError, err: + if isinstance(self.data, Exception): + args = [str(SafeString(arg, self.encoding, + self.encoding_errors)) + for arg in self.data.args] + return ', '.join(args) + if isinstance(self.data, unicode): + return self.data.encode(self.encoding, self.encoding_errors) + raise + + def __unicode__(self): + """ + Return unicode representation of `self.data`. + + Try ``unicode(self.data)``, catch `UnicodeError` and + + * if `self.data` is an Exception instance, work around + http://bugs.python.org/issue2517 with an emulation of + Exception.__unicode__, + + * else decode with `self.encoding` and `self.decoding_errors`. + """ + try: + u = unicode(self.data) + if isinstance(self.data, EnvironmentError): + u = u.replace(": u'", ": '") # normalize filename quoting + return u + except UnicodeError, error: # catch ..Encode.. and ..Decode.. errors + if isinstance(self.data, EnvironmentError): + return u"[Errno %s] %s: '%s'" % (self.data.errno, + SafeString(self.data.strerror, self.encoding, + self.decoding_errors), + SafeString(self.data.filename, self.encoding, + self.decoding_errors)) + if isinstance(self.data, Exception): + args = [unicode(SafeString(arg, self.encoding, + decoding_errors=self.decoding_errors)) + for arg in self.data.args] + return u', '.join(args) + if isinstance(error, UnicodeDecodeError): + return unicode(self.data, self.encoding, self.decoding_errors) + raise + +class ErrorString(SafeString): + """ + Safely report exception type and message. + """ + def __str__(self): + return '%s: %s' % (self.data.__class__.__name__, + super(ErrorString, self).__str__()) + + def __unicode__(self): + return u'%s: %s' % (self.data.__class__.__name__, + super(ErrorString, self).__unicode__()) + + +class ErrorOutput(object): + """ + Wrapper class for file-like error streams with + failsave de- and encoding of `str`, `bytes`, `unicode` and + `Exception` instances. + """ + + def __init__(self, stream=None, encoding=None, + encoding_errors='backslashreplace', + decoding_errors='replace'): + """ + :Parameters: + - `stream`: a file-like object, + a string (path to a file), + `None` (write to `sys.stderr`, default), or + evaluating to `False` (write() requests are ignored). + - `encoding`: `stream` text encoding. Guessed if None. + - `encoding_errors`: how to treat encoding errors. + """ + if stream is None: + stream = sys.stderr + elif not(stream): + stream = False + # if `stream` is a file name, open it + elif isinstance(stream, str): + stream = open(stream, 'w') + elif isinstance(stream, unicode): + stream = open(stream.encode(sys.getfilesystemencoding()), 'w') + + self.stream = stream + """Where warning output is sent.""" + + self.encoding = (encoding or getattr(stream, 'encoding', None) or + locale_encoding or 'ascii') + """The output character encoding.""" + + self.encoding_errors = encoding_errors + """Encoding error handler.""" + + self.decoding_errors = decoding_errors + """Decoding error handler.""" + + def write(self, data): + """ + Write `data` to self.stream. Ignore, if self.stream is False. + + `data` can be a `string`, `unicode`, or `Exception` instance. + """ + if self.stream is False: + return + if isinstance(data, Exception): + data = unicode(SafeString(data, self.encoding, + self.encoding_errors, self.decoding_errors)) + try: + self.stream.write(data) + except UnicodeEncodeError: + self.stream.write(data.encode(self.encoding, self.encoding_errors)) + except TypeError: # in Python 3, stderr expects unicode + if self.stream in (sys.stderr, sys.stdout): + self.stream.buffer.write(data) # write bytes to raw stream + else: + self.stream.write(unicode(data, self.encoding, + self.decoding_errors)) + + def close(self): + """ + Close the error-output stream. + + Ignored if the stream is` sys.stderr` or `sys.stdout` or has no + close() method. + """ + if self.stream in (sys.stdout, sys.stderr): + return + try: + self.stream.close() + except AttributeError: + pass diff --git a/docutils/examples.py b/docutils/examples.py new file mode 100644 index 000000000..395dbbf14 --- /dev/null +++ b/docutils/examples.py @@ -0,0 +1,97 @@ +# $Id: examples.py 7320 2012-01-19 22:33:02Z milde $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +This module contains practical examples of Docutils client code. + +Importing this module from client code is not recommended; its contents are +subject to change in future Docutils releases. Instead, it is recommended +that you copy and paste the parts you need into your own code, modifying as +necessary. +""" + +from docutils import core, io + + +def html_parts(input_string, source_path=None, destination_path=None, + input_encoding='unicode', doctitle=True, + initial_header_level=1): + """ + Given an input string, returns a dictionary of HTML document parts. + + Dictionary keys are the names of parts, and values are Unicode strings; + encoding is up to the client. + + Parameters: + + - `input_string`: A multi-line text string; required. + - `source_path`: Path to the source file or object. Optional, but useful + for diagnostic output (system messages). + - `destination_path`: Path to the file or object which will receive the + output; optional. Used for determining relative paths (stylesheets, + source links, etc.). + - `input_encoding`: The encoding of `input_string`. If it is an encoded + 8-bit string, provide the correct encoding. If it is a Unicode string, + use "unicode", the default. + - `doctitle`: Disable the promotion of a lone top-level section title to + document title (and subsequent section title to document subtitle + promotion); enabled by default. + - `initial_header_level`: The initial level for header elements (e.g. 1 + for "

"). + """ + overrides = {'input_encoding': input_encoding, + 'doctitle_xform': doctitle, + 'initial_header_level': initial_header_level} + parts = core.publish_parts( + source=input_string, source_path=source_path, + destination_path=destination_path, + writer_name='html', settings_overrides=overrides) + return parts + +def html_body(input_string, source_path=None, destination_path=None, + input_encoding='unicode', output_encoding='unicode', + doctitle=True, initial_header_level=1): + """ + Given an input string, returns an HTML fragment as a string. + + The return value is the contents of the element. + + Parameters (see `html_parts()` for the remainder): + + - `output_encoding`: The desired encoding of the output. If a Unicode + string is desired, use the default value of "unicode" . + """ + parts = html_parts( + input_string=input_string, source_path=source_path, + destination_path=destination_path, + input_encoding=input_encoding, doctitle=doctitle, + initial_header_level=initial_header_level) + fragment = parts['html_body'] + if output_encoding != 'unicode': + fragment = fragment.encode(output_encoding) + return fragment + +def internals(input_string, source_path=None, destination_path=None, + input_encoding='unicode', settings_overrides=None): + """ + Return the document tree and publisher, for exploring Docutils internals. + + Parameters: see `html_parts()`. + """ + if settings_overrides: + overrides = settings_overrides.copy() + else: + overrides = {} + overrides['input_encoding'] = input_encoding + output, pub = core.publish_programmatically( + source_class=io.StringInput, source=input_string, + source_path=source_path, + destination_class=io.NullOutput, destination=None, + destination_path=destination_path, + reader=None, reader_name='standalone', + parser=None, parser_name='restructuredtext', + writer=None, writer_name='null', + settings=None, settings_spec=None, settings_overrides=overrides, + config_section=None, enable_exit_status=None) + return pub.writer.document, pub diff --git a/docutils/frontend.py b/docutils/frontend.py new file mode 100644 index 000000000..83f5fde59 --- /dev/null +++ b/docutils/frontend.py @@ -0,0 +1,788 @@ +# $Id: frontend.py 7339 2012-02-03 12:23:27Z milde $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +Command-line and common processing for Docutils front-end tools. + +Exports the following classes: + +* `OptionParser`: Standard Docutils command-line processing. +* `Option`: Customized version of `optparse.Option`; validation support. +* `Values`: Runtime settings; objects are simple structs + (``object.attribute``). Supports cumulative list settings (attributes). +* `ConfigParser`: Standard Docutils config file processing. + +Also exports the following functions: + +* Option callbacks: `store_multiple`, `read_config_file`. +* Setting validators: `validate_encoding`, + `validate_encoding_error_handler`, + `validate_encoding_and_error_handler`, `validate_boolean`, + `validate_threshold`, `validate_colon_separated_string_list`, + `validate_dependency_file`. +* `make_paths_absolute`. +* SettingSpec manipulation: `filter_settings_spec`. +""" + +__docformat__ = 'reStructuredText' + +import os +import os.path +import sys +import warnings +import ConfigParser as CP +import codecs +import optparse +from optparse import SUPPRESS_HELP +import docutils +import docutils.utils +import docutils.nodes +from docutils.error_reporting import locale_encoding, ErrorOutput, ErrorString + + +def store_multiple(option, opt, value, parser, *args, **kwargs): + """ + Store multiple values in `parser.values`. (Option callback.) + + Store `None` for each attribute named in `args`, and store the value for + each key (attribute name) in `kwargs`. + """ + for attribute in args: + setattr(parser.values, attribute, None) + for key, value in kwargs.items(): + setattr(parser.values, key, value) + +def read_config_file(option, opt, value, parser): + """ + Read a configuration file during option processing. (Option callback.) + """ + try: + new_settings = parser.get_config_file_settings(value) + except ValueError, error: + parser.error(error) + parser.values.update(new_settings, parser) + +def validate_encoding(setting, value, option_parser, + config_parser=None, config_section=None): + try: + codecs.lookup(value) + except LookupError: + raise (LookupError('setting "%s": unknown encoding: "%s"' + % (setting, value)), + None, sys.exc_info()[2]) + return value + +def validate_encoding_error_handler(setting, value, option_parser, + config_parser=None, config_section=None): + try: + codecs.lookup_error(value) + except LookupError: + raise (LookupError( + 'unknown encoding error handler: "%s" (choices: ' + '"strict", "ignore", "replace", "backslashreplace", ' + '"xmlcharrefreplace", and possibly others; see documentation for ' + 'the Python ``codecs`` module)' % value), + None, sys.exc_info()[2]) + return value + +def validate_encoding_and_error_handler( + setting, value, option_parser, config_parser=None, config_section=None): + """ + Side-effect: if an error handler is included in the value, it is inserted + into the appropriate place as if it was a separate setting/option. + """ + if ':' in value: + encoding, handler = value.split(':') + validate_encoding_error_handler( + setting + '_error_handler', handler, option_parser, + config_parser, config_section) + if config_parser: + config_parser.set(config_section, setting + '_error_handler', + handler) + else: + setattr(option_parser.values, setting + '_error_handler', handler) + else: + encoding = value + validate_encoding(setting, encoding, option_parser, + config_parser, config_section) + return encoding + +def validate_boolean(setting, value, option_parser, + config_parser=None, config_section=None): + if isinstance(value, unicode): + try: + return option_parser.booleans[value.strip().lower()] + except KeyError: + raise (LookupError('unknown boolean value: "%s"' % value), + None, sys.exc_info()[2]) + return value + +def validate_nonnegative_int(setting, value, option_parser, + config_parser=None, config_section=None): + value = int(value) + if value < 0: + raise ValueError('negative value; must be positive or zero') + return value + +def validate_threshold(setting, value, option_parser, + config_parser=None, config_section=None): + try: + return int(value) + except ValueError: + try: + return option_parser.thresholds[value.lower()] + except (KeyError, AttributeError): + raise (LookupError('unknown threshold: %r.' % value), + None, sys.exc_info[2]) + +def validate_colon_separated_string_list( + setting, value, option_parser, config_parser=None, config_section=None): + if isinstance(value, unicode): + value = value.split(':') + else: + last = value.pop() + value.extend(last.split(':')) + return value + +def validate_url_trailing_slash( + setting, value, option_parser, config_parser=None, config_section=None): + if not value: + return './' + elif value.endswith('/'): + return value + else: + return value + '/' + +def validate_dependency_file(setting, value, option_parser, + config_parser=None, config_section=None): + try: + return docutils.utils.DependencyList(value) + except IOError: + return docutils.utils.DependencyList(None) + +def validate_strip_class(setting, value, option_parser, + config_parser=None, config_section=None): + # convert to list: + if isinstance(value, unicode): + value = [value] + class_values = filter(None, [v.strip() for v in value.pop().split(',')]) + # validate: + for class_value in class_values: + normalized = docutils.nodes.make_id(class_value) + if class_value != normalized: + raise ValueError('invalid class value %r (perhaps %r?)' + % (class_value, normalized)) + value.extend(class_values) + return value + +def make_paths_absolute(pathdict, keys, base_path=None): + """ + Interpret filesystem path settings relative to the `base_path` given. + + Paths are values in `pathdict` whose keys are in `keys`. Get `keys` from + `OptionParser.relative_path_settings`. + """ + if base_path is None: + base_path = os.getcwdu() # type(base_path) == unicode + # to allow combining non-ASCII cwd with unicode values in `pathdict` + for key in keys: + if key in pathdict: + value = pathdict[key] + if isinstance(value, list): + value = [make_one_path_absolute(base_path, path) + for path in value] + elif value: + value = make_one_path_absolute(base_path, value) + pathdict[key] = value + +def make_one_path_absolute(base_path, path): + return os.path.abspath(os.path.join(base_path, path)) + +def filter_settings_spec(settings_spec, *exclude, **replace): + """Return a copy of `settings_spec` excluding/replacing some settings. + + `settings_spec` is a tuple of configuration settings with a structure + described for docutils.SettingsSpec.settings_spec. + + Optional positional arguments are names of to-be-excluded settings. + Keyword arguments are option specification replacements. + (See the html4strict writer for an example.) + """ + settings = list(settings_spec) + # every third item is a sequence of option tuples + for i in range(2, len(settings), 3): + newopts = [] + for opt_spec in settings[i]: + # opt_spec is ("", [

\n') + self.in_document_title = len(self.body) + elif isinstance(node.parent, nodes.section): + tag = 'h%s' % (self.section_level + self.initial_header_level - 1) + self.body.append( + self.starttag(node, tag, '', CLASS='section-subtitle') + + self.starttag({}, 'span', '', CLASS='section-subtitle')) + self.context.append('\n' % tag) + + def depart_subtitle(self, node): + self.body.append(self.context.pop()) + if self.in_document_title: + self.subtitle = self.body[self.in_document_title:-1] + self.in_document_title = 0 + self.body_pre_docinfo.extend(self.body) + self.html_subtitle.extend(self.body) + del self.body[:] + + def visit_superscript(self, node): + self.body.append(self.starttag(node, 'sup', '')) + + def depart_superscript(self, node): + self.body.append('') + + def visit_system_message(self, node): + self.body.append(self.starttag(node, 'div', CLASS='system-message')) + self.body.append('

') + backref_text = '' + if len(node['backrefs']): + backrefs = node['backrefs'] + if len(backrefs) == 1: + backref_text = ('; backlink' + % backrefs[0]) + else: + i = 1 + backlinks = [] + for backref in backrefs: + backlinks.append('%s' % (backref, i)) + i += 1 + backref_text = ('; backlinks: %s' + % ', '.join(backlinks)) + if node.hasattr('line'): + line = ', line %s' % node['line'] + else: + line = '' + self.body.append('System Message: %s/%s ' + '(%s%s)%s

\n' + % (node['type'], node['level'], + self.encode(node['source']), line, backref_text)) + + def depart_system_message(self, node): + self.body.append('\n') + + def visit_table(self, node): + classes = ' '.join(['docutils', self.settings.table_style]).strip() + self.body.append( + self.starttag(node, 'table', CLASS=classes, border="1")) + + def depart_table(self, node): + self.body.append('\n') + + def visit_target(self, node): + if not ('refuri' in node or 'refid' in node + or 'refname' in node): + self.body.append(self.starttag(node, 'span', '', CLASS='target')) + self.context.append('') + else: + self.context.append('') + + def depart_target(self, node): + self.body.append(self.context.pop()) + + def visit_tbody(self, node): + self.write_colspecs() + self.body.append(self.context.pop()) # '\n' or '' + self.body.append(self.starttag(node, 'tbody', valign='top')) + + def depart_tbody(self, node): + self.body.append('\n') + + def visit_term(self, node): + self.body.append(self.starttag(node, 'dt', '')) + + def depart_term(self, node): + """ + Leave the end tag to `self.visit_definition()`, in case there's a + classifier. + """ + pass + + def visit_tgroup(self, node): + # Mozilla needs : + self.body.append(self.starttag(node, 'colgroup')) + # Appended by thead or tbody: + self.context.append('\n') + node.stubs = [] + + def depart_tgroup(self, node): + pass + + def visit_thead(self, node): + self.write_colspecs() + self.body.append(self.context.pop()) # '\n' + # There may or may not be a ; this is for to use: + self.context.append('') + self.body.append(self.starttag(node, 'thead', valign='bottom')) + + def depart_thead(self, node): + self.body.append('\n') + + def visit_title(self, node): + """Only 6 section levels are supported by HTML.""" + check_id = 0 # TODO: is this a bool (False) or a counter? + close_tag = '

\n' + if isinstance(node.parent, nodes.topic): + self.body.append( + self.starttag(node, 'p', '', CLASS='topic-title first')) + elif isinstance(node.parent, nodes.sidebar): + self.body.append( + self.starttag(node, 'p', '', CLASS='sidebar-title')) + elif isinstance(node.parent, nodes.Admonition): + self.body.append( + self.starttag(node, 'p', '', CLASS='admonition-title')) + elif isinstance(node.parent, nodes.table): + self.body.append( + self.starttag(node, 'caption', '')) + close_tag = '\n' + elif isinstance(node.parent, nodes.document): + self.body.append(self.starttag(node, 'h1', '', CLASS='title')) + close_tag = '\n' + self.in_document_title = len(self.body) + else: + assert isinstance(node.parent, nodes.section) + h_level = self.section_level + self.initial_header_level - 1 + atts = {} + if (len(node.parent) >= 2 and + isinstance(node.parent[1], nodes.subtitle)): + atts['CLASS'] = 'with-subtitle' + self.body.append( + self.starttag(node, 'h%s' % h_level, '', **atts)) + atts = {} + if node.hasattr('refid'): + atts['class'] = 'toc-backref' + atts['href'] = '#' + node['refid'] + if atts: + self.body.append(self.starttag({}, 'a', '', **atts)) + close_tag = '\n' % (h_level) + else: + close_tag = '\n' % (h_level) + self.context.append(close_tag) + + def depart_title(self, node): + self.body.append(self.context.pop()) + if self.in_document_title: + self.title = self.body[self.in_document_title:-1] + self.in_document_title = 0 + self.body_pre_docinfo.extend(self.body) + self.html_title.extend(self.body) + del self.body[:] + + def visit_title_reference(self, node): + self.body.append(self.starttag(node, 'cite', '')) + + def depart_title_reference(self, node): + self.body.append('') + + def visit_topic(self, node): + self.body.append(self.starttag(node, 'div', CLASS='topic')) + self.topic_classes = node['classes'] + + def depart_topic(self, node): + self.body.append('\n') + self.topic_classes = [] + + def visit_transition(self, node): + self.body.append(self.emptytag(node, 'hr', CLASS='docutils')) + + def depart_transition(self, node): + pass + + def visit_version(self, node): + self.visit_docinfo_item(node, 'version', meta=False) + + def depart_version(self, node): + self.depart_docinfo_item() + + def unimplemented_visit(self, node): + raise NotImplementedError('visiting unimplemented node type: %s' + % node.__class__.__name__) + + +class SimpleListChecker(nodes.GenericNodeVisitor): + + """ + Raise `nodes.NodeFound` if non-simple list item is encountered. + + Here "simple" means a list item containing nothing other than a single + paragraph, a simple list, or a paragraph followed by a simple list. + """ + + def default_visit(self, node): + raise nodes.NodeFound + + def visit_bullet_list(self, node): + pass + + def visit_enumerated_list(self, node): + pass + + def visit_list_item(self, node): + children = [] + for child in node.children: + if not isinstance(child, nodes.Invisible): + children.append(child) + if (children and isinstance(children[0], nodes.paragraph) + and (isinstance(children[-1], nodes.bullet_list) + or isinstance(children[-1], nodes.enumerated_list))): + children.pop() + if len(children) <= 1: + return + else: + raise nodes.NodeFound + + def visit_paragraph(self, node): + raise nodes.SkipNode + + def invisible_visit(self, node): + """Invisible nodes should be ignored.""" + raise nodes.SkipNode + + visit_comment = invisible_visit + visit_substitution_definition = invisible_visit + visit_target = invisible_visit + visit_pending = invisible_visit diff --git a/docutils/writers/html4css1/html4css1.css b/docutils/writers/html4css1/html4css1.css new file mode 100644 index 000000000..f5285ad09 --- /dev/null +++ b/docutils/writers/html4css1/html4css1.css @@ -0,0 +1,311 @@ +/* +:Author: David Goodger (goodger@python.org) +:Id: $Id: html4css1.css 7434 2012-05-11 21:06:27Z milde $ +:Copyright: This stylesheet has been placed in the public domain. + +Default cascading style sheet for the HTML output of Docutils. + +See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to +customize this style sheet. +*/ + +/* used to remove borders from tables and images */ +.borderless, table.borderless td, table.borderless th { + border: 0 } + +table.borderless td, table.borderless th { + /* Override padding for "table.docutils td" with "! important". + The right padding separates the table cells. */ + padding: 0 0.5em 0 0 ! important } + +.first { + /* Override more specific margin styles with "! important". */ + margin-top: 0 ! important } + +.last, .with-subtitle { + margin-bottom: 0 ! important } + +.hidden { + display: none } + +a.toc-backref { + text-decoration: none ; + color: black } + +blockquote.epigraph { + margin: 2em 5em ; } + +dl.docutils dd { + margin-bottom: 0.5em } + +object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] { + overflow: hidden; +} + +/* Uncomment (and remove this text!) to get bold-faced definition list terms +dl.docutils dt { + font-weight: bold } +*/ + +div.abstract { + margin: 2em 5em } + +div.abstract p.topic-title { + font-weight: bold ; + text-align: center } + +div.admonition, div.attention, div.caution, div.danger, div.error, +div.hint, div.important, div.note, div.tip, div.warning { + margin: 2em ; + border: medium outset ; + padding: 1em } + +div.admonition p.admonition-title, div.hint p.admonition-title, +div.important p.admonition-title, div.note p.admonition-title, +div.tip p.admonition-title { + font-weight: bold ; + font-family: sans-serif } + +div.attention p.admonition-title, div.caution p.admonition-title, +div.danger p.admonition-title, div.error p.admonition-title, +div.warning p.admonition-title { + color: red ; + font-weight: bold ; + font-family: sans-serif } + +/* Uncomment (and remove this text!) to get reduced vertical space in + compound paragraphs. +div.compound .compound-first, div.compound .compound-middle { + margin-bottom: 0.5em } + +div.compound .compound-last, div.compound .compound-middle { + margin-top: 0.5em } +*/ + +div.dedication { + margin: 2em 5em ; + text-align: center ; + font-style: italic } + +div.dedication p.topic-title { + font-weight: bold ; + font-style: normal } + +div.figure { + margin-left: 2em ; + margin-right: 2em } + +div.footer, div.header { + clear: both; + font-size: smaller } + +div.line-block { + display: block ; + margin-top: 1em ; + margin-bottom: 1em } + +div.line-block div.line-block { + margin-top: 0 ; + margin-bottom: 0 ; + margin-left: 1.5em } + +div.sidebar { + margin: 0 0 0.5em 1em ; + border: medium outset ; + padding: 1em ; + background-color: #ffffee ; + width: 40% ; + float: right ; + clear: right } + +div.sidebar p.rubric { + font-family: sans-serif ; + font-size: medium } + +div.system-messages { + margin: 5em } + +div.system-messages h1 { + color: red } + +div.system-message { + border: medium outset ; + padding: 1em } + +div.system-message p.system-message-title { + color: red ; + font-weight: bold } + +div.topic { + margin: 2em } + +h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, +h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { + margin-top: 0.4em } + +h1.title { + text-align: center } + +h2.subtitle { + text-align: center } + +hr.docutils { + width: 75% } + +img.align-left, .figure.align-left, object.align-left { + clear: left ; + float: left ; + margin-right: 1em } + +img.align-right, .figure.align-right, object.align-right { + clear: right ; + float: right ; + margin-left: 1em } + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left } + +.align-center { + clear: both ; + text-align: center } + +.align-right { + text-align: right } + +/* reset inner alignment in figures */ +div.align-right { + text-align: inherit } + +/* div.align-center * { */ +/* text-align: left } */ + +ol.simple, ul.simple { + margin-bottom: 1em } + +ol.arabic { + list-style: decimal } + +ol.loweralpha { + list-style: lower-alpha } + +ol.upperalpha { + list-style: upper-alpha } + +ol.lowerroman { + list-style: lower-roman } + +ol.upperroman { + list-style: upper-roman } + +p.attribution { + text-align: right ; + margin-left: 50% } + +p.caption { + font-style: italic } + +p.credits { + font-style: italic ; + font-size: smaller } + +p.label { + white-space: nowrap } + +p.rubric { + font-weight: bold ; + font-size: larger ; + color: maroon ; + text-align: center } + +p.sidebar-title { + font-family: sans-serif ; + font-weight: bold ; + font-size: larger } + +p.sidebar-subtitle { + font-family: sans-serif ; + font-weight: bold } + +p.topic-title { + font-weight: bold } + +pre.address { + margin-bottom: 0 ; + margin-top: 0 ; + font: inherit } + +pre.literal-block, pre.doctest-block, pre.math, pre.code { + margin-left: 2em ; + margin-right: 2em } + +pre.code .ln { /* line numbers */ + color: grey; +} + +.code { + background-color: #eeeeee +} + +span.classifier { + font-family: sans-serif ; + font-style: oblique } + +span.classifier-delimiter { + font-family: sans-serif ; + font-weight: bold } + +span.interpreted { + font-family: sans-serif } + +span.option { + white-space: nowrap } + +span.pre { + white-space: pre } + +span.problematic { + color: red } + +span.section-subtitle { + /* font-size relative to parent (h1..h6 element) */ + font-size: 80% } + +table.citation { + border-left: solid 1px gray; + margin-left: 1px } + +table.docinfo { + margin: 2em 4em } + +table.docutils { + margin-top: 0.5em ; + margin-bottom: 0.5em } + +table.footnote { + border-left: solid 1px black; + margin-left: 1px } + +table.docutils td, table.docutils th, +table.docinfo td, table.docinfo th { + padding-left: 0.5em ; + padding-right: 0.5em ; + vertical-align: top } + +table.docutils th.field-name, table.docinfo th.docinfo-name { + font-weight: bold ; + text-align: left ; + white-space: nowrap ; + padding-left: 0 } + +h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, +h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { + font-size: 100% } + +ul.auto-toc { + list-style-type: none } diff --git a/docutils/writers/html4css1/math.css b/docutils/writers/html4css1/math.css new file mode 100644 index 000000000..4f43b37e0 --- /dev/null +++ b/docutils/writers/html4css1/math.css @@ -0,0 +1,274 @@ +/* +* math2html: convert LaTeX equations to HTML output. +* +* Copyright (C) 2009,2010 Alex Fernández +* +* Released under the terms of the `2-Clause BSD license'_, in short: +* Copying and distribution of this file, with or without modification, +* are permitted in any medium without royalty provided the copyright +* notice and this notice are preserved. +* This file is offered as-is, without any warranty. +* +* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause +* +* Based on eLyXer: convert LyX source files to HTML output. +* http://elyxer.nongnu.org/ +*/ +/* --end-- +* CSS file for LaTeX formulas. +*/ + +/* Formulas */ +.formula { + text-align: center; + font-family: "DejaVu Serif", serif; + margin: 1.2em 0; +} +span.formula { + white-space: nowrap; +} +div.formula { + padding: 0.5ex; + margin-left: auto; + margin-right: auto; +} + +/* Basic features */ +a.eqnumber { + display: inline-block; + float: right; + clear: right; + font-weight: bold; +} +span.unknown { + color: #800000; +} +span.ignored, span.arraydef { + display: none; +} +.formula i { + letter-spacing: 0.1ex; +} + +/* Alignment */ +.align-left, .align-l { + text-align: left; +} +.align-right, .align-r { + text-align: right; +} +.align-center, .align-c { + text-align: center; +} + +/* Structures */ +span.overline, span.bar { + text-decoration: overline; +} +.fraction, .fullfraction { + display: inline-block; + vertical-align: middle; + text-align: center; +} +.fraction .fraction { + font-size: 80%; + line-height: 100%; +} +span.numerator { + display: block; +} +span.denominator { + display: block; + padding: 0ex; + border-top: thin solid; +} +sup.numerator, sup.unit { + font-size: 70%; + vertical-align: 80%; +} +sub.denominator, sub.unit { + font-size: 70%; + vertical-align: -20%; +} +span.sqrt { + display: inline-block; + vertical-align: middle; + padding: 0.1ex; +} +sup.root { + font-size: 70%; + position: relative; + left: 1.4ex; +} +span.radical { + display: inline-block; + padding: 0ex; + font-size: 150%; + vertical-align: top; +} +span.root { + display: inline-block; + border-top: thin solid; + padding: 0ex; + vertical-align: middle; +} +span.symbol { + font-size: 125%; +} +span.bigsymbol { + font-size: 150%; +} +span.largesymbol { + font-size: 175%; +} +span.hugesymbol { + font-size: 200%; +} +span.scripts { + display: inline-table; + vertical-align: middle; +} +.script { + display: table-row; + text-align: left; + line-height: 150%; +} +span.limits { + display: inline-table; + vertical-align: middle; +} +.limit { + display: table-row; + line-height: 95%; +} +sup.limit, sub.limit { + line-height: 150%; +} +span.symbolover { + display: inline-block; + text-align: center; + position: relative; + float: right; + right: 100%; + bottom: 0.5em; + width: 0px; +} +span.withsymbol { + display: inline-block; +} +span.symbolunder { + display: inline-block; + text-align: center; + position: relative; + float: right; + right: 80%; + top: 0.3em; + width: 0px; +} + +/* Environments */ +span.array, span.bracketcases, span.binomial, span.environment { + display: inline-table; + text-align: center; + border-collapse: collapse; + margin: 0em; + vertical-align: middle; +} +span.arrayrow, span.binomrow { + display: table-row; + padding: 0ex; + border: 0ex; +} +span.arraycell, span.bracket, span.case, span.binomcell, span.environmentcell { + display: table-cell; + padding: 0ex 0.2ex; + line-height: 99%; + border: 0ex; +} +/* +* CSS file for LaTeX formulas, extra stuff: +* binomials, vertical braces, stackrel, fonts and colors. +*/ + +/* Inline binomials */ +span.binom { + display: inline-block; + vertical-align: middle; + text-align: center; + font-size: 80%; +} +span.binomstack { + display: block; + padding: 0em; +} + +/* Over- and underbraces */ +span.overbrace { + border-top: 2pt solid; +} +span.underbrace { + border-bottom: 2pt solid; +} + +/* Stackrel */ +span.stackrel { + display: inline-block; + text-align: center; +} +span.upstackrel { + display: block; + padding: 0em; + font-size: 80%; + line-height: 64%; + position: relative; + top: 0.15em; + +} +span.downstackrel { + display: block; + vertical-align: bottom; + padding: 0em; +} + +/* Fonts */ +span.mathsf, span.textsf { + font-style: normal; + font-family: sans-serif; +} +span.mathrm, span.textrm { + font-style: normal; + font-family: serif; +} +span.text, span.textnormal { + font-style: normal; +} +span.textipa { + color: #008080; +} +span.fraktur { + font-family: "Lucida Blackletter", eufm10, blackletter; +} +span.blackboard { + font-family: Blackboard, msbm10, serif; +} +span.scriptfont { + font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive; + font-style: italic; +} + +/* Colors */ +span.colorbox { + display: inline-block; + padding: 5px; +} +span.fbox { + display: inline-block; + border: thin solid black; + padding: 2px; +} +span.boxed, span.framebox { + display: inline-block; + border: thin solid black; + padding: 5px; +} + diff --git a/docutils/writers/html4css1/template.txt b/docutils/writers/html4css1/template.txt new file mode 100644 index 000000000..2591bce35 --- /dev/null +++ b/docutils/writers/html4css1/template.txt @@ -0,0 +1,8 @@ +%(head_prefix)s +%(head)s +%(stylesheet)s +%(body_prefix)s +%(body_pre_docinfo)s +%(docinfo)s +%(body)s +%(body_suffix)s diff --git a/docutils/writers/latex2e/__init__.py b/docutils/writers/latex2e/__init__.py new file mode 100644 index 000000000..cb71f0994 --- /dev/null +++ b/docutils/writers/latex2e/__init__.py @@ -0,0 +1,3041 @@ +# .. coding: utf8 +# $Id: __init__.py 7389 2012-03-30 11:58:21Z milde $ +# Author: Engelbert Gruber, Günter Milde +# Maintainer: docutils-develop@lists.sourceforge.net +# Copyright: This module has been placed in the public domain. + +"""LaTeX2e document tree Writer.""" + +__docformat__ = 'reStructuredText' + +# code contributions from several people included, thanks to all. +# some named: David Abrahams, Julien Letessier, Lele Gaifax, and others. +# +# convention deactivate code by two # i.e. ##. + +import sys +import os +import time +import re +import string +import urllib +try: + import roman +except ImportError: + import docutils.utils.roman as roman +from docutils import frontend, nodes, languages, writers, utils, io +from docutils.error_reporting import SafeString +from docutils.transforms import writer_aux +from docutils.math import pick_math_environment, unichar2tex + +# compatibility module for Python 2.3 +if not hasattr(string, 'Template'): + import docutils._string_template_compat + string.Template = docutils._string_template_compat.Template + +class Writer(writers.Writer): + + supported = ('latex','latex2e') + """Formats this writer supports.""" + + default_template = 'default.tex' + default_template_path = os.path.dirname(__file__) + + default_preamble = '\n'.join([r'% PDF Standard Fonts', + r'\usepackage{mathptmx} % Times', + r'\usepackage[scaled=.90]{helvet}', + r'\usepackage{courier}']) + settings_spec = ( + 'LaTeX-Specific Options', + None, + (('Specify documentclass. Default is "article".', + ['--documentclass'], + {'default': 'article', }), + ('Specify document options. Multiple options can be given, ' + 'separated by commas. Default is "a4paper".', + ['--documentoptions'], + {'default': 'a4paper', }), + ('Footnotes with numbers/symbols by Docutils. (default)', + ['--docutils-footnotes'], + {'default': True, 'action': 'store_true', + 'validator': frontend.validate_boolean}), + ('Alias for --docutils-footnotes (deprecated)', + ['--use-latex-footnotes'], + {'action': 'store_true', + 'validator': frontend.validate_boolean}), + ('Use figure floats for footnote text (deprecated)', + ['--figure-footnotes'], + {'action': 'store_true', + 'validator': frontend.validate_boolean}), + ('Format for footnote references: one of "superscript" or ' + '"brackets". Default is "superscript".', + ['--footnote-references'], + {'choices': ['superscript', 'brackets'], 'default': 'superscript', + 'metavar': '', + 'overrides': 'trim_footnote_reference_space'}), + ('Use \\cite command for citations. ', + ['--use-latex-citations'], + {'default': 0, 'action': 'store_true', + 'validator': frontend.validate_boolean}), + ('Use figure floats for citations ' + '(might get mixed with real figures). (default)', + ['--figure-citations'], + {'dest': 'use_latex_citations', 'action': 'store_false', + 'validator': frontend.validate_boolean}), + ('Format for block quote attributions: one of "dash" (em-dash ' + 'prefix), "parentheses"/"parens", or "none". Default is "dash".', + ['--attribution'], + {'choices': ['dash', 'parentheses', 'parens', 'none'], + 'default': 'dash', 'metavar': ''}), + ('Specify LaTeX packages/stylesheets. ' + ' A style is referenced with \\usepackage if extension is ' + '".sty" or omitted and with \\input else. ' + ' Overrides previous --stylesheet and --stylesheet-path settings.', + ['--stylesheet'], + {'default': '', 'metavar': '', + 'overrides': 'stylesheet_path'}), + ('Like --stylesheet, but the path is rewritten ' + 'relative to the output file. ', + ['--stylesheet-path'], + {'metavar': '', 'overrides': 'stylesheet'}), + ('Link to the stylesheet(s) in the output file. (default)', + ['--link-stylesheet'], + {'dest': 'embed_stylesheet', 'action': 'store_false'}), + ('Embed the stylesheet(s) in the output file. ' + 'Stylesheets must be accessible during processing. ', + ['--embed-stylesheet'], + {'default': 0, 'action': 'store_true', + 'validator': frontend.validate_boolean}), + ('Customization by LaTeX code in the preamble. ' + 'Default: select PDF standard fonts (Times, Helvetica, Courier).', + ['--latex-preamble'], + {'default': default_preamble}), + ('Specify the template file. Default: "%s".' % default_template, + ['--template'], + {'default': default_template, 'metavar': ''}), + ('Table of contents by LaTeX. (default) ', + ['--use-latex-toc'], + {'default': 1, 'action': 'store_true', + 'validator': frontend.validate_boolean}), + ('Table of contents by Docutils (without page numbers). ', + ['--use-docutils-toc'], + {'dest': 'use_latex_toc', 'action': 'store_false', + 'validator': frontend.validate_boolean}), + ('Add parts on top of the section hierarchy.', + ['--use-part-section'], + {'default': 0, 'action': 'store_true', + 'validator': frontend.validate_boolean}), + ('Attach author and date to the document info table. (default) ', + ['--use-docutils-docinfo'], + {'dest': 'use_latex_docinfo', 'action': 'store_false', + 'validator': frontend.validate_boolean}), + ('Attach author and date to the document title.', + ['--use-latex-docinfo'], + {'default': 0, 'action': 'store_true', + 'validator': frontend.validate_boolean}), + ("Typeset abstract as topic. (default)", + ['--topic-abstract'], + {'dest': 'use_latex_abstract', 'action': 'store_false', + 'validator': frontend.validate_boolean}), + ("Use LaTeX abstract environment for the document's abstract. ", + ['--use-latex-abstract'], + {'default': 0, 'action': 'store_true', + 'validator': frontend.validate_boolean}), + ('Color of any hyperlinks embedded in text ' + '(default: "blue", "false" to disable).', + ['--hyperlink-color'], {'default': 'blue'}), + ('Additional options to the "hyperref" package ' + '(default: "").', + ['--hyperref-options'], {'default': ''}), + ('Enable compound enumerators for nested enumerated lists ' + '(e.g. "1.2.a.ii"). Default: disabled.', + ['--compound-enumerators'], + {'default': None, 'action': 'store_true', + 'validator': frontend.validate_boolean}), + ('Disable compound enumerators for nested enumerated lists. ' + 'This is the default.', + ['--no-compound-enumerators'], + {'action': 'store_false', 'dest': 'compound_enumerators'}), + ('Enable section ("." subsection ...) prefixes for compound ' + 'enumerators. This has no effect without --compound-enumerators.' + 'Default: disabled.', + ['--section-prefix-for-enumerators'], + {'default': None, 'action': 'store_true', + 'validator': frontend.validate_boolean}), + ('Disable section prefixes for compound enumerators. ' + 'This is the default.', + ['--no-section-prefix-for-enumerators'], + {'action': 'store_false', 'dest': 'section_prefix_for_enumerators'}), + ('Set the separator between section number and enumerator ' + 'for compound enumerated lists. Default is "-".', + ['--section-enumerator-separator'], + {'default': '-', 'metavar': ''}), + ('When possibile, use the specified environment for literal-blocks. ' + 'Default is quoting of whitespace and special chars.', + ['--literal-block-env'], + {'default': ''}), + ('When possibile, use verbatim for literal-blocks. ' + 'Compatibility alias for "--literal-block-env=verbatim".', + ['--use-verbatim-when-possible'], + {'default': 0, 'action': 'store_true', + 'validator': frontend.validate_boolean}), + ('Table style. "standard" with horizontal and vertical lines, ' + '"booktabs" (LaTeX booktabs style) only horizontal lines ' + 'above and below the table and below the header or "borderless". ' + 'Default: "standard"', + ['--table-style'], + {'choices': ['standard', 'booktabs','nolines', 'borderless'], + 'default': 'standard', + 'metavar': ''}), + ('LaTeX graphicx package option. ' + 'Possible values are "dvips", "pdftex". "auto" includes LaTeX code ' + 'to use "pdftex" if processing with pdf(la)tex and dvips otherwise. ' + 'Default is no option.', + ['--graphicx-option'], + {'default': ''}), + ('LaTeX font encoding. ' + 'Possible values are "", "T1" (default), "OT1", "LGR,T1" or ' + 'any other combination of options to the `fontenc` package. ', + ['--font-encoding'], + {'default': 'T1'}), + ('Per default the latex-writer puts the reference title into ' + 'hyperreferences. Specify "ref*" or "pageref*" to get the section ' + 'number or the page number.', + ['--reference-label'], + {'default': None, }), + ('Specify style and database for bibtex, for example ' + '"--use-bibtex=mystyle,mydb1,mydb2".', + ['--use-bibtex'], + {'default': None, }), + ),) + + settings_defaults = {'sectnum_depth': 0 # updated by SectNum transform + } + relative_path_settings = ('stylesheet_path',) + + config_section = 'latex2e writer' + config_section_dependencies = ('writers',) + + head_parts = ('head_prefix', 'requirements', 'latex_preamble', + 'stylesheet', 'fallbacks', 'pdfsetup', + 'title', 'subtitle', 'titledata') + visitor_attributes = head_parts + ('body_pre_docinfo', 'docinfo', + 'dedication', 'abstract', 'body') + + output = None + """Final translated form of `document`.""" + + def __init__(self): + writers.Writer.__init__(self) + self.translator_class = LaTeXTranslator + + # Override parent method to add latex-specific transforms + def get_transforms(self): + # call the parent class' method + transform_list = writers.Writer.get_transforms(self) + # print transform_list + # Convert specific admonitions to generic one + transform_list.append(writer_aux.Admonitions) + # TODO: footnote collection transform + # transform_list.append(footnotes.collect) + return transform_list + + def translate(self): + visitor = self.translator_class(self.document) + self.document.walkabout(visitor) + # copy parts + for part in self.visitor_attributes: + setattr(self, part, getattr(visitor, part)) + # get template string from file + try: + template_file = open(self.document.settings.template, 'rb') + except IOError: + template_file = open(os.path.join(self.default_template_path, + self.document.settings.template), 'rb') + template = string.Template(unicode(template_file.read(), 'utf-8')) + template_file.close() + # fill template + self.assemble_parts() # create dictionary of parts + self.output = template.substitute(self.parts) + + def assemble_parts(self): + """Assemble the `self.parts` dictionary of output fragments.""" + writers.Writer.assemble_parts(self) + for part in self.visitor_attributes: + lines = getattr(self, part) + if part in self.head_parts: + if lines: + lines.append('') # to get a trailing newline + self.parts[part] = '\n'.join(lines) + else: + # body contains inline elements, so join without newline + self.parts[part] = ''.join(lines) + + +class Babel(object): + """Language specifics for LaTeX.""" + + # TeX (babel) language names: + # ! not all of these are supported by Docutils! + # + # based on LyX' languages file with adaptions to `BCP 47`_ + # (http://www.rfc-editor.org/rfc/bcp/bcp47.txt) and + # http://www.tug.org/TUGboat/Articles/tb29-3/tb93miklavec.pdf + # * the key without subtags is the default + # * case is ignored + # cf. http://docutils.sourceforge.net/docs/howto/i18n.html + # http://www.w3.org/International/articles/language-tags/ + # and http://www.iana.org/assignments/language-subtag-registry + language_codes = { + # code TeX/Babel-name comment + 'af': 'afrikaans', + 'ar': 'arabic', + # 'be': 'belarusian', + 'bg': 'bulgarian', + 'br': 'breton', + 'ca': 'catalan', + # 'cop': 'coptic', + 'cs': 'czech', + 'cy': 'welsh', + 'da': 'danish', + 'de': 'ngerman', # new spelling (de_1996) + 'de_1901': 'german', # old spelling + 'de_at': 'naustrian', + 'de_at_1901': 'austrian', + 'dsb': 'lowersorbian', + 'el': 'greek', # monotonic (el-monoton) + 'el_polyton': 'polutonikogreek', + 'en': 'english', # TeX' default language + 'en_au': 'australian', + 'en_ca': 'canadian', + 'en_gb': 'british', + 'en_nz': 'newzealand', + 'en_us': 'american', + 'eo': 'esperanto', # '^' is active + 'es': 'spanish', + 'et': 'estonian', + 'eu': 'basque', + # 'fa': 'farsi', + 'fi': 'finnish', + 'fr': 'french', + 'fr_ca': 'canadien', + 'ga': 'irish', # Irish Gaelic + # 'grc': # Ancient Greek + 'grc_ibycus': 'ibycus', # Ibycus encoding + 'gl': 'galician', + 'he': 'hebrew', + 'hr': 'croatian', + 'hsb': 'uppersorbian', + 'hu': 'magyar', + 'ia': 'interlingua', + 'id': 'bahasai', # Bahasa (Indonesian) + 'is': 'icelandic', + 'it': 'italian', + 'ja': 'japanese', + 'kk': 'kazakh', + 'la': 'latin', + 'lt': 'lithuanian', + 'lv': 'latvian', + 'mn': 'mongolian', # Mongolian, Cyrillic script (mn-cyrl) + 'ms': 'bahasam', # Bahasa (Malay) + 'nb': 'norsk', # Norwegian Bokmal + 'nl': 'dutch', + 'nn': 'nynorsk', # Norwegian Nynorsk + 'no': 'norsk', # Norwegian Bokmal + 'pl': 'polish', + 'pt': 'portuges', + 'pt_br': 'brazil', + 'ro': 'romanian', + 'ru': 'russian', # '"' is active + 'se': 'samin', # North Sami + # sh-cyrl: Serbo-Croatian, Cyrillic script + 'sh-latn': 'serbian', # Serbo-Croatian, Latin script + 'sk': 'slovak', + 'sl': 'slovene', + 'sq': 'albanian', + # 'sr-cyrl': Serbian, Cyrillic script (sr-cyrl) + 'sr-latn': 'serbian', # Serbian, Latin script, " active. + 'sv': 'swedish', + # 'th': 'thai', + 'tr': 'turkish', + 'uk': 'ukrainian', + 'vi': 'vietnam', + # zh-latn: Chinese Pinyin + } + warn_msg = 'Language "%s" not supported by LaTeX (babel)' + + def __init__(self, language_code, reporter=None): + self.reporter = reporter + self.language = self.language_name(language_code) + self.otherlanguages = {} + self.quote_index = 0 + self.quotes = ('``', "''") + # language dependent configuration: + # double quotes are "active" in some languages (e.g. German). + self.literal_double_quote = u'"' + if self.language in ('ngerman', 'german', 'austrian', 'naustrian', + 'russian'): + self.quotes = (r'\glqq{}', r'\grqq{}') + self.literal_double_quote = ur'\dq{}' + if self.language == 'french': + self.quotes = (r'\og{}', r'\fg{}') + if self.language == 'italian': + self.literal_double_quote = ur'{\char`\"}' + + def __call__(self): + """Return the babel call with correct options and settings""" + languages = self.otherlanguages.keys() + languages.append(self.language or 'english') + self.setup = [r'\usepackage[%s]{babel}' % ','.join(languages)] + if 'spanish' in languages: + # reset active chars to the original meaning: + self.setup.append( + r'\addto\shorthandsspanish{\spanishdeactivate{."~<>}}') + # or prepend r'\def\spanishoptions{es-noshorthands}' + if (languages[-1] == 'english' and + 'french' in self.otherlanguages.keys()): + self.setup += ['% Prevent side-effects if French hyphenation ' + 'patterns are not loaded:', + r'\frenchbsetup{StandardLayout}', + r'\AtBeginDocument{\selectlanguage{%s}' + r'\noextrasfrench}' % self.language] + return '\n'.join(self.setup) + + def next_quote(self): + q = self.quotes[self.quote_index] + self.quote_index = (self.quote_index+1) % 2 + return q + + def quote_quotes(self,text): + t = None + for part in text.split('"'): + if t == None: + t = part + else: + t += self.next_quote() + part + return t + + def language_name(self, language_code): + """Return TeX language name for `language_code`""" + for tag in utils.normalize_language_tag(language_code): + try: + return self.language_codes[tag] + except KeyError: + pass + if self.reporter is not None: + self.reporter.warning(self.warn_msg % language_code) + return '' + + def get_language(self): + """Return `self.language` (for backwards compatibility with Sphinx). + """ + return self.language + + +# Building blocks for the latex preamble +# -------------------------------------- + +class SortableDict(dict): + """Dictionary with additional sorting methods + + Tip: use key starting with with '_' for sorting before small letters + and with '~' for sorting after small letters. + """ + def sortedkeys(self): + """Return sorted list of keys""" + keys = self.keys() + keys.sort() + return keys + + def sortedvalues(self): + """Return list of values sorted by keys""" + return [self[key] for key in self.sortedkeys()] + + +# PreambleCmds +# ````````````` +# A container for LaTeX code snippets that can be +# inserted into the preamble if required in the document. +# +# .. The package 'makecmds' would enable shorter definitions using the +# \providelength and \provideenvironment commands. +# However, it is pretty non-standard (texlive-latex-extra). + +class PreambleCmds(object): + """Building blocks for the latex preamble.""" + +PreambleCmds.abstract = r""" +% abstract title +\providecommand*{\DUtitleabstract}[1]{\centerline{\textbf{#1}}}""" + +PreambleCmds.admonition = r""" +% admonition (specially marked topic) +\providecommand{\DUadmonition}[2][class-arg]{% + % try \DUadmonition#1{#2}: + \ifcsname DUadmonition#1\endcsname% + \csname DUadmonition#1\endcsname{#2}% + \else + \begin{center} + \fbox{\parbox{0.9\textwidth}{#2}} + \end{center} + \fi +}""" + +PreambleCmds.align_center = r""" +\makeatletter +\@namedef{DUrolealign-center}{\centering} +\makeatother +""" + +## PreambleCmds.caption = r"""% configure caption layout +## \usepackage{caption} +## \captionsetup{singlelinecheck=false}% no exceptions for one-liners""" + +PreambleCmds.color = r"""\usepackage{color}""" + +PreambleCmds.docinfo = r""" +% docinfo (width of docinfo table) +\DUprovidelength{\DUdocinfowidth}{0.9\textwidth}""" +# PreambleCmds.docinfo._depends = 'providelength' + +PreambleCmds.dedication = r""" +% dedication topic +\providecommand{\DUtopicdedication}[1]{\begin{center}#1\end{center}}""" + +PreambleCmds.error = r""" +% error admonition title +\providecommand*{\DUtitleerror}[1]{\DUtitle{\color{red}#1}}""" +# PreambleCmds.errortitle._depends = 'color' + +PreambleCmds.fieldlist = r""" +% fieldlist environment +\ifthenelse{\isundefined{\DUfieldlist}}{ + \newenvironment{DUfieldlist}% + {\quote\description} + {\enddescription\endquote} +}{}""" + +PreambleCmds.float_settings = r"""\usepackage{float} % float configuration +\floatplacement{figure}{H} % place figures here definitely""" + +PreambleCmds.footnotes = r"""% numeric or symbol footnotes with hyperlinks +\providecommand*{\DUfootnotemark}[3]{% + \raisebox{1em}{\hypertarget{#1}{}}% + \hyperlink{#2}{\textsuperscript{#3}}% +} +\providecommand{\DUfootnotetext}[4]{% + \begingroup% + \renewcommand{\thefootnote}{% + \protect\raisebox{1em}{\protect\hypertarget{#1}{}}% + \protect\hyperlink{#2}{#3}}% + \footnotetext{#4}% + \endgroup% +}""" + +PreambleCmds.footnote_floats = r"""% settings for footnotes as floats: +\setlength{\floatsep}{0.5em} +\setlength{\textfloatsep}{\fill} +\addtolength{\textfloatsep}{3em} +\renewcommand{\textfraction}{0.5} +\renewcommand{\topfraction}{0.5} +\renewcommand{\bottomfraction}{0.5} +\setcounter{totalnumber}{50} +\setcounter{topnumber}{50} +\setcounter{bottomnumber}{50}""" + +PreambleCmds.graphicx_auto = r"""% Check output format +\ifx\pdftexversion\undefined + \usepackage{graphicx} +\else + \usepackage[pdftex]{graphicx} +\fi'))""" + +PreambleCmds.inline = r""" +% inline markup (custom roles) +% \DUrole{#1}{#2} tries \DUrole#1{#2} +\providecommand*{\DUrole}[2]{% + \ifcsname DUrole#1\endcsname% + \csname DUrole#1\endcsname{#2}% + \else% backwards compatibility: try \docutilsrole#1{#2} + \ifcsname docutilsrole#1\endcsname% + \csname docutilsrole#1\endcsname{#2}% + \else% + #2% + \fi% + \fi% +}""" + +PreambleCmds.legend = r""" +% legend environment +\ifthenelse{\isundefined{\DUlegend}}{ + \newenvironment{DUlegend}{\small}{} +}{}""" + +PreambleCmds.lineblock = r""" +% lineblock environment +\DUprovidelength{\DUlineblockindent}{2.5em} +\ifthenelse{\isundefined{\DUlineblock}}{ + \newenvironment{DUlineblock}[1]{% + \list{}{\setlength{\partopsep}{\parskip} + \addtolength{\partopsep}{\baselineskip} + \setlength{\topsep}{0pt} + \setlength{\itemsep}{0.15\baselineskip} + \setlength{\parsep}{0pt} + \setlength{\leftmargin}{#1}} + \raggedright + } + {\endlist} +}{}""" +# PreambleCmds.lineblock._depends = 'providelength' + +PreambleCmds.linking = r""" +%% hyperlinks: +\ifthenelse{\isundefined{\hypersetup}}{ + \usepackage[%s]{hyperref} + \urlstyle{same} %% normal text font (alternatives: tt, rm, sf) +}{}""" + +PreambleCmds.minitoc = r"""%% local table of contents +\usepackage{minitoc}""" + +PreambleCmds.optionlist = r""" +% optionlist environment +\providecommand*{\DUoptionlistlabel}[1]{\bf #1 \hfill} +\DUprovidelength{\DUoptionlistindent}{3cm} +\ifthenelse{\isundefined{\DUoptionlist}}{ + \newenvironment{DUoptionlist}{% + \list{}{\setlength{\labelwidth}{\DUoptionlistindent} + \setlength{\rightmargin}{1cm} + \setlength{\leftmargin}{\rightmargin} + \addtolength{\leftmargin}{\labelwidth} + \addtolength{\leftmargin}{\labelsep} + \renewcommand{\makelabel}{\DUoptionlistlabel}} + } + {\endlist} +}{}""" +# PreambleCmds.optionlist._depends = 'providelength' + +PreambleCmds.providelength = r""" +% providelength (provide a length variable and set default, if it is new) +\providecommand*{\DUprovidelength}[2]{ + \ifthenelse{\isundefined{#1}}{\newlength{#1}\setlength{#1}{#2}}{} +}""" + +PreambleCmds.rubric = r""" +% rubric (informal heading) +\providecommand*{\DUrubric}[2][class-arg]{% + \subsubsection*{\centering\textit{\textmd{#2}}}}""" + +PreambleCmds.sidebar = r""" +% sidebar (text outside the main text flow) +\providecommand{\DUsidebar}[2][class-arg]{% + \begin{center} + \colorbox[gray]{0.80}{\parbox{0.9\textwidth}{#2}} + \end{center} +}""" + +PreambleCmds.subtitle = r""" +% subtitle (for topic/sidebar) +\providecommand*{\DUsubtitle}[2][class-arg]{\par\emph{#2}\smallskip}""" + +PreambleCmds.table = r"""\usepackage{longtable,ltcaption,array} +\setlength{\extrarowheight}{2pt} +\newlength{\DUtablewidth} % internal use in tables""" + +# Options [force,almostfull] prevent spurious error messages, see +# de.comp.text.tex/2005-12/msg01855 +PreambleCmds.textcomp = """\ +\\usepackage{textcomp} % text symbol macros""" + +PreambleCmds.titlereference = r""" +% titlereference role +\providecommand*{\DUroletitlereference}[1]{\textsl{#1}}""" + +PreambleCmds.title = r""" +% title for topics, admonitions, unsupported section levels, and sidebar +\providecommand*{\DUtitle}[2][class-arg]{% + % call \DUtitle#1{#2} if it exists: + \ifcsname DUtitle#1\endcsname% + \csname DUtitle#1\endcsname{#2}% + \else + \smallskip\noindent\textbf{#2}\smallskip% + \fi +}""" + +PreambleCmds.topic = r""" +% topic (quote with heading) +\providecommand{\DUtopic}[2][class-arg]{% + \ifcsname DUtopic#1\endcsname% + \csname DUtopic#1\endcsname{#2}% + \else + \begin{quote}#2\end{quote} + \fi +}""" + +PreambleCmds.transition = r""" +% transition (break, fancybreak, anonymous section) +\providecommand*{\DUtransition}[1][class-arg]{% + \hspace*{\fill}\hrulefill\hspace*{\fill} + \vskip 0.5\baselineskip +}""" + + +# LaTeX encoding maps +# ------------------- +# :: + +class CharMaps(object): + """LaTeX representations for active and Unicode characters.""" + + # characters that always need escaping: + special = { + ord('#'): ur'\#', + ord('$'): ur'\$', + ord('%'): ur'\%', + ord('&'): ur'\&', + ord('~'): ur'\textasciitilde{}', + ord('_'): ur'\_', + ord('^'): ur'\textasciicircum{}', + ord('\\'): ur'\textbackslash{}', + ord('{'): ur'\{', + ord('}'): ur'\}', + # Square brackets are ordinary chars and cannot be escaped with '\', + # so we put them in a group '{[}'. (Alternative: ensure that all + # macros with optional arguments are terminated with {} and text + # inside any optional argument is put in a group ``[{text}]``). + # Commands with optional args inside an optional arg must be put in a + # group, e.g. ``\item[{\hyperref[label]{text}}]``. + ord('['): ur'{[}', + ord(']'): ur'{]}', + # the soft hyphen is unknown in 8-bit text and not properly handled by XeTeX + 0x00AD: ur'\-', # SOFT HYPHEN + } + # Unicode chars that are not recognized by LaTeX's utf8 encoding + unsupported_unicode = { + 0x00A0: ur'~', # NO-BREAK SPACE + # TODO: ensure white space also at the beginning of a line? + # 0x00A0: ur'\leavevmode\nobreak\vadjust{}~' + 0x2008: ur'\,', # PUNCTUATION SPACE    + 0x2011: ur'\hbox{-}', # NON-BREAKING HYPHEN + 0x202F: ur'\,', # NARROW NO-BREAK SPACE + 0x21d4: ur'$\Leftrightarrow$', + # Docutils footnote symbols: + 0x2660: ur'$\spadesuit$', + 0x2663: ur'$\clubsuit$', + } + # Unicode chars that are recognized by LaTeX's utf8 encoding + utf8_supported_unicode = { + 0x200C: ur'\textcompwordmark', # ZERO WIDTH NON-JOINER + 0x2013: ur'\textendash{}', + 0x2014: ur'\textemdash{}', + 0x2018: ur'\textquoteleft{}', + 0x2019: ur'\textquoteright{}', + 0x201A: ur'\quotesinglbase{}', # SINGLE LOW-9 QUOTATION MARK + 0x201C: ur'\textquotedblleft{}', + 0x201D: ur'\textquotedblright{}', + 0x201E: ur'\quotedblbase{}', # DOUBLE LOW-9 QUOTATION MARK + 0x2030: ur'\textperthousand{}', # PER MILLE SIGN + 0x2031: ur'\textpertenthousand{}', # PER TEN THOUSAND SIGN + 0x2039: ur'\guilsinglleft{}', + 0x203A: ur'\guilsinglright{}', + 0x2423: ur'\textvisiblespace{}', # OPEN BOX + 0x2020: ur'\dag{}', + 0x2021: ur'\ddag{}', + 0x2026: ur'\dots{}', + 0x2122: ur'\texttrademark{}', + } + # recognized with 'utf8', if textcomp is loaded + textcomp = { + # Latin-1 Supplement + 0x00a2: ur'\textcent{}', # ¢ CENT SIGN + 0x00a4: ur'\textcurrency{}', # ¤ CURRENCY SYMBOL + 0x00a5: ur'\textyen{}', # ¥ YEN SIGN + 0x00a6: ur'\textbrokenbar{}', # ¦ BROKEN BAR + 0x00a7: ur'\textsection{}', # § SECTION SIGN + 0x00a8: ur'\textasciidieresis{}', # ¨ DIAERESIS + 0x00a9: ur'\textcopyright{}', # © COPYRIGHT SIGN + 0x00aa: ur'\textordfeminine{}', # ª FEMININE ORDINAL INDICATOR + 0x00ac: ur'\textlnot{}', # ¬ NOT SIGN + 0x00ae: ur'\textregistered{}', # ® REGISTERED SIGN + 0x00af: ur'\textasciimacron{}', # ¯ MACRON + 0x00b0: ur'\textdegree{}', # ° DEGREE SIGN + 0x00b1: ur'\textpm{}', # ± PLUS-MINUS SIGN + 0x00b2: ur'\texttwosuperior{}', # ² SUPERSCRIPT TWO + 0x00b3: ur'\textthreesuperior{}', # ³ SUPERSCRIPT THREE + 0x00b4: ur'\textasciiacute{}', # ´ ACUTE ACCENT + 0x00b5: ur'\textmu{}', # µ MICRO SIGN + 0x00b6: ur'\textparagraph{}', # ¶ PILCROW SIGN # not equal to \textpilcrow + 0x00b9: ur'\textonesuperior{}', # ¹ SUPERSCRIPT ONE + 0x00ba: ur'\textordmasculine{}', # º MASCULINE ORDINAL INDICATOR + 0x00bc: ur'\textonequarter{}', # 1/4 FRACTION + 0x00bd: ur'\textonehalf{}', # 1/2 FRACTION + 0x00be: ur'\textthreequarters{}', # 3/4 FRACTION + 0x00d7: ur'\texttimes{}', # × MULTIPLICATION SIGN + 0x00f7: ur'\textdiv{}', # ÷ DIVISION SIGN + # + 0x0192: ur'\textflorin{}', # LATIN SMALL LETTER F WITH HOOK + 0x02b9: ur'\textasciiacute{}', # MODIFIER LETTER PRIME + 0x02ba: ur'\textacutedbl{}', # MODIFIER LETTER DOUBLE PRIME + 0x2016: ur'\textbardbl{}', # DOUBLE VERTICAL LINE + 0x2022: ur'\textbullet{}', # BULLET + 0x2032: ur'\textasciiacute{}', # PRIME + 0x2033: ur'\textacutedbl{}', # DOUBLE PRIME + 0x2035: ur'\textasciigrave{}', # REVERSED PRIME + 0x2036: ur'\textgravedbl{}', # REVERSED DOUBLE PRIME + 0x203b: ur'\textreferencemark{}', # REFERENCE MARK + 0x203d: ur'\textinterrobang{}', # INTERROBANG + 0x2044: ur'\textfractionsolidus{}', # FRACTION SLASH + 0x2045: ur'\textlquill{}', # LEFT SQUARE BRACKET WITH QUILL + 0x2046: ur'\textrquill{}', # RIGHT SQUARE BRACKET WITH QUILL + 0x2052: ur'\textdiscount{}', # COMMERCIAL MINUS SIGN + 0x20a1: ur'\textcolonmonetary{}', # COLON SIGN + 0x20a3: ur'\textfrenchfranc{}', # FRENCH FRANC SIGN + 0x20a4: ur'\textlira{}', # LIRA SIGN + 0x20a6: ur'\textnaira{}', # NAIRA SIGN + 0x20a9: ur'\textwon{}', # WON SIGN + 0x20ab: ur'\textdong{}', # DONG SIGN + 0x20ac: ur'\texteuro{}', # EURO SIGN + 0x20b1: ur'\textpeso{}', # PESO SIGN + 0x20b2: ur'\textguarani{}', # GUARANI SIGN + 0x2103: ur'\textcelsius{}', # DEGREE CELSIUS + 0x2116: ur'\textnumero{}', # NUMERO SIGN + 0x2117: ur'\textcircledP{}', # SOUND RECORDING COYRIGHT + 0x211e: ur'\textrecipe{}', # PRESCRIPTION TAKE + 0x2120: ur'\textservicemark{}', # SERVICE MARK + 0x2122: ur'\texttrademark{}', # TRADE MARK SIGN + 0x2126: ur'\textohm{}', # OHM SIGN + 0x2127: ur'\textmho{}', # INVERTED OHM SIGN + 0x212e: ur'\textestimated{}', # ESTIMATED SYMBOL + 0x2190: ur'\textleftarrow{}', # LEFTWARDS ARROW + 0x2191: ur'\textuparrow{}', # UPWARDS ARROW + 0x2192: ur'\textrightarrow{}', # RIGHTWARDS ARROW + 0x2193: ur'\textdownarrow{}', # DOWNWARDS ARROW + 0x2212: ur'\textminus{}', # MINUS SIGN + 0x2217: ur'\textasteriskcentered{}', # ASTERISK OPERATOR + 0x221a: ur'\textsurd{}', # SQUARE ROOT + 0x2422: ur'\textblank{}', # BLANK SYMBOL + 0x25e6: ur'\textopenbullet{}', # WHITE BULLET + 0x25ef: ur'\textbigcircle{}', # LARGE CIRCLE + 0x266a: ur'\textmusicalnote{}', # EIGHTH NOTE + 0x26ad: ur'\textmarried{}', # MARRIAGE SYMBOL + 0x26ae: ur'\textdivorced{}', # DIVORCE SYMBOL + 0x27e8: ur'\textlangle{}', # MATHEMATICAL LEFT ANGLE BRACKET + 0x27e9: ur'\textrangle{}', # MATHEMATICAL RIGHT ANGLE BRACKET + } + # Unicode chars that require a feature/package to render + pifont = { + 0x2665: ur'\ding{170}', # black heartsuit + 0x2666: ur'\ding{169}', # black diamondsuit + 0x2713: ur'\ding{51}', # check mark + 0x2717: ur'\ding{55}', # check mark + } + # TODO: greek alphabet ... ? + # see also LaTeX codec + # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/252124 + # and unimap.py from TeXML + + +class DocumentClass(object): + """Details of a LaTeX document class.""" + + def __init__(self, document_class, with_part=False): + self.document_class = document_class + self._with_part = with_part + self.sections = ['section', 'subsection', 'subsubsection', + 'paragraph', 'subparagraph'] + if self.document_class in ('book', 'memoir', 'report', + 'scrbook', 'scrreprt'): + self.sections.insert(0, 'chapter') + if self._with_part: + self.sections.insert(0, 'part') + + def section(self, level): + """Return the LaTeX section name for section `level`. + + The name depends on the specific document class. + Level is 1,2,3..., as level 0 is the title. + """ + if level <= len(self.sections): + return self.sections[level-1] + else: # unsupported levels + return 'DUtitle[section%s]' % roman.toRoman(level) + +class Table(object): + """Manage a table while traversing. + + Maybe change to a mixin defining the visit/departs, but then + class Table internal variables are in the Translator. + + Table style might be + + :standard: horizontal and vertical lines + :booktabs: only horizontal lines (requires "booktabs" LaTeX package) + :borderless: no borders around table cells + :nolines: alias for borderless + """ + def __init__(self,translator,latex_type,table_style): + self._translator = translator + self._latex_type = latex_type + self._table_style = table_style + self._open = False + # miscellaneous attributes + self._attrs = {} + self._col_width = [] + self._rowspan = [] + self.stubs = [] + self._in_thead = 0 + + def open(self): + self._open = True + self._col_specs = [] + self.caption = [] + self._attrs = {} + self._in_head = False # maybe context with search + def close(self): + self._open = False + self._col_specs = None + self.caption = [] + self._attrs = {} + self.stubs = [] + def is_open(self): + return self._open + + def set_table_style(self, table_style): + if not table_style in ('standard','booktabs','borderless','nolines'): + return + self._table_style = table_style + + def get_latex_type(self): + if self._latex_type == 'longtable' and not self.caption: + # do not advance the "table" counter (requires "ltcaption" package) + return('longtable*') + return self._latex_type + + def set(self,attr,value): + self._attrs[attr] = value + def get(self,attr): + if attr in self._attrs: + return self._attrs[attr] + return None + + def get_vertical_bar(self): + if self._table_style == 'standard': + return '|' + return '' + + # horizontal lines are drawn below a row, + def get_opening(self): + return '\n'.join([r'\setlength{\DUtablewidth}{\linewidth}', + r'\begin{%s}[c]' % self.get_latex_type()]) + + def get_closing(self): + closing = [] + if self._table_style == 'booktabs': + closing.append(r'\bottomrule') + # elif self._table_style == 'standard': + # closing.append(r'\hline') + closing.append(r'\end{%s}' % self.get_latex_type()) + return '\n'.join(closing) + + def visit_colspec(self, node): + self._col_specs.append(node) + # "stubs" list is an attribute of the tgroup element: + self.stubs.append(node.attributes.get('stub')) + + def get_colspecs(self): + """Return column specification for longtable. + + Assumes reST line length being 80 characters. + Table width is hairy. + + === === + ABC DEF + === === + + usually gets to narrow, therefore we add 1 (fiddlefactor). + """ + width = 80 + + total_width = 0.0 + # first see if we get too wide. + for node in self._col_specs: + colwidth = float(node['colwidth']+1) / width + total_width += colwidth + self._col_width = [] + self._rowspan = [] + # donot make it full linewidth + factor = 0.93 + if total_width > 1.0: + factor /= total_width + bar = self.get_vertical_bar() + latex_table_spec = '' + for node in self._col_specs: + colwidth = factor * float(node['colwidth']+1) / width + self._col_width.append(colwidth+0.005) + self._rowspan.append(0) + latex_table_spec += '%sp{%.3f\\DUtablewidth}' % (bar, colwidth+0.005) + return latex_table_spec+bar + + def get_column_width(self): + """Return columnwidth for current cell (not multicell).""" + return '%.2f\\DUtablewidth' % self._col_width[self._cell_in_row-1] + + def get_multicolumn_width(self, start, len_): + """Return sum of columnwidths for multicell.""" + mc_width = sum([width + for width in ([self._col_width[start + co - 1] + for co in range (len_)])]) + return '%.2f\\DUtablewidth' % mc_width + + def get_caption(self): + if not self.caption: + return '' + caption = ''.join(self.caption) + if 1 == self._translator.thead_depth(): + return r'\caption{%s}\\' '\n' % caption + return r'\caption[]{%s (... continued)}\\' '\n' % caption + + def need_recurse(self): + if self._latex_type == 'longtable': + return 1 == self._translator.thead_depth() + return 0 + + def visit_thead(self): + self._in_thead += 1 + if self._table_style == 'standard': + return ['\\hline\n'] + elif self._table_style == 'booktabs': + return ['\\toprule\n'] + return [] + + def depart_thead(self): + a = [] + #if self._table_style == 'standard': + # a.append('\\hline\n') + if self._table_style == 'booktabs': + a.append('\\midrule\n') + if self._latex_type == 'longtable': + if 1 == self._translator.thead_depth(): + a.append('\\endfirsthead\n') + else: + a.append('\\endhead\n') + a.append(r'\multicolumn{%d}{c}' % len(self._col_specs) + + r'{\hfill ... continued on next page} \\') + a.append('\n\\endfoot\n\\endlastfoot\n') + # for longtable one could add firsthead, foot and lastfoot + self._in_thead -= 1 + return a + def visit_row(self): + self._cell_in_row = 0 + def depart_row(self): + res = [' \\\\\n'] + self._cell_in_row = None # remove cell counter + for i in range(len(self._rowspan)): + if (self._rowspan[i]>0): + self._rowspan[i] -= 1 + + if self._table_style == 'standard': + rowspans = [i+1 for i in range(len(self._rowspan)) + if (self._rowspan[i]<=0)] + if len(rowspans)==len(self._rowspan): + res.append('\\hline\n') + else: + cline = '' + rowspans.reverse() + # TODO merge clines + while True: + try: + c_start = rowspans.pop() + except: + break + cline += '\\cline{%d-%d}\n' % (c_start,c_start) + res.append(cline) + return res + + def set_rowspan(self,cell,value): + try: + self._rowspan[cell] = value + except: + pass + def get_rowspan(self,cell): + try: + return self._rowspan[cell] + except: + return 0 + def get_entry_number(self): + return self._cell_in_row + def visit_entry(self): + self._cell_in_row += 1 + def is_stub_column(self): + if len(self.stubs) >= self._cell_in_row: + return self.stubs[self._cell_in_row-1] + return False + + +class LaTeXTranslator(nodes.NodeVisitor): + + # When options are given to the documentclass, latex will pass them + # to other packages, as done with babel. + # Dummy settings might be taken from document settings + + # Write code for typesetting with 8-bit tex/pdftex (vs. xetex/luatex) engine + # overwritten by the XeTeX writer + is_xetex = False + + # Config setting defaults + # ----------------------- + + # TODO: use mixins for different implementations. + # list environment for docinfo. else tabularx + ## use_optionlist_for_docinfo = False # TODO: NOT YET IN USE + + # Use compound enumerations (1.A.1.) + compound_enumerators = False + + # If using compound enumerations, include section information. + section_prefix_for_enumerators = False + + # This is the character that separates the section ("." subsection ...) + # prefix from the regular list enumerator. + section_enumerator_separator = '-' + + # Auxiliary variables + # ------------------- + + has_latex_toc = False # is there a toc in the doc? (needed by minitoc) + is_toc_list = False # is the current bullet_list a ToC? + section_level = 0 + + # Flags to encode(): + # inside citation reference labels underscores dont need to be escaped + inside_citation_reference_label = False + verbatim = False # do not encode + insert_non_breaking_blanks = False # replace blanks by "~" + insert_newline = False # add latex newline commands + literal = False # literal text (block or inline) + + + def __init__(self, document, babel_class=Babel): + nodes.NodeVisitor.__init__(self, document) + # Reporter + # ~~~~~~~~ + self.warn = self.document.reporter.warning + self.error = self.document.reporter.error + + # Settings + # ~~~~~~~~ + self.settings = settings = document.settings + self.latex_encoding = self.to_latex_encoding(settings.output_encoding) + self.use_latex_toc = settings.use_latex_toc + self.use_latex_docinfo = settings.use_latex_docinfo + self._use_latex_citations = settings.use_latex_citations + self._reference_label = settings.reference_label + self.hyperlink_color = settings.hyperlink_color + self.compound_enumerators = settings.compound_enumerators + self.font_encoding = getattr(settings, 'font_encoding', '') + self.section_prefix_for_enumerators = ( + settings.section_prefix_for_enumerators) + self.section_enumerator_separator = ( + settings.section_enumerator_separator.replace('_', r'\_')) + # literal blocks: + self.literal_block_env = '' + self.literal_block_options = '' + if settings.literal_block_env != '': + (none, + self.literal_block_env, + self.literal_block_options, + none ) = re.split('(\w+)(.*)', settings.literal_block_env) + elif settings.use_verbatim_when_possible: + self.literal_block_env = 'verbatim' + # + if self.settings.use_bibtex: + self.bibtex = self.settings.use_bibtex.split(',',1) + # TODO avoid errors on not declared citations. + else: + self.bibtex = None + # language module for Docutils-generated text + # (labels, bibliographic_fields, and author_separators) + self.language_module = languages.get_language(settings.language_code, + document.reporter) + self.babel = babel_class(settings.language_code, document.reporter) + self.author_separator = self.language_module.author_separators[0] + d_options = [self.settings.documentoptions] + if self.babel.language not in ('english', ''): + d_options.append(self.babel.language) + self.documentoptions = ','.join(filter(None, d_options)) + self.d_class = DocumentClass(settings.documentclass, + settings.use_part_section) + # graphic package options: + if self.settings.graphicx_option == '': + self.graphicx_package = r'\usepackage{graphicx}' + elif self.settings.graphicx_option.lower() == 'auto': + self.graphicx_package = PreambleCmds.graphicx_auto + else: + self.graphicx_package = (r'\usepackage[%s]{graphicx}' % + self.settings.graphicx_option) + # footnotes: + self.docutils_footnotes = settings.docutils_footnotes + if settings.use_latex_footnotes: + self.docutils_footnotes = True + self.warn('`use_latex_footnotes` is deprecated. ' + 'The setting has been renamed to `docutils_footnotes` ' + 'and the alias will be removed in a future version.') + self.figure_footnotes = settings.figure_footnotes + if self.figure_footnotes: + self.docutils_footnotes = True + self.warn('The "figure footnotes" workaround/setting is strongly ' + 'deprecated and will be removed in a future version.') + + # Output collection stacks + # ~~~~~~~~~~~~~~~~~~~~~~~~ + + # Document parts + self.head_prefix = [r'\documentclass[%s]{%s}' % + (self.documentoptions, self.settings.documentclass)] + self.requirements = SortableDict() # made a list in depart_document() + self.requirements['__static'] = r'\usepackage{ifthen}' + self.latex_preamble = [settings.latex_preamble] + self.fallbacks = SortableDict() # made a list in depart_document() + self.pdfsetup = [] # PDF properties (hyperref package) + self.title = [] + self.subtitle = [] + self.titledata = [] # \title, \author, \date + ## self.body_prefix = ['\\begin{document}\n'] + self.body_pre_docinfo = [] # \maketitle + self.docinfo = [] + self.dedication = [] + self.abstract = [] + self.body = [] + ## self.body_suffix = ['\\end{document}\n'] + + # A heterogenous stack used in conjunction with the tree traversal. + # Make sure that the pops correspond to the pushes: + self.context = [] + + # Title metadata: + self.title_labels = [] + self.subtitle_labels = [] + # (if use_latex_docinfo: collects lists of + # author/organization/contact/address lines) + self.author_stack = [] + self.date = [] + + # PDF properties: pdftitle, pdfauthor + # TODO?: pdfcreator, pdfproducer, pdfsubject, pdfkeywords + self.pdfinfo = [] + self.pdfauthor = [] + + # Stack of section counters so that we don't have to use_latex_toc. + # This will grow and shrink as processing occurs. + # Initialized for potential first-level sections. + self._section_number = [0] + + # The current stack of enumerations so that we can expand + # them into a compound enumeration. + self._enumeration_counters = [] + # The maximum number of enumeration counters we've used. + # If we go beyond this number, we need to create a new + # counter; otherwise, just reuse an old one. + self._max_enumeration_counters = 0 + + self._bibitems = [] + + # object for a table while proccessing. + self.table_stack = [] + self.active_table = Table(self, 'longtable', settings.table_style) + + # Where to collect the output of visitor methods (default: body) + self.out = self.body + self.out_stack = [] # stack of output collectors + + # Process settings + # ~~~~~~~~~~~~~~~~ + # Encodings: + # Docutils' output-encoding => TeX input encoding + if self.latex_encoding != 'ascii': + self.requirements['_inputenc'] = (r'\usepackage[%s]{inputenc}' + % self.latex_encoding) + # TeX font encoding + if self.font_encoding and not self.is_xetex: + self.requirements['_fontenc'] = (r'\usepackage[%s]{fontenc}' % + self.font_encoding) + # page layout with typearea (if there are relevant document options) + if (settings.documentclass.find('scr') == -1 and + (self.documentoptions.find('DIV') != -1 or + self.documentoptions.find('BCOR') != -1)): + self.requirements['typearea'] = r'\usepackage{typearea}' + + # Stylesheets + # (the name `self.stylesheet` is singular because only one + # stylesheet was supported before Docutils 0.6). + self.stylesheet = [self.stylesheet_call(path) + for path in utils.get_stylesheet_list(settings)] + + # PDF setup + if self.hyperlink_color in ('0', 'false', 'False', ''): + self.hyperref_options = '' + else: + self.hyperref_options = 'colorlinks=true,linkcolor=%s,urlcolor=%s' % ( + self.hyperlink_color, self.hyperlink_color) + if settings.hyperref_options: + self.hyperref_options += ',' + settings.hyperref_options + + # LaTeX Toc + # include all supported sections in toc and PDF bookmarks + # (or use documentclass-default (as currently))? + ## if self.use_latex_toc: + ## self.requirements['tocdepth'] = (r'\setcounter{tocdepth}{%d}' % + ## len(self.d_class.sections)) + + # Section numbering + if not self.settings.sectnum_xform: # section numbering by Docutils + PreambleCmds.secnumdepth = r'\setcounter{secnumdepth}{0}' + else: # section numbering by LaTeX: + secnumdepth = settings.sectnum_depth + # Possible values of settings.sectnum_depth: + # None "sectnum" directive without depth arg -> LaTeX default + # 0 no "sectnum" directive -> no section numbers + # >0 value of "depth" argument -> translate to LaTeX levels: + # -1 part (0 with "article" document class) + # 0 chapter (missing in "article" document class) + # 1 section + # 2 subsection + # 3 subsubsection + # 4 paragraph + # 5 subparagraph + if secnumdepth is not None: + # limit to supported levels + secnumdepth = min(secnumdepth, len(self.d_class.sections)) + # adjust to document class and use_part_section settings + if 'chapter' in self.d_class.sections: + secnumdepth -= 1 + if self.d_class.sections[0] == 'part': + secnumdepth -= 1 + PreambleCmds.secnumdepth = \ + r'\setcounter{secnumdepth}{%d}' % secnumdepth + + # start with specified number: + if (hasattr(settings, 'sectnum_start') and + settings.sectnum_start != 1): + self.requirements['sectnum_start'] = ( + r'\setcounter{%s}{%d}' % (self.d_class.sections[0], + settings.sectnum_start-1)) + # TODO: currently ignored (configure in a stylesheet): + ## settings.sectnum_prefix + ## settings.sectnum_suffix + + # Auxiliary Methods + # ----------------- + + def stylesheet_call(self, path): + """Return code to reference or embed stylesheet file `path`""" + # is it a package (no extension or *.sty) or "normal" tex code: + (base, ext) = os.path.splitext(path) + is_package = ext in ['.sty', ''] + # Embed content of style file: + if self.settings.embed_stylesheet: + if is_package: + path = base + '.sty' # ensure extension + try: + content = io.FileInput(source_path=path, + encoding='utf-8', + handle_io_errors=False).read() + self.settings.record_dependencies.add(path) + except IOError, err: + msg = u"Cannot embed stylesheet '%s':\n %s." % ( + path, SafeString(err.strerror)) + self.document.reporter.error(msg) + return '% ' + msg.replace('\n', '\n% ') + if is_package: + content = '\n'.join([r'\makeatletter', + content, + r'\makeatother']) + return '%% embedded stylesheet: %s\n%s' % (path, content) + # Link to style file: + if is_package: + path = base # drop extension + cmd = r'\usepackage{%s}' + else: + cmd = r'\input{%s}' + if self.settings.stylesheet_path: + # adapt path relative to output (cf. config.html#stylesheet-path) + path = utils.relative_path(self.settings._destination, path) + return cmd % path + + def to_latex_encoding(self,docutils_encoding): + """Translate docutils encoding name into LaTeX's. + + Default method is remove "-" and "_" chars from docutils_encoding. + """ + tr = { 'iso-8859-1': 'latin1', # west european + 'iso-8859-2': 'latin2', # east european + 'iso-8859-3': 'latin3', # esperanto, maltese + 'iso-8859-4': 'latin4', # north european, scandinavian, baltic + 'iso-8859-5': 'iso88595', # cyrillic (ISO) + 'iso-8859-9': 'latin5', # turkish + 'iso-8859-15': 'latin9', # latin9, update to latin1. + 'mac_cyrillic': 'maccyr', # cyrillic (on Mac) + 'windows-1251': 'cp1251', # cyrillic (on Windows) + 'koi8-r': 'koi8-r', # cyrillic (Russian) + 'koi8-u': 'koi8-u', # cyrillic (Ukrainian) + 'windows-1250': 'cp1250', # + 'windows-1252': 'cp1252', # + 'us-ascii': 'ascii', # ASCII (US) + # unmatched encodings + #'': 'applemac', + #'': 'ansinew', # windows 3.1 ansi + #'': 'ascii', # ASCII encoding for the range 32--127. + #'': 'cp437', # dos latin us + #'': 'cp850', # dos latin 1 + #'': 'cp852', # dos latin 2 + #'': 'decmulti', + #'': 'latin10', + #'iso-8859-6': '' # arabic + #'iso-8859-7': '' # greek + #'iso-8859-8': '' # hebrew + #'iso-8859-10': '' # latin6, more complete iso-8859-4 + } + encoding = docutils_encoding.lower() + if encoding in tr: + return tr[encoding] + # drop hyphen or low-line from "latin-1", "latin_1", "utf-8" and similar + encoding = encoding.replace('_', '').replace('-', '') + # strip the error handler + return encoding.split(':')[0] + + def language_label(self, docutil_label): + return self.language_module.labels[docutil_label] + + def encode(self, text): + """Return text with 'problematic' characters escaped. + + * Escape the ten special printing characters ``# $ % & ~ _ ^ \ { }``, + square brackets ``[ ]``, double quotes and (in OT1) ``< | >``. + * Translate non-supported Unicode characters. + * Separate ``-`` (and more in literal text) to prevent input ligatures. + """ + if self.verbatim: + return text + + # Set up the translation table: + table = CharMaps.special.copy() + # keep the underscore in citation references + if self.inside_citation_reference_label: + del(table[ord('_')]) + # Workarounds for OT1 font-encoding + if self.font_encoding in ['OT1', ''] and not self.is_xetex: + # * out-of-order characters in cmtt + if self.literal: + # replace underscore by underlined blank, + # because this has correct width. + table[ord('_')] = u'\\underline{~}' + # the backslash doesn't work, so we use a mirrored slash. + # \reflectbox is provided by graphicx: + self.requirements['graphicx'] = self.graphicx_package + table[ord('\\')] = ur'\reflectbox{/}' + # * ``< | >`` come out as different chars (except for cmtt): + else: + table[ord('|')] = ur'\textbar{}' + table[ord('<')] = ur'\textless{}' + table[ord('>')] = ur'\textgreater{}' + if self.insert_non_breaking_blanks: + table[ord(' ')] = ur'~' + if self.literal: + # double quotes are 'active' in some languages + # TODO: use \textquotedbl if font encoding starts with T? + table[ord('"')] = self.babel.literal_double_quote + # Unicode replacements for 8-bit tex engines (not required with XeTeX/LuaTeX): + if not self.is_xetex: + table.update(CharMaps.unsupported_unicode) + if not self.latex_encoding.startswith('utf8'): + table.update(CharMaps.utf8_supported_unicode) + table.update(CharMaps.textcomp) + table.update(CharMaps.pifont) + # Characters that require a feature/package to render + if [True for ch in text if ord(ch) in CharMaps.textcomp]: + self.requirements['textcomp'] = PreambleCmds.textcomp + if [True for ch in text if ord(ch) in CharMaps.pifont]: + self.requirements['pifont'] = '\\usepackage{pifont}' + + text = text.translate(table) + + # Break up input ligatures e.g. '--' to '-{}-'. + if not self.is_xetex: # Not required with xetex/luatex + separate_chars = '-' + # In monospace-font, we also separate ',,', '``' and "''" and some + # other characters which can't occur in non-literal text. + if self.literal: + separate_chars += ',`\'"<>' + for char in separate_chars * 2: + # Do it twice ("* 2") because otherwise we would replace + # '---' by '-{}--'. + text = text.replace(char + char, char + '{}' + char) + + # Literal line breaks (in address or literal blocks): + if self.insert_newline: + lines = text.split('\n') + # Add a protected space to blank lines (except the last) + # to avoid ``! LaTeX Error: There's no line here to end.`` + for i, line in enumerate(lines[:-1]): + if not line.lstrip(): + lines[i] += '~' + text = (r'\\' + '\n').join(lines) + if not self.literal: + text = self.babel.quote_quotes(text) + if self.literal and not self.insert_non_breaking_blanks: + # preserve runs of spaces but allow wrapping + text = text.replace(' ', ' ~') + return text + + def attval(self, text, + whitespace=re.compile('[\n\r\t\v\f]')): + """Cleanse, encode, and return attribute value text.""" + return self.encode(whitespace.sub(' ', text)) + + # TODO: is this used anywhere? -> update (use template) or delete + ## def astext(self): + ## """Assemble document parts and return as string.""" + ## head = '\n'.join(self.head_prefix + self.stylesheet + self.head) + ## body = ''.join(self.body_prefix + self.body + self.body_suffix) + ## return head + '\n' + body + + def is_inline(self, node): + """Check whether a node represents an inline element""" + return isinstance(node.parent, nodes.TextElement) + + def append_hypertargets(self, node): + """Append hypertargets for all ids of `node`""" + # hypertarget places the anchor at the target's baseline, + # so we raise it explicitely + self.out.append('%\n'.join(['\\raisebox{1em}{\\hypertarget{%s}{}}' % + id for id in node['ids']])) + + def ids_to_labels(self, node, set_anchor=True): + """Return list of label definitions for all ids of `node` + + If `set_anchor` is True, an anchor is set with \phantomsection. + """ + labels = ['\\label{%s}' % id for id in node.get('ids', [])] + if set_anchor and labels: + labels.insert(0, '\\phantomsection') + return labels + + def push_output_collector(self, new_out): + self.out_stack.append(self.out) + self.out = new_out + + def pop_output_collector(self): + self.out = self.out_stack.pop() + + # Visitor methods + # --------------- + + def visit_Text(self, node): + self.out.append(self.encode(node.astext())) + + def depart_Text(self, node): + pass + + def visit_abbreviation(self, node): + node['classes'].insert(0, 'abbreviation') + self.visit_inline(node) + + def depart_abbreviation(self, node): + self.depart_inline(node) + + def visit_acronym(self, node): + node['classes'].insert(0, 'acronym') + self.visit_inline(node) + + def depart_acronym(self, node): + self.depart_inline(node) + + def visit_address(self, node): + self.visit_docinfo_item(node, 'address') + + def depart_address(self, node): + self.depart_docinfo_item(node) + + def visit_admonition(self, node): + self.fallbacks['admonition'] = PreambleCmds.admonition + if 'error' in node['classes']: + self.fallbacks['error'] = PreambleCmds.error + # strip the generic 'admonition' from the list of classes + node['classes'] = [cls for cls in node['classes'] + if cls != 'admonition'] + self.out.append('\n\\DUadmonition[%s]{\n' % ','.join(node['classes'])) + + def depart_admonition(self, node=None): + self.out.append('}\n') + + def visit_author(self, node): + self.visit_docinfo_item(node, 'author') + + def depart_author(self, node): + self.depart_docinfo_item(node) + + def visit_authors(self, node): + # not used: visit_author is called anyway for each author. + pass + + def depart_authors(self, node): + pass + + def visit_block_quote(self, node): + self.out.append( '%\n\\begin{quote}\n') + if node['classes']: + self.visit_inline(node) + + def depart_block_quote(self, node): + if node['classes']: + self.depart_inline(node) + self.out.append( '\n\\end{quote}\n') + + def visit_bullet_list(self, node): + if self.is_toc_list: + self.out.append( '%\n\\begin{list}{}{}\n' ) + else: + self.out.append( '%\n\\begin{itemize}\n' ) + + def depart_bullet_list(self, node): + if self.is_toc_list: + self.out.append( '\n\\end{list}\n' ) + else: + self.out.append( '\n\\end{itemize}\n' ) + + def visit_superscript(self, node): + self.out.append(r'\textsuperscript{') + if node['classes']: + self.visit_inline(node) + + def depart_superscript(self, node): + if node['classes']: + self.depart_inline(node) + self.out.append('}') + + def visit_subscript(self, node): + self.out.append(r'\textsubscript{') # requires `fixltx2e` + if node['classes']: + self.visit_inline(node) + + def depart_subscript(self, node): + if node['classes']: + self.depart_inline(node) + self.out.append('}') + + def visit_caption(self, node): + self.out.append( '\\caption{' ) + + def depart_caption(self, node): + self.out.append('}\n') + + def visit_title_reference(self, node): + self.fallbacks['titlereference'] = PreambleCmds.titlereference + self.out.append(r'\DUroletitlereference{') + if node['classes']: + self.visit_inline(node) + + def depart_title_reference(self, node): + if node['classes']: + self.depart_inline(node) + self.out.append( '}' ) + + def visit_citation(self, node): + # TODO maybe use cite bibitems + if self._use_latex_citations: + self.push_output_collector([]) + else: + # TODO: do we need these? + ## self.requirements['~fnt_floats'] = PreambleCmds.footnote_floats + self.out.append(r'\begin{figure}[b]') + self.append_hypertargets(node) + + def depart_citation(self, node): + if self._use_latex_citations: + label = self.out[0] + text = ''.join(self.out[1:]) + self._bibitems.append([label, text]) + self.pop_output_collector() + else: + self.out.append('\\end{figure}\n') + + def visit_citation_reference(self, node): + if self._use_latex_citations: + if not self.inside_citation_reference_label: + self.out.append(r'\cite{') + self.inside_citation_reference_label = 1 + else: + assert self.body[-1] in (' ', '\n'),\ + 'unexpected non-whitespace while in reference label' + del self.body[-1] + else: + href = '' + if 'refid' in node: + href = node['refid'] + elif 'refname' in node: + href = self.document.nameids[node['refname']] + self.out.append('\\hyperlink{%s}{[' % href) + + def depart_citation_reference(self, node): + if self._use_latex_citations: + followup_citation = False + # check for a following citation separated by a space or newline + next_siblings = node.traverse(descend=False, siblings=True, + include_self=False) + if len(next_siblings) > 1: + next = next_siblings[0] + if (isinstance(next, nodes.Text) and + next.astext() in (' ', '\n')): + if next_siblings[1].__class__ == node.__class__: + followup_citation = True + if followup_citation: + self.out.append(',') + else: + self.out.append('}') + self.inside_citation_reference_label = False + else: + self.out.append(']}') + + def visit_classifier(self, node): + self.out.append( '(\\textbf{' ) + + def depart_classifier(self, node): + self.out.append( '})\n' ) + + def visit_colspec(self, node): + self.active_table.visit_colspec(node) + + def depart_colspec(self, node): + pass + + def visit_comment(self, node): + # Precede every line with a comment sign, wrap in newlines + self.out.append('\n%% %s\n' % node.astext().replace('\n', '\n% ')) + raise nodes.SkipNode + + def depart_comment(self, node): + pass + + def visit_compound(self, node): + pass + + def depart_compound(self, node): + pass + + def visit_contact(self, node): + self.visit_docinfo_item(node, 'contact') + + def depart_contact(self, node): + self.depart_docinfo_item(node) + + def visit_container(self, node): + pass + + def depart_container(self, node): + pass + + def visit_copyright(self, node): + self.visit_docinfo_item(node, 'copyright') + + def depart_copyright(self, node): + self.depart_docinfo_item(node) + + def visit_date(self, node): + self.visit_docinfo_item(node, 'date') + + def depart_date(self, node): + self.depart_docinfo_item(node) + + def visit_decoration(self, node): + # header and footer + pass + + def depart_decoration(self, node): + pass + + def visit_definition(self, node): + pass + + def depart_definition(self, node): + self.out.append('\n') + + def visit_definition_list(self, node): + self.out.append( '%\n\\begin{description}\n' ) + + def depart_definition_list(self, node): + self.out.append( '\\end{description}\n' ) + + def visit_definition_list_item(self, node): + pass + + def depart_definition_list_item(self, node): + pass + + def visit_description(self, node): + self.out.append(' ') + + def depart_description(self, node): + pass + + def visit_docinfo(self, node): + self.push_output_collector(self.docinfo) + + def depart_docinfo(self, node): + self.pop_output_collector() + # Some itmes (e.g. author) end up at other places + if self.docinfo: + # tabularx: automatic width of columns, no page breaks allowed. + self.requirements['tabularx'] = r'\usepackage{tabularx}' + self.fallbacks['_providelength'] = PreambleCmds.providelength + self.fallbacks['docinfo'] = PreambleCmds.docinfo + # + self.docinfo.insert(0, '\n% Docinfo\n' + '\\begin{center}\n' + '\\begin{tabularx}{\\DUdocinfowidth}{lX}\n') + self.docinfo.append('\\end{tabularx}\n' + '\\end{center}\n') + + def visit_docinfo_item(self, node, name): + if name == 'author': + self.pdfauthor.append(self.attval(node.astext())) + if self.use_latex_docinfo: + if name in ('author', 'organization', 'contact', 'address'): + # We attach these to the last author. If any of them precedes + # the first author, put them in a separate "author" group + # (in lack of better semantics). + if name == 'author' or not self.author_stack: + self.author_stack.append([]) + if name == 'address': # newlines are meaningful + self.insert_newline = True + text = self.encode(node.astext()) + self.insert_newline = False + else: + text = self.attval(node.astext()) + self.author_stack[-1].append(text) + raise nodes.SkipNode + elif name == 'date': + self.date.append(self.attval(node.astext())) + raise nodes.SkipNode + self.out.append('\\textbf{%s}: &\n\t' % self.language_label(name)) + if name == 'address': + self.insert_newline = 1 + self.out.append('{\\raggedright\n') + self.context.append(' } \\\\\n') + else: + self.context.append(' \\\\\n') + + def depart_docinfo_item(self, node): + self.out.append(self.context.pop()) + # for address we did set insert_newline + self.insert_newline = False + + def visit_doctest_block(self, node): + self.visit_literal_block(node) + + def depart_doctest_block(self, node): + self.depart_literal_block(node) + + def visit_document(self, node): + # titled document? + if (self.use_latex_docinfo or len(node) and + isinstance(node[0], nodes.title)): + self.title_labels += self.ids_to_labels(node, set_anchor=False) + + def depart_document(self, node): + # Complete header with information gained from walkabout + # * language setup + if (self.babel.otherlanguages or + self.babel.language not in ('', 'english')): + self.requirements['babel'] = self.babel() + # * conditional requirements (before style sheet) + self.requirements = self.requirements.sortedvalues() + # * coditional fallback definitions (after style sheet) + self.fallbacks = self.fallbacks.sortedvalues() + # * PDF properties + self.pdfsetup.append(PreambleCmds.linking % self.hyperref_options) + if self.pdfauthor: + authors = self.author_separator.join(self.pdfauthor) + self.pdfinfo.append(' pdfauthor={%s}' % authors) + if self.pdfinfo: + self.pdfsetup += [r'\hypersetup{'] + self.pdfinfo + ['}'] + # Complete body + # * document title (with "use_latex_docinfo" also + # 'author', 'organization', 'contact', 'address' and 'date') + if self.title or ( + self.use_latex_docinfo and (self.author_stack or self.date)): + # with the default template, titledata is written to the preamble + self.titledata.append('%%% Title Data') + # \title (empty \title prevents error with \maketitle) + if self.title: + self.title.insert(0, '\phantomsection%\n ') + title = [''.join(self.title)] + self.title_labels + if self.subtitle: + title += [r'\\ % subtitle', + r'\large{%s}' % ''.join(self.subtitle) + ] + self.subtitle_labels + self.titledata.append(r'\title{%s}' % '%\n '.join(title)) + # \author (empty \author prevents warning with \maketitle) + authors = ['\\\\\n'.join(author_entry) + for author_entry in self.author_stack] + self.titledata.append(r'\author{%s}' % + ' \\and\n'.join(authors)) + # \date (empty \date prevents defaulting to \today) + self.titledata.append(r'\date{%s}' % ', '.join(self.date)) + # \maketitle in the body formats title with LaTeX + self.body_pre_docinfo.append('\\maketitle\n') + + # * bibliography + # TODO insertion point of bibliography should be configurable. + if self._use_latex_citations and len(self._bibitems)>0: + if not self.bibtex: + widest_label = '' + for bi in self._bibitems: + if len(widest_label) self._max_enumeration_counters: + self._max_enumeration_counters = len(self._enumeration_counters) + self.out.append('\\newcounter{%s}\n' % counter_name) + else: + self.out.append('\\setcounter{%s}{0}\n' % counter_name) + + self.out.append('\\begin{list}{%s\\%s{%s}%s}\n' % + (enum_prefix,enum_type,counter_name,enum_suffix)) + self.out.append('{\n') + self.out.append('\\usecounter{%s}\n' % counter_name) + # set start after usecounter, because it initializes to zero. + if 'start' in node: + self.out.append('\\addtocounter{%s}{%d}\n' % + (counter_name,node['start']-1)) + ## set rightmargin equal to leftmargin + self.out.append('\\setlength{\\rightmargin}{\\leftmargin}\n') + self.out.append('}\n') + + def depart_enumerated_list(self, node): + self.out.append('\\end{list}\n') + self._enumeration_counters.pop() + + def visit_field(self, node): + # real output is done in siblings: _argument, _body, _name + pass + + def depart_field(self, node): + self.out.append('\n') + ##self.out.append('%[depart_field]\n') + + def visit_field_argument(self, node): + self.out.append('%[visit_field_argument]\n') + + def depart_field_argument(self, node): + self.out.append('%[depart_field_argument]\n') + + def visit_field_body(self, node): + pass + + def depart_field_body(self, node): + if self.out is self.docinfo: + self.out.append(r'\\') + + def visit_field_list(self, node): + if self.out is not self.docinfo: + self.fallbacks['fieldlist'] = PreambleCmds.fieldlist + self.out.append('%\n\\begin{DUfieldlist}\n') + + def depart_field_list(self, node): + if self.out is not self.docinfo: + self.out.append('\\end{DUfieldlist}\n') + + def visit_field_name(self, node): + if self.out is self.docinfo: + self.out.append('\\textbf{') + else: + # Commands with optional args inside an optional arg must be put + # in a group, e.g. ``\item[{\hyperref[label]{text}}]``. + self.out.append('\\item[{') + + def depart_field_name(self, node): + if self.out is self.docinfo: + self.out.append('}: &') + else: + self.out.append(':}]') + + def visit_figure(self, node): + self.requirements['float_settings'] = PreambleCmds.float_settings + # ! the 'align' attribute should set "outer alignment" ! + # For "inner alignment" use LaTeX default alignment (similar to HTML) + ## if ('align' not in node.attributes or + ## node.attributes['align'] == 'center'): + ## align = '\n\\centering' + ## align_end = '' + ## else: + ## # TODO non vertical space for other alignments. + ## align = '\\begin{flush%s}' % node.attributes['align'] + ## align_end = '\\end{flush%s}' % node.attributes['align'] + ## self.out.append( '\\begin{figure}%s\n' % align ) + ## self.context.append( '%s\\end{figure}\n' % align_end ) + self.out.append('\\begin{figure}') + if node.get('ids'): + self.out += ['\n'] + self.ids_to_labels(node) + + def depart_figure(self, node): + self.out.append('\\end{figure}\n') + + def visit_footer(self, node): + self.push_output_collector([]) + self.out.append(r'\newcommand{\DUfooter}{') + + def depart_footer(self, node): + self.out.append('}') + self.requirements['~footer'] = ''.join(self.out) + self.pop_output_collector() + + def visit_footnote(self, node): + try: + backref = node['backrefs'][0] + except IndexError: + backref = node['ids'][0] # no backref, use self-ref instead + if self.settings.figure_footnotes: + self.requirements['~fnt_floats'] = PreambleCmds.footnote_floats + self.out.append('\\begin{figure}[b]') + self.append_hypertargets(node) + if node.get('id') == node.get('name'): # explicite label + self.out += self.ids_to_labels(node) + elif self.docutils_footnotes: + self.fallbacks['footnotes'] = PreambleCmds.footnotes + num,text = node.astext().split(None,1) + if self.settings.footnote_references == 'brackets': + num = '[%s]' % num + self.out.append('%%\n\\DUfootnotetext{%s}{%s}{%s}{' % + (node['ids'][0], backref, self.encode(num))) + if node['ids'] == node['names']: + self.out += self.ids_to_labels(node) + # mask newline to prevent spurious whitespace: + self.out.append('%') + ## else: # TODO: "real" LaTeX \footnote{}s + + def depart_footnote(self, node): + if self.figure_footnotes: + self.out.append('\\end{figure}\n') + else: + self.out.append('}\n') + + def visit_footnote_reference(self, node): + href = '' + if 'refid' in node: + href = node['refid'] + elif 'refname' in node: + href = self.document.nameids[node['refname']] + # if not self.docutils_footnotes: + # TODO: insert footnote content at (or near) this place + # print "footnote-ref to", node['refid'] + # footnotes = (self.document.footnotes + + # self.document.autofootnotes + + # self.document.symbol_footnotes) + # for footnote in footnotes: + # # print footnote['ids'] + # if node.get('refid', '') in footnote['ids']: + # print 'matches', footnote['ids'] + format = self.settings.footnote_references + if format == 'brackets': + self.append_hypertargets(node) + self.out.append('\\hyperlink{%s}{[' % href) + self.context.append(']}') + else: + self.fallbacks['footnotes'] = PreambleCmds.footnotes + self.out.append(r'\DUfootnotemark{%s}{%s}{' % + (node['ids'][0], href)) + self.context.append('}') + + def depart_footnote_reference(self, node): + self.out.append(self.context.pop()) + + # footnote/citation label + def label_delim(self, node, bracket, superscript): + if isinstance(node.parent, nodes.footnote): + if not self.figure_footnotes: + raise nodes.SkipNode + if self.settings.footnote_references == 'brackets': + self.out.append(bracket) + else: + self.out.append(superscript) + else: + assert isinstance(node.parent, nodes.citation) + if not self._use_latex_citations: + self.out.append(bracket) + + def visit_label(self, node): + """footnote or citation label: in brackets or as superscript""" + self.label_delim(node, '[', '\\textsuperscript{') + + def depart_label(self, node): + self.label_delim(node, ']', '}') + + # elements generated by the framework e.g. section numbers. + def visit_generated(self, node): + pass + + def depart_generated(self, node): + pass + + def visit_header(self, node): + self.push_output_collector([]) + self.out.append(r'\newcommand{\DUheader}{') + + def depart_header(self, node): + self.out.append('}') + self.requirements['~header'] = ''.join(self.out) + self.pop_output_collector() + + def to_latex_length(self, length_str, pxunit=None): + """Convert `length_str` with rst lenght to LaTeX length + """ + if pxunit is not None: + sys.stderr.write('deprecation warning: LaTeXTranslator.to_latex_length()' + ' option `pxunit` will be removed.') + match = re.match('(\d*\.?\d*)\s*(\S*)', length_str) + if not match: + return length_str + value, unit = match.groups()[:2] + # no unit or "DTP" points (called 'bp' in TeX): + if unit in ('', 'pt'): + length_str = '%sbp' % value + # percentage: relate to current line width + elif unit == '%': + length_str = '%.3f\\linewidth' % (float(value)/100.0) + elif self.is_xetex and unit == 'px': + # XeTeX does not know the length unit px. + # Use \pdfpxdimen, the macro to set the value of 1 px in pdftex. + # This way, configuring works the same for pdftex and xetex. + self.fallbacks['_providelength'] = PreambleCmds.providelength + self.fallbacks['px'] = '\n\\DUprovidelength{\\pdfpxdimen}{1bp}\n' + length_str = r'%s\pdfpxdimen' % value + return length_str + + def visit_image(self, node): + self.requirements['graphicx'] = self.graphicx_package + attrs = node.attributes + # Convert image URI to a local file path + imagepath = urllib.url2pathname(attrs['uri']).replace('\\', '/') + # alignment defaults: + if not 'align' in attrs: + # Set default align of image in a figure to 'center' + if isinstance(node.parent, nodes.figure): + attrs['align'] = 'center' + # query 'align-*' class argument + for cls in node['classes']: + if cls.startswith('align-'): + attrs['align'] = cls.split('-')[1] + # pre- and postfix (prefix inserted in reverse order) + pre = [] + post = [] + include_graphics_options = [] + display_style = ('block-', 'inline-')[self.is_inline(node)] + align_codes = { + # inline images: by default latex aligns the bottom. + 'bottom': ('', ''), + 'middle': (r'\raisebox{-0.5\height}{', '}'), + 'top': (r'\raisebox{-\height}{', '}'), + # block level images: + 'center': (r'\noindent\makebox[\textwidth][c]{', '}'), + 'left': (r'\noindent{', r'\hfill}'), + 'right': (r'\noindent{\hfill', '}'),} + if 'align' in attrs: + try: + align_code = align_codes[attrs['align']] + pre.append(align_code[0]) + post.append(align_code[1]) + except KeyError: + pass # TODO: warn? + if 'height' in attrs: + include_graphics_options.append('height=%s' % + self.to_latex_length(attrs['height'])) + if 'scale' in attrs: + include_graphics_options.append('scale=%f' % + (attrs['scale'] / 100.0)) + if 'width' in attrs: + include_graphics_options.append('width=%s' % + self.to_latex_length(attrs['width'])) + if not self.is_inline(node): + pre.append('\n') + post.append('\n') + pre.reverse() + self.out.extend(pre) + options = '' + if include_graphics_options: + options = '[%s]' % (','.join(include_graphics_options)) + self.out.append('\\includegraphics%s{%s}' % (options, imagepath)) + self.out.extend(post) + + def depart_image(self, node): + if node.get('ids'): + self.out += self.ids_to_labels(node) + ['\n'] + + def visit_inline(self, node): # , i.e. custom roles + # Make a copy to keep ``node['classes']`` True if a + # language argument is popped (used in conditional calls of + # depart_inline()): + classes = node['classes'][:] + self.context.append('}' * len(classes)) + # handle language specification: + language_tags = [cls for cls in classes + if cls.startswith('language-')] + if language_tags: + language = self.babel.language_name(language_tags[0][9:]) + if language: + self.babel.otherlanguages[language] = True + self.out.append(r'\otherlanguage{%s}{' % language) + classes.pop(classes.index(language_tags[0])) + if not classes: + return + # mark up for styling with custom macros + if 'align-center' in classes: + self.fallbacks['align-center'] = PreambleCmds.align_center + self.fallbacks['inline'] = PreambleCmds.inline + self.out += [r'\DUrole{%s}{' % cls for cls in classes] + + def depart_inline(self, node): + self.out.append(self.context.pop()) + + def visit_interpreted(self, node): + # @@@ Incomplete, pending a proper implementation on the + # Parser/Reader end. + self.visit_literal(node) + + def depart_interpreted(self, node): + self.depart_literal(node) + + def visit_legend(self, node): + self.fallbacks['legend'] = PreambleCmds.legend + self.out.append('\\begin{DUlegend}') + + def depart_legend(self, node): + self.out.append('\\end{DUlegend}\n') + + def visit_line(self, node): + self.out.append('\item[] ') + + def depart_line(self, node): + self.out.append('\n') + + def visit_line_block(self, node): + self.fallbacks['_providelength'] = PreambleCmds.providelength + self.fallbacks['lineblock'] = PreambleCmds.lineblock + if isinstance(node.parent, nodes.line_block): + self.out.append('\\item[]\n' + '\\begin{DUlineblock}{\\DUlineblockindent}\n') + else: + self.out.append('\n\\begin{DUlineblock}{0em}\n') + if node['classes']: + self.visit_inline(node) + self.out.append('\n') + + def depart_line_block(self, node): + if node['classes']: + self.depart_inline(node) + self.out.append('\n') + self.out.append('\\end{DUlineblock}\n') + + def visit_list_item(self, node): + self.out.append('\n\\item ') + + def depart_list_item(self, node): + pass + + def visit_literal(self, node): + self.literal = True + self.out.append('\\texttt{') + if node['classes']: + self.visit_inline(node) + + def depart_literal(self, node): + self.literal = False + if node['classes']: + self.depart_inline(node) + self.out.append('}') + + # Literal blocks are used for '::'-prefixed literal-indented + # blocks of text, where the inline markup is not recognized, + # but are also the product of the "parsed-literal" directive, + # where the markup is respected. + # + # In both cases, we want to use a typewriter/monospaced typeface. + # For "real" literal-blocks, we can use \verbatim, while for all + # the others we must use \mbox or \alltt. + # + # We can distinguish between the two kinds by the number of + # siblings that compose this node: if it is composed by a + # single element, it's either + # * a real one, + # * a parsed-literal that does not contain any markup, or + # * a parsed-literal containing just one markup construct. + def is_plaintext(self, node): + """Check whether a node can be typeset verbatim""" + return (len(node) == 1) and isinstance(node[0], nodes.Text) + + def visit_literal_block(self, node): + """Render a literal block.""" + # environments and packages to typeset literal blocks + packages = {'listing': r'\usepackage{moreverb}', + 'lstlisting': r'\usepackage{listings}', + 'Verbatim': r'\usepackage{fancyvrb}', + # 'verbatim': '', + 'verbatimtab': r'\usepackage{moreverb}'} + + if not self.active_table.is_open(): + # no quote inside tables, to avoid vertical space between + # table border and literal block. + # BUG: fails if normal text preceeds the literal block. + self.out.append('%\n\\begin{quote}') + self.context.append('\n\\end{quote}\n') + else: + self.out.append('\n') + self.context.append('\n') + if self.literal_block_env != '' and self.is_plaintext(node): + self.requirements['literal_block'] = packages.get( + self.literal_block_env, '') + self.verbatim = True + self.out.append('\\begin{%s}%s\n' % (self.literal_block_env, + self.literal_block_options)) + else: + self.literal = True + self.insert_newline = True + self.insert_non_breaking_blanks = True + self.out.append('{\\ttfamily \\raggedright \\noindent\n') + + def depart_literal_block(self, node): + if self.verbatim: + self.out.append('\n\\end{%s}\n' % self.literal_block_env) + self.verbatim = False + else: + self.out.append('\n}') + self.insert_non_breaking_blanks = False + self.insert_newline = False + self.literal = False + self.out.append(self.context.pop()) + + ## def visit_meta(self, node): + ## self.out.append('[visit_meta]\n') + # TODO: set keywords for pdf? + # But: + # The reStructuredText "meta" directive creates a "pending" node, + # which contains knowledge that the embedded "meta" node can only + # be handled by HTML-compatible writers. The "pending" node is + # resolved by the docutils.transforms.components.Filter transform, + # which checks that the calling writer supports HTML; if it doesn't, + # the "pending" node (and enclosed "meta" node) is removed from the + # document. + # --- docutils/docs/peps/pep-0258.html#transformer + + ## def depart_meta(self, node): + ## self.out.append('[depart_meta]\n') + + def visit_math(self, node, math_env='$'): + """math role""" + if node['classes']: + self.visit_inline(node) + self.requirements['amsmath'] = r'\usepackage{amsmath}' + math_code = node.astext().translate(unichar2tex.uni2tex_table) + if node.get('ids'): + math_code = '\n'.join([math_code] + self.ids_to_labels(node)) + if math_env == '$': + wrapper = u'$%s$' + else: + wrapper = u'\n'.join(['%%', + r'\begin{%s}' % math_env, + '%s', + r'\end{%s}' % math_env]) + # print repr(wrapper), repr(math_code) + self.out.append(wrapper % math_code) + if node['classes']: + self.depart_inline(node) + # Content already processed: + raise nodes.SkipNode + + def depart_math(self, node): + pass # never reached + + def visit_math_block(self, node): + math_env = pick_math_environment(node.astext()) + self.visit_math(node, math_env=math_env) + + def depart_math_block(self, node): + pass # never reached + + def visit_option(self, node): + if self.context[-1]: + # this is not the first option + self.out.append(', ') + + def depart_option(self, node): + # flag that the first option is done. + self.context[-1] += 1 + + def visit_option_argument(self, node): + """Append the delimiter betweeen an option and its argument to body.""" + self.out.append(node.get('delimiter', ' ')) + + def depart_option_argument(self, node): + pass + + def visit_option_group(self, node): + self.out.append('\n\\item[') + # flag for first option + self.context.append(0) + + def depart_option_group(self, node): + self.context.pop() # the flag + self.out.append('] ') + + def visit_option_list(self, node): + self.fallbacks['_providelength'] = PreambleCmds.providelength + self.fallbacks['optionlist'] = PreambleCmds.optionlist + self.out.append('%\n\\begin{DUoptionlist}\n') + + def depart_option_list(self, node): + self.out.append('\n\\end{DUoptionlist}\n') + + def visit_option_list_item(self, node): + pass + + def depart_option_list_item(self, node): + pass + + def visit_option_string(self, node): + ##self.out.append(self.starttag(node, 'span', '', CLASS='option')) + pass + + def depart_option_string(self, node): + ##self.out.append('') + pass + + def visit_organization(self, node): + self.visit_docinfo_item(node, 'organization') + + def depart_organization(self, node): + self.depart_docinfo_item(node) + + def visit_paragraph(self, node): + # insert blank line, if the paragraph is not first in a list item + # nor follows a non-paragraph node in a compound + index = node.parent.index(node) + if (index == 0 and (isinstance(node.parent, nodes.list_item) or + isinstance(node.parent, nodes.description))): + pass + elif (index > 0 and isinstance(node.parent, nodes.compound) and + not isinstance(node.parent[index - 1], nodes.paragraph) and + not isinstance(node.parent[index - 1], nodes.compound)): + pass + else: + self.out.append('\n') + if node.get('ids'): + self.out += self.ids_to_labels(node) + ['\n'] + if node['classes']: + self.visit_inline(node) + + def depart_paragraph(self, node): + if node['classes']: + self.depart_inline(node) + self.out.append('\n') + + def visit_problematic(self, node): + self.requirements['color'] = PreambleCmds.color + self.out.append('%\n') + self.append_hypertargets(node) + self.out.append(r'\hyperlink{%s}{\textbf{\color{red}' % node['refid']) + + def depart_problematic(self, node): + self.out.append('}}') + + def visit_raw(self, node): + if not 'latex' in node.get('format', '').split(): + raise nodes.SkipNode + if not self.is_inline(node): + self.out.append('\n') + if node['classes']: + self.visit_inline(node) + # append "as-is" skipping any LaTeX-encoding + self.verbatim = True + + def depart_raw(self, node): + self.verbatim = False + if node['classes']: + self.depart_inline(node) + if not self.is_inline(node): + self.out.append('\n') + + def has_unbalanced_braces(self, string): + """Test whether there are unmatched '{' or '}' characters.""" + level = 0 + for ch in string: + if ch == '{': + level += 1 + if ch == '}': + level -= 1 + if level < 0: + return True + return level != 0 + + def visit_reference(self, node): + # We need to escape #, \, and % if we use the URL in a command. + special_chars = {ord('#'): ur'\#', + ord('%'): ur'\%', + ord('\\'): ur'\\', + } + # external reference (URL) + if 'refuri' in node: + href = unicode(node['refuri']).translate(special_chars) + # problematic chars double caret and unbalanced braces: + if href.find('^^') != -1 or self.has_unbalanced_braces(href): + self.error( + 'External link "%s" not supported by LaTeX.\n' + ' (Must not contain "^^" or unbalanced braces.)' % href) + if node['refuri'] == node.astext(): + self.out.append(r'\url{%s}' % href) + raise nodes.SkipNode + self.out.append(r'\href{%s}{' % href) + return + # internal reference + if 'refid' in node: + href = node['refid'] + elif 'refname' in node: + href = self.document.nameids[node['refname']] + else: + raise AssertionError('Unknown reference.') + if not self.is_inline(node): + self.out.append('\n') + self.out.append('\\hyperref[%s]{' % href) + if self._reference_label: + self.out.append('\\%s{%s}}' % + (self._reference_label, href.replace('#', ''))) + raise nodes.SkipNode + + def depart_reference(self, node): + self.out.append('}') + if not self.is_inline(node): + self.out.append('\n') + + def visit_revision(self, node): + self.visit_docinfo_item(node, 'revision') + + def depart_revision(self, node): + self.depart_docinfo_item(node) + + def visit_section(self, node): + self.section_level += 1 + # Initialize counter for potential subsections: + self._section_number.append(0) + # Counter for this section's level (initialized by parent section): + self._section_number[self.section_level - 1] += 1 + + def depart_section(self, node): + # Remove counter for potential subsections: + self._section_number.pop() + self.section_level -= 1 + + def visit_sidebar(self, node): + self.requirements['color'] = PreambleCmds.color + self.fallbacks['sidebar'] = PreambleCmds.sidebar + self.out.append('\n\\DUsidebar{\n') + + def depart_sidebar(self, node): + self.out.append('}\n') + + attribution_formats = {'dash': (u'—', ''), # EM DASH + 'parentheses': ('(', ')'), + 'parens': ('(', ')'), + 'none': ('', '')} + + def visit_attribution(self, node): + prefix, suffix = self.attribution_formats[self.settings.attribution] + self.out.append('\\nopagebreak\n\n\\raggedleft ') + self.out.append(prefix) + self.context.append(suffix) + + def depart_attribution(self, node): + self.out.append(self.context.pop() + '\n') + + def visit_status(self, node): + self.visit_docinfo_item(node, 'status') + + def depart_status(self, node): + self.depart_docinfo_item(node) + + def visit_strong(self, node): + self.out.append('\\textbf{') + if node['classes']: + self.visit_inline(node) + + def depart_strong(self, node): + if node['classes']: + self.depart_inline(node) + self.out.append('}') + + def visit_substitution_definition(self, node): + raise nodes.SkipNode + + def visit_substitution_reference(self, node): + self.unimplemented_visit(node) + + def visit_subtitle(self, node): + if isinstance(node.parent, nodes.document): + self.push_output_collector(self.subtitle) + self.subtitle_labels += self.ids_to_labels(node, set_anchor=False) + # section subtitle: "starred" (no number, not in ToC) + elif isinstance(node.parent, nodes.section): + self.out.append(r'\%s*{' % + self.d_class.section(self.section_level + 1)) + else: + self.fallbacks['subtitle'] = PreambleCmds.subtitle + self.out.append('\n\\DUsubtitle[%s]{' % node.parent.tagname) + + def depart_subtitle(self, node): + if isinstance(node.parent, nodes.document): + self.pop_output_collector() + else: + self.out.append('}\n') + + def visit_system_message(self, node): + self.requirements['color'] = PreambleCmds.color + self.fallbacks['title'] = PreambleCmds.title + node['classes'] = ['system-message'] + self.visit_admonition(node) + self.out.append('\\DUtitle[system-message]{system-message}\n') + self.append_hypertargets(node) + try: + line = ', line~%s' % node['line'] + except KeyError: + line = '' + self.out.append('\n\n{\color{red}%s/%s} in \\texttt{%s}%s\n' % + (node['type'], node['level'], + self.encode(node['source']), line)) + if len(node['backrefs']) == 1: + self.out.append('\n\\hyperlink{%s}{' % node['backrefs'][0]) + self.context.append('}') + else: + backrefs = ['\\hyperlink{%s}{%d}' % (href, i+1) + for (i, href) in enumerate(node['backrefs'])] + self.context.append('backrefs: ' + ' '.join(backrefs)) + + def depart_system_message(self, node): + self.out.append(self.context.pop()) + self.depart_admonition() + + def visit_table(self, node): + self.requirements['table'] = PreambleCmds.table + if self.active_table.is_open(): + self.table_stack.append(self.active_table) + # nesting longtable does not work (e.g. 2007-04-18) + self.active_table = Table(self,'tabular',self.settings.table_style) + # A longtable moves before \paragraph and \subparagraph + # section titles if it immediately follows them: + if (self.active_table._latex_type == 'longtable' and + isinstance(node.parent, nodes.section) and + node.parent.index(node) == 1 and + self.d_class.section(self.section_level).find('paragraph') != -1): + self.out.append('\\leavevmode') + self.active_table.open() + for cls in node['classes']: + self.active_table.set_table_style(cls) + if self.active_table._table_style == 'booktabs': + self.requirements['booktabs'] = r'\usepackage{booktabs}' + self.push_output_collector([]) + + def depart_table(self, node): + # wrap content in the right environment: + content = self.out + self.pop_output_collector() + self.out.append('\n' + self.active_table.get_opening()) + self.out += content + self.out.append(self.active_table.get_closing() + '\n') + self.active_table.close() + if len(self.table_stack)>0: + self.active_table = self.table_stack.pop() + else: + self.active_table.set_table_style(self.settings.table_style) + # Insert hyperlabel after (long)table, as + # other places (beginning, caption) result in LaTeX errors. + if node.get('ids'): + self.out += self.ids_to_labels(node, set_anchor=False) + ['\n'] + + def visit_target(self, node): + # Skip indirect targets: + if ('refuri' in node # external hyperlink + or 'refid' in node # resolved internal link + or 'refname' in node): # unresolved internal link + ## self.out.append('%% %s\n' % node) # for debugging + return + self.out.append('%\n') + # do we need an anchor (\phantomsection)? + set_anchor = not(isinstance(node.parent, nodes.caption) or + isinstance(node.parent, nodes.title)) + # TODO: where else can/must we omit the \phantomsection? + self.out += self.ids_to_labels(node, set_anchor) + + def depart_target(self, node): + pass + + def visit_tbody(self, node): + # BUG write preamble if not yet done (colspecs not []) + # for tables without heads. + if not self.active_table.get('preamble written'): + self.visit_thead(None) + self.depart_thead(None) + + def depart_tbody(self, node): + pass + + def visit_term(self, node): + """definition list term""" + # Commands with optional args inside an optional arg must be put + # in a group, e.g. ``\item[{\hyperref[label]{text}}]``. + self.out.append('\\item[{') + + def depart_term(self, node): + # \leavevmode results in a line break if the + # term is followed by an item list. + self.out.append('}] \leavevmode ') + + def visit_tgroup(self, node): + #self.out.append(self.starttag(node, 'colgroup')) + #self.context.append('\n') + pass + + def depart_tgroup(self, node): + pass + + _thead_depth = 0 + def thead_depth (self): + return self._thead_depth + + def visit_thead(self, node): + self._thead_depth += 1 + if 1 == self.thead_depth(): + self.out.append('{%s}\n' % self.active_table.get_colspecs()) + self.active_table.set('preamble written',1) + self.out.append(self.active_table.get_caption()) + self.out.extend(self.active_table.visit_thead()) + + def depart_thead(self, node): + if node is not None: + self.out.extend(self.active_table.depart_thead()) + if self.active_table.need_recurse(): + node.walkabout(self) + self._thead_depth -= 1 + + def visit_title(self, node): + """Append section and other titles.""" + # Document title + if node.parent.tagname == 'document': + self.push_output_collector(self.title) + self.context.append('') + self.pdfinfo.append(' pdftitle={%s},' % + self.encode(node.astext())) + # Topic titles (topic, admonition, sidebar) + elif (isinstance(node.parent, nodes.topic) or + isinstance(node.parent, nodes.admonition) or + isinstance(node.parent, nodes.sidebar)): + self.fallbacks['title'] = PreambleCmds.title + classes = ','.join(node.parent['classes']) + if not classes: + classes = node.tagname + self.out.append('\\DUtitle[%s]{' % classes) + self.context.append('}\n') + # Table caption + elif isinstance(node.parent, nodes.table): + self.push_output_collector(self.active_table.caption) + self.context.append('') + # Section title + else: + self.requirements['secnumdepth'] = PreambleCmds.secnumdepth + section_name = self.d_class.section(self.section_level) + self.out.append('\n\n') + # System messages heading in red: + if ('system-messages' in node.parent['classes']): + self.requirements['color'] = PreambleCmds.color + section_title = self.encode(node.astext()) + self.out.append(r'\%s[%s]{\color{red}' % ( + section_name,section_title)) + else: + self.out.append(r'\%s{' % section_name) + if self.section_level > len(self.d_class.sections): + # section level not supported by LaTeX + self.fallbacks['title'] = PreambleCmds.title + # self.out.append('\\phantomsection%\n ') + # label and ToC entry: + bookmark = [''] + # add sections with unsupported level to toc and pdfbookmarks? + ## if self.section_level > len(self.d_class.sections): + ## section_title = self.encode(node.astext()) + ## bookmark.append(r'\addcontentsline{toc}{%s}{%s}' % + ## (section_name, section_title)) + bookmark += self.ids_to_labels(node.parent, set_anchor=False) + self.context.append('%\n '.join(bookmark) + '%\n}\n') + + # MAYBE postfix paragraph and subparagraph with \leavemode to + # ensure floats stay in the section and text starts on a new line. + + def depart_title(self, node): + self.out.append(self.context.pop()) + if (isinstance(node.parent, nodes.table) or + node.parent.tagname == 'document'): + self.pop_output_collector() + + def minitoc(self, node, title, depth): + """Generate a local table of contents with LaTeX package minitoc""" + section_name = self.d_class.section(self.section_level) + # name-prefix for current section level + minitoc_names = {'part': 'part', 'chapter': 'mini'} + if 'chapter' not in self.d_class.sections: + minitoc_names['section'] = 'sect' + try: + minitoc_name = minitoc_names[section_name] + except KeyError: # minitoc only supports part- and toplevel + self.warn('Skipping local ToC at %s level.\n' % section_name + + ' Feature not supported with option "use-latex-toc"', + base_node=node) + return + # Requirements/Setup + self.requirements['minitoc'] = PreambleCmds.minitoc + self.requirements['minitoc-'+minitoc_name] = (r'\do%stoc' % + minitoc_name) + # depth: (Docutils defaults to unlimited depth) + maxdepth = len(self.d_class.sections) + self.requirements['minitoc-%s-depth' % minitoc_name] = ( + r'\mtcsetdepth{%stoc}{%d}' % (minitoc_name, maxdepth)) + # Process 'depth' argument (!Docutils stores a relative depth while + # minitoc expects an absolute depth!): + offset = {'sect': 1, 'mini': 0, 'part': 0} + if 'chapter' in self.d_class.sections: + offset['part'] = -1 + if depth: + self.out.append('\\setcounter{%stocdepth}{%d}' % + (minitoc_name, depth + offset[minitoc_name])) + # title: + self.out.append('\\mtcsettitle{%stoc}{%s}\n' % (minitoc_name, title)) + # the toc-generating command: + self.out.append('\\%stoc\n' % minitoc_name) + + def visit_topic(self, node): + # Topic nodes can be generic topic, abstract, dedication, or ToC. + # table of contents: + if 'contents' in node['classes']: + self.out.append('\n') + self.out += self.ids_to_labels(node) + # add contents to PDF bookmarks sidebar + if isinstance(node.next_node(), nodes.title): + self.out.append('\n\\pdfbookmark[%d]{%s}{%s}\n' % + (self.section_level+1, + node.next_node().astext(), + node.get('ids', ['contents'])[0] + )) + if self.use_latex_toc: + title = '' + if isinstance(node.next_node(), nodes.title): + title = self.encode(node.pop(0).astext()) + depth = node.get('depth', 0) + if 'local' in node['classes']: + self.minitoc(node, title, depth) + self.context.append('') + return + if depth: + self.out.append('\\setcounter{tocdepth}{%d}\n' % depth) + if title != 'Contents': + self.out.append('\\renewcommand{\\contentsname}{%s}\n' % + title) + self.out.append('\\tableofcontents\n\n') + self.has_latex_toc = True + else: # Docutils generated contents list + # set flag for visit_bullet_list() and visit_title() + self.is_toc_list = True + self.context.append('') + elif ('abstract' in node['classes'] and + self.settings.use_latex_abstract): + self.push_output_collector(self.abstract) + self.out.append('\\begin{abstract}') + self.context.append('\\end{abstract}\n') + if isinstance(node.next_node(), nodes.title): + node.pop(0) # LaTeX provides its own title + else: + self.fallbacks['topic'] = PreambleCmds.topic + # special topics: + if 'abstract' in node['classes']: + self.fallbacks['abstract'] = PreambleCmds.abstract + self.push_output_collector(self.abstract) + if 'dedication' in node['classes']: + self.fallbacks['dedication'] = PreambleCmds.dedication + self.push_output_collector(self.dedication) + self.out.append('\n\\DUtopic[%s]{\n' % ','.join(node['classes'])) + self.context.append('}\n') + + def depart_topic(self, node): + self.out.append(self.context.pop()) + self.is_toc_list = False + if ('abstract' in node['classes'] or + 'dedication' in node['classes']): + self.pop_output_collector() + + def visit_rubric(self, node): + self.fallbacks['rubric'] = PreambleCmds.rubric + self.out.append('\n\\DUrubric{') + self.context.append('}\n') + + def depart_rubric(self, node): + self.out.append(self.context.pop()) + + def visit_transition(self, node): + self.fallbacks['transition'] = PreambleCmds.transition + self.out.append('\n\n') + self.out.append('%' + '_' * 75 + '\n') + self.out.append(r'\DUtransition') + self.out.append('\n\n') + + def depart_transition(self, node): + pass + + def visit_version(self, node): + self.visit_docinfo_item(node, 'version') + + def depart_version(self, node): + self.depart_docinfo_item(node) + + def unimplemented_visit(self, node): + raise NotImplementedError('visiting unimplemented node type: %s' % + node.__class__.__name__) + +# def unknown_visit(self, node): +# def default_visit(self, node): + +# vim: set ts=4 et ai : diff --git a/docutils/writers/latex2e/default.tex b/docutils/writers/latex2e/default.tex new file mode 100644 index 000000000..69e99fd7b --- /dev/null +++ b/docutils/writers/latex2e/default.tex @@ -0,0 +1,15 @@ +$head_prefix% generated by Docutils +\usepackage{fixltx2e} % LaTeX patches, \textsubscript +\usepackage{cmap} % fix search and cut-and-paste in Acrobat +$requirements +%%% Custom LaTeX preamble +$latex_preamble +%%% User specified packages and stylesheets +$stylesheet +%%% Fallback definitions for Docutils-specific commands +$fallbacks$pdfsetup +$titledata +%%% Body +\begin{document} +$body_pre_docinfo$docinfo$dedication$abstract$body +\end{document} diff --git a/docutils/writers/latex2e/docutils-05-compat.sty b/docutils/writers/latex2e/docutils-05-compat.sty new file mode 100644 index 000000000..cc6b6a8aa --- /dev/null +++ b/docutils/writers/latex2e/docutils-05-compat.sty @@ -0,0 +1,738 @@ +% ================================================================== +% Changes to the Docutils latex2e writer since version 0.5 +% ================================================================== +% +% A backwards compatibility style sheet +% ************************************* +% +% :Author: Guenter Milde +% :Contact: milde@users.sourceforge.net +% :Revision: $Revision: 6156 $ +% :Date: $Date: 2009-02-24 $ +% :Copyright: © 2009 Günter Milde, +% :License: Released under the terms of the `2-Clause BSD license`_, in short: +% +% Copying and distribution of this file, with or without modification, +% are permitted in any medium without royalty provided the copyright +% notice and this notice are preserved. +% This file is offered as-is, without any warranty. +% +% :Abstract: This file documents changes and provides a style for best +% possible compatibility to the behaviour of the `latex2e` +% writer of Doctutils release 0.5. +% +% .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause +% +% :: + +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{docutils-05-compat} +[2009/03/26 v0.1 compatibility with rst2latex from Docutils 0.5] + +% .. contents:: +% :depth: 3 +% +% Usage +% ===== +% +% * To get an (almost) identic look for your old documents, +% place ``docutils-05-compat.sty`` in the TEXINPUT path (e.g. +% the current work directory) and pass the +% ``--stylesheet=docutils-05-compat`` option to ``rst2latex.py``. +% +% * To use your custom stylesheets without change, add them to the +% compatibility style, e.g. +% ``--stylesheet="docutils-05-compat,mystyle.tex``. +% +% .. tip:: As the changes include bug fixes that are partly reverted by this +% style, it is recommended to adapt the stylesheets to the new version or +% copy just the relevant parts of this style into them. +% +% Changes since 0.5 +% ================= +% +% Bugfixes +% -------- +% +% * Newlines around comments, targets and references prevent run-together +% paragraphs. +% +% + An image directive with hyperlink reference or target did not start a +% new paragraph (e.g. the first two image examples in +% standalone_rst_latex.tex). +% +% + Paragraphs were not separated if there was a (hyper) target definition +% inbetween. +% +% + Paragraphs did run together, if separated by a comment-paragraph in the +% rst source. +% +% * Fixed missing and spurious internal links/targets. +% Internal links now take you to the correct place. +% +% * Verbose and linked system messages. +% +% * `Figure and image alignment`_ now conforms to the rst definition. +% +% * Put `header and footer directive`__ content in \DUheader respective +% \DUfooter macros (ignored by the default style/template). +% +% (They were put inside hard-coded markup at the top/bottom of the document +% without an option to get them on every page.) +% +% __ ../ref/rst/directives.html#document-header-footer +% +% * Render doctest blocks as literal blocks (fixes bug [1586058] doctest block +% nested in admonition). I.e. +% +% + indent doctest blocks by nesting in a quote environment. This is also +% the rendering by the HTML writer (html4css2.css). +% + apply the ``--literal-block-env`` setting also to doctest blocks. +% +% .. warning:: +% (``--literal-block-env=verbatim`` and +% ``--literal-block-env=lstlistings`` fail with literal or doctest +% blocks nested in an admonition. +% +% * Two-way hyperlinked footnotes and support for symbol footnotes and +% ``--footnote-references=brackets`` with ``--use-latex-footnotes``. +% +% * The packages `fixltx2e` (providing LaTeX patches and the \textsubscript +% command) and `cmap` (including character maps in the generated PDF for +% better search and copy-and-paste operations) are now always loaded +% (configurable with custom templates_). +% +% Backwards compatibility: +% "Bug for bug compatibility" is not provided. +% +% +% New configuration setting defaults +% ---------------------------------- +% +% - font-encoding: "T1" (formerly implicitely set by 'ae'). +% - use-latex-toc: true (ToC with page numbers). +% - use-latex-footnotes: true (no mixup with figures). +% +% Backwards compatibility: +% Reset to the former defaults with: +% +% | font-encoding: '' +% | use-latex-toc: False +% | use-latex-footnotes: False +% +% (in the config file) or the command line options: +% +% ``--figure-footnotes --use-docutils-toc --font-encoding=''`` +% +% +% Cleaner LaTeX source +% -------------------- +% +% New features: +% * Remove redundant "double protection" from the encoding of the "special +% printing characters" and square brackets, e.g. ``\%`` instead of +% ``{\%}``. +% * Remove some spurious whitespace, e.g. ``\item [what:] -> \item[what:]``. +% * Use conventional style for "named" macros, e.g. ``\dots{}`` instead of +% ``{\dots}`` +% +% Backwards compatibility: +% Changes do not affect the output. +% +% +% LaTeX style sheets +% ------------------ +% +% New Feature: +% LaTeX packages can be used as ``--stylesheet`` argument without +% restriction. +% +% Implementation: +% Use ``\usepackage`` if style sheet ends with ``.sty`` or has no +% extension and ``\input`` else. +% +% Rationale: +% while ``\input`` works with extension as well as without extension, +% ``\usepackage`` expects the package name without extension. (The latex2e +% writer will strip a ``.sty`` extension.) +% +% +% Backwards compatibility: +% Up to Docutils 0.5, if no filename extension is given in the +% ``stylesheet`` argument, ``.tex`` is assumed (by latex). +% +% Since Docutils 0.6, a stylesheet without filename extension is assumed to +% be a LaTeX package (``*.sty``) and referenced with the ``\usepackage`` +% command. +% +% .. important:: +% Always specify the extension if you want the style sheet to be +% ``\input`` by LaTeX. +% +% +% Templates +% --------- +% +% New Feature: +% Advanced configuration via custom templates. +% +% Implementation: +% A ``--template`` option and config setting allows specification of a +% template file. +% +% See the `LaTeX writer documentation`__ for details. +% +% __ latex.html#templates +% +% +% Custom roles +% ------------ +% +% New Feature: failsave implementation +% As with classes to HTML objects, class arguments are silently ignored if +% there is no styling rule for this class in a custom style sheet. +% +% New Feature: custom roles based on standard roles +% As class support needs to be handled by the LaTeX writer, this feature was +% not present "automatically" (as in HTML). Modified visit/depart_*() +% methods for the standard roles now call visit/depart_inline() if there are +% class arguments to the node. +% +% Backwards compatibility: +% The implementation is fully backwards compatible. (SVN versions 5742 to +% 5861 contained an implementation that did not work with commands expecting +% an argument.) +% +% Length units +% ------------ +% +% New Features: +% 1. Add default unit if none given. +% A poll on docutils-users favoured ``bp`` (Big Point: 1 bp = 1/72 in). +% +% 2. Do not change ``px`` to ``pt``. +% +% 3. Lengths specified in the document with unit "pt" will be written with +% unit "bp" to the LaTeX source. +% +% Rationale: +% 1. prevent LaTeX error "missing unit". +% +% 2. ``px`` is a valid unit in pdftex since version 1.3.0 released on +% 2005-02-04: +% +% 1px defaults to 1bp (or 72dpi), but can be changed with the +% ``\pdfpxdimen`` primitive.:: + + \pdfpxdimen=1in % 1 dpi + \divide\pdfpxdimen by 96 % 96 dpi + +% -- http://www.tug.org/applications/pdftex/NEWS +% +% Modern TeX distributions use pdftex also for dvi generation (i.e. +% ``latex`` actually calls ``pdftex`` with some options). +% +% 3. In Docutils (as well as CSS) the unit symbol "pt" denotes the +% `Postscript point` or `DTP point` while LaTeX uses "pt" for the `LaTeX +% point`, which is unknown to Docutils and 0.3 % smaller. +% +% The `DTP point` is available in LaTeX as "bp" (big point): +% +% 1 pt = 1/72.25 in < 1 bp = 1/72 in +% +% +% Backwards compatibility: +% Images with width specification in ``px`` come out slightly (0.3 %) larger: +% +% 1 px = 1 bp = 1/72 in > 1 pt = 1/72.25 in +% +% This can be reset with :: + + \pdfpxdimen=1pt + +% .. caution:: It is impossible to revert the change of lengths specified with +% "pt" or without unit in a style sheet, however the 0.3 % change will be +% imperceptible in most cases. +% +% .. admonition:: Error ``illegal unit px`` +% +% The unit ``px`` is not defined in "pure" LaTeX, but introduced by the +% `pdfTeX` converter on 2005-02-04. `pdfTeX` is used in all modern LaTeX +% distributions (since ca. 2006) also for conversion into DVI. +% +% If you convert the LaTeX source with a legacy program, you might get the +% error ``illegal unit px``. +% +% If updating LaTeX is not an option, just remove the ``px`` from the length +% specification. HTML/CSS will default to ``px`` while the `latexe2` writer +% will add the fallback unit ``bp``. +% +% +% Font encoding +% ------------- +% +% New feature: +% Do not mix font-encoding and font settings: do not load the obsolete +% `ae` and `aeguill` packages unless explicitely required via the +% ``--stylesheet`` option. +% +% :font-encoding = "": do not load `ae` and `aeguill`, i.e. +% +% * do not change font settings, +% * do not use the fontenc package +% (implicitely loaded via `ae`), +% * use LaTeX default font encoding (OT1) +% +% :font-encoding = "OT1": load `fontenc` with ``\usepackage[OT1]{fontenc}`` +% +% Example: +% ``--font-encoding=LGR,T1`` becomes ``\usepackage[LGR,T1]{fontenc}`` +% (Latin, Latin-1 Supplement, and Greek) +% +% +% Backwards compatibility: +% Load the ae and aeguill packages if fontenc is not used. +% +% .. tip:: Using `ae` is not recommended. A similar look (but better +% implementation) can be achieved with the packages `lmodern`, `cmsuper`, +% or `cmlgr` all providing Computer Modern look-alikes in vector format and +% T1 encoding, e.g. ``--font-encoding=T1 --stylesheet=lmodern``. +% +% Sub- and superscript as text +% ---------------------------- +% +% New feature: +% Set sub- and superscript role argument in text mode not as math. +% +% Pass the role content to ``\textsubscript`` or ``\textsuperscript``. +% +% Backwards compatibility: +% The old implementation set the role content in Math mode, where +% +% * whitespace is ignored, +% * a different command set and font setting scheme is active, +% * Latin letters are typeset italic but numbers upright. +% +% Although it is possible to redefine ``\textsubscript`` and +% ``\textsuperscript`` to typeset the content in math-mode, this can lead to +% errors with certain input and is therefore not done in this style sheet. +% +% .. tip:: To get italic subscripts, define and use in your document +% `custom roles`_ like ``.. role:: sub(subscript)`` and +% ``.. role:: super(superscript)`` and define the "role commands":: + + \newcommand{\DUrolesub}{\itshape} + \newcommand{\DUrolesuper}{\itshape} + +% Alternatively, if you want all sub- and superscripts in italic, redefine +% the macros:: + + %% \let\DUsup\textsubscript + %% \let\DUsuper\textsuperscript + %% \renewcommand*{\textsubscript}{\DUsub\itshape} + %% \renewcommand*{\textsuperscript}{\DUsuper\itshape} + +% This is not fully backwards compatible, as it will also set numbers in +% italic shape and not ignore whitespace. +% +% Page layout +% ----------- +% +% New features: +% * Margins are configurable via the ``DIV=...`` document option. +% +% * The ``\raggedbottom`` setting is no longer inserted into the document. It +% is the default for article and report classes. If requested in combination +% with a book class, it can be given in a custom style sheet. +% +% Backwards compatibility: +% Up to version 0.5, use of `typearea` and a DIV setting of 12 were +% hard-coded into the latex2e writer :: + + \usepackage{typearea} + \typearea{12} + +% and the vertical alignment of lower boundary of the text area in book +% classes disabled via :: + + \raggedbottom + + +% ToC and section numbers +% ----------------------- +% +% Better conformance to Docutils specifications. +% +% New feature: +% * The "depth" argument of the "contents" and "sectnum" directives is +% respected. +% +% * section numbering independent of 'use-latex-toc': +% +% + sections are only numbered if there is a "sectnum" directive in the +% document +% +% + section numbering by LaTeX if the "sectnum_xforms" config setting is +% False. +% +% Backwards compatibility: +% +% The previous behaviour was to always number sections if 'use-latex-toc' is +% true, using the document class defaults. It cannot be restored +% universally, the following code sets the default values of the "article" +% document class:: + + \setcounter{secnumdepth}{3} + \setcounter{tocdepth}{3} + +% .. TODO or not to do? (Back-compatibility problems) +% * The default "depth" of the LaTeX-created ToC and the LaTeX section +% numbering is increased to the number of supported section levels. +% +% New feature: +% If 'use-latex-toc' is set, local tables of content are typeset using the +% 'minitoc' package (instead of being ignored). +% +% Backwards compatibility: +% Disable the creation of local ToCs (ignoring all special commands) by +% replacing ``\usepackage{minitoc} with ``\usepackage{mtcoff}``. +% +% +% Default font in admonitions and sidebar +% --------------------------------------- +% +% New feature: +% Use default font in admonitions and sidebar. +% +% Backward compatibility: +% See the fallback definitions for admonitions_, `topic title`_ and +% `sidebar`_. +% +% +% Figure placement +% ---------------- +% +% New feature: +% Use ``\floatplacement`` from the `float` package instead of +% "hard-coded" optional argument for the global setting. +% +% Default to ``\floatplacement{figure}{H}`` (here definitely). This +% corresponds most closely to the source and HTML placement (principle of +% least surprise). +% +% Backwards compatibility: +% Set the global default back to the previous used value:: + + \usepackage{float} + \floatplacement{figure}{htbp} % here, top, bottom, extra-page + + +% Figure and image alignment +% -------------------------- +% +% New features: +% +% a) Fix behaviour of 'align' argument to a figure (do not align figure +% contents). +% +% As the 'figwidth' argument is still ignored and the "natural width" of a +% figure in LaTeX is 100% \textwidth, setting the 'align' argument of a +% figure has currently no effect on the LaTeX output. +% +% b) Set default align of image in a figure to 'center'. +% +% c) Also center images that are wider than textwidth. +% +% d) Align images with class "align-[right|center|left]" (allows setting the +% alignment of an image in a figure). +% +% Backwards compatibility: +% There is no "automatic" way to reverse these changes via a style sheet. +% +% a) The alignment of the image can be set with the "align-left", +% "align-center" and "align-right" class arguments. +% +% As previously, the caption of a figure is aligned according to the +% document class -- configurable with a style sheet using the "caption" +% package. +% +% b) See a) +% +% c) Set the alignment of "oversized" images to "left" to get back the +% old placement. +% +% Shorter preamble +% ---------------- +% +% New feature: +% The document preamble is pruned to contain only relevant commands and +% settings. +% +% Packages that are no longer required +% ```````````````````````````````````` +% +% The following packages where required in pre-0.5 versions and still loaded +% with version 0.5:: + +\usepackage{shortvrb} +\usepackage{amsmath} + + +% Packages that are conditionally loaded +% `````````````````````````````````````` +% +% Additional to the `typearea` for `page layout`_, the following packages are +% only loaded if actually required by doctree elements: +% +% Tables +% ^^^^^^ +% +% Standard package for tables across several pages:: + +\usepackage{longtable} + +% Extra space between text in tables and the line above them +% ('array' is implicitely loaded by 'tabularx', see below):: + +\usepackage{array} +\setlength{\extrarowheight}{2pt} + +% Table cells spanning multiple rows:: + +\usepackage{multirow} + +% Docinfo +% ^^^^^^^ +% +% One-page tables with auto-width columns:: + +\usepackage{tabularx} + +% Images +% ^^^^^^ +% Include graphic files:: + +\usepackage{graphicx} + +% Problematic, Sidebar +% ^^^^^^^^^^^^^^^^^^^^ +% Set text and/or background colour, coloured boxes with ``\colorbox``:: + +\usepackage{color} + +% Floats for footnotes settings +% ````````````````````````````` +% +% Settings for the use of floats for footnotes are only included if +% +% * the option "use-latex-footnotes" is False, and +% * there is at least one footnote in the document. +% +% :: + +% begin: floats for footnotes tweaking. +\setlength{\floatsep}{0.5em} +\setlength{\textfloatsep}{\fill} +\addtolength{\textfloatsep}{3em} +\renewcommand{\textfraction}{0.5} +\renewcommand{\topfraction}{0.5} +\renewcommand{\bottomfraction}{0.5} +\setcounter{totalnumber}{50} +\setcounter{topnumber}{50} +\setcounter{bottomnumber}{50} +% end floats for footnotes + + +% Special lengths, commands, and environments +% ------------------------------------------- +% +% Removed definitions +% ``````````````````` +% +% admonition width +% ^^^^^^^^^^^^^^^^ +% The ``admonitionwith`` lenght is replaced by the more powerful +% ``\DUadmonition`` command (see admonitions_). +% +% Backwards compatibility: +% The default value (90 % of the textwidth) is unchanged. +% +% To configure the admonition width, you must redefine the ``DUadmonition`` +% command instead of changing the ``admonitionwith`` length value. +% +% +% Renamed definitions (now conditional) +% ````````````````````````````````````` +% +% The names for special doctree elements are now prefixed with ``DU``. +% +% Up to version 0.5, all definitions were included in the preamble (before the +% style sheet) of every document -- even if not used in the body. Since +% version 0.6, fallback definitions are included after the style sheet and +% only if required. +% +% Customization is done by an alternative definition in a style sheet with +% ``\newcommand`` instead of the former ``\renewcommand``. +% +% The following code provides the old definitions and maps them (or their +% custom variants) to the new interface. +% +% docinfo width +% ^^^^^^^^^^^^^ +% :: + +\newlength{\docinfowidth} +\setlength{\docinfowidth}{0.9\textwidth} + +\newlength{\DUdocinfowidth} +\AtBeginDocument{\setlength{\DUdocinfowidth}{\docinfowidth}} + +% line block +% ^^^^^^^^^^ +% :: + +\newlength{\lineblockindentation} +\setlength{\lineblockindentation}{2.5em} +\newenvironment{lineblock}[1] +{\begin{list}{} + {\setlength{\partopsep}{\parskip} + \addtolength{\partopsep}{\baselineskip} + \topsep0pt\itemsep0.15\baselineskip\parsep0pt + \leftmargin#1} + \raggedright} +{\end{list}} + +\newlength{\DUlineblockindent} +\AtBeginDocument{\setlength{\DUlineblockindent}{\lineblockindentation}} +\newenvironment{DUlineblock}[1] + {\begin{lineblock}{#1}} + {\end{lineblock}} + +% local line width +% ^^^^^^^^^^^^^^^^ +% +% The ``\locallinewidth`` length for internal use in tables is replaced +% by ``\DUtablewidth``. It was never intended for customization:: + +\newlength{\locallinewidth} + +% option lists +% ^^^^^^^^^^^^ +% :: + +\newcommand{\optionlistlabel}[1]{\bf #1 \hfill} +\newenvironment{optionlist}[1] +{\begin{list}{} + {\setlength{\labelwidth}{#1} + \setlength{\rightmargin}{1cm} + \setlength{\leftmargin}{\rightmargin} + \addtolength{\leftmargin}{\labelwidth} + \addtolength{\leftmargin}{\labelsep} + \renewcommand{\makelabel}{\optionlistlabel}} +}{\end{list}} + +\newcommand{\DUoptionlistlabel}{\optionlistlabel} +\newenvironment{DUoptionlist} + {\begin{optionlist}{3cm}} + {\end{optionlist}} + +% rubric +% ^^^^^^ +% Now less prominent (not bold, normal size) restore with:: + +\newcommand{\rubric}[1]{\subsection*{~\hfill {\it #1} \hfill ~}} +\newcommand{\DUrubric}[2][class-arg]{\rubric{#2}} + +% title reference role +% ^^^^^^^^^^^^^^^^^^^^ +% :: + +\newcommand{\titlereference}[1]{\textsl{#1}} +\newcommand{\DUroletitlereference}[1]{\titlereference{#1}} + + +% New definitions +% ``````````````` +% +% New Feature: +% Enable customization of some more Docutils elements with special commands +% +% :admonition: ``DUadmonition`` command (replacing ``\admonitionwidth``), +% :field list: ``DUfieldlist`` environment, +% :legend: ``DUlegend`` environment, +% :sidebar: ``\DUsidebar``, ``\DUtitle``, and +% ``DUsubtitle`` commands, +% :topic: ``\DUtopic`` and ``\DUtitle`` commands, +% :transition: ``\DUtransition`` command. +% :footnotes: ``\DUfootnotemark`` and ``\DUfootnotetext`` commands with +% hyperlink support using the Docutils-provided footnote label. +% +% Backwards compatibility: +% In most cases, the default definition corresponds to the previously used +% construct. The following definitions restore the old behaviour in case of +% changes. +% +% admonitions +% ^^^^^^^^^^^ +% Use sans-serif fonts:: + +\newcommand{\DUadmonition}[2][class-arg]{% + \begin{center} + \fbox{\parbox{0.9\textwidth}{\sffamily #2}} + \end{center} +} + +% dedication +% ^^^^^^^^^^ +% Do not center:: + +\newcommand{\DUtopicdedication}[1]{#1} + +% But center the title:: + +\newcommand*{\DUtitlededication}[1]{\centerline{\textbf{#1}}} + +% sidebar +% ^^^^^^^ +% Use sans-serif fonts, a frame, and a darker shade of grey:: + +\providecommand{\DUsidebar}[2][class-arg]{% + \begin{center} + \sffamily + \fbox{\colorbox[gray]{0.80}{\parbox{0.9\textwidth}{#2}}} + \end{center} +} + +% sidebar sub-title +% ^^^^^^^^^^^^^^^^^ +% Bold instead of emphasized:: + +\providecommand*{\DUsubtitlesidebar}[1]{\hspace*{\fill}\\ + \textbf{#1}\smallskip} + +% topic +% ^^^^^ +% No quote but normal text:: + +\newcommand{\DUtopic}[2][class-arg]{% + \ifcsname DUtopic#1\endcsname% + \csname DUtopic#1\endcsname{#2}% + \else + #2 + \fi +} + +% topic title +% ^^^^^^^^^^^ +% Title for "topics" (admonitions, sidebar). +% +% Larger font size:: + +\providecommand*{\DUtitletopic}[1]{\textbf{\large #1}\smallskip} + +% transition +% ^^^^^^^^^^ +% Do not add vertical space after the transition. :: + +\providecommand*{\DUtransition}[1][class-arg]{% + \hspace*{\fill}\hrulefill\hspace*{\fill}} diff --git a/docutils/writers/latex2e/titlepage.tex b/docutils/writers/latex2e/titlepage.tex new file mode 100644 index 000000000..d57deca3d --- /dev/null +++ b/docutils/writers/latex2e/titlepage.tex @@ -0,0 +1,20 @@ +% generated by Docutils +$head_prefix +\usepackage{fixltx2e} % LaTeX patches, \textsubscript +\usepackage{cmap} % fix search and cut-and-paste in Acrobat +$requirements +%%% Custom LaTeX preamble +$latex_preamble +%%% User specified packages and stylesheets +$stylesheet +%%% Fallback definitions for Docutils-specific commands +$fallbacks$pdfsetup +$titledata +%%% Body +\begin{document} +\begin{titlepage} +$body_pre_docinfo$docinfo$dedication$abstract +\thispagestyle{empty} +\end{titlepage} +$body +\end{document} diff --git a/docutils/writers/latex2e/xelatex.tex b/docutils/writers/latex2e/xelatex.tex new file mode 100644 index 000000000..09110278d --- /dev/null +++ b/docutils/writers/latex2e/xelatex.tex @@ -0,0 +1,16 @@ +$head_prefix% generated by Docutils +% rubber: set program xelatex +\usepackage[no-sscript]{xltxtra} % loads fixltx2e, metalogo, xunicode, fontspec +\defaultfontfeatures{Scale=MatchLowercase} +$requirements +%%% Custom LaTeX preamble +$latex_preamble +%%% User specified packages and stylesheets +$stylesheet +%%% Fallback definitions for Docutils-specific commands +$fallbacks$pdfsetup +$titledata +%%% Body +\begin{document} +$body_pre_docinfo$docinfo$dedication$abstract$body +\end{document} diff --git a/docutils/writers/manpage.py b/docutils/writers/manpage.py new file mode 100644 index 000000000..2d4735866 --- /dev/null +++ b/docutils/writers/manpage.py @@ -0,0 +1,1135 @@ +# -*- coding: utf-8 -*- +# $Id: manpage.py 7321 2012-01-20 06:50:44Z grubert $ +# Author: Engelbert Gruber +# Copyright: This module is put into the public domain. + +""" +Simple man page writer for reStructuredText. + +Man pages (short for "manual pages") contain system documentation on unix-like +systems. The pages are grouped in numbered sections: + + 1 executable programs and shell commands + 2 system calls + 3 library functions + 4 special files + 5 file formats + 6 games + 7 miscellaneous + 8 system administration + +Man pages are written *troff*, a text file formatting system. + +See http://www.tldp.org/HOWTO/Man-Page for a start. + +Man pages have no subsection only parts. +Standard parts + + NAME , + SYNOPSIS , + DESCRIPTION , + OPTIONS , + FILES , + SEE ALSO , + BUGS , + +and + + AUTHOR . + +A unix-like system keeps an index of the DESCRIPTIONs, which is accesable +by the command whatis or apropos. + +""" + +__docformat__ = 'reStructuredText' + +import re + +import docutils +from docutils import nodes, writers, languages +try: + import roman +except ImportError: + import docutils.utils.roman as roman + +FIELD_LIST_INDENT = 7 +DEFINITION_LIST_INDENT = 7 +OPTION_LIST_INDENT = 7 +BLOCKQOUTE_INDENT = 3.5 + +# Define two macros so man/roff can calculate the +# indent/unindent margins by itself +MACRO_DEF = (r""". +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +""") + +class Writer(writers.Writer): + + supported = ('manpage',) + """Formats this writer supports.""" + + output = None + """Final translated form of `document`.""" + + def __init__(self): + writers.Writer.__init__(self) + self.translator_class = Translator + + def translate(self): + visitor = self.translator_class(self.document) + self.document.walkabout(visitor) + self.output = visitor.astext() + + +class Table: + def __init__(self): + self._rows = [] + self._options = ['center'] + self._tab_char = '\t' + self._coldefs = [] + def new_row(self): + self._rows.append([]) + def append_separator(self, separator): + """Append the separator for table head.""" + self._rows.append([separator]) + def append_cell(self, cell_lines): + """cell_lines is an array of lines""" + start = 0 + if len(cell_lines) > 0 and cell_lines[0] == '.sp\n': + start = 1 + self._rows[-1].append(cell_lines[start:]) + if len(self._coldefs) < len(self._rows[-1]): + self._coldefs.append('l') + def _minimize_cell(self, cell_lines): + """Remove leading and trailing blank and ``.sp`` lines""" + while (cell_lines and cell_lines[0] in ('\n', '.sp\n')): + del cell_lines[0] + while (cell_lines and cell_lines[-1] in ('\n', '.sp\n')): + del cell_lines[-1] + def as_list(self): + text = ['.TS\n'] + text.append(' '.join(self._options) + ';\n') + text.append('|%s|.\n' % ('|'.join(self._coldefs))) + for row in self._rows: + # row = array of cells. cell = array of lines. + text.append('_\n') # line above + text.append('T{\n') + for i in range(len(row)): + cell = row[i] + self._minimize_cell(cell) + text.extend(cell) + if not text[-1].endswith('\n'): + text[-1] += '\n' + if i < len(row)-1: + text.append('T}'+self._tab_char+'T{\n') + else: + text.append('T}\n') + text.append('_\n') + text.append('.TE\n') + return text + +class Translator(nodes.NodeVisitor): + """""" + + words_and_spaces = re.compile(r'\S+| +|\n') + possibly_a_roff_command = re.compile(r'\.\w') + document_start = """Man page generated from reStructuredText.""" + + def __init__(self, document): + nodes.NodeVisitor.__init__(self, document) + self.settings = settings = document.settings + lcode = settings.language_code + self.language = languages.get_language(lcode, document.reporter) + self.head = [] + self.body = [] + self.foot = [] + self.section_level = 0 + self.context = [] + self.topic_class = '' + self.colspecs = [] + self.compact_p = 1 + self.compact_simple = None + # the list style "*" bullet or "#" numbered + self._list_char = [] + # writing the header .TH and .SH NAME is postboned after + # docinfo. + self._docinfo = { + "title" : "", "title_upper": "", + "subtitle" : "", + "manual_section" : "", "manual_group" : "", + "author" : [], + "date" : "", + "copyright" : "", + "version" : "", + } + self._docinfo_keys = [] # a list to keep the sequence as in source. + self._docinfo_names = {} # to get name from text not normalized. + self._in_docinfo = None + self._active_table = None + self._in_literal = False + self.header_written = 0 + self._line_block = 0 + self.authors = [] + self.section_level = 0 + self._indent = [0] + # central definition of simple processing rules + # what to output on : visit, depart + # Do not use paragraph requests ``.PP`` because these set indentation. + # use ``.sp``. Remove superfluous ``.sp`` in ``astext``. + # + # Fonts are put on a stack, the top one is used. + # ``.ft P`` or ``\\fP`` pop from stack. + # ``B`` bold, ``I`` italic, ``R`` roman should be available. + # Hopefully ``C`` courier too. + self.defs = { + 'indent' : ('.INDENT %.1f\n', '.UNINDENT\n'), + 'definition_list_item' : ('.TP', ''), + 'field_name' : ('.TP\n.B ', '\n'), + 'literal' : ('\\fB', '\\fP'), + 'literal_block' : ('.sp\n.nf\n.ft C\n', '\n.ft P\n.fi\n'), + + 'option_list_item' : ('.TP\n', ''), + + 'reference' : (r'\fI\%', r'\fP'), + 'emphasis': ('\\fI', '\\fP'), + 'strong' : ('\\fB', '\\fP'), + 'term' : ('\n.B ', '\n'), + 'title_reference' : ('\\fI', '\\fP'), + + 'topic-title' : ('.SS ',), + 'sidebar-title' : ('.SS ',), + + 'problematic' : ('\n.nf\n', '\n.fi\n'), + } + # NOTE do not specify the newline before a dot-command, but ensure + # it is there. + + def comment_begin(self, text): + """Return commented version of the passed text WITHOUT end of + line/comment.""" + prefix = '.\\" ' + out_text = ''.join( + [(prefix + in_line + '\n') + for in_line in text.split('\n')]) + return out_text + + def comment(self, text): + """Return commented version of the passed text.""" + return self.comment_begin(text)+'.\n' + + def ensure_eol(self): + """Ensure the last line in body is terminated by new line.""" + if self.body[-1][-1] != '\n': + self.body.append('\n') + + def astext(self): + """Return the final formatted document as a string.""" + if not self.header_written: + # ensure we get a ".TH" as viewers require it. + self.head.append(self.header()) + # filter body + for i in xrange(len(self.body)-1, 0, -1): + # remove superfluous vertical gaps. + if self.body[i] == '.sp\n': + if self.body[i - 1][:4] in ('.BI ','.IP '): + self.body[i] = '.\n' + elif (self.body[i - 1][:3] == '.B ' and + self.body[i - 2][:4] == '.TP\n'): + self.body[i] = '.\n' + elif (self.body[i - 1] == '\n' and + not self.possibly_a_roff_command.match(self.body[i - 2]) and + (self.body[i - 3][:7] == '.TP\n.B ' + or self.body[i - 3][:4] == '\n.B ') + ): + self.body[i] = '.\n' + return ''.join(self.head + self.body + self.foot) + + def deunicode(self, text): + text = text.replace(u'\xa0', '\\ ') + text = text.replace(u'\u2020', '\\(dg') + return text + + def visit_Text(self, node): + text = node.astext() + text = text.replace('\\','\\e') + replace_pairs = [ + (u'-', ur'\-'), + (u'\'', ur'\(aq'), + (u'´', ur'\''), + (u'`', ur'\(ga'), + ] + for (in_char, out_markup) in replace_pairs: + text = text.replace(in_char, out_markup) + # unicode + text = self.deunicode(text) + if self._in_literal: + # prevent interpretation of "." at line start + if text[0] == '.': + text = '\\&' + text + text = text.replace('\n.', '\n\\&.') + self.body.append(text) + + def depart_Text(self, node): + pass + + def list_start(self, node): + class enum_char: + enum_style = { + 'bullet' : '\\(bu', + 'emdash' : '\\(em', + } + + def __init__(self, style): + self._style = style + if node.has_key('start'): + self._cnt = node['start'] - 1 + else: + self._cnt = 0 + self._indent = 2 + if style == 'arabic': + # indentation depends on number of childrens + # and start value. + self._indent = len(str(len(node.children))) + self._indent += len(str(self._cnt)) + 1 + elif style == 'loweralpha': + self._cnt += ord('a') - 1 + self._indent = 3 + elif style == 'upperalpha': + self._cnt += ord('A') - 1 + self._indent = 3 + elif style.endswith('roman'): + self._indent = 5 + + def next(self): + if self._style == 'bullet': + return self.enum_style[self._style] + elif self._style == 'emdash': + return self.enum_style[self._style] + self._cnt += 1 + # TODO add prefix postfix + if self._style == 'arabic': + return "%d." % self._cnt + elif self._style in ('loweralpha', 'upperalpha'): + return "%c." % self._cnt + elif self._style.endswith('roman'): + res = roman.toRoman(self._cnt) + '.' + if self._style.startswith('upper'): + return res.upper() + return res.lower() + else: + return "%d." % self._cnt + def get_width(self): + return self._indent + def __repr__(self): + return 'enum_style-%s' % list(self._style) + + if node.has_key('enumtype'): + self._list_char.append(enum_char(node['enumtype'])) + else: + self._list_char.append(enum_char('bullet')) + if len(self._list_char) > 1: + # indent nested lists + self.indent(self._list_char[-2].get_width()) + else: + self.indent(self._list_char[-1].get_width()) + + def list_end(self): + self.dedent() + self._list_char.pop() + + def header(self): + tmpl = (".TH %(title_upper)s %(manual_section)s" + " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n" + ".SH NAME\n" + "%(title)s \- %(subtitle)s\n") + return tmpl % self._docinfo + + def append_header(self): + """append header with .TH and .SH NAME""" + # NOTE before everything + # .TH title_upper section date source manual + if self.header_written: + return + self.body.append(self.header()) + self.body.append(MACRO_DEF) + self.header_written = 1 + + def visit_address(self, node): + self.visit_docinfo_item(node, 'address') + + def depart_address(self, node): + pass + + def visit_admonition(self, node, name=None): + if name: + self.body.append('.IP %s\n' % + self.language.labels.get(name, name)) + + def depart_admonition(self, node): + self.body.append('.RE\n') + + def visit_attention(self, node): + self.visit_admonition(node, 'attention') + + depart_attention = depart_admonition + + def visit_docinfo_item(self, node, name): + if name == 'author': + self._docinfo[name].append(node.astext()) + else: + self._docinfo[name] = node.astext() + self._docinfo_keys.append(name) + raise nodes.SkipNode + + def depart_docinfo_item(self, node): + pass + + def visit_author(self, node): + self.visit_docinfo_item(node, 'author') + + depart_author = depart_docinfo_item + + def visit_authors(self, node): + # _author is called anyway. + pass + + def depart_authors(self, node): + pass + + def visit_block_quote(self, node): + # BUG/HACK: indent alway uses the _last_ indention, + # thus we need two of them. + self.indent(BLOCKQOUTE_INDENT) + self.indent(0) + + def depart_block_quote(self, node): + self.dedent() + self.dedent() + + def visit_bullet_list(self, node): + self.list_start(node) + + def depart_bullet_list(self, node): + self.list_end() + + def visit_caption(self, node): + pass + + def depart_caption(self, node): + pass + + def visit_caution(self, node): + self.visit_admonition(node, 'caution') + + depart_caution = depart_admonition + + def visit_citation(self, node): + num, text = node.astext().split(None, 1) + num = num.strip() + self.body.append('.IP [%s] 5\n' % num) + + def depart_citation(self, node): + pass + + def visit_citation_reference(self, node): + self.body.append('['+node.astext()+']') + raise nodes.SkipNode + + def visit_classifier(self, node): + pass + + def depart_classifier(self, node): + pass + + def visit_colspec(self, node): + self.colspecs.append(node) + + def depart_colspec(self, node): + pass + + def write_colspecs(self): + self.body.append("%s.\n" % ('L '*len(self.colspecs))) + + def visit_comment(self, node, + sub=re.compile('-(?=-)').sub): + self.body.append(self.comment(node.astext())) + raise nodes.SkipNode + + def visit_contact(self, node): + self.visit_docinfo_item(node, 'contact') + + depart_contact = depart_docinfo_item + + def visit_container(self, node): + pass + + def depart_container(self, node): + pass + + def visit_compound(self, node): + pass + + def depart_compound(self, node): + pass + + def visit_copyright(self, node): + self.visit_docinfo_item(node, 'copyright') + + def visit_danger(self, node): + self.visit_admonition(node, 'danger') + + depart_danger = depart_admonition + + def visit_date(self, node): + self.visit_docinfo_item(node, 'date') + + def visit_decoration(self, node): + pass + + def depart_decoration(self, node): + pass + + def visit_definition(self, node): + pass + + def depart_definition(self, node): + pass + + def visit_definition_list(self, node): + self.indent(DEFINITION_LIST_INDENT) + + def depart_definition_list(self, node): + self.dedent() + + def visit_definition_list_item(self, node): + self.body.append(self.defs['definition_list_item'][0]) + + def depart_definition_list_item(self, node): + self.body.append(self.defs['definition_list_item'][1]) + + def visit_description(self, node): + pass + + def depart_description(self, node): + pass + + def visit_docinfo(self, node): + self._in_docinfo = 1 + + def depart_docinfo(self, node): + self._in_docinfo = None + # NOTE nothing should be written before this + self.append_header() + + def visit_doctest_block(self, node): + self.body.append(self.defs['literal_block'][0]) + self._in_literal = True + + def depart_doctest_block(self, node): + self._in_literal = False + self.body.append(self.defs['literal_block'][1]) + + def visit_document(self, node): + # no blank line between comment and header. + self.body.append(self.comment(self.document_start).rstrip()+'\n') + # writing header is postboned + self.header_written = 0 + + def depart_document(self, node): + if self._docinfo['author']: + self.body.append('.SH AUTHOR\n%s\n' + % ', '.join(self._docinfo['author'])) + skip = ('author', 'copyright', 'date', + 'manual_group', 'manual_section', + 'subtitle', + 'title', 'title_upper', 'version') + for name in self._docinfo_keys: + if name == 'address': + self.body.append("\n%s:\n%s%s.nf\n%s\n.fi\n%s%s" % ( + self.language.labels.get(name, name), + self.defs['indent'][0] % 0, + self.defs['indent'][0] % BLOCKQOUTE_INDENT, + self._docinfo[name], + self.defs['indent'][1], + self.defs['indent'][1])) + elif not name in skip: + if name in self._docinfo_names: + label = self._docinfo_names[name] + else: + label = self.language.labels.get(name, name) + self.body.append("\n%s: %s\n" % (label, self._docinfo[name])) + if self._docinfo['copyright']: + self.body.append('.SH COPYRIGHT\n%s\n' + % self._docinfo['copyright']) + self.body.append(self.comment( + 'Generated by docutils manpage writer.')) + + def visit_emphasis(self, node): + self.body.append(self.defs['emphasis'][0]) + + def depart_emphasis(self, node): + self.body.append(self.defs['emphasis'][1]) + + def visit_entry(self, node): + # a cell in a table row + if 'morerows' in node: + self.document.reporter.warning('"table row spanning" not supported', + base_node=node) + if 'morecols' in node: + self.document.reporter.warning( + '"table cell spanning" not supported', base_node=node) + self.context.append(len(self.body)) + + def depart_entry(self, node): + start = self.context.pop() + self._active_table.append_cell(self.body[start:]) + del self.body[start:] + + def visit_enumerated_list(self, node): + self.list_start(node) + + def depart_enumerated_list(self, node): + self.list_end() + + def visit_error(self, node): + self.visit_admonition(node, 'error') + + depart_error = depart_admonition + + def visit_field(self, node): + pass + + def depart_field(self, node): + pass + + def visit_field_body(self, node): + if self._in_docinfo: + name_normalized = self._field_name.lower().replace(" ","_") + self._docinfo_names[name_normalized] = self._field_name + self.visit_docinfo_item(node, name_normalized) + raise nodes.SkipNode + + def depart_field_body(self, node): + pass + + def visit_field_list(self, node): + self.indent(FIELD_LIST_INDENT) + + def depart_field_list(self, node): + self.dedent() + + def visit_field_name(self, node): + if self._in_docinfo: + self._field_name = node.astext() + raise nodes.SkipNode + else: + self.body.append(self.defs['field_name'][0]) + + def depart_field_name(self, node): + self.body.append(self.defs['field_name'][1]) + + def visit_figure(self, node): + self.indent(2.5) + self.indent(0) + + def depart_figure(self, node): + self.dedent() + self.dedent() + + def visit_footer(self, node): + self.document.reporter.warning('"footer" not supported', + base_node=node) + + def depart_footer(self, node): + pass + + def visit_footnote(self, node): + num, text = node.astext().split(None, 1) + num = num.strip() + self.body.append('.IP [%s] 5\n' % self.deunicode(num)) + + def depart_footnote(self, node): + pass + + def footnote_backrefs(self, node): + self.document.reporter.warning('"footnote_backrefs" not supported', + base_node=node) + + def visit_footnote_reference(self, node): + self.body.append('['+self.deunicode(node.astext())+']') + raise nodes.SkipNode + + def depart_footnote_reference(self, node): + pass + + def visit_generated(self, node): + pass + + def depart_generated(self, node): + pass + + def visit_header(self, node): + raise NotImplementedError, node.astext() + + def depart_header(self, node): + pass + + def visit_hint(self, node): + self.visit_admonition(node, 'hint') + + depart_hint = depart_admonition + + def visit_subscript(self, node): + self.body.append('\\s-2\\d') + + def depart_subscript(self, node): + self.body.append('\\u\\s0') + + def visit_superscript(self, node): + self.body.append('\\s-2\\u') + + def depart_superscript(self, node): + self.body.append('\\d\\s0') + + def visit_attribution(self, node): + self.body.append('\\(em ') + + def depart_attribution(self, node): + self.body.append('\n') + + def visit_image(self, node): + self.document.reporter.warning('"image" not supported', + base_node=node) + text = [] + if 'alt' in node.attributes: + text.append(node.attributes['alt']) + if 'uri' in node.attributes: + text.append(node.attributes['uri']) + self.body.append('[image: %s]\n' % ('/'.join(text))) + raise nodes.SkipNode + + def visit_important(self, node): + self.visit_admonition(node, 'important') + + depart_important = depart_admonition + + def visit_label(self, node): + # footnote and citation + if (isinstance(node.parent, nodes.footnote) + or isinstance(node.parent, nodes.citation)): + raise nodes.SkipNode + self.document.reporter.warning('"unsupported "label"', + base_node=node) + self.body.append('[') + + def depart_label(self, node): + self.body.append(']\n') + + def visit_legend(self, node): + pass + + def depart_legend(self, node): + pass + + # WHAT should we use .INDENT, .UNINDENT ? + def visit_line_block(self, node): + self._line_block += 1 + if self._line_block == 1: + # TODO: separate inline blocks from previous paragraphs + # see http://hg.intevation.org/mercurial/crew/rev/9c142ed9c405 + # self.body.append('.sp\n') + # but it does not work for me. + self.body.append('.nf\n') + else: + self.body.append('.in +2\n') + + def depart_line_block(self, node): + self._line_block -= 1 + if self._line_block == 0: + self.body.append('.fi\n') + self.body.append('.sp\n') + else: + self.body.append('.in -2\n') + + def visit_line(self, node): + pass + + def depart_line(self, node): + self.body.append('\n') + + def visit_list_item(self, node): + # man 7 man argues to use ".IP" instead of ".TP" + self.body.append('.IP %s %d\n' % ( + self._list_char[-1].next(), + self._list_char[-1].get_width(),)) + + def depart_list_item(self, node): + pass + + def visit_literal(self, node): + self.body.append(self.defs['literal'][0]) + + def depart_literal(self, node): + self.body.append(self.defs['literal'][1]) + + def visit_literal_block(self, node): + self.body.append(self.defs['literal_block'][0]) + self._in_literal = True + + def depart_literal_block(self, node): + self._in_literal = False + self.body.append(self.defs['literal_block'][1]) + + def visit_math(self, node): + self.document.reporter.warning('"math" role not supported', + base_node=node) + self.visit_literal(node) + + def depart_math(self, node): + self.depart_literal(node) + + def visit_math_block(self, node): + self.document.reporter.warning('"math" directive not supported', + base_node=node) + self.visit_literal_block(node) + + def depart_math_block(self, node): + self.depart_literal_block(node) + + def visit_meta(self, node): + raise NotImplementedError, node.astext() + + def depart_meta(self, node): + pass + + def visit_note(self, node): + self.visit_admonition(node, 'note') + + depart_note = depart_admonition + + def indent(self, by=0.5): + # if we are in a section ".SH" there already is a .RS + step = self._indent[-1] + self._indent.append(by) + self.body.append(self.defs['indent'][0] % step) + + def dedent(self): + self._indent.pop() + self.body.append(self.defs['indent'][1]) + + def visit_option_list(self, node): + self.indent(OPTION_LIST_INDENT) + + def depart_option_list(self, node): + self.dedent() + + def visit_option_list_item(self, node): + # one item of the list + self.body.append(self.defs['option_list_item'][0]) + + def depart_option_list_item(self, node): + self.body.append(self.defs['option_list_item'][1]) + + def visit_option_group(self, node): + # as one option could have several forms it is a group + # options without parameter bold only, .B, -v + # options with parameter bold italic, .BI, -f file + # + # we do not know if .B or .BI + self.context.append('.B') # blind guess + self.context.append(len(self.body)) # to be able to insert later + self.context.append(0) # option counter + + def depart_option_group(self, node): + self.context.pop() # the counter + start_position = self.context.pop() + text = self.body[start_position:] + del self.body[start_position:] + self.body.append('%s%s\n' % (self.context.pop(), ''.join(text))) + + def visit_option(self, node): + # each form of the option will be presented separately + if self.context[-1] > 0: + self.body.append(', ') + if self.context[-3] == '.BI': + self.body.append('\\') + self.body.append(' ') + + def depart_option(self, node): + self.context[-1] += 1 + + def visit_option_string(self, node): + # do not know if .B or .BI + pass + + def depart_option_string(self, node): + pass + + def visit_option_argument(self, node): + self.context[-3] = '.BI' # bold/italic alternate + if node['delimiter'] != ' ': + self.body.append('\\fB%s ' % node['delimiter']) + elif self.body[len(self.body)-1].endswith('='): + # a blank only means no blank in output, just changing font + self.body.append(' ') + else: + # blank backslash blank, switch font then a blank + self.body.append(' \\ ') + + def depart_option_argument(self, node): + pass + + def visit_organization(self, node): + self.visit_docinfo_item(node, 'organization') + + def depart_organization(self, node): + pass + + def first_child(self, node): + first = isinstance(node.parent[0], nodes.label) # skip label + for child in node.parent.children[first:]: + if isinstance(child, nodes.Invisible): + continue + if child is node: + return 1 + break + return 0 + + def visit_paragraph(self, node): + # ``.PP`` : Start standard indented paragraph. + # ``.LP`` : Start block paragraph, all except the first. + # ``.P [type]`` : Start paragraph type. + # NOTE dont use paragraph starts because they reset indentation. + # ``.sp`` is only vertical space + self.ensure_eol() + if not self.first_child(node): + self.body.append('.sp\n') + + def depart_paragraph(self, node): + self.body.append('\n') + + def visit_problematic(self, node): + self.body.append(self.defs['problematic'][0]) + + def depart_problematic(self, node): + self.body.append(self.defs['problematic'][1]) + + def visit_raw(self, node): + if node.get('format') == 'manpage': + self.body.append(node.astext() + "\n") + # Keep non-manpage raw text out of output: + raise nodes.SkipNode + + def visit_reference(self, node): + """E.g. link or email address.""" + self.body.append(self.defs['reference'][0]) + + def depart_reference(self, node): + self.body.append(self.defs['reference'][1]) + + def visit_revision(self, node): + self.visit_docinfo_item(node, 'revision') + + depart_revision = depart_docinfo_item + + def visit_row(self, node): + self._active_table.new_row() + + def depart_row(self, node): + pass + + def visit_section(self, node): + self.section_level += 1 + + def depart_section(self, node): + self.section_level -= 1 + + def visit_status(self, node): + self.visit_docinfo_item(node, 'status') + + depart_status = depart_docinfo_item + + def visit_strong(self, node): + self.body.append(self.defs['strong'][0]) + + def depart_strong(self, node): + self.body.append(self.defs['strong'][1]) + + def visit_substitution_definition(self, node): + """Internal only.""" + raise nodes.SkipNode + + def visit_substitution_reference(self, node): + self.document.reporter.warning('"substitution_reference" not supported', + base_node=node) + + def visit_subtitle(self, node): + if isinstance(node.parent, nodes.sidebar): + self.body.append(self.defs['strong'][0]) + elif isinstance(node.parent, nodes.document): + self.visit_docinfo_item(node, 'subtitle') + elif isinstance(node.parent, nodes.section): + self.body.append(self.defs['strong'][0]) + + def depart_subtitle(self, node): + # document subtitle calls SkipNode + self.body.append(self.defs['strong'][1]+'\n.PP\n') + + def visit_system_message(self, node): + # TODO add report_level + #if node['level'] < self.document.reporter['writer'].report_level: + # Level is too low to display: + # raise nodes.SkipNode + attr = {} + backref_text = '' + if node.hasattr('id'): + attr['name'] = node['id'] + if node.hasattr('line'): + line = ', line %s' % node['line'] + else: + line = '' + self.body.append('.IP "System Message: %s/%s (%s:%s)"\n' + % (node['type'], node['level'], node['source'], line)) + + def depart_system_message(self, node): + pass + + def visit_table(self, node): + self._active_table = Table() + + def depart_table(self, node): + self.ensure_eol() + self.body.extend(self._active_table.as_list()) + self._active_table = None + + def visit_target(self, node): + # targets are in-document hyper targets, without any use for man-pages. + raise nodes.SkipNode + + def visit_tbody(self, node): + pass + + def depart_tbody(self, node): + pass + + def visit_term(self, node): + self.body.append(self.defs['term'][0]) + + def depart_term(self, node): + self.body.append(self.defs['term'][1]) + + def visit_tgroup(self, node): + pass + + def depart_tgroup(self, node): + pass + + def visit_thead(self, node): + # MAYBE double line '=' + pass + + def depart_thead(self, node): + # MAYBE double line '=' + pass + + def visit_tip(self, node): + self.visit_admonition(node, 'tip') + + depart_tip = depart_admonition + + def visit_title(self, node): + if isinstance(node.parent, nodes.topic): + self.body.append(self.defs['topic-title'][0]) + elif isinstance(node.parent, nodes.sidebar): + self.body.append(self.defs['sidebar-title'][0]) + elif isinstance(node.parent, nodes.admonition): + self.body.append('.IP "') + elif self.section_level == 0: + self._docinfo['title'] = node.astext() + # document title for .TH + self._docinfo['title_upper'] = node.astext().upper() + raise nodes.SkipNode + elif self.section_level == 1: + self.body.append('.SH %s\n' % self.deunicode(node.astext().upper())) + raise nodes.SkipNode + else: + self.body.append('.SS ') + + def depart_title(self, node): + if isinstance(node.parent, nodes.admonition): + self.body.append('"') + self.body.append('\n') + + def visit_title_reference(self, node): + """inline citation reference""" + self.body.append(self.defs['title_reference'][0]) + + def depart_title_reference(self, node): + self.body.append(self.defs['title_reference'][1]) + + def visit_topic(self, node): + pass + + def depart_topic(self, node): + pass + + def visit_sidebar(self, node): + pass + + def depart_sidebar(self, node): + pass + + def visit_rubric(self, node): + pass + + def depart_rubric(self, node): + pass + + def visit_transition(self, node): + # .PP Begin a new paragraph and reset prevailing indent. + # .sp N leaves N lines of blank space. + # .ce centers the next line + self.body.append('\n.sp\n.ce\n----\n') + + def depart_transition(self, node): + self.body.append('\n.ce 0\n.sp\n') + + def visit_version(self, node): + self.visit_docinfo_item(node, 'version') + + def visit_warning(self, node): + self.visit_admonition(node, 'warning') + + depart_warning = depart_admonition + + def unimplemented_visit(self, node): + raise NotImplementedError('visiting unimplemented node type: %s' + % node.__class__.__name__) + +# vim: set fileencoding=utf-8 et ts=4 ai : diff --git a/docutils/writers/null.py b/docutils/writers/null.py new file mode 100644 index 000000000..b870788a4 --- /dev/null +++ b/docutils/writers/null.py @@ -0,0 +1,21 @@ +# $Id: null.py 4564 2006-05-21 20:44:42Z wiemann $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +A do-nothing Writer. +""" + +from docutils import writers + + +class Writer(writers.UnfilteredWriter): + + supported = ('null',) + """Formats this writer supports.""" + + config_section = 'null writer' + config_section_dependencies = ('writers',) + + def translate(self): + pass diff --git a/docutils/writers/odf_odt/__init__.py b/docutils/writers/odf_odt/__init__.py new file mode 100644 index 000000000..3a9f6fa7b --- /dev/null +++ b/docutils/writers/odf_odt/__init__.py @@ -0,0 +1,3266 @@ +# $Id: __init__.py 7422 2012-05-03 10:55:30Z milde $ +# Author: Dave Kuhlman +# Copyright: This module has been placed in the public domain. + +""" +Open Document Format (ODF) Writer. + +""" + +VERSION = '1.0a' + +__docformat__ = 'reStructuredText' + + +import sys +import os +import os.path +import tempfile +import zipfile +from xml.dom import minidom +import time +import re +import StringIO +import inspect +import imp +import copy +import urllib2 +import docutils +from docutils import frontend, nodes, utils, writers, languages +from docutils.parsers import rst +from docutils.readers import standalone +from docutils.transforms import references + + +WhichElementTree = '' +try: + # 1. Try to use lxml. + #from lxml import etree + #WhichElementTree = 'lxml' + raise ImportError('Ignoring lxml') +except ImportError, e: + try: + # 2. Try to use ElementTree from the Python standard library. + from xml.etree import ElementTree as etree + WhichElementTree = 'elementtree' + except ImportError, e: + try: + # 3. Try to use a version of ElementTree installed as a separate + # product. + from elementtree import ElementTree as etree + WhichElementTree = 'elementtree' + except ImportError, e: + s1 = 'Must install either a version of Python containing ' \ + 'ElementTree (Python version >=2.5) or install ElementTree.' + raise ImportError(s1) + +# +# Import pygments and odtwriter pygments formatters if possible. +try: + import pygments + import pygments.lexers + from pygmentsformatter import OdtPygmentsProgFormatter, \ + OdtPygmentsLaTeXFormatter +except ImportError, exp: + pygments = None + +try: # check for the Python Imaging Library + import PIL +except ImportError: + try: # sometimes PIL modules are put in PYTHONPATH's root + import Image + class PIL(object): pass # dummy wrapper + PIL.Image = Image + except ImportError: + PIL = None + +## import warnings +## warnings.warn('importing IPShellEmbed', UserWarning) +## from IPython.Shell import IPShellEmbed +## args = ['-pdb', '-pi1', 'In <\\#>: ', '-pi2', ' .\\D.: ', +## '-po', 'Out<\\#>: ', '-nosep'] +## ipshell = IPShellEmbed(args, +## banner = 'Entering IPython. Press Ctrl-D to exit.', +## exit_msg = 'Leaving Interpreter, back to program.') + + +# +# ElementTree does not support getparent method (lxml does). +# This wrapper class and the following support functions provide +# that support for the ability to get the parent of an element. +# +if WhichElementTree == 'elementtree': + class _ElementInterfaceWrapper(etree._ElementInterface): + def __init__(self, tag, attrib=None): + etree._ElementInterface.__init__(self, tag, attrib) + if attrib is None: + attrib = {} + self.parent = None + def setparent(self, parent): + self.parent = parent + def getparent(self): + return self.parent + + +# +# Constants and globals + +SPACES_PATTERN = re.compile(r'( +)') +TABS_PATTERN = re.compile(r'(\t+)') +FILL_PAT1 = re.compile(r'^ +') +FILL_PAT2 = re.compile(r' {2,}') + +TABLESTYLEPREFIX = 'rststyle-table-' +TABLENAMEDEFAULT = '%s0' % TABLESTYLEPREFIX +TABLEPROPERTYNAMES = ('border', 'border-top', 'border-left', + 'border-right', 'border-bottom', ) + +GENERATOR_DESC = 'Docutils.org/odf_odt' + +NAME_SPACE_1 = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0' + +CONTENT_NAMESPACE_DICT = CNSD = { +# 'office:version': '1.0', + 'chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0', + 'dc': 'http://purl.org/dc/elements/1.1/', + 'dom': 'http://www.w3.org/2001/xml-events', + 'dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0', + 'draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0', + 'fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0', + 'form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0', + 'math': 'http://www.w3.org/1998/Math/MathML', + 'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0', + 'number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0', + 'office': NAME_SPACE_1, + 'ooo': 'http://openoffice.org/2004/office', + 'oooc': 'http://openoffice.org/2004/calc', + 'ooow': 'http://openoffice.org/2004/writer', + 'presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0', + + 'script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0', + 'style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0', + 'svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0', + 'table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0', + 'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0', + 'xforms': 'http://www.w3.org/2002/xforms', + 'xlink': 'http://www.w3.org/1999/xlink', + 'xsd': 'http://www.w3.org/2001/XMLSchema', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + } + +STYLES_NAMESPACE_DICT = SNSD = { +# 'office:version': '1.0', + 'chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0', + 'dc': 'http://purl.org/dc/elements/1.1/', + 'dom': 'http://www.w3.org/2001/xml-events', + 'dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0', + 'draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0', + 'fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0', + 'form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0', + 'math': 'http://www.w3.org/1998/Math/MathML', + 'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0', + 'number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0', + 'office': NAME_SPACE_1, + 'presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0', + 'ooo': 'http://openoffice.org/2004/office', + 'oooc': 'http://openoffice.org/2004/calc', + 'ooow': 'http://openoffice.org/2004/writer', + 'script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0', + 'style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0', + 'svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0', + 'table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0', + 'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0', + 'xlink': 'http://www.w3.org/1999/xlink', + } + +MANIFEST_NAMESPACE_DICT = MANNSD = { + 'manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0', +} + +META_NAMESPACE_DICT = METNSD = { +# 'office:version': '1.0', + 'dc': 'http://purl.org/dc/elements/1.1/', + 'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0', + 'office': NAME_SPACE_1, + 'ooo': 'http://openoffice.org/2004/office', + 'xlink': 'http://www.w3.org/1999/xlink', +} + +# +# Attribute dictionaries for use with ElementTree (not lxml), which +# does not support use of nsmap parameter on Element() and SubElement(). + +CONTENT_NAMESPACE_ATTRIB = { + #'office:version': '1.0', + 'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0', + 'xmlns:dc': 'http://purl.org/dc/elements/1.1/', + 'xmlns:dom': 'http://www.w3.org/2001/xml-events', + 'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0', + 'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0', + 'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0', + 'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0', + 'xmlns:math': 'http://www.w3.org/1998/Math/MathML', + 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0', + 'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0', + 'xmlns:office': NAME_SPACE_1, + 'xmlns:presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0', + 'xmlns:ooo': 'http://openoffice.org/2004/office', + 'xmlns:oooc': 'http://openoffice.org/2004/calc', + 'xmlns:ooow': 'http://openoffice.org/2004/writer', + 'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0', + 'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0', + 'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0', + 'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0', + 'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0', + 'xmlns:xforms': 'http://www.w3.org/2002/xforms', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'xmlns:xsd': 'http://www.w3.org/2001/XMLSchema', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + } + +STYLES_NAMESPACE_ATTRIB = { + #'office:version': '1.0', + 'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0', + 'xmlns:dc': 'http://purl.org/dc/elements/1.1/', + 'xmlns:dom': 'http://www.w3.org/2001/xml-events', + 'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0', + 'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0', + 'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0', + 'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0', + 'xmlns:math': 'http://www.w3.org/1998/Math/MathML', + 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0', + 'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0', + 'xmlns:office': NAME_SPACE_1, + 'xmlns:presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0', + 'xmlns:ooo': 'http://openoffice.org/2004/office', + 'xmlns:oooc': 'http://openoffice.org/2004/calc', + 'xmlns:ooow': 'http://openoffice.org/2004/writer', + 'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0', + 'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0', + 'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0', + 'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0', + 'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + } + +MANIFEST_NAMESPACE_ATTRIB = { + 'xmlns:manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0', +} + +META_NAMESPACE_ATTRIB = { + #'office:version': '1.0', + 'xmlns:dc': 'http://purl.org/dc/elements/1.1/', + 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0', + 'xmlns:office': NAME_SPACE_1, + 'xmlns:ooo': 'http://openoffice.org/2004/office', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', +} + + +# +# Functions +# + +# +# ElementTree support functions. +# In order to be able to get the parent of elements, must use these +# instead of the functions with same name provided by ElementTree. +# +def Element(tag, attrib=None, nsmap=None, nsdict=CNSD): + if attrib is None: + attrib = {} + tag, attrib = fix_ns(tag, attrib, nsdict) + if WhichElementTree == 'lxml': + el = etree.Element(tag, attrib, nsmap=nsmap) + else: + el = _ElementInterfaceWrapper(tag, attrib) + return el + +def SubElement(parent, tag, attrib=None, nsmap=None, nsdict=CNSD): + if attrib is None: + attrib = {} + tag, attrib = fix_ns(tag, attrib, nsdict) + if WhichElementTree == 'lxml': + el = etree.SubElement(parent, tag, attrib, nsmap=nsmap) + else: + el = _ElementInterfaceWrapper(tag, attrib) + parent.append(el) + el.setparent(parent) + return el + +def fix_ns(tag, attrib, nsdict): + nstag = add_ns(tag, nsdict) + nsattrib = {} + for key, val in attrib.iteritems(): + nskey = add_ns(key, nsdict) + nsattrib[nskey] = val + return nstag, nsattrib + +def add_ns(tag, nsdict=CNSD): + if WhichElementTree == 'lxml': + nstag, name = tag.split(':') + ns = nsdict.get(nstag) + if ns is None: + raise RuntimeError, 'Invalid namespace prefix: %s' % nstag + tag = '{%s}%s' % (ns, name,) + return tag + +def ToString(et): + outstream = StringIO.StringIO() + if sys.version_info >= (3, 2): + et.write(outstream, encoding="unicode") + else: + et.write(outstream) + s1 = outstream.getvalue() + outstream.close() + return s1 + + +def escape_cdata(text): + text = text.replace("&", "&") + text = text.replace("<", "<") + text = text.replace(">", ">") + ascii = '' + for char in text: + if ord(char) >= ord("\x7f"): + ascii += "&#x%X;" % ( ord(char), ) + else: + ascii += char + return ascii + + + +WORD_SPLIT_PAT1 = re.compile(r'\b(\w*)\b\W*') + +def split_words(line): + # We need whitespace at the end of the string for our regexpr. + line += ' ' + words = [] + pos1 = 0 + mo = WORD_SPLIT_PAT1.search(line, pos1) + while mo is not None: + word = mo.groups()[0] + words.append(word) + pos1 = mo.end() + mo = WORD_SPLIT_PAT1.search(line, pos1) + return words + + +# +# Classes +# + + +class TableStyle(object): + def __init__(self, border=None, backgroundcolor=None): + self.border = border + self.backgroundcolor = backgroundcolor + def get_border_(self): + return self.border_ + def set_border_(self, border): + self.border_ = border + border = property(get_border_, set_border_) + def get_backgroundcolor_(self): + return self.backgroundcolor_ + def set_backgroundcolor_(self, backgroundcolor): + self.backgroundcolor_ = backgroundcolor + backgroundcolor = property(get_backgroundcolor_, set_backgroundcolor_) + +BUILTIN_DEFAULT_TABLE_STYLE = TableStyle( + border = '0.0007in solid #000000') + +# +# Information about the indentation level for lists nested inside +# other contexts, e.g. dictionary lists. +class ListLevel(object): + def __init__(self, level, sibling_level=True, nested_level=True): + self.level = level + self.sibling_level = sibling_level + self.nested_level = nested_level + def set_sibling(self, sibling_level): self.sibling_level = sibling_level + def get_sibling(self): return self.sibling_level + def set_nested(self, nested_level): self.nested_level = nested_level + def get_nested(self): return self.nested_level + def set_level(self, level): self.level = level + def get_level(self): return self.level + + +class Writer(writers.Writer): + + MIME_TYPE = 'application/vnd.oasis.opendocument.text' + EXTENSION = '.odt' + + supported = ('odt', ) + """Formats this writer supports.""" + + default_stylesheet = 'styles' + EXTENSION + + default_stylesheet_path = utils.relative_path( + os.path.join(os.getcwd(), 'dummy'), + os.path.join(os.path.dirname(__file__), default_stylesheet)) + + default_template = 'template.txt' + + default_template_path = utils.relative_path( + os.path.join(os.getcwd(), 'dummy'), + os.path.join(os.path.dirname(__file__), default_template)) + + settings_spec = ( + 'ODF-Specific Options', + None, + ( + ('Specify a stylesheet. ' + 'Default: "%s"' % default_stylesheet_path, + ['--stylesheet'], + { + 'default': default_stylesheet_path, + 'dest': 'stylesheet' + }), + ('Specify a configuration/mapping file relative to the ' + 'current working ' + 'directory for additional ODF options. ' + 'In particular, this file may contain a section named ' + '"Formats" that maps default style names to ' + 'names to be used in the resulting output file allowing for ' + 'adhering to external standards. ' + 'For more info and the format of the configuration/mapping file, ' + 'see the odtwriter doc.', + ['--odf-config-file'], + {'metavar': ''}), + ('Obfuscate email addresses to confuse harvesters while still ' + 'keeping email links usable with standards-compliant browsers.', + ['--cloak-email-addresses'], + {'default': False, + 'action': 'store_true', + 'dest': 'cloak_email_addresses', + 'validator': frontend.validate_boolean}), + ('Do not obfuscate email addresses.', + ['--no-cloak-email-addresses'], + {'default': False, + 'action': 'store_false', + 'dest': 'cloak_email_addresses', + 'validator': frontend.validate_boolean}), + ('Specify the thickness of table borders in thousands of a cm. ' + 'Default is 35.', + ['--table-border-thickness'], + {'default': None, + 'validator': frontend.validate_nonnegative_int}), + ('Add syntax highlighting in literal code blocks.', + ['--add-syntax-highlighting'], + {'default': False, + 'action': 'store_true', + 'dest': 'add_syntax_highlighting', + 'validator': frontend.validate_boolean}), + ('Do not add syntax highlighting in literal code blocks. (default)', + ['--no-syntax-highlighting'], + {'default': False, + 'action': 'store_false', + 'dest': 'add_syntax_highlighting', + 'validator': frontend.validate_boolean}), + ('Create sections for headers. (default)', + ['--create-sections'], + {'default': True, + 'action': 'store_true', + 'dest': 'create_sections', + 'validator': frontend.validate_boolean}), + ('Do not create sections for headers.', + ['--no-sections'], + {'default': True, + 'action': 'store_false', + 'dest': 'create_sections', + 'validator': frontend.validate_boolean}), + ('Create links.', + ['--create-links'], + {'default': False, + 'action': 'store_true', + 'dest': 'create_links', + 'validator': frontend.validate_boolean}), + ('Do not create links. (default)', + ['--no-links'], + {'default': False, + 'action': 'store_false', + 'dest': 'create_links', + 'validator': frontend.validate_boolean}), + ('Generate endnotes at end of document, not footnotes ' + 'at bottom of page.', + ['--endnotes-end-doc'], + {'default': False, + 'action': 'store_true', + 'dest': 'endnotes_end_doc', + 'validator': frontend.validate_boolean}), + ('Generate footnotes at bottom of page, not endnotes ' + 'at end of document. (default)', + ['--no-endnotes-end-doc'], + {'default': False, + 'action': 'store_false', + 'dest': 'endnotes_end_doc', + 'validator': frontend.validate_boolean}), + ('Generate a bullet list table of contents, not ' + 'an ODF/oowriter table of contents.', + ['--generate-list-toc'], + {'default': True, + 'action': 'store_false', + 'dest': 'generate_oowriter_toc', + 'validator': frontend.validate_boolean}), + ('Generate an ODF/oowriter table of contents, not ' + 'a bullet list. (default)', + ['--generate-oowriter-toc'], + {'default': True, + 'action': 'store_true', + 'dest': 'generate_oowriter_toc', + 'validator': frontend.validate_boolean}), + ('Specify the contents of an custom header line. ' + 'See odf_odt writer documentation for details ' + 'about special field character sequences.', + ['--custom-odt-header'], + { 'default': '', + 'dest': 'custom_header', + }), + ('Specify the contents of an custom footer line. ' + 'See odf_odt writer documentation for details ' + 'about special field character sequences.', + ['--custom-odt-footer'], + { 'default': '', + 'dest': 'custom_footer', + }), + ) + ) + + settings_defaults = { + 'output_encoding_error_handler': 'xmlcharrefreplace', + } + + relative_path_settings = ( + 'stylesheet_path', + ) + + config_section = 'opendocument odf writer' + config_section_dependencies = ( + 'writers', + ) + + def __init__(self): + writers.Writer.__init__(self) + self.translator_class = ODFTranslator + + def translate(self): + self.settings = self.document.settings + self.visitor = self.translator_class(self.document) + self.visitor.retrieve_styles(self.EXTENSION) + self.document.walkabout(self.visitor) + self.visitor.add_doc_title() + self.assemble_my_parts() + self.output = self.parts['whole'] + + def assemble_my_parts(self): + """Assemble the `self.parts` dictionary. Extend in subclasses. + """ + writers.Writer.assemble_parts(self) + f = tempfile.NamedTemporaryFile() + zfile = zipfile.ZipFile(f, 'w', zipfile.ZIP_DEFLATED) + self.write_zip_str(zfile, 'mimetype', self.MIME_TYPE, + compress_type=zipfile.ZIP_STORED) + content = self.visitor.content_astext() + self.write_zip_str(zfile, 'content.xml', content) + s1 = self.create_manifest() + self.write_zip_str(zfile, 'META-INF/manifest.xml', s1) + s1 = self.create_meta() + self.write_zip_str(zfile, 'meta.xml', s1) + s1 = self.get_stylesheet() + self.write_zip_str(zfile, 'styles.xml', s1) + self.store_embedded_files(zfile) + self.copy_from_stylesheet(zfile) + zfile.close() + f.seek(0) + whole = f.read() + f.close() + self.parts['whole'] = whole + self.parts['encoding'] = self.document.settings.output_encoding + self.parts['version'] = docutils.__version__ + + def write_zip_str(self, zfile, name, bytes, compress_type=zipfile.ZIP_DEFLATED): + localtime = time.localtime(time.time()) + zinfo = zipfile.ZipInfo(name, localtime) + # Add some standard UNIX file access permissions (-rw-r--r--). + zinfo.external_attr = (0x81a4 & 0xFFFF) << 16L + zinfo.compress_type = compress_type + zfile.writestr(zinfo, bytes) + + def store_embedded_files(self, zfile): + embedded_files = self.visitor.get_embedded_file_list() + for source, destination in embedded_files: + if source is None: + continue + try: + # encode/decode + destination1 = destination.decode('latin-1').encode('utf-8') + zfile.write(source, destination1) + except OSError, e: + self.document.reporter.warning( + "Can't open file %s." % (source, )) + + def get_settings(self): + """ + modeled after get_stylesheet + """ + stylespath = self.settings.stylesheet + zfile = zipfile.ZipFile(stylespath, 'r') + s1 = zfile.read('settings.xml') + zfile.close() + return s1 + + def get_stylesheet(self): + """Get the stylesheet from the visitor. + Ask the visitor to setup the page. + """ + s1 = self.visitor.setup_page() + return s1 + + def copy_from_stylesheet(self, outzipfile): + """Copy images, settings, etc from the stylesheet doc into target doc. + """ + stylespath = self.settings.stylesheet + inzipfile = zipfile.ZipFile(stylespath, 'r') + # Copy the styles. + s1 = inzipfile.read('settings.xml') + self.write_zip_str(outzipfile, 'settings.xml', s1) + # Copy the images. + namelist = inzipfile.namelist() + for name in namelist: + if name.startswith('Pictures/'): + imageobj = inzipfile.read(name) + outzipfile.writestr(name, imageobj) + inzipfile.close() + + def assemble_parts(self): + pass + + def create_manifest(self): + if WhichElementTree == 'lxml': + root = Element('manifest:manifest', + nsmap=MANIFEST_NAMESPACE_DICT, + nsdict=MANIFEST_NAMESPACE_DICT, + ) + else: + root = Element('manifest:manifest', + attrib=MANIFEST_NAMESPACE_ATTRIB, + nsdict=MANIFEST_NAMESPACE_DICT, + ) + doc = etree.ElementTree(root) + SubElement(root, 'manifest:file-entry', attrib={ + 'manifest:media-type': self.MIME_TYPE, + 'manifest:full-path': '/', + }, nsdict=MANNSD) + SubElement(root, 'manifest:file-entry', attrib={ + 'manifest:media-type': 'text/xml', + 'manifest:full-path': 'content.xml', + }, nsdict=MANNSD) + SubElement(root, 'manifest:file-entry', attrib={ + 'manifest:media-type': 'text/xml', + 'manifest:full-path': 'styles.xml', + }, nsdict=MANNSD) + SubElement(root, 'manifest:file-entry', attrib={ + 'manifest:media-type': 'text/xml', + 'manifest:full-path': 'settings.xml', + }, nsdict=MANNSD) + SubElement(root, 'manifest:file-entry', attrib={ + 'manifest:media-type': 'text/xml', + 'manifest:full-path': 'meta.xml', + }, nsdict=MANNSD) + s1 = ToString(doc) + doc = minidom.parseString(s1) + s1 = doc.toprettyxml(' ') + return s1 + + def create_meta(self): + if WhichElementTree == 'lxml': + root = Element('office:document-meta', + nsmap=META_NAMESPACE_DICT, + nsdict=META_NAMESPACE_DICT, + ) + else: + root = Element('office:document-meta', + attrib=META_NAMESPACE_ATTRIB, + nsdict=META_NAMESPACE_DICT, + ) + doc = etree.ElementTree(root) + root = SubElement(root, 'office:meta', nsdict=METNSD) + el1 = SubElement(root, 'meta:generator', nsdict=METNSD) + el1.text = 'Docutils/rst2odf.py/%s' % (VERSION, ) + s1 = os.environ.get('USER', '') + el1 = SubElement(root, 'meta:initial-creator', nsdict=METNSD) + el1.text = s1 + s2 = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime()) + el1 = SubElement(root, 'meta:creation-date', nsdict=METNSD) + el1.text = s2 + el1 = SubElement(root, 'dc:creator', nsdict=METNSD) + el1.text = s1 + el1 = SubElement(root, 'dc:date', nsdict=METNSD) + el1.text = s2 + el1 = SubElement(root, 'dc:language', nsdict=METNSD) + el1.text = 'en-US' + el1 = SubElement(root, 'meta:editing-cycles', nsdict=METNSD) + el1.text = '1' + el1 = SubElement(root, 'meta:editing-duration', nsdict=METNSD) + el1.text = 'PT00M01S' + title = self.visitor.get_title() + el1 = SubElement(root, 'dc:title', nsdict=METNSD) + if title: + el1.text = title + else: + el1.text = '[no title]' + meta_dict = self.visitor.get_meta_dict() + keywordstr = meta_dict.get('keywords') + if keywordstr is not None: + keywords = split_words(keywordstr) + for keyword in keywords: + el1 = SubElement(root, 'meta:keyword', nsdict=METNSD) + el1.text = keyword + description = meta_dict.get('description') + if description is not None: + el1 = SubElement(root, 'dc:description', nsdict=METNSD) + el1.text = description + s1 = ToString(doc) + #doc = minidom.parseString(s1) + #s1 = doc.toprettyxml(' ') + return s1 + +# class ODFTranslator(nodes.SparseNodeVisitor): + +class ODFTranslator(nodes.GenericNodeVisitor): + + used_styles = ( + 'attribution', 'blockindent', 'blockquote', 'blockquote-bulletitem', + 'blockquote-bulletlist', 'blockquote-enumitem', 'blockquote-enumlist', + 'bulletitem', 'bulletlist', + 'caption', 'legend', + 'centeredtextbody', 'codeblock', 'codeblock-indented', + 'codeblock-classname', 'codeblock-comment', 'codeblock-functionname', + 'codeblock-keyword', 'codeblock-name', 'codeblock-number', + 'codeblock-operator', 'codeblock-string', 'emphasis', 'enumitem', + 'enumlist', 'epigraph', 'epigraph-bulletitem', 'epigraph-bulletlist', + 'epigraph-enumitem', 'epigraph-enumlist', 'footer', + 'footnote', 'citation', + 'header', 'highlights', 'highlights-bulletitem', + 'highlights-bulletlist', 'highlights-enumitem', 'highlights-enumlist', + 'horizontalline', 'inlineliteral', 'quotation', 'rubric', + 'strong', 'table-title', 'textbody', 'tocbulletlist', 'tocenumlist', + 'title', + 'subtitle', + 'heading1', + 'heading2', + 'heading3', + 'heading4', + 'heading5', + 'heading6', + 'heading7', + 'admon-attention-hdr', + 'admon-attention-body', + 'admon-caution-hdr', + 'admon-caution-body', + 'admon-danger-hdr', + 'admon-danger-body', + 'admon-error-hdr', + 'admon-error-body', + 'admon-generic-hdr', + 'admon-generic-body', + 'admon-hint-hdr', + 'admon-hint-body', + 'admon-important-hdr', + 'admon-important-body', + 'admon-note-hdr', + 'admon-note-body', + 'admon-tip-hdr', + 'admon-tip-body', + 'admon-warning-hdr', + 'admon-warning-body', + 'tableoption', + 'tableoption.%c', 'tableoption.%c%d', 'Table%d', 'Table%d.%c', + 'Table%d.%c%d', + 'lineblock1', + 'lineblock2', + 'lineblock3', + 'lineblock4', + 'lineblock5', + 'lineblock6', + 'image', 'figureframe', + ) + + def __init__(self, document): + #nodes.SparseNodeVisitor.__init__(self, document) + nodes.GenericNodeVisitor.__init__(self, document) + self.settings = document.settings + lcode = self.settings.language_code + self.language = languages.get_language(lcode, document.reporter) + self.format_map = { } + if self.settings.odf_config_file: + from ConfigParser import ConfigParser + + parser = ConfigParser() + parser.read(self.settings.odf_config_file) + for rststyle, format in parser.items("Formats"): + if rststyle not in self.used_styles: + self.document.reporter.warning( + 'Style "%s" is not a style used by odtwriter.' % ( + rststyle, )) + self.format_map[rststyle] = format.decode('utf-8') + self.section_level = 0 + self.section_count = 0 + # Create ElementTree content and styles documents. + if WhichElementTree == 'lxml': + root = Element( + 'office:document-content', + nsmap=CONTENT_NAMESPACE_DICT, + ) + else: + root = Element( + 'office:document-content', + attrib=CONTENT_NAMESPACE_ATTRIB, + ) + self.content_tree = etree.ElementTree(element=root) + self.current_element = root + SubElement(root, 'office:scripts') + SubElement(root, 'office:font-face-decls') + el = SubElement(root, 'office:automatic-styles') + self.automatic_styles = el + el = SubElement(root, 'office:body') + el = self.generate_content_element(el) + self.current_element = el + self.body_text_element = el + self.paragraph_style_stack = [self.rststyle('textbody'), ] + self.list_style_stack = [] + self.table_count = 0 + self.column_count = ord('A') - 1 + self.trace_level = -1 + self.optiontablestyles_generated = False + self.field_name = None + self.field_element = None + self.title = None + self.image_count = 0 + self.image_style_count = 0 + self.image_dict = {} + self.embedded_file_list = [] + self.syntaxhighlighting = 1 + self.syntaxhighlight_lexer = 'python' + self.header_content = [] + self.footer_content = [] + self.in_header = False + self.in_footer = False + self.blockstyle = '' + self.in_table_of_contents = False + self.table_of_content_index_body = None + self.list_level = 0 + self.def_list_level = 0 + self.footnote_ref_dict = {} + self.footnote_list = [] + self.footnote_chars_idx = 0 + self.footnote_level = 0 + self.pending_ids = [ ] + self.in_paragraph = False + self.found_doc_title = False + self.bumped_list_level_stack = [] + self.meta_dict = {} + self.line_block_level = 0 + self.line_indent_level = 0 + self.citation_id = None + self.style_index = 0 # use to form unique style names + self.str_stylesheet = '' + self.str_stylesheetcontent = '' + self.dom_stylesheet = None + self.table_styles = None + self.in_citation = False + + + def get_str_stylesheet(self): + return self.str_stylesheet + + def retrieve_styles(self, extension): + """Retrieve the stylesheet from either a .xml file or from + a .odt (zip) file. Return the content as a string. + """ + s2 = None + stylespath = self.settings.stylesheet + ext = os.path.splitext(stylespath)[1] + if ext == '.xml': + stylesfile = open(stylespath, 'r') + s1 = stylesfile.read() + stylesfile.close() + elif ext == extension: + zfile = zipfile.ZipFile(stylespath, 'r') + s1 = zfile.read('styles.xml') + s2 = zfile.read('content.xml') + zfile.close() + else: + raise RuntimeError, 'stylesheet path (%s) must be %s or .xml file' %(stylespath, extension) + self.str_stylesheet = s1 + self.str_stylesheetcontent = s2 + self.dom_stylesheet = etree.fromstring(self.str_stylesheet) + self.dom_stylesheetcontent = etree.fromstring(self.str_stylesheetcontent) + self.table_styles = self.extract_table_styles(s2) + + def extract_table_styles(self, styles_str): + root = etree.fromstring(styles_str) + table_styles = {} + auto_styles = root.find( + '{%s}automatic-styles' % (CNSD['office'], )) + for stylenode in auto_styles: + name = stylenode.get('{%s}name' % (CNSD['style'], )) + tablename = name.split('.')[0] + family = stylenode.get('{%s}family' % (CNSD['style'], )) + if name.startswith(TABLESTYLEPREFIX): + tablestyle = table_styles.get(tablename) + if tablestyle is None: + tablestyle = TableStyle() + table_styles[tablename] = tablestyle + if family == 'table': + properties = stylenode.find( + '{%s}table-properties' % (CNSD['style'], )) + property = properties.get('{%s}%s' % (CNSD['fo'], + 'background-color', )) + if property is not None and property != 'none': + tablestyle.backgroundcolor = property + elif family == 'table-cell': + properties = stylenode.find( + '{%s}table-cell-properties' % (CNSD['style'], )) + if properties is not None: + border = self.get_property(properties) + if border is not None: + tablestyle.border = border + return table_styles + + def get_property(self, stylenode): + border = None + for propertyname in TABLEPROPERTYNAMES: + border = stylenode.get('{%s}%s' % (CNSD['fo'], propertyname, )) + if border is not None and border != 'none': + return border + return border + + def add_doc_title(self): + text = self.settings.title + if text: + self.title = text + if not self.found_doc_title: + el = Element('text:p', attrib = { + 'text:style-name': self.rststyle('title'), + }) + el.text = text + self.body_text_element.insert(0, el) + + def rststyle(self, name, parameters=( )): + """ + Returns the style name to use for the given style. + + If `parameters` is given `name` must contain a matching number of ``%`` and + is used as a format expression with `parameters` as the value. + """ + name1 = name % parameters + stylename = self.format_map.get(name1, 'rststyle-%s' % name1) + return stylename + + def generate_content_element(self, root): + return SubElement(root, 'office:text') + + def setup_page(self): + self.setup_paper(self.dom_stylesheet) + if (len(self.header_content) > 0 or len(self.footer_content) > 0 or + self.settings.custom_header or self.settings.custom_footer): + self.add_header_footer(self.dom_stylesheet) + new_content = etree.tostring(self.dom_stylesheet) + return new_content + + def setup_paper(self, root_el): + try: + fin = os.popen("paperconf -s 2> /dev/null") + w, h = map(float, fin.read().split()) + fin.close() + except: + w, h = 612, 792 # default to Letter + def walk(el): + if el.tag == "{%s}page-layout-properties" % SNSD["style"] and \ + not el.attrib.has_key("{%s}page-width" % SNSD["fo"]): + el.attrib["{%s}page-width" % SNSD["fo"]] = "%.3fpt" % w + el.attrib["{%s}page-height" % SNSD["fo"]] = "%.3fpt" % h + el.attrib["{%s}margin-left" % SNSD["fo"]] = \ + el.attrib["{%s}margin-right" % SNSD["fo"]] = \ + "%.3fpt" % (.1 * w) + el.attrib["{%s}margin-top" % SNSD["fo"]] = \ + el.attrib["{%s}margin-bottom" % SNSD["fo"]] = \ + "%.3fpt" % (.1 * h) + else: + for subel in el.getchildren(): walk(subel) + walk(root_el) + + def add_header_footer(self, root_el): + automatic_styles = root_el.find( + '{%s}automatic-styles' % SNSD['office']) + path = '{%s}master-styles' % (NAME_SPACE_1, ) + master_el = root_el.find(path) + if master_el is None: + return + path = '{%s}master-page' % (SNSD['style'], ) + master_el = master_el.find(path) + if master_el is None: + return + el1 = master_el + if self.header_content or self.settings.custom_header: + if WhichElementTree == 'lxml': + el2 = SubElement(el1, 'style:header', nsdict=SNSD) + else: + el2 = SubElement(el1, 'style:header', + attrib=STYLES_NAMESPACE_ATTRIB, + nsdict=STYLES_NAMESPACE_DICT, + ) + for el in self.header_content: + attrkey = add_ns('text:style-name', nsdict=SNSD) + el.attrib[attrkey] = self.rststyle('header') + el2.append(el) + if self.settings.custom_header: + elcustom = self.create_custom_headfoot(el2, + self.settings.custom_header, 'header', automatic_styles) + if self.footer_content or self.settings.custom_footer: + if WhichElementTree == 'lxml': + el2 = SubElement(el1, 'style:footer', nsdict=SNSD) + else: + el2 = SubElement(el1, 'style:footer', + attrib=STYLES_NAMESPACE_ATTRIB, + nsdict=STYLES_NAMESPACE_DICT, + ) + for el in self.footer_content: + attrkey = add_ns('text:style-name', nsdict=SNSD) + el.attrib[attrkey] = self.rststyle('footer') + el2.append(el) + if self.settings.custom_footer: + elcustom = self.create_custom_headfoot(el2, + self.settings.custom_footer, 'footer', automatic_styles) + + code_none, code_field, code_text = range(3) + field_pat = re.compile(r'%(..?)%') + + def create_custom_headfoot(self, parent, text, style_name, automatic_styles): + parent = SubElement(parent, 'text:p', attrib={ + 'text:style-name': self.rststyle(style_name), + }) + current_element = None + field_iter = self.split_field_specifiers_iter(text) + for item in field_iter: + if item[0] == ODFTranslator.code_field: + if item[1] not in ('p', 'P', + 't1', 't2', 't3', 't4', + 'd1', 'd2', 'd3', 'd4', 'd5', + 's', 't', 'a'): + msg = 'bad field spec: %%%s%%' % (item[1], ) + raise RuntimeError, msg + el1 = self.make_field_element(parent, + item[1], style_name, automatic_styles) + if el1 is None: + msg = 'bad field spec: %%%s%%' % (item[1], ) + raise RuntimeError, msg + else: + current_element = el1 + else: + if current_element is None: + parent.text = item[1] + else: + current_element.tail = item[1] + + def make_field_element(self, parent, text, style_name, automatic_styles): + if text == 'p': + el1 = SubElement(parent, 'text:page-number', attrib={ + #'text:style-name': self.rststyle(style_name), + 'text:select-page': 'current', + }) + elif text == 'P': + el1 = SubElement(parent, 'text:page-count', attrib={ + #'text:style-name': self.rststyle(style_name), + }) + elif text == 't1': + self.style_index += 1 + el1 = SubElement(parent, 'text:time', attrib={ + 'text:style-name': self.rststyle(style_name), + 'text:fixed': 'true', + 'style:data-style-name': 'rst-time-style-%d' % self.style_index, + }) + el2 = SubElement(automatic_styles, 'number:time-style', attrib={ + 'style:name': 'rst-time-style-%d' % self.style_index, + 'xmlns:number': SNSD['number'], + 'xmlns:style': SNSD['style'], + }) + el3 = SubElement(el2, 'number:hours', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = ':' + el3 = SubElement(el2, 'number:minutes', attrib={ + 'number:style': 'long', + }) + elif text == 't2': + self.style_index += 1 + el1 = SubElement(parent, 'text:time', attrib={ + 'text:style-name': self.rststyle(style_name), + 'text:fixed': 'true', + 'style:data-style-name': 'rst-time-style-%d' % self.style_index, + }) + el2 = SubElement(automatic_styles, 'number:time-style', attrib={ + 'style:name': 'rst-time-style-%d' % self.style_index, + 'xmlns:number': SNSD['number'], + 'xmlns:style': SNSD['style'], + }) + el3 = SubElement(el2, 'number:hours', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = ':' + el3 = SubElement(el2, 'number:minutes', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = ':' + el3 = SubElement(el2, 'number:seconds', attrib={ + 'number:style': 'long', + }) + elif text == 't3': + self.style_index += 1 + el1 = SubElement(parent, 'text:time', attrib={ + 'text:style-name': self.rststyle(style_name), + 'text:fixed': 'true', + 'style:data-style-name': 'rst-time-style-%d' % self.style_index, + }) + el2 = SubElement(automatic_styles, 'number:time-style', attrib={ + 'style:name': 'rst-time-style-%d' % self.style_index, + 'xmlns:number': SNSD['number'], + 'xmlns:style': SNSD['style'], + }) + el3 = SubElement(el2, 'number:hours', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = ':' + el3 = SubElement(el2, 'number:minutes', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = ' ' + el3 = SubElement(el2, 'number:am-pm') + elif text == 't4': + self.style_index += 1 + el1 = SubElement(parent, 'text:time', attrib={ + 'text:style-name': self.rststyle(style_name), + 'text:fixed': 'true', + 'style:data-style-name': 'rst-time-style-%d' % self.style_index, + }) + el2 = SubElement(automatic_styles, 'number:time-style', attrib={ + 'style:name': 'rst-time-style-%d' % self.style_index, + 'xmlns:number': SNSD['number'], + 'xmlns:style': SNSD['style'], + }) + el3 = SubElement(el2, 'number:hours', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = ':' + el3 = SubElement(el2, 'number:minutes', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = ':' + el3 = SubElement(el2, 'number:seconds', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = ' ' + el3 = SubElement(el2, 'number:am-pm') + elif text == 'd1': + self.style_index += 1 + el1 = SubElement(parent, 'text:date', attrib={ + 'text:style-name': self.rststyle(style_name), + 'style:data-style-name': 'rst-date-style-%d' % self.style_index, + }) + el2 = SubElement(automatic_styles, 'number:date-style', attrib={ + 'style:name': 'rst-date-style-%d' % self.style_index, + 'number:automatic-order': 'true', + 'xmlns:number': SNSD['number'], + 'xmlns:style': SNSD['style'], + }) + el3 = SubElement(el2, 'number:month', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = '/' + el3 = SubElement(el2, 'number:day', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = '/' + el3 = SubElement(el2, 'number:year') + elif text == 'd2': + self.style_index += 1 + el1 = SubElement(parent, 'text:date', attrib={ + 'text:style-name': self.rststyle(style_name), + 'style:data-style-name': 'rst-date-style-%d' % self.style_index, + }) + el2 = SubElement(automatic_styles, 'number:date-style', attrib={ + 'style:name': 'rst-date-style-%d' % self.style_index, + 'number:automatic-order': 'true', + 'xmlns:number': SNSD['number'], + 'xmlns:style': SNSD['style'], + }) + el3 = SubElement(el2, 'number:month', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = '/' + el3 = SubElement(el2, 'number:day', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = '/' + el3 = SubElement(el2, 'number:year', attrib={ + 'number:style': 'long', + }) + elif text == 'd3': + self.style_index += 1 + el1 = SubElement(parent, 'text:date', attrib={ + 'text:style-name': self.rststyle(style_name), + 'style:data-style-name': 'rst-date-style-%d' % self.style_index, + }) + el2 = SubElement(automatic_styles, 'number:date-style', attrib={ + 'style:name': 'rst-date-style-%d' % self.style_index, + 'number:automatic-order': 'true', + 'xmlns:number': SNSD['number'], + 'xmlns:style': SNSD['style'], + }) + el3 = SubElement(el2, 'number:month', attrib={ + 'number:textual': 'true', + }) + el3 = SubElement(el2, 'number:text') + el3.text = ' ' + el3 = SubElement(el2, 'number:day', attrib={ + }) + el3 = SubElement(el2, 'number:text') + el3.text = ', ' + el3 = SubElement(el2, 'number:year', attrib={ + 'number:style': 'long', + }) + elif text == 'd4': + self.style_index += 1 + el1 = SubElement(parent, 'text:date', attrib={ + 'text:style-name': self.rststyle(style_name), + 'style:data-style-name': 'rst-date-style-%d' % self.style_index, + }) + el2 = SubElement(automatic_styles, 'number:date-style', attrib={ + 'style:name': 'rst-date-style-%d' % self.style_index, + 'number:automatic-order': 'true', + 'xmlns:number': SNSD['number'], + 'xmlns:style': SNSD['style'], + }) + el3 = SubElement(el2, 'number:month', attrib={ + 'number:textual': 'true', + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = ' ' + el3 = SubElement(el2, 'number:day', attrib={ + }) + el3 = SubElement(el2, 'number:text') + el3.text = ', ' + el3 = SubElement(el2, 'number:year', attrib={ + 'number:style': 'long', + }) + elif text == 'd5': + self.style_index += 1 + el1 = SubElement(parent, 'text:date', attrib={ + 'text:style-name': self.rststyle(style_name), + 'style:data-style-name': 'rst-date-style-%d' % self.style_index, + }) + el2 = SubElement(automatic_styles, 'number:date-style', attrib={ + 'style:name': 'rst-date-style-%d' % self.style_index, + 'xmlns:number': SNSD['number'], + 'xmlns:style': SNSD['style'], + }) + el3 = SubElement(el2, 'number:year', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = '-' + el3 = SubElement(el2, 'number:month', attrib={ + 'number:style': 'long', + }) + el3 = SubElement(el2, 'number:text') + el3.text = '-' + el3 = SubElement(el2, 'number:day', attrib={ + 'number:style': 'long', + }) + elif text == 's': + el1 = SubElement(parent, 'text:subject', attrib={ + 'text:style-name': self.rststyle(style_name), + }) + elif text == 't': + el1 = SubElement(parent, 'text:title', attrib={ + 'text:style-name': self.rststyle(style_name), + }) + elif text == 'a': + el1 = SubElement(parent, 'text:author-name', attrib={ + 'text:fixed': 'false', + }) + else: + el1 = None + return el1 + + def split_field_specifiers_iter(self, text): + pos1 = 0 + pos_end = len(text) + while True: + mo = ODFTranslator.field_pat.search(text, pos1) + if mo: + pos2 = mo.start() + if pos2 > pos1: + yield (ODFTranslator.code_text, text[pos1:pos2]) + yield (ODFTranslator.code_field, mo.group(1)) + pos1 = mo.end() + else: + break + trailing = text[pos1:] + if trailing: + yield (ODFTranslator.code_text, trailing) + + + def astext(self): + root = self.content_tree.getroot() + et = etree.ElementTree(root) + s1 = ToString(et) + return s1 + + def content_astext(self): + return self.astext() + + def set_title(self, title): self.title = title + def get_title(self): return self.title + def set_embedded_file_list(self, embedded_file_list): + self.embedded_file_list = embedded_file_list + def get_embedded_file_list(self): return self.embedded_file_list + def get_meta_dict(self): return self.meta_dict + + def process_footnotes(self): + for node, el1 in self.footnote_list: + backrefs = node.attributes.get('backrefs', []) + first = True + for ref in backrefs: + el2 = self.footnote_ref_dict.get(ref) + if el2 is not None: + if first: + first = False + el3 = copy.deepcopy(el1) + el2.append(el3) + else: + children = el2.getchildren() + if len(children) > 0: # and 'id' in el2.attrib: + child = children[0] + ref1 = child.text + attribkey = add_ns('text:id', nsdict=SNSD) + id1 = el2.get(attribkey, 'footnote-error') + if id1 is None: + id1 = '' + tag = add_ns('text:note-ref', nsdict=SNSD) + el2.tag = tag + if self.settings.endnotes_end_doc: + note_class = 'endnote' + else: + note_class = 'footnote' + el2.attrib.clear() + attribkey = add_ns('text:note-class', nsdict=SNSD) + el2.attrib[attribkey] = note_class + attribkey = add_ns('text:ref-name', nsdict=SNSD) + el2.attrib[attribkey] = id1 + attribkey = add_ns('text:reference-format', nsdict=SNSD) + el2.attrib[attribkey] = 'page' + el2.text = ref1 + + # + # Utility methods + + def append_child(self, tag, attrib=None, parent=None): + if parent is None: + parent = self.current_element + if attrib is None: + el = SubElement(parent, tag) + else: + el = SubElement(parent, tag, attrib) + return el + + def append_p(self, style, text=None): + result = self.append_child('text:p', attrib={ + 'text:style-name': self.rststyle(style)}) + self.append_pending_ids(result) + if text is not None: + result.text = text + return result + + def append_pending_ids(self, el): + if self.settings.create_links: + for id in self.pending_ids: + SubElement(el, 'text:reference-mark', attrib={ + 'text:name': id}) + self.pending_ids = [ ] + + def set_current_element(self, el): + self.current_element = el + + def set_to_parent(self): + self.current_element = self.current_element.getparent() + + def generate_labeled_block(self, node, label): + label = '%s:' % (self.language.labels[label], ) + el = self.append_p('textbody') + el1 = SubElement(el, 'text:span', + attrib={'text:style-name': self.rststyle('strong')}) + el1.text = label + el = self.append_p('blockindent') + return el + + def generate_labeled_line(self, node, label): + label = '%s:' % (self.language.labels[label], ) + el = self.append_p('textbody') + el1 = SubElement(el, 'text:span', + attrib={'text:style-name': self.rststyle('strong')}) + el1.text = label + el1.tail = node.astext() + return el + + def encode(self, text): + text = text.replace(u'\u00a0', " ") + return text + + # + # Visitor functions + # + # In alphabetic order, more or less. + # See docutils.docutils.nodes.node_class_names. + # + + def dispatch_visit(self, node): + """Override to catch basic attributes which many nodes have.""" + self.handle_basic_atts(node) + nodes.GenericNodeVisitor.dispatch_visit(self, node) + + def handle_basic_atts(self, node): + if isinstance(node, nodes.Element) and node['ids']: + self.pending_ids += node['ids'] + + def default_visit(self, node): + self.document.reporter.warning('missing visit_%s' % (node.tagname, )) + + def default_departure(self, node): + self.document.reporter.warning('missing depart_%s' % (node.tagname, )) + +## def add_text_to_element(self, text): +## # Are we in a citation. If so, add text to current element, not +## # to children. +## # Are we in mixed content? If so, add the text to the +## # etree tail of the previous sibling element. +## if not self.in_citation and len(self.current_element.getchildren()) > 0: +## if self.current_element.getchildren()[-1].tail: +## self.current_element.getchildren()[-1].tail += text +## else: +## self.current_element.getchildren()[-1].tail = text +## else: +## if self.current_element.text: +## self.current_element.text += text +## else: +## self.current_element.text = text +## +## def visit_Text(self, node): +## # Skip nodes whose text has been processed in parent nodes. +## if isinstance(node.parent, docutils.nodes.literal_block): +## return +## text = node.astext() +## self.add_text_to_element(text) + + def visit_Text(self, node): + # Skip nodes whose text has been processed in parent nodes. + if isinstance(node.parent, docutils.nodes.literal_block): + return + text = node.astext() + # Are we in mixed content? If so, add the text to the + # etree tail of the previous sibling element. + if len(self.current_element.getchildren()) > 0: + if self.current_element.getchildren()[-1].tail: + self.current_element.getchildren()[-1].tail += text + else: + self.current_element.getchildren()[-1].tail = text + else: + if self.current_element.text: + self.current_element.text += text + else: + self.current_element.text = text + + def depart_Text(self, node): + pass + + # + # Pre-defined fields + # + + def visit_address(self, node): + el = self.generate_labeled_block(node, 'address') + self.set_current_element(el) + + def depart_address(self, node): + self.set_to_parent() + + def visit_author(self, node): + if isinstance(node.parent, nodes.authors): + el = self.append_p('blockindent') + else: + el = self.generate_labeled_block(node, 'author') + self.set_current_element(el) + + def depart_author(self, node): + self.set_to_parent() + + def visit_authors(self, node): + label = '%s:' % (self.language.labels['authors'], ) + el = self.append_p('textbody') + el1 = SubElement(el, 'text:span', + attrib={'text:style-name': self.rststyle('strong')}) + el1.text = label + + def depart_authors(self, node): + pass + + def visit_contact(self, node): + el = self.generate_labeled_block(node, 'contact') + self.set_current_element(el) + + def depart_contact(self, node): + self.set_to_parent() + + def visit_copyright(self, node): + el = self.generate_labeled_block(node, 'copyright') + self.set_current_element(el) + + def depart_copyright(self, node): + self.set_to_parent() + + def visit_date(self, node): + self.generate_labeled_line(node, 'date') + + def depart_date(self, node): + pass + + def visit_organization(self, node): + el = self.generate_labeled_block(node, 'organization') + self.set_current_element(el) + + def depart_organization(self, node): + self.set_to_parent() + + def visit_status(self, node): + el = self.generate_labeled_block(node, 'status') + self.set_current_element(el) + + def depart_status(self, node): + self.set_to_parent() + + def visit_revision(self, node): + el = self.generate_labeled_line(node, 'revision') + + def depart_revision(self, node): + pass + + def visit_version(self, node): + el = self.generate_labeled_line(node, 'version') + #self.set_current_element(el) + + def depart_version(self, node): + #self.set_to_parent() + pass + + def visit_attribution(self, node): + el = self.append_p('attribution', node.astext()) + + def depart_attribution(self, node): + pass + + def visit_block_quote(self, node): + if 'epigraph' in node.attributes['classes']: + self.paragraph_style_stack.append(self.rststyle('epigraph')) + self.blockstyle = self.rststyle('epigraph') + elif 'highlights' in node.attributes['classes']: + self.paragraph_style_stack.append(self.rststyle('highlights')) + self.blockstyle = self.rststyle('highlights') + else: + self.paragraph_style_stack.append(self.rststyle('blockquote')) + self.blockstyle = self.rststyle('blockquote') + self.line_indent_level += 1 + + def depart_block_quote(self, node): + self.paragraph_style_stack.pop() + self.blockstyle = '' + self.line_indent_level -= 1 + + def visit_bullet_list(self, node): + self.list_level +=1 + if self.in_table_of_contents: + if self.settings.generate_oowriter_toc: + pass + else: + if node.has_key('classes') and \ + 'auto-toc' in node.attributes['classes']: + el = SubElement(self.current_element, 'text:list', attrib={ + 'text:style-name': self.rststyle('tocenumlist'), + }) + self.list_style_stack.append(self.rststyle('enumitem')) + else: + el = SubElement(self.current_element, 'text:list', attrib={ + 'text:style-name': self.rststyle('tocbulletlist'), + }) + self.list_style_stack.append(self.rststyle('bulletitem')) + self.set_current_element(el) + else: + if self.blockstyle == self.rststyle('blockquote'): + el = SubElement(self.current_element, 'text:list', attrib={ + 'text:style-name': self.rststyle('blockquote-bulletlist'), + }) + self.list_style_stack.append( + self.rststyle('blockquote-bulletitem')) + elif self.blockstyle == self.rststyle('highlights'): + el = SubElement(self.current_element, 'text:list', attrib={ + 'text:style-name': self.rststyle('highlights-bulletlist'), + }) + self.list_style_stack.append( + self.rststyle('highlights-bulletitem')) + elif self.blockstyle == self.rststyle('epigraph'): + el = SubElement(self.current_element, 'text:list', attrib={ + 'text:style-name': self.rststyle('epigraph-bulletlist'), + }) + self.list_style_stack.append( + self.rststyle('epigraph-bulletitem')) + else: + el = SubElement(self.current_element, 'text:list', attrib={ + 'text:style-name': self.rststyle('bulletlist'), + }) + self.list_style_stack.append(self.rststyle('bulletitem')) + self.set_current_element(el) + + def depart_bullet_list(self, node): + if self.in_table_of_contents: + if self.settings.generate_oowriter_toc: + pass + else: + self.set_to_parent() + self.list_style_stack.pop() + else: + self.set_to_parent() + self.list_style_stack.pop() + self.list_level -=1 + + def visit_caption(self, node): + raise nodes.SkipChildren() + pass + + def depart_caption(self, node): + pass + + def visit_comment(self, node): + el = self.append_p('textbody') + el1 = SubElement(el, 'office:annotation', attrib={}) + el2 = SubElement(el1, 'dc:creator', attrib={}) + s1 = os.environ.get('USER', '') + el2.text = s1 + el2 = SubElement(el1, 'text:p', attrib={}) + el2.text = node.astext() + + def depart_comment(self, node): + pass + + def visit_compound(self, node): + # The compound directive currently receives no special treatment. + pass + + def depart_compound(self, node): + pass + + def visit_container(self, node): + styles = node.attributes.get('classes', ()) + if len(styles) > 0: + self.paragraph_style_stack.append(self.rststyle(styles[0])) + + def depart_container(self, node): + styles = node.attributes.get('classes', ()) + if len(styles) > 0: + self.paragraph_style_stack.pop() + + def visit_decoration(self, node): + pass + + def depart_decoration(self, node): + pass + + def visit_definition_list(self, node): + self.def_list_level +=1 + if self.list_level > 5: + raise RuntimeError( + 'max definition list nesting level exceeded') + + def depart_definition_list(self, node): + self.def_list_level -=1 + + def visit_definition_list_item(self, node): + pass + + def depart_definition_list_item(self, node): + pass + + def visit_term(self, node): + el = self.append_p('deflist-term-%d' % self.def_list_level) + el.text = node.astext() + self.set_current_element(el) + raise nodes.SkipChildren() + + def depart_term(self, node): + self.set_to_parent() + + def visit_definition(self, node): + self.paragraph_style_stack.append( + self.rststyle('deflist-def-%d' % self.def_list_level)) + self.bumped_list_level_stack.append(ListLevel(1)) + + def depart_definition(self, node): + self.paragraph_style_stack.pop() + self.bumped_list_level_stack.pop() + + def visit_classifier(self, node): + els = self.current_element.getchildren() + if len(els) > 0: + el = els[-1] + el1 = SubElement(el, 'text:span', + attrib={'text:style-name': self.rststyle('emphasis') + }) + el1.text = ' (%s)' % (node.astext(), ) + + def depart_classifier(self, node): + pass + + def visit_document(self, node): + pass + + def depart_document(self, node): + self.process_footnotes() + + def visit_docinfo(self, node): + self.section_level += 1 + self.section_count += 1 + if self.settings.create_sections: + el = self.append_child('text:section', attrib={ + 'text:name': 'Section%d' % self.section_count, + 'text:style-name': 'Sect%d' % self.section_level, + }) + self.set_current_element(el) + + def depart_docinfo(self, node): + self.section_level -= 1 + if self.settings.create_sections: + self.set_to_parent() + + def visit_emphasis(self, node): + el = SubElement(self.current_element, 'text:span', + attrib={'text:style-name': self.rststyle('emphasis')}) + self.set_current_element(el) + + def depart_emphasis(self, node): + self.set_to_parent() + + def visit_enumerated_list(self, node): + el1 = self.current_element + if self.blockstyle == self.rststyle('blockquote'): + el2 = SubElement(el1, 'text:list', attrib={ + 'text:style-name': self.rststyle('blockquote-enumlist'), + }) + self.list_style_stack.append(self.rststyle('blockquote-enumitem')) + elif self.blockstyle == self.rststyle('highlights'): + el2 = SubElement(el1, 'text:list', attrib={ + 'text:style-name': self.rststyle('highlights-enumlist'), + }) + self.list_style_stack.append(self.rststyle('highlights-enumitem')) + elif self.blockstyle == self.rststyle('epigraph'): + el2 = SubElement(el1, 'text:list', attrib={ + 'text:style-name': self.rststyle('epigraph-enumlist'), + }) + self.list_style_stack.append(self.rststyle('epigraph-enumitem')) + else: + liststylename = 'enumlist-%s' % (node.get('enumtype', 'arabic'), ) + el2 = SubElement(el1, 'text:list', attrib={ + 'text:style-name': self.rststyle(liststylename), + }) + self.list_style_stack.append(self.rststyle('enumitem')) + self.set_current_element(el2) + + def depart_enumerated_list(self, node): + self.set_to_parent() + self.list_style_stack.pop() + + def visit_list_item(self, node): + # If we are in a "bumped" list level, then wrap this + # list in an outer lists in order to increase the + # indentation level. + if self.in_table_of_contents: + if self.settings.generate_oowriter_toc: + self.paragraph_style_stack.append( + self.rststyle('contents-%d' % (self.list_level, ))) + else: + el1 = self.append_child('text:list-item') + self.set_current_element(el1) + else: + el1 = self.append_child('text:list-item') + el3 = el1 + if len(self.bumped_list_level_stack) > 0: + level_obj = self.bumped_list_level_stack[-1] + if level_obj.get_sibling(): + level_obj.set_nested(False) + for level_obj1 in self.bumped_list_level_stack: + for idx in range(level_obj1.get_level()): + el2 = self.append_child('text:list', parent=el3) + el3 = self.append_child( + 'text:list-item', parent=el2) + self.paragraph_style_stack.append(self.list_style_stack[-1]) + self.set_current_element(el3) + + def depart_list_item(self, node): + if self.in_table_of_contents: + if self.settings.generate_oowriter_toc: + self.paragraph_style_stack.pop() + else: + self.set_to_parent() + else: + if len(self.bumped_list_level_stack) > 0: + level_obj = self.bumped_list_level_stack[-1] + if level_obj.get_sibling(): + level_obj.set_nested(True) + for level_obj1 in self.bumped_list_level_stack: + for idx in range(level_obj1.get_level()): + self.set_to_parent() + self.set_to_parent() + self.paragraph_style_stack.pop() + self.set_to_parent() + + def visit_header(self, node): + self.in_header = True + + def depart_header(self, node): + self.in_header = False + + def visit_footer(self, node): + self.in_footer = True + + def depart_footer(self, node): + self.in_footer = False + + def visit_field(self, node): + pass + + def depart_field(self, node): + pass + + def visit_field_list(self, node): + pass + + def depart_field_list(self, node): + pass + + def visit_field_name(self, node): + el = self.append_p('textbody') + el1 = SubElement(el, 'text:span', + attrib={'text:style-name': self.rststyle('strong')}) + el1.text = node.astext() + + def depart_field_name(self, node): + pass + + def visit_field_body(self, node): + self.paragraph_style_stack.append(self.rststyle('blockindent')) + + def depart_field_body(self, node): + self.paragraph_style_stack.pop() + + def visit_figure(self, node): + pass + + def depart_figure(self, node): + pass + + def visit_footnote(self, node): + self.footnote_level += 1 + self.save_footnote_current = self.current_element + el1 = Element('text:note-body') + self.current_element = el1 + self.footnote_list.append((node, el1)) + if isinstance(node, docutils.nodes.citation): + self.paragraph_style_stack.append(self.rststyle('citation')) + else: + self.paragraph_style_stack.append(self.rststyle('footnote')) + + def depart_footnote(self, node): + self.paragraph_style_stack.pop() + self.current_element = self.save_footnote_current + self.footnote_level -= 1 + + footnote_chars = [ + '*', '**', '***', + '++', '+++', + '##', '###', + '@@', '@@@', + ] + + def visit_footnote_reference(self, node): + if self.footnote_level <= 0: + id = node.attributes['ids'][0] + refid = node.attributes.get('refid') + if refid is None: + refid = '' + if self.settings.endnotes_end_doc: + note_class = 'endnote' + else: + note_class = 'footnote' + el1 = self.append_child('text:note', attrib={ + 'text:id': '%s' % (refid, ), + 'text:note-class': note_class, + }) + note_auto = str(node.attributes.get('auto', 1)) + if isinstance(node, docutils.nodes.citation_reference): + citation = '[%s]' % node.astext() + el2 = SubElement(el1, 'text:note-citation', attrib={ + 'text:label': citation, + }) + el2.text = citation + elif note_auto == '1': + el2 = SubElement(el1, 'text:note-citation', attrib={ + 'text:label': node.astext(), + }) + el2.text = node.astext() + elif note_auto == '*': + if self.footnote_chars_idx >= len( + ODFTranslator.footnote_chars): + self.footnote_chars_idx = 0 + footnote_char = ODFTranslator.footnote_chars[ + self.footnote_chars_idx] + self.footnote_chars_idx += 1 + el2 = SubElement(el1, 'text:note-citation', attrib={ + 'text:label': footnote_char, + }) + el2.text = footnote_char + self.footnote_ref_dict[id] = el1 + raise nodes.SkipChildren() + + def depart_footnote_reference(self, node): + pass + + def visit_citation(self, node): + self.in_citation = True + for id in node.attributes['ids']: + self.citation_id = id + break + self.paragraph_style_stack.append(self.rststyle('blockindent')) + self.bumped_list_level_stack.append(ListLevel(1)) + + def depart_citation(self, node): + self.citation_id = None + self.paragraph_style_stack.pop() + self.bumped_list_level_stack.pop() + self.in_citation = False + + def visit_citation_reference(self, node): + if self.settings.create_links: + id = node.attributes['refid'] + el = self.append_child('text:reference-ref', attrib={ + 'text:ref-name': '%s' % (id, ), + 'text:reference-format': 'text', + }) + el.text = '[' + self.set_current_element(el) + elif self.current_element.text is None: + self.current_element.text = '[' + else: + self.current_element.text += '[' + + def depart_citation_reference(self, node): + self.current_element.text += ']' + if self.settings.create_links: + self.set_to_parent() + + def visit_label(self, node): + if isinstance(node.parent, docutils.nodes.footnote): + raise nodes.SkipChildren() + elif self.citation_id is not None: + el = self.append_p('textbody') + self.set_current_element(el) + el.text = '[' + if self.settings.create_links: + el1 = self.append_child('text:reference-mark-start', attrib={ + 'text:name': '%s' % (self.citation_id, ), + }) + + def depart_label(self, node): + if isinstance(node.parent, docutils.nodes.footnote): + pass + elif self.citation_id is not None: + self.current_element.text += ']' + if self.settings.create_links: + el = self.append_child('text:reference-mark-end', attrib={ + 'text:name': '%s' % (self.citation_id, ), + }) + self.set_to_parent() + + def visit_generated(self, node): + pass + + def depart_generated(self, node): + pass + + def check_file_exists(self, path): + if os.path.exists(path): + return 1 + else: + return 0 + + def visit_image(self, node): + # Capture the image file. + if 'uri' in node.attributes: + source = node.attributes['uri'] + if not source.startswith('http:'): + if not source.startswith(os.sep): + docsource, line = utils.get_source_line(node) + if docsource: + dirname = os.path.dirname(docsource) + if dirname: + source = '%s%s%s' % (dirname, os.sep, source, ) + if not self.check_file_exists(source): + self.document.reporter.warning( + 'Cannot find image file %s.' % (source, )) + return + else: + return + if source in self.image_dict: + filename, destination = self.image_dict[source] + else: + self.image_count += 1 + filename = os.path.split(source)[1] + destination = 'Pictures/1%08x%s' % (self.image_count, filename, ) + if source.startswith('http:'): + try: + imgfile = urllib2.urlopen(source) + content = imgfile.read() + imgfile.close() + imgfile2 = tempfile.NamedTemporaryFile('wb', delete=False) + imgfile2.write(content) + imgfile2.close() + imgfilename = imgfile2.name + source = imgfilename + except urllib2.HTTPError, e: + self.document.reporter.warning( + "Can't open image url %s." % (source, )) + spec = (source, destination,) + else: + spec = (os.path.abspath(source), destination,) + self.embedded_file_list.append(spec) + self.image_dict[source] = (source, destination,) + # Is this a figure (containing an image) or just a plain image? + if self.in_paragraph: + el1 = self.current_element + else: + el1 = SubElement(self.current_element, 'text:p', + attrib={'text:style-name': self.rststyle('textbody')}) + el2 = el1 + if isinstance(node.parent, docutils.nodes.figure): + el3, el4, el5, caption = self.generate_figure(node, source, + destination, el2) + attrib = {} + el6, width = self.generate_image(node, source, destination, + el5, attrib) + if caption is not None: + el6.tail = caption + else: #if isinstance(node.parent, docutils.nodes.image): + el3 = self.generate_image(node, source, destination, el2) + + def depart_image(self, node): + pass + + def get_image_width_height(self, node, attr): + size = None + if attr in node.attributes: + size = node.attributes[attr] + unit = size[-2:] + if unit.isalpha(): + size = size[:-2] + else: + unit = 'px' + try: + size = float(size) + except ValueError, e: + self.document.reporter.warning( + 'Invalid %s for image: "%s"' % ( + attr, node.attributes[attr])) + size = [size, unit] + return size + + def get_image_scale(self, node): + if 'scale' in node.attributes: + try: + scale = int(node.attributes['scale']) + if scale < 1: # or scale > 100: + self.document.reporter.warning( + 'scale out of range (%s), using 1.' % (scale, )) + scale = 1 + scale = scale * 0.01 + except ValueError, e: + self.document.reporter.warning( + 'Invalid scale for image: "%s"' % ( + node.attributes['scale'], )) + else: + scale = 1.0 + return scale + + def get_image_scaled_width_height(self, node, source): + scale = self.get_image_scale(node) + width = self.get_image_width_height(node, 'width') + height = self.get_image_width_height(node, 'height') + + dpi = (72, 72) + if PIL is not None and source in self.image_dict: + filename, destination = self.image_dict[source] + imageobj = PIL.Image.open(filename, 'r') + dpi = imageobj.info.get('dpi', dpi) + # dpi information can be (xdpi, ydpi) or xydpi + try: iter(dpi) + except: dpi = (dpi, dpi) + else: + imageobj = None + + if width is None or height is None: + if imageobj is None: + raise RuntimeError( + 'image size not fully specified and PIL not installed') + if width is None: width = [imageobj.size[0], 'px'] + if height is None: height = [imageobj.size[1], 'px'] + + width[0] *= scale + height[0] *= scale + if width[1] == 'px': width = [width[0] / dpi[0], 'in'] + if height[1] == 'px': height = [height[0] / dpi[1], 'in'] + + width[0] = str(width[0]) + height[0] = str(height[0]) + return ''.join(width), ''.join(height) + + def generate_figure(self, node, source, destination, current_element): + caption = None + width, height = self.get_image_scaled_width_height(node, source) + for node1 in node.parent.children: + if node1.tagname == 'caption': + caption = node1.astext() + self.image_style_count += 1 + # + # Add the style for the caption. + if caption is not None: + attrib = { + 'style:class': 'extra', + 'style:family': 'paragraph', + 'style:name': 'Caption', + 'style:parent-style-name': 'Standard', + } + el1 = SubElement(self.automatic_styles, 'style:style', + attrib=attrib, nsdict=SNSD) + attrib = { + 'fo:margin-bottom': '0.0835in', + 'fo:margin-top': '0.0835in', + 'text:line-number': '0', + 'text:number-lines': 'false', + } + el2 = SubElement(el1, 'style:paragraph-properties', + attrib=attrib, nsdict=SNSD) + attrib = { + 'fo:font-size': '12pt', + 'fo:font-style': 'italic', + 'style:font-name': 'Times', + 'style:font-name-complex': 'Lucidasans1', + 'style:font-size-asian': '12pt', + 'style:font-size-complex': '12pt', + 'style:font-style-asian': 'italic', + 'style:font-style-complex': 'italic', + } + el2 = SubElement(el1, 'style:text-properties', + attrib=attrib, nsdict=SNSD) + style_name = 'rstframestyle%d' % self.image_style_count + # Add the styles + attrib = { + 'style:name': style_name, + 'style:family': 'graphic', + 'style:parent-style-name': self.rststyle('figureframe'), + } + el1 = SubElement(self.automatic_styles, + 'style:style', attrib=attrib, nsdict=SNSD) + halign = 'center' + valign = 'top' + if 'align' in node.attributes: + align = node.attributes['align'].split() + for val in align: + if val in ('left', 'center', 'right'): + halign = val + elif val in ('top', 'middle', 'bottom'): + valign = val + attrib = {} + wrap = False + classes = node.parent.attributes.get('classes') + if classes and 'wrap' in classes: + wrap = True + if wrap: + attrib['style:wrap'] = 'dynamic' + else: + attrib['style:wrap'] = 'none' + el2 = SubElement(el1, + 'style:graphic-properties', attrib=attrib, nsdict=SNSD) + attrib = { + 'draw:style-name': style_name, + 'draw:name': 'Frame1', + 'text:anchor-type': 'paragraph', + 'draw:z-index': '0', + } + attrib['svg:width'] = width + # dbg + #attrib['svg:height'] = height + el3 = SubElement(current_element, 'draw:frame', attrib=attrib) + attrib = {} + el4 = SubElement(el3, 'draw:text-box', attrib=attrib) + attrib = { + 'text:style-name': self.rststyle('caption'), + } + el5 = SubElement(el4, 'text:p', attrib=attrib) + return el3, el4, el5, caption + + def generate_image(self, node, source, destination, current_element, + frame_attrs=None): + width, height = self.get_image_scaled_width_height(node, source) + self.image_style_count += 1 + style_name = 'rstframestyle%d' % self.image_style_count + # Add the style. + attrib = { + 'style:name': style_name, + 'style:family': 'graphic', + 'style:parent-style-name': self.rststyle('image'), + } + el1 = SubElement(self.automatic_styles, + 'style:style', attrib=attrib, nsdict=SNSD) + halign = None + valign = None + if 'align' in node.attributes: + align = node.attributes['align'].split() + for val in align: + if val in ('left', 'center', 'right'): + halign = val + elif val in ('top', 'middle', 'bottom'): + valign = val + if frame_attrs is None: + attrib = { + 'style:vertical-pos': 'top', + 'style:vertical-rel': 'paragraph', + 'style:horizontal-rel': 'paragraph', + 'style:mirror': 'none', + 'fo:clip': 'rect(0cm 0cm 0cm 0cm)', + 'draw:luminance': '0%', + 'draw:contrast': '0%', + 'draw:red': '0%', + 'draw:green': '0%', + 'draw:blue': '0%', + 'draw:gamma': '100%', + 'draw:color-inversion': 'false', + 'draw:image-opacity': '100%', + 'draw:color-mode': 'standard', + } + else: + attrib = frame_attrs + if halign is not None: + attrib['style:horizontal-pos'] = halign + if valign is not None: + attrib['style:vertical-pos'] = valign + # If there is a classes/wrap directive or we are + # inside a table, add a no-wrap style. + wrap = False + classes = node.attributes.get('classes') + if classes and 'wrap' in classes: + wrap = True + if wrap: + attrib['style:wrap'] = 'dynamic' + else: + attrib['style:wrap'] = 'none' + # If we are inside a table, add a no-wrap style. + if self.is_in_table(node): + attrib['style:wrap'] = 'none' + el2 = SubElement(el1, + 'style:graphic-properties', attrib=attrib, nsdict=SNSD) + # Add the content. + #el = SubElement(current_element, 'text:p', + # attrib={'text:style-name': self.rststyle('textbody')}) + attrib={ + 'draw:style-name': style_name, + 'draw:name': 'graphics2', + 'draw:z-index': '1', + } + if isinstance(node.parent, nodes.TextElement): + attrib['text:anchor-type'] = 'as-char' #vds + else: + attrib['text:anchor-type'] = 'paragraph' + attrib['svg:width'] = width + attrib['svg:height'] = height + el1 = SubElement(current_element, 'draw:frame', attrib=attrib) + el2 = SubElement(el1, 'draw:image', attrib={ + 'xlink:href': '%s' % (destination, ), + 'xlink:type': 'simple', + 'xlink:show': 'embed', + 'xlink:actuate': 'onLoad', + }) + return el1, width + + def is_in_table(self, node): + node1 = node.parent + while node1: + if isinstance(node1, docutils.nodes.entry): + return True + node1 = node1.parent + return False + + def visit_legend(self, node): + if isinstance(node.parent, docutils.nodes.figure): + el1 = self.current_element[-1] + el1 = el1[0][0] + self.current_element = el1 + self.paragraph_style_stack.append(self.rststyle('legend')) + + def depart_legend(self, node): + if isinstance(node.parent, docutils.nodes.figure): + self.paragraph_style_stack.pop() + self.set_to_parent() + self.set_to_parent() + self.set_to_parent() + + def visit_line_block(self, node): + self.line_indent_level += 1 + self.line_block_level += 1 + + def depart_line_block(self, node): + self.line_indent_level -= 1 + self.line_block_level -= 1 + + def visit_line(self, node): + style = 'lineblock%d' % self.line_indent_level + el1 = SubElement(self.current_element, 'text:p', attrib={ + 'text:style-name': self.rststyle(style), + }) + self.current_element = el1 + + def depart_line(self, node): + self.set_to_parent() + + def visit_literal(self, node): + el = SubElement(self.current_element, 'text:span', + attrib={'text:style-name': self.rststyle('inlineliteral')}) + self.set_current_element(el) + + def depart_literal(self, node): + self.set_to_parent() + + def visit_inline(self, node): + styles = node.attributes.get('classes', ()) + if len(styles) > 0: + inline_style = styles[0] + el = SubElement(self.current_element, 'text:span', + attrib={'text:style-name': self.rststyle(inline_style)}) + self.set_current_element(el) + + def depart_inline(self, node): + self.set_to_parent() + + def _calculate_code_block_padding(self, line): + count = 0 + matchobj = SPACES_PATTERN.match(line) + if matchobj: + pad = matchobj.group() + count = len(pad) + else: + matchobj = TABS_PATTERN.match(line) + if matchobj: + pad = matchobj.group() + count = len(pad) * 8 + return count + + def _add_syntax_highlighting(self, insource, language): + lexer = pygments.lexers.get_lexer_by_name(language, stripall=True) + if language in ('latex', 'tex'): + fmtr = OdtPygmentsLaTeXFormatter(lambda name, parameters=(): + self.rststyle(name, parameters), + escape_function=escape_cdata) + else: + fmtr = OdtPygmentsProgFormatter(lambda name, parameters=(): + self.rststyle(name, parameters), + escape_function=escape_cdata) + outsource = pygments.highlight(insource, lexer, fmtr) + return outsource + + def fill_line(self, line): + line = FILL_PAT1.sub(self.fill_func1, line) + line = FILL_PAT2.sub(self.fill_func2, line) + return line + + def fill_func1(self, matchobj): + spaces = matchobj.group(0) + repl = '' % (len(spaces), ) + return repl + + def fill_func2(self, matchobj): + spaces = matchobj.group(0) + repl = ' ' % (len(spaces) - 1, ) + return repl + + def visit_literal_block(self, node): + if len(self.paragraph_style_stack) > 1: + wrapper1 = '%%s' % ( + self.rststyle('codeblock-indented'), ) + else: + wrapper1 = '%%s' % ( + self.rststyle('codeblock'), ) + source = node.astext() + if (pygments and + self.settings.add_syntax_highlighting + #and + #node.get('hilight', False) + ): + language = node.get('language', 'python') + source = self._add_syntax_highlighting(source, language) + else: + source = escape_cdata(source) + lines = source.split('\n') + # If there is an empty last line, remove it. + if lines[-1] == '': + del lines[-1] + lines1 = [''] + + my_lines = [] + for my_line in lines: + my_line = self.fill_line(my_line) + my_line = my_line.replace(" ", "\n") + my_lines.append(my_line) + my_lines_str = ''.join(my_lines) + my_lines_str2 = wrapper1 % (my_lines_str, ) + lines1.append(my_lines_str2) + lines1.append('') + s1 = ''.join(lines1) + if WhichElementTree != "lxml": + s1 = s1.encode("utf-8") + el1 = etree.fromstring(s1) + children = el1.getchildren() + for child in children: + self.current_element.append(child) + + def depart_literal_block(self, node): + pass + + visit_doctest_block = visit_literal_block + depart_doctest_block = depart_literal_block + + # placeholder for math (see docs/dev/todo.txt) + def visit_math(self, node): + self.document.reporter.warning('"math" role not supported', + base_node=node) + self.visit_literal(node) + + def depart_math(self, node): + self.depart_literal(node) + + def visit_math_block(self, node): + self.document.reporter.warning('"math" directive not supported', + base_node=node) + self.visit_literal_block(node) + + def depart_math_block(self, node): + self.depart_literal_block(node) + + def visit_meta(self, node): + name = node.attributes.get('name') + content = node.attributes.get('content') + if name is not None and content is not None: + self.meta_dict[name] = content + + def depart_meta(self, node): + pass + + def visit_option_list(self, node): + table_name = 'tableoption' + # + # Generate automatic styles + if not self.optiontablestyles_generated: + self.optiontablestyles_generated = True + el = SubElement(self.automatic_styles, 'style:style', attrib={ + 'style:name': self.rststyle(table_name), + 'style:family': 'table'}, nsdict=SNSD) + el1 = SubElement(el, 'style:table-properties', attrib={ + 'style:width': '17.59cm', + 'table:align': 'left', + 'style:shadow': 'none'}, nsdict=SNSD) + el = SubElement(self.automatic_styles, 'style:style', attrib={ + 'style:name': self.rststyle('%s.%%c' % table_name, ( 'A', )), + 'style:family': 'table-column'}, nsdict=SNSD) + el1 = SubElement(el, 'style:table-column-properties', attrib={ + 'style:column-width': '4.999cm'}, nsdict=SNSD) + el = SubElement(self.automatic_styles, 'style:style', attrib={ + 'style:name': self.rststyle('%s.%%c' % table_name, ( 'B', )), + 'style:family': 'table-column'}, nsdict=SNSD) + el1 = SubElement(el, 'style:table-column-properties', attrib={ + 'style:column-width': '12.587cm'}, nsdict=SNSD) + el = SubElement(self.automatic_styles, 'style:style', attrib={ + 'style:name': self.rststyle( + '%s.%%c%%d' % table_name, ( 'A', 1, )), + 'style:family': 'table-cell'}, nsdict=SNSD) + el1 = SubElement(el, 'style:table-cell-properties', attrib={ + 'fo:background-color': 'transparent', + 'fo:padding': '0.097cm', + 'fo:border-left': '0.035cm solid #000000', + 'fo:border-right': 'none', + 'fo:border-top': '0.035cm solid #000000', + 'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD) + el2 = SubElement(el1, 'style:background-image', nsdict=SNSD) + el = SubElement(self.automatic_styles, 'style:style', attrib={ + 'style:name': self.rststyle( + '%s.%%c%%d' % table_name, ( 'B', 1, )), + 'style:family': 'table-cell'}, nsdict=SNSD) + el1 = SubElement(el, 'style:table-cell-properties', attrib={ + 'fo:padding': '0.097cm', + 'fo:border': '0.035cm solid #000000'}, nsdict=SNSD) + el = SubElement(self.automatic_styles, 'style:style', attrib={ + 'style:name': self.rststyle( + '%s.%%c%%d' % table_name, ( 'A', 2, )), + 'style:family': 'table-cell'}, nsdict=SNSD) + el1 = SubElement(el, 'style:table-cell-properties', attrib={ + 'fo:padding': '0.097cm', + 'fo:border-left': '0.035cm solid #000000', + 'fo:border-right': 'none', + 'fo:border-top': 'none', + 'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD) + el = SubElement(self.automatic_styles, 'style:style', attrib={ + 'style:name': self.rststyle( + '%s.%%c%%d' % table_name, ( 'B', 2, )), + 'style:family': 'table-cell'}, nsdict=SNSD) + el1 = SubElement(el, 'style:table-cell-properties', attrib={ + 'fo:padding': '0.097cm', + 'fo:border-left': '0.035cm solid #000000', + 'fo:border-right': '0.035cm solid #000000', + 'fo:border-top': 'none', + 'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD) + # + # Generate table data + el = self.append_child('table:table', attrib={ + 'table:name': self.rststyle(table_name), + 'table:style-name': self.rststyle(table_name), + }) + el1 = SubElement(el, 'table:table-column', attrib={ + 'table:style-name': self.rststyle( + '%s.%%c' % table_name, ( 'A', ))}) + el1 = SubElement(el, 'table:table-column', attrib={ + 'table:style-name': self.rststyle( + '%s.%%c' % table_name, ( 'B', ))}) + el1 = SubElement(el, 'table:table-header-rows') + el2 = SubElement(el1, 'table:table-row') + el3 = SubElement(el2, 'table:table-cell', attrib={ + 'table:style-name': self.rststyle( + '%s.%%c%%d' % table_name, ( 'A', 1, )), + 'office:value-type': 'string'}) + el4 = SubElement(el3, 'text:p', attrib={ + 'text:style-name': 'Table_20_Heading'}) + el4.text= 'Option' + el3 = SubElement(el2, 'table:table-cell', attrib={ + 'table:style-name': self.rststyle( + '%s.%%c%%d' % table_name, ( 'B', 1, )), + 'office:value-type': 'string'}) + el4 = SubElement(el3, 'text:p', attrib={ + 'text:style-name': 'Table_20_Heading'}) + el4.text= 'Description' + self.set_current_element(el) + + def depart_option_list(self, node): + self.set_to_parent() + + def visit_option_list_item(self, node): + el = self.append_child('table:table-row') + self.set_current_element(el) + + def depart_option_list_item(self, node): + self.set_to_parent() + + def visit_option_group(self, node): + el = self.append_child('table:table-cell', attrib={ + 'table:style-name': 'Table%d.A2' % self.table_count, + 'office:value-type': 'string', + }) + self.set_current_element(el) + + def depart_option_group(self, node): + self.set_to_parent() + + def visit_option(self, node): + el = self.append_child('text:p', attrib={ + 'text:style-name': 'Table_20_Contents'}) + el.text = node.astext() + + def depart_option(self, node): + pass + + def visit_option_string(self, node): + pass + + def depart_option_string(self, node): + pass + + def visit_option_argument(self, node): + pass + + def depart_option_argument(self, node): + pass + + def visit_description(self, node): + el = self.append_child('table:table-cell', attrib={ + 'table:style-name': 'Table%d.B2' % self.table_count, + 'office:value-type': 'string', + }) + el1 = SubElement(el, 'text:p', attrib={ + 'text:style-name': 'Table_20_Contents'}) + el1.text = node.astext() + raise nodes.SkipChildren() + + def depart_description(self, node): + pass + + def visit_paragraph(self, node): + self.in_paragraph = True + if self.in_header: + el = self.append_p('header') + elif self.in_footer: + el = self.append_p('footer') + else: + style_name = self.paragraph_style_stack[-1] + el = self.append_child('text:p', + attrib={'text:style-name': style_name}) + self.append_pending_ids(el) + self.set_current_element(el) + + def depart_paragraph(self, node): + self.in_paragraph = False + self.set_to_parent() + if self.in_header: + self.header_content.append( + self.current_element.getchildren()[-1]) + self.current_element.remove( + self.current_element.getchildren()[-1]) + elif self.in_footer: + self.footer_content.append( + self.current_element.getchildren()[-1]) + self.current_element.remove( + self.current_element.getchildren()[-1]) + + def visit_problematic(self, node): + pass + + def depart_problematic(self, node): + pass + + def visit_raw(self, node): + if 'format' in node.attributes: + formats = node.attributes['format'] + formatlist = formats.split() + if 'odt' in formatlist: + rawstr = node.astext() + attrstr = ' '.join(['%s="%s"' % (k, v, ) + for k,v in CONTENT_NAMESPACE_ATTRIB.items()]) + contentstr = '%s' % (attrstr, rawstr, ) + if WhichElementTree != "lxml": + contentstr = contentstr.encode("utf-8") + content = etree.fromstring(contentstr) + elements = content.getchildren() + if len(elements) > 0: + el1 = elements[0] + if self.in_header: + pass + elif self.in_footer: + pass + else: + self.current_element.append(el1) + raise nodes.SkipChildren() + + def depart_raw(self, node): + if self.in_header: + pass + elif self.in_footer: + pass + else: + pass + + def visit_reference(self, node): + text = node.astext() + if self.settings.create_links: + if node.has_key('refuri'): + href = node['refuri'] + if ( self.settings.cloak_email_addresses + and href.startswith('mailto:')): + href = self.cloak_mailto(href) + el = self.append_child('text:a', attrib={ + 'xlink:href': '%s' % href, + 'xlink:type': 'simple', + }) + self.set_current_element(el) + elif node.has_key('refid'): + if self.settings.create_links: + href = node['refid'] + el = self.append_child('text:reference-ref', attrib={ + 'text:ref-name': '%s' % href, + 'text:reference-format': 'text', + }) + else: + self.document.reporter.warning( + 'References must have "refuri" or "refid" attribute.') + if (self.in_table_of_contents and + len(node.children) >= 1 and + isinstance(node.children[0], docutils.nodes.generated)): + node.remove(node.children[0]) + + def depart_reference(self, node): + if self.settings.create_links: + if node.has_key('refuri'): + self.set_to_parent() + + def visit_rubric(self, node): + style_name = self.rststyle('rubric') + classes = node.get('classes') + if classes: + class1 = classes[0] + if class1: + style_name = class1 + el = SubElement(self.current_element, 'text:h', attrib = { + #'text:outline-level': '%d' % section_level, + #'text:style-name': 'Heading_20_%d' % section_level, + 'text:style-name': style_name, + }) + text = node.astext() + el.text = self.encode(text) + + def depart_rubric(self, node): + pass + + def visit_section(self, node, move_ids=1): + self.section_level += 1 + self.section_count += 1 + if self.settings.create_sections: + el = self.append_child('text:section', attrib={ + 'text:name': 'Section%d' % self.section_count, + 'text:style-name': 'Sect%d' % self.section_level, + }) + self.set_current_element(el) + + def depart_section(self, node): + self.section_level -= 1 + if self.settings.create_sections: + self.set_to_parent() + + def visit_strong(self, node): + el = SubElement(self.current_element, 'text:span', + attrib={'text:style-name': self.rststyle('strong')}) + self.set_current_element(el) + + def depart_strong(self, node): + self.set_to_parent() + + def visit_substitution_definition(self, node): + raise nodes.SkipChildren() + + def depart_substitution_definition(self, node): + pass + + def visit_system_message(self, node): + pass + + def depart_system_message(self, node): + pass + + def get_table_style(self, node): + table_style = None + table_name = None + use_predefined_table_style = False + str_classes = node.get('classes') + if str_classes is not None: + for str_class in str_classes: + if str_class.startswith(TABLESTYLEPREFIX): + table_name = str_class + use_predefined_table_style = True + break + if table_name is not None: + table_style = self.table_styles.get(table_name) + if table_style is None: + # If we can't find the table style, issue warning + # and use the default table style. + self.document.reporter.warning( + 'Can\'t find table style "%s". Using default.' % ( + table_name, )) + table_name = TABLENAMEDEFAULT + table_style = self.table_styles.get(table_name) + if table_style is None: + # If we can't find the default table style, issue a warning + # and use a built-in default style. + self.document.reporter.warning( + 'Can\'t find default table style "%s". Using built-in default.' % ( + table_name, )) + table_style = BUILTIN_DEFAULT_TABLE_STYLE + else: + table_name = TABLENAMEDEFAULT + table_style = self.table_styles.get(table_name) + if table_style is None: + # If we can't find the default table style, issue a warning + # and use a built-in default style. + self.document.reporter.warning( + 'Can\'t find default table style "%s". Using built-in default.' % ( + table_name, )) + table_style = BUILTIN_DEFAULT_TABLE_STYLE + return table_style + + def visit_table(self, node): + self.table_count += 1 + table_style = self.get_table_style(node) + table_name = '%s%%d' % TABLESTYLEPREFIX + el1 = SubElement(self.automatic_styles, 'style:style', attrib={ + 'style:name': self.rststyle( + '%s' % table_name, ( self.table_count, )), + 'style:family': 'table', + }, nsdict=SNSD) + if table_style.backgroundcolor is None: + el1_1 = SubElement(el1, 'style:table-properties', attrib={ + #'style:width': '17.59cm', + #'table:align': 'margins', + 'table:align': 'left', + 'fo:margin-top': '0in', + 'fo:margin-bottom': '0.10in', + }, nsdict=SNSD) + else: + el1_1 = SubElement(el1, 'style:table-properties', attrib={ + #'style:width': '17.59cm', + 'table:align': 'margins', + 'fo:margin-top': '0in', + 'fo:margin-bottom': '0.10in', + 'fo:background-color': table_style.backgroundcolor, + }, nsdict=SNSD) + # We use a single cell style for all cells in this table. + # That's probably not correct, but seems to work. + el2 = SubElement(self.automatic_styles, 'style:style', attrib={ + 'style:name': self.rststyle( + '%s.%%c%%d' % table_name, ( self.table_count, 'A', 1, )), + 'style:family': 'table-cell', + }, nsdict=SNSD) + thickness = self.settings.table_border_thickness + if thickness is None: + line_style1 = table_style.border + else: + line_style1 = '0.%03dcm solid #000000' % (thickness, ) + el2_1 = SubElement(el2, 'style:table-cell-properties', attrib={ + 'fo:padding': '0.049cm', + 'fo:border-left': line_style1, + 'fo:border-right': line_style1, + 'fo:border-top': line_style1, + 'fo:border-bottom': line_style1, + }, nsdict=SNSD) + title = None + for child in node.children: + if child.tagname == 'title': + title = child.astext() + break + if title is not None: + el3 = self.append_p('table-title', title) + else: + pass + el4 = SubElement(self.current_element, 'table:table', attrib={ + 'table:name': self.rststyle( + '%s' % table_name, ( self.table_count, )), + 'table:style-name': self.rststyle( + '%s' % table_name, ( self.table_count, )), + }) + self.set_current_element(el4) + self.current_table_style = el1 + self.table_width = 0.0 + + def depart_table(self, node): + attribkey = add_ns('style:width', nsdict=SNSD) + attribval = '%.4fin' % (self.table_width, ) + el1 = self.current_table_style + el2 = el1[0] + el2.attrib[attribkey] = attribval + self.set_to_parent() + + def visit_tgroup(self, node): + self.column_count = ord('A') - 1 + + def depart_tgroup(self, node): + pass + + def visit_colspec(self, node): + self.column_count += 1 + colspec_name = self.rststyle( + '%s%%d.%%s' % TABLESTYLEPREFIX, + (self.table_count, chr(self.column_count), ) + ) + colwidth = node['colwidth'] / 12.0 + el1 = SubElement(self.automatic_styles, 'style:style', attrib={ + 'style:name': colspec_name, + 'style:family': 'table-column', + }, nsdict=SNSD) + el1_1 = SubElement(el1, 'style:table-column-properties', attrib={ + 'style:column-width': '%.4fin' % colwidth + }, + nsdict=SNSD) + el2 = self.append_child('table:table-column', attrib={ + 'table:style-name': colspec_name, + }) + self.table_width += colwidth + + def depart_colspec(self, node): + pass + + def visit_thead(self, node): + el = self.append_child('table:table-header-rows') + self.set_current_element(el) + self.in_thead = True + self.paragraph_style_stack.append('Table_20_Heading') + + def depart_thead(self, node): + self.set_to_parent() + self.in_thead = False + self.paragraph_style_stack.pop() + + def visit_row(self, node): + self.column_count = ord('A') - 1 + el = self.append_child('table:table-row') + self.set_current_element(el) + + def depart_row(self, node): + self.set_to_parent() + + def visit_entry(self, node): + self.column_count += 1 + cellspec_name = self.rststyle( + '%s%%d.%%c%%d' % TABLESTYLEPREFIX, + (self.table_count, 'A', 1, ) + ) + attrib={ + 'table:style-name': cellspec_name, + 'office:value-type': 'string', + } + morecols = node.get('morecols', 0) + if morecols > 0: + attrib['table:number-columns-spanned'] = '%d' % (morecols + 1,) + self.column_count += morecols + morerows = node.get('morerows', 0) + if morerows > 0: + attrib['table:number-rows-spanned'] = '%d' % (morerows + 1,) + el1 = self.append_child('table:table-cell', attrib=attrib) + self.set_current_element(el1) + + def depart_entry(self, node): + self.set_to_parent() + + def visit_tbody(self, node): + pass + + def depart_tbody(self, node): + pass + + def visit_target(self, node): + # + # I don't know how to implement targets in ODF. + # How do we create a target in oowriter? A cross-reference? + if not (node.has_key('refuri') or node.has_key('refid') + or node.has_key('refname')): + pass + else: + pass + + def depart_target(self, node): + pass + + def visit_title(self, node, move_ids=1, title_type='title'): + if isinstance(node.parent, docutils.nodes.section): + section_level = self.section_level + if section_level > 7: + self.document.reporter.warning( + 'Heading/section levels greater than 7 not supported.') + self.document.reporter.warning( + ' Reducing to heading level 7 for heading: "%s"' % ( + node.astext(), )) + section_level = 7 + el1 = self.append_child('text:h', attrib = { + 'text:outline-level': '%d' % section_level, + #'text:style-name': 'Heading_20_%d' % section_level, + 'text:style-name': self.rststyle( + 'heading%d', (section_level, )), + }) + self.append_pending_ids(el1) + self.set_current_element(el1) + elif isinstance(node.parent, docutils.nodes.document): + # text = self.settings.title + #else: + # text = node.astext() + el1 = SubElement(self.current_element, 'text:p', attrib = { + 'text:style-name': self.rststyle(title_type), + }) + self.append_pending_ids(el1) + text = node.astext() + self.title = text + self.found_doc_title = True + self.set_current_element(el1) + + def depart_title(self, node): + if (isinstance(node.parent, docutils.nodes.section) or + isinstance(node.parent, docutils.nodes.document)): + self.set_to_parent() + + def visit_subtitle(self, node, move_ids=1): + self.visit_title(node, move_ids, title_type='subtitle') + + def depart_subtitle(self, node): + self.depart_title(node) + + def visit_title_reference(self, node): + el = self.append_child('text:span', attrib={ + 'text:style-name': self.rststyle('quotation')}) + el.text = self.encode(node.astext()) + raise nodes.SkipChildren() + + def depart_title_reference(self, node): + pass + + def generate_table_of_content_entry_template(self, el1): + for idx in range(1, 11): + el2 = SubElement(el1, + 'text:table-of-content-entry-template', + attrib={ + 'text:outline-level': "%d" % (idx, ), + 'text:style-name': self.rststyle('contents-%d' % (idx, )), + }) + el3 = SubElement(el2, 'text:index-entry-chapter') + el3 = SubElement(el2, 'text:index-entry-text') + el3 = SubElement(el2, 'text:index-entry-tab-stop', attrib={ + 'style:leader-char': ".", + 'style:type': "right", + }) + el3 = SubElement(el2, 'text:index-entry-page-number') + + def find_title_label(self, node, class_type, label_key): + label = '' + title_node = None + for child in node.children: + if isinstance(child, class_type): + title_node = child + break + if title_node is not None: + label = title_node.astext() + else: + label = self.language.labels[label_key] + return label + + def visit_topic(self, node): + if 'classes' in node.attributes: + if 'contents' in node.attributes['classes']: + label = self.find_title_label(node, docutils.nodes.title, + 'contents') + if self.settings.generate_oowriter_toc: + el1 = self.append_child('text:table-of-content', attrib={ + 'text:name': 'Table of Contents1', + 'text:protected': 'true', + 'text:style-name': 'Sect1', + }) + el2 = SubElement(el1, + 'text:table-of-content-source', + attrib={ + 'text:outline-level': '10', + }) + el3 =SubElement(el2, 'text:index-title-template', attrib={ + 'text:style-name': 'Contents_20_Heading', + }) + el3.text = label + self.generate_table_of_content_entry_template(el2) + el4 = SubElement(el1, 'text:index-body') + el5 = SubElement(el4, 'text:index-title') + el6 = SubElement(el5, 'text:p', attrib={ + 'text:style-name': self.rststyle('contents-heading'), + }) + el6.text = label + self.save_current_element = self.current_element + self.table_of_content_index_body = el4 + self.set_current_element(el4) + else: + el = self.append_p('horizontalline') + el = self.append_p('centeredtextbody') + el1 = SubElement(el, 'text:span', + attrib={'text:style-name': self.rststyle('strong')}) + el1.text = label + self.in_table_of_contents = True + elif 'abstract' in node.attributes['classes']: + el = self.append_p('horizontalline') + el = self.append_p('centeredtextbody') + el1 = SubElement(el, 'text:span', + attrib={'text:style-name': self.rststyle('strong')}) + label = self.find_title_label(node, docutils.nodes.title, + 'abstract') + el1.text = label + elif 'dedication' in node.attributes['classes']: + el = self.append_p('horizontalline') + el = self.append_p('centeredtextbody') + el1 = SubElement(el, 'text:span', + attrib={'text:style-name': self.rststyle('strong')}) + label = self.find_title_label(node, docutils.nodes.title, + 'dedication') + el1.text = label + + def depart_topic(self, node): + if 'classes' in node.attributes: + if 'contents' in node.attributes['classes']: + if self.settings.generate_oowriter_toc: + self.update_toc_page_numbers( + self.table_of_content_index_body) + self.set_current_element(self.save_current_element) + else: + el = self.append_p('horizontalline') + self.in_table_of_contents = False + + def update_toc_page_numbers(self, el): + collection = [] + self.update_toc_collect(el, 0, collection) + self.update_toc_add_numbers(collection) + + def update_toc_collect(self, el, level, collection): + collection.append((level, el)) + level += 1 + for child_el in el.getchildren(): + if child_el.tag != 'text:index-body': + self.update_toc_collect(child_el, level, collection) + + def update_toc_add_numbers(self, collection): + for level, el1 in collection: + if (el1.tag == 'text:p' and + el1.text != 'Table of Contents'): + el2 = SubElement(el1, 'text:tab') + el2.tail = '9999' + + + def visit_transition(self, node): + el = self.append_p('horizontalline') + + def depart_transition(self, node): + pass + + # + # Admonitions + # + def visit_warning(self, node): + self.generate_admonition(node, 'warning') + + def depart_warning(self, node): + self.paragraph_style_stack.pop() + + def visit_attention(self, node): + self.generate_admonition(node, 'attention') + + depart_attention = depart_warning + + def visit_caution(self, node): + self.generate_admonition(node, 'caution') + + depart_caution = depart_warning + + def visit_danger(self, node): + self.generate_admonition(node, 'danger') + + depart_danger = depart_warning + + def visit_error(self, node): + self.generate_admonition(node, 'error') + + depart_error = depart_warning + + def visit_hint(self, node): + self.generate_admonition(node, 'hint') + + depart_hint = depart_warning + + def visit_important(self, node): + self.generate_admonition(node, 'important') + + depart_important = depart_warning + + def visit_note(self, node): + self.generate_admonition(node, 'note') + + depart_note = depart_warning + + def visit_tip(self, node): + self.generate_admonition(node, 'tip') + + depart_tip = depart_warning + + def visit_admonition(self, node): + title = None + for child in node.children: + if child.tagname == 'title': + title = child.astext() + if title is None: + classes1 = node.get('classes') + if classes1: + title = classes1[0] + self.generate_admonition(node, 'generic', title) + + depart_admonition = depart_warning + + def generate_admonition(self, node, label, title=None): + el1 = SubElement(self.current_element, 'text:p', attrib = { + 'text:style-name': self.rststyle('admon-%s-hdr', ( label, )), + }) + if title: + el1.text = title + else: + el1.text = '%s!' % (label.capitalize(), ) + s1 = self.rststyle('admon-%s-body', ( label, )) + self.paragraph_style_stack.append(s1) + + # + # Roles (e.g. subscript, superscript, strong, ... + # + def visit_subscript(self, node): + el = self.append_child('text:span', attrib={ + 'text:style-name': 'rststyle-subscript', + }) + self.set_current_element(el) + + def depart_subscript(self, node): + self.set_to_parent() + + def visit_superscript(self, node): + el = self.append_child('text:span', attrib={ + 'text:style-name': 'rststyle-superscript', + }) + self.set_current_element(el) + + def depart_superscript(self, node): + self.set_to_parent() + + +# Use an own reader to modify transformations done. +class Reader(standalone.Reader): + + def get_transforms(self): + default = standalone.Reader.get_transforms(self) + if self.settings.create_links: + return default + return [ i + for i in default + if i is not references.DanglingReferences ] diff --git a/docutils/writers/odf_odt/pygmentsformatter.py b/docutils/writers/odf_odt/pygmentsformatter.py new file mode 100644 index 000000000..e8ce8279e --- /dev/null +++ b/docutils/writers/odf_odt/pygmentsformatter.py @@ -0,0 +1,109 @@ +# $Id: pygmentsformatter.py 5853 2009-01-19 21:02:02Z dkuhlman $ +# Author: Dave Kuhlman +# Copyright: This module has been placed in the public domain. + +""" + +Additional support for Pygments formatter. + +""" + + +import pygments +import pygments.formatter + + +class OdtPygmentsFormatter(pygments.formatter.Formatter): + def __init__(self, rststyle_function, escape_function): + pygments.formatter.Formatter.__init__(self) + self.rststyle_function = rststyle_function + self.escape_function = escape_function + + def rststyle(self, name, parameters=( )): + return self.rststyle_function(name, parameters) + + +class OdtPygmentsProgFormatter(OdtPygmentsFormatter): + def format(self, tokensource, outfile): + tokenclass = pygments.token.Token + for ttype, value in tokensource: + value = self.escape_function(value) + if ttype == tokenclass.Keyword: + s2 = self.rststyle('codeblock-keyword') + s1 = '%s' % \ + (s2, value, ) + elif ttype == tokenclass.Literal.String: + s2 = self.rststyle('codeblock-string') + s1 = '%s' % \ + (s2, value, ) + elif ttype in ( + tokenclass.Literal.Number.Integer, + tokenclass.Literal.Number.Integer.Long, + tokenclass.Literal.Number.Float, + tokenclass.Literal.Number.Hex, + tokenclass.Literal.Number.Oct, + tokenclass.Literal.Number, + ): + s2 = self.rststyle('codeblock-number') + s1 = '%s' % \ + (s2, value, ) + elif ttype == tokenclass.Operator: + s2 = self.rststyle('codeblock-operator') + s1 = '%s' % \ + (s2, value, ) + elif ttype == tokenclass.Comment: + s2 = self.rststyle('codeblock-comment') + s1 = '%s' % \ + (s2, value, ) + elif ttype == tokenclass.Name.Class: + s2 = self.rststyle('codeblock-classname') + s1 = '%s' % \ + (s2, value, ) + elif ttype == tokenclass.Name.Function: + s2 = self.rststyle('codeblock-functionname') + s1 = '%s' % \ + (s2, value, ) + elif ttype == tokenclass.Name: + s2 = self.rststyle('codeblock-name') + s1 = '%s' % \ + (s2, value, ) + else: + s1 = value + outfile.write(s1) + + +class OdtPygmentsLaTeXFormatter(OdtPygmentsFormatter): + def format(self, tokensource, outfile): + tokenclass = pygments.token.Token + for ttype, value in tokensource: + value = self.escape_function(value) + if ttype == tokenclass.Keyword: + s2 = self.rststyle('codeblock-keyword') + s1 = '%s' % \ + (s2, value, ) + elif ttype in (tokenclass.Literal.String, + tokenclass.Literal.String.Backtick, + ): + s2 = self.rststyle('codeblock-string') + s1 = '%s' % \ + (s2, value, ) + elif ttype == tokenclass.Name.Attribute: + s2 = self.rststyle('codeblock-operator') + s1 = '%s' % \ + (s2, value, ) + elif ttype == tokenclass.Comment: + if value[-1] == '\n': + s2 = self.rststyle('codeblock-comment') + s1 = '%s\n' % \ + (s2, value[:-1], ) + else: + s2 = self.rststyle('codeblock-comment') + s1 = '%s' % \ + (s2, value, ) + elif ttype == tokenclass.Name.Builtin: + s2 = self.rststyle('codeblock-name') + s1 = '%s' % \ + (s2, value, ) + else: + s1 = value + outfile.write(s1) diff --git a/docutils/writers/odf_odt/styles.odt b/docutils/writers/odf_odt/styles.odt new file mode 100644 index 000000000..e17b00721 Binary files /dev/null and b/docutils/writers/odf_odt/styles.odt differ diff --git a/docutils/writers/pep_html/__init__.py b/docutils/writers/pep_html/__init__.py new file mode 100644 index 000000000..503fa17bf --- /dev/null +++ b/docutils/writers/pep_html/__init__.py @@ -0,0 +1,105 @@ +# $Id: __init__.py 6328 2010-05-23 21:20:29Z gbrandl $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +PEP HTML Writer. +""" + +__docformat__ = 'reStructuredText' + + +import sys +import os +import os.path +import codecs +import docutils +from docutils import frontend, nodes, utils, writers +from docutils.writers import html4css1 + + +class Writer(html4css1.Writer): + + default_stylesheet = 'pep.css' + + default_stylesheet_path = utils.relative_path( + os.path.join(os.getcwd(), 'dummy'), + os.path.join(os.path.dirname(__file__), default_stylesheet)) + + default_template = 'template.txt' + + default_template_path = utils.relative_path( + os.path.join(os.getcwd(), 'dummy'), + os.path.join(os.path.dirname(__file__), default_template)) + + settings_spec = html4css1.Writer.settings_spec + ( + 'PEP/HTML-Specific Options', + 'For the PEP/HTML writer, the default value for the --stylesheet-path ' + 'option is "%s", and the default value for --template is "%s". ' + 'See HTML-Specific Options above.' + % (default_stylesheet_path, default_template_path), + (('Python\'s home URL. Default is "http://www.python.org".', + ['--python-home'], + {'default': 'http://www.python.org', 'metavar': ''}), + ('Home URL prefix for PEPs. Default is "." (current directory).', + ['--pep-home'], + {'default': '.', 'metavar': ''}), + # For testing. + (frontend.SUPPRESS_HELP, + ['--no-random'], + {'action': 'store_true', 'validator': frontend.validate_boolean}),)) + + settings_default_overrides = {'stylesheet_path': default_stylesheet_path, + 'template': default_template_path,} + + relative_path_settings = (html4css1.Writer.relative_path_settings + + ('template',)) + + config_section = 'pep_html writer' + config_section_dependencies = ('writers', 'html4css1 writer') + + def __init__(self): + html4css1.Writer.__init__(self) + self.translator_class = HTMLTranslator + + def interpolation_dict(self): + subs = html4css1.Writer.interpolation_dict(self) + settings = self.document.settings + pyhome = settings.python_home + subs['pyhome'] = pyhome + subs['pephome'] = settings.pep_home + if pyhome == '..': + subs['pepindex'] = '.' + else: + subs['pepindex'] = pyhome + '/dev/peps' + index = self.document.first_child_matching_class(nodes.field_list) + header = self.document[index] + self.pepnum = header[0][1].astext() + subs['pep'] = self.pepnum + if settings.no_random: + subs['banner'] = 0 + else: + import random + subs['banner'] = random.randrange(64) + try: + subs['pepnum'] = '%04i' % int(self.pepnum) + except ValueError: + subs['pepnum'] = self.pepnum + self.title = header[1][1].astext() + subs['title'] = self.title + subs['body'] = ''.join( + self.body_pre_docinfo + self.docinfo + self.body) + return subs + + def assemble_parts(self): + html4css1.Writer.assemble_parts(self) + self.parts['title'] = [self.title] + self.parts['pepnum'] = self.pepnum + + +class HTMLTranslator(html4css1.HTMLTranslator): + + def depart_field_list(self, node): + html4css1.HTMLTranslator.depart_field_list(self, node) + if 'rfc2822' in node['classes']: + self.body.append('
\n') diff --git a/docutils/writers/pep_html/pep.css b/docutils/writers/pep_html/pep.css new file mode 100644 index 000000000..620740978 --- /dev/null +++ b/docutils/writers/pep_html/pep.css @@ -0,0 +1,344 @@ +/* +:Author: David Goodger +:Contact: goodger@python.org +:date: $Date: 2006-05-21 22:44:42 +0200 (Son, 21 Mai 2006) $ +:version: $Revision: 4564 $ +:copyright: This stylesheet has been placed in the public domain. + +Default cascading style sheet for the PEP HTML output of Docutils. +*/ + +/* "! important" is used here to override other ``margin-top`` and + ``margin-bottom`` styles that are later in the stylesheet or + more specific. See http://www.w3.org/TR/CSS1#the-cascade */ +.first { + margin-top: 0 ! important } + +.last, .with-subtitle { + margin-bottom: 0 ! important } + +.hidden { + display: none } + +.navigation { + width: 100% ; + background: #99ccff ; + margin-top: 0px ; + margin-bottom: 0px } + +.navigation .navicon { + width: 150px ; + height: 35px } + +.navigation .textlinks { + padding-left: 1em ; + text-align: left } + +.navigation td, .navigation th { + padding-left: 0em ; + padding-right: 0em ; + vertical-align: middle } + +.rfc2822 { + margin-top: 0.5em ; + margin-left: 0.5em ; + margin-right: 0.5em ; + margin-bottom: 0em } + +.rfc2822 td { + text-align: left } + +.rfc2822 th.field-name { + text-align: right ; + font-family: sans-serif ; + padding-right: 0.5em ; + font-weight: bold ; + margin-bottom: 0em } + +a.toc-backref { + text-decoration: none ; + color: black } + +blockquote.epigraph { + margin: 2em 5em ; } + +body { + margin: 0px ; + margin-bottom: 1em ; + padding: 0px } + +dl.docutils dd { + margin-bottom: 0.5em } + +div.section { + margin-left: 1em ; + margin-right: 1em ; + margin-bottom: 1.5em } + +div.section div.section { + margin-left: 0em ; + margin-right: 0em ; + margin-top: 1.5em } + +div.abstract { + margin: 2em 5em } + +div.abstract p.topic-title { + font-weight: bold ; + text-align: center } + +div.admonition, div.attention, div.caution, div.danger, div.error, +div.hint, div.important, div.note, div.tip, div.warning { + margin: 2em ; + border: medium outset ; + padding: 1em } + +div.admonition p.admonition-title, div.hint p.admonition-title, +div.important p.admonition-title, div.note p.admonition-title, +div.tip p.admonition-title { + font-weight: bold ; + font-family: sans-serif } + +div.attention p.admonition-title, div.caution p.admonition-title, +div.danger p.admonition-title, div.error p.admonition-title, +div.warning p.admonition-title { + color: red ; + font-weight: bold ; + font-family: sans-serif } + +/* Uncomment (and remove this text!) to get reduced vertical space in + compound paragraphs. +div.compound .compound-first, div.compound .compound-middle { + margin-bottom: 0.5em } + +div.compound .compound-last, div.compound .compound-middle { + margin-top: 0.5em } +*/ + +div.dedication { + margin: 2em 5em ; + text-align: center ; + font-style: italic } + +div.dedication p.topic-title { + font-weight: bold ; + font-style: normal } + +div.figure { + margin-left: 2em ; + margin-right: 2em } + +div.footer, div.header { + clear: both; + font-size: smaller } + +div.footer { + margin-left: 1em ; + margin-right: 1em } + +div.line-block { + display: block ; + margin-top: 1em ; + margin-bottom: 1em } + +div.line-block div.line-block { + margin-top: 0 ; + margin-bottom: 0 ; + margin-left: 1.5em } + +div.sidebar { + margin-left: 1em ; + border: medium outset ; + padding: 1em ; + background-color: #ffffee ; + width: 40% ; + float: right ; + clear: right } + +div.sidebar p.rubric { + font-family: sans-serif ; + font-size: medium } + +div.system-messages { + margin: 5em } + +div.system-messages h1 { + color: red } + +div.system-message { + border: medium outset ; + padding: 1em } + +div.system-message p.system-message-title { + color: red ; + font-weight: bold } + +div.topic { + margin: 2em } + +h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, +h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { + margin-top: 0.4em } + +h1 { + font-family: sans-serif ; + font-size: large } + +h2 { + font-family: sans-serif ; + font-size: medium } + +h3 { + font-family: sans-serif ; + font-size: small } + +h4 { + font-family: sans-serif ; + font-style: italic ; + font-size: small } + +h5 { + font-family: sans-serif; + font-size: x-small } + +h6 { + font-family: sans-serif; + font-style: italic ; + font-size: x-small } + +hr.docutils { + width: 75% } + +img.align-left { + clear: left } + +img.align-right { + clear: right } + +img.borderless { + border: 0 } + +ol.simple, ul.simple { + margin-bottom: 1em } + +ol.arabic { + list-style: decimal } + +ol.loweralpha { + list-style: lower-alpha } + +ol.upperalpha { + list-style: upper-alpha } + +ol.lowerroman { + list-style: lower-roman } + +ol.upperroman { + list-style: upper-roman } + +p.attribution { + text-align: right ; + margin-left: 50% } + +p.caption { + font-style: italic } + +p.credits { + font-style: italic ; + font-size: smaller } + +p.label { + white-space: nowrap } + +p.rubric { + font-weight: bold ; + font-size: larger ; + color: maroon ; + text-align: center } + +p.sidebar-title { + font-family: sans-serif ; + font-weight: bold ; + font-size: larger } + +p.sidebar-subtitle { + font-family: sans-serif ; + font-weight: bold } + +p.topic-title { + font-family: sans-serif ; + font-weight: bold } + +pre.address { + margin-bottom: 0 ; + margin-top: 0 ; + font-family: serif ; + font-size: 100% } + +pre.literal-block, pre.doctest-block { + margin-left: 2em ; + margin-right: 2em } + +span.classifier { + font-family: sans-serif ; + font-style: oblique } + +span.classifier-delimiter { + font-family: sans-serif ; + font-weight: bold } + +span.interpreted { + font-family: sans-serif } + +span.option { + white-space: nowrap } + +span.option-argument { + font-style: italic } + +span.pre { + white-space: pre } + +span.problematic { + color: red } + +span.section-subtitle { + /* font-size relative to parent (h1..h6 element) */ + font-size: 80% } + +table.citation { + border-left: solid 1px gray; + margin-left: 1px } + +table.docinfo { + margin: 2em 4em } + +table.docutils { + margin-top: 0.5em ; + margin-bottom: 0.5em } + +table.footnote { + border-left: solid 1px black; + margin-left: 1px } + +table.docutils td, table.docutils th, +table.docinfo td, table.docinfo th { + padding-left: 0.5em ; + padding-right: 0.5em ; + vertical-align: top } + +td.num { + text-align: right } + +th.field-name { + font-weight: bold ; + text-align: left ; + white-space: nowrap ; + padding-left: 0 } + +h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, +h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { + font-size: 100% } + +ul.auto-toc { + list-style-type: none } diff --git a/docutils/writers/pep_html/template.txt b/docutils/writers/pep_html/template.txt new file mode 100644 index 000000000..62c07a87f --- /dev/null +++ b/docutils/writers/pep_html/template.txt @@ -0,0 +1,29 @@ + + + + + + + + PEP %(pep)s -- %(title)s + %(stylesheet)s + + + + + +
+%(body)s +%(body_suffix)s diff --git a/docutils/writers/pseudoxml.py b/docutils/writers/pseudoxml.py new file mode 100644 index 000000000..bb833d668 --- /dev/null +++ b/docutils/writers/pseudoxml.py @@ -0,0 +1,31 @@ +# $Id: pseudoxml.py 7320 2012-01-19 22:33:02Z milde $ +# Author: David Goodger +# Copyright: This module has been placed in the public domain. + +""" +Simple internal document tree Writer, writes indented pseudo-XML. +""" + +__docformat__ = 'reStructuredText' + + +from docutils import writers + + +class Writer(writers.Writer): + + supported = ('pprint', 'pformat', 'pseudoxml') + """Formats this writer supports.""" + + config_section = 'pseudoxml writer' + config_section_dependencies = ('writers',) + + output = None + """Final translated form of `document`.""" + + def translate(self): + self.output = self.document.pformat() + + def supports(self, format): + """This writer supports all format-specific elements.""" + return True diff --git a/docutils/writers/s5_html/__init__.py b/docutils/writers/s5_html/__init__.py new file mode 100644 index 000000000..44d32201c --- /dev/null +++ b/docutils/writers/s5_html/__init__.py @@ -0,0 +1,347 @@ +# $Id: __init__.py 7320 2012-01-19 22:33:02Z milde $ +# Authors: Chris Liechti ; +# David Goodger +# Copyright: This module has been placed in the public domain. + +""" +S5/HTML Slideshow Writer. +""" + +__docformat__ = 'reStructuredText' + + +import sys +import os +import re +import docutils +from docutils import frontend, nodes, utils +from docutils.writers import html4css1 +from docutils.parsers.rst import directives +from docutils._compat import b + +themes_dir_path = utils.relative_path( + os.path.join(os.getcwd(), 'dummy'), + os.path.join(os.path.dirname(__file__), 'themes')) + +def find_theme(name): + # Where else to look for a theme? + # Check working dir? Destination dir? Config dir? Plugins dir? + path = os.path.join(themes_dir_path, name) + if not os.path.isdir(path): + raise docutils.ApplicationError( + 'Theme directory not found: %r (path: %r)' % (name, path)) + return path + + +class Writer(html4css1.Writer): + + settings_spec = html4css1.Writer.settings_spec + ( + 'S5 Slideshow Specific Options', + 'For the S5/HTML writer, the --no-toc-backlinks option ' + '(defined in General Docutils Options above) is the default, ' + 'and should not be changed.', + (('Specify an installed S5 theme by name. Overrides --theme-url. ' + 'The default theme name is "default". The theme files will be ' + 'copied into a "ui/" directory, in the same directory as the ' + 'destination file (output HTML). Note that existing theme files ' + 'will not be overwritten (unless --overwrite-theme-files is used).', + ['--theme'], + {'default': 'default', 'metavar': '', + 'overrides': 'theme_url'}), + ('Specify an S5 theme URL. The destination file (output HTML) will ' + 'link to this theme; nothing will be copied. Overrides --theme.', + ['--theme-url'], + {'metavar': '', 'overrides': 'theme'}), + ('Allow existing theme files in the ``ui/`` directory to be ' + 'overwritten. The default is not to overwrite theme files.', + ['--overwrite-theme-files'], + {'action': 'store_true', 'validator': frontend.validate_boolean}), + ('Keep existing theme files in the ``ui/`` directory; do not ' + 'overwrite any. This is the default.', + ['--keep-theme-files'], + {'dest': 'overwrite_theme_files', 'action': 'store_false'}), + ('Set the initial view mode to "slideshow" [default] or "outline".', + ['--view-mode'], + {'choices': ['slideshow', 'outline'], 'default': 'slideshow', + 'metavar': ''}), + ('Normally hide the presentation controls in slideshow mode. ' + 'This is the default.', + ['--hidden-controls'], + {'action': 'store_true', 'default': True, + 'validator': frontend.validate_boolean}), + ('Always show the presentation controls in slideshow mode. ' + 'The default is to hide the controls.', + ['--visible-controls'], + {'dest': 'hidden_controls', 'action': 'store_false'}), + ('Enable the current slide indicator ("1 / 15"). ' + 'The default is to disable it.', + ['--current-slide'], + {'action': 'store_true', 'validator': frontend.validate_boolean}), + ('Disable the current slide indicator. This is the default.', + ['--no-current-slide'], + {'dest': 'current_slide', 'action': 'store_false'}),)) + + settings_default_overrides = {'toc_backlinks': 0} + + config_section = 's5_html writer' + config_section_dependencies = ('writers', 'html4css1 writer') + + def __init__(self): + html4css1.Writer.__init__(self) + self.translator_class = S5HTMLTranslator + + +class S5HTMLTranslator(html4css1.HTMLTranslator): + + s5_stylesheet_template = """\ + + + + + + + + +\n""" + # The script element must go in front of the link elements to + # avoid a flash of unstyled content (FOUC), reproducible with + # Firefox. + + disable_current_slide = """ +\n""" + + layout_template = """\ +
+
+
+ + +
\n""" +#
+#
+#
+#
+ + default_theme = 'default' + """Name of the default theme.""" + + base_theme_file = '__base__' + """Name of the file containing the name of the base theme.""" + + direct_theme_files = ( + 'slides.css', 'outline.css', 'print.css', 'opera.css', 'slides.js') + """Names of theme files directly linked to in the output HTML""" + + indirect_theme_files = ( + 's5-core.css', 'framing.css', 'pretty.css', 'blank.gif', 'iepngfix.htc') + """Names of files used indirectly; imported or used by files in + `direct_theme_files`.""" + + required_theme_files = indirect_theme_files + direct_theme_files + """Names of mandatory theme files.""" + + def __init__(self, *args): + html4css1.HTMLTranslator.__init__(self, *args) + #insert S5-specific stylesheet and script stuff: + self.theme_file_path = None + self.setup_theme() + view_mode = self.document.settings.view_mode + control_visibility = ('visible', 'hidden')[self.document.settings + .hidden_controls] + self.stylesheet.append(self.s5_stylesheet_template + % {'path': self.theme_file_path, + 'view_mode': view_mode, + 'control_visibility': control_visibility}) + if not self.document.settings.current_slide: + self.stylesheet.append(self.disable_current_slide) + self.add_meta('\n') + self.s5_footer = [] + self.s5_header = [] + self.section_count = 0 + self.theme_files_copied = None + + def setup_theme(self): + if self.document.settings.theme: + self.copy_theme() + elif self.document.settings.theme_url: + self.theme_file_path = self.document.settings.theme_url + else: + raise docutils.ApplicationError( + 'No theme specified for S5/HTML writer.') + + def copy_theme(self): + """ + Locate & copy theme files. + + A theme may be explicitly based on another theme via a '__base__' + file. The default base theme is 'default'. Files are accumulated + from the specified theme, any base themes, and 'default'. + """ + settings = self.document.settings + path = find_theme(settings.theme) + theme_paths = [path] + self.theme_files_copied = {} + required_files_copied = {} + # This is a link (URL) in HTML, so we use "/", not os.sep: + self.theme_file_path = '%s/%s' % ('ui', settings.theme) + if settings._destination: + dest = os.path.join( + os.path.dirname(settings._destination), 'ui', settings.theme) + if not os.path.isdir(dest): + os.makedirs(dest) + else: + # no destination, so we can't copy the theme + return + default = False + while path: + for f in os.listdir(path): # copy all files from each theme + if f == self.base_theme_file: + continue # ... except the "__base__" file + if ( self.copy_file(f, path, dest) + and f in self.required_theme_files): + required_files_copied[f] = 1 + if default: + break # "default" theme has no base theme + # Find the "__base__" file in theme directory: + base_theme_file = os.path.join(path, self.base_theme_file) + # If it exists, read it and record the theme path: + if os.path.isfile(base_theme_file): + lines = open(base_theme_file).readlines() + for line in lines: + line = line.strip() + if line and not line.startswith('#'): + path = find_theme(line) + if path in theme_paths: # check for duplicates (cycles) + path = None # if found, use default base + else: + theme_paths.append(path) + break + else: # no theme name found + path = None # use default base + else: # no base theme file found + path = None # use default base + if not path: + path = find_theme(self.default_theme) + theme_paths.append(path) + default = True + if len(required_files_copied) != len(self.required_theme_files): + # Some required files weren't found & couldn't be copied. + required = list(self.required_theme_files) + for f in required_files_copied.keys(): + required.remove(f) + raise docutils.ApplicationError( + 'Theme files not found: %s' + % ', '.join(['%r' % f for f in required])) + + files_to_skip_pattern = re.compile(r'~$|\.bak$|#$|\.cvsignore$') + + def copy_file(self, name, source_dir, dest_dir): + """ + Copy file `name` from `source_dir` to `dest_dir`. + Return 1 if the file exists in either `source_dir` or `dest_dir`. + """ + source = os.path.join(source_dir, name) + dest = os.path.join(dest_dir, name) + if dest in self.theme_files_copied: + return 1 + else: + self.theme_files_copied[dest] = 1 + if os.path.isfile(source): + if self.files_to_skip_pattern.search(source): + return None + settings = self.document.settings + if os.path.exists(dest) and not settings.overwrite_theme_files: + settings.record_dependencies.add(dest) + else: + src_file = open(source, 'rb') + src_data = src_file.read() + src_file.close() + dest_file = open(dest, 'wb') + dest_dir = dest_dir.replace(os.sep, '/') + dest_file.write(src_data.replace( + b('ui/default'), + dest_dir[dest_dir.rfind('ui/'):].encode( + sys.getfilesystemencoding()))) + dest_file.close() + settings.record_dependencies.add(source) + return 1 + if os.path.isfile(dest): + return 1 + + def depart_document(self, node): + self.head_prefix.extend([self.doctype, + self.head_prefix_template % + {'lang': self.settings.language_code}]) + self.html_prolog.append(self.doctype) + self.meta.insert(0, self.content_type % self.settings.output_encoding) + self.head.insert(0, self.content_type % self.settings.output_encoding) + + header = ''.join(self.s5_header) + footer = ''.join(self.s5_footer) + title = ''.join(self.html_title).replace('

', '

') + layout = self.layout_template % {'header': header, + 'title': title, + 'footer': footer} + self.fragment.extend(self.body) + self.body_prefix.extend(layout) + self.body_prefix.append('
\n') + self.body_prefix.append( + self.starttag({'classes': ['slide'], 'ids': ['slide0']}, 'div')) + if not self.section_count: + self.body.append('
\n') + self.body_suffix.insert(0, '

\n') + # skip content-type meta tag with interpolated charset value: + self.html_head.extend(self.head[1:]) + self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo + + self.docinfo + self.body + + self.body_suffix[:-1]) + + def depart_footer(self, node): + start = self.context.pop() + self.s5_footer.append('

') + self.s5_footer.extend(self.body[start:]) + self.s5_footer.append('

') + del self.body[start:] + + def depart_header(self, node): + start = self.context.pop() + header = ['\n') + del self.body[start:] + self.s5_header.extend(header) + + def visit_section(self, node): + if not self.section_count: + self.body.append('\n\n') + self.section_count += 1 + self.section_level += 1 + if self.section_level > 1: + # dummy for matching div's + self.body.append(self.starttag(node, 'div', CLASS='section')) + else: + self.body.append(self.starttag(node, 'div', CLASS='slide')) + + def visit_subtitle(self, node): + if isinstance(node.parent, nodes.section): + level = self.section_level + self.initial_header_level - 1 + if level == 1: + level = 2 + tag = 'h%s' % level + self.body.append(self.starttag(node, tag, '')) + self.context.append('\n' % tag) + else: + html4css1.HTMLTranslator.visit_subtitle(self, node) + + def visit_title(self, node): + html4css1.HTMLTranslator.visit_title(self, node) diff --git a/docutils/writers/s5_html/themes/README.txt b/docutils/writers/s5_html/themes/README.txt new file mode 100644 index 000000000..2e01b51ee --- /dev/null +++ b/docutils/writers/s5_html/themes/README.txt @@ -0,0 +1,6 @@ +Except where otherwise noted (default/iepngfix.htc), all files in this +directory have been released into the Public Domain. + +These files are based on files from S5 1.1, released into the Public +Domain by Eric Meyer. For further details, please see +http://www.meyerweb.com/eric/tools/s5/credits.html. diff --git a/docutils/writers/s5_html/themes/big-black/__base__ b/docutils/writers/s5_html/themes/big-black/__base__ new file mode 100644 index 000000000..f08be9ad5 --- /dev/null +++ b/docutils/writers/s5_html/themes/big-black/__base__ @@ -0,0 +1,2 @@ +# base theme of this theme: +big-white diff --git a/docutils/writers/s5_html/themes/big-black/framing.css b/docutils/writers/s5_html/themes/big-black/framing.css new file mode 100644 index 000000000..5a31113fb --- /dev/null +++ b/docutils/writers/s5_html/themes/big-black/framing.css @@ -0,0 +1,25 @@ +/* The following styles size, place, and layer the slide components. + Edit these if you want to change the overall slide layout. + The commented lines can be uncommented (and modified, if necessary) + to help you with the rearrangement process. */ + +/* target = 1024x768 */ + +div#header, div#footer, .slide {width: 100%; top: 0; left: 0;} +div#header {top: 0; z-index: 1;} +div#footer {display:none;} +.slide {top: 0; width: 92%; padding: 0.1em 4% 4%; z-index: 2;} +/* list-style: none;} */ +div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;} +div#controls form {position: absolute; bottom: 0; right: 0; width: 100%; + margin: 0;} +#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em; + z-index: 10;} +html>body #currentSlide {position: fixed;} + +/* +div#header {background: #FCC;} +div#footer {background: #CCF;} +div#controls {background: #BBD;} +div#currentSlide {background: #FFC;} +*/ diff --git a/docutils/writers/s5_html/themes/big-black/pretty.css b/docutils/writers/s5_html/themes/big-black/pretty.css new file mode 100644 index 000000000..82bcc9dc4 --- /dev/null +++ b/docutils/writers/s5_html/themes/big-black/pretty.css @@ -0,0 +1,109 @@ +/* This file has been placed in the public domain. */ +/* Following are the presentation styles -- edit away! */ + +html, body {margin: 0; padding: 0;} +body {background: black; color: white;} +:link, :visited {text-decoration: none; color: cyan;} +#controls :active {color: #888 !important;} +#controls :focus {outline: 1px dotted #CCC;} + +blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;} +blockquote p {margin: 0;} + +kbd {font-weight: bold; font-size: 1em;} +sup {font-size: smaller; line-height: 1px;} + +.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;} +.slide ul ul li {list-style: square;} +.slide img.leader {display: block; margin: 0 auto;} +.slide tt {font-size: 90%;} + +.slide {font-size: 3em; font-family: sans-serif; font-weight: bold;} +.slide h1 {padding-top: 0; z-index: 1; margin: 0; font-size: 120%;} +.slide h2 {font-size: 110%;} +.slide h3 {font-size: 105%;} +h1 abbr {font-variant: small-caps;} + +div#controls {position: absolute; left: 50%; bottom: 0; + width: 50%; text-align: right; font: bold 0.9em sans-serif;} +html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;} +div#controls form {position: absolute; bottom: 0; right: 0; width: 100%; + margin: 0; padding: 0;} +#controls #navLinks a {padding: 0; margin: 0 0.5em; + border: none; color: #888; cursor: pointer;} +#controls #navList {height: 1em;} +#controls #navList #jumplist {position: absolute; bottom: 0; right: 0; + background: black; color: #CCC;} + +#currentSlide {text-align: center; font-size: 0.5em; color: #AAA; + font-family: sans-serif; font-weight: bold;} + +#slide0 h1 {position: static; margin: 0 0 0.5em; padding-top: 0.3em; top: 0; + font-size: 150%; white-space: normal; background: transparent;} +#slide0 h2 {font: 110%; font-style: italic; color: gray;} +#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;} +#slide0 h4 {margin-top: 0; font-size: 1em;} + +ul.urls {list-style: none; display: inline; margin: 0;} +.urls li {display: inline; margin: 0;} +.external {border-bottom: 1px dotted gray;} +html>body .external {border-bottom: none;} +.external:after {content: " \274F"; font-size: smaller; color: #FCC;} + +.incremental, .incremental *, .incremental *:after { + color: black; visibility: visible; border: 0;} +img.incremental {visibility: hidden;} +.slide .current {color: lime;} + +.slide-display {display: inline ! important;} + +.huge {font-size: 150%;} +.big {font-size: 120%;} +.small {font-size: 75%;} +.tiny {font-size: 50%;} +.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;} +.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;} + +.maroon {color: maroon;} +.red {color: red;} +.magenta {color: magenta;} +.fuchsia {color: fuchsia;} +.pink {color: #FAA;} +.orange {color: orange;} +.yellow {color: yellow;} +.lime {color: lime;} +.green {color: green;} +.olive {color: olive;} +.teal {color: teal;} +.cyan {color: cyan;} +.aqua {color: aqua;} +.blue {color: blue;} +.navy {color: navy;} +.purple {color: purple;} +.black {color: black;} +.gray {color: gray;} +.silver {color: silver;} +.white {color: white;} + +.left {text-align: left ! important;} +.center {text-align: center ! important;} +.right {text-align: right ! important;} + +.animation {position: relative; margin: 1em 0; padding: 0;} +.animation img {position: absolute;} + +/* Docutils-specific overrides */ + +.slide table.docinfo {margin: 0.5em 0 0.5em 1em;} + +div.sidebar {background-color: black;} + +pre.literal-block, pre.doctest-block {background-color: black;} + +tt.docutils {background-color: black;} + +/* diagnostics */ +/* +li:after {content: " [" attr(class) "]"; color: #F88;} +div:before {content: "[" attr(class) "]"; color: #F88;} +*/ diff --git a/docutils/writers/s5_html/themes/big-white/framing.css b/docutils/writers/s5_html/themes/big-white/framing.css new file mode 100644 index 000000000..cd343432b --- /dev/null +++ b/docutils/writers/s5_html/themes/big-white/framing.css @@ -0,0 +1,24 @@ +/* This file has been placed in the public domain. */ +/* The following styles size, place, and layer the slide components. + Edit these if you want to change the overall slide layout. + The commented lines can be uncommented (and modified, if necessary) + to help you with the rearrangement process. */ + +/* target = 1024x768 */ + +div#header, div#footer, .slide {width: 100%; top: 0; left: 0;} +div#footer {display:none;} +.slide {top: 0; width: 92%; padding: 0.25em 4% 4%; z-index: 2;} +div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;} +div#controls form {position: absolute; bottom: 0; right: 0; width: 100%; + margin: 0;} +#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em; + z-index: 10;} +html>body #currentSlide {position: fixed;} + +/* +div#header {background: #FCC;} +div#footer {background: #CCF;} +div#controls {background: #BBD;} +div#currentSlide {background: #FFC;} +*/ diff --git a/docutils/writers/s5_html/themes/big-white/pretty.css b/docutils/writers/s5_html/themes/big-white/pretty.css new file mode 100644 index 000000000..c5e2fcf97 --- /dev/null +++ b/docutils/writers/s5_html/themes/big-white/pretty.css @@ -0,0 +1,107 @@ +/* This file has been placed in the public domain. */ +/* Following are the presentation styles -- edit away! */ + +html, body {margin: 0; padding: 0;} +body {background: white; color: black;} +:link, :visited {text-decoration: none; color: #00C;} +#controls :active {color: #88A !important;} +#controls :focus {outline: 1px dotted #227;} + +blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;} +blockquote p {margin: 0;} + +kbd {font-weight: bold; font-size: 1em;} +sup {font-size: smaller; line-height: 1px;} + +.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;} +.slide ul ul li {list-style: square;} +.slide img.leader {display: block; margin: 0 auto;} +.slide tt {font-size: 90%;} + +.slide {font-size: 3em; font-family: sans-serif; font-weight: bold;} +.slide h1 {padding-top: 0; z-index: 1; margin: 0; font-size: 120%;} +.slide h2 {font-size: 110%;} +.slide h3 {font-size: 105%;} +h1 abbr {font-variant: small-caps;} + +div#controls {position: absolute; left: 50%; bottom: 0; + width: 50%; text-align: right; font: bold 0.9em sans-serif;} +html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;} +div#controls form {position: absolute; bottom: 0; right: 0; width: 100%; + margin: 0; padding: 0;} +#controls #navLinks a {padding: 0; margin: 0 0.5em; + border: none; color: #005; cursor: pointer;} +#controls #navList {height: 1em;} +#controls #navList #jumplist {position: absolute; bottom: 0; right: 0; + background: #DDD; color: #227;} + +#currentSlide {text-align: center; font-size: 0.5em; color: #444; + font-family: sans-serif; font-weight: bold;} + +#slide0 h1 {position: static; margin: 0 0 0.5em; padding-top: 0.3em; top: 0; + font-size: 150%; white-space: normal; background: transparent;} +#slide0 h2 {font: 110%; font-style: italic; color: gray;} +#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;} +#slide0 h4 {margin-top: 0; font-size: 1em;} + +ul.urls {list-style: none; display: inline; margin: 0;} +.urls li {display: inline; margin: 0;} +.external {border-bottom: 1px dotted gray;} +html>body .external {border-bottom: none;} +.external:after {content: " \274F"; font-size: smaller; color: #77B;} + +.incremental, .incremental *, .incremental *:after { + color: white; visibility: visible; border: 0;} +img.incremental {visibility: hidden;} +.slide .current {color: green;} + +.slide-display {display: inline ! important;} + +.huge {font-size: 150%;} +.big {font-size: 120%;} +.small {font-size: 75%;} +.tiny {font-size: 50%;} +.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;} +.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;} + +.maroon {color: maroon;} +.red {color: red;} +.magenta {color: magenta;} +.fuchsia {color: fuchsia;} +.pink {color: #FAA;} +.orange {color: orange;} +.yellow {color: yellow;} +.lime {color: lime;} +.green {color: green;} +.olive {color: olive;} +.teal {color: teal;} +.cyan {color: cyan;} +.aqua {color: aqua;} +.blue {color: blue;} +.navy {color: navy;} +.purple {color: purple;} +.black {color: black;} +.gray {color: gray;} +.silver {color: silver;} +.white {color: white;} + +.left {text-align: left ! important;} +.center {text-align: center ! important;} +.right {text-align: right ! important;} + +.animation {position: relative; margin: 1em 0; padding: 0;} +.animation img {position: absolute;} + +/* Docutils-specific overrides */ + +.slide table.docinfo {margin: 0.5em 0 0.5em 1em;} + +pre.literal-block, pre.doctest-block {background-color: white;} + +tt.docutils {background-color: white;} + +/* diagnostics */ +/* +li:after {content: " [" attr(class) "]"; color: #F88;} +div:before {content: "[" attr(class) "]"; color: #F88;} +*/ diff --git a/docutils/writers/s5_html/themes/default/blank.gif b/docutils/writers/s5_html/themes/default/blank.gif new file mode 100644 index 000000000..75b945d25 Binary files /dev/null and b/docutils/writers/s5_html/themes/default/blank.gif differ diff --git a/docutils/writers/s5_html/themes/default/framing.css b/docutils/writers/s5_html/themes/default/framing.css new file mode 100644 index 000000000..c4727f303 --- /dev/null +++ b/docutils/writers/s5_html/themes/default/framing.css @@ -0,0 +1,25 @@ +/* This file has been placed in the public domain. */ +/* The following styles size, place, and layer the slide components. + Edit these if you want to change the overall slide layout. + The commented lines can be uncommented (and modified, if necessary) + to help you with the rearrangement process. */ + +/* target = 1024x768 */ + +div#header, div#footer, .slide {width: 100%; top: 0; left: 0;} +div#header {position: fixed; top: 0; height: 3em; z-index: 1;} +div#footer {top: auto; bottom: 0; height: 2.5em; z-index: 5;} +.slide {top: 0; width: 92%; padding: 2.5em 4% 4%; z-index: 2;} +div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;} +div#controls form {position: absolute; bottom: 0; right: 0; width: 100%; + margin: 0;} +#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em; + z-index: 10;} +html>body #currentSlide {position: fixed;} + +/* +div#header {background: #FCC;} +div#footer {background: #CCF;} +div#controls {background: #BBD;} +div#currentSlide {background: #FFC;} +*/ diff --git a/docutils/writers/s5_html/themes/default/iepngfix.htc b/docutils/writers/s5_html/themes/default/iepngfix.htc new file mode 100644 index 000000000..9f3d628b5 --- /dev/null +++ b/docutils/writers/s5_html/themes/default/iepngfix.htc @@ -0,0 +1,42 @@ + + + + + \ No newline at end of file diff --git a/docutils/writers/s5_html/themes/default/opera.css b/docutils/writers/s5_html/themes/default/opera.css new file mode 100644 index 000000000..c9d1148be --- /dev/null +++ b/docutils/writers/s5_html/themes/default/opera.css @@ -0,0 +1,8 @@ +/* This file has been placed in the public domain. */ +/* DO NOT CHANGE THESE unless you really want to break Opera Show */ +.slide { + visibility: visible !important; + position: static !important; + page-break-before: always; +} +#slide0 {page-break-before: avoid;} diff --git a/docutils/writers/s5_html/themes/default/outline.css b/docutils/writers/s5_html/themes/default/outline.css new file mode 100644 index 000000000..fa767e227 --- /dev/null +++ b/docutils/writers/s5_html/themes/default/outline.css @@ -0,0 +1,16 @@ +/* This file has been placed in the public domain. */ +/* Don't change this unless you want the layout stuff to show up in the + outline view! */ + +.layout div, #footer *, #controlForm * {display: none;} +#footer, #controls, #controlForm, #navLinks, #toggle { + display: block; visibility: visible; margin: 0; padding: 0;} +#toggle {float: right; padding: 0.5em;} +html>body #toggle {position: fixed; top: 0; right: 0;} + +/* making the outline look pretty-ish */ + +#slide0 h1, #slide0 h2, #slide0 h3, #slide0 h4 {border: none; margin: 0;} +#toggle {border: 1px solid; border-width: 0 0 1px 1px; background: #FFF;} + +.outline {display: inline ! important;} diff --git a/docutils/writers/s5_html/themes/default/pretty.css b/docutils/writers/s5_html/themes/default/pretty.css new file mode 100644 index 000000000..1cede72d4 --- /dev/null +++ b/docutils/writers/s5_html/themes/default/pretty.css @@ -0,0 +1,120 @@ +/* This file has been placed in the public domain. */ +/* Following are the presentation styles -- edit away! */ + +html, body {margin: 0; padding: 0;} +body {background: white; color: black;} +/* Replace the background style above with the style below (and again for + div#header) for a graphic: */ +/* background: white url(bodybg.gif) -16px 0 no-repeat; */ +:link, :visited {text-decoration: none; color: #00C;} +#controls :active {color: #88A !important;} +#controls :focus {outline: 1px dotted #227;} +h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;} + +blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;} +blockquote p {margin: 0;} + +kbd {font-weight: bold; font-size: 1em;} +sup {font-size: smaller; line-height: 1px;} + +.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;} +.slide ul ul li {list-style: square;} +.slide img.leader {display: block; margin: 0 auto;} +.slide tt {font-size: 90%;} + +div#header, div#footer {background: #005; color: #AAB; font-family: sans-serif;} +/* background: #005 url(bodybg.gif) -16px 0 no-repeat; */ +div#footer {font-size: 0.5em; font-weight: bold; padding: 1em 0;} +#footer h1 {display: block; padding: 0 1em;} +#footer h2 {display: block; padding: 0.8em 1em 0;} + +.slide {font-size: 1.2em;} +.slide h1 {position: absolute; top: 0.45em; z-index: 1; + margin: 0; padding-left: 0.7em; white-space: nowrap; + font: bold 150% sans-serif; color: #DDE; background: #005;} +.slide h2 {font: bold 120%/1em sans-serif; padding-top: 0.5em;} +.slide h3 {font: bold 100% sans-serif; padding-top: 0.5em;} +h1 abbr {font-variant: small-caps;} + +div#controls {position: absolute; left: 50%; bottom: 0; + width: 50%; text-align: right; font: bold 0.9em sans-serif;} +html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;} +div#controls form {position: absolute; bottom: 0; right: 0; width: 100%; + margin: 0; padding: 0;} +#controls #navLinks a {padding: 0; margin: 0 0.5em; + background: #005; border: none; color: #779; cursor: pointer;} +#controls #navList {height: 1em;} +#controls #navList #jumplist {position: absolute; bottom: 0; right: 0; + background: #DDD; color: #227;} + +#currentSlide {text-align: center; font-size: 0.5em; color: #449; + font-family: sans-serif; font-weight: bold;} + +#slide0 {padding-top: 1.5em} +#slide0 h1 {position: static; margin: 1em 0 0; padding: 0; color: #000; + font: bold 2em sans-serif; white-space: normal; background: transparent;} +#slide0 h2 {font: bold italic 1em sans-serif; margin: 0.25em;} +#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;} +#slide0 h4 {margin-top: 0; font-size: 1em;} + +ul.urls {list-style: none; display: inline; margin: 0;} +.urls li {display: inline; margin: 0;} +.external {border-bottom: 1px dotted gray;} +html>body .external {border-bottom: none;} +.external:after {content: " \274F"; font-size: smaller; color: #77B;} + +.incremental, .incremental *, .incremental *:after {visibility: visible; + color: white; border: 0;} +img.incremental {visibility: hidden;} +.slide .current {color: green;} + +.slide-display {display: inline ! important;} + +.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;} +.big {font-family: sans-serif; font-weight: bold; font-size: 120%;} +.small {font-size: 75%;} +.tiny {font-size: 50%;} +.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;} +.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;} + +.maroon {color: maroon;} +.red {color: red;} +.magenta {color: magenta;} +.fuchsia {color: fuchsia;} +.pink {color: #FAA;} +.orange {color: orange;} +.yellow {color: yellow;} +.lime {color: lime;} +.green {color: green;} +.olive {color: olive;} +.teal {color: teal;} +.cyan {color: cyan;} +.aqua {color: aqua;} +.blue {color: blue;} +.navy {color: navy;} +.purple {color: purple;} +.black {color: black;} +.gray {color: gray;} +.silver {color: silver;} +.white {color: white;} + +.left {text-align: left ! important;} +.center {text-align: center ! important;} +.right {text-align: right ! important;} + +.animation {position: relative; margin: 1em 0; padding: 0;} +.animation img {position: absolute;} + +/* Docutils-specific overrides */ + +.slide table.docinfo {margin: 1em 0 0.5em 2em;} + +pre.literal-block, pre.doctest-block {background-color: white;} + +tt.docutils {background-color: white;} + +/* diagnostics */ +/* +li:after {content: " [" attr(class) "]"; color: #F88;} +div:before {content: "[" attr(class) "]"; color: #F88;} +*/ diff --git a/docutils/writers/s5_html/themes/default/print.css b/docutils/writers/s5_html/themes/default/print.css new file mode 100644 index 000000000..9d057cc8c --- /dev/null +++ b/docutils/writers/s5_html/themes/default/print.css @@ -0,0 +1,24 @@ +/* This file has been placed in the public domain. */ +/* The following rule is necessary to have all slides appear in print! + DO NOT REMOVE IT! */ +.slide, ul {page-break-inside: avoid; visibility: visible !important;} +h1 {page-break-after: avoid;} + +body {font-size: 12pt; background: white;} +* {color: black;} + +#slide0 h1 {font-size: 200%; border: none; margin: 0.5em 0 0.25em;} +#slide0 h3 {margin: 0; padding: 0;} +#slide0 h4 {margin: 0 0 0.5em; padding: 0;} +#slide0 {margin-bottom: 3em;} + +#header {display: none;} +#footer h1 {margin: 0; border-bottom: 1px solid; color: gray; + font-style: italic;} +#footer h2, #controls {display: none;} + +.print {display: inline ! important;} + +/* The following rule keeps the layout stuff out of print. + Remove at your own risk! */ +.layout, .layout * {display: none !important;} diff --git a/docutils/writers/s5_html/themes/default/s5-core.css b/docutils/writers/s5_html/themes/default/s5-core.css new file mode 100644 index 000000000..6965f5e8f --- /dev/null +++ b/docutils/writers/s5_html/themes/default/s5-core.css @@ -0,0 +1,11 @@ +/* This file has been placed in the public domain. */ +/* Do not edit or override these styles! + The system will likely break if you do. */ + +div#header, div#footer, div#controls, .slide {position: absolute;} +html>body div#header, html>body div#footer, + html>body div#controls, html>body .slide {position: fixed;} +.handout {display: none;} +.layout {display: block;} +.slide, .hideme, .incremental {visibility: hidden;} +#slide0 {visibility: visible;} diff --git a/docutils/writers/s5_html/themes/default/slides.css b/docutils/writers/s5_html/themes/default/slides.css new file mode 100644 index 000000000..82bdc0ee0 --- /dev/null +++ b/docutils/writers/s5_html/themes/default/slides.css @@ -0,0 +1,10 @@ +/* This file has been placed in the public domain. */ + +/* required to make the slide show run at all */ +@import url(s5-core.css); + +/* sets basic placement and size of slide components */ +@import url(framing.css); + +/* styles that make the slides look good */ +@import url(pretty.css); diff --git a/docutils/writers/s5_html/themes/default/slides.js b/docutils/writers/s5_html/themes/default/slides.js new file mode 100644 index 000000000..81e04e5d4 --- /dev/null +++ b/docutils/writers/s5_html/themes/default/slides.js @@ -0,0 +1,558 @@ +// S5 v1.1 slides.js -- released into the Public Domain +// Modified for Docutils (http://docutils.sf.net) by David Goodger +// +// Please see http://www.meyerweb.com/eric/tools/s5/credits.html for +// information about all the wonderful and talented contributors to this code! + +var undef; +var slideCSS = ''; +var snum = 0; +var smax = 1; +var slideIDs = new Array(); +var incpos = 0; +var number = undef; +var s5mode = true; +var defaultView = 'slideshow'; +var controlVis = 'visible'; + +var isIE = navigator.appName == 'Microsoft Internet Explorer' ? 1 : 0; +var isOp = navigator.userAgent.indexOf('Opera') > -1 ? 1 : 0; +var isGe = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('Safari') < 1 ? 1 : 0; + +function hasClass(object, className) { + if (!object.className) return false; + return (object.className.search('(^|\\s)' + className + '(\\s|$)') != -1); +} + +function hasValue(object, value) { + if (!object) return false; + return (object.search('(^|\\s)' + value + '(\\s|$)') != -1); +} + +function removeClass(object,className) { + if (!object) return; + object.className = object.className.replace(new RegExp('(^|\\s)'+className+'(\\s|$)'), RegExp.$1+RegExp.$2); +} + +function addClass(object,className) { + if (!object || hasClass(object, className)) return; + if (object.className) { + object.className += ' '+className; + } else { + object.className = className; + } +} + +function GetElementsWithClassName(elementName,className) { + var allElements = document.getElementsByTagName(elementName); + var elemColl = new Array(); + for (var i = 0; i< allElements.length; i++) { + if (hasClass(allElements[i], className)) { + elemColl[elemColl.length] = allElements[i]; + } + } + return elemColl; +} + +function isParentOrSelf(element, id) { + if (element == null || element.nodeName=='BODY') return false; + else if (element.id == id) return true; + else return isParentOrSelf(element.parentNode, id); +} + +function nodeValue(node) { + var result = ""; + if (node.nodeType == 1) { + var children = node.childNodes; + for (var i = 0; i < children.length; ++i) { + result += nodeValue(children[i]); + } + } + else if (node.nodeType == 3) { + result = node.nodeValue; + } + return(result); +} + +function slideLabel() { + var slideColl = GetElementsWithClassName('*','slide'); + var list = document.getElementById('jumplist'); + smax = slideColl.length; + for (var n = 0; n < smax; n++) { + var obj = slideColl[n]; + + var did = 'slide' + n.toString(); + if (obj.getAttribute('id')) { + slideIDs[n] = obj.getAttribute('id'); + } + else { + obj.setAttribute('id',did); + slideIDs[n] = did; + } + if (isOp) continue; + + var otext = ''; + var menu = obj.firstChild; + if (!menu) continue; // to cope with empty slides + while (menu && menu.nodeType == 3) { + menu = menu.nextSibling; + } + if (!menu) continue; // to cope with slides with only text nodes + + var menunodes = menu.childNodes; + for (var o = 0; o < menunodes.length; o++) { + otext += nodeValue(menunodes[o]); + } + list.options[list.length] = new Option(n + ' : ' + otext, n); + } +} + +function currentSlide() { + var cs; + var footer_nodes; + var vis = 'visible'; + if (document.getElementById) { + cs = document.getElementById('currentSlide'); + footer_nodes = document.getElementById('footer').childNodes; + } else { + cs = document.currentSlide; + footer = document.footer.childNodes; + } + cs.innerHTML = '' + snum + '<\/span> ' + + '\/<\/span> ' + + '' + (smax-1) + '<\/span>'; + if (snum == 0) { + vis = 'hidden'; + } + cs.style.visibility = vis; + for (var i = 0; i < footer_nodes.length; i++) { + if (footer_nodes[i].nodeType == 1) { + footer_nodes[i].style.visibility = vis; + } + } +} + +function go(step) { + if (document.getElementById('slideProj').disabled || step == 0) return; + var jl = document.getElementById('jumplist'); + var cid = slideIDs[snum]; + var ce = document.getElementById(cid); + if (incrementals[snum].length > 0) { + for (var i = 0; i < incrementals[snum].length; i++) { + removeClass(incrementals[snum][i], 'current'); + removeClass(incrementals[snum][i], 'incremental'); + } + } + if (step != 'j') { + snum += step; + lmax = smax - 1; + if (snum > lmax) snum = lmax; + if (snum < 0) snum = 0; + } else + snum = parseInt(jl.value); + var nid = slideIDs[snum]; + var ne = document.getElementById(nid); + if (!ne) { + ne = document.getElementById(slideIDs[0]); + snum = 0; + } + if (step < 0) {incpos = incrementals[snum].length} else {incpos = 0;} + if (incrementals[snum].length > 0 && incpos == 0) { + for (var i = 0; i < incrementals[snum].length; i++) { + if (hasClass(incrementals[snum][i], 'current')) + incpos = i + 1; + else + addClass(incrementals[snum][i], 'incremental'); + } + } + if (incrementals[snum].length > 0 && incpos > 0) + addClass(incrementals[snum][incpos - 1], 'current'); + ce.style.visibility = 'hidden'; + ne.style.visibility = 'visible'; + jl.selectedIndex = snum; + currentSlide(); + number = 0; +} + +function goTo(target) { + if (target >= smax || target == snum) return; + go(target - snum); +} + +function subgo(step) { + if (step > 0) { + removeClass(incrementals[snum][incpos - 1],'current'); + removeClass(incrementals[snum][incpos], 'incremental'); + addClass(incrementals[snum][incpos],'current'); + incpos++; + } else { + incpos--; + removeClass(incrementals[snum][incpos],'current'); + addClass(incrementals[snum][incpos], 'incremental'); + addClass(incrementals[snum][incpos - 1],'current'); + } +} + +function toggle() { + var slideColl = GetElementsWithClassName('*','slide'); + var slides = document.getElementById('slideProj'); + var outline = document.getElementById('outlineStyle'); + if (!slides.disabled) { + slides.disabled = true; + outline.disabled = false; + s5mode = false; + fontSize('1em'); + for (var n = 0; n < smax; n++) { + var slide = slideColl[n]; + slide.style.visibility = 'visible'; + } + } else { + slides.disabled = false; + outline.disabled = true; + s5mode = true; + fontScale(); + for (var n = 0; n < smax; n++) { + var slide = slideColl[n]; + slide.style.visibility = 'hidden'; + } + slideColl[snum].style.visibility = 'visible'; + } +} + +function showHide(action) { + var obj = GetElementsWithClassName('*','hideme')[0]; + switch (action) { + case 's': obj.style.visibility = 'visible'; break; + case 'h': obj.style.visibility = 'hidden'; break; + case 'k': + if (obj.style.visibility != 'visible') { + obj.style.visibility = 'visible'; + } else { + obj.style.visibility = 'hidden'; + } + break; + } +} + +// 'keys' code adapted from MozPoint (http://mozpoint.mozdev.org/) +function keys(key) { + if (!key) { + key = event; + key.which = key.keyCode; + } + if (key.which == 84) { + toggle(); + return; + } + if (s5mode) { + switch (key.which) { + case 10: // return + case 13: // enter + if (window.event && isParentOrSelf(window.event.srcElement, 'controls')) return; + if (key.target && isParentOrSelf(key.target, 'controls')) return; + if(number != undef) { + goTo(number); + break; + } + case 32: // spacebar + case 34: // page down + case 39: // rightkey + case 40: // downkey + if(number != undef) { + go(number); + } else if (!incrementals[snum] || incpos >= incrementals[snum].length) { + go(1); + } else { + subgo(1); + } + break; + case 33: // page up + case 37: // leftkey + case 38: // upkey + if(number != undef) { + go(-1 * number); + } else if (!incrementals[snum] || incpos <= 0) { + go(-1); + } else { + subgo(-1); + } + break; + case 36: // home + goTo(0); + break; + case 35: // end + goTo(smax-1); + break; + case 67: // c + showHide('k'); + break; + } + if (key.which < 48 || key.which > 57) { + number = undef; + } else { + if (window.event && isParentOrSelf(window.event.srcElement, 'controls')) return; + if (key.target && isParentOrSelf(key.target, 'controls')) return; + number = (((number != undef) ? number : 0) * 10) + (key.which - 48); + } + } + return false; +} + +function clicker(e) { + number = undef; + var target; + if (window.event) { + target = window.event.srcElement; + e = window.event; + } else target = e.target; + if (target.href != null || hasValue(target.rel, 'external') || isParentOrSelf(target, 'controls') || isParentOrSelf(target,'embed') || isParentOrSelf(target, 'object')) return true; + if (!e.which || e.which == 1) { + if (!incrementals[snum] || incpos >= incrementals[snum].length) { + go(1); + } else { + subgo(1); + } + } +} + +function findSlide(hash) { + var target = document.getElementById(hash); + if (target) { + for (var i = 0; i < slideIDs.length; i++) { + if (target.id == slideIDs[i]) return i; + } + } + return null; +} + +function slideJump() { + if (window.location.hash == null || window.location.hash == '') { + currentSlide(); + return; + } + if (window.location.hash == null) return; + var dest = null; + dest = findSlide(window.location.hash.slice(1)); + if (dest == null) { + dest = 0; + } + go(dest - snum); +} + +function fixLinks() { + var thisUri = window.location.href; + thisUri = thisUri.slice(0, thisUri.length - window.location.hash.length); + var aelements = document.getElementsByTagName('A'); + for (var i = 0; i < aelements.length; i++) { + var a = aelements[i].href; + var slideID = a.match('\#.+'); + if ((slideID) && (slideID[0].slice(0,1) == '#')) { + var dest = findSlide(slideID[0].slice(1)); + if (dest != null) { + if (aelements[i].addEventListener) { + aelements[i].addEventListener("click", new Function("e", + "if (document.getElementById('slideProj').disabled) return;" + + "go("+dest+" - snum); " + + "if (e.preventDefault) e.preventDefault();"), true); + } else if (aelements[i].attachEvent) { + aelements[i].attachEvent("onclick", new Function("", + "if (document.getElementById('slideProj').disabled) return;" + + "go("+dest+" - snum); " + + "event.returnValue = false;")); + } + } + } + } +} + +function externalLinks() { + if (!document.getElementsByTagName) return; + var anchors = document.getElementsByTagName('a'); + for (var i=0; i' + + '