diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py | 3461 |
1 files changed, 3461 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py new file mode 100644 index 00000000..c538af34 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py @@ -0,0 +1,3461 @@ +# $Id: __init__.py 9541 2024-02-17 10:37:13Z milde $ +# Author: Dave Kuhlman <dkuhlman@davekuhlman.org> +# Copyright: This module has been placed in the public domain. + +""" +Open Document Format (ODF) Writer. + +This module is provisional: +the API is not settled and may change with any minor Docutils version. +""" + +__docformat__ = 'reStructuredText' + + +from configparser import ConfigParser +import copy +from io import StringIO +import itertools +import locale +import os +import os.path +from pathlib import Path +import re +import subprocess +import tempfile +import time +import urllib +import weakref +from xml.etree import ElementTree as etree +from xml.dom import minidom +import zipfile + +import docutils +from docutils import frontend, nodes, utils, writers, languages +from docutils.parsers.rst.directives.images import PIL # optional +from docutils.readers import standalone +from docutils.transforms import references + +# Import pygments and odtwriter pygments formatters if possible. +try: + import pygments + import pygments.lexers + from .pygmentsformatter import (OdtPygmentsProgFormatter, + OdtPygmentsLaTeXFormatter) +except (ImportError, SyntaxError): + pygments = 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.') + +VERSION = '1.0a' + +IMAGE_NAME_COUNTER = itertools.count() + + +# +# 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. +# +_parents = weakref.WeakKeyDictionary() +if isinstance(etree.Element, type): + _ElementInterface = etree.Element +else: + _ElementInterface = etree._ElementInterface + + +class _ElementInterfaceWrapper(_ElementInterface): + def __init__(self, tag, attrib=None): + _ElementInterface.__init__(self, tag, attrib) + _parents[self] = None + + def setparent(self, parent): + _parents[self] = parent + + def getparent(self): + return _parents[self] + + +# +# 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, 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) + return _ElementInterfaceWrapper(tag, attrib) + + +def SubElement(parent, tag, attrib=None, nsmap=None, nsdict=CNSD): + if attrib is None: + attrib = {} + tag, attrib = fix_ns(tag, attrib, nsdict) + 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 list(attrib.items()): + nskey = add_ns(key, nsdict) + nsattrib[nskey] = val + return nstag, nsattrib + + +def add_ns(tag, nsdict=CNSD): + return tag + + +def ToString(et): + outstream = StringIO() + et.write(outstream, encoding="unicode") + 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 + + +# +# Classes +# + + +class TableStyle: + 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: + 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 an ODF-specific configuration/mapping file ' + 'relative to the current working directory.', + ['--odf-config-file'], + {'metavar': '<file>'}), + ('Obfuscate email addresses to confuse harvesters.', + ['--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, + 'metavar': '<int>', + '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 a native ODF table of contents.', + ['--generate-list-toc'], + {'action': 'store_false', + 'dest': 'generate_oowriter_toc', + 'validator': frontend.validate_boolean}), + ('Generate a native ODF 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', + 'metavar': '<custom header>'}), + ('Specify the contents of an custom footer line. ' + 'See ODF/ODT writer documentation for details.', + ['--custom-odt-footer'], + {'default': '', + 'dest': 'custom_footer', + 'metavar': '<custom footer>'}), + ) + ) + + settings_defaults = { + 'output_encoding_error_handler': 'xmlcharrefreplace', + } + + relative_path_settings = ('odf_config_file', 'stylesheet',) + + config_section = 'odf_odt 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() + # Set default language in document to be generated. + # Language is specified by the -l/--language command line option. + # The format is described in BCP 47. If region is omitted, we use + # local.normalize(ll) to obtain a region. + language_code = None + region_code = None + if self.visitor.language_code: + language_ids = self.visitor.language_code.replace('_', '-') + language_ids = language_ids.split('-') + # first tag is primary language tag + language_code = language_ids[0].lower() + # 2-letter region subtag may follow in 2nd or 3rd position + for subtag in language_ids[1:]: + if len(subtag) == 2 and subtag.isalpha(): + region_code = subtag.upper() + break + elif len(subtag) == 1: + break # 1-letter tag is never before valid region tag + if region_code is None: + try: + rcode = locale.normalize(language_code) + except NameError: + rcode = language_code + rcode = rcode.split('_') + if len(rcode) > 1: + rcode = rcode[1].split('.') + region_code = rcode[0] + if region_code is None: + self.document.reporter.warning( + 'invalid language-region.\n' + ' Could not find region with locale.normalize().\n' + ' Please specify both language and region (ll-RR).\n' + ' Examples: es-MX (Spanish, Mexico),\n' + ' en-AU (English, Australia).') + # Update the style ElementTree with the language and region. + # Note that we keep a reference to the modified node because + # it is possible that ElementTree will throw away the Python + # representation of the updated node if we do not. + updated, new_dom_styles, updated_node = self.update_stylesheet( + self.visitor.get_dom_stylesheet(), language_code, region_code) + if updated: + s1 = etree.tostring(new_dom_styles) + 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 update_stylesheet(self, stylesheet_root, language_code, region_code): + """Update xml style sheet element with language and region/country.""" + updated = False + modified_nodes = set() + if language_code is not None or region_code is not None: + n1 = stylesheet_root.find( + '{urn:oasis:names:tc:opendocument:xmlns:office:1.0}' + 'styles') + if n1 is None: + raise RuntimeError( + "Cannot find 'styles' element in styles.odt/styles.xml") + n2_nodes = n1.findall( + '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}' + 'default-style') + if not n2_nodes: + raise RuntimeError( + "Cannot find 'default-style' " + "element in styles.xml") + for node in n2_nodes: + family = node.attrib.get( + '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}' + 'family') + if family == 'paragraph' or family == 'graphic': + n3 = node.find( + '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}' + 'text-properties') + if n3 is None: + raise RuntimeError( + "Cannot find 'text-properties' " + "element in styles.xml") + if language_code is not None: + n3.attrib[ + '{urn:oasis:names:tc:opendocument:xmlns:' + 'xsl-fo-compatible:1.0}language'] = language_code + n3.attrib[ + '{urn:oasis:names:tc:opendocument:xmlns:' + 'style:1.0}language-complex'] = language_code + updated = True + modified_nodes.add(n3) + if region_code is not None: + n3.attrib[ + '{urn:oasis:names:tc:opendocument:xmlns:' + 'xsl-fo-compatible:1.0}country'] = region_code + n3.attrib[ + '{urn:oasis:names:tc:opendocument:xmlns:' + 'style:1.0}country-complex'] = region_code + updated = True + modified_nodes.add(n3) + return updated, stylesheet_root, modified_nodes + + 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) << 16 + 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: + zfile.write(source, destination) + except OSError: + 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. + """ + return self.visitor.setup_page() + + 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): + 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) + return doc.toprettyxml(' ') + + def create_meta(self): + 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]' + for prop, value in self.visitor.get_meta_dict().items(): + # 'keywords', 'description', and 'subject' have their own fields: + if prop == 'keywords': + keywords = re.split(', *', value) + for keyword in keywords: + el1 = SubElement(root, 'meta:keyword', nsdict=METNSD) + el1.text = keyword + elif prop == 'description': + el1 = SubElement(root, 'dc:description', nsdict=METNSD) + el1.text = value + elif prop == 'subject': + el1 = SubElement(root, 'dc:subject', nsdict=METNSD) + el1.text = value + else: # Store remaining properties as custom/user-defined + el1 = SubElement(root, 'meta:user-defined', + attrib={'meta:name': prop}, nsdict=METNSD) + el1.text = value + 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 + self.language_code = self.settings.language_code + self.language = languages.get_language( + self.language_code, + document.reporter) + self.format_map = {} + if self.settings.odf_config_file: + 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 + self.section_level = 0 + self.section_count = 0 + # Create ElementTree content and styles documents. + 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 + + # Keep track of nested styling classes + self.inline_style_count_stack = [] + + 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': + with open(stylespath, 'r', encoding='utf-8') as stylesfile: + s1 = stylesfile.read() + 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) + el = self.find_first_text_p(self.body_text_element) + if el is not None: + self.attach_page_style(el) + + def find_first_text_p(self, el): + """Search the generated doc and return the first <text:p> element. + """ + if el.tag == 'text:p' or el.tag == 'text:h': + return el + else: + for child in el: + el1 = self.find_first_text_p(child) + if el1 is not None: + return el1 + return None + + def attach_page_style(self, el): + """Attach the default page style. + + Create an automatic-style that refers to the current style + of this element and that refers to the default page style. + """ + current_style = el.get('text:style-name') + style_name = 'P1003' + el1 = SubElement( + self.automatic_styles, 'style:style', attrib={ + 'style:name': style_name, + 'style:master-page-name': "rststyle-pagedefault", + 'style:family': "paragraph", + }, nsdict=SNSD) + if current_style: + el1.set('style:parent-style-name', current_style) + el.set('text:style-name', style_name) + + 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 + return self.format_map.get(name1, 'rststyle-%s' % name1) + + 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) + return etree.tostring(self.dom_stylesheet) + + def get_dom_stylesheet(self): + return self.dom_stylesheet + + def setup_paper(self, root_el): + # TODO: only call paperconf, if it is actually used + # (i.e. page size removed from "styles.odt" with rst2odt_prepstyles.py + # cf. conditional in walk() below)? + try: + dimensions = subprocess.check_output(('paperconf', '-s'), + stderr=subprocess.STDOUT) + w, h = (float(s) for s in dimensions.split()) + except (subprocess.CalledProcessError, FileNotFoundError, ValueError): + self.document.reporter.info( + 'Cannot use `paperconf`, defaulting to Letter.') + w, h = 612, 792 # default to Letter + + def walk(el): + if el.tag == "{%s}page-layout-properties" % SNSD["style"] and \ + "{%s}page-width" % SNSD["fo"] not in el.attrib: + 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: + 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_container = master_el.findall(path) + master_el = None + target_attrib = '{%s}name' % (SNSD['style'], ) + target_name = self.rststyle('pagedefault') + for el in master_el_container: + if el.get(target_attrib) == target_name: + master_el = el + break + if master_el is None: + return + el1 = master_el + if self.header_content or self.settings.custom_header: + 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: + self.create_custom_headfoot( + el2, + self.settings.custom_header, 'header', automatic_styles) + if self.footer_content or self.settings.custom_footer: + 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: + self.create_custom_headfoot( + el2, + self.settings.custom_footer, 'footer', automatic_styles) + + code_none, code_field, code_text = list(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 + 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) + return ToString(et) + + 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: + if len(el2) > 0: # and 'id' in el2.attrib: + child = el2[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 + return SubElement(parent, tag, attrib) + + 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 + return self.append_p('blockindent') + + 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): + return text.replace('\n', " ") + + # + # 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 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) > 0: + if self.current_element[-1].tail: + self.current_element[-1].tail += text + else: + self.current_element[-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): + self.generate_labeled_line(node, 'revision') + + def depart_revision(self, node): + pass + + def visit_version(self, node): + 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): + 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 'classes' in node 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() + + 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): + if len(self.current_element) > 0: + el = self.current_element[-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) + if self.settings.create_links: + el0 = SubElement(el, 'text:span') + el0.text = '[' + self.append_child('text:reference-mark-start', attrib={ + 'text:name': '%s' % (self.citation_id, ), + }) + else: + el.text = '[' + + def depart_label(self, node): + if isinstance(node.parent, docutils.nodes.footnote): + pass + elif self.citation_id is not None: + if self.settings.create_links: + self.append_child('text:reference-mark-end', attrib={ + 'text:name': '%s' % (self.citation_id, ), + }) + el0 = SubElement(self.current_element, 'text:span') + el0.text = ']' + else: + self.current_element.text += ']' + 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. + source = node['uri'] + uri_parts = urllib.parse.urlparse(source) + if uri_parts.scheme in ('', 'file'): + source = urllib.parse.unquote(uri_parts.path) + if source.startswith('/'): + root_prefix = Path(self.settings.root_prefix) + source = (root_prefix/source[1:]).as_posix() + else: + # adapt relative paths + docsource, line = utils.get_source_line(node) + if docsource: + dirname = os.path.dirname(docsource) + if dirname: + source = os.path.join(dirname, source) + if not self.check_file_exists(source): + self.document.reporter.warning( + f'Cannot find image file "{source}".') + 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 uri_parts.scheme in ('', 'file'): + spec = (os.path.abspath(source), destination,) + else: + try: + with urllib.request.urlopen(source) as imgfile: + content = imgfile.read() + except urllib.error.URLError as err: + self.document.reporter.warning( + f'Cannot open image URL "{source}". {err}') + return + with tempfile.NamedTemporaryFile('wb', + delete=False) as imgfile2: + imgfile2.write(content) + source = imgfile2.name + spec = (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): + self.generate_image(node, source, destination, el2) + + def depart_image(self, node): + pass + + def get_image_width_height(self, node, attr): + size = None + unit = None + if attr in node.attributes: + size = node.attributes[attr] + size = size.strip() + # For conversion factors, see: + # http://www.unitconversion.org/unit_converter/typography-ex.html + try: + if size.endswith('%'): + if attr == 'height': + # Percentage allowed for width but not height. + raise ValueError('percentage not allowed for height') + size = size.rstrip(' %') + size = float(size) / 100.0 + unit = '%' + else: + size, unit = self.convert_to_cm(size) + except ValueError as exp: + self.document.reporter.warning( + 'Invalid %s for image: "%s". ' + 'Error: "%s".' % ( + attr, node.attributes[attr], exp)) + return size, unit + + def convert_to_cm(self, size): + """Convert various units to centimeters. + + Note that a call to this method should be wrapped in: + try: except ValueError: + """ + size = size.strip() + if size.endswith('px'): + size = float(size[:-2]) * 0.026 # convert px to cm + elif size.endswith('in'): + size = float(size[:-2]) * 2.54 # convert in to cm + elif size.endswith('pt'): + size = float(size[:-2]) * 0.035 # convert pt to cm + elif size.endswith('pc'): + size = float(size[:-2]) * 2.371 # convert pc to cm + elif size.endswith('mm'): + size = float(size[:-2]) * 0.1 # convert mm to cm + elif size.endswith('cm'): + size = float(size[:-2]) + else: + raise ValueError('unknown unit type') + unit = 'cm' + return size, unit + + def get_image_scale(self, node): + if 'scale' in node.attributes: + scale = node.attributes['scale'] + try: + scale = int(scale) + except ValueError: + self.document.reporter.warning( + 'Invalid scale for image: "%s"' % ( + 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 + else: + scale = 1.0 + return scale + + def get_image_scaled_width_height(self, node, source): + """Return the image size in centimeters adjusted by image attrs.""" + scale = self.get_image_scale(node) + width, width_unit = 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] + with PIL.Image.open(filename, 'r') as img: + img_size = img.size + dpi = img.info.get('dpi', dpi) + # dpi information can be (xdpi, ydpi) or xydpi + try: + iter(dpi) + except TypeError: + dpi = (dpi, dpi) + else: + img_size = None + if width is None or height is None: + if img_size is None: + raise RuntimeError( + 'image size not fully specified and PIL not installed') + if width is None: + width = img_size[0] + width = float(width) * 0.026 # convert px to cm + if height is None: + height = img_size[1] + height = float(height) * 0.026 # convert px to cm + if width_unit == '%': + factor = width + image_width = img_size[0] + image_width = float(image_width) * 0.026 # convert px to cm + image_height = img_size[1] + image_height = float(image_height) * 0.026 # convert px to cm + line_width = self.get_page_width() + width = factor * line_width + factor = (factor * line_width) / image_width + height = factor * image_height + width *= scale + height *= scale + width = '%.2fcm' % width + height = '%.2fcm' % height + return width, height + + def get_page_width(self): + """Return the document's page width in centimeters.""" + root = self.get_dom_stylesheet() + nodes = root.iterfind( + './/{urn:oasis:names:tc:opendocument:xmlns:style:1.0}' + 'page-layout/' + '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}' + 'page-layout-properties') + width = None + for node in nodes: + page_width = node.get( + '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}' + 'page-width') + margin_left = node.get( + '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}' + 'margin-left') + margin_right = node.get( + '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}' + 'margin-right') + if (page_width is None + or margin_left is None + or margin_right is None): + continue + try: + page_width, _ = self.convert_to_cm(page_width) + margin_left, _ = self.convert_to_cm(margin_left) + margin_right, _ = self.convert_to_cm(margin_right) + except ValueError: + self.document.reporter.warning( + 'Stylesheet file contains invalid page width ' + 'or margin size.') + width = page_width - margin_left - margin_right + if width is None: + # We can't find the width in styles, so we make a guess. + # Use a width of 6 in = 15.24 cm. + width = 15.24 + return width + + 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', + } + 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', + } + SubElement(el1, 'style:text-properties', + attrib=attrib, nsdict=SNSD) + style_name = 'rstframestyle%d' % self.image_style_count + draw_name = 'graphics%d' % next(IMAGE_NAME_COUNTER) + # 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) + 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' + SubElement(el1, 'style:graphic-properties', + attrib=attrib, nsdict=SNSD) + attrib = { + 'draw:style-name': style_name, + 'draw:name': draw_name, + 'text:anchor-type': 'paragraph', + 'draw:z-index': '0', + } + attrib['svg:width'] = width + 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' + SubElement(el1, 'style:graphic-properties', + attrib=attrib, nsdict=SNSD) + draw_name = 'graphics%d' % next(IMAGE_NAME_COUNTER) + # Add the content. + # el = SubElement(current_element, 'text:p', + # attrib={'text:style-name': self.rststyle('textbody')}) + attrib = { + 'draw:style-name': style_name, + 'draw:name': draw_name, + '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) + 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 styles: + el = self.current_element + for inline_style in styles: + el = SubElement(el, 'text:span', + attrib={'text:style-name': + self.rststyle(inline_style)}) + count = len(styles) + else: + # No style was specified so use a default style (old code + # crashed if no style was given) + el = SubElement(self.current_element, 'text:span') + count = 1 + + self.set_current_element(el) + self.inline_style_count_stack.append(count) + + def depart_inline(self, node): + count = self.inline_style_count_stack.pop() + for x in range(count): + 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) + return pygments.highlight(insource, lexer, fmtr) + + def fill_line(self, line): + line = FILL_PAT1.sub(self.fill_func1, line) + return FILL_PAT2.sub(self.fill_func2, line) + + def fill_func1(self, matchobj): + spaces = matchobj.group(0) + return '<text:s text:c="%d"/>' % (len(spaces), ) + + def fill_func2(self, matchobj): + spaces = matchobj.group(0) + return ' <text:s text:c="%d"/>' % (len(spaces) - 1, ) + + def visit_literal_block(self, node): + if len(self.paragraph_style_stack) > 1: + wrapper1 = '<text:p text:style-name="%s">%%s</text:p>' % ( + self.rststyle('codeblock-indented'), ) + else: + wrapper1 = '<text:p text:style-name="%s">%%s</text:p>' % ( + self.rststyle('codeblock'), ) + source = node.astext() + if (pygments and self.settings.add_syntax_highlighting): + 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 = ['<wrappertag1 xmlns:text="urn:oasis:names:tc:' + 'opendocument:xmlns:text:1.0">'] + 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 = '<text:line-break/>'.join(my_lines) + my_lines_str2 = wrapper1 % (my_lines_str, ) + lines1.append(my_lines_str2) + lines1.append('</wrappertag1>') + s1 = ''.join(lines1) + s1 = s1.encode("utf-8") + el1 = etree.fromstring(s1) + for child in el1: + 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[-1]) + self.current_element.remove(self.current_element[-1]) + elif self.in_footer: + self.footer_content.append(self.current_element[-1]) + self.current_element.remove(self.current_element[-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 list(CONTENT_NAMESPACE_ATTRIB.items())) + contentstr = '<stuff %s>%s</stuff>' % (attrstr, rawstr, ) + contentstr = contentstr.encode("utf-8") + content = etree.fromstring(contentstr) + if len(content) > 0: + el1 = content[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 'refuri' in node: + 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 'refid' in node: + 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 'refuri' in node: + 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 + 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 + 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: + 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: + 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, ) + 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: + 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) + SubElement(el1, 'style:table-column-properties', + attrib={'style:column-width': '%.4fin' % colwidth}, + nsdict=SNSD) + 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 ('refuri' not in node + and 'refid' not in node + and 'refname' not in node): + 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, )), + }) + SubElement(el2, 'text:index-entry-chapter') + SubElement(el2, 'text:index-entry-text') + SubElement(el2, 'text:index-entry-tab-stop', attrib={ + 'style:leader-char': ".", + 'style:type': "right", + }) + 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: + 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: + 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): + 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): + if hasattr(self.language, 'labels'): + translated_label = self.language.labels.get(label, label) + else: + translated_label = label + 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!' % (translated_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() + + def visit_abbreviation(self, node): + pass + + def depart_abbreviation(self, node): + pass + + def visit_acronym(self, node): + pass + + def depart_acronym(self, node): + pass + + def visit_sidebar(self, node): + pass + + def depart_sidebar(self, node): + pass + + +# Use an own reader to modify transformations done. +class Reader(standalone.Reader): + + def get_transforms(self): + transforms = super().get_transforms() + if not self.settings.create_links: + transforms.remove(references.DanglingReferences) + return transforms |