2 # Author: Dave Kuhlman <dkuhlman@davekuhlman.org>
3 # Copyright: This module has been placed in the public domain.
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.
12 __docformat__
= 'reStructuredText'
15 from configparser
import ConfigParser
17 from io
import StringIO
22 from pathlib
import Path
29 from xml
.etree
import ElementTree
as etree
30 from xml
.dom
import minidom
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.
42 import pygments
.lexers
43 from .pygmentsformatter
import (OdtPygmentsProgFormatter
,
44 OdtPygmentsLaTeXFormatter
)
45 except (ImportError, SyntaxError):
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.')
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
71 _ElementInterface
= etree
._ElementInterface
74 class _ElementInterfaceWrapper(_ElementInterface
):
75 def __init__(self
, tag
, attrib
=None):
76 _ElementInterface
.__init
__(self
, tag
, attrib
)
79 def setparent(self
, parent
):
80 _parents
[self
] = parent
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',
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
):
255 tag
, attrib
= fix_ns(tag
, attrib
, nsdict
)
256 return _ElementInterfaceWrapper(tag
, attrib
)
259 def SubElement(parent
, tag
, attrib
=None, nsmap
=None, nsdict
=CNSD
):
262 tag
, attrib
= fix_ns(tag
, attrib
, nsdict
)
263 el
= _ElementInterfaceWrapper(tag
, attrib
)
269 def fix_ns(tag
, attrib
, nsdict
):
270 nstag
= add_ns(tag
, nsdict
)
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
):
283 outstream
= StringIO()
284 et
.write(outstream
, encoding
="unicode")
285 s1
= outstream
.getvalue()
290 def escape_cdata(text
):
291 text
= text
.replace("&", "&")
292 text
= text
.replace("<", "<")
293 text
= text
.replace(">", ">")
296 if ord(char
) >= ord("\x7f"):
297 ascii
+= "&#x%X;" % (ord(char
), )
309 def __init__(self
, border
=None, backgroundcolor
=None):
311 self
.backgroundcolor
= backgroundcolor
313 def get_border_(self
):
316 def set_border_(self
, border
):
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
):
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.
337 def __init__(self
, level
, sibling_level
=True, nested_level
=True):
339 self
.sibling_level
= sibling_level
340 self
.nested_level
= nested_level
342 def set_sibling(self
, sibling_level
):
343 self
.sibling_level
= sibling_level
345 def get_sibling(self
):
346 return self
.sibling_level
348 def set_nested(self
, nested_level
):
349 self
.nested_level
= nested_level
351 def get_nested(self
):
352 return self
.nested_level
354 def set_level(self
, level
):
361 class Writer(writers
.Writer
):
363 MIME_TYPE
= 'application/vnd.oasis.opendocument.text'
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
))
382 'ODF-Specific Options.',
385 ('Specify a stylesheet. '
386 'Default: "%s"' % default_stylesheet_path
,
389 'default': default_stylesheet_path
,
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'],
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'],
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. '
410 ['--table-border-thickness'],
413 'validator': frontend
.validate_nonnegative_int
}),
414 ('Add syntax highlighting in literal code blocks.',
415 ['--add-syntax-highlighting'],
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'],
424 'action': 'store_false',
425 'dest': 'add_syntax_highlighting',
426 'validator': frontend
.validate_boolean
}),
427 ('Create sections for headers. (default)',
428 ['--create-sections'],
430 'action': 'store_true',
431 'dest': 'create_sections',
432 'validator': frontend
.validate_boolean
}),
433 ('Do not create sections for headers.',
436 'action': 'store_false',
437 'dest': 'create_sections',
438 'validator': frontend
.validate_boolean
}),
442 'action': 'store_true',
443 'dest': 'create_links',
444 'validator': frontend
.validate_boolean
}),
445 ('Do not create links. (default)',
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'],
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'],
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'],
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'],
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'],
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',)
504 writers
.Writer
.__init
__(self
)
505 self
.translator_class
= ODFTranslator
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
):
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
)
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.
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()
548 elif len(subtag
) == 1:
549 break # 1-letter tag is never before valid region tag
550 if region_code
is None:
552 rcode
= locale
.normalize(language_code
)
554 rcode
= language_code
555 rcode
= rcode
.split('_')
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
)
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
)
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."""
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}'
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}'
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}'
607 if family
== 'paragraph' or family
== 'graphic':
609 '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
613 "Cannot find 'text-properties' "
614 "element in styles.xml")
615 if language_code
is not None:
617 '{urn:oasis:names:tc:opendocument:xmlns:'
618 'xsl-fo-compatible:1.0}language'] = language_code
620 '{urn:oasis:names:tc:opendocument:xmlns:'
621 'style:1.0}language-complex'] = language_code
623 modified_nodes
.add(n3
)
624 if region_code
is not None:
626 '{urn:oasis:names:tc:opendocument:xmlns:'
627 'xsl-fo-compatible:1.0}country'] = region_code
629 '{urn:oasis:names:tc:opendocument:xmlns:'
630 'style:1.0}country-complex'] = region_code
632 modified_nodes
.add(n3
)
633 return updated
, stylesheet_root
, modified_nodes
636 self
, zfile
, name
, bytes
, compress_type
=zipfile
.ZIP_DEFLATED
):
637 localtime
= time
.localtime(time
.time())
638 zinfo
= zipfile
.ZipInfo(name
, localtime
)
639 # Add some standard UNIX file access permissions (-rw-r--r--).
640 zinfo
.external_attr
= (0x81a4 & 0xFFFF) << 16
641 zinfo
.compress_type
= compress_type
642 zfile
.writestr(zinfo
, bytes
)
644 def store_embedded_files(self
, zfile
):
645 embedded_files
= self
.visitor
.get_embedded_file_list()
646 for source
, destination
in embedded_files
:
650 zfile
.write(source
, destination
)
652 self
.document
.reporter
.warning(
653 "Can't open file %s." % (source
, ))
655 def get_settings(self
):
657 modeled after get_stylesheet
659 stylespath
= self
.settings
.stylesheet
660 zfile
= zipfile
.ZipFile(stylespath
, 'r')
661 s1
= zfile
.read('settings.xml')
665 def get_stylesheet(self
):
666 """Get the stylesheet from the visitor.
667 Ask the visitor to setup the page.
669 return self
.visitor
.setup_page()
671 def copy_from_stylesheet(self
, outzipfile
):
672 """Copy images, settings, etc from the stylesheet doc into target doc.
674 stylespath
= self
.settings
.stylesheet
675 inzipfile
= zipfile
.ZipFile(stylespath
, 'r')
677 s1
= inzipfile
.read('settings.xml')
678 self
.write_zip_str(outzipfile
, 'settings.xml', s1
)
680 namelist
= inzipfile
.namelist()
681 for name
in namelist
:
682 if name
.startswith('Pictures/'):
683 imageobj
= inzipfile
.read(name
)
684 outzipfile
.writestr(name
, imageobj
)
687 def assemble_parts(self
):
690 def create_manifest(self
):
693 attrib
=MANIFEST_NAMESPACE_ATTRIB
,
694 nsdict
=MANIFEST_NAMESPACE_DICT
,
696 doc
= etree
.ElementTree(root
)
697 SubElement(root
, 'manifest:file-entry', attrib
={
698 'manifest:media-type': self
.MIME_TYPE
,
699 'manifest:full-path': '/',
701 SubElement(root
, 'manifest:file-entry', attrib
={
702 'manifest:media-type': 'text/xml',
703 'manifest:full-path': 'content.xml',
705 SubElement(root
, 'manifest:file-entry', attrib
={
706 'manifest:media-type': 'text/xml',
707 'manifest:full-path': 'styles.xml',
709 SubElement(root
, 'manifest:file-entry', attrib
={
710 'manifest:media-type': 'text/xml',
711 'manifest:full-path': 'settings.xml',
713 SubElement(root
, 'manifest:file-entry', attrib
={
714 'manifest:media-type': 'text/xml',
715 'manifest:full-path': 'meta.xml',
718 doc
= minidom
.parseString(s1
)
719 return doc
.toprettyxml(' ')
721 def create_meta(self
):
723 'office:document-meta',
724 attrib
=META_NAMESPACE_ATTRIB
,
725 nsdict
=META_NAMESPACE_DICT
,
727 doc
= etree
.ElementTree(root
)
728 root
= SubElement(root
, 'office:meta', nsdict
=METNSD
)
729 el1
= SubElement(root
, 'meta:generator', nsdict
=METNSD
)
730 el1
.text
= 'Docutils/rst2odf.py/%s' % (VERSION
, )
731 s1
= os
.environ
.get('USER', '')
732 el1
= SubElement(root
, 'meta:initial-creator', nsdict
=METNSD
)
734 s2
= time
.strftime('%Y-%m-%dT%H:%M:%S', time
.localtime())
735 el1
= SubElement(root
, 'meta:creation-date', nsdict
=METNSD
)
737 el1
= SubElement(root
, 'dc:creator', nsdict
=METNSD
)
739 el1
= SubElement(root
, 'dc:date', nsdict
=METNSD
)
741 el1
= SubElement(root
, 'dc:language', nsdict
=METNSD
)
743 el1
= SubElement(root
, 'meta:editing-cycles', nsdict
=METNSD
)
745 el1
= SubElement(root
, 'meta:editing-duration', nsdict
=METNSD
)
746 el1
.text
= 'PT00M01S'
747 title
= self
.visitor
.get_title()
748 el1
= SubElement(root
, 'dc:title', nsdict
=METNSD
)
752 el1
.text
= '[no title]'
753 for prop
, value
in self
.visitor
.get_meta_dict().items():
754 # 'keywords', 'description', and 'subject' have their own fields:
755 if prop
== 'keywords':
756 keywords
= re
.split(', *', value
)
757 for keyword
in keywords
:
758 el1
= SubElement(root
, 'meta:keyword', nsdict
=METNSD
)
760 elif prop
== 'description':
761 el1
= SubElement(root
, 'dc:description', nsdict
=METNSD
)
763 elif prop
== 'subject':
764 el1
= SubElement(root
, 'dc:subject', nsdict
=METNSD
)
766 else: # Store remaining properties as custom/user-defined
767 el1
= SubElement(root
, 'meta:user-defined',
768 attrib
={'meta:name': prop
}, nsdict
=METNSD
)
771 # doc = minidom.parseString(s1)
772 # s1 = doc.toprettyxml(' ')
776 # class ODFTranslator(nodes.SparseNodeVisitor):
777 class ODFTranslator(nodes
.GenericNodeVisitor
):
780 'attribution', 'blockindent', 'blockquote', 'blockquote-bulletitem',
781 'blockquote-bulletlist', 'blockquote-enumitem', 'blockquote-enumlist',
782 'bulletitem', 'bulletlist',
784 'centeredtextbody', 'codeblock', 'codeblock-indented',
785 'codeblock-classname', 'codeblock-comment', 'codeblock-functionname',
786 'codeblock-keyword', 'codeblock-name', 'codeblock-number',
787 'codeblock-operator', 'codeblock-string', 'emphasis', 'enumitem',
788 'enumlist', 'epigraph', 'epigraph-bulletitem', 'epigraph-bulletlist',
789 'epigraph-enumitem', 'epigraph-enumlist', 'footer',
790 'footnote', 'citation',
791 'header', 'highlights', 'highlights-bulletitem',
792 'highlights-bulletlist', 'highlights-enumitem', 'highlights-enumlist',
793 'horizontalline', 'inlineliteral', 'quotation', 'rubric',
794 'strong', 'table-title', 'textbody', 'tocbulletlist', 'tocenumlist',
804 'admon-attention-hdr',
805 'admon-attention-body',
807 'admon-caution-body',
813 'admon-generic-body',
816 'admon-important-hdr',
817 'admon-important-body',
823 'admon-warning-body',
825 'tableoption.%c', 'tableoption.%c%d', 'Table%d', 'Table%d.%c',
833 'image', 'figureframe',
836 def __init__(self
, document
):
837 # nodes.SparseNodeVisitor.__init__(self, document)
838 nodes
.GenericNodeVisitor
.__init
__(self
, document
)
839 self
.settings
= document
.settings
840 self
.language_code
= self
.settings
.language_code
841 self
.language
= languages
.get_language(
845 if self
.settings
.odf_config_file
:
846 parser
= ConfigParser()
847 parser
.read(self
.settings
.odf_config_file
)
848 for rststyle
, format
in parser
.items("Formats"):
849 if rststyle
not in self
.used_styles
:
850 self
.document
.reporter
.warning(
851 'Style "%s" is not a style used by odtwriter.' % (
853 self
.format_map
[rststyle
] = format
854 self
.section_level
= 0
855 self
.section_count
= 0
856 # Create ElementTree content and styles documents.
858 'office:document-content',
859 attrib
=CONTENT_NAMESPACE_ATTRIB
,
861 self
.content_tree
= etree
.ElementTree(element
=root
)
862 self
.current_element
= root
863 SubElement(root
, 'office:scripts')
864 SubElement(root
, 'office:font-face-decls')
865 el
= SubElement(root
, 'office:automatic-styles')
866 self
.automatic_styles
= el
867 el
= SubElement(root
, 'office:body')
868 el
= self
.generate_content_element(el
)
869 self
.current_element
= el
870 self
.body_text_element
= el
871 self
.paragraph_style_stack
= [self
.rststyle('textbody'), ]
872 self
.list_style_stack
= []
874 self
.column_count
= ord('A') - 1
875 self
.trace_level
= -1
876 self
.optiontablestyles_generated
= False
877 self
.field_name
= None
878 self
.field_element
= None
881 self
.image_style_count
= 0
883 self
.embedded_file_list
= []
884 self
.syntaxhighlighting
= 1
885 self
.syntaxhighlight_lexer
= 'python'
886 self
.header_content
= []
887 self
.footer_content
= []
888 self
.in_header
= False
889 self
.in_footer
= False
891 self
.in_table_of_contents
= False
892 self
.table_of_content_index_body
= None
894 self
.def_list_level
= 0
895 self
.footnote_ref_dict
= {}
896 self
.footnote_list
= []
897 self
.footnote_chars_idx
= 0
898 self
.footnote_level
= 0
899 self
.pending_ids
= []
900 self
.in_paragraph
= False
901 self
.found_doc_title
= False
902 self
.bumped_list_level_stack
= []
904 self
.line_block_level
= 0
905 self
.line_indent_level
= 0
906 self
.citation_id
= None
907 self
.style_index
= 0 # use to form unique style names
908 self
.str_stylesheet
= ''
909 self
.str_stylesheetcontent
= ''
910 self
.dom_stylesheet
= None
911 self
.table_styles
= None
912 self
.in_citation
= False
914 # Keep track of nested styling classes
915 self
.inline_style_count_stack
= []
917 def get_str_stylesheet(self
):
918 return self
.str_stylesheet
920 def retrieve_styles(self
, extension
):
921 """Retrieve the stylesheet from either a .xml file or from
922 a .odt (zip) file. Return the content as a string.
925 stylespath
= self
.settings
.stylesheet
926 ext
= os
.path
.splitext(stylespath
)[1]
928 with
open(stylespath
, 'r', encoding
='utf-8') as stylesfile
:
929 s1
= stylesfile
.read()
930 elif ext
== extension
:
931 zfile
= zipfile
.ZipFile(stylespath
, 'r')
932 s1
= zfile
.read('styles.xml')
933 s2
= zfile
.read('content.xml')
936 raise RuntimeError('stylesheet path (%s) must be %s or '
937 '.xml file' % (stylespath
, extension
))
938 self
.str_stylesheet
= s1
939 self
.str_stylesheetcontent
= s2
940 self
.dom_stylesheet
= etree
.fromstring(self
.str_stylesheet
)
941 self
.dom_stylesheetcontent
= etree
.fromstring(
942 self
.str_stylesheetcontent
)
943 self
.table_styles
= self
.extract_table_styles(s2
)
945 def extract_table_styles(self
, styles_str
):
946 root
= etree
.fromstring(styles_str
)
948 auto_styles
= root
.find(
949 '{%s}automatic-styles' % (CNSD
['office'], ))
950 for stylenode
in auto_styles
:
951 name
= stylenode
.get('{%s}name' % (CNSD
['style'], ))
952 tablename
= name
.split('.')[0]
953 family
= stylenode
.get('{%s}family' % (CNSD
['style'], ))
954 if name
.startswith(TABLESTYLEPREFIX
):
955 tablestyle
= table_styles
.get(tablename
)
956 if tablestyle
is None:
957 tablestyle
= TableStyle()
958 table_styles
[tablename
] = tablestyle
959 if family
== 'table':
960 properties
= stylenode
.find(
961 '{%s}table-properties' % (CNSD
['style'], ))
962 property = properties
.get(
963 '{%s}%s' % (CNSD
['fo'], 'background-color', ))
964 if property is not None and property != 'none':
965 tablestyle
.backgroundcolor
= property
966 elif family
== 'table-cell':
967 properties
= stylenode
.find(
968 '{%s}table-cell-properties' % (CNSD
['style'], ))
969 if properties
is not None:
970 border
= self
.get_property(properties
)
971 if border
is not None:
972 tablestyle
.border
= border
975 def get_property(self
, stylenode
):
977 for propertyname
in TABLEPROPERTYNAMES
:
978 border
= stylenode
.get('{%s}%s' % (CNSD
['fo'], propertyname
, ))
979 if border
is not None and border
!= 'none':
983 def add_doc_title(self
):
984 text
= self
.settings
.title
987 if not self
.found_doc_title
:
988 el
= Element('text:p', attrib
={
989 'text:style-name': self
.rststyle('title'),
992 self
.body_text_element
.insert(0, el
)
993 el
= self
.find_first_text_p(self
.body_text_element
)
995 self
.attach_page_style(el
)
997 def find_first_text_p(self
, el
):
998 """Search the generated doc and return the first <text:p> element.
1000 if el
.tag
== 'text:p' or el
.tag
== 'text:h':
1004 el1
= self
.find_first_text_p(child
)
1009 def attach_page_style(self
, el
):
1010 """Attach the default page style.
1012 Create an automatic-style that refers to the current style
1013 of this element and that refers to the default page style.
1015 current_style
= el
.get('text:style-name')
1016 style_name
= 'P1003'
1018 self
.automatic_styles
, 'style:style', attrib
={
1019 'style:name': style_name
,
1020 'style:master-page-name': "rststyle-pagedefault",
1021 'style:family': "paragraph",
1024 el1
.set('style:parent-style-name', current_style
)
1025 el
.set('text:style-name', style_name
)
1027 def rststyle(self
, name
, parameters
=()):
1029 Returns the style name to use for the given style.
1031 If `parameters` is given `name` must contain a matching number of
1032 ``%`` and is used as a format expression with `parameters` as
1035 name1
= name
% parameters
1036 return self
.format_map
.get(name1
, 'rststyle-%s' % name1
)
1038 def generate_content_element(self
, root
):
1039 return SubElement(root
, 'office:text')
1041 def setup_page(self
):
1042 self
.setup_paper(self
.dom_stylesheet
)
1043 if (len(self
.header_content
) > 0
1044 or len(self
.footer_content
) > 0
1045 or self
.settings
.custom_header
1046 or self
.settings
.custom_footer
):
1047 self
.add_header_footer(self
.dom_stylesheet
)
1048 return etree
.tostring(self
.dom_stylesheet
)
1050 def get_dom_stylesheet(self
):
1051 return self
.dom_stylesheet
1053 def setup_paper(self
, root_el
):
1054 # TODO: only call paperconf, if it is actually used
1055 # (i.e. page size removed from "styles.odt" with rst2odt_prepstyles.py
1056 # cf. conditional in walk() below)?
1058 dimensions
= subprocess
.check_output(('paperconf', '-s'),
1059 stderr
=subprocess
.STDOUT
)
1060 w
, h
= (float(s
) for s
in dimensions
.split())
1061 except (subprocess
.CalledProcessError
, FileNotFoundError
, ValueError):
1062 self
.document
.reporter
.info(
1063 'Cannot use `paperconf`, defaulting to Letter.')
1064 w
, h
= 612, 792 # default to Letter
1067 if el
.tag
== "{%s}page-layout-properties" % SNSD
["style"] and \
1068 "{%s}page-width" % SNSD
["fo"] not in el
.attrib
:
1069 el
.attrib
["{%s}page-width" % SNSD
["fo"]] = "%.3fpt" % w
1070 el
.attrib
["{%s}page-height" % SNSD
["fo"]] = "%.3fpt" % h
1071 el
.attrib
["{%s}margin-left" % SNSD
["fo"]] = \
1072 el
.attrib
["{%s}margin-right" % SNSD
["fo"]] = \
1074 el
.attrib
["{%s}margin-top" % SNSD
["fo"]] = \
1075 el
.attrib
["{%s}margin-bottom" % SNSD
["fo"]] = \
1082 def add_header_footer(self
, root_el
):
1083 automatic_styles
= root_el
.find(
1084 '{%s}automatic-styles' % SNSD
['office'])
1085 path
= '{%s}master-styles' % (NAME_SPACE_1
, )
1086 master_el
= root_el
.find(path
)
1087 if master_el
is None:
1089 path
= '{%s}master-page' % (SNSD
['style'], )
1090 master_el_container
= master_el
.findall(path
)
1092 target_attrib
= '{%s}name' % (SNSD
['style'], )
1093 target_name
= self
.rststyle('pagedefault')
1094 for el
in master_el_container
:
1095 if el
.get(target_attrib
) == target_name
:
1098 if master_el
is None:
1101 if self
.header_content
or self
.settings
.custom_header
:
1103 el1
, 'style:header',
1104 attrib
=STYLES_NAMESPACE_ATTRIB
,
1105 nsdict
=STYLES_NAMESPACE_DICT
,
1107 for el
in self
.header_content
:
1108 attrkey
= add_ns('text:style-name', nsdict
=SNSD
)
1109 el
.attrib
[attrkey
] = self
.rststyle('header')
1111 if self
.settings
.custom_header
:
1112 self
.create_custom_headfoot(
1114 self
.settings
.custom_header
, 'header', automatic_styles
)
1115 if self
.footer_content
or self
.settings
.custom_footer
:
1117 el1
, 'style:footer',
1118 attrib
=STYLES_NAMESPACE_ATTRIB
,
1119 nsdict
=STYLES_NAMESPACE_DICT
,
1121 for el
in self
.footer_content
:
1122 attrkey
= add_ns('text:style-name', nsdict
=SNSD
)
1123 el
.attrib
[attrkey
] = self
.rststyle('footer')
1125 if self
.settings
.custom_footer
:
1126 self
.create_custom_headfoot(
1128 self
.settings
.custom_footer
, 'footer', automatic_styles
)
1130 code_none
, code_field
, code_text
= list(range(3))
1131 field_pat
= re
.compile(r
'%(..?)%')
1133 def create_custom_headfoot(
1134 self
, parent
, text
, style_name
, automatic_styles
):
1135 parent
= SubElement(parent
, 'text:p', attrib
={
1136 'text:style-name': self
.rststyle(style_name
),
1138 current_element
= None
1139 field_iter
= self
.split_field_specifiers_iter(text
)
1140 for item
in field_iter
:
1141 if item
[0] == ODFTranslator
.code_field
:
1144 't1', 't2', 't3', 't4',
1145 'd1', 'd2', 'd3', 'd4', 'd5',
1147 msg
= 'bad field spec: %%%s%%' % (item
[1], )
1148 raise RuntimeError(msg
)
1149 el1
= self
.make_field_element(
1151 item
[1], style_name
, automatic_styles
)
1153 msg
= 'bad field spec: %%%s%%' % (item
[1], )
1154 raise RuntimeError(msg
)
1156 current_element
= el1
1158 if current_element
is None:
1159 parent
.text
= item
[1]
1161 current_element
.tail
= item
[1]
1163 def make_field_element(self
, parent
, text
, style_name
, automatic_styles
):
1165 el1
= SubElement(parent
, 'text:page-number', attrib
={
1166 # 'text:style-name': self.rststyle(style_name),
1167 'text:select-page': 'current',
1170 el1
= SubElement(parent
, 'text:page-count', attrib
={
1171 # 'text:style-name': self.rststyle(style_name),
1174 self
.style_index
+= 1
1175 el1
= SubElement(parent
, 'text:time', attrib
={
1176 'text:style-name': self
.rststyle(style_name
),
1177 'text:fixed': 'true',
1178 'style:data-style-name':
1179 'rst-time-style-%d' % self
.style_index
,
1181 el2
= SubElement(automatic_styles
, 'number:time-style', attrib
={
1182 'style:name': 'rst-time-style-%d' % self
.style_index
,
1183 'xmlns:number': SNSD
['number'],
1184 'xmlns:style': SNSD
['style'],
1186 el3
= SubElement(el2
, 'number:hours', attrib
={
1187 'number:style': 'long',
1189 el3
= SubElement(el2
, 'number:text')
1191 el3
= SubElement(el2
, 'number:minutes', attrib
={
1192 'number:style': 'long',
1195 self
.style_index
+= 1
1196 el1
= SubElement(parent
, 'text:time', attrib
={
1197 'text:style-name': self
.rststyle(style_name
),
1198 'text:fixed': 'true',
1199 'style:data-style-name':
1200 'rst-time-style-%d' % self
.style_index
,
1202 el2
= SubElement(automatic_styles
, 'number:time-style', attrib
={
1203 'style:name': 'rst-time-style-%d' % self
.style_index
,
1204 'xmlns:number': SNSD
['number'],
1205 'xmlns:style': SNSD
['style'],
1207 el3
= SubElement(el2
, 'number:hours', attrib
={
1208 'number:style': 'long',
1210 el3
= SubElement(el2
, 'number:text')
1212 el3
= SubElement(el2
, 'number:minutes', attrib
={
1213 'number:style': 'long',
1215 el3
= SubElement(el2
, 'number:text')
1217 el3
= SubElement(el2
, 'number:seconds', attrib
={
1218 'number:style': 'long',
1221 self
.style_index
+= 1
1222 el1
= SubElement(parent
, 'text:time', attrib
={
1223 'text:style-name': self
.rststyle(style_name
),
1224 'text:fixed': 'true',
1225 'style:data-style-name':
1226 'rst-time-style-%d' % self
.style_index
,
1228 el2
= SubElement(automatic_styles
, 'number:time-style', attrib
={
1229 'style:name': 'rst-time-style-%d' % self
.style_index
,
1230 'xmlns:number': SNSD
['number'],
1231 'xmlns:style': SNSD
['style'],
1233 el3
= SubElement(el2
, 'number:hours', attrib
={
1234 'number:style': 'long',
1236 el3
= SubElement(el2
, 'number:text')
1238 el3
= SubElement(el2
, 'number:minutes', attrib
={
1239 'number:style': 'long',
1241 el3
= SubElement(el2
, 'number:text')
1243 el3
= SubElement(el2
, 'number:am-pm')
1245 self
.style_index
+= 1
1246 el1
= SubElement(parent
, 'text:time', attrib
={
1247 'text:style-name': self
.rststyle(style_name
),
1248 'text:fixed': 'true',
1249 'style:data-style-name':
1250 'rst-time-style-%d' % self
.style_index
,
1252 el2
= SubElement(automatic_styles
, 'number:time-style', attrib
={
1253 'style:name': 'rst-time-style-%d' % self
.style_index
,
1254 'xmlns:number': SNSD
['number'],
1255 'xmlns:style': SNSD
['style'],
1257 el3
= SubElement(el2
, 'number:hours', attrib
={
1258 'number:style': 'long',
1260 el3
= SubElement(el2
, 'number:text')
1262 el3
= SubElement(el2
, 'number:minutes', attrib
={
1263 'number:style': 'long',
1265 el3
= SubElement(el2
, 'number:text')
1267 el3
= SubElement(el2
, 'number:seconds', attrib
={
1268 'number:style': 'long',
1270 el3
= SubElement(el2
, 'number:text')
1272 el3
= SubElement(el2
, 'number:am-pm')
1274 self
.style_index
+= 1
1275 el1
= SubElement(parent
, 'text:date', attrib
={
1276 'text:style-name': self
.rststyle(style_name
),
1277 'style:data-style-name':
1278 'rst-date-style-%d' % self
.style_index
,
1280 el2
= SubElement(automatic_styles
, 'number:date-style', attrib
={
1281 'style:name': 'rst-date-style-%d' % self
.style_index
,
1282 'number:automatic-order': 'true',
1283 'xmlns:number': SNSD
['number'],
1284 'xmlns:style': SNSD
['style'],
1286 el3
= SubElement(el2
, 'number:month', attrib
={
1287 'number:style': 'long',
1289 el3
= SubElement(el2
, 'number:text')
1291 el3
= SubElement(el2
, 'number:day', attrib
={
1292 'number:style': 'long',
1294 el3
= SubElement(el2
, 'number:text')
1296 el3
= SubElement(el2
, 'number:year')
1298 self
.style_index
+= 1
1299 el1
= SubElement(parent
, 'text:date', attrib
={
1300 'text:style-name': self
.rststyle(style_name
),
1301 'style:data-style-name':
1302 'rst-date-style-%d' % self
.style_index
,
1304 el2
= SubElement(automatic_styles
, 'number:date-style', attrib
={
1305 'style:name': 'rst-date-style-%d' % self
.style_index
,
1306 'number:automatic-order': 'true',
1307 'xmlns:number': SNSD
['number'],
1308 'xmlns:style': SNSD
['style'],
1310 el3
= SubElement(el2
, 'number:month', attrib
={
1311 'number:style': 'long',
1313 el3
= SubElement(el2
, 'number:text')
1315 el3
= SubElement(el2
, 'number:day', attrib
={
1316 'number:style': 'long',
1318 el3
= SubElement(el2
, 'number:text')
1320 el3
= SubElement(el2
, 'number:year', attrib
={
1321 'number:style': 'long',
1324 self
.style_index
+= 1
1325 el1
= SubElement(parent
, 'text:date', attrib
={
1326 'text:style-name': self
.rststyle(style_name
),
1327 'style:data-style-name':
1328 'rst-date-style-%d' % self
.style_index
,
1330 el2
= SubElement(automatic_styles
, 'number:date-style', attrib
={
1331 'style:name': 'rst-date-style-%d' % self
.style_index
,
1332 'number:automatic-order': 'true',
1333 'xmlns:number': SNSD
['number'],
1334 'xmlns:style': SNSD
['style'],
1336 el3
= SubElement(el2
, 'number:month', attrib
={
1337 'number:textual': 'true',
1339 el3
= SubElement(el2
, 'number:text')
1341 el3
= SubElement(el2
, 'number:day', attrib
={})
1342 el3
= SubElement(el2
, 'number:text')
1344 el3
= SubElement(el2
, 'number:year', attrib
={
1345 'number:style': 'long',
1348 self
.style_index
+= 1
1349 el1
= SubElement(parent
, 'text:date', attrib
={
1350 'text:style-name': self
.rststyle(style_name
),
1351 'style:data-style-name':
1352 'rst-date-style-%d' % self
.style_index
,
1354 el2
= SubElement(automatic_styles
, 'number:date-style', attrib
={
1355 'style:name': 'rst-date-style-%d' % self
.style_index
,
1356 'number:automatic-order': 'true',
1357 'xmlns:number': SNSD
['number'],
1358 'xmlns:style': SNSD
['style'],
1360 el3
= SubElement(el2
, 'number:month', attrib
={
1361 'number:textual': 'true',
1362 'number:style': 'long',
1364 el3
= SubElement(el2
, 'number:text')
1366 el3
= SubElement(el2
, 'number:day', attrib
={})
1367 el3
= SubElement(el2
, 'number:text')
1369 el3
= SubElement(el2
, 'number:year', attrib
={
1370 'number:style': 'long',
1373 self
.style_index
+= 1
1374 el1
= SubElement(parent
, 'text:date', attrib
={
1375 'text:style-name': self
.rststyle(style_name
),
1376 'style:data-style-name':
1377 'rst-date-style-%d' % self
.style_index
,
1379 el2
= SubElement(automatic_styles
, 'number:date-style', attrib
={
1380 'style:name': 'rst-date-style-%d' % self
.style_index
,
1381 'xmlns:number': SNSD
['number'],
1382 'xmlns:style': SNSD
['style'],
1384 el3
= SubElement(el2
, 'number:year', attrib
={
1385 'number:style': 'long',
1387 el3
= SubElement(el2
, 'number:text')
1389 el3
= SubElement(el2
, 'number:month', attrib
={
1390 'number:style': 'long',
1392 el3
= SubElement(el2
, 'number:text')
1394 el3
= SubElement(el2
, 'number:day', attrib
={
1395 'number:style': 'long',
1398 el1
= SubElement(parent
, 'text:subject', attrib
={
1399 'text:style-name': self
.rststyle(style_name
),
1402 el1
= SubElement(parent
, 'text:title', attrib
={
1403 'text:style-name': self
.rststyle(style_name
),
1406 el1
= SubElement(parent
, 'text:author-name', attrib
={
1407 'text:fixed': 'false',
1413 def split_field_specifiers_iter(self
, text
):
1416 mo
= ODFTranslator
.field_pat
.search(text
, pos1
)
1420 yield ODFTranslator
.code_text
, text
[pos1
:pos2
]
1421 yield ODFTranslator
.code_field
, mo
.group(1)
1425 trailing
= text
[pos1
:]
1427 yield ODFTranslator
.code_text
, trailing
1430 root
= self
.content_tree
.getroot()
1431 et
= etree
.ElementTree(root
)
1434 def content_astext(self
):
1435 return self
.astext()
1437 def set_title(self
, title
):
1440 def get_title(self
):
1443 def set_embedded_file_list(self
, embedded_file_list
):
1444 self
.embedded_file_list
= embedded_file_list
1446 def get_embedded_file_list(self
):
1447 return self
.embedded_file_list
1449 def get_meta_dict(self
):
1450 return self
.meta_dict
1452 def process_footnotes(self
):
1453 for node
, el1
in self
.footnote_list
:
1454 backrefs
= node
.attributes
.get('backrefs', [])
1456 for ref
in backrefs
:
1457 el2
= self
.footnote_ref_dict
.get(ref
)
1461 el3
= copy
.deepcopy(el1
)
1464 if len(el2
) > 0: # and 'id' in el2.attrib:
1467 attribkey
= add_ns('text:id', nsdict
=SNSD
)
1468 id1
= el2
.get(attribkey
, 'footnote-error')
1471 tag
= add_ns('text:note-ref', nsdict
=SNSD
)
1473 if self
.settings
.endnotes_end_doc
:
1474 note_class
= 'endnote'
1476 note_class
= 'footnote'
1478 attribkey
= add_ns('text:note-class', nsdict
=SNSD
)
1479 el2
.attrib
[attribkey
] = note_class
1480 attribkey
= add_ns('text:ref-name', nsdict
=SNSD
)
1481 el2
.attrib
[attribkey
] = id1
1483 'text:reference-format', nsdict
=SNSD
)
1484 el2
.attrib
[attribkey
] = 'page'
1490 def append_child(self
, tag
, attrib
=None, parent
=None):
1492 parent
= self
.current_element
1493 return SubElement(parent
, tag
, attrib
)
1495 def append_p(self
, style
, text
=None):
1496 result
= self
.append_child('text:p', attrib
={
1497 'text:style-name': self
.rststyle(style
)})
1498 self
.append_pending_ids(result
)
1499 if text
is not None:
1503 def append_pending_ids(self
, el
):
1504 if self
.settings
.create_links
:
1505 for id in self
.pending_ids
:
1506 SubElement(el
, 'text:reference-mark', attrib
={
1508 self
.pending_ids
= []
1510 def set_current_element(self
, el
):
1511 self
.current_element
= el
1513 def set_to_parent(self
):
1514 self
.current_element
= self
.current_element
.getparent()
1516 def generate_labeled_block(self
, node
, label
):
1517 label
= '%s:' % (self
.language
.labels
[label
], )
1518 el
= self
.append_p('textbody')
1521 attrib
={'text:style-name': self
.rststyle('strong')})
1523 return self
.append_p('blockindent')
1525 def generate_labeled_line(self
, node
, label
):
1526 label
= '%s:' % (self
.language
.labels
[label
], )
1527 el
= self
.append_p('textbody')
1530 attrib
={'text:style-name': self
.rststyle('strong')})
1532 el1
.tail
= node
.astext()
1535 def encode(self
, text
):
1536 return text
.replace('\n', " ")
1541 # In alphabetic order, more or less.
1542 # See docutils.docutils.nodes.node_class_names.
1545 def dispatch_visit(self
, node
):
1546 """Override to catch basic attributes which many nodes have."""
1547 self
.handle_basic_atts(node
)
1548 nodes
.GenericNodeVisitor
.dispatch_visit(self
, node
)
1550 def handle_basic_atts(self
, node
):
1551 if isinstance(node
, nodes
.Element
) and node
['ids']:
1552 self
.pending_ids
+= node
['ids']
1554 def default_visit(self
, node
):
1555 self
.document
.reporter
.warning('missing visit_%s' % (node
.tagname
, ))
1557 def default_departure(self
, node
):
1558 self
.document
.reporter
.warning('missing depart_%s' % (node
.tagname
, ))
1560 def visit_Text(self
, node
):
1561 # Skip nodes whose text has been processed in parent nodes.
1562 if isinstance(node
.parent
, docutils
.nodes
.literal_block
):
1564 text
= node
.astext()
1565 # Are we in mixed content? If so, add the text to the
1566 # etree tail of the previous sibling element.
1567 if len(self
.current_element
) > 0:
1568 if self
.current_element
[-1].tail
:
1569 self
.current_element
[-1].tail
+= text
1571 self
.current_element
[-1].tail
= text
1573 if self
.current_element
.text
:
1574 self
.current_element
.text
+= text
1576 self
.current_element
.text
= text
1578 def depart_Text(self
, node
):
1582 # Pre-defined fields
1585 def visit_address(self
, node
):
1586 el
= self
.generate_labeled_block(node
, 'address')
1587 self
.set_current_element(el
)
1589 def depart_address(self
, node
):
1590 self
.set_to_parent()
1592 def visit_author(self
, node
):
1593 if isinstance(node
.parent
, nodes
.authors
):
1594 el
= self
.append_p('blockindent')
1596 el
= self
.generate_labeled_block(node
, 'author')
1597 self
.set_current_element(el
)
1599 def depart_author(self
, node
):
1600 self
.set_to_parent()
1602 def visit_authors(self
, node
):
1603 label
= '%s:' % (self
.language
.labels
['authors'], )
1604 el
= self
.append_p('textbody')
1607 attrib
={'text:style-name': self
.rststyle('strong')})
1610 def depart_authors(self
, node
):
1613 def visit_contact(self
, node
):
1614 el
= self
.generate_labeled_block(node
, 'contact')
1615 self
.set_current_element(el
)
1617 def depart_contact(self
, node
):
1618 self
.set_to_parent()
1620 def visit_copyright(self
, node
):
1621 el
= self
.generate_labeled_block(node
, 'copyright')
1622 self
.set_current_element(el
)
1624 def depart_copyright(self
, node
):
1625 self
.set_to_parent()
1627 def visit_date(self
, node
):
1628 self
.generate_labeled_line(node
, 'date')
1630 def depart_date(self
, node
):
1633 def visit_organization(self
, node
):
1634 el
= self
.generate_labeled_block(node
, 'organization')
1635 self
.set_current_element(el
)
1637 def depart_organization(self
, node
):
1638 self
.set_to_parent()
1640 def visit_status(self
, node
):
1641 el
= self
.generate_labeled_block(node
, 'status')
1642 self
.set_current_element(el
)
1644 def depart_status(self
, node
):
1645 self
.set_to_parent()
1647 def visit_revision(self
, node
):
1648 self
.generate_labeled_line(node
, 'revision')
1650 def depart_revision(self
, node
):
1653 def visit_version(self
, node
):
1654 self
.generate_labeled_line(node
, 'version')
1655 # self.set_current_element(el)
1657 def depart_version(self
, node
):
1658 # self.set_to_parent()
1661 def visit_attribution(self
, node
):
1662 self
.append_p('attribution', node
.astext())
1664 def depart_attribution(self
, node
):
1667 def visit_block_quote(self
, node
):
1668 if 'epigraph' in node
.attributes
['classes']:
1669 self
.paragraph_style_stack
.append(self
.rststyle('epigraph'))
1670 self
.blockstyle
= self
.rststyle('epigraph')
1671 elif 'highlights' in node
.attributes
['classes']:
1672 self
.paragraph_style_stack
.append(self
.rststyle('highlights'))
1673 self
.blockstyle
= self
.rststyle('highlights')
1675 self
.paragraph_style_stack
.append(self
.rststyle('blockquote'))
1676 self
.blockstyle
= self
.rststyle('blockquote')
1677 self
.line_indent_level
+= 1
1679 def depart_block_quote(self
, node
):
1680 self
.paragraph_style_stack
.pop()
1681 self
.blockstyle
= ''
1682 self
.line_indent_level
-= 1
1684 def visit_bullet_list(self
, node
):
1685 self
.list_level
+= 1
1686 if self
.in_table_of_contents
:
1687 if self
.settings
.generate_oowriter_toc
:
1690 if 'classes' in node
and \
1691 'auto-toc' in node
.attributes
['classes']:
1692 el
= SubElement(self
.current_element
, 'text:list', attrib
={
1693 'text:style-name': self
.rststyle('tocenumlist'),
1695 self
.list_style_stack
.append(self
.rststyle('enumitem'))
1697 el
= SubElement(self
.current_element
, 'text:list', attrib
={
1698 'text:style-name': self
.rststyle('tocbulletlist'),
1700 self
.list_style_stack
.append(self
.rststyle('bulletitem'))
1701 self
.set_current_element(el
)
1703 if self
.blockstyle
== self
.rststyle('blockquote'):
1704 el
= SubElement(self
.current_element
, 'text:list', attrib
={
1705 'text:style-name': self
.rststyle('blockquote-bulletlist'),
1707 self
.list_style_stack
.append(
1708 self
.rststyle('blockquote-bulletitem'))
1709 elif self
.blockstyle
== self
.rststyle('highlights'):
1710 el
= SubElement(self
.current_element
, 'text:list', attrib
={
1711 'text:style-name': self
.rststyle('highlights-bulletlist'),
1713 self
.list_style_stack
.append(
1714 self
.rststyle('highlights-bulletitem'))
1715 elif self
.blockstyle
== self
.rststyle('epigraph'):
1716 el
= SubElement(self
.current_element
, 'text:list', attrib
={
1717 'text:style-name': self
.rststyle('epigraph-bulletlist'),
1719 self
.list_style_stack
.append(
1720 self
.rststyle('epigraph-bulletitem'))
1722 el
= SubElement(self
.current_element
, 'text:list', attrib
={
1723 'text:style-name': self
.rststyle('bulletlist'),
1725 self
.list_style_stack
.append(self
.rststyle('bulletitem'))
1726 self
.set_current_element(el
)
1728 def depart_bullet_list(self
, node
):
1729 if self
.in_table_of_contents
:
1730 if self
.settings
.generate_oowriter_toc
:
1733 self
.set_to_parent()
1734 self
.list_style_stack
.pop()
1736 self
.set_to_parent()
1737 self
.list_style_stack
.pop()
1738 self
.list_level
-= 1
1740 def visit_caption(self
, node
):
1741 raise nodes
.SkipChildren()
1743 def depart_caption(self
, node
):
1746 def visit_comment(self
, node
):
1747 el
= self
.append_p('textbody')
1748 el1
= SubElement(el
, 'office:annotation', attrib
={})
1749 el2
= SubElement(el1
, 'dc:creator', attrib
={})
1750 s1
= os
.environ
.get('USER', '')
1752 el2
= SubElement(el1
, 'text:p', attrib
={})
1753 el2
.text
= node
.astext()
1755 def depart_comment(self
, node
):
1758 def visit_compound(self
, node
):
1759 # The compound directive currently receives no special treatment.
1762 def depart_compound(self
, node
):
1765 def visit_container(self
, node
):
1766 styles
= node
.attributes
.get('classes', ())
1768 self
.paragraph_style_stack
.append(self
.rststyle(styles
[0]))
1770 def depart_container(self
, node
):
1771 styles
= node
.attributes
.get('classes', ())
1773 self
.paragraph_style_stack
.pop()
1775 def visit_decoration(self
, node
):
1778 def depart_decoration(self
, node
):
1781 def visit_definition_list(self
, node
):
1782 self
.def_list_level
+= 1
1783 if self
.list_level
> 5:
1785 'max definition list nesting level exceeded')
1787 def depart_definition_list(self
, node
):
1788 self
.def_list_level
-= 1
1790 def visit_definition_list_item(self
, node
):
1793 def depart_definition_list_item(self
, node
):
1796 def visit_term(self
, node
):
1797 el
= self
.append_p('deflist-term-%d' % self
.def_list_level
)
1798 el
.text
= node
.astext()
1799 self
.set_current_element(el
)
1800 raise nodes
.SkipChildren()
1802 def depart_term(self
, node
):
1803 self
.set_to_parent()
1805 def visit_definition(self
, node
):
1806 self
.paragraph_style_stack
.append(
1807 self
.rststyle('deflist-def-%d' % self
.def_list_level
))
1808 self
.bumped_list_level_stack
.append(ListLevel(1))
1810 def depart_definition(self
, node
):
1811 self
.paragraph_style_stack
.pop()
1812 self
.bumped_list_level_stack
.pop()
1814 def visit_classifier(self
, node
):
1815 if len(self
.current_element
) > 0:
1816 el
= self
.current_element
[-1]
1819 attrib
={'text:style-name': self
.rststyle('emphasis')})
1820 el1
.text
= ' (%s)' % (node
.astext(), )
1822 def depart_classifier(self
, node
):
1825 def visit_document(self
, node
):
1828 def depart_document(self
, node
):
1829 self
.process_footnotes()
1831 def visit_docinfo(self
, node
):
1832 self
.section_level
+= 1
1833 self
.section_count
+= 1
1834 if self
.settings
.create_sections
:
1835 el
= self
.append_child(
1836 'text:section', attrib
={
1837 'text:name': 'Section%d' % self
.section_count
,
1838 'text:style-name': 'Sect%d' % self
.section_level
,
1841 self
.set_current_element(el
)
1843 def depart_docinfo(self
, node
):
1844 self
.section_level
-= 1
1845 if self
.settings
.create_sections
:
1846 self
.set_to_parent()
1848 def visit_emphasis(self
, node
):
1850 self
.current_element
, 'text:span',
1851 attrib
={'text:style-name': self
.rststyle('emphasis')})
1852 self
.set_current_element(el
)
1854 def depart_emphasis(self
, node
):
1855 self
.set_to_parent()
1857 def visit_enumerated_list(self
, node
):
1858 el1
= self
.current_element
1859 if self
.blockstyle
== self
.rststyle('blockquote'):
1860 el2
= SubElement(el1
, 'text:list', attrib
={
1861 'text:style-name': self
.rststyle('blockquote-enumlist'),
1863 self
.list_style_stack
.append(self
.rststyle('blockquote-enumitem'))
1864 elif self
.blockstyle
== self
.rststyle('highlights'):
1865 el2
= SubElement(el1
, 'text:list', attrib
={
1866 'text:style-name': self
.rststyle('highlights-enumlist'),
1868 self
.list_style_stack
.append(self
.rststyle('highlights-enumitem'))
1869 elif self
.blockstyle
== self
.rststyle('epigraph'):
1870 el2
= SubElement(el1
, 'text:list', attrib
={
1871 'text:style-name': self
.rststyle('epigraph-enumlist'),
1873 self
.list_style_stack
.append(self
.rststyle('epigraph-enumitem'))
1875 liststylename
= 'enumlist-%s' % (node
.get('enumtype', 'arabic'), )
1876 el2
= SubElement(el1
, 'text:list', attrib
={
1877 'text:style-name': self
.rststyle(liststylename
),
1879 self
.list_style_stack
.append(self
.rststyle('enumitem'))
1880 self
.set_current_element(el2
)
1882 def depart_enumerated_list(self
, node
):
1883 self
.set_to_parent()
1884 self
.list_style_stack
.pop()
1886 def visit_list_item(self
, node
):
1887 # If we are in a "bumped" list level, then wrap this
1888 # list in an outer lists in order to increase the
1889 # indentation level.
1890 if self
.in_table_of_contents
:
1891 if self
.settings
.generate_oowriter_toc
:
1892 self
.paragraph_style_stack
.append(
1893 self
.rststyle('contents-%d' % (self
.list_level
, )))
1895 el1
= self
.append_child('text:list-item')
1896 self
.set_current_element(el1
)
1898 el1
= self
.append_child('text:list-item')
1900 if len(self
.bumped_list_level_stack
) > 0:
1901 level_obj
= self
.bumped_list_level_stack
[-1]
1902 if level_obj
.get_sibling():
1903 level_obj
.set_nested(False)
1904 for level_obj1
in self
.bumped_list_level_stack
:
1905 for idx
in range(level_obj1
.get_level()):
1906 el2
= self
.append_child('text:list', parent
=el3
)
1907 el3
= self
.append_child(
1908 'text:list-item', parent
=el2
)
1909 self
.paragraph_style_stack
.append(self
.list_style_stack
[-1])
1910 self
.set_current_element(el3
)
1912 def depart_list_item(self
, node
):
1913 if self
.in_table_of_contents
:
1914 if self
.settings
.generate_oowriter_toc
:
1915 self
.paragraph_style_stack
.pop()
1917 self
.set_to_parent()
1919 if len(self
.bumped_list_level_stack
) > 0:
1920 level_obj
= self
.bumped_list_level_stack
[-1]
1921 if level_obj
.get_sibling():
1922 level_obj
.set_nested(True)
1923 for level_obj1
in self
.bumped_list_level_stack
:
1924 for idx
in range(level_obj1
.get_level()):
1925 self
.set_to_parent()
1926 self
.set_to_parent()
1927 self
.paragraph_style_stack
.pop()
1928 self
.set_to_parent()
1930 def visit_header(self
, node
):
1931 self
.in_header
= True
1933 def depart_header(self
, node
):
1934 self
.in_header
= False
1936 def visit_footer(self
, node
):
1937 self
.in_footer
= True
1939 def depart_footer(self
, node
):
1940 self
.in_footer
= False
1942 def visit_field(self
, node
):
1945 def depart_field(self
, node
):
1948 def visit_field_list(self
, node
):
1951 def depart_field_list(self
, node
):
1954 def visit_field_name(self
, node
):
1955 el
= self
.append_p('textbody')
1958 attrib
={'text:style-name': self
.rststyle('strong')})
1959 el1
.text
= node
.astext()
1961 def depart_field_name(self
, node
):
1964 def visit_field_body(self
, node
):
1965 self
.paragraph_style_stack
.append(self
.rststyle('blockindent'))
1967 def depart_field_body(self
, node
):
1968 self
.paragraph_style_stack
.pop()
1970 def visit_figure(self
, node
):
1973 def depart_figure(self
, node
):
1976 def visit_footnote(self
, node
):
1977 self
.footnote_level
+= 1
1978 self
.save_footnote_current
= self
.current_element
1979 el1
= Element('text:note-body')
1980 self
.current_element
= el1
1981 self
.footnote_list
.append((node
, el1
))
1982 if isinstance(node
, docutils
.nodes
.citation
):
1983 self
.paragraph_style_stack
.append(self
.rststyle('citation'))
1985 self
.paragraph_style_stack
.append(self
.rststyle('footnote'))
1987 def depart_footnote(self
, node
):
1988 self
.paragraph_style_stack
.pop()
1989 self
.current_element
= self
.save_footnote_current
1990 self
.footnote_level
-= 1
1999 def visit_footnote_reference(self
, node
):
2000 if self
.footnote_level
<= 0:
2001 id = node
.attributes
['ids'][0]
2002 refid
= node
.attributes
.get('refid')
2005 if self
.settings
.endnotes_end_doc
:
2006 note_class
= 'endnote'
2008 note_class
= 'footnote'
2009 el1
= self
.append_child('text:note', attrib
={
2010 'text:id': '%s' % (refid
, ),
2011 'text:note-class': note_class
,
2013 note_auto
= str(node
.attributes
.get('auto', 1))
2014 if isinstance(node
, docutils
.nodes
.citation_reference
):
2015 citation
= '[%s]' % node
.astext()
2016 el2
= SubElement(el1
, 'text:note-citation', attrib
={
2017 'text:label': citation
,
2020 elif note_auto
== '1':
2021 el2
= SubElement(el1
, 'text:note-citation', attrib
={
2022 'text:label': node
.astext(),
2024 el2
.text
= node
.astext()
2025 elif note_auto
== '*':
2026 if self
.footnote_chars_idx
>= len(
2027 ODFTranslator
.footnote_chars
):
2028 self
.footnote_chars_idx
= 0
2029 footnote_char
= ODFTranslator
.footnote_chars
[
2030 self
.footnote_chars_idx
]
2031 self
.footnote_chars_idx
+= 1
2032 el2
= SubElement(el1
, 'text:note-citation', attrib
={
2033 'text:label': footnote_char
,
2035 el2
.text
= footnote_char
2036 self
.footnote_ref_dict
[id] = el1
2037 raise nodes
.SkipChildren()
2039 def depart_footnote_reference(self
, node
):
2042 def visit_citation(self
, node
):
2043 self
.in_citation
= True
2044 for id in node
.attributes
['ids']:
2045 self
.citation_id
= id
2047 self
.paragraph_style_stack
.append(self
.rststyle('blockindent'))
2048 self
.bumped_list_level_stack
.append(ListLevel(1))
2050 def depart_citation(self
, node
):
2051 self
.citation_id
= None
2052 self
.paragraph_style_stack
.pop()
2053 self
.bumped_list_level_stack
.pop()
2054 self
.in_citation
= False
2056 def visit_citation_reference(self
, node
):
2057 if self
.settings
.create_links
:
2058 id = node
.attributes
['refid']
2059 el
= self
.append_child('text:reference-ref', attrib
={
2060 'text:ref-name': '%s' % (id, ),
2061 'text:reference-format': 'text',
2064 self
.set_current_element(el
)
2065 elif self
.current_element
.text
is None:
2066 self
.current_element
.text
= '['
2068 self
.current_element
.text
+= '['
2070 def depart_citation_reference(self
, node
):
2071 self
.current_element
.text
+= ']'
2072 if self
.settings
.create_links
:
2073 self
.set_to_parent()
2075 def visit_label(self
, node
):
2076 if isinstance(node
.parent
, docutils
.nodes
.footnote
):
2077 raise nodes
.SkipChildren()
2078 elif self
.citation_id
is not None:
2079 el
= self
.append_p('textbody')
2080 self
.set_current_element(el
)
2081 if self
.settings
.create_links
:
2082 el0
= SubElement(el
, 'text:span')
2084 self
.append_child('text:reference-mark-start', attrib
={
2085 'text:name': '%s' % (self
.citation_id
, ),
2090 def depart_label(self
, node
):
2091 if isinstance(node
.parent
, docutils
.nodes
.footnote
):
2093 elif self
.citation_id
is not None:
2094 if self
.settings
.create_links
:
2095 self
.append_child('text:reference-mark-end', attrib
={
2096 'text:name': '%s' % (self
.citation_id
, ),
2098 el0
= SubElement(self
.current_element
, 'text:span')
2101 self
.current_element
.text
+= ']'
2102 self
.set_to_parent()
2104 def visit_generated(self
, node
):
2107 def depart_generated(self
, node
):
2110 def check_file_exists(self
, path
):
2111 if os
.path
.exists(path
):
2116 def visit_image(self
, node
):
2117 # Capture the image file.
2118 source
= node
['uri']
2119 uri_parts
= urllib
.parse
.urlparse(source
)
2120 if uri_parts
.scheme
in ('', 'file'):
2121 source
= urllib
.parse
.unquote(uri_parts
.path
)
2122 if source
.startswith('/'):
2123 root_prefix
= Path(self
.settings
.root_prefix
)
2124 source
= (root_prefix
/source
[1:]).as_posix()
2126 # adapt relative paths
2127 docsource
, line
= utils
.get_source_line(node
)
2129 dirname
= os
.path
.dirname(docsource
)
2131 source
= os
.path
.join(dirname
, source
)
2132 if not self
.check_file_exists(source
):
2133 self
.document
.reporter
.warning(
2134 f
'Cannot find image file "{source}".')
2136 if source
in self
.image_dict
:
2137 filename
, destination
= self
.image_dict
[source
]
2139 self
.image_count
+= 1
2140 filename
= os
.path
.split(source
)[1]
2141 destination
= 'Pictures/1%08x%s' % (self
.image_count
, filename
)
2142 if uri_parts
.scheme
in ('', 'file'):
2143 spec
= (os
.path
.abspath(source
), destination
,)
2146 with urllib
.request
.urlopen(source
) as imgfile
:
2147 content
= imgfile
.read()
2148 except urllib
.error
.URLError
as err
:
2149 self
.document
.reporter
.warning(
2150 f
'Cannot open image URL "{source}". {err}')
2152 with tempfile
.NamedTemporaryFile('wb',
2153 delete
=False) as imgfile2
:
2154 imgfile2
.write(content
)
2155 source
= imgfile2
.name
2156 spec
= (source
, destination
,)
2157 self
.embedded_file_list
.append(spec
)
2158 self
.image_dict
[source
] = (source
, destination
,)
2159 # Is this a figure (containing an image) or just a plain image?
2160 if self
.in_paragraph
:
2161 el1
= self
.current_element
2164 self
.current_element
, 'text:p',
2165 attrib
={'text:style-name': self
.rststyle('textbody')})
2167 if isinstance(node
.parent
, docutils
.nodes
.figure
):
2168 el3
, el4
, el5
, caption
= self
.generate_figure(
2172 el6
, width
= self
.generate_image(
2173 node
, source
, destination
,
2175 if caption
is not None:
2177 else: # if isinstance(node.parent, docutils.nodes.image):
2178 self
.generate_image(node
, source
, destination
, el2
)
2180 def depart_image(self
, node
):
2183 def get_image_width_height(self
, node
, attr
):
2186 if attr
in node
.attributes
:
2187 size
= node
.attributes
[attr
]
2189 # For conversion factors, see:
2190 # http://www.unitconversion.org/unit_converter/typography-ex.html
2192 if size
.endswith('%'):
2193 if attr
== 'height':
2194 # Percentage allowed for width but not height.
2195 raise ValueError('percentage not allowed for height')
2196 size
= size
.rstrip(' %')
2197 size
= float(size
) / 100.0
2200 size
, unit
= self
.convert_to_cm(size
)
2201 except ValueError as exp
:
2202 self
.document
.reporter
.warning(
2203 'Invalid %s for image: "%s". '
2205 attr
, node
.attributes
[attr
], exp
))
2208 def convert_to_cm(self
, size
):
2209 """Convert various units to centimeters.
2211 Note that a call to this method should be wrapped in:
2212 try: except ValueError:
2215 if size
.endswith('px'):
2216 size
= float(size
[:-2]) * 0.026 # convert px to cm
2217 elif size
.endswith('in'):
2218 size
= float(size
[:-2]) * 2.54 # convert in to cm
2219 elif size
.endswith('pt'):
2220 size
= float(size
[:-2]) * 0.035 # convert pt to cm
2221 elif size
.endswith('pc'):
2222 size
= float(size
[:-2]) * 2.371 # convert pc to cm
2223 elif size
.endswith('mm'):
2224 size
= float(size
[:-2]) * 0.1 # convert mm to cm
2225 elif size
.endswith('cm'):
2226 size
= float(size
[:-2])
2228 raise ValueError('unknown unit type')
2232 def get_image_scale(self
, node
):
2233 if 'scale' in node
.attributes
:
2234 scale
= node
.attributes
['scale']
2238 self
.document
.reporter
.warning(
2239 'Invalid scale for image: "%s"' % (
2240 node
.attributes
['scale'], ))
2241 if scale
< 1: # or scale > 100:
2242 self
.document
.reporter
.warning(
2243 'scale out of range (%s), using 1.' % (scale
, ))
2245 scale
= scale
* 0.01
2250 def get_image_scaled_width_height(self
, node
, source
):
2251 """Return the image size in centimeters adjusted by image attrs."""
2252 scale
= self
.get_image_scale(node
)
2253 width
, width_unit
= self
.get_image_width_height(node
, 'width')
2254 height
, _
= self
.get_image_width_height(node
, 'height')
2256 if PIL
is not None and source
in self
.image_dict
:
2257 filename
, destination
= self
.image_dict
[source
]
2258 with PIL
.Image
.open(filename
, 'r') as img
:
2260 dpi
= img
.info
.get('dpi', dpi
)
2261 # dpi information can be (xdpi, ydpi) or xydpi
2268 if width
is None or height
is None:
2269 if img_size
is None:
2271 'image size not fully specified and PIL not installed')
2274 width
= float(width
) * 0.026 # convert px to cm
2276 height
= img_size
[1]
2277 height
= float(height
) * 0.026 # convert px to cm
2278 if width_unit
== '%':
2280 image_width
= img_size
[0]
2281 image_width
= float(image_width
) * 0.026 # convert px to cm
2282 image_height
= img_size
[1]
2283 image_height
= float(image_height
) * 0.026 # convert px to cm
2284 line_width
= self
.get_page_width()
2285 width
= factor
* line_width
2286 factor
= (factor
* line_width
) / image_width
2287 height
= factor
* image_height
2290 width
= '%.2fcm' % width
2291 height
= '%.2fcm' % height
2292 return width
, height
2294 def get_page_width(self
):
2295 """Return the document's page width in centimeters."""
2296 root
= self
.get_dom_stylesheet()
2297 nodes
= root
.iterfind(
2298 './/{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
2300 '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
2301 'page-layout-properties')
2304 page_width
= node
.get(
2305 '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
2307 margin_left
= node
.get(
2308 '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
2310 margin_right
= node
.get(
2311 '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
2313 if (page_width
is None
2314 or margin_left
is None
2315 or margin_right
is None):
2318 page_width
, _
= self
.convert_to_cm(page_width
)
2319 margin_left
, _
= self
.convert_to_cm(margin_left
)
2320 margin_right
, _
= self
.convert_to_cm(margin_right
)
2322 self
.document
.reporter
.warning(
2323 'Stylesheet file contains invalid page width '
2325 width
= page_width
- margin_left
- margin_right
2327 # We can't find the width in styles, so we make a guess.
2328 # Use a width of 6 in = 15.24 cm.
2332 def generate_figure(self
, node
, source
, destination
, current_element
):
2334 width
, height
= self
.get_image_scaled_width_height(node
, source
)
2335 for node1
in node
.parent
.children
:
2336 if node1
.tagname
== 'caption':
2337 caption
= node1
.astext()
2338 self
.image_style_count
+= 1
2340 # Add the style for the caption.
2341 if caption
is not None:
2343 'style:class': 'extra',
2344 'style:family': 'paragraph',
2345 'style:name': 'Caption',
2346 'style:parent-style-name': 'Standard',
2348 el1
= SubElement(self
.automatic_styles
, 'style:style',
2349 attrib
=attrib
, nsdict
=SNSD
)
2351 'fo:margin-bottom': '0.0835in',
2352 'fo:margin-top': '0.0835in',
2353 'text:line-number': '0',
2354 'text:number-lines': 'false',
2356 SubElement(el1
, 'style:paragraph-properties',
2357 attrib
=attrib
, nsdict
=SNSD
)
2359 'fo:font-size': '12pt',
2360 'fo:font-style': 'italic',
2361 'style:font-name': 'Times',
2362 'style:font-name-complex': 'Lucidasans1',
2363 'style:font-size-asian': '12pt',
2364 'style:font-size-complex': '12pt',
2365 'style:font-style-asian': 'italic',
2366 'style:font-style-complex': 'italic',
2368 SubElement(el1
, 'style:text-properties',
2369 attrib
=attrib
, nsdict
=SNSD
)
2370 style_name
= 'rstframestyle%d' % self
.image_style_count
2371 draw_name
= 'graphics%d' % next(IMAGE_NAME_COUNTER
)
2374 'style:name': style_name
,
2375 'style:family': 'graphic',
2376 'style:parent-style-name': self
.rststyle('figureframe'),
2378 el1
= SubElement(self
.automatic_styles
,
2379 'style:style', attrib
=attrib
, nsdict
=SNSD
)
2382 classes
= node
.parent
.attributes
.get('classes')
2383 if classes
and 'wrap' in classes
:
2386 attrib
['style:wrap'] = 'dynamic'
2388 attrib
['style:wrap'] = 'none'
2389 SubElement(el1
, 'style:graphic-properties',
2390 attrib
=attrib
, nsdict
=SNSD
)
2392 'draw:style-name': style_name
,
2393 'draw:name': draw_name
,
2394 'text:anchor-type': 'paragraph',
2395 'draw:z-index': '0',
2397 attrib
['svg:width'] = width
2398 el3
= SubElement(current_element
, 'draw:frame', attrib
=attrib
)
2400 el4
= SubElement(el3
, 'draw:text-box', attrib
=attrib
)
2402 'text:style-name': self
.rststyle('caption'),
2404 el5
= SubElement(el4
, 'text:p', attrib
=attrib
)
2405 return el3
, el4
, el5
, caption
2407 def generate_image(self
, node
, source
, destination
, current_element
,
2409 width
, height
= self
.get_image_scaled_width_height(node
, source
)
2410 self
.image_style_count
+= 1
2411 style_name
= 'rstframestyle%d' % self
.image_style_count
2414 'style:name': style_name
,
2415 'style:family': 'graphic',
2416 'style:parent-style-name': self
.rststyle('image'),
2418 el1
= SubElement(self
.automatic_styles
,
2419 'style:style', attrib
=attrib
, nsdict
=SNSD
)
2422 if 'align' in node
.attributes
:
2423 align
= node
.attributes
['align'].split()
2425 if val
in ('left', 'center', 'right'):
2427 elif val
in ('top', 'middle', 'bottom'):
2429 if frame_attrs
is None:
2431 'style:vertical-pos': 'top',
2432 'style:vertical-rel': 'paragraph',
2433 'style:horizontal-rel': 'paragraph',
2434 'style:mirror': 'none',
2435 'fo:clip': 'rect(0cm 0cm 0cm 0cm)',
2436 'draw:luminance': '0%',
2437 'draw:contrast': '0%',
2441 'draw:gamma': '100%',
2442 'draw:color-inversion': 'false',
2443 'draw:image-opacity': '100%',
2444 'draw:color-mode': 'standard',
2447 attrib
= frame_attrs
2448 if halign
is not None:
2449 attrib
['style:horizontal-pos'] = halign
2450 if valign
is not None:
2451 attrib
['style:vertical-pos'] = valign
2452 # If there is a classes/wrap directive or we are
2453 # inside a table, add a no-wrap style.
2455 classes
= node
.attributes
.get('classes')
2456 if classes
and 'wrap' in classes
:
2459 attrib
['style:wrap'] = 'dynamic'
2461 attrib
['style:wrap'] = 'none'
2462 # If we are inside a table, add a no-wrap style.
2463 if self
.is_in_table(node
):
2464 attrib
['style:wrap'] = 'none'
2465 SubElement(el1
, 'style:graphic-properties',
2466 attrib
=attrib
, nsdict
=SNSD
)
2467 draw_name
= 'graphics%d' % next(IMAGE_NAME_COUNTER
)
2469 # el = SubElement(current_element, 'text:p',
2470 # attrib={'text:style-name': self.rststyle('textbody')})
2472 'draw:style-name': style_name
,
2473 'draw:name': draw_name
,
2474 'draw:z-index': '1',
2476 if isinstance(node
.parent
, nodes
.TextElement
):
2477 attrib
['text:anchor-type'] = 'as-char' # vds
2479 attrib
['text:anchor-type'] = 'paragraph'
2480 attrib
['svg:width'] = width
2481 attrib
['svg:height'] = height
2482 el1
= SubElement(current_element
, 'draw:frame', attrib
=attrib
)
2483 SubElement(el1
, 'draw:image', attrib
={
2484 'xlink:href': '%s' % (destination
, ),
2485 'xlink:type': 'simple',
2486 'xlink:show': 'embed',
2487 'xlink:actuate': 'onLoad',
2491 def is_in_table(self
, node
):
2494 if isinstance(node1
, docutils
.nodes
.entry
):
2496 node1
= node1
.parent
2499 def visit_legend(self
, node
):
2500 if isinstance(node
.parent
, docutils
.nodes
.figure
):
2501 el1
= self
.current_element
[-1]
2503 self
.current_element
= el1
2504 self
.paragraph_style_stack
.append(self
.rststyle('legend'))
2506 def depart_legend(self
, node
):
2507 if isinstance(node
.parent
, docutils
.nodes
.figure
):
2508 self
.paragraph_style_stack
.pop()
2509 self
.set_to_parent()
2510 self
.set_to_parent()
2511 self
.set_to_parent()
2513 def visit_line_block(self
, node
):
2514 self
.line_indent_level
+= 1
2515 self
.line_block_level
+= 1
2517 def depart_line_block(self
, node
):
2518 self
.line_indent_level
-= 1
2519 self
.line_block_level
-= 1
2521 def visit_line(self
, node
):
2522 style
= 'lineblock%d' % self
.line_indent_level
2523 el1
= SubElement(self
.current_element
, 'text:p',
2524 attrib
={'text:style-name': self
.rststyle(style
), })
2525 self
.current_element
= el1
2527 def depart_line(self
, node
):
2528 self
.set_to_parent()
2530 def visit_literal(self
, node
):
2532 self
.current_element
, 'text:span',
2533 attrib
={'text:style-name': self
.rststyle('inlineliteral')})
2534 self
.set_current_element(el
)
2536 def depart_literal(self
, node
):
2537 self
.set_to_parent()
2539 def visit_inline(self
, node
):
2540 styles
= node
.attributes
.get('classes', ())
2542 el
= self
.current_element
2543 for inline_style
in styles
:
2544 el
= SubElement(el
, 'text:span',
2545 attrib
={'text:style-name':
2546 self
.rststyle(inline_style
)})
2549 # No style was specified so use a default style (old code
2550 # crashed if no style was given)
2551 el
= SubElement(self
.current_element
, 'text:span')
2554 self
.set_current_element(el
)
2555 self
.inline_style_count_stack
.append(count
)
2557 def depart_inline(self
, node
):
2558 count
= self
.inline_style_count_stack
.pop()
2559 for x
in range(count
):
2560 self
.set_to_parent()
2562 def _calculate_code_block_padding(self
, line
):
2564 matchobj
= SPACES_PATTERN
.match(line
)
2566 pad
= matchobj
.group()
2569 matchobj
= TABS_PATTERN
.match(line
)
2571 pad
= matchobj
.group()
2572 count
= len(pad
) * 8
2575 def _add_syntax_highlighting(self
, insource
, language
):
2576 lexer
= pygments
.lexers
.get_lexer_by_name(language
, stripall
=True)
2577 if language
in ('latex', 'tex'):
2578 fmtr
= OdtPygmentsLaTeXFormatter(
2579 lambda name
, parameters
=():
2580 self
.rststyle(name
, parameters
),
2581 escape_function
=escape_cdata
)
2583 fmtr
= OdtPygmentsProgFormatter(
2584 lambda name
, parameters
=():
2585 self
.rststyle(name
, parameters
),
2586 escape_function
=escape_cdata
)
2587 return pygments
.highlight(insource
, lexer
, fmtr
)
2589 def fill_line(self
, line
):
2590 line
= FILL_PAT1
.sub(self
.fill_func1
, line
)
2591 return FILL_PAT2
.sub(self
.fill_func2
, line
)
2593 def fill_func1(self
, matchobj
):
2594 spaces
= matchobj
.group(0)
2595 return '<text:s text:c="%d"/>' % (len(spaces
), )
2597 def fill_func2(self
, matchobj
):
2598 spaces
= matchobj
.group(0)
2599 return ' <text:s text:c="%d"/>' % (len(spaces
) - 1, )
2601 def visit_literal_block(self
, node
):
2602 if len(self
.paragraph_style_stack
) > 1:
2603 wrapper1
= '<text:p text:style-name="%s">%%s</text:p>' % (
2604 self
.rststyle('codeblock-indented'), )
2606 wrapper1
= '<text:p text:style-name="%s">%%s</text:p>' % (
2607 self
.rststyle('codeblock'), )
2608 source
= node
.astext()
2609 if (pygments
and self
.settings
.add_syntax_highlighting
):
2610 language
= node
.get('language', 'python')
2611 source
= self
._add
_syntax
_highlighting
(source
, language
)
2613 source
= escape_cdata(source
)
2614 lines
= source
.split('\n')
2615 # If there is an empty last line, remove it.
2618 lines1
= ['<wrappertag1 xmlns:text="urn:oasis:names:tc:'
2619 'opendocument:xmlns:text:1.0">']
2621 for my_line
in lines
:
2622 my_line
= self
.fill_line(my_line
)
2623 my_line
= my_line
.replace(" ", "\n")
2624 my_lines
.append(my_line
)
2625 my_lines_str
= '<text:line-break/>'.join(my_lines
)
2626 my_lines_str2
= wrapper1
% (my_lines_str
, )
2627 lines1
.append(my_lines_str2
)
2628 lines1
.append('</wrappertag1>')
2629 s1
= ''.join(lines1
)
2630 s1
= s1
.encode("utf-8")
2631 el1
= etree
.fromstring(s1
)
2633 self
.current_element
.append(child
)
2635 def depart_literal_block(self
, node
):
2638 visit_doctest_block
= visit_literal_block
2639 depart_doctest_block
= depart_literal_block
2641 # placeholder for math (see docs/dev/todo.txt)
2642 def visit_math(self
, node
):
2643 self
.document
.reporter
.warning('"math" role not supported',
2645 self
.visit_literal(node
)
2647 def depart_math(self
, node
):
2648 self
.depart_literal(node
)
2650 def visit_math_block(self
, node
):
2651 self
.document
.reporter
.warning('"math" directive not supported',
2653 self
.visit_literal_block(node
)
2655 def depart_math_block(self
, node
):
2656 self
.depart_literal_block(node
)
2658 def visit_meta(self
, node
):
2659 name
= node
.attributes
.get('name')
2660 content
= node
.attributes
.get('content')
2661 if name
is not None and content
is not None:
2662 self
.meta_dict
[name
] = content
2664 def depart_meta(self
, node
):
2667 def visit_option_list(self
, node
):
2668 table_name
= 'tableoption'
2670 # Generate automatic styles
2671 if not self
.optiontablestyles_generated
:
2672 self
.optiontablestyles_generated
= True
2673 el
= SubElement(self
.automatic_styles
, 'style:style', attrib
={
2674 'style:name': self
.rststyle(table_name
),
2675 'style:family': 'table'}, nsdict
=SNSD
)
2676 el1
= SubElement(el
, 'style:table-properties', attrib
={
2677 'style:width': '17.59cm',
2678 'table:align': 'left',
2679 'style:shadow': 'none'}, nsdict
=SNSD
)
2680 el
= SubElement(self
.automatic_styles
, 'style:style', attrib
={
2681 'style:name': self
.rststyle('%s.%%c' % table_name
, ('A', )),
2682 'style:family': 'table-column'}, nsdict
=SNSD
)
2683 el1
= SubElement(el
, 'style:table-column-properties', attrib
={
2684 'style:column-width': '4.999cm'}, nsdict
=SNSD
)
2685 el
= SubElement(self
.automatic_styles
, 'style:style', attrib
={
2686 'style:name': self
.rststyle('%s.%%c' % table_name
, ('B', )),
2687 'style:family': 'table-column'}, nsdict
=SNSD
)
2688 el1
= SubElement(el
, 'style:table-column-properties', attrib
={
2689 'style:column-width': '12.587cm'}, nsdict
=SNSD
)
2690 el
= SubElement(self
.automatic_styles
, 'style:style', attrib
={
2691 'style:name': self
.rststyle(
2692 '%s.%%c%%d' % table_name
, ('A', 1, )),
2693 'style:family': 'table-cell'}, nsdict
=SNSD
)
2694 el1
= SubElement(el
, 'style:table-cell-properties', attrib
={
2695 'fo:background-color': 'transparent',
2696 'fo:padding': '0.097cm',
2697 'fo:border-left': '0.035cm solid #000000',
2698 'fo:border-right': 'none',
2699 'fo:border-top': '0.035cm solid #000000',
2700 'fo:border-bottom': '0.035cm solid #000000'}, nsdict
=SNSD
)
2701 el2
= SubElement(el1
, 'style:background-image', nsdict
=SNSD
)
2702 el
= SubElement(self
.automatic_styles
, 'style:style', attrib
={
2703 'style:name': self
.rststyle(
2704 '%s.%%c%%d' % table_name
, ('B', 1, )),
2705 'style:family': 'table-cell'}, nsdict
=SNSD
)
2706 el1
= SubElement(el
, 'style:table-cell-properties', attrib
={
2707 'fo:padding': '0.097cm',
2708 'fo:border': '0.035cm solid #000000'}, nsdict
=SNSD
)
2709 el
= SubElement(self
.automatic_styles
, 'style:style', attrib
={
2710 'style:name': self
.rststyle(
2711 '%s.%%c%%d' % table_name
, ('A', 2, )),
2712 'style:family': 'table-cell'}, nsdict
=SNSD
)
2713 el1
= SubElement(el
, 'style:table-cell-properties', attrib
={
2714 'fo:padding': '0.097cm',
2715 'fo:border-left': '0.035cm solid #000000',
2716 'fo:border-right': 'none',
2717 'fo:border-top': 'none',
2718 'fo:border-bottom': '0.035cm solid #000000'}, nsdict
=SNSD
)
2719 el
= SubElement(self
.automatic_styles
, 'style:style', attrib
={
2720 'style:name': self
.rststyle(
2721 '%s.%%c%%d' % table_name
, ('B', 2, )),
2722 'style:family': 'table-cell'}, nsdict
=SNSD
)
2723 el1
= SubElement(el
, 'style:table-cell-properties', attrib
={
2724 'fo:padding': '0.097cm',
2725 'fo:border-left': '0.035cm solid #000000',
2726 'fo:border-right': '0.035cm solid #000000',
2727 'fo:border-top': 'none',
2728 'fo:border-bottom': '0.035cm solid #000000'}, nsdict
=SNSD
)
2730 # Generate table data
2731 el
= self
.append_child('table:table', attrib
={
2732 'table:name': self
.rststyle(table_name
),
2733 'table:style-name': self
.rststyle(table_name
),
2735 el1
= SubElement(el
, 'table:table-column', attrib
={
2736 'table:style-name': self
.rststyle(
2737 '%s.%%c' % table_name
, ('A', ))})
2738 el1
= SubElement(el
, 'table:table-column', attrib
={
2739 'table:style-name': self
.rststyle(
2740 '%s.%%c' % table_name
, ('B', ))})
2741 el1
= SubElement(el
, 'table:table-header-rows')
2742 el2
= SubElement(el1
, 'table:table-row')
2743 el3
= SubElement(el2
, 'table:table-cell', attrib
={
2744 'table:style-name': self
.rststyle(
2745 '%s.%%c%%d' % table_name
, ('A', 1, )),
2746 'office:value-type': 'string'})
2747 el4
= SubElement(el3
, 'text:p', attrib
={
2748 'text:style-name': 'Table_20_Heading'})
2750 el3
= SubElement(el2
, 'table:table-cell', attrib
={
2751 'table:style-name': self
.rststyle(
2752 '%s.%%c%%d' % table_name
, ('B', 1, )),
2753 'office:value-type': 'string'})
2754 el4
= SubElement(el3
, 'text:p', attrib
={
2755 'text:style-name': 'Table_20_Heading'})
2756 el4
.text
= 'Description'
2757 self
.set_current_element(el
)
2759 def depart_option_list(self
, node
):
2760 self
.set_to_parent()
2762 def visit_option_list_item(self
, node
):
2763 el
= self
.append_child('table:table-row')
2764 self
.set_current_element(el
)
2766 def depart_option_list_item(self
, node
):
2767 self
.set_to_parent()
2769 def visit_option_group(self
, node
):
2770 el
= self
.append_child('table:table-cell', attrib
={
2771 'table:style-name': 'Table%d.A2' % self
.table_count
,
2772 'office:value-type': 'string',
2774 self
.set_current_element(el
)
2776 def depart_option_group(self
, node
):
2777 self
.set_to_parent()
2779 def visit_option(self
, node
):
2780 el
= self
.append_child('text:p', attrib
={
2781 'text:style-name': 'Table_20_Contents'})
2782 el
.text
= node
.astext()
2784 def depart_option(self
, node
):
2787 def visit_option_string(self
, node
):
2790 def depart_option_string(self
, node
):
2793 def visit_option_argument(self
, node
):
2796 def depart_option_argument(self
, node
):
2799 def visit_description(self
, node
):
2800 el
= self
.append_child('table:table-cell', attrib
={
2801 'table:style-name': 'Table%d.B2' % self
.table_count
,
2802 'office:value-type': 'string',
2804 el1
= SubElement(el
, 'text:p', attrib
={
2805 'text:style-name': 'Table_20_Contents'})
2806 el1
.text
= node
.astext()
2807 raise nodes
.SkipChildren()
2809 def depart_description(self
, node
):
2812 def visit_paragraph(self
, node
):
2813 self
.in_paragraph
= True
2815 el
= self
.append_p('header')
2816 elif self
.in_footer
:
2817 el
= self
.append_p('footer')
2819 style_name
= self
.paragraph_style_stack
[-1]
2820 el
= self
.append_child(
2822 attrib
={'text:style-name': style_name
})
2823 self
.append_pending_ids(el
)
2824 self
.set_current_element(el
)
2826 def depart_paragraph(self
, node
):
2827 self
.in_paragraph
= False
2828 self
.set_to_parent()
2830 self
.header_content
.append(self
.current_element
[-1])
2831 self
.current_element
.remove(self
.current_element
[-1])
2832 elif self
.in_footer
:
2833 self
.footer_content
.append(self
.current_element
[-1])
2834 self
.current_element
.remove(self
.current_element
[-1])
2836 def visit_problematic(self
, node
):
2839 def depart_problematic(self
, node
):
2842 def visit_raw(self
, node
):
2843 if 'format' in node
.attributes
:
2844 formats
= node
.attributes
['format']
2845 formatlist
= formats
.split()
2846 if 'odt' in formatlist
:
2847 rawstr
= node
.astext()
2849 '%s="%s"' % (k
, v
, )
2850 for k
, v
in list(CONTENT_NAMESPACE_ATTRIB
.items()))
2851 contentstr
= '<stuff %s>%s</stuff>' % (attrstr
, rawstr
, )
2852 contentstr
= contentstr
.encode("utf-8")
2853 content
= etree
.fromstring(contentstr
)
2854 if len(content
) > 0:
2858 elif self
.in_footer
:
2861 self
.current_element
.append(el1
)
2862 raise nodes
.SkipChildren()
2864 def depart_raw(self
, node
):
2867 elif self
.in_footer
:
2872 def visit_reference(self
, node
):
2873 # text = node.astext()
2874 if self
.settings
.create_links
:
2875 if 'refuri' in node
:
2876 href
= node
['refuri']
2877 if (self
.settings
.cloak_email_addresses
2878 and href
.startswith('mailto:')):
2879 href
= self
.cloak_mailto(href
)
2880 el
= self
.append_child('text:a', attrib
={
2881 'xlink:href': '%s' % href
,
2882 'xlink:type': 'simple',
2884 self
.set_current_element(el
)
2885 elif 'refid' in node
:
2886 if self
.settings
.create_links
:
2887 href
= node
['refid']
2888 el
= self
.append_child('text:reference-ref', attrib
={
2889 'text:ref-name': '%s' % href
,
2890 'text:reference-format': 'text',
2893 self
.document
.reporter
.warning(
2894 'References must have "refuri" or "refid" attribute.')
2895 if (self
.in_table_of_contents
2896 and len(node
.children
) >= 1
2897 and isinstance(node
.children
[0], docutils
.nodes
.generated
)):
2898 node
.remove(node
.children
[0])
2900 def depart_reference(self
, node
):
2901 if self
.settings
.create_links
:
2902 if 'refuri' in node
:
2903 self
.set_to_parent()
2905 def visit_rubric(self
, node
):
2906 style_name
= self
.rststyle('rubric')
2907 classes
= node
.get('classes')
2912 el
= SubElement(self
.current_element
, 'text:h', attrib
={
2913 # 'text:outline-level': '%d' % section_level,
2914 # 'text:style-name': 'Heading_20_%d' % section_level,
2915 'text:style-name': style_name
,
2917 text
= node
.astext()
2918 el
.text
= self
.encode(text
)
2920 def depart_rubric(self
, node
):
2923 def visit_section(self
, node
, move_ids
=1):
2924 self
.section_level
+= 1
2925 self
.section_count
+= 1
2926 if self
.settings
.create_sections
:
2927 el
= self
.append_child('text:section', attrib
={
2928 'text:name': 'Section%d' % self
.section_count
,
2929 'text:style-name': 'Sect%d' % self
.section_level
,
2931 self
.set_current_element(el
)
2933 def depart_section(self
, node
):
2934 self
.section_level
-= 1
2935 if self
.settings
.create_sections
:
2936 self
.set_to_parent()
2938 def visit_strong(self
, node
):
2939 el
= SubElement(self
.current_element
, 'text:span',
2940 attrib
={'text:style-name': self
.rststyle('strong')})
2941 self
.set_current_element(el
)
2943 def depart_strong(self
, node
):
2944 self
.set_to_parent()
2946 def visit_substitution_definition(self
, node
):
2947 raise nodes
.SkipChildren()
2949 def depart_substitution_definition(self
, node
):
2952 def visit_system_message(self
, node
):
2955 def depart_system_message(self
, node
):
2958 def get_table_style(self
, node
):
2961 str_classes
= node
.get('classes')
2962 if str_classes
is not None:
2963 for str_class
in str_classes
:
2964 if str_class
.startswith(TABLESTYLEPREFIX
):
2965 table_name
= str_class
2967 if table_name
is not None:
2968 table_style
= self
.table_styles
.get(table_name
)
2969 if table_style
is None:
2970 # If we can't find the table style, issue warning
2971 # and use the default table style.
2972 self
.document
.reporter
.warning(
2973 'Can\'t find table style "%s". Using default.' % (
2975 table_name
= TABLENAMEDEFAULT
2976 table_style
= self
.table_styles
.get(table_name
)
2977 if table_style
is None:
2978 # If we can't find the default table style, issue a warning
2979 # and use a built-in default style.
2980 self
.document
.reporter
.warning(
2981 'Can\'t find default table style "%s". '
2982 'Using built-in default.' % (
2984 table_style
= BUILTIN_DEFAULT_TABLE_STYLE
2986 table_name
= TABLENAMEDEFAULT
2987 table_style
= self
.table_styles
.get(table_name
)
2988 if table_style
is None:
2989 # If we can't find the default table style, issue a warning
2990 # and use a built-in default style.
2991 self
.document
.reporter
.warning(
2992 'Can\'t find default table style "%s". '
2993 'Using built-in default.' % (
2995 table_style
= BUILTIN_DEFAULT_TABLE_STYLE
2998 def visit_table(self
, node
):
2999 self
.table_count
+= 1
3000 table_style
= self
.get_table_style(node
)
3001 table_name
= '%s%%d' % TABLESTYLEPREFIX
3002 el1
= SubElement(self
.automatic_styles
, 'style:style', attrib
={
3003 'style:name': self
.rststyle(
3004 '%s' % table_name
, (self
.table_count
, )),
3005 'style:family': 'table',
3007 if table_style
.backgroundcolor
is None:
3008 SubElement(el1
, 'style:table-properties', attrib
={
3009 # 'style:width': '17.59cm',
3010 # 'table:align': 'margins',
3011 'table:align': 'left',
3012 'fo:margin-top': '0in',
3013 'fo:margin-bottom': '0.10in',
3016 SubElement(el1
, 'style:table-properties', attrib
={
3017 # 'style:width': '17.59cm',
3018 'table:align': 'margins',
3019 'fo:margin-top': '0in',
3020 'fo:margin-bottom': '0.10in',
3021 'fo:background-color': table_style
.backgroundcolor
,
3023 # We use a single cell style for all cells in this table.
3024 # That's probably not correct, but seems to work.
3025 el2
= SubElement(self
.automatic_styles
, 'style:style', attrib
={
3026 'style:name': self
.rststyle(
3027 '%s.%%c%%d' % table_name
, (self
.table_count
, 'A', 1, )),
3028 'style:family': 'table-cell',
3030 thickness
= self
.settings
.table_border_thickness
3031 if thickness
is None:
3032 line_style1
= table_style
.border
3034 line_style1
= '0.%03dcm solid #000000' % (thickness
, )
3035 SubElement(el2
, 'style:table-cell-properties', attrib
={
3036 'fo:padding': '0.049cm',
3037 'fo:border-left': line_style1
,
3038 'fo:border-right': line_style1
,
3039 'fo:border-top': line_style1
,
3040 'fo:border-bottom': line_style1
,
3043 for child
in node
.children
:
3044 if child
.tagname
== 'title':
3045 title
= child
.astext()
3047 if title
is not None:
3048 self
.append_p('table-title', title
)
3051 el4
= SubElement(self
.current_element
, 'table:table', attrib
={
3052 'table:name': self
.rststyle(
3053 '%s' % table_name
, (self
.table_count
, )),
3054 'table:style-name': self
.rststyle(
3055 '%s' % table_name
, (self
.table_count
, )),
3057 self
.set_current_element(el4
)
3058 self
.current_table_style
= el1
3059 self
.table_width
= 0.0
3061 def depart_table(self
, node
):
3062 attribkey
= add_ns('style:width', nsdict
=SNSD
)
3063 attribval
= '%.4fin' % (self
.table_width
, )
3064 el1
= self
.current_table_style
3066 el2
.attrib
[attribkey
] = attribval
3067 self
.set_to_parent()
3069 def visit_tgroup(self
, node
):
3070 self
.column_count
= ord('A') - 1
3072 def depart_tgroup(self
, node
):
3075 def visit_colspec(self
, node
):
3076 self
.column_count
+= 1
3077 colspec_name
= self
.rststyle(
3078 '%s%%d.%%s' % TABLESTYLEPREFIX
,
3079 (self
.table_count
, chr(self
.column_count
), )
3081 colwidth
= node
['colwidth'] / 12.0
3082 el1
= SubElement(self
.automatic_styles
, 'style:style', attrib
={
3083 'style:name': colspec_name
,
3084 'style:family': 'table-column',
3086 SubElement(el1
, 'style:table-column-properties',
3087 attrib
={'style:column-width': '%.4fin' % colwidth
},
3089 self
.append_child('table:table-column',
3090 attrib
={'table:style-name': colspec_name
, })
3091 self
.table_width
+= colwidth
3093 def depart_colspec(self
, node
):
3096 def visit_thead(self
, node
):
3097 el
= self
.append_child('table:table-header-rows')
3098 self
.set_current_element(el
)
3099 self
.in_thead
= True
3100 self
.paragraph_style_stack
.append('Table_20_Heading')
3102 def depart_thead(self
, node
):
3103 self
.set_to_parent()
3104 self
.in_thead
= False
3105 self
.paragraph_style_stack
.pop()
3107 def visit_row(self
, node
):
3108 self
.column_count
= ord('A') - 1
3109 el
= self
.append_child('table:table-row')
3110 self
.set_current_element(el
)
3112 def depart_row(self
, node
):
3113 self
.set_to_parent()
3115 def visit_entry(self
, node
):
3116 self
.column_count
+= 1
3117 cellspec_name
= self
.rststyle(
3118 '%s%%d.%%c%%d' % TABLESTYLEPREFIX
,
3119 (self
.table_count
, 'A', 1, )
3122 'table:style-name': cellspec_name
,
3123 'office:value-type': 'string',
3125 morecols
= node
.get('morecols', 0)
3127 attrib
['table:number-columns-spanned'] = '%d' % (morecols
+ 1,)
3128 self
.column_count
+= morecols
3129 morerows
= node
.get('morerows', 0)
3131 attrib
['table:number-rows-spanned'] = '%d' % (morerows
+ 1,)
3132 el1
= self
.append_child('table:table-cell', attrib
=attrib
)
3133 self
.set_current_element(el1
)
3135 def depart_entry(self
, node
):
3136 self
.set_to_parent()
3138 def visit_tbody(self
, node
):
3141 def depart_tbody(self
, node
):
3144 def visit_target(self
, node
):
3146 # I don't know how to implement targets in ODF.
3147 # How do we create a target in oowriter? A cross-reference?
3148 if ('refuri' not in node
3149 and 'refid' not in node
3150 and 'refname' not in node
):
3155 def depart_target(self
, node
):
3158 def visit_title(self
, node
, move_ids
=1, title_type
='title'):
3159 if isinstance(node
.parent
, docutils
.nodes
.section
):
3160 section_level
= self
.section_level
3161 if section_level
> 7:
3162 self
.document
.reporter
.warning(
3163 'Heading/section levels greater than 7 not supported.')
3164 self
.document
.reporter
.warning(
3165 ' Reducing to heading level 7 for heading: "%s"' % (
3168 el1
= self
.append_child(
3170 'text:outline-level': '%d' % section_level
,
3171 # 'text:style-name': 'Heading_20_%d' % section_level,
3172 'text:style-name': self
.rststyle(
3173 'heading%d', (section_level
, )),
3175 self
.append_pending_ids(el1
)
3176 self
.set_current_element(el1
)
3177 elif isinstance(node
.parent
, docutils
.nodes
.document
):
3178 # text = self.settings.title
3180 # text = node.astext()
3181 el1
= SubElement(self
.current_element
, 'text:p', attrib
={
3182 'text:style-name': self
.rststyle(title_type
),
3184 self
.append_pending_ids(el1
)
3185 text
= node
.astext()
3187 self
.found_doc_title
= True
3188 self
.set_current_element(el1
)
3190 def depart_title(self
, node
):
3191 if (isinstance(node
.parent
, docutils
.nodes
.section
)
3192 or isinstance(node
.parent
, docutils
.nodes
.document
)):
3193 self
.set_to_parent()
3195 def visit_subtitle(self
, node
, move_ids
=1):
3196 self
.visit_title(node
, move_ids
, title_type
='subtitle')
3198 def depart_subtitle(self
, node
):
3199 self
.depart_title(node
)
3201 def visit_title_reference(self
, node
):
3202 el
= self
.append_child('text:span', attrib
={
3203 'text:style-name': self
.rststyle('quotation')})
3204 el
.text
= self
.encode(node
.astext())
3205 raise nodes
.SkipChildren()
3207 def depart_title_reference(self
, node
):
3210 def generate_table_of_content_entry_template(self
, el1
):
3211 for idx
in range(1, 11):
3214 'text:table-of-content-entry-template',
3216 'text:outline-level': "%d" % (idx
, ),
3217 'text:style-name': self
.rststyle('contents-%d' % (idx
, )),
3219 SubElement(el2
, 'text:index-entry-chapter')
3220 SubElement(el2
, 'text:index-entry-text')
3221 SubElement(el2
, 'text:index-entry-tab-stop', attrib
={
3222 'style:leader-char': ".",
3223 'style:type': "right",
3225 SubElement(el2
, 'text:index-entry-page-number')
3227 def find_title_label(self
, node
, class_type
, label_key
):
3230 for child
in node
.children
:
3231 if isinstance(child
, class_type
):
3234 if title_node
is not None:
3235 label
= title_node
.astext()
3237 label
= self
.language
.labels
[label_key
]
3240 def visit_topic(self
, node
):
3241 if 'classes' in node
.attributes
:
3242 if 'contents' in node
.attributes
['classes']:
3243 label
= self
.find_title_label(
3244 node
, docutils
.nodes
.title
, 'contents')
3245 if self
.settings
.generate_oowriter_toc
:
3246 el1
= self
.append_child('text:table-of-content', attrib
={
3247 'text:name': 'Table of Contents1',
3248 'text:protected': 'true',
3249 'text:style-name': 'Sect1',
3253 'text:table-of-content-source',
3255 'text:outline-level': '10',
3257 el3
= SubElement(el2
, 'text:index-title-template', attrib
={
3258 'text:style-name': 'Contents_20_Heading',
3261 self
.generate_table_of_content_entry_template(el2
)
3262 el4
= SubElement(el1
, 'text:index-body')
3263 el5
= SubElement(el4
, 'text:index-title')
3264 el6
= SubElement(el5
, 'text:p', attrib
={
3265 'text:style-name': self
.rststyle('contents-heading'),
3268 self
.save_current_element
= self
.current_element
3269 self
.table_of_content_index_body
= el4
3270 self
.set_current_element(el4
)
3272 el
= self
.append_p('horizontalline')
3273 el
= self
.append_p('centeredtextbody')
3276 attrib
={'text:style-name': self
.rststyle('strong')})
3278 self
.in_table_of_contents
= True
3279 elif 'abstract' in node
.attributes
['classes']:
3280 el
= self
.append_p('horizontalline')
3281 el
= self
.append_p('centeredtextbody')
3284 attrib
={'text:style-name': self
.rststyle('strong')})
3285 label
= self
.find_title_label(
3286 node
, docutils
.nodes
.title
,
3289 elif 'dedication' in node
.attributes
['classes']:
3290 el
= self
.append_p('horizontalline')
3291 el
= self
.append_p('centeredtextbody')
3294 attrib
={'text:style-name': self
.rststyle('strong')})
3295 label
= self
.find_title_label(
3296 node
, docutils
.nodes
.title
,
3300 def depart_topic(self
, node
):
3301 if 'classes' in node
.attributes
:
3302 if 'contents' in node
.attributes
['classes']:
3303 if self
.settings
.generate_oowriter_toc
:
3304 self
.update_toc_page_numbers(
3305 self
.table_of_content_index_body
)
3306 self
.set_current_element(self
.save_current_element
)
3308 self
.append_p('horizontalline')
3309 self
.in_table_of_contents
= False
3311 def update_toc_page_numbers(self
, el
):
3313 self
.update_toc_collect(el
, 0, collection
)
3314 self
.update_toc_add_numbers(collection
)
3316 def update_toc_collect(self
, el
, level
, collection
):
3317 collection
.append((level
, el
))
3320 if child_el
.tag
!= 'text:index-body':
3321 self
.update_toc_collect(child_el
, level
, collection
)
3323 def update_toc_add_numbers(self
, collection
):
3324 for level
, el1
in collection
:
3325 if (el1
.tag
== 'text:p'
3326 and el1
.text
!= 'Table of Contents'):
3327 el2
= SubElement(el1
, 'text:tab')
3330 def visit_transition(self
, node
):
3331 self
.append_p('horizontalline')
3333 def depart_transition(self
, node
):
3339 def visit_warning(self
, node
):
3340 self
.generate_admonition(node
, 'warning')
3342 def depart_warning(self
, node
):
3343 self
.paragraph_style_stack
.pop()
3345 def visit_attention(self
, node
):
3346 self
.generate_admonition(node
, 'attention')
3348 depart_attention
= depart_warning
3350 def visit_caution(self
, node
):
3351 self
.generate_admonition(node
, 'caution')
3353 depart_caution
= depart_warning
3355 def visit_danger(self
, node
):
3356 self
.generate_admonition(node
, 'danger')
3358 depart_danger
= depart_warning
3360 def visit_error(self
, node
):
3361 self
.generate_admonition(node
, 'error')
3363 depart_error
= depart_warning
3365 def visit_hint(self
, node
):
3366 self
.generate_admonition(node
, 'hint')
3368 depart_hint
= depart_warning
3370 def visit_important(self
, node
):
3371 self
.generate_admonition(node
, 'important')
3373 depart_important
= depart_warning
3375 def visit_note(self
, node
):
3376 self
.generate_admonition(node
, 'note')
3378 depart_note
= depart_warning
3380 def visit_tip(self
, node
):
3381 self
.generate_admonition(node
, 'tip')
3383 depart_tip
= depart_warning
3385 def visit_admonition(self
, node
):
3387 for child
in node
.children
:
3388 if child
.tagname
== 'title':
3389 title
= child
.astext()
3391 classes1
= node
.get('classes')
3394 self
.generate_admonition(node
, 'generic', title
)
3396 depart_admonition
= depart_warning
3398 def generate_admonition(self
, node
, label
, title
=None):
3399 if hasattr(self
.language
, 'labels'):
3400 translated_label
= self
.language
.labels
.get(label
, label
)
3402 translated_label
= label
3403 el1
= SubElement(self
.current_element
, 'text:p', attrib
={
3404 'text:style-name': self
.rststyle(
3405 'admon-%s-hdr', (label
, )),
3410 el1
.text
= '%s!' % (translated_label
.capitalize(), )
3411 s1
= self
.rststyle('admon-%s-body', (label
, ))
3412 self
.paragraph_style_stack
.append(s1
)
3415 # Roles (e.g. subscript, superscript, strong, ...
3417 def visit_subscript(self
, node
):
3418 el
= self
.append_child('text:span', attrib
={
3419 'text:style-name': 'rststyle-subscript',
3421 self
.set_current_element(el
)
3423 def depart_subscript(self
, node
):
3424 self
.set_to_parent()
3426 def visit_superscript(self
, node
):
3427 el
= self
.append_child('text:span', attrib
={
3428 'text:style-name': 'rststyle-superscript',
3430 self
.set_current_element(el
)
3432 def depart_superscript(self
, node
):
3433 self
.set_to_parent()
3435 def visit_abbreviation(self
, node
):
3438 def depart_abbreviation(self
, node
):
3441 def visit_acronym(self
, node
):
3444 def depart_acronym(self
, node
):
3447 def visit_sidebar(self
, node
):
3450 def depart_sidebar(self
, node
):
3454 # Use an own reader to modify transformations done.
3455 class Reader(standalone
.Reader
):
3457 def get_transforms(self
):
3458 transforms
= super().get_transforms()
3459 if not self
.settings
.create_links
:
3460 transforms
.remove(references
.DanglingReferences
)