Provide a generic Translator base class for writers.
[docutils.git] / docutils / docutils / writers / odf_odt / __init__.py
blobdce9eb6f485bf67933d05ad004b56102cfe55ba3
1 # $Id$
2 # Author: Dave Kuhlman <dkuhlman@davekuhlman.org>
3 # Copyright: This module has been placed in the public domain.
5 """
6 Open Document Format (ODF) Writer.
8 This module is provisional:
9 the API is not settled and may change with any minor Docutils version.
10 """
12 __docformat__ = 'reStructuredText'
15 from configparser import ConfigParser
16 import copy
17 from io import StringIO
18 import itertools
19 import locale
20 import os
21 import os.path
22 from pathlib import Path
23 import re
24 import subprocess
25 import tempfile
26 import time
27 import urllib
28 import weakref
29 from xml.etree import ElementTree as etree
30 from xml.dom import minidom
31 import zipfile
33 import docutils
34 from docutils import frontend, nodes, utils, writers, languages
35 from docutils.parsers.rst.directives.images import PIL # optional
36 from docutils.readers import standalone
37 from docutils.transforms import references
39 # Import pygments and odtwriter pygments formatters if possible.
40 try:
41 import pygments
42 import pygments.lexers
43 from .pygmentsformatter import (OdtPygmentsProgFormatter,
44 OdtPygmentsLaTeXFormatter)
45 except (ImportError, SyntaxError):
46 pygments = None
48 # import warnings
49 # warnings.warn('importing IPShellEmbed', UserWarning)
50 # from IPython.Shell import IPShellEmbed
51 # args = ['-pdb', '-pi1', 'In <\\#>: ', '-pi2', ' .\\D.: ',
52 # '-po', 'Out<\\#>: ', '-nosep']
53 # ipshell = IPShellEmbed(args,
54 # banner = 'Entering IPython. Press Ctrl-D to exit.',
55 # exit_msg = 'Leaving Interpreter, back to program.')
57 VERSION = '1.0a'
59 IMAGE_NAME_COUNTER = itertools.count()
63 # ElementTree does not support getparent method (lxml does).
64 # This wrapper class and the following support functions provide
65 # that support for the ability to get the parent of an element.
67 _parents = weakref.WeakKeyDictionary()
68 if isinstance(etree.Element, type):
69 _ElementInterface = etree.Element
70 else:
71 _ElementInterface = etree._ElementInterface
74 class _ElementInterfaceWrapper(_ElementInterface):
75 def __init__(self, tag, attrib=None) -> None:
76 _ElementInterface.__init__(self, tag, attrib)
77 _parents[self] = None
79 def setparent(self, parent) -> None:
80 _parents[self] = parent
82 def getparent(self):
83 return _parents[self]
87 # Constants and globals
89 SPACES_PATTERN = re.compile(r'( +)')
90 TABS_PATTERN = re.compile(r'(\t+)')
91 FILL_PAT1 = re.compile(r'^ +')
92 FILL_PAT2 = re.compile(r' {2,}')
94 TABLESTYLEPREFIX = 'rststyle-table-'
95 TABLENAMEDEFAULT = '%s0' % TABLESTYLEPREFIX
96 TABLEPROPERTYNAMES = (
97 'border', 'border-top', 'border-left',
98 'border-right', 'border-bottom', )
100 GENERATOR_DESC = 'Docutils.org/odf_odt'
102 NAME_SPACE_1 = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'
104 CONTENT_NAMESPACE_DICT = CNSD = {
105 # 'office:version': '1.0',
106 'chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
107 'dc': 'http://purl.org/dc/elements/1.1/',
108 'dom': 'http://www.w3.org/2001/xml-events',
109 'dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
110 'draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
111 'fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
112 'form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
113 'math': 'http://www.w3.org/1998/Math/MathML',
114 'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
115 'number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
116 'office': NAME_SPACE_1,
117 'ooo': 'http://openoffice.org/2004/office',
118 'oooc': 'http://openoffice.org/2004/calc',
119 'ooow': 'http://openoffice.org/2004/writer',
120 'presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
122 'script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
123 'style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
124 'svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
125 'table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
126 'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
127 'xforms': 'http://www.w3.org/2002/xforms',
128 'xlink': 'http://www.w3.org/1999/xlink',
129 'xsd': 'http://www.w3.org/2001/XMLSchema',
130 'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
133 STYLES_NAMESPACE_DICT = SNSD = {
134 # 'office:version': '1.0',
135 'chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
136 'dc': 'http://purl.org/dc/elements/1.1/',
137 'dom': 'http://www.w3.org/2001/xml-events',
138 'dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
139 'draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
140 'fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
141 'form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
142 'math': 'http://www.w3.org/1998/Math/MathML',
143 'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
144 'number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
145 'office': NAME_SPACE_1,
146 'presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
147 'ooo': 'http://openoffice.org/2004/office',
148 'oooc': 'http://openoffice.org/2004/calc',
149 'ooow': 'http://openoffice.org/2004/writer',
150 'script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
151 'style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
152 'svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
153 'table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
154 'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
155 'xlink': 'http://www.w3.org/1999/xlink',
158 MANIFEST_NAMESPACE_DICT = MANNSD = {
159 'manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
162 META_NAMESPACE_DICT = METNSD = {
163 # 'office:version': '1.0',
164 'dc': 'http://purl.org/dc/elements/1.1/',
165 'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
166 'office': NAME_SPACE_1,
167 'ooo': 'http://openoffice.org/2004/office',
168 'xlink': 'http://www.w3.org/1999/xlink',
171 # Attribute dictionaries for use with ElementTree, which
172 # does not support use of nsmap parameter on Element() and SubElement().
174 CONTENT_NAMESPACE_ATTRIB = {
175 # 'office:version': '1.0',
176 'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
177 'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
178 'xmlns:dom': 'http://www.w3.org/2001/xml-events',
179 'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
180 'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
181 'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
182 'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
183 'xmlns:math': 'http://www.w3.org/1998/Math/MathML',
184 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
185 'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
186 'xmlns:office': NAME_SPACE_1,
187 'xmlns:presentation':
188 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
189 'xmlns:ooo': 'http://openoffice.org/2004/office',
190 'xmlns:oooc': 'http://openoffice.org/2004/calc',
191 'xmlns:ooow': 'http://openoffice.org/2004/writer',
192 'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
193 'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
194 'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
195 'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
196 'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
197 'xmlns:xforms': 'http://www.w3.org/2002/xforms',
198 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
199 'xmlns:xsd': 'http://www.w3.org/2001/XMLSchema',
200 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
203 STYLES_NAMESPACE_ATTRIB = {
204 # 'office:version': '1.0',
205 'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
206 'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
207 'xmlns:dom': 'http://www.w3.org/2001/xml-events',
208 'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
209 'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
210 'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
211 'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
212 'xmlns:math': 'http://www.w3.org/1998/Math/MathML',
213 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
214 'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
215 'xmlns:office': NAME_SPACE_1,
216 'xmlns:presentation':
217 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
218 'xmlns:ooo': 'http://openoffice.org/2004/office',
219 'xmlns:oooc': 'http://openoffice.org/2004/calc',
220 'xmlns:ooow': 'http://openoffice.org/2004/writer',
221 'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
222 'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
223 'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
224 'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
225 'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
226 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
229 MANIFEST_NAMESPACE_ATTRIB = {
230 'xmlns:manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
233 META_NAMESPACE_ATTRIB = {
234 # 'office:version': '1.0',
235 'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
236 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
237 'xmlns:office': NAME_SPACE_1,
238 'xmlns:ooo': 'http://openoffice.org/2004/office',
239 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
244 # Functions
248 # ElementTree support functions.
249 # In order to be able to get the parent of elements, must use these
250 # instead of the functions with same name provided by ElementTree.
252 def Element(tag, attrib=None, nsmap=None, nsdict=CNSD):
253 if attrib is None:
254 attrib = {}
255 tag, attrib = fix_ns(tag, attrib, nsdict)
256 return _ElementInterfaceWrapper(tag, attrib)
259 def SubElement(parent, tag, attrib=None, nsmap=None, nsdict=CNSD):
260 if attrib is None:
261 attrib = {}
262 tag, attrib = fix_ns(tag, attrib, nsdict)
263 el = _ElementInterfaceWrapper(tag, attrib)
264 parent.append(el)
265 el.setparent(parent)
266 return el
269 def fix_ns(tag, attrib, nsdict):
270 nstag = add_ns(tag, nsdict)
271 nsattrib = {}
272 for key, val in list(attrib.items()):
273 nskey = add_ns(key, nsdict)
274 nsattrib[nskey] = val
275 return nstag, nsattrib
278 def add_ns(tag, nsdict=CNSD):
279 return tag
282 def ToString(et):
283 outstream = StringIO()
284 et.write(outstream, encoding="unicode")
285 s1 = outstream.getvalue()
286 outstream.close()
287 return s1
290 def escape_cdata(text):
291 text = text.replace("&", "&amp;")
292 text = text.replace("<", "&lt;")
293 text = text.replace(">", "&gt;")
294 ascii_ = ''
295 for char in text:
296 if ord(char) >= ord("\x7f"):
297 ascii_ += "&#x%X;" % (ord(char), )
298 else:
299 ascii_ += char
300 return ascii_
304 # Classes
308 class TableStyle:
309 def __init__(self, border=None, backgroundcolor=None) -> None:
310 self.border = border
311 self.backgroundcolor = backgroundcolor
313 def get_border_(self):
314 return self.border_
316 def set_border_(self, border) -> None:
317 self.border_ = border
319 border = property(get_border_, set_border_)
321 def get_backgroundcolor_(self):
322 return self.backgroundcolor_
324 def set_backgroundcolor_(self, backgroundcolor) -> None:
325 self.backgroundcolor_ = backgroundcolor
326 backgroundcolor = property(get_backgroundcolor_, set_backgroundcolor_)
329 BUILTIN_DEFAULT_TABLE_STYLE = TableStyle(
330 border='0.0007in solid #000000')
334 # Information about the indentation level for lists nested inside
335 # other contexts, e.g. dictionary lists.
336 class ListLevel:
337 def __init__(self, level, sibling_level=True, nested_level=True) -> None:
338 self.level = level
339 self.sibling_level = sibling_level
340 self.nested_level = nested_level
342 def set_sibling(self, sibling_level) -> None:
343 self.sibling_level = sibling_level
345 def get_sibling(self):
346 return self.sibling_level
348 def set_nested(self, nested_level) -> None:
349 self.nested_level = nested_level
351 def get_nested(self):
352 return self.nested_level
354 def set_level(self, level) -> None:
355 self.level = level
357 def get_level(self):
358 return self.level
361 class Writer(writers.Writer):
363 MIME_TYPE = 'application/vnd.oasis.opendocument.text'
364 EXTENSION = '.odt'
366 supported = ('odt', )
367 """Formats this writer supports."""
369 default_stylesheet = 'styles' + EXTENSION
371 default_stylesheet_path = utils.relative_path(
372 os.path.join(os.getcwd(), 'dummy'),
373 os.path.join(os.path.dirname(__file__), default_stylesheet))
375 default_template = 'template.txt'
377 default_template_path = utils.relative_path(
378 os.path.join(os.getcwd(), 'dummy'),
379 os.path.join(os.path.dirname(__file__), default_template))
381 settings_spec = (
382 'ODF-Specific Options.',
383 None,
385 ('Specify a stylesheet. '
386 'Default: "%s"' % default_stylesheet_path,
387 ['--stylesheet'],
389 'default': default_stylesheet_path,
390 'dest': 'stylesheet'
392 ('Specify an ODF-specific configuration/mapping file '
393 'relative to the current working directory.',
394 ['--odf-config-file'],
395 {'metavar': '<file>'}),
396 ('Obfuscate email addresses to confuse harvesters.',
397 ['--cloak-email-addresses'],
398 {'default': False,
399 'action': 'store_true',
400 'dest': 'cloak_email_addresses',
401 'validator': frontend.validate_boolean}),
402 ('Do not obfuscate email addresses.',
403 ['--no-cloak-email-addresses'],
404 {'default': False,
405 'action': 'store_false',
406 'dest': 'cloak_email_addresses',
407 'validator': frontend.validate_boolean}),
408 ('Specify the thickness of table borders in thousands of a cm. '
409 'Default is 35.',
410 ['--table-border-thickness'],
411 {'default': None,
412 'metavar': '<int>',
413 'validator': frontend.validate_nonnegative_int}),
414 ('Add syntax highlighting in literal code blocks.',
415 ['--add-syntax-highlighting'],
416 {'default': False,
417 'action': 'store_true',
418 'dest': 'add_syntax_highlighting',
419 'validator': frontend.validate_boolean}),
420 ('Do not add syntax highlighting in '
421 'literal code blocks. (default)',
422 ['--no-syntax-highlighting'],
423 {'default': False,
424 'action': 'store_false',
425 'dest': 'add_syntax_highlighting',
426 'validator': frontend.validate_boolean}),
427 ('Create sections for headers. (default)',
428 ['--create-sections'],
429 {'default': True,
430 'action': 'store_true',
431 'dest': 'create_sections',
432 'validator': frontend.validate_boolean}),
433 ('Do not create sections for headers.',
434 ['--no-sections'],
435 {'default': True,
436 'action': 'store_false',
437 'dest': 'create_sections',
438 'validator': frontend.validate_boolean}),
439 ('Create links.',
440 ['--create-links'],
441 {'default': False,
442 'action': 'store_true',
443 'dest': 'create_links',
444 'validator': frontend.validate_boolean}),
445 ('Do not create links. (default)',
446 ['--no-links'],
447 {'default': False,
448 'action': 'store_false',
449 'dest': 'create_links',
450 'validator': frontend.validate_boolean}),
451 ('Generate endnotes at end of document, not footnotes '
452 'at bottom of page.',
453 ['--endnotes-end-doc'],
454 {'default': False,
455 'action': 'store_true',
456 'dest': 'endnotes_end_doc',
457 'validator': frontend.validate_boolean}),
458 ('Generate footnotes at bottom of page, not endnotes '
459 'at end of document. (default)',
460 ['--no-endnotes-end-doc'],
461 {'default': False,
462 'action': 'store_false',
463 'dest': 'endnotes_end_doc',
464 'validator': frontend.validate_boolean}),
465 ('Generate a bullet list table of contents, '
466 'not a native ODF table of contents.',
467 ['--generate-list-toc'],
468 {'action': 'store_false',
469 'dest': 'generate_oowriter_toc',
470 'validator': frontend.validate_boolean}),
471 ('Generate a native ODF table of contents, '
472 'not a bullet list. (default)',
473 ['--generate-oowriter-toc'],
474 {'default': True,
475 'action': 'store_true',
476 'dest': 'generate_oowriter_toc',
477 'validator': frontend.validate_boolean}),
478 ('Specify the contents of an custom header line. '
479 'See ODF/ODT writer documentation for details '
480 'about special field character sequences.',
481 ['--custom-odt-header'],
482 {'default': '',
483 'dest': 'custom_header',
484 'metavar': '<custom header>'}),
485 ('Specify the contents of an custom footer line. '
486 'See ODF/ODT writer documentation for details.',
487 ['--custom-odt-footer'],
488 {'default': '',
489 'dest': 'custom_footer',
490 'metavar': '<custom footer>'}),
494 settings_defaults = {
495 'output_encoding_error_handler': 'xmlcharrefreplace',
498 relative_path_settings = ('odf_config_file', 'stylesheet',)
500 config_section = 'odf_odt writer'
501 config_section_dependencies = ('writers',)
503 def __init__(self) -> None:
504 writers.Writer.__init__(self)
505 self.translator_class = ODFTranslator
507 def translate(self) -> None:
508 self.settings = self.document.settings
509 self.visitor = self.translator_class(self.document)
510 self.visitor.retrieve_styles(self.EXTENSION)
511 self.document.walkabout(self.visitor)
512 self.visitor.add_doc_title()
513 self.assemble_my_parts()
514 self.output = self.parts['whole']
516 def assemble_my_parts(self) -> None:
517 """Assemble the `self.parts` dictionary. Extend in subclasses.
519 writers.Writer.assemble_parts(self)
520 f = tempfile.NamedTemporaryFile()
521 zfile = zipfile.ZipFile(f, 'w', zipfile.ZIP_DEFLATED)
522 self.write_zip_str(
523 zfile, 'mimetype', self.MIME_TYPE,
524 compress_type=zipfile.ZIP_STORED)
525 content = self.visitor.content_astext()
526 self.write_zip_str(zfile, 'content.xml', content)
527 s1 = self.create_manifest()
528 self.write_zip_str(zfile, 'META-INF/manifest.xml', s1)
529 s1 = self.create_meta()
530 self.write_zip_str(zfile, 'meta.xml', s1)
531 s1 = self.get_stylesheet()
532 # Set default language in document to be generated.
533 # Language is specified by the -l/--language command line option.
534 # The format is described in BCP 47. If region is omitted, we use
535 # local.normalize(ll) to obtain a region.
536 language_code = None
537 region_code = None
538 if self.visitor.language_code:
539 language_ids = self.visitor.language_code.replace('_', '-')
540 language_ids = language_ids.split('-')
541 # first tag is primary language tag
542 language_code = language_ids[0].lower()
543 # 2-letter region subtag may follow in 2nd or 3rd position
544 for subtag in language_ids[1:]:
545 if len(subtag) == 2 and subtag.isalpha():
546 region_code = subtag.upper()
547 break
548 elif len(subtag) == 1:
549 break # 1-letter tag is never before valid region tag
550 if region_code is None:
551 try:
552 rcode = locale.normalize(language_code)
553 except NameError:
554 rcode = language_code
555 rcode = rcode.split('_')
556 if len(rcode) > 1:
557 rcode = rcode[1].split('.')
558 region_code = rcode[0]
559 if region_code is None:
560 self.document.reporter.warning(
561 'invalid language-region.\n'
562 ' Could not find region with locale.normalize().\n'
563 ' Please specify both language and region (ll-RR).\n'
564 ' Examples: es-MX (Spanish, Mexico),\n'
565 ' en-AU (English, Australia).')
566 # Update the style ElementTree with the language and region.
567 # Note that we keep a reference to the modified node because
568 # it is possible that ElementTree will throw away the Python
569 # representation of the updated node if we do not.
570 updated, new_dom_styles, updated_node = self.update_stylesheet(
571 self.visitor.get_dom_stylesheet(), language_code, region_code)
572 if updated:
573 s1 = etree.tostring(new_dom_styles)
574 self.write_zip_str(zfile, 'styles.xml', s1)
575 self.store_embedded_files(zfile)
576 self.copy_from_stylesheet(zfile)
577 zfile.close()
578 f.seek(0)
579 whole = f.read()
580 f.close()
581 self.parts['whole'] = whole
582 self.parts['encoding'] = self.document.settings.output_encoding
583 self.parts['version'] = docutils.__version__
585 def update_stylesheet(self, stylesheet_root, language_code, region_code):
586 """Update xml style sheet element with language and region/country."""
587 updated = False
588 modified_nodes = set()
589 if language_code is not None or region_code is not None:
590 n1 = stylesheet_root.find(
591 '{urn:oasis:names:tc:opendocument:xmlns:office:1.0}'
592 'styles')
593 if n1 is None:
594 raise RuntimeError(
595 "Cannot find 'styles' element in styles.odt/styles.xml")
596 n2_nodes = n1.findall(
597 '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
598 'default-style')
599 if not n2_nodes:
600 raise RuntimeError(
601 "Cannot find 'default-style' "
602 "element in styles.xml")
603 for node in n2_nodes:
604 family = node.attrib.get(
605 '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
606 'family')
607 if family == 'paragraph' or family == 'graphic':
608 n3 = node.find(
609 '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
610 'text-properties')
611 if n3 is None:
612 raise RuntimeError(
613 "Cannot find 'text-properties' "
614 "element in styles.xml")
615 if language_code is not None:
616 n3.attrib[
617 '{urn:oasis:names:tc:opendocument:xmlns:'
618 'xsl-fo-compatible:1.0}language'] = language_code
619 n3.attrib[
620 '{urn:oasis:names:tc:opendocument:xmlns:'
621 'style:1.0}language-complex'] = language_code
622 updated = True
623 modified_nodes.add(n3)
624 if region_code is not None:
625 n3.attrib[
626 '{urn:oasis:names:tc:opendocument:xmlns:'
627 'xsl-fo-compatible:1.0}country'] = region_code
628 n3.attrib[
629 '{urn:oasis:names:tc:opendocument:xmlns:'
630 'style:1.0}country-complex'] = region_code
631 updated = True
632 modified_nodes.add(n3)
633 return updated, stylesheet_root, modified_nodes
635 def write_zip_str(
636 self, zfile, name, bytes_, compress_type=zipfile.ZIP_DEFLATED,
637 ) -> None:
638 localtime = time.localtime(time.time())
639 zinfo = zipfile.ZipInfo(name, localtime)
640 # Add some standard UNIX file access permissions (-rw-r--r--).
641 zinfo.external_attr = (0x81a4 & 0xFFFF) << 16
642 zinfo.compress_type = compress_type
643 zfile.writestr(zinfo, bytes_)
645 def store_embedded_files(self, zfile) -> None:
646 embedded_files = self.visitor.get_embedded_file_list()
647 for source, destination in embedded_files:
648 if source is None:
649 continue
650 try:
651 zfile.write(source, destination)
652 except OSError:
653 self.document.reporter.warning(
654 "Can't open file %s." % (source, ))
656 def get_settings(self):
658 modeled after get_stylesheet
660 stylespath = self.settings.stylesheet
661 zfile = zipfile.ZipFile(stylespath, 'r')
662 s1 = zfile.read('settings.xml')
663 zfile.close()
664 return s1
666 def get_stylesheet(self):
667 """Get the stylesheet from the visitor.
668 Ask the visitor to setup the page.
670 return self.visitor.setup_page()
672 def copy_from_stylesheet(self, outzipfile) -> None:
673 """Copy images, settings, etc from the stylesheet doc into target doc.
675 stylespath = self.settings.stylesheet
676 if not stylespath.endswith('.odt'):
677 return # an '.xml' stylesheet does not have settings or images
678 inzipfile = zipfile.ZipFile(stylespath, 'r')
679 # Copy the settings.
680 s1 = inzipfile.read('settings.xml')
681 self.write_zip_str(outzipfile, 'settings.xml', s1)
682 # Copy the images.
683 namelist = inzipfile.namelist()
684 for name in namelist:
685 if name.startswith('Pictures/'):
686 imageobj = inzipfile.read(name)
687 outzipfile.writestr(name, imageobj)
688 inzipfile.close()
690 def assemble_parts(self) -> None:
691 pass
693 def create_manifest(self):
694 root = Element(
695 'manifest:manifest',
696 attrib=MANIFEST_NAMESPACE_ATTRIB,
697 nsdict=MANIFEST_NAMESPACE_DICT,
699 doc = etree.ElementTree(root)
700 SubElement(root, 'manifest:file-entry', attrib={
701 'manifest:media-type': self.MIME_TYPE,
702 'manifest:full-path': '/',
703 }, nsdict=MANNSD)
704 SubElement(root, 'manifest:file-entry', attrib={
705 'manifest:media-type': 'text/xml',
706 'manifest:full-path': 'content.xml',
707 }, nsdict=MANNSD)
708 SubElement(root, 'manifest:file-entry', attrib={
709 'manifest:media-type': 'text/xml',
710 'manifest:full-path': 'styles.xml',
711 }, nsdict=MANNSD)
712 SubElement(root, 'manifest:file-entry', attrib={
713 'manifest:media-type': 'text/xml',
714 'manifest:full-path': 'settings.xml',
715 }, nsdict=MANNSD)
716 SubElement(root, 'manifest:file-entry', attrib={
717 'manifest:media-type': 'text/xml',
718 'manifest:full-path': 'meta.xml',
719 }, nsdict=MANNSD)
720 s1 = ToString(doc)
721 doc = minidom.parseString(s1)
722 return doc.toprettyxml(' ')
724 def create_meta(self):
725 root = Element(
726 'office:document-meta',
727 attrib=META_NAMESPACE_ATTRIB,
728 nsdict=META_NAMESPACE_DICT,
730 doc = etree.ElementTree(root)
731 root = SubElement(root, 'office:meta', nsdict=METNSD)
732 el1 = SubElement(root, 'meta:generator', nsdict=METNSD)
733 el1.text = 'Docutils/rst2odf.py/%s' % (VERSION, )
734 s1 = os.environ.get('USER', '')
735 el1 = SubElement(root, 'meta:initial-creator', nsdict=METNSD)
736 el1.text = s1
737 s2 = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime())
738 el1 = SubElement(root, 'meta:creation-date', nsdict=METNSD)
739 el1.text = s2
740 el1 = SubElement(root, 'dc:creator', nsdict=METNSD)
741 el1.text = s1
742 el1 = SubElement(root, 'dc:date', nsdict=METNSD)
743 el1.text = s2
744 el1 = SubElement(root, 'dc:language', nsdict=METNSD)
745 el1.text = 'en-US'
746 el1 = SubElement(root, 'meta:editing-cycles', nsdict=METNSD)
747 el1.text = '1'
748 el1 = SubElement(root, 'meta:editing-duration', nsdict=METNSD)
749 el1.text = 'PT00M01S'
750 title = self.visitor.get_title()
751 el1 = SubElement(root, 'dc:title', nsdict=METNSD)
752 if title:
753 el1.text = title
754 else:
755 el1.text = '[no title]'
756 for prop, value in self.visitor.get_meta_dict().items():
757 # 'keywords', 'description', and 'subject' have their own fields:
758 if prop == 'keywords':
759 keywords = re.split(', *', value)
760 for keyword in keywords:
761 el1 = SubElement(root, 'meta:keyword', nsdict=METNSD)
762 el1.text = keyword
763 elif prop == 'description':
764 el1 = SubElement(root, 'dc:description', nsdict=METNSD)
765 el1.text = value
766 elif prop == 'subject':
767 el1 = SubElement(root, 'dc:subject', nsdict=METNSD)
768 el1.text = value
769 else: # Store remaining properties as custom/user-defined
770 el1 = SubElement(root, 'meta:user-defined',
771 attrib={'meta:name': prop}, nsdict=METNSD)
772 el1.text = value
773 s1 = ToString(doc)
774 # doc = minidom.parseString(s1)
775 # s1 = doc.toprettyxml(' ')
776 return s1
779 # class ODFTranslator(nodes.SparseNodeVisitor):
780 class ODFTranslator(nodes.GenericNodeVisitor):
782 used_styles = (
783 'attribution', 'blockindent', 'blockquote', 'blockquote-bulletitem',
784 'blockquote-bulletlist', 'blockquote-enumitem', 'blockquote-enumlist',
785 'bulletitem', 'bulletlist',
786 'caption', 'legend',
787 'centeredtextbody', 'codeblock', 'codeblock-indented',
788 'codeblock-classname', 'codeblock-comment', 'codeblock-functionname',
789 'codeblock-keyword', 'codeblock-name', 'codeblock-number',
790 'codeblock-operator', 'codeblock-string', 'emphasis', 'enumitem',
791 'enumlist', 'epigraph', 'epigraph-bulletitem', 'epigraph-bulletlist',
792 'epigraph-enumitem', 'epigraph-enumlist', 'footer',
793 'footnote', 'citation',
794 'header', 'highlights', 'highlights-bulletitem',
795 'highlights-bulletlist', 'highlights-enumitem', 'highlights-enumlist',
796 'horizontalline', 'inlineliteral', 'quotation', 'rubric',
797 'strong', 'table-title', 'textbody', 'tocbulletlist', 'tocenumlist',
798 'title',
799 'subtitle',
800 'heading1',
801 'heading2',
802 'heading3',
803 'heading4',
804 'heading5',
805 'heading6',
806 'heading7',
807 'admon-attention-hdr',
808 'admon-attention-body',
809 'admon-caution-hdr',
810 'admon-caution-body',
811 'admon-danger-hdr',
812 'admon-danger-body',
813 'admon-error-hdr',
814 'admon-error-body',
815 'admon-generic-hdr',
816 'admon-generic-body',
817 'admon-hint-hdr',
818 'admon-hint-body',
819 'admon-important-hdr',
820 'admon-important-body',
821 'admon-note-hdr',
822 'admon-note-body',
823 'admon-tip-hdr',
824 'admon-tip-body',
825 'admon-warning-hdr',
826 'admon-warning-body',
827 'tableoption',
828 'tableoption.%c', 'tableoption.%c%d', 'Table%d', 'Table%d.%c',
829 'Table%d.%c%d',
830 'lineblock1',
831 'lineblock2',
832 'lineblock3',
833 'lineblock4',
834 'lineblock5',
835 'lineblock6',
836 'image', 'figureframe',
839 def __init__(self, document) -> None:
840 # nodes.SparseNodeVisitor.__init__(self, document)
841 nodes.GenericNodeVisitor.__init__(self, document)
842 self.settings = document.settings
843 if self.settings.output_encoding == 'unicode':
844 self.document.reporter.severe('The ODT writer returns `bytes` '
845 'that cannot be decoded to `str`')
846 # TODO: return document as "flat" XML?
847 self.language_code = self.settings.language_code
848 self.language = languages.get_language(
849 self.language_code,
850 document.reporter)
851 self.format_map = {}
852 if self.settings.odf_config_file:
853 parser = ConfigParser()
854 parser.read(self.settings.odf_config_file)
855 for rststyle, format in parser.items("Formats"):
856 if rststyle not in self.used_styles:
857 self.document.reporter.warning(
858 'Style "%s" is not a style used by odtwriter.' % (
859 rststyle, ))
860 self.format_map[rststyle] = format
861 self.section_level = 0
862 self.section_count = 0
863 # Create ElementTree content and styles documents.
864 root = Element(
865 'office:document-content',
866 attrib=CONTENT_NAMESPACE_ATTRIB,
868 self.content_tree = etree.ElementTree(element=root)
869 self.current_element = root
870 SubElement(root, 'office:scripts')
871 SubElement(root, 'office:font-face-decls')
872 el = SubElement(root, 'office:automatic-styles')
873 self.automatic_styles = el
874 el = SubElement(root, 'office:body')
875 el = self.generate_content_element(el)
876 self.current_element = el
877 self.body_text_element = el
878 self.paragraph_style_stack = [self.rststyle('textbody'), ]
879 self.list_style_stack = []
880 self.table_count = 0
881 self.column_count = ord('A') - 1
882 self.trace_level = -1
883 self.optiontablestyles_generated = False
884 self.field_name = None
885 self.field_element = None
886 self.title = None
887 self.image_count = 0
888 self.image_style_count = 0
889 self.image_dict = {}
890 self.embedded_file_list = []
891 self.syntaxhighlighting = 1
892 self.syntaxhighlight_lexer = 'python'
893 self.header_content = []
894 self.footer_content = []
895 self.in_header = False
896 self.in_footer = False
897 self.blockstyle = ''
898 self.in_table_of_contents = False
899 self.table_of_content_index_body = None
900 self.list_level = 0
901 self.def_list_level = 0
902 self.footnote_ref_dict = {}
903 self.footnote_list = []
904 self.footnote_chars_idx = 0
905 self.footnote_level = 0
906 self.pending_ids = []
907 self.in_paragraph = False
908 self.found_doc_title = False
909 self.bumped_list_level_stack = []
910 self.meta_dict = {}
911 self.line_block_level = 0
912 self.line_indent_level = 0
913 self.citation_id = None
914 self.style_index = 0 # use to form unique style names
915 self.str_stylesheet = ''
916 self.str_stylesheetcontent = ''
917 self.dom_stylesheet = None
918 self.table_styles = None
919 self.in_citation = False
921 # Keep track of nested styling classes
922 self.inline_style_count_stack = []
924 def get_str_stylesheet(self):
925 return self.str_stylesheet
927 def retrieve_styles(self, extension):
928 """Retrieve the stylesheet from a .xml or .odt (zip) file.
930 Store in `self.*_styles*` attributes.
932 stylespath = self.settings.stylesheet
933 ext = os.path.splitext(stylespath)[1]
934 if ext == '.xml':
935 s1 = Path(stylespath).read_text(encoding='utf-8')
936 s2 = ''
937 elif ext == extension:
938 zfile = zipfile.ZipFile(stylespath, 'r')
939 s1 = zfile.read('styles.xml')
940 s2 = zfile.read('content.xml')
941 zfile.close()
942 else:
943 raise RuntimeError('stylesheet path (%s) must be %s or '
944 '.xml file' % (stylespath, extension))
945 self.str_stylesheet = s1
946 self.str_stylesheetcontent = s2
947 self.dom_stylesheet = etree.fromstring(self.str_stylesheet)
948 if not s2:
949 return
950 # TODO: dom_stylesheetcontent is never used. Remove?
951 self.dom_stylesheetcontent = etree.fromstring(
952 self.str_stylesheetcontent)
953 self.table_styles = self.extract_table_styles(s2)
955 def extract_table_styles(self, styles_str):
956 root = etree.fromstring(styles_str)
957 table_styles = {}
958 auto_styles = root.find(
959 '{%s}automatic-styles' % (CNSD['office'], ))
960 for stylenode in auto_styles:
961 name = stylenode.get('{%s}name' % (CNSD['style'], ))
962 tablename = name.split('.')[0]
963 family = stylenode.get('{%s}family' % (CNSD['style'], ))
964 if name.startswith(TABLESTYLEPREFIX):
965 tablestyle = table_styles.get(tablename)
966 if tablestyle is None:
967 tablestyle = TableStyle()
968 table_styles[tablename] = tablestyle
969 if family == 'table':
970 properties = stylenode.find(
971 '{%s}table-properties' % (CNSD['style'], ))
972 property_ = properties.get(
973 '{%s}%s' % (CNSD['fo'], 'background-color', ))
974 if property_ is not None and property_ != 'none':
975 tablestyle.backgroundcolor = property_
976 elif family == 'table-cell':
977 properties = stylenode.find(
978 '{%s}table-cell-properties' % (CNSD['style'], ))
979 if properties is not None:
980 border = self.get_property(properties)
981 if border is not None:
982 tablestyle.border = border
983 return table_styles
985 def get_property(self, stylenode):
986 border = None
987 for propertyname in TABLEPROPERTYNAMES:
988 border = stylenode.get('{%s}%s' % (CNSD['fo'], propertyname, ))
989 if border is not None and border != 'none':
990 return border
991 return border
993 def add_doc_title(self) -> None:
994 text = self.settings.title
995 if text:
996 self.title = text
997 if not self.found_doc_title:
998 el = Element('text:p', attrib={
999 'text:style-name': self.rststyle('title'),
1001 el.text = text
1002 self.body_text_element.insert(0, el)
1003 el = self.find_first_text_p(self.body_text_element)
1004 if el is not None:
1005 self.attach_page_style(el)
1007 def find_first_text_p(self, el):
1008 """Search the generated doc and return the first <text:p> element.
1010 if el.tag == 'text:p' or el.tag == 'text:h':
1011 return el
1012 else:
1013 for child in el:
1014 el1 = self.find_first_text_p(child)
1015 if el1 is not None:
1016 return el1
1017 return None
1019 def attach_page_style(self, el) -> None:
1020 """Attach the default page style.
1022 Create an automatic-style that refers to the current style
1023 of this element and that refers to the default page style.
1025 current_style = el.get('text:style-name')
1026 style_name = 'P1003'
1027 el1 = SubElement(
1028 self.automatic_styles, 'style:style', attrib={
1029 'style:name': style_name,
1030 'style:master-page-name': "rststyle-pagedefault",
1031 'style:family': "paragraph",
1032 }, nsdict=SNSD)
1033 if current_style:
1034 el1.set('style:parent-style-name', current_style)
1035 el.set('text:style-name', style_name)
1037 def rststyle(self, name, parameters=()):
1039 Returns the style name to use for the given style.
1041 If `parameters` is given `name` must contain a matching number of
1042 ``%`` and is used as a format expression with `parameters` as
1043 the value.
1045 name1 = name % parameters
1046 return self.format_map.get(name1, 'rststyle-%s' % name1)
1048 def generate_content_element(self, root):
1049 return SubElement(root, 'office:text')
1051 def setup_page(self):
1052 self.setup_paper(self.dom_stylesheet)
1053 if (len(self.header_content) > 0
1054 or len(self.footer_content) > 0
1055 or self.settings.custom_header
1056 or self.settings.custom_footer):
1057 self.add_header_footer(self.dom_stylesheet)
1058 return etree.tostring(self.dom_stylesheet)
1060 def get_dom_stylesheet(self):
1061 return self.dom_stylesheet
1063 def setup_paper(self, root_el) -> None:
1064 # TODO: only call paperconf, if it is actually used
1065 # (i.e. page size removed from "styles.odt" with rst2odt_prepstyles.py
1066 # cf. conditional in walk() below)?
1067 try:
1068 dimensions = subprocess.check_output(('paperconf', '-s'),
1069 stderr=subprocess.STDOUT)
1070 w, h = (float(s) for s in dimensions.split())
1071 except (subprocess.CalledProcessError, FileNotFoundError, ValueError):
1072 self.document.reporter.info(
1073 'Cannot use `paperconf`, defaulting to Letter.')
1074 w, h = 612, 792 # default to Letter
1076 def walk(el) -> None:
1077 if el.tag == "{%s}page-layout-properties" % SNSD["style"] and \
1078 "{%s}page-width" % SNSD["fo"] not in el.attrib:
1079 el.attrib["{%s}page-width" % SNSD["fo"]] = "%.3fpt" % w
1080 el.attrib["{%s}page-height" % SNSD["fo"]] = "%.3fpt" % h
1081 el.attrib["{%s}margin-left" % SNSD["fo"]] = \
1082 el.attrib["{%s}margin-right" % SNSD["fo"]] = \
1083 "%.3fpt" % (.1 * w)
1084 el.attrib["{%s}margin-top" % SNSD["fo"]] = \
1085 el.attrib["{%s}margin-bottom" % SNSD["fo"]] = \
1086 "%.3fpt" % (.1 * h)
1087 else:
1088 for subel in el:
1089 walk(subel)
1090 walk(root_el)
1092 def add_header_footer(self, root_el) -> None:
1093 automatic_styles = root_el.find(
1094 '{%s}automatic-styles' % SNSD['office'])
1095 path = '{%s}master-styles' % (NAME_SPACE_1, )
1096 master_el = root_el.find(path)
1097 if master_el is None:
1098 return
1099 path = '{%s}master-page' % (SNSD['style'], )
1100 master_el_container = master_el.findall(path)
1101 master_el = None
1102 target_attrib = '{%s}name' % (SNSD['style'], )
1103 target_name = self.rststyle('pagedefault')
1104 for el in master_el_container:
1105 if el.get(target_attrib) == target_name:
1106 master_el = el
1107 break
1108 if master_el is None:
1109 return
1110 el1 = master_el
1111 if self.header_content or self.settings.custom_header:
1112 el2 = SubElement(
1113 el1, 'style:header',
1114 attrib=STYLES_NAMESPACE_ATTRIB,
1115 nsdict=STYLES_NAMESPACE_DICT,
1117 for el in self.header_content:
1118 attrkey = add_ns('text:style-name', nsdict=SNSD)
1119 el.attrib[attrkey] = self.rststyle('header')
1120 el2.append(el)
1121 if self.settings.custom_header:
1122 self.create_custom_headfoot(
1123 el2,
1124 self.settings.custom_header, 'header', automatic_styles)
1125 if self.footer_content or self.settings.custom_footer:
1126 el2 = SubElement(
1127 el1, 'style:footer',
1128 attrib=STYLES_NAMESPACE_ATTRIB,
1129 nsdict=STYLES_NAMESPACE_DICT,
1131 for el in self.footer_content:
1132 attrkey = add_ns('text:style-name', nsdict=SNSD)
1133 el.attrib[attrkey] = self.rststyle('footer')
1134 el2.append(el)
1135 if self.settings.custom_footer:
1136 self.create_custom_headfoot(
1137 el2,
1138 self.settings.custom_footer, 'footer', automatic_styles)
1140 code_none, code_field, code_text = list(range(3))
1141 field_pat = re.compile(r'%(..?)%')
1143 def create_custom_headfoot(
1144 self, parent, text, style_name, automatic_styles):
1145 parent = SubElement(parent, 'text:p', attrib={
1146 'text:style-name': self.rststyle(style_name),
1148 current_element = None
1149 field_iter = self.split_field_specifiers_iter(text)
1150 for item in field_iter:
1151 if item[0] == ODFTranslator.code_field:
1152 if item[1] not in (
1153 'p', 'P',
1154 't1', 't2', 't3', 't4',
1155 'd1', 'd2', 'd3', 'd4', 'd5',
1156 's', 't', 'a'):
1157 msg = 'bad field spec: %%%s%%' % (item[1], )
1158 raise RuntimeError(msg)
1159 el1 = self.make_field_element(
1160 parent,
1161 item[1], style_name, automatic_styles)
1162 if el1 is None:
1163 msg = 'bad field spec: %%%s%%' % (item[1], )
1164 raise RuntimeError(msg)
1165 else:
1166 current_element = el1
1167 else:
1168 if current_element is None:
1169 parent.text = item[1]
1170 else:
1171 current_element.tail = item[1]
1173 def make_field_element(self, parent, text, style_name, automatic_styles):
1174 if text == 'p':
1175 el1 = SubElement(parent, 'text:page-number', attrib={
1176 # 'text:style-name': self.rststyle(style_name),
1177 'text:select-page': 'current',
1179 elif text == 'P':
1180 el1 = SubElement(parent, 'text:page-count', attrib={
1181 # 'text:style-name': self.rststyle(style_name),
1183 elif text == 't1':
1184 self.style_index += 1
1185 el1 = SubElement(parent, 'text:time', attrib={
1186 'text:style-name': self.rststyle(style_name),
1187 'text:fixed': 'true',
1188 'style:data-style-name':
1189 'rst-time-style-%d' % self.style_index,
1191 el2 = SubElement(automatic_styles, 'number:time-style', attrib={
1192 'style:name': 'rst-time-style-%d' % self.style_index,
1193 'xmlns:number': SNSD['number'],
1194 'xmlns:style': SNSD['style'],
1196 el3 = SubElement(el2, 'number:hours', attrib={
1197 'number:style': 'long',
1199 el3 = SubElement(el2, 'number:text')
1200 el3.text = ':'
1201 el3 = SubElement(el2, 'number:minutes', attrib={
1202 'number:style': 'long',
1204 elif text == 't2':
1205 self.style_index += 1
1206 el1 = SubElement(parent, 'text:time', attrib={
1207 'text:style-name': self.rststyle(style_name),
1208 'text:fixed': 'true',
1209 'style:data-style-name':
1210 'rst-time-style-%d' % self.style_index,
1212 el2 = SubElement(automatic_styles, 'number:time-style', attrib={
1213 'style:name': 'rst-time-style-%d' % self.style_index,
1214 'xmlns:number': SNSD['number'],
1215 'xmlns:style': SNSD['style'],
1217 el3 = SubElement(el2, 'number:hours', attrib={
1218 'number:style': 'long',
1220 el3 = SubElement(el2, 'number:text')
1221 el3.text = ':'
1222 el3 = SubElement(el2, 'number:minutes', attrib={
1223 'number:style': 'long',
1225 el3 = SubElement(el2, 'number:text')
1226 el3.text = ':'
1227 el3 = SubElement(el2, 'number:seconds', attrib={
1228 'number:style': 'long',
1230 elif text == 't3':
1231 self.style_index += 1
1232 el1 = SubElement(parent, 'text:time', attrib={
1233 'text:style-name': self.rststyle(style_name),
1234 'text:fixed': 'true',
1235 'style:data-style-name':
1236 'rst-time-style-%d' % self.style_index,
1238 el2 = SubElement(automatic_styles, 'number:time-style', attrib={
1239 'style:name': 'rst-time-style-%d' % self.style_index,
1240 'xmlns:number': SNSD['number'],
1241 'xmlns:style': SNSD['style'],
1243 el3 = SubElement(el2, 'number:hours', attrib={
1244 'number:style': 'long',
1246 el3 = SubElement(el2, 'number:text')
1247 el3.text = ':'
1248 el3 = SubElement(el2, 'number:minutes', attrib={
1249 'number:style': 'long',
1251 el3 = SubElement(el2, 'number:text')
1252 el3.text = ' '
1253 el3 = SubElement(el2, 'number:am-pm')
1254 elif text == 't4':
1255 self.style_index += 1
1256 el1 = SubElement(parent, 'text:time', attrib={
1257 'text:style-name': self.rststyle(style_name),
1258 'text:fixed': 'true',
1259 'style:data-style-name':
1260 'rst-time-style-%d' % self.style_index,
1262 el2 = SubElement(automatic_styles, 'number:time-style', attrib={
1263 'style:name': 'rst-time-style-%d' % self.style_index,
1264 'xmlns:number': SNSD['number'],
1265 'xmlns:style': SNSD['style'],
1267 el3 = SubElement(el2, 'number:hours', attrib={
1268 'number:style': 'long',
1270 el3 = SubElement(el2, 'number:text')
1271 el3.text = ':'
1272 el3 = SubElement(el2, 'number:minutes', attrib={
1273 'number:style': 'long',
1275 el3 = SubElement(el2, 'number:text')
1276 el3.text = ':'
1277 el3 = SubElement(el2, 'number:seconds', attrib={
1278 'number:style': 'long',
1280 el3 = SubElement(el2, 'number:text')
1281 el3.text = ' '
1282 el3 = SubElement(el2, 'number:am-pm')
1283 elif text == 'd1':
1284 self.style_index += 1
1285 el1 = SubElement(parent, 'text:date', attrib={
1286 'text:style-name': self.rststyle(style_name),
1287 'style:data-style-name':
1288 'rst-date-style-%d' % self.style_index,
1290 el2 = SubElement(automatic_styles, 'number:date-style', attrib={
1291 'style:name': 'rst-date-style-%d' % self.style_index,
1292 'number:automatic-order': 'true',
1293 'xmlns:number': SNSD['number'],
1294 'xmlns:style': SNSD['style'],
1296 el3 = SubElement(el2, 'number:month', attrib={
1297 'number:style': 'long',
1299 el3 = SubElement(el2, 'number:text')
1300 el3.text = '/'
1301 el3 = SubElement(el2, 'number:day', attrib={
1302 'number:style': 'long',
1304 el3 = SubElement(el2, 'number:text')
1305 el3.text = '/'
1306 el3 = SubElement(el2, 'number:year')
1307 elif text == 'd2':
1308 self.style_index += 1
1309 el1 = SubElement(parent, 'text:date', attrib={
1310 'text:style-name': self.rststyle(style_name),
1311 'style:data-style-name':
1312 'rst-date-style-%d' % self.style_index,
1314 el2 = SubElement(automatic_styles, 'number:date-style', attrib={
1315 'style:name': 'rst-date-style-%d' % self.style_index,
1316 'number:automatic-order': 'true',
1317 'xmlns:number': SNSD['number'],
1318 'xmlns:style': SNSD['style'],
1320 el3 = SubElement(el2, 'number:month', attrib={
1321 'number:style': 'long',
1323 el3 = SubElement(el2, 'number:text')
1324 el3.text = '/'
1325 el3 = SubElement(el2, 'number:day', attrib={
1326 'number:style': 'long',
1328 el3 = SubElement(el2, 'number:text')
1329 el3.text = '/'
1330 el3 = SubElement(el2, 'number:year', attrib={
1331 'number:style': 'long',
1333 elif text == 'd3':
1334 self.style_index += 1
1335 el1 = SubElement(parent, 'text:date', attrib={
1336 'text:style-name': self.rststyle(style_name),
1337 'style:data-style-name':
1338 'rst-date-style-%d' % self.style_index,
1340 el2 = SubElement(automatic_styles, 'number:date-style', attrib={
1341 'style:name': 'rst-date-style-%d' % self.style_index,
1342 'number:automatic-order': 'true',
1343 'xmlns:number': SNSD['number'],
1344 'xmlns:style': SNSD['style'],
1346 el3 = SubElement(el2, 'number:month', attrib={
1347 'number:textual': 'true',
1349 el3 = SubElement(el2, 'number:text')
1350 el3.text = ' '
1351 el3 = SubElement(el2, 'number:day', attrib={})
1352 el3 = SubElement(el2, 'number:text')
1353 el3.text = ', '
1354 el3 = SubElement(el2, 'number:year', attrib={
1355 'number:style': 'long',
1357 elif text == 'd4':
1358 self.style_index += 1
1359 el1 = SubElement(parent, 'text:date', attrib={
1360 'text:style-name': self.rststyle(style_name),
1361 'style:data-style-name':
1362 'rst-date-style-%d' % self.style_index,
1364 el2 = SubElement(automatic_styles, 'number:date-style', attrib={
1365 'style:name': 'rst-date-style-%d' % self.style_index,
1366 'number:automatic-order': 'true',
1367 'xmlns:number': SNSD['number'],
1368 'xmlns:style': SNSD['style'],
1370 el3 = SubElement(el2, 'number:month', attrib={
1371 'number:textual': 'true',
1372 'number:style': 'long',
1374 el3 = SubElement(el2, 'number:text')
1375 el3.text = ' '
1376 el3 = SubElement(el2, 'number:day', attrib={})
1377 el3 = SubElement(el2, 'number:text')
1378 el3.text = ', '
1379 el3 = SubElement(el2, 'number:year', attrib={
1380 'number:style': 'long',
1382 elif text == 'd5':
1383 self.style_index += 1
1384 el1 = SubElement(parent, 'text:date', attrib={
1385 'text:style-name': self.rststyle(style_name),
1386 'style:data-style-name':
1387 'rst-date-style-%d' % self.style_index,
1389 el2 = SubElement(automatic_styles, 'number:date-style', attrib={
1390 'style:name': 'rst-date-style-%d' % self.style_index,
1391 'xmlns:number': SNSD['number'],
1392 'xmlns:style': SNSD['style'],
1394 el3 = SubElement(el2, 'number:year', attrib={
1395 'number:style': 'long',
1397 el3 = SubElement(el2, 'number:text')
1398 el3.text = '-'
1399 el3 = SubElement(el2, 'number:month', attrib={
1400 'number:style': 'long',
1402 el3 = SubElement(el2, 'number:text')
1403 el3.text = '-'
1404 el3 = SubElement(el2, 'number:day', attrib={
1405 'number:style': 'long',
1407 elif text == 's':
1408 el1 = SubElement(parent, 'text:subject', attrib={
1409 'text:style-name': self.rststyle(style_name),
1411 elif text == 't':
1412 el1 = SubElement(parent, 'text:title', attrib={
1413 'text:style-name': self.rststyle(style_name),
1415 elif text == 'a':
1416 el1 = SubElement(parent, 'text:author-name', attrib={
1417 'text:fixed': 'false',
1419 else:
1420 el1 = None
1421 return el1
1423 def split_field_specifiers_iter(self, text):
1424 pos1 = 0
1425 while True:
1426 mo = ODFTranslator.field_pat.search(text, pos1)
1427 if mo:
1428 pos2 = mo.start()
1429 if pos2 > pos1:
1430 yield ODFTranslator.code_text, text[pos1:pos2]
1431 yield ODFTranslator.code_field, mo.group(1)
1432 pos1 = mo.end()
1433 else:
1434 break
1435 trailing = text[pos1:]
1436 if trailing:
1437 yield ODFTranslator.code_text, trailing
1439 def astext(self):
1440 root = self.content_tree.getroot()
1441 et = etree.ElementTree(root)
1442 return ToString(et)
1444 def content_astext(self):
1445 return self.astext()
1447 def set_title(self, title) -> None:
1448 self.title = title
1450 def get_title(self):
1451 return self.title
1453 def set_embedded_file_list(self, embedded_file_list) -> None:
1454 self.embedded_file_list = embedded_file_list
1456 def get_embedded_file_list(self):
1457 return self.embedded_file_list
1459 def get_meta_dict(self):
1460 return self.meta_dict
1462 def process_footnotes(self) -> None:
1463 for node, el1 in self.footnote_list:
1464 backrefs = node.attributes.get('backrefs', [])
1465 first = True
1466 for ref in backrefs:
1467 el2 = self.footnote_ref_dict.get(ref)
1468 if el2 is not None:
1469 if first:
1470 first = False
1471 el3 = copy.deepcopy(el1)
1472 el2.append(el3)
1473 else:
1474 if len(el2) > 0: # and 'id' in el2.attrib:
1475 child = el2[0]
1476 ref1 = child.text
1477 attribkey = add_ns('text:id', nsdict=SNSD)
1478 id1 = el2.get(attribkey, 'footnote-error')
1479 if id1 is None:
1480 id1 = ''
1481 tag = add_ns('text:note-ref', nsdict=SNSD)
1482 el2.tag = tag
1483 if self.settings.endnotes_end_doc:
1484 note_class = 'endnote'
1485 else:
1486 note_class = 'footnote'
1487 el2.attrib.clear()
1488 attribkey = add_ns('text:note-class', nsdict=SNSD)
1489 el2.attrib[attribkey] = note_class
1490 attribkey = add_ns('text:ref-name', nsdict=SNSD)
1491 el2.attrib[attribkey] = id1
1492 attribkey = add_ns(
1493 'text:reference-format', nsdict=SNSD)
1494 el2.attrib[attribkey] = 'page'
1495 el2.text = ref1
1498 # Utility methods
1500 def append_child(self, tag, attrib=None, parent=None):
1501 if parent is None:
1502 parent = self.current_element
1503 return SubElement(parent, tag, attrib)
1505 def append_p(self, style, text=None):
1506 result = self.append_child('text:p', attrib={
1507 'text:style-name': self.rststyle(style)})
1508 self.append_pending_ids(result)
1509 if text is not None:
1510 result.text = text
1511 return result
1513 def append_pending_ids(self, el) -> None:
1514 if self.settings.create_links:
1515 for id in self.pending_ids:
1516 SubElement(el, 'text:reference-mark', attrib={
1517 'text:name': id})
1518 self.pending_ids = []
1520 def set_current_element(self, el) -> None:
1521 self.current_element = el
1523 def set_to_parent(self) -> None:
1524 self.current_element = self.current_element.getparent()
1526 def generate_labeled_block(self, node, label):
1527 label = '%s:' % (self.language.labels[label], )
1528 el = self.append_p('textbody')
1529 el1 = SubElement(
1530 el, 'text:span',
1531 attrib={'text:style-name': self.rststyle('strong')})
1532 el1.text = label
1533 return self.append_p('blockindent')
1535 def generate_labeled_line(self, node, label):
1536 label = '%s:' % (self.language.labels[label], )
1537 el = self.append_p('textbody')
1538 el1 = SubElement(
1539 el, 'text:span',
1540 attrib={'text:style-name': self.rststyle('strong')})
1541 el1.text = label
1542 el1.tail = node.astext()
1543 return el
1545 def encode(self, text):
1546 return text.replace('\n', " ")
1549 # Visitor functions
1551 # In alphabetic order, more or less.
1552 # See docutils.docutils.nodes.node_class_names.
1555 def dispatch_visit(self, node) -> None:
1556 """Override to catch basic attributes which many nodes have."""
1557 self.handle_basic_atts(node)
1558 nodes.GenericNodeVisitor.dispatch_visit(self, node)
1560 def handle_basic_atts(self, node) -> None:
1561 if isinstance(node, nodes.Element) and node['ids']:
1562 self.pending_ids += node['ids']
1564 def default_visit(self, node) -> None:
1565 self.document.reporter.warning(f'missing visit_{node.tagname}',
1566 base_node=node)
1568 def default_departure(self, node) -> None:
1569 self.document.reporter.warning(f'missing depart_{node.tagname}',
1570 base_node=node)
1572 def visit_Text(self, node) -> None:
1573 # Skip nodes whose text has been processed in parent nodes.
1574 if isinstance(node.parent, docutils.nodes.literal_block):
1575 return
1576 text = node.astext()
1577 # Are we in mixed content? If so, add the text to the
1578 # etree tail of the previous sibling element.
1579 if len(self.current_element) > 0:
1580 if self.current_element[-1].tail:
1581 self.current_element[-1].tail += text
1582 else:
1583 self.current_element[-1].tail = text
1584 else:
1585 if self.current_element.text:
1586 self.current_element.text += text
1587 else:
1588 self.current_element.text = text
1590 def depart_Text(self, node) -> None:
1591 pass
1594 # Pre-defined fields
1597 def visit_address(self, node) -> None:
1598 el = self.generate_labeled_block(node, 'address')
1599 self.set_current_element(el)
1601 def depart_address(self, node) -> None:
1602 self.set_to_parent()
1604 def visit_author(self, node) -> None:
1605 if isinstance(node.parent, nodes.authors):
1606 el = self.append_p('blockindent')
1607 else:
1608 el = self.generate_labeled_block(node, 'author')
1609 self.set_current_element(el)
1611 def depart_author(self, node) -> None:
1612 self.set_to_parent()
1614 def visit_authors(self, node) -> None:
1615 label = '%s:' % (self.language.labels['authors'], )
1616 el = self.append_p('textbody')
1617 el1 = SubElement(
1618 el, 'text:span',
1619 attrib={'text:style-name': self.rststyle('strong')})
1620 el1.text = label
1622 def depart_authors(self, node) -> None:
1623 pass
1625 def visit_contact(self, node) -> None:
1626 el = self.generate_labeled_block(node, 'contact')
1627 self.set_current_element(el)
1629 def depart_contact(self, node) -> None:
1630 self.set_to_parent()
1632 def visit_copyright(self, node) -> None:
1633 el = self.generate_labeled_block(node, 'copyright')
1634 self.set_current_element(el)
1636 def depart_copyright(self, node) -> None:
1637 self.set_to_parent()
1639 def visit_date(self, node) -> None:
1640 self.generate_labeled_line(node, 'date')
1642 def depart_date(self, node) -> None:
1643 pass
1645 def visit_organization(self, node) -> None:
1646 el = self.generate_labeled_block(node, 'organization')
1647 self.set_current_element(el)
1649 def depart_organization(self, node) -> None:
1650 self.set_to_parent()
1652 def visit_status(self, node) -> None:
1653 el = self.generate_labeled_block(node, 'status')
1654 self.set_current_element(el)
1656 def depart_status(self, node) -> None:
1657 self.set_to_parent()
1659 def visit_revision(self, node) -> None:
1660 self.generate_labeled_line(node, 'revision')
1662 def depart_revision(self, node) -> None:
1663 pass
1665 def visit_version(self, node) -> None:
1666 self.generate_labeled_line(node, 'version')
1667 # self.set_current_element(el)
1669 def depart_version(self, node) -> None:
1670 # self.set_to_parent()
1671 pass
1673 def visit_attribution(self, node) -> None:
1674 self.append_p('attribution', node.astext())
1676 def depart_attribution(self, node) -> None:
1677 pass
1679 def visit_block_quote(self, node) -> None:
1680 if 'epigraph' in node.attributes['classes']:
1681 self.paragraph_style_stack.append(self.rststyle('epigraph'))
1682 self.blockstyle = self.rststyle('epigraph')
1683 elif 'highlights' in node.attributes['classes']:
1684 self.paragraph_style_stack.append(self.rststyle('highlights'))
1685 self.blockstyle = self.rststyle('highlights')
1686 else:
1687 self.paragraph_style_stack.append(self.rststyle('blockquote'))
1688 self.blockstyle = self.rststyle('blockquote')
1689 self.line_indent_level += 1
1691 def depart_block_quote(self, node) -> None:
1692 self.paragraph_style_stack.pop()
1693 self.blockstyle = ''
1694 self.line_indent_level -= 1
1696 def visit_bullet_list(self, node) -> None:
1697 self.list_level += 1
1698 if self.in_table_of_contents:
1699 if self.settings.generate_oowriter_toc:
1700 pass
1701 else:
1702 if 'classes' in node and \
1703 'auto-toc' in node.attributes['classes']:
1704 el = SubElement(self.current_element, 'text:list', attrib={
1705 'text:style-name': self.rststyle('tocenumlist'),
1707 self.list_style_stack.append(self.rststyle('enumitem'))
1708 else:
1709 el = SubElement(self.current_element, 'text:list', attrib={
1710 'text:style-name': self.rststyle('tocbulletlist'),
1712 self.list_style_stack.append(self.rststyle('bulletitem'))
1713 self.set_current_element(el)
1714 else:
1715 if self.blockstyle == self.rststyle('blockquote'):
1716 el = SubElement(self.current_element, 'text:list', attrib={
1717 'text:style-name': self.rststyle('blockquote-bulletlist'),
1719 self.list_style_stack.append(
1720 self.rststyle('blockquote-bulletitem'))
1721 elif self.blockstyle == self.rststyle('highlights'):
1722 el = SubElement(self.current_element, 'text:list', attrib={
1723 'text:style-name': self.rststyle('highlights-bulletlist'),
1725 self.list_style_stack.append(
1726 self.rststyle('highlights-bulletitem'))
1727 elif self.blockstyle == self.rststyle('epigraph'):
1728 el = SubElement(self.current_element, 'text:list', attrib={
1729 'text:style-name': self.rststyle('epigraph-bulletlist'),
1731 self.list_style_stack.append(
1732 self.rststyle('epigraph-bulletitem'))
1733 else:
1734 el = SubElement(self.current_element, 'text:list', attrib={
1735 'text:style-name': self.rststyle('bulletlist'),
1737 self.list_style_stack.append(self.rststyle('bulletitem'))
1738 self.set_current_element(el)
1740 def depart_bullet_list(self, node) -> None:
1741 if self.in_table_of_contents:
1742 if self.settings.generate_oowriter_toc:
1743 pass
1744 else:
1745 self.set_to_parent()
1746 self.list_style_stack.pop()
1747 else:
1748 self.set_to_parent()
1749 self.list_style_stack.pop()
1750 self.list_level -= 1
1752 def visit_caption(self, node):
1753 raise nodes.SkipChildren
1755 def depart_caption(self, node) -> None:
1756 pass
1758 def visit_comment(self, node) -> None:
1759 el = self.append_p('textbody')
1760 el1 = SubElement(el, 'office:annotation', attrib={})
1761 el2 = SubElement(el1, 'dc:creator', attrib={})
1762 s1 = os.environ.get('USER', '')
1763 el2.text = s1
1764 el2 = SubElement(el1, 'text:p', attrib={})
1765 el2.text = node.astext()
1767 def depart_comment(self, node) -> None:
1768 pass
1770 def visit_compound(self, node) -> None:
1771 # The compound directive currently receives no special treatment.
1772 pass
1774 def depart_compound(self, node) -> None:
1775 pass
1777 def visit_container(self, node) -> None:
1778 styles = node.attributes.get('classes', ())
1779 if len(styles) > 0:
1780 self.paragraph_style_stack.append(self.rststyle(styles[0]))
1782 def depart_container(self, node) -> None:
1783 styles = node.attributes.get('classes', ())
1784 if len(styles) > 0:
1785 self.paragraph_style_stack.pop()
1787 def visit_decoration(self, node) -> None:
1788 pass
1790 def depart_decoration(self, node) -> None:
1791 pass
1793 def visit_definition_list(self, node):
1794 self.def_list_level += 1
1795 if self.list_level > 5:
1796 raise RuntimeError(
1797 'max definition list nesting level exceeded')
1799 def depart_definition_list(self, node) -> None:
1800 self.def_list_level -= 1
1802 def visit_definition_list_item(self, node) -> None:
1803 pass
1805 def depart_definition_list_item(self, node) -> None:
1806 pass
1808 def visit_term(self, node):
1809 el = self.append_p('deflist-term-%d' % self.def_list_level)
1810 el.text = node.astext()
1811 self.set_current_element(el)
1812 raise nodes.SkipChildren
1814 def depart_term(self, node) -> None:
1815 self.set_to_parent()
1817 def visit_definition(self, node) -> None:
1818 self.paragraph_style_stack.append(
1819 self.rststyle('deflist-def-%d' % self.def_list_level))
1820 self.bumped_list_level_stack.append(ListLevel(1))
1822 def depart_definition(self, node) -> None:
1823 self.paragraph_style_stack.pop()
1824 self.bumped_list_level_stack.pop()
1826 def visit_classifier(self, node) -> None:
1827 if len(self.current_element) > 0:
1828 el = self.current_element[-1]
1829 el1 = SubElement(
1830 el, 'text:span',
1831 attrib={'text:style-name': self.rststyle('emphasis')})
1832 el1.text = ' (%s)' % (node.astext(), )
1834 def depart_classifier(self, node) -> None:
1835 pass
1837 def visit_document(self, node) -> None:
1838 pass
1840 def depart_document(self, node) -> None:
1841 self.process_footnotes()
1843 def visit_docinfo(self, node) -> None:
1844 self.section_level += 1
1845 self.section_count += 1
1846 if self.settings.create_sections:
1847 el = self.append_child(
1848 'text:section', attrib={
1849 'text:name': 'Section%d' % self.section_count,
1850 'text:style-name': 'Sect%d' % self.section_level,
1853 self.set_current_element(el)
1855 def depart_docinfo(self, node) -> None:
1856 self.section_level -= 1
1857 if self.settings.create_sections:
1858 self.set_to_parent()
1860 def visit_emphasis(self, node) -> None:
1861 el = SubElement(
1862 self.current_element, 'text:span',
1863 attrib={'text:style-name': self.rststyle('emphasis')})
1864 self.set_current_element(el)
1866 def depart_emphasis(self, node) -> None:
1867 self.set_to_parent()
1869 def visit_enumerated_list(self, node) -> None:
1870 el1 = self.current_element
1871 if self.blockstyle == self.rststyle('blockquote'):
1872 el2 = SubElement(el1, 'text:list', attrib={
1873 'text:style-name': self.rststyle('blockquote-enumlist'),
1875 self.list_style_stack.append(self.rststyle('blockquote-enumitem'))
1876 elif self.blockstyle == self.rststyle('highlights'):
1877 el2 = SubElement(el1, 'text:list', attrib={
1878 'text:style-name': self.rststyle('highlights-enumlist'),
1880 self.list_style_stack.append(self.rststyle('highlights-enumitem'))
1881 elif self.blockstyle == self.rststyle('epigraph'):
1882 el2 = SubElement(el1, 'text:list', attrib={
1883 'text:style-name': self.rststyle('epigraph-enumlist'),
1885 self.list_style_stack.append(self.rststyle('epigraph-enumitem'))
1886 else:
1887 liststylename = 'enumlist-%s' % (node.get('enumtype', 'arabic'), )
1888 el2 = SubElement(el1, 'text:list', attrib={
1889 'text:style-name': self.rststyle(liststylename),
1891 self.list_style_stack.append(self.rststyle('enumitem'))
1892 self.set_current_element(el2)
1894 def depart_enumerated_list(self, node) -> None:
1895 self.set_to_parent()
1896 self.list_style_stack.pop()
1898 def visit_list_item(self, node) -> None:
1899 # If we are in a "bumped" list level, then wrap this
1900 # list in an outer lists in order to increase the
1901 # indentation level.
1902 if self.in_table_of_contents:
1903 if self.settings.generate_oowriter_toc:
1904 self.paragraph_style_stack.append(
1905 self.rststyle('contents-%d' % (self.list_level, )))
1906 else:
1907 el1 = self.append_child('text:list-item')
1908 self.set_current_element(el1)
1909 else:
1910 el1 = self.append_child('text:list-item')
1911 el3 = el1
1912 if len(self.bumped_list_level_stack) > 0:
1913 level_obj = self.bumped_list_level_stack[-1]
1914 if level_obj.get_sibling():
1915 level_obj.set_nested(False)
1916 for level_obj1 in self.bumped_list_level_stack:
1917 for idx in range(level_obj1.get_level()):
1918 el2 = self.append_child('text:list', parent=el3)
1919 el3 = self.append_child(
1920 'text:list-item', parent=el2)
1921 self.paragraph_style_stack.append(self.list_style_stack[-1])
1922 self.set_current_element(el3)
1924 def depart_list_item(self, node) -> None:
1925 if self.in_table_of_contents:
1926 if self.settings.generate_oowriter_toc:
1927 self.paragraph_style_stack.pop()
1928 else:
1929 self.set_to_parent()
1930 else:
1931 if len(self.bumped_list_level_stack) > 0:
1932 level_obj = self.bumped_list_level_stack[-1]
1933 if level_obj.get_sibling():
1934 level_obj.set_nested(True)
1935 for level_obj1 in self.bumped_list_level_stack:
1936 for idx in range(level_obj1.get_level()):
1937 self.set_to_parent()
1938 self.set_to_parent()
1939 self.paragraph_style_stack.pop()
1940 self.set_to_parent()
1942 def visit_header(self, node) -> None:
1943 self.in_header = True
1945 def depart_header(self, node) -> None:
1946 self.in_header = False
1948 def visit_footer(self, node) -> None:
1949 self.in_footer = True
1951 def depart_footer(self, node) -> None:
1952 self.in_footer = False
1954 def visit_field(self, node) -> None:
1955 pass
1957 def depart_field(self, node) -> None:
1958 pass
1960 def visit_field_list(self, node) -> None:
1961 pass
1963 def depart_field_list(self, node) -> None:
1964 pass
1966 def visit_field_name(self, node) -> None:
1967 el = self.append_p('textbody')
1968 el1 = SubElement(
1969 el, 'text:span',
1970 attrib={'text:style-name': self.rststyle('strong')})
1971 el1.text = node.astext()
1973 def depart_field_name(self, node) -> None:
1974 pass
1976 def visit_field_body(self, node) -> None:
1977 self.paragraph_style_stack.append(self.rststyle('blockindent'))
1979 def depart_field_body(self, node) -> None:
1980 self.paragraph_style_stack.pop()
1982 def visit_figure(self, node) -> None:
1983 pass
1985 def depart_figure(self, node) -> None:
1986 pass
1988 def visit_footnote(self, node) -> None:
1989 self.footnote_level += 1
1990 self.save_footnote_current = self.current_element
1991 el1 = Element('text:note-body')
1992 self.current_element = el1
1993 self.footnote_list.append((node, el1))
1994 if isinstance(node, docutils.nodes.citation):
1995 self.paragraph_style_stack.append(self.rststyle('citation'))
1996 else:
1997 self.paragraph_style_stack.append(self.rststyle('footnote'))
1999 def depart_footnote(self, node) -> None:
2000 self.paragraph_style_stack.pop()
2001 self.current_element = self.save_footnote_current
2002 self.footnote_level -= 1
2004 footnote_chars = [
2005 '*', '**', '***',
2006 '++', '+++',
2007 '##', '###',
2008 '@@', '@@@',
2011 def visit_footnote_reference(self, node):
2012 if self.footnote_level <= 0:
2013 id = node.attributes['ids'][0]
2014 refid = node.attributes.get('refid')
2015 if refid is None:
2016 refid = ''
2017 if self.settings.endnotes_end_doc:
2018 note_class = 'endnote'
2019 else:
2020 note_class = 'footnote'
2021 el1 = self.append_child('text:note', attrib={
2022 'text:id': '%s' % (refid, ),
2023 'text:note-class': note_class,
2025 note_auto = str(node.attributes.get('auto', 1))
2026 if isinstance(node, docutils.nodes.citation_reference):
2027 citation = '[%s]' % node.astext()
2028 el2 = SubElement(el1, 'text:note-citation', attrib={
2029 'text:label': citation,
2031 el2.text = citation
2032 elif note_auto == '1':
2033 el2 = SubElement(el1, 'text:note-citation', attrib={
2034 'text:label': node.astext(),
2036 el2.text = node.astext()
2037 elif note_auto == '*':
2038 if self.footnote_chars_idx >= len(
2039 ODFTranslator.footnote_chars):
2040 self.footnote_chars_idx = 0
2041 footnote_char = ODFTranslator.footnote_chars[
2042 self.footnote_chars_idx]
2043 self.footnote_chars_idx += 1
2044 el2 = SubElement(el1, 'text:note-citation', attrib={
2045 'text:label': footnote_char,
2047 el2.text = footnote_char
2048 self.footnote_ref_dict[id] = el1
2049 raise nodes.SkipChildren
2051 def depart_footnote_reference(self, node) -> None:
2052 pass
2054 def visit_citation(self, node) -> None:
2055 self.in_citation = True
2056 for id in node.attributes['ids']:
2057 self.citation_id = id
2058 break
2059 self.paragraph_style_stack.append(self.rststyle('blockindent'))
2060 self.bumped_list_level_stack.append(ListLevel(1))
2062 def depart_citation(self, node) -> None:
2063 self.citation_id = None
2064 self.paragraph_style_stack.pop()
2065 self.bumped_list_level_stack.pop()
2066 self.in_citation = False
2068 def visit_citation_reference(self, node) -> None:
2069 if self.settings.create_links:
2070 id = node.attributes['refid']
2071 el = self.append_child('text:reference-ref', attrib={
2072 'text:ref-name': '%s' % (id, ),
2073 'text:reference-format': 'text',
2075 el.text = '['
2076 self.set_current_element(el)
2077 elif self.current_element.text is None:
2078 self.current_element.text = '['
2079 else:
2080 self.current_element.text += '['
2082 def depart_citation_reference(self, node) -> None:
2083 self.current_element.text += ']'
2084 if self.settings.create_links:
2085 self.set_to_parent()
2087 def visit_label(self, node):
2088 if isinstance(node.parent, docutils.nodes.footnote):
2089 raise nodes.SkipChildren
2090 elif self.citation_id is not None:
2091 el = self.append_p('textbody')
2092 self.set_current_element(el)
2093 if self.settings.create_links:
2094 el0 = SubElement(el, 'text:span')
2095 el0.text = '['
2096 self.append_child('text:reference-mark-start', attrib={
2097 'text:name': '%s' % (self.citation_id, ),
2099 else:
2100 el.text = '['
2102 def depart_label(self, node) -> None:
2103 if isinstance(node.parent, docutils.nodes.footnote):
2104 pass
2105 elif self.citation_id is not None:
2106 if self.settings.create_links:
2107 self.append_child('text:reference-mark-end', attrib={
2108 'text:name': '%s' % (self.citation_id, ),
2110 el0 = SubElement(self.current_element, 'text:span')
2111 el0.text = ']'
2112 else:
2113 self.current_element.text += ']'
2114 self.set_to_parent()
2116 def visit_generated(self, node) -> None:
2117 pass
2119 def depart_generated(self, node) -> None:
2120 pass
2122 def check_file_exists(self, path) -> int:
2123 if os.path.exists(path):
2124 return 1
2125 else:
2126 return 0
2128 def visit_image(self, node) -> None:
2129 # Capture the image file.
2130 source = node['uri']
2131 uri_parts = urllib.parse.urlparse(source)
2132 if uri_parts.scheme in ('', 'file'):
2133 source = urllib.parse.unquote(uri_parts.path)
2134 if self.settings.root_prefix and source.startswith('/'):
2135 root_prefix = Path(self.settings.root_prefix)
2136 source = (root_prefix/source[1:]).as_posix()
2137 else:
2138 # adapt relative paths
2139 docsource, line = utils.get_source_line(node)
2140 if docsource:
2141 dirname = os.path.dirname(docsource)
2142 if dirname:
2143 source = os.path.join(dirname, source)
2144 if not self.check_file_exists(source):
2145 self.document.reporter.warning(
2146 f'Cannot find image file "{source}".', base_node=node)
2147 return
2148 if source in self.image_dict:
2149 filename, destination = self.image_dict[source]
2150 else:
2151 self.image_count += 1
2152 filename = os.path.split(source)[1]
2153 destination = 'Pictures/1%08x%s' % (self.image_count, filename)
2154 if uri_parts.scheme in ('', 'file'):
2155 spec = (os.path.abspath(source), destination,)
2156 else:
2157 try:
2158 with urllib.request.urlopen(source) as imgfile:
2159 content = imgfile.read()
2160 except urllib.error.URLError as err:
2161 self.document.reporter.warning(
2162 f'Cannot open image URL "{source}". {err}',
2163 base_node=node)
2164 return
2165 with tempfile.NamedTemporaryFile('wb',
2166 delete=False) as imgfile2:
2167 imgfile2.write(content)
2168 source = imgfile2.name
2169 spec = (source, destination,)
2170 self.embedded_file_list.append(spec)
2171 self.image_dict[source] = (source, destination,)
2172 # Is this a figure (containing an image) or just a plain image?
2173 if self.in_paragraph:
2174 el1 = self.current_element
2175 else:
2176 el1 = SubElement(
2177 self.current_element, 'text:p',
2178 attrib={'text:style-name': self.rststyle('textbody')})
2179 el2 = el1
2180 if isinstance(node.parent, docutils.nodes.figure):
2181 el3, el4, el5, caption = self.generate_figure(
2182 node, source,
2183 destination, el2)
2184 attrib = {}
2185 el6, width = self.generate_image(
2186 node, source, destination,
2187 el5, attrib)
2188 if caption is not None:
2189 el6.tail = caption
2190 else: # if isinstance(node.parent, docutils.nodes.image):
2191 self.generate_image(node, source, destination, el2)
2193 def depart_image(self, node) -> None:
2194 pass
2196 def get_image_width_height(self, node, attr):
2197 size = None
2198 unit = None
2199 if attr in node.attributes:
2200 size = node.attributes[attr]
2201 size = size.strip()
2202 # For conversion factors, see:
2203 # http://www.unitconversion.org/unit_converter/typography-ex.html
2204 try:
2205 if size.endswith('%'):
2206 if attr == 'height':
2207 # Percentage allowed for width but not height.
2208 raise ValueError('percentage not allowed for height')
2209 size = size.rstrip(' %')
2210 size = float(size) / 100.0
2211 unit = '%'
2212 else:
2213 size, unit = self.convert_to_cm(size)
2214 except ValueError as exp:
2215 self.document.reporter.warning(
2216 f'Invalid {attr} for image: "{node.attributes[attr]}". '
2217 f'Error: "{exp}".', base_node=node)
2218 size, unit = 0.5, 'cm' # fallback to avoid consequential error
2219 return size, unit
2221 def convert_to_cm(self, size):
2222 """Convert various units to centimeters.
2224 Note that a call to this method should be wrapped in:
2225 try: except ValueError:
2227 size = size.strip()
2228 if size.endswith('px'):
2229 size = float(size[:-2]) * 0.026 # convert px to cm
2230 elif size.endswith('in'):
2231 size = float(size[:-2]) * 2.54 # convert in to cm
2232 elif size.endswith('pt'):
2233 size = float(size[:-2]) * 0.035 # convert pt to cm
2234 elif size.endswith('pc'):
2235 size = float(size[:-2]) * 0.423 # convert pc to cm
2236 elif size.endswith('mm'):
2237 size = float(size[:-2]) * 0.1 # convert mm to cm
2238 elif size.endswith('Q'):
2239 size = float(size[:-1]) * 0.25 # convert Q to cm
2240 elif size.endswith('cm'):
2241 size = float(size[:-2])
2242 elif size[-1:] in '0123456789.': # no unit, use px
2243 size = float(size) * 0.026 # convert px to cm
2244 else:
2245 raise ValueError('unit not supported with ODT')
2246 unit = 'cm'
2247 return size, unit
2249 def get_image_scale(self, node):
2250 if 'scale' in node.attributes:
2251 scale = node.attributes['scale']
2252 if scale < 1:
2253 self.document.reporter.warning(
2254 f'scale out of range ({scale}), using 1.', base_node=node)
2255 scale = 1
2256 scale = scale * 0.01
2257 else:
2258 scale = 1.0
2259 return scale
2261 def get_image_scaled_width_height(self, node, source):
2262 """Return the image size in centimeters adjusted by image attrs."""
2263 scale = self.get_image_scale(node)
2264 width, width_unit = self.get_image_width_height(node, 'width')
2265 height, _ = self.get_image_width_height(node, 'height')
2266 dpi = (96, 96) # image resolution in pixel per inch
2267 if width is None or height is None:
2268 if PIL is None:
2269 self.document.reporter.warning(
2270 'image size not fully specified and PIL not installed',
2271 base_node=node)
2272 return '0.5cm', '0.5cm' # prevent consequential error
2273 filename, destination = self.image_dict[source]
2274 with PIL.Image.open(filename, 'r') as img:
2275 img_size = img.size
2276 dpi = img.info.get('dpi', dpi)
2277 # dpi information can be (xdpi, ydpi) or xydpi
2278 try:
2279 iter(dpi)
2280 except TypeError:
2281 dpi = (dpi, dpi)
2282 # TODO: use dpi when converting px to cm
2283 if width is None:
2284 width = img_size[0] * 0.026 # convert px to cm
2285 if height is None and width_unit != '%':
2286 height = img_size[1] * 0.026 # convert px to cm
2287 if width_unit == '%':
2288 factor = width
2289 line_width = self.get_page_width()
2290 width = factor * line_width
2291 if height is None:
2292 # scale proportionally
2293 image_width = img_size[0] * 0.026 # convert px to cm
2294 image_height = img_size[1] * 0.026 # convert px to cm
2295 factor = (factor * line_width) / image_width
2296 height = factor * image_height
2297 width *= scale
2298 height *= scale
2299 width = '%.2fcm' % width
2300 height = '%.2fcm' % height
2301 return width, height
2303 def get_page_width(self):
2304 """Return the document's page width in centimeters."""
2305 root = self.get_dom_stylesheet()
2306 nodes = root.iterfind(
2307 './/{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
2308 'page-layout/'
2309 '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
2310 'page-layout-properties')
2311 width = None
2312 for node in nodes:
2313 page_width = node.get(
2314 '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
2315 'page-width')
2316 margin_left = node.get(
2317 '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
2318 'margin-left')
2319 margin_right = node.get(
2320 '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
2321 'margin-right')
2322 if (page_width is None
2323 or margin_left is None
2324 or margin_right is None):
2325 continue
2326 try:
2327 page_width, _ = self.convert_to_cm(page_width)
2328 margin_left, _ = self.convert_to_cm(margin_left)
2329 margin_right, _ = self.convert_to_cm(margin_right)
2330 except ValueError:
2331 self.document.reporter.warning(
2332 'Stylesheet file contains invalid page width '
2333 'or margin size.')
2334 width = page_width - margin_left - margin_right
2335 if width is None:
2336 # We can't find the width in styles, so we make a guess.
2337 # Use a width of 6 in = 15.24 cm.
2338 width = 15.24
2339 return width
2341 def generate_figure(self, node, source, destination, current_element):
2342 caption = None
2343 width, height = self.get_image_scaled_width_height(node, source)
2344 for node1 in node.parent.children:
2345 if node1.tagname == 'caption':
2346 caption = node1.astext()
2347 self.image_style_count += 1
2349 # Add the style for the caption.
2350 if caption is not None:
2351 attrib = {
2352 'style:class': 'extra',
2353 'style:family': 'paragraph',
2354 'style:name': 'Caption',
2355 'style:parent-style-name': 'Standard',
2357 el1 = SubElement(self.automatic_styles, 'style:style',
2358 attrib=attrib, nsdict=SNSD)
2359 attrib = {
2360 'fo:margin-bottom': '0.0835in',
2361 'fo:margin-top': '0.0835in',
2362 'text:line-number': '0',
2363 'text:number-lines': 'false',
2365 SubElement(el1, 'style:paragraph-properties',
2366 attrib=attrib, nsdict=SNSD)
2367 attrib = {
2368 'fo:font-size': '12pt',
2369 'fo:font-style': 'italic',
2370 'style:font-name': 'Times',
2371 'style:font-name-complex': 'Lucidasans1',
2372 'style:font-size-asian': '12pt',
2373 'style:font-size-complex': '12pt',
2374 'style:font-style-asian': 'italic',
2375 'style:font-style-complex': 'italic',
2377 SubElement(el1, 'style:text-properties',
2378 attrib=attrib, nsdict=SNSD)
2379 style_name = 'rstframestyle%d' % self.image_style_count
2380 draw_name = 'graphics%d' % next(IMAGE_NAME_COUNTER)
2381 # Add the styles
2382 attrib = {
2383 'style:name': style_name,
2384 'style:family': 'graphic',
2385 'style:parent-style-name': self.rststyle('figureframe'),
2387 el1 = SubElement(self.automatic_styles,
2388 'style:style', attrib=attrib, nsdict=SNSD)
2389 attrib = {}
2390 wrap = False
2391 classes = node.parent.attributes.get('classes')
2392 if classes and 'wrap' in classes:
2393 wrap = True
2394 if wrap:
2395 attrib['style:wrap'] = 'dynamic'
2396 else:
2397 attrib['style:wrap'] = 'none'
2398 SubElement(el1, 'style:graphic-properties',
2399 attrib=attrib, nsdict=SNSD)
2400 attrib = {
2401 'draw:style-name': style_name,
2402 'draw:name': draw_name,
2403 'text:anchor-type': 'paragraph',
2404 'draw:z-index': '0',
2406 attrib['svg:width'] = width
2407 el3 = SubElement(current_element, 'draw:frame', attrib=attrib)
2408 attrib = {}
2409 el4 = SubElement(el3, 'draw:text-box', attrib=attrib)
2410 attrib = {
2411 'text:style-name': self.rststyle('caption'),
2413 el5 = SubElement(el4, 'text:p', attrib=attrib)
2414 return el3, el4, el5, caption
2416 def generate_image(self, node, source, destination, current_element,
2417 frame_attrs=None):
2418 width, height = self.get_image_scaled_width_height(node, source)
2419 self.image_style_count += 1
2420 style_name = 'rstframestyle%d' % self.image_style_count
2421 # Add the style.
2422 attrib = {
2423 'style:name': style_name,
2424 'style:family': 'graphic',
2425 'style:parent-style-name': self.rststyle('image'),
2427 el1 = SubElement(self.automatic_styles,
2428 'style:style', attrib=attrib, nsdict=SNSD)
2429 halign = None
2430 valign = None
2431 if 'align' in node.attributes:
2432 align = node.attributes['align'].split()
2433 for val in align:
2434 if val in ('left', 'center', 'right'):
2435 halign = val
2436 elif val in ('top', 'middle', 'bottom'):
2437 valign = val
2438 if frame_attrs is None:
2439 attrib = {
2440 'style:vertical-pos': 'top',
2441 'style:vertical-rel': 'paragraph',
2442 'style:horizontal-rel': 'paragraph',
2443 'style:mirror': 'none',
2444 'fo:clip': 'rect(0cm 0cm 0cm 0cm)',
2445 'draw:luminance': '0%',
2446 'draw:contrast': '0%',
2447 'draw:red': '0%',
2448 'draw:green': '0%',
2449 'draw:blue': '0%',
2450 'draw:gamma': '100%',
2451 'draw:color-inversion': 'false',
2452 'draw:image-opacity': '100%',
2453 'draw:color-mode': 'standard',
2455 else:
2456 attrib = frame_attrs
2457 if halign is not None:
2458 attrib['style:horizontal-pos'] = halign
2459 if valign is not None:
2460 attrib['style:vertical-pos'] = valign
2461 # If there is a classes/wrap directive or we are
2462 # inside a table, add a no-wrap style.
2463 wrap = False
2464 classes = node.attributes.get('classes')
2465 if classes and 'wrap' in classes:
2466 wrap = True
2467 if wrap:
2468 attrib['style:wrap'] = 'dynamic'
2469 else:
2470 attrib['style:wrap'] = 'none'
2471 # If we are inside a table, add a no-wrap style.
2472 if self.is_in_table(node):
2473 attrib['style:wrap'] = 'none'
2474 SubElement(el1, 'style:graphic-properties',
2475 attrib=attrib, nsdict=SNSD)
2476 draw_name = 'graphics%d' % next(IMAGE_NAME_COUNTER)
2477 # Add the content.
2478 # el = SubElement(current_element, 'text:p',
2479 # attrib={'text:style-name': self.rststyle('textbody')})
2480 attrib = {
2481 'draw:style-name': style_name,
2482 'draw:name': draw_name,
2483 'draw:z-index': '1',
2485 if isinstance(node.parent, nodes.TextElement):
2486 attrib['text:anchor-type'] = 'as-char' # vds
2487 else:
2488 attrib['text:anchor-type'] = 'paragraph'
2489 attrib['svg:width'] = width
2490 attrib['svg:height'] = height
2491 el1 = SubElement(current_element, 'draw:frame', attrib=attrib)
2492 SubElement(el1, 'draw:image', attrib={
2493 'xlink:href': '%s' % (destination, ),
2494 'xlink:type': 'simple',
2495 'xlink:show': 'embed',
2496 'xlink:actuate': 'onLoad',
2498 return el1, width
2500 def is_in_table(self, node) -> bool:
2501 node1 = node.parent
2502 while node1:
2503 if isinstance(node1, docutils.nodes.entry):
2504 return True
2505 node1 = node1.parent
2506 return False
2508 def visit_legend(self, node) -> None:
2509 if isinstance(node.parent, docutils.nodes.figure):
2510 el1 = self.current_element[-1]
2511 el1 = el1[0][0]
2512 self.current_element = el1
2513 self.paragraph_style_stack.append(self.rststyle('legend'))
2515 def depart_legend(self, node) -> None:
2516 if isinstance(node.parent, docutils.nodes.figure):
2517 self.paragraph_style_stack.pop()
2518 self.set_to_parent()
2519 self.set_to_parent()
2520 self.set_to_parent()
2522 def visit_line_block(self, node) -> None:
2523 self.line_indent_level += 1
2524 self.line_block_level += 1
2526 def depart_line_block(self, node) -> None:
2527 self.line_indent_level -= 1
2528 self.line_block_level -= 1
2530 def visit_line(self, node) -> None:
2531 style = 'lineblock%d' % self.line_indent_level
2532 el1 = SubElement(self.current_element, 'text:p',
2533 attrib={'text:style-name': self.rststyle(style), })
2534 self.current_element = el1
2536 def depart_line(self, node) -> None:
2537 self.set_to_parent()
2539 def visit_literal(self, node) -> None:
2540 el = SubElement(
2541 self.current_element, 'text:span',
2542 attrib={'text:style-name': self.rststyle('inlineliteral')})
2543 self.set_current_element(el)
2545 def depart_literal(self, node) -> None:
2546 self.set_to_parent()
2548 def visit_inline(self, node) -> None:
2549 styles = node.attributes.get('classes', ())
2550 if styles:
2551 el = self.current_element
2552 for inline_style in styles:
2553 el = SubElement(el, 'text:span',
2554 attrib={'text:style-name':
2555 self.rststyle(inline_style)})
2556 count = len(styles)
2557 else:
2558 # No style was specified so use a default style (old code
2559 # crashed if no style was given)
2560 el = SubElement(self.current_element, 'text:span')
2561 count = 1
2563 self.set_current_element(el)
2564 self.inline_style_count_stack.append(count)
2566 def depart_inline(self, node) -> None:
2567 count = self.inline_style_count_stack.pop()
2568 for x in range(count):
2569 self.set_to_parent()
2571 def _calculate_code_block_padding(self, line):
2572 count = 0
2573 matchobj = SPACES_PATTERN.match(line)
2574 if matchobj:
2575 pad = matchobj.group()
2576 count = len(pad)
2577 else:
2578 matchobj = TABS_PATTERN.match(line)
2579 if matchobj:
2580 pad = matchobj.group()
2581 count = len(pad) * 8
2582 return count
2584 def _add_syntax_highlighting(self, insource, language):
2585 lexer = pygments.lexers.get_lexer_by_name(language, stripall=True)
2586 if language in ('latex', 'tex'):
2587 fmtr = OdtPygmentsLaTeXFormatter(
2588 lambda name, parameters=():
2589 self.rststyle(name, parameters),
2590 escape_function=escape_cdata)
2591 else:
2592 fmtr = OdtPygmentsProgFormatter(
2593 lambda name, parameters=():
2594 self.rststyle(name, parameters),
2595 escape_function=escape_cdata)
2596 return pygments.highlight(insource, lexer, fmtr)
2598 def fill_line(self, line):
2599 line = FILL_PAT1.sub(self.fill_func1, line)
2600 return FILL_PAT2.sub(self.fill_func2, line)
2602 def fill_func1(self, matchobj) -> str:
2603 spaces = matchobj.group(0)
2604 return '<text:s text:c="%d"/>' % (len(spaces), )
2606 def fill_func2(self, matchobj) -> str:
2607 spaces = matchobj.group(0)
2608 return ' <text:s text:c="%d"/>' % (len(spaces) - 1, )
2610 def visit_literal_block(self, node) -> None:
2611 if len(self.paragraph_style_stack) > 1:
2612 wrapper1 = '<text:p text:style-name="%s">%%s</text:p>' % (
2613 self.rststyle('codeblock-indented'), )
2614 else:
2615 wrapper1 = '<text:p text:style-name="%s">%%s</text:p>' % (
2616 self.rststyle('codeblock'), )
2617 source = node.astext()
2618 if (pygments and self.settings.add_syntax_highlighting):
2619 language = node.get('language', 'python')
2620 source = self._add_syntax_highlighting(source, language)
2621 else:
2622 source = escape_cdata(source)
2623 lines = source.split('\n')
2624 # If there is an empty last line, remove it.
2625 if lines[-1] == '':
2626 del lines[-1]
2627 lines1 = ['<wrappertag1 xmlns:text="urn:oasis:names:tc:'
2628 'opendocument:xmlns:text:1.0">']
2629 my_lines = []
2630 for my_line in lines:
2631 my_line = self.fill_line(my_line)
2632 my_line = my_line.replace("&#10;", "\n")
2633 my_lines.append(my_line)
2634 my_lines_str = '<text:line-break/>'.join(my_lines)
2635 my_lines_str2 = wrapper1 % (my_lines_str, )
2636 lines1.extend((my_lines_str2, '</wrappertag1>'))
2637 s1 = ''.join(lines1)
2638 s1 = s1.encode("utf-8")
2639 el1 = etree.fromstring(s1)
2640 for child in el1:
2641 self.current_element.append(child)
2643 def depart_literal_block(self, node) -> None:
2644 pass
2646 visit_doctest_block = visit_literal_block
2647 depart_doctest_block = depart_literal_block
2649 # placeholder for math (see docs/dev/todo.rst)
2650 def visit_math(self, node) -> None:
2651 self.document.reporter.warning('"math" role not supported',
2652 base_node=node)
2653 self.visit_literal(node)
2655 def depart_math(self, node) -> None:
2656 self.depart_literal(node)
2658 def visit_math_block(self, node) -> None:
2659 self.document.reporter.warning('"math" directive not supported',
2660 base_node=node)
2661 self.visit_literal_block(node)
2663 def depart_math_block(self, node) -> None:
2664 self.depart_literal_block(node)
2666 def visit_meta(self, node) -> None:
2667 name = node.attributes.get('name')
2668 content = node.attributes.get('content')
2669 if name is not None and content is not None:
2670 self.meta_dict[name] = content
2672 def depart_meta(self, node) -> None:
2673 pass
2675 def visit_option_list(self, node) -> None:
2676 table_name = 'tableoption'
2678 # Generate automatic styles
2679 if not self.optiontablestyles_generated:
2680 self.optiontablestyles_generated = True
2681 el = SubElement(self.automatic_styles, 'style:style', attrib={
2682 'style:name': self.rststyle(table_name),
2683 'style:family': 'table'}, nsdict=SNSD)
2684 el1 = SubElement(el, 'style:table-properties', attrib={
2685 'style:width': '17.59cm',
2686 'table:align': 'left',
2687 'style:shadow': 'none'}, nsdict=SNSD)
2688 el = SubElement(self.automatic_styles, 'style:style', attrib={
2689 'style:name': self.rststyle('%s.%%c' % table_name, ('A', )),
2690 'style:family': 'table-column'}, nsdict=SNSD)
2691 el1 = SubElement(el, 'style:table-column-properties', attrib={
2692 'style:column-width': '4.999cm'}, nsdict=SNSD)
2693 el = SubElement(self.automatic_styles, 'style:style', attrib={
2694 'style:name': self.rststyle('%s.%%c' % table_name, ('B', )),
2695 'style:family': 'table-column'}, nsdict=SNSD)
2696 el1 = SubElement(el, 'style:table-column-properties', attrib={
2697 'style:column-width': '12.587cm'}, nsdict=SNSD)
2698 el = SubElement(self.automatic_styles, 'style:style', attrib={
2699 'style:name': self.rststyle(
2700 '%s.%%c%%d' % table_name, ('A', 1, )),
2701 'style:family': 'table-cell'}, nsdict=SNSD)
2702 el1 = SubElement(el, 'style:table-cell-properties', attrib={
2703 'fo:background-color': 'transparent',
2704 'fo:padding': '0.097cm',
2705 'fo:border-left': '0.035cm solid #000000',
2706 'fo:border-right': 'none',
2707 'fo:border-top': '0.035cm solid #000000',
2708 'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD)
2709 el2 = SubElement(el1, 'style:background-image', nsdict=SNSD)
2710 el = SubElement(self.automatic_styles, 'style:style', attrib={
2711 'style:name': self.rststyle(
2712 '%s.%%c%%d' % table_name, ('B', 1, )),
2713 'style:family': 'table-cell'}, nsdict=SNSD)
2714 el1 = SubElement(el, 'style:table-cell-properties', attrib={
2715 'fo:padding': '0.097cm',
2716 'fo:border': '0.035cm solid #000000'}, nsdict=SNSD)
2717 el = SubElement(self.automatic_styles, 'style:style', attrib={
2718 'style:name': self.rststyle(
2719 '%s.%%c%%d' % table_name, ('A', 2, )),
2720 'style:family': 'table-cell'}, nsdict=SNSD)
2721 el1 = SubElement(el, 'style:table-cell-properties', attrib={
2722 'fo:padding': '0.097cm',
2723 'fo:border-left': '0.035cm solid #000000',
2724 'fo:border-right': 'none',
2725 'fo:border-top': 'none',
2726 'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD)
2727 el = SubElement(self.automatic_styles, 'style:style', attrib={
2728 'style:name': self.rststyle(
2729 '%s.%%c%%d' % table_name, ('B', 2, )),
2730 'style:family': 'table-cell'}, nsdict=SNSD)
2731 el1 = SubElement(el, 'style:table-cell-properties', attrib={
2732 'fo:padding': '0.097cm',
2733 'fo:border-left': '0.035cm solid #000000',
2734 'fo:border-right': '0.035cm solid #000000',
2735 'fo:border-top': 'none',
2736 'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD)
2738 # Generate table data
2739 el = self.append_child('table:table', attrib={
2740 'table:name': self.rststyle(table_name),
2741 'table:style-name': self.rststyle(table_name),
2743 el1 = SubElement(el, 'table:table-column', attrib={
2744 'table:style-name': self.rststyle(
2745 '%s.%%c' % table_name, ('A', ))})
2746 el1 = SubElement(el, 'table:table-column', attrib={
2747 'table:style-name': self.rststyle(
2748 '%s.%%c' % table_name, ('B', ))})
2749 el1 = SubElement(el, 'table:table-header-rows')
2750 el2 = SubElement(el1, 'table:table-row')
2751 el3 = SubElement(el2, 'table:table-cell', attrib={
2752 'table:style-name': self.rststyle(
2753 '%s.%%c%%d' % table_name, ('A', 1, )),
2754 'office:value-type': 'string'})
2755 el4 = SubElement(el3, 'text:p', attrib={
2756 'text:style-name': 'Table_20_Heading'})
2757 el4.text = 'Option'
2758 el3 = SubElement(el2, 'table:table-cell', attrib={
2759 'table:style-name': self.rststyle(
2760 '%s.%%c%%d' % table_name, ('B', 1, )),
2761 'office:value-type': 'string'})
2762 el4 = SubElement(el3, 'text:p', attrib={
2763 'text:style-name': 'Table_20_Heading'})
2764 el4.text = 'Description'
2765 self.set_current_element(el)
2767 def depart_option_list(self, node) -> None:
2768 self.set_to_parent()
2770 def visit_option_list_item(self, node) -> None:
2771 el = self.append_child('table:table-row')
2772 self.set_current_element(el)
2774 def depart_option_list_item(self, node) -> None:
2775 self.set_to_parent()
2777 def visit_option_group(self, node) -> None:
2778 el = self.append_child('table:table-cell', attrib={
2779 'table:style-name': 'Table%d.A2' % self.table_count,
2780 'office:value-type': 'string',
2782 self.set_current_element(el)
2784 def depart_option_group(self, node) -> None:
2785 self.set_to_parent()
2787 def visit_option(self, node) -> None:
2788 el = self.append_child('text:p', attrib={
2789 'text:style-name': 'Table_20_Contents'})
2790 el.text = node.astext()
2792 def depart_option(self, node) -> None:
2793 pass
2795 def visit_option_string(self, node) -> None:
2796 pass
2798 def depart_option_string(self, node) -> None:
2799 pass
2801 def visit_option_argument(self, node) -> None:
2802 pass
2804 def depart_option_argument(self, node) -> None:
2805 pass
2807 def visit_description(self, node):
2808 el = self.append_child('table:table-cell', attrib={
2809 'table:style-name': 'Table%d.B2' % self.table_count,
2810 'office:value-type': 'string',
2812 el1 = SubElement(el, 'text:p', attrib={
2813 'text:style-name': 'Table_20_Contents'})
2814 el1.text = node.astext()
2815 raise nodes.SkipChildren
2817 def depart_description(self, node) -> None:
2818 pass
2820 def visit_paragraph(self, node) -> None:
2821 self.in_paragraph = True
2822 if self.in_header:
2823 el = self.append_p('header')
2824 elif self.in_footer:
2825 el = self.append_p('footer')
2826 else:
2827 style_name = self.paragraph_style_stack[-1]
2828 el = self.append_child(
2829 'text:p',
2830 attrib={'text:style-name': style_name})
2831 self.append_pending_ids(el)
2832 self.set_current_element(el)
2834 def depart_paragraph(self, node) -> None:
2835 self.in_paragraph = False
2836 self.set_to_parent()
2837 if self.in_header:
2838 self.header_content.append(self.current_element[-1])
2839 self.current_element.remove(self.current_element[-1])
2840 elif self.in_footer:
2841 self.footer_content.append(self.current_element[-1])
2842 self.current_element.remove(self.current_element[-1])
2844 def visit_problematic(self, node) -> None:
2845 pass
2847 def depart_problematic(self, node) -> None:
2848 pass
2850 def visit_raw(self, node):
2851 if 'format' in node.attributes:
2852 formats = node.attributes['format']
2853 formatlist = formats.split()
2854 if 'odt' in formatlist:
2855 rawstr = node.astext()
2856 attrstr = ' '.join(
2857 '%s="%s"' % (k, v, )
2858 for k, v in list(CONTENT_NAMESPACE_ATTRIB.items()))
2859 contentstr = '<stuff %s>%s</stuff>' % (attrstr, rawstr, )
2860 contentstr = contentstr.encode("utf-8")
2861 content = etree.fromstring(contentstr)
2862 if len(content) > 0:
2863 el1 = content[0]
2864 if self.in_header:
2865 pass
2866 elif self.in_footer:
2867 pass
2868 else:
2869 self.current_element.append(el1)
2870 raise nodes.SkipChildren
2872 def depart_raw(self, node) -> None:
2873 if self.in_header:
2874 pass
2875 elif self.in_footer:
2876 pass
2877 else:
2878 pass
2880 def visit_reference(self, node) -> None:
2881 # text = node.astext()
2882 if self.settings.create_links:
2883 if 'refuri' in node:
2884 href = node['refuri']
2885 if (self.settings.cloak_email_addresses
2886 and href.startswith('mailto:')):
2887 href = self.cloak_mailto(href)
2888 el = self.append_child('text:a', attrib={
2889 'xlink:href': '%s' % href,
2890 'xlink:type': 'simple',
2892 self.set_current_element(el)
2893 elif 'refid' in node:
2894 if self.settings.create_links:
2895 href = node['refid']
2896 el = self.append_child('text:reference-ref', attrib={
2897 'text:ref-name': '%s' % href,
2898 'text:reference-format': 'text',
2900 else:
2901 self.document.reporter.warning(
2902 'References must have "refuri" or "refid" attribute.',
2903 base_node=node)
2904 if (self.in_table_of_contents
2905 and len(node.children) >= 1
2906 and isinstance(node.children[0], docutils.nodes.generated)):
2907 node.remove(node.children[0])
2909 def depart_reference(self, node) -> None:
2910 if self.settings.create_links:
2911 if 'refuri' in node:
2912 self.set_to_parent()
2914 def visit_rubric(self, node) -> None:
2915 style_name = self.rststyle('rubric')
2916 classes = node.get('classes')
2917 if classes:
2918 class1 = classes[0]
2919 if class1:
2920 style_name = class1
2921 el = SubElement(self.current_element, 'text:h', attrib={
2922 # 'text:outline-level': '%d' % section_level,
2923 # 'text:style-name': 'Heading_20_%d' % section_level,
2924 'text:style-name': style_name,
2926 text = node.astext()
2927 el.text = self.encode(text)
2929 def depart_rubric(self, node) -> None:
2930 pass
2932 def visit_section(self, node, move_ids=1) -> None:
2933 self.section_level += 1
2934 self.section_count += 1
2935 if self.settings.create_sections:
2936 el = self.append_child('text:section', attrib={
2937 'text:name': 'Section%d' % self.section_count,
2938 'text:style-name': 'Sect%d' % self.section_level,
2940 self.set_current_element(el)
2942 def depart_section(self, node) -> None:
2943 self.section_level -= 1
2944 if self.settings.create_sections:
2945 self.set_to_parent()
2947 def visit_strong(self, node) -> None:
2948 el = SubElement(self.current_element, 'text:span',
2949 attrib={'text:style-name': self.rststyle('strong')})
2950 self.set_current_element(el)
2952 def depart_strong(self, node) -> None:
2953 self.set_to_parent()
2955 def visit_substitution_definition(self, node):
2956 raise nodes.SkipChildren
2958 def depart_substitution_definition(self, node) -> None:
2959 pass
2961 def visit_system_message(self, node) -> None:
2962 pass
2964 def depart_system_message(self, node) -> None:
2965 pass
2967 def get_table_style(self, node):
2968 table_style = None
2969 table_name = None
2970 str_classes = node.get('classes')
2971 if str_classes is not None:
2972 for str_class in str_classes:
2973 if str_class.startswith(TABLESTYLEPREFIX):
2974 table_name = str_class
2975 break
2976 if table_name is not None:
2977 table_style = self.table_styles.get(table_name)
2978 if table_style is None:
2979 # If we can't find the table style, issue warning
2980 # and use the default table style.
2981 self.document.reporter.warning(
2982 f'Can\'t find table style "{table_name}". Using default.',
2983 base_node=node)
2984 table_name = TABLENAMEDEFAULT
2985 table_style = self.table_styles.get(table_name)
2986 if table_style is None:
2987 # If we can't find the default table style, issue a warning
2988 # and use a built-in default style.
2989 self.document.reporter.warning(
2990 f'Can\'t find default table style "{table_name}". '
2991 'Using built-in default.', base_node=node)
2992 table_style = BUILTIN_DEFAULT_TABLE_STYLE
2993 else:
2994 table_name = TABLENAMEDEFAULT
2995 table_style = self.table_styles.get(table_name)
2996 if table_style is None:
2997 # If we can't find the default table style, issue a warning
2998 # and use a built-in default style.
2999 self.document.reporter.warning(
3000 'Can\'t find default table style "{table_name}". '
3001 'Using built-in default.', base_node=node)
3002 table_style = BUILTIN_DEFAULT_TABLE_STYLE
3003 return table_style
3005 def visit_table(self, node) -> None:
3006 self.table_count += 1
3007 table_style = self.get_table_style(node)
3008 table_name = '%s%%d' % TABLESTYLEPREFIX
3009 el1 = SubElement(self.automatic_styles, 'style:style', attrib={
3010 'style:name': self.rststyle(
3011 '%s' % table_name, (self.table_count, )),
3012 'style:family': 'table',
3013 }, nsdict=SNSD)
3014 if table_style.backgroundcolor is None:
3015 SubElement(el1, 'style:table-properties', attrib={
3016 # 'style:width': '17.59cm',
3017 # 'table:align': 'margins',
3018 'table:align': 'left',
3019 'fo:margin-top': '0in',
3020 'fo:margin-bottom': '0.10in',
3021 }, nsdict=SNSD)
3022 else:
3023 SubElement(el1, 'style:table-properties', attrib={
3024 # 'style:width': '17.59cm',
3025 'table:align': 'margins',
3026 'fo:margin-top': '0in',
3027 'fo:margin-bottom': '0.10in',
3028 'fo:background-color': table_style.backgroundcolor,
3029 }, nsdict=SNSD)
3030 # We use a single cell style for all cells in this table.
3031 # That's probably not correct, but seems to work.
3032 el2 = SubElement(self.automatic_styles, 'style:style', attrib={
3033 'style:name': self.rststyle(
3034 '%s.%%c%%d' % table_name, (self.table_count, 'A', 1, )),
3035 'style:family': 'table-cell',
3036 }, nsdict=SNSD)
3037 thickness = self.settings.table_border_thickness
3038 if thickness is None:
3039 line_style1 = table_style.border
3040 else:
3041 line_style1 = '0.%03dcm solid #000000' % (thickness, )
3042 SubElement(el2, 'style:table-cell-properties', attrib={
3043 'fo:padding': '0.049cm',
3044 'fo:border-left': line_style1,
3045 'fo:border-right': line_style1,
3046 'fo:border-top': line_style1,
3047 'fo:border-bottom': line_style1,
3048 }, nsdict=SNSD)
3049 title = None
3050 for child in node.children:
3051 if child.tagname == 'title':
3052 title = child.astext()
3053 break
3054 if title is not None:
3055 self.append_p('table-title', title)
3056 else:
3057 pass
3058 el4 = SubElement(self.current_element, 'table:table', attrib={
3059 'table:name': self.rststyle(
3060 '%s' % table_name, (self.table_count, )),
3061 'table:style-name': self.rststyle(
3062 '%s' % table_name, (self.table_count, )),
3064 self.set_current_element(el4)
3065 self.current_table_style = el1
3066 self.table_width = 0.0
3068 def depart_table(self, node) -> None:
3069 attribkey = add_ns('style:width', nsdict=SNSD)
3070 attribval = '%.4fin' % (self.table_width, )
3071 el1 = self.current_table_style
3072 el2 = el1[0]
3073 el2.attrib[attribkey] = attribval
3074 self.set_to_parent()
3076 def visit_tgroup(self, node) -> None:
3077 self.column_count = ord('A') - 1
3079 def depart_tgroup(self, node) -> None:
3080 pass
3082 def visit_colspec(self, node) -> None:
3083 self.column_count += 1
3084 colspec_name = self.rststyle(
3085 '%s%%d.%%s' % TABLESTYLEPREFIX,
3086 (self.table_count, chr(self.column_count), )
3088 colwidth = node.propwidth() / 12.0
3089 el1 = SubElement(self.automatic_styles, 'style:style', attrib={
3090 'style:name': colspec_name,
3091 'style:family': 'table-column',
3092 }, nsdict=SNSD)
3093 SubElement(el1, 'style:table-column-properties',
3094 attrib={'style:column-width': '%.4fin' % colwidth},
3095 nsdict=SNSD)
3096 self.append_child('table:table-column',
3097 attrib={'table:style-name': colspec_name, })
3098 self.table_width += colwidth
3100 def depart_colspec(self, node) -> None:
3101 pass
3103 def visit_thead(self, node) -> None:
3104 el = self.append_child('table:table-header-rows')
3105 self.set_current_element(el)
3106 self.in_thead = True
3107 self.paragraph_style_stack.append('Table_20_Heading')
3109 def depart_thead(self, node) -> None:
3110 self.set_to_parent()
3111 self.in_thead = False
3112 self.paragraph_style_stack.pop()
3114 def visit_row(self, node) -> None:
3115 self.column_count = ord('A') - 1
3116 el = self.append_child('table:table-row')
3117 self.set_current_element(el)
3119 def depart_row(self, node) -> None:
3120 self.set_to_parent()
3122 def visit_entry(self, node) -> None:
3123 self.column_count += 1
3124 cellspec_name = self.rststyle(
3125 '%s%%d.%%c%%d' % TABLESTYLEPREFIX,
3126 (self.table_count, 'A', 1, )
3128 attrib = {
3129 'table:style-name': cellspec_name,
3130 'office:value-type': 'string',
3132 morecols = node.get('morecols', 0)
3133 if morecols > 0:
3134 attrib['table:number-columns-spanned'] = '%d' % (morecols + 1,)
3135 self.column_count += morecols
3136 morerows = node.get('morerows', 0)
3137 if morerows > 0:
3138 attrib['table:number-rows-spanned'] = '%d' % (morerows + 1,)
3139 el1 = self.append_child('table:table-cell', attrib=attrib)
3140 self.set_current_element(el1)
3142 def depart_entry(self, node) -> None:
3143 self.set_to_parent()
3145 def visit_tbody(self, node) -> None:
3146 pass
3148 def depart_tbody(self, node) -> None:
3149 pass
3151 def visit_target(self, node) -> None:
3153 # I don't know how to implement targets in ODF.
3154 # How do we create a target in oowriter? A cross-reference?
3155 if ('refuri' not in node
3156 and 'refid' not in node
3157 and 'refname' not in node):
3158 pass
3159 else:
3160 pass
3162 def depart_target(self, node) -> None:
3163 pass
3165 def visit_title(self, node, move_ids=1, title_type='title') -> None:
3166 if isinstance(node.parent, docutils.nodes.section):
3167 section_level = self.section_level
3168 if section_level > 7:
3169 self.document.reporter.warning(
3170 'Heading/section levels greater than 7 not supported.'
3171 ' Reducing to heading level 7 for heading: "%s"' % (
3172 node.astext(), ), base_node=node)
3173 section_level = 7
3174 el1 = self.append_child(
3175 'text:h', attrib={
3176 'text:outline-level': '%d' % section_level,
3177 # 'text:style-name': 'Heading_20_%d' % section_level,
3178 'text:style-name': self.rststyle(
3179 'heading%d', (section_level, )),
3181 self.append_pending_ids(el1)
3182 self.set_current_element(el1)
3183 elif isinstance(node.parent, docutils.nodes.document):
3184 # text = self.settings.title
3185 # else:
3186 # text = node.astext()
3187 el1 = SubElement(self.current_element, 'text:p', attrib={
3188 'text:style-name': self.rststyle(title_type),
3190 self.append_pending_ids(el1)
3191 text = node.astext()
3192 self.title = text
3193 self.found_doc_title = True
3194 self.set_current_element(el1)
3196 def depart_title(self, node) -> None:
3197 if (isinstance(node.parent, docutils.nodes.section)
3198 or isinstance(node.parent, docutils.nodes.document)):
3199 self.set_to_parent()
3201 def visit_subtitle(self, node, move_ids=1) -> None:
3202 self.visit_title(node, move_ids, title_type='subtitle')
3204 def depart_subtitle(self, node) -> None:
3205 self.depart_title(node)
3207 def visit_title_reference(self, node):
3208 el = self.append_child('text:span', attrib={
3209 'text:style-name': self.rststyle('quotation')})
3210 el.text = self.encode(node.astext())
3211 raise nodes.SkipChildren
3213 def depart_title_reference(self, node) -> None:
3214 pass
3216 def generate_table_of_content_entry_template(self, el1) -> None:
3217 for idx in range(1, 11):
3218 el2 = SubElement(
3219 el1,
3220 'text:table-of-content-entry-template',
3221 attrib={
3222 'text:outline-level': "%d" % (idx, ),
3223 'text:style-name': self.rststyle('contents-%d' % (idx, )),
3225 SubElement(el2, 'text:index-entry-chapter')
3226 SubElement(el2, 'text:index-entry-text')
3227 SubElement(el2, 'text:index-entry-tab-stop', attrib={
3228 'style:leader-char': ".",
3229 'style:type': "right",
3231 SubElement(el2, 'text:index-entry-page-number')
3233 def find_title_label(self, node, class_type, label_key):
3234 label = ''
3235 title_node = None
3236 for child in node.children:
3237 if isinstance(child, class_type):
3238 title_node = child
3239 break
3240 if title_node is not None:
3241 label = title_node.astext()
3242 else:
3243 label = self.language.labels[label_key]
3244 return label
3246 def visit_topic(self, node) -> None:
3247 if 'classes' in node.attributes:
3248 if 'contents' in node.attributes['classes']:
3249 label = self.find_title_label(
3250 node, docutils.nodes.title, 'contents')
3251 if self.settings.generate_oowriter_toc:
3252 el1 = self.append_child('text:table-of-content', attrib={
3253 'text:name': 'Table of Contents1',
3254 'text:protected': 'true',
3255 'text:style-name': 'Sect1',
3257 el2 = SubElement(
3258 el1,
3259 'text:table-of-content-source',
3260 attrib={
3261 'text:outline-level': '10',
3263 el3 = SubElement(el2, 'text:index-title-template', attrib={
3264 'text:style-name': 'Contents_20_Heading',
3266 el3.text = label
3267 self.generate_table_of_content_entry_template(el2)
3268 el4 = SubElement(el1, 'text:index-body')
3269 el5 = SubElement(el4, 'text:index-title')
3270 el6 = SubElement(el5, 'text:p', attrib={
3271 'text:style-name': self.rststyle('contents-heading'),
3273 el6.text = label
3274 self.save_current_element = self.current_element
3275 self.table_of_content_index_body = el4
3276 self.set_current_element(el4)
3277 else:
3278 el = self.append_p('horizontalline')
3279 el = self.append_p('centeredtextbody')
3280 el1 = SubElement(
3281 el, 'text:span',
3282 attrib={'text:style-name': self.rststyle('strong')})
3283 el1.text = label
3284 self.in_table_of_contents = True
3285 elif 'abstract' in node.attributes['classes']:
3286 el = self.append_p('horizontalline')
3287 el = self.append_p('centeredtextbody')
3288 el1 = SubElement(
3289 el, 'text:span',
3290 attrib={'text:style-name': self.rststyle('strong')})
3291 label = self.find_title_label(
3292 node, docutils.nodes.title,
3293 'abstract')
3294 el1.text = label
3295 elif 'dedication' in node.attributes['classes']:
3296 el = self.append_p('horizontalline')
3297 el = self.append_p('centeredtextbody')
3298 el1 = SubElement(
3299 el, 'text:span',
3300 attrib={'text:style-name': self.rststyle('strong')})
3301 label = self.find_title_label(
3302 node, docutils.nodes.title,
3303 'dedication')
3304 el1.text = label
3306 def depart_topic(self, node) -> None:
3307 if 'classes' in node.attributes:
3308 if 'contents' in node.attributes['classes']:
3309 if self.settings.generate_oowriter_toc:
3310 self.update_toc_page_numbers(
3311 self.table_of_content_index_body)
3312 self.set_current_element(self.save_current_element)
3313 else:
3314 self.append_p('horizontalline')
3315 self.in_table_of_contents = False
3317 def update_toc_page_numbers(self, el) -> None:
3318 collection = []
3319 self.update_toc_collect(el, 0, collection)
3320 self.update_toc_add_numbers(collection)
3322 def update_toc_collect(self, el, level, collection) -> None:
3323 collection.append((level, el))
3324 level += 1
3325 for child_el in el:
3326 if child_el.tag != 'text:index-body':
3327 self.update_toc_collect(child_el, level, collection)
3329 def update_toc_add_numbers(self, collection) -> None:
3330 for level, el1 in collection:
3331 if (el1.tag == 'text:p'
3332 and el1.text != 'Table of Contents'):
3333 el2 = SubElement(el1, 'text:tab')
3334 el2.tail = '9999'
3336 def visit_transition(self, node) -> None:
3337 self.append_p('horizontalline')
3339 def depart_transition(self, node) -> None:
3340 pass
3343 # Admonitions
3345 def visit_warning(self, node) -> None:
3346 self.generate_admonition(node, 'warning')
3348 def depart_warning(self, node) -> None:
3349 self.paragraph_style_stack.pop()
3351 def visit_attention(self, node) -> None:
3352 self.generate_admonition(node, 'attention')
3354 depart_attention = depart_warning
3356 def visit_caution(self, node) -> None:
3357 self.generate_admonition(node, 'caution')
3359 depart_caution = depart_warning
3361 def visit_danger(self, node) -> None:
3362 self.generate_admonition(node, 'danger')
3364 depart_danger = depart_warning
3366 def visit_error(self, node) -> None:
3367 self.generate_admonition(node, 'error')
3369 depart_error = depart_warning
3371 def visit_hint(self, node) -> None:
3372 self.generate_admonition(node, 'hint')
3374 depart_hint = depart_warning
3376 def visit_important(self, node) -> None:
3377 self.generate_admonition(node, 'important')
3379 depart_important = depart_warning
3381 def visit_note(self, node) -> None:
3382 self.generate_admonition(node, 'note')
3384 depart_note = depart_warning
3386 def visit_tip(self, node) -> None:
3387 self.generate_admonition(node, 'tip')
3389 depart_tip = depart_warning
3391 def visit_admonition(self, node) -> None:
3392 title = None
3393 for child in node.children:
3394 if child.tagname == 'title':
3395 title = child.astext()
3396 if title is None:
3397 classes1 = node.get('classes')
3398 if classes1:
3399 title = classes1[0]
3400 self.generate_admonition(node, 'generic', title)
3402 depart_admonition = depart_warning
3404 def generate_admonition(self, node, label, title=None) -> None:
3405 if hasattr(self.language, 'labels'):
3406 translated_label = self.language.labels.get(label, label)
3407 else:
3408 translated_label = label
3409 el1 = SubElement(self.current_element, 'text:p', attrib={
3410 'text:style-name': self.rststyle(
3411 'admon-%s-hdr', (label, )),
3413 if title:
3414 el1.text = title
3415 else:
3416 el1.text = '%s!' % (translated_label.capitalize(), )
3417 s1 = self.rststyle('admon-%s-body', (label, ))
3418 self.paragraph_style_stack.append(s1)
3421 # Roles (e.g. subscript, superscript, strong, ...
3423 def visit_subscript(self, node) -> None:
3424 el = self.append_child('text:span', attrib={
3425 'text:style-name': 'rststyle-subscript',
3427 self.set_current_element(el)
3429 def depart_subscript(self, node) -> None:
3430 self.set_to_parent()
3432 def visit_superscript(self, node) -> None:
3433 el = self.append_child('text:span', attrib={
3434 'text:style-name': 'rststyle-superscript',
3436 self.set_current_element(el)
3438 def depart_superscript(self, node) -> None:
3439 self.set_to_parent()
3441 def visit_abbreviation(self, node) -> None:
3442 pass
3444 def depart_abbreviation(self, node) -> None:
3445 pass
3447 def visit_acronym(self, node) -> None:
3448 pass
3450 def depart_acronym(self, node) -> None:
3451 pass
3453 def visit_sidebar(self, node) -> None:
3454 pass
3456 def depart_sidebar(self, node) -> None:
3457 pass
3460 # Use an own reader to modify transformations done.
3461 class Reader(standalone.Reader):
3463 def get_transforms(self):
3464 transforms = super().get_transforms()
3465 if not self.settings.create_links:
3466 transforms.remove(references.DanglingReferences)
3467 return transforms