/python/helpers/py3only/docutils/writers/odf_odt/__init__.py
Python | 3297 lines | 3131 code | 101 blank | 65 comment | 77 complexity | 93563875264b030ebfef20b0d0a830de MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0, MPL-2.0-no-copyleft-exception, MIT, EPL-1.0, AGPL-1.0
Large files files are truncated, but you can click here to view the full file
- # $Id: __init__.py 7717 2013-08-21 22:01:21Z milde $
- # Author: Dave Kuhlman <dkuhlman@rexx.com>
- # Copyright: This module has been placed in the public domain.
- """
- Open Document Format (ODF) Writer.
- """
- VERSION = '1.0a'
- __docformat__ = 'reStructuredText'
- import copy
- import io
- import os
- import os.path
- import re
- import sys
- import tempfile
- import time
- import zipfile
- from xml.dom import minidom
- import urllib.error
- import urllib.parse
- import urllib.request
- import docutils
- from docutils import frontend, nodes, utils, writers, languages
- from docutils.readers import standalone
- from docutils.transforms import references
- WhichElementTree = ''
- try:
- # 1. Try to use lxml.
- #from lxml import etree
- #WhichElementTree = 'lxml'
- raise ImportError('Ignoring lxml')
- except ImportError as e:
- try:
- # 2. Try to use ElementTree from the Python standard library.
- from xml.etree import ElementTree as etree
- WhichElementTree = 'elementtree'
- except ImportError as e:
- try:
- # 3. Try to use a version of ElementTree installed as a separate
- # product.
- from elementtree import ElementTree as etree
- WhichElementTree = 'elementtree'
- except ImportError as e:
- s1 = 'Must install either a version of Python containing ' \
- 'ElementTree (Python version >=2.5) or install ElementTree.'
- raise ImportError(s1)
- #
- # Import pygments and odtwriter pygments formatters if possible.
- try:
- import pygments
- import pygments.lexers
- from .pygmentsformatter import OdtPygmentsProgFormatter, \
- OdtPygmentsLaTeXFormatter
- except ImportError as exp:
- pygments = None
- # check for the Python Imaging Library
- try:
- import PIL.Image
- except ImportError:
- try: # sometimes PIL modules are put in PYTHONPATH's root
- import Image
- class PIL(object): pass # dummy wrapper
- PIL.Image = Image
- except ImportError:
- PIL = None
- ## import warnings
- ## warnings.warn('importing IPShellEmbed', UserWarning)
- ## from IPython.Shell import IPShellEmbed
- ## args = ['-pdb', '-pi1', 'In <\\#>: ', '-pi2', ' .\\D.: ',
- ## '-po', 'Out<\\#>: ', '-nosep']
- ## ipshell = IPShellEmbed(args,
- ## banner = 'Entering IPython. Press Ctrl-D to exit.',
- ## exit_msg = 'Leaving Interpreter, back to program.')
- #
- # ElementTree does not support getparent method (lxml does).
- # This wrapper class and the following support functions provide
- # that support for the ability to get the parent of an element.
- #
- if WhichElementTree == 'elementtree':
- import weakref
- _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 (not lxml), which
- # does not support use of nsmap parameter on Element() and SubElement().
- CONTENT_NAMESPACE_ATTRIB = {
- #'office:version': '1.0',
- 'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
- 'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
- 'xmlns:dom': 'http://www.w3.org/2001/xml-events',
- 'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
- 'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
- 'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
- 'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
- 'xmlns:math': 'http://www.w3.org/1998/Math/MathML',
- 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
- 'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
- 'xmlns:office': NAME_SPACE_1,
- 'xmlns:presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
- 'xmlns:ooo': 'http://openoffice.org/2004/office',
- 'xmlns:oooc': 'http://openoffice.org/2004/calc',
- 'xmlns:ooow': 'http://openoffice.org/2004/writer',
- 'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
- 'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
- 'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
- 'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
- 'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
- 'xmlns:xforms': 'http://www.w3.org/2002/xforms',
- 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
- 'xmlns:xsd': 'http://www.w3.org/2001/XMLSchema',
- 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
- }
- STYLES_NAMESPACE_ATTRIB = {
- #'office:version': '1.0',
- 'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
- 'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
- 'xmlns:dom': 'http://www.w3.org/2001/xml-events',
- 'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
- 'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
- 'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
- 'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
- 'xmlns:math': 'http://www.w3.org/1998/Math/MathML',
- 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
- 'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
- 'xmlns:office': NAME_SPACE_1,
- 'xmlns:presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
- 'xmlns:ooo': 'http://openoffice.org/2004/office',
- 'xmlns:oooc': 'http://openoffice.org/2004/calc',
- 'xmlns:ooow': 'http://openoffice.org/2004/writer',
- 'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
- 'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
- 'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
- 'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
- 'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
- 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
- }
- MANIFEST_NAMESPACE_ATTRIB = {
- 'xmlns:manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
- }
- META_NAMESPACE_ATTRIB = {
- #'office:version': '1.0',
- 'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
- 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
- 'xmlns:office': NAME_SPACE_1,
- 'xmlns:ooo': 'http://openoffice.org/2004/office',
- 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
- }
- #
- # Functions
- #
- #
- # ElementTree support functions.
- # In order to be able to get the parent of elements, must use these
- # instead of the functions with same name provided by ElementTree.
- #
- def Element(tag, attrib=None, nsmap=None, nsdict=CNSD):
- if attrib is None:
- attrib = {}
- tag, attrib = fix_ns(tag, attrib, nsdict)
- if WhichElementTree == 'lxml':
- el = etree.Element(tag, attrib, nsmap=nsmap)
- else:
- el = _ElementInterfaceWrapper(tag, attrib)
- return el
- def SubElement(parent, tag, attrib=None, nsmap=None, nsdict=CNSD):
- if attrib is None:
- attrib = {}
- tag, attrib = fix_ns(tag, attrib, nsdict)
- if WhichElementTree == 'lxml':
- el = etree.SubElement(parent, tag, attrib, nsmap=nsmap)
- else:
- el = _ElementInterfaceWrapper(tag, attrib)
- parent.append(el)
- el.setparent(parent)
- return el
- def fix_ns(tag, attrib, nsdict):
- nstag = add_ns(tag, nsdict)
- nsattrib = {}
- for key, val in attrib.items():
- nskey = add_ns(key, nsdict)
- nsattrib[nskey] = val
- return nstag, nsattrib
- def add_ns(tag, nsdict=CNSD):
- if WhichElementTree == 'lxml':
- nstag, name = tag.split(':')
- ns = nsdict.get(nstag)
- if ns is None:
- raise RuntimeError('Invalid namespace prefix: %s' % nstag)
- tag = '{%s}%s' % (ns, name,)
- return tag
- def ToString(et):
- outstream = io.StringIO()
- if sys.version_info >= (3, 2):
- et.write(outstream, encoding="unicode")
- else:
- et.write(outstream)
- s1 = outstream.getvalue()
- outstream.close()
- return s1
- def escape_cdata(text):
- text = text.replace("&", "&")
- text = text.replace("<", "<")
- text = text.replace(">", ">")
- ascii = ''
- for char in text:
- if ord(char) >= ord("\x7f"):
- ascii += "&#x%X;" % ( ord(char), )
- else:
- ascii += char
- return ascii
- WORD_SPLIT_PAT1 = re.compile(r'\b(\w*)\b\W*')
- def split_words(line):
- # We need whitespace at the end of the string for our regexpr.
- line += ' '
- words = []
- pos1 = 0
- mo = WORD_SPLIT_PAT1.search(line, pos1)
- while mo is not None:
- word = mo.groups()[0]
- words.append(word)
- pos1 = mo.end()
- mo = WORD_SPLIT_PAT1.search(line, pos1)
- return words
- #
- # Classes
- #
- class TableStyle(object):
- def __init__(self, border=None, backgroundcolor=None):
- self.border = border
- self.backgroundcolor = backgroundcolor
- def get_border_(self):
- return self.border_
- def set_border_(self, border):
- self.border_ = border
- border = property(get_border_, set_border_)
- def get_backgroundcolor_(self):
- return self.backgroundcolor_
- def set_backgroundcolor_(self, backgroundcolor):
- self.backgroundcolor_ = backgroundcolor
- backgroundcolor = property(get_backgroundcolor_, set_backgroundcolor_)
- BUILTIN_DEFAULT_TABLE_STYLE = TableStyle(
- border = '0.0007in solid #000000')
- #
- # Information about the indentation level for lists nested inside
- # other contexts, e.g. dictionary lists.
- class ListLevel(object):
- def __init__(self, level, sibling_level=True, nested_level=True):
- self.level = level
- self.sibling_level = sibling_level
- self.nested_level = nested_level
- def set_sibling(self, sibling_level): self.sibling_level = sibling_level
- def get_sibling(self): return self.sibling_level
- def set_nested(self, nested_level): self.nested_level = nested_level
- def get_nested(self): return self.nested_level
- def set_level(self, level): self.level = level
- def get_level(self): return self.level
- class Writer(writers.Writer):
- MIME_TYPE = 'application/vnd.oasis.opendocument.text'
- EXTENSION = '.odt'
- supported = ('odt', )
- """Formats this writer supports."""
- default_stylesheet = 'styles' + EXTENSION
- default_stylesheet_path = utils.relative_path(
- os.path.join(os.getcwd(), 'dummy'),
- os.path.join(os.path.dirname(__file__), default_stylesheet))
- default_template = 'template.txt'
- default_template_path = utils.relative_path(
- os.path.join(os.getcwd(), 'dummy'),
- os.path.join(os.path.dirname(__file__), default_template))
- settings_spec = (
- 'ODF-Specific Options',
- None,
- (
- ('Specify a stylesheet. '
- 'Default: "%s"' % default_stylesheet_path,
- ['--stylesheet'],
- {
- 'default': default_stylesheet_path,
- 'dest': 'stylesheet'
- }),
- ('Specify a configuration/mapping file relative to the '
- 'current working '
- 'directory for additional ODF options. '
- 'In particular, this file may contain a section named '
- '"Formats" that maps default style names to '
- 'names to be used in the resulting output file allowing for '
- 'adhering to external standards. '
- 'For more info and the format of the configuration/mapping file, '
- 'see the odtwriter doc.',
- ['--odf-config-file'],
- {'metavar': '<file>'}),
- ('Obfuscate email addresses to confuse harvesters while still '
- 'keeping email links usable with standards-compliant browsers.',
- ['--cloak-email-addresses'],
- {'default': False,
- 'action': 'store_true',
- 'dest': 'cloak_email_addresses',
- 'validator': frontend.validate_boolean}),
- ('Do not obfuscate email addresses.',
- ['--no-cloak-email-addresses'],
- {'default': False,
- 'action': 'store_false',
- 'dest': 'cloak_email_addresses',
- 'validator': frontend.validate_boolean}),
- ('Specify the thickness of table borders in thousands of a cm. '
- 'Default is 35.',
- ['--table-border-thickness'],
- {'default': None,
- 'validator': frontend.validate_nonnegative_int}),
- ('Add syntax highlighting in literal code blocks.',
- ['--add-syntax-highlighting'],
- {'default': False,
- 'action': 'store_true',
- 'dest': 'add_syntax_highlighting',
- 'validator': frontend.validate_boolean}),
- ('Do not add syntax highlighting in literal code blocks. (default)',
- ['--no-syntax-highlighting'],
- {'default': False,
- 'action': 'store_false',
- 'dest': 'add_syntax_highlighting',
- 'validator': frontend.validate_boolean}),
- ('Create sections for headers. (default)',
- ['--create-sections'],
- {'default': True,
- 'action': 'store_true',
- 'dest': 'create_sections',
- 'validator': frontend.validate_boolean}),
- ('Do not create sections for headers.',
- ['--no-sections'],
- {'default': True,
- 'action': 'store_false',
- 'dest': 'create_sections',
- 'validator': frontend.validate_boolean}),
- ('Create links.',
- ['--create-links'],
- {'default': False,
- 'action': 'store_true',
- 'dest': 'create_links',
- 'validator': frontend.validate_boolean}),
- ('Do not create links. (default)',
- ['--no-links'],
- {'default': False,
- 'action': 'store_false',
- 'dest': 'create_links',
- 'validator': frontend.validate_boolean}),
- ('Generate endnotes at end of document, not footnotes '
- 'at bottom of page.',
- ['--endnotes-end-doc'],
- {'default': False,
- 'action': 'store_true',
- 'dest': 'endnotes_end_doc',
- 'validator': frontend.validate_boolean}),
- ('Generate footnotes at bottom of page, not endnotes '
- 'at end of document. (default)',
- ['--no-endnotes-end-doc'],
- {'default': False,
- 'action': 'store_false',
- 'dest': 'endnotes_end_doc',
- 'validator': frontend.validate_boolean}),
- ('Generate a bullet list table of contents, not '
- 'an ODF/oowriter table of contents.',
- ['--generate-list-toc'],
- {'default': True,
- 'action': 'store_false',
- 'dest': 'generate_oowriter_toc',
- 'validator': frontend.validate_boolean}),
- ('Generate an ODF/oowriter table of contents, not '
- 'a bullet list. (default)',
- ['--generate-oowriter-toc'],
- {'default': True,
- 'action': 'store_true',
- 'dest': 'generate_oowriter_toc',
- 'validator': frontend.validate_boolean}),
- ('Specify the contents of an custom header line. '
- 'See odf_odt writer documentation for details '
- 'about special field character sequences.',
- ['--custom-odt-header'],
- { 'default': '',
- 'dest': 'custom_header',
- }),
- ('Specify the contents of an custom footer line. '
- 'See odf_odt writer documentation for details '
- 'about special field character sequences.',
- ['--custom-odt-footer'],
- { 'default': '',
- 'dest': 'custom_footer',
- }),
- )
- )
- settings_defaults = {
- 'output_encoding_error_handler': 'xmlcharrefreplace',
- }
- relative_path_settings = (
- 'stylesheet_path',
- )
- config_section = '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()
- self.write_zip_str(zfile, 'styles.xml', s1)
- self.store_embedded_files(zfile)
- self.copy_from_stylesheet(zfile)
- zfile.close()
- f.seek(0)
- whole = f.read()
- f.close()
- self.parts['whole'] = whole
- self.parts['encoding'] = self.document.settings.output_encoding
- self.parts['version'] = docutils.__version__
- def write_zip_str(self, zfile, name, bytes, compress_type=zipfile.ZIP_DEFLATED):
- localtime = time.localtime(time.time())
- zinfo = zipfile.ZipInfo(name, localtime)
- # Add some standard UNIX file access permissions (-rw-r--r--).
- zinfo.external_attr = (0x81a4 & 0xFFFF) << 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:
- # encode/decode
- destination1 = destination.decode('latin-1').encode('utf-8')
- zfile.write(source, destination1)
- except OSError as e:
- self.document.reporter.warning(
- "Can't open file %s." % (source, ))
- def get_settings(self):
- """
- modeled after get_stylesheet
- """
- stylespath = self.settings.stylesheet
- zfile = zipfile.ZipFile(stylespath, 'r')
- s1 = zfile.read('settings.xml')
- zfile.close()
- return s1
- def get_stylesheet(self):
- """Get the stylesheet from the visitor.
- Ask the visitor to setup the page.
- """
- s1 = self.visitor.setup_page()
- return s1
- def copy_from_stylesheet(self, outzipfile):
- """Copy images, settings, etc from the stylesheet doc into target doc.
- """
- stylespath = self.settings.stylesheet
- inzipfile = zipfile.ZipFile(stylespath, 'r')
- # Copy the styles.
- s1 = inzipfile.read('settings.xml')
- self.write_zip_str(outzipfile, 'settings.xml', s1)
- # Copy the images.
- namelist = inzipfile.namelist()
- for name in namelist:
- if name.startswith('Pictures/'):
- imageobj = inzipfile.read(name)
- outzipfile.writestr(name, imageobj)
- inzipfile.close()
- def assemble_parts(self):
- pass
- def create_manifest(self):
- if WhichElementTree == 'lxml':
- root = Element('manifest:manifest',
- nsmap=MANIFEST_NAMESPACE_DICT,
- nsdict=MANIFEST_NAMESPACE_DICT,
- )
- else:
- root = Element('manifest:manifest',
- attrib=MANIFEST_NAMESPACE_ATTRIB,
- nsdict=MANIFEST_NAMESPACE_DICT,
- )
- doc = etree.ElementTree(root)
- SubElement(root, 'manifest:file-entry', attrib={
- 'manifest:media-type': self.MIME_TYPE,
- 'manifest:full-path': '/',
- }, nsdict=MANNSD)
- SubElement(root, 'manifest:file-entry', attrib={
- 'manifest:media-type': 'text/xml',
- 'manifest:full-path': 'content.xml',
- }, nsdict=MANNSD)
- SubElement(root, 'manifest:file-entry', attrib={
- 'manifest:media-type': 'text/xml',
- 'manifest:full-path': 'styles.xml',
- }, nsdict=MANNSD)
- SubElement(root, 'manifest:file-entry', attrib={
- 'manifest:media-type': 'text/xml',
- 'manifest:full-path': 'settings.xml',
- }, nsdict=MANNSD)
- SubElement(root, 'manifest:file-entry', attrib={
- 'manifest:media-type': 'text/xml',
- 'manifest:full-path': 'meta.xml',
- }, nsdict=MANNSD)
- s1 = ToString(doc)
- doc = minidom.parseString(s1)
- s1 = doc.toprettyxml(' ')
- return s1
- def create_meta(self):
- if WhichElementTree == 'lxml':
- root = Element('office:document-meta',
- nsmap=META_NAMESPACE_DICT,
- nsdict=META_NAMESPACE_DICT,
- )
- else:
- root = Element('office:document-meta',
- attrib=META_NAMESPACE_ATTRIB,
- nsdict=META_NAMESPACE_DICT,
- )
- doc = etree.ElementTree(root)
- root = SubElement(root, 'office:meta', nsdict=METNSD)
- el1 = SubElement(root, 'meta:generator', nsdict=METNSD)
- el1.text = 'Docutils/rst2odf.py/%s' % (VERSION, )
- s1 = os.environ.get('USER', '')
- el1 = SubElement(root, 'meta:initial-creator', nsdict=METNSD)
- el1.text = s1
- s2 = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime())
- el1 = SubElement(root, 'meta:creation-date', nsdict=METNSD)
- el1.text = s2
- el1 = SubElement(root, 'dc:creator', nsdict=METNSD)
- el1.text = s1
- el1 = SubElement(root, 'dc:date', nsdict=METNSD)
- el1.text = s2
- el1 = SubElement(root, 'dc:language', nsdict=METNSD)
- el1.text = 'en-US'
- el1 = SubElement(root, 'meta:editing-cycles', nsdict=METNSD)
- el1.text = '1'
- el1 = SubElement(root, 'meta:editing-duration', nsdict=METNSD)
- el1.text = 'PT00M01S'
- title = self.visitor.get_title()
- el1 = SubElement(root, 'dc:title', nsdict=METNSD)
- if title:
- el1.text = title
- else:
- el1.text = '[no title]'
- meta_dict = self.visitor.get_meta_dict()
- keywordstr = meta_dict.get('keywords')
- if keywordstr is not None:
- keywords = split_words(keywordstr)
- for keyword in keywords:
- el1 = SubElement(root, 'meta:keyword', nsdict=METNSD)
- el1.text = keyword
- description = meta_dict.get('description')
- if description is not None:
- el1 = SubElement(root, 'dc:description', nsdict=METNSD)
- el1.text = description
- s1 = ToString(doc)
- #doc = minidom.parseString(s1)
- #s1 = doc.toprettyxml(' ')
- return s1
- # class ODFTranslator(nodes.SparseNodeVisitor):
- class ODFTranslator(nodes.GenericNodeVisitor):
- used_styles = (
- 'attribution', 'blockindent', 'blockquote', 'blockquote-bulletitem',
- 'blockquote-bulletlist', 'blockquote-enumitem', 'blockquote-enumlist',
- 'bulletitem', 'bulletlist',
- 'caption', 'legend',
- 'centeredtextbody', 'codeblock', 'codeblock-indented',
- 'codeblock-classname', 'codeblock-comment', 'codeblock-functionname',
- 'codeblock-keyword', 'codeblock-name', 'codeblock-number',
- 'codeblock-operator', 'codeblock-string', 'emphasis', 'enumitem',
- 'enumlist', 'epigraph', 'epigraph-bulletitem', 'epigraph-bulletlist',
- 'epigraph-enumitem', 'epigraph-enumlist', 'footer',
- 'footnote', 'citation',
- 'header', 'highlights', 'highlights-bulletitem',
- 'highlights-bulletlist', 'highlights-enumitem', 'highlights-enumlist',
- 'horizontalline', 'inlineliteral', 'quotation', 'rubric',
- 'strong', 'table-title', 'textbody', 'tocbulletlist', 'tocenumlist',
- 'title',
- 'subtitle',
- 'heading1',
- 'heading2',
- 'heading3',
- 'heading4',
- 'heading5',
- 'heading6',
- 'heading7',
- 'admon-attention-hdr',
- 'admon-attention-body',
- 'admon-caution-hdr',
- 'admon-caution-body',
- 'admon-danger-hdr',
- 'admon-danger-body',
- 'admon-error-hdr',
- 'admon-error-body',
- 'admon-generic-hdr',
- 'admon-generic-body',
- 'admon-hint-hdr',
- 'admon-hint-body',
- 'admon-important-hdr',
- 'admon-important-body',
- 'admon-note-hdr',
- 'admon-note-body',
- 'admon-tip-hdr',
- 'admon-tip-body',
- 'admon-warning-hdr',
- 'admon-warning-body',
- 'tableoption',
- 'tableoption.%c', 'tableoption.%c%d', 'Table%d', 'Table%d.%c',
- 'Table%d.%c%d',
- 'lineblock1',
- 'lineblock2',
- 'lineblock3',
- 'lineblock4',
- 'lineblock5',
- 'lineblock6',
- 'image', 'figureframe',
- )
- def __init__(self, document):
- #nodes.SparseNodeVisitor.__init__(self, document)
- nodes.GenericNodeVisitor.__init__(self, document)
- self.settings = document.settings
- lcode = self.settings.language_code
- self.language = languages.get_language(lcode, document.reporter)
- self.format_map = { }
- if self.settings.odf_config_file:
- from configparser import ConfigParser
- parser = ConfigParser()
- parser.read(self.settings.odf_config_file)
- for rststyle, format in parser.items("Formats"):
- if rststyle not in self.used_styles:
- self.document.reporter.warning(
- 'Style "%s" is not a style used by odtwriter.' % (
- rststyle, ))
- self.format_map[rststyle] = format.decode('utf-8')
- self.section_level = 0
- self.section_count = 0
- # Create ElementTree content and styles documents.
- if WhichElementTree == 'lxml':
- root = Element(
- 'office:document-content',
- nsmap=CONTENT_NAMESPACE_DICT,
- )
- else:
- root = Element(
- 'office:document-content',
- attrib=CONTENT_NAMESPACE_ATTRIB,
- )
- self.content_tree = etree.ElementTree(element=root)
- self.current_element = root
- SubElement(root, 'office:scripts')
- SubElement(root, 'office:font-face-decls')
- el = SubElement(root, 'office:automatic-styles')
- self.automatic_styles = el
- el = SubElement(root, 'office:body')
- el = self.generate_content_element(el)
- self.current_element = el
- self.body_text_element = el
- self.paragraph_style_stack = [self.rststyle('textbody'), ]
- self.list_style_stack = []
- self.table_count = 0
- self.column_count = ord('A') - 1
- self.trace_level = -1
- self.optiontablestyles_generated = False
- self.field_name = None
- self.field_element = None
- self.title = None
- self.image_count = 0
- self.image_style_count = 0
- self.image_dict = {}
- self.embedded_file_list = []
- self.syntaxhighlighting = 1
- self.syntaxhighlight_lexer = 'python'
- self.header_content = []
- self.footer_content = []
- self.in_header = False
- self.in_footer = False
- self.blockstyle = ''
- self.in_table_of_contents = False
- self.table_of_content_index_body = None
- self.list_level = 0
- self.def_list_level = 0
- self.footnote_ref_dict = {}
- self.footnote_list = []
- self.footnote_chars_idx = 0
- self.footnote_level = 0
- self.pending_ids = [ ]
- self.in_paragraph = False
- self.found_doc_title = False
- self.bumped_list_level_stack = []
- self.meta_dict = {}
- self.line_block_level = 0
- self.line_indent_level = 0
- self.citation_id = None
- self.style_index = 0 # use to form unique style names
- self.str_stylesheet = ''
- self.str_stylesheetcontent = ''
- self.dom_stylesheet = None
- self.table_styles = None
- self.in_citation = False
- def get_str_stylesheet(self):
- return self.str_stylesheet
- def retrieve_styles(self, extension):
- """Retrieve the stylesheet from either a .xml file or from
- a .odt (zip) file. Return the content as a string.
- """
- s2 = None
- stylespath = self.settings.stylesheet
- ext = os.path.splitext(stylespath)[1]
- if ext == '.xml':
- stylesfile = open(stylespath, 'r')
- s1 = stylesfile.read()
- stylesfile.close()
- elif ext == extension:
- zfile = zipfile.ZipFile(stylespath, 'r')
- s1 = zfile.read('styles.xml')
- s2 = zfile.read('content.xml')
- zfile.close()
- else:
- raise RuntimeError('stylesheet path (%s) must be %s or .xml file' %(stylespath, extension))
- self.str_stylesheet = s1
- self.str_stylesheetcontent = s2
- self.dom_stylesheet = etree.fromstring(self.str_stylesheet)
- self.dom_stylesheetcontent = etree.fromstring(self.str_stylesheetcontent)
- self.table_styles = self.extract_table_styles(s2)
- def extract_table_styles(self, styles_str):
- root = etree.fromstring(styles_str)
- table_styles = {}
- auto_styles = root.find(
- '{%s}automatic-styles' % (CNSD['office'], ))
- for stylenode in auto_styles:
- name = stylenode.get('{%s}name' % (CNSD['style'], ))
- tablename = name.split('.')[0]
- family = stylenode.get('{%s}family' % (CNSD['style'], ))
- if name.startswith(TABLESTYLEPREFIX):
- tablestyle = table_styles.get(tablename)
- if tablestyle is None:
- tablestyle = TableStyle()
- table_styles[tablename] = tablestyle
- if family == 'table':
- properties = stylenode.find(
- '{%s}table-properties' % (CNSD['style'], ))
- property = properties.get('{%s}%s' % (CNSD['fo'],
- 'background-color', ))
- if property is not None and property != 'none':
- tablestyle.backgroundcolor = property
- elif family == 'table-cell':
- properties = stylenode.find(
- '{%s}table-cell-properties' % (CNSD['style'], ))
- if properties is not None:
- border = self.get_property(properties)
- if border is not None:
- tablestyle.border = border
- return table_styles
- def get_property(self, stylenode):
- border = None
- for propertyname in TABLEPROPERTYNAMES:
- border = stylenode.get('{%s}%s' % (CNSD['fo'], propertyname, ))
- if border is not None and border != 'none':
- return border
- return border
- def add_doc_title(self):
- text = self.settings.title
- if text:
- self.title = text
- if not self.found_doc_title:
- el = Element('text:p', attrib = {
- 'text:style-name': self.rststyle('title'),
- })
- el.text = text
- self.body_text_element.insert(0, el)
- 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
- elif el.getchildren():
- for child in el.getchildren():
- el1 = self.find_first_text_p(child)
- if el1 is not None:
- return el1
- return None
- else:
- 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
- stylename = self.format_map.get(name1, 'rststyle-%s' % name1)
- return stylename
- def generate_content_element(self, root):
- return SubElement(root, 'office:text')
- def setup_page(self):
- self.setup_paper(self.dom_stylesheet)
- if (len(self.header_content) > 0 or len(self.footer_content) > 0 or
- self.settings.custom_header or self.settings.custom_footer):
- self.add_header_footer(self.dom_stylesheet)
- new_content = etree.tostring(self.dom_stylesheet)
- return new_content
- def setup_paper(self, root_el):
- try:
- fin = os.popen("paperconf -s 2> /dev/null")
- w, h = list(map(float, fin.read().split()))
- fin.close()
- except:
- w, h = 612, 792 # default to Letter
- def walk(el):
- if el.tag == "{%s}page-layout-properties" % SNSD["style"] and \
- "{%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.getchildren(): walk(subel)
- walk(root_el)
- def add_header_footer(self, root_el):
- automatic_styles = root_el.find(
- '{%s}automatic-styles' % SNSD['office'])
- path = '{%s}master-styles' % (NAME_SPACE_1, )
- master_el = root_el.find(path)
- if master_el is None:
- return
- path = '{%s}master-page' % (SNSD['style'], )
- master_el_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:
- if WhichElementTree == 'lxml':
- el2 = SubElement(el1, 'style:header', nsdict=SNSD)
- else:
- el2 = SubElement(el1, 'style:header',
- attrib=STYLES_NAMESPACE_ATTRIB,
- nsdict=STYLES_NAMESPACE_DICT,
- )
- for el in self.header_content:
- attrkey = add_ns('text:style-name', nsdict=SNSD)
- el.attrib[attrkey] = self.rststyle('header')
- el2.append(el)
- if self.settings.custom_header:
- elcustom = self.create_custom_headfoot(el2,
- self.settings.custom_header, 'header', automatic_styles)
- if self.footer_content or self.settings.custom_footer:
- if WhichElementTree == 'lxml':
- el2 = SubElement(el1, 'style:footer', nsdict=SNSD)
- else:
- el2 = SubElement(el1, 'style:footer',
- attrib=STYLES_NAMESPACE_ATTRIB,
- nsdict=STYLES_NAMESPACE_DICT,
- )
- for el in self.footer_content:
- attrkey = add_ns('text:style-name', nsdict=SNSD)
- el.attrib[attrkey] = self.rststyle('footer')
- el2.append(el)
- if self.settings.custom_footer:
- elcustom = self.create_custom_headfoot(el2,
- self.settings.custom_footer, 'footer', automatic_styles)
- code_none, code_field, code_text = 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…
Large files files are truncated, but you can click here to view the full file