aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/docutils/writers
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/docutils/writers
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/docutils/writers')
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/__init__.py159
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/_html_base.py1887
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/docutils_xml.py187
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html4css1/__init__.py955
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html4css1/html4css1.css350
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html4css1/template.txt8
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/__init__.py393
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/italic-field-names.css26
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/math.css332
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/minimal.css293
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/plain.css307
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/responsive.css486
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/template.txt8
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/tuftig.css566
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/__init__.py3323
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/default.tex14
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/docutils.sty223
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlepage.tex19
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlingpage.tex18
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/latex2e/xelatex.tex21
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/manpage.py1214
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/null.py25
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py3461
-rwxr-xr-x.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/prepstyles.py78
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/pygmentsformatter.py109
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/styles.odtbin0 -> 16500 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/pep_html/__init__.py101
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/pep_html/pep.css344
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/pep_html/template.txt25
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/pseudoxml.py40
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/__init__.py353
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/README.txt6
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/__base__2
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/framing.css25
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/pretty.css109
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/framing.css24
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/pretty.css107
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/framing.css25
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/opera.css8
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/outline.css16
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/pretty.css120
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/print.css24
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/s5-core.css11
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.css10
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.js558
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/__base__2
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/pretty.css115
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/framing.css24
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/pretty.css113
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/__base__2
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/pretty.css116
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/framing.css24
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/pretty.css114
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/writers/xetex/__init__.py147
54 files changed, 17027 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/__init__.py
new file mode 100644
index 00000000..eb6d3d27
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/__init__.py
@@ -0,0 +1,159 @@
+# $Id: __init__.py 9368 2023-04-28 21:26:36Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+This package contains Docutils Writer modules.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from importlib import import_module
+
+import docutils
+from docutils import languages, Component
+from docutils.transforms import universal
+
+
+class Writer(Component):
+
+ """
+ Abstract base class for docutils Writers.
+
+ Each writer module or package must export a subclass also called 'Writer'.
+ Each writer must support all standard node types listed in
+ `docutils.nodes.node_class_names`.
+
+ The `write()` method is the main entry point.
+ """
+
+ component_type = 'writer'
+ config_section = 'writers'
+
+ def get_transforms(self):
+ return super().get_transforms() + [universal.Messages,
+ universal.FilterMessages,
+ universal.StripClassesAndElements]
+
+ document = None
+ """The document to write (Docutils doctree); set by `write()`."""
+
+ output = None
+ """Final translated form of `document`
+
+ (`str` for text, `bytes` for binary formats); set by `translate()`.
+ """
+
+ language = None
+ """Language module for the document; set by `write()`."""
+
+ destination = None
+ """`docutils.io` Output object; where to write the document.
+
+ Set by `write()`.
+ """
+
+ def __init__(self):
+
+ self.parts = {}
+ """Mapping of document part names to fragments of `self.output`.
+
+ See `Writer.assemble_parts()` below and
+ <https://docutils.sourceforge.io/docs/api/publisher.html>.
+ """
+
+ def write(self, document, destination):
+ """
+ Process a document into its final form.
+
+ Translate `document` (a Docutils document tree) into the Writer's
+ native format, and write it out to its `destination` (a
+ `docutils.io.Output` subclass object).
+
+ Normally not overridden or extended in subclasses.
+ """
+ self.document = document
+ self.language = languages.get_language(
+ document.settings.language_code,
+ document.reporter)
+ self.destination = destination
+ self.translate()
+ return self.destination.write(self.output)
+
+ def translate(self):
+ """
+ Do final translation of `self.document` into `self.output`. Called
+ from `write`. Override in subclasses.
+
+ Usually done with a `docutils.nodes.NodeVisitor` subclass, in
+ combination with a call to `docutils.nodes.Node.walk()` or
+ `docutils.nodes.Node.walkabout()`. The ``NodeVisitor`` subclass must
+ support all standard elements (listed in
+ `docutils.nodes.node_class_names`) and possibly non-standard elements
+ used by the current Reader as well.
+ """
+ raise NotImplementedError('subclass must override this method')
+
+ def assemble_parts(self):
+ """Assemble the `self.parts` dictionary. Extend in subclasses.
+
+ See <https://docutils.sourceforge.io/docs/api/publisher.html>.
+ """
+ self.parts['whole'] = self.output
+ self.parts['encoding'] = self.document.settings.output_encoding
+ self.parts['errors'] = (
+ self.document.settings.output_encoding_error_handler)
+ self.parts['version'] = docutils.__version__
+
+
+class UnfilteredWriter(Writer):
+
+ """
+ A writer that passes the document tree on unchanged (e.g. a
+ serializer.)
+
+ Documents written by UnfilteredWriters are typically reused at a
+ later date using a subclass of `readers.ReReader`.
+ """
+
+ def get_transforms(self):
+ # Do not add any transforms. When the document is reused
+ # later, the then-used writer will add the appropriate
+ # transforms.
+ return Component.get_transforms(self)
+
+
+_writer_aliases = {
+ 'html': 'html4css1', # may change to html5 some day
+ 'html4': 'html4css1',
+ 'xhtml10': 'html4css1',
+ 'html5': 'html5_polyglot',
+ 'xhtml': 'html5_polyglot',
+ 's5': 's5_html',
+ 'latex': 'latex2e',
+ 'xelatex': 'xetex',
+ 'luatex': 'xetex',
+ 'lualatex': 'xetex',
+ 'odf': 'odf_odt',
+ 'odt': 'odf_odt',
+ 'ooffice': 'odf_odt',
+ 'openoffice': 'odf_odt',
+ 'libreoffice': 'odf_odt',
+ 'pprint': 'pseudoxml',
+ 'pformat': 'pseudoxml',
+ 'pdf': 'rlpdf',
+ 'xml': 'docutils_xml'}
+
+
+def get_writer_class(writer_name):
+ """Return the Writer class from the `writer_name` module."""
+ name = writer_name.lower()
+ name = _writer_aliases.get(name, name)
+ try:
+ module = import_module('docutils.writers.'+name)
+ except ImportError:
+ try:
+ module = import_module(name)
+ except ImportError as err:
+ raise ImportError(f'Writer "{writer_name}" not found. {err}')
+ return module.Writer
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/_html_base.py b/.venv/lib/python3.12/site-packages/docutils/writers/_html_base.py
new file mode 100644
index 00000000..8122f4b2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/_html_base.py
@@ -0,0 +1,1887 @@
+#!/usr/bin/env python3
+# :Author: David Goodger, Günter Milde
+# Based on the html4css1 writer by David Goodger.
+# :Maintainer: docutils-develop@lists.sourceforge.net
+# :Revision: $Revision: 9614 $
+# :Date: $Date: 2005-06-28$
+# :Copyright: © 2016 David Goodger, Günter Milde
+# :License: Released under the terms of the `2-Clause BSD license`_, in short:
+#
+# Copying and distribution of this file, with or without modification,
+# are permitted in any medium without royalty provided the copyright
+# notice and this notice are preserved.
+# This file is offered as-is, without any warranty.
+#
+# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+
+"""common definitions for Docutils HTML writers"""
+
+import base64
+import mimetypes
+import os
+import os.path
+from pathlib import Path
+import re
+import urllib
+import warnings
+import xml.etree.ElementTree as ET # TODO: lazy import in prepare_svg()?
+
+import docutils
+from docutils import frontend, languages, nodes, utils, writers
+from docutils.parsers.rst.directives import length_or_percentage_or_unitless
+from docutils.parsers.rst.directives.images import PIL
+from docutils.transforms import writer_aux
+from docutils.utils.math import (latex2mathml, math2html, tex2mathml_extern,
+ unichar2tex, wrap_math_code, MathError)
+
+
+class Writer(writers.Writer):
+
+ supported = ('html', 'xhtml') # update in subclass
+ """Formats this writer supports."""
+
+ settings_spec = (
+ 'HTML Writer Options',
+ None,
+ (('Specify the template file (UTF-8 encoded). '
+ '(default: writer dependent)',
+ ['--template'],
+ {'metavar': '<file>'}),
+ ('Comma separated list of stylesheet URLs. '
+ 'Overrides previous --stylesheet and --stylesheet-path settings.',
+ ['--stylesheet'],
+ {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
+ 'validator': frontend.validate_comma_separated_list}),
+ ('Comma separated list of stylesheet paths. '
+ 'Relative paths are expanded if a matching file is found in '
+ 'the --stylesheet-dirs. With --link-stylesheet, '
+ 'the path is rewritten relative to the output HTML file. '
+ '(default: writer dependent)',
+ ['--stylesheet-path'],
+ {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
+ 'validator': frontend.validate_comma_separated_list}),
+ ('Comma-separated list of directories where stylesheets are found. '
+ 'Used by --stylesheet-path when expanding relative path arguments. '
+ '(default: writer dependent)',
+ ['--stylesheet-dirs'],
+ {'metavar': '<dir[,dir,...]>',
+ 'validator': frontend.validate_comma_separated_list}),
+ ('Embed the stylesheet(s) in the output HTML file. The stylesheet '
+ 'files must be accessible during processing. (default)',
+ ['--embed-stylesheet'],
+ {'default': True, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Link to the stylesheet(s) in the output HTML file. ',
+ ['--link-stylesheet'],
+ {'dest': 'embed_stylesheet', 'action': 'store_false'}),
+ ('Specify the initial header level. '
+ 'Does not affect document title & subtitle (see --no-doc-title).'
+ '(default: writer dependent).',
+ ['--initial-header-level'],
+ {'choices': '1 2 3 4 5 6'.split(), 'default': '2',
+ 'metavar': '<level>'}),
+ ('Format for footnote references: one of "superscript" or '
+ '"brackets". (default: "brackets")',
+ ['--footnote-references'],
+ {'choices': ['superscript', 'brackets'], 'default': 'brackets',
+ 'metavar': '<format>',
+ 'overrides': 'trim_footnote_reference_space'}),
+ ('Format for block quote attributions: '
+ 'one of "dash" (em-dash prefix), "parentheses"/"parens", or "none". '
+ '(default: "dash")',
+ ['--attribution'],
+ {'choices': ['dash', 'parentheses', 'parens', 'none'],
+ 'default': 'dash', 'metavar': '<format>'}),
+ ('Remove extra vertical whitespace between items of "simple" bullet '
+ 'lists and enumerated lists. (default)',
+ ['--compact-lists'],
+ {'default': True, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Disable compact simple bullet and enumerated lists.',
+ ['--no-compact-lists'],
+ {'dest': 'compact_lists', 'action': 'store_false'}),
+ ('Remove extra vertical whitespace between items of simple field '
+ 'lists. (default)',
+ ['--compact-field-lists'],
+ {'default': True, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Disable compact simple field lists.',
+ ['--no-compact-field-lists'],
+ {'dest': 'compact_field_lists', 'action': 'store_false'}),
+ ('Added to standard table classes. '
+ 'Defined styles: borderless, booktabs, '
+ 'align-left, align-center, align-right, '
+ 'colwidths-auto, colwidths-grid.',
+ ['--table-style'],
+ {'default': ''}),
+ ('Math output format (one of "MathML", "HTML", "MathJax", '
+ 'or "LaTeX") and option(s). '
+ '(default: "HTML math.css")',
+ ['--math-output'],
+ {'default': 'HTML math.css',
+ 'validator': frontend.validate_math_output}),
+ ('Prepend an XML declaration. ',
+ ['--xml-declaration'],
+ {'default': False, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Omit the XML declaration.',
+ ['--no-xml-declaration'],
+ {'dest': 'xml_declaration', 'action': 'store_false'}),
+ ('Obfuscate email addresses to confuse harvesters while still '
+ 'keeping email links usable with standards-compliant browsers.',
+ ['--cloak-email-addresses'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ )
+ )
+
+ settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
+
+ relative_path_settings = ('template',)
+
+ config_section = 'html base writer' # overwrite in subclass
+ config_section_dependencies = ('writers', 'html writers')
+
+ visitor_attributes = (
+ 'head_prefix', 'head', 'stylesheet', 'body_prefix',
+ 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
+ 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
+ 'html_prolog', 'html_head', 'html_title', 'html_subtitle',
+ 'html_body')
+
+ def get_transforms(self):
+ return super().get_transforms() + [writer_aux.Admonitions]
+
+ def translate(self):
+ self.visitor = visitor = self.translator_class(self.document)
+ self.document.walkabout(visitor)
+ for attr in self.visitor_attributes:
+ setattr(self, attr, getattr(visitor, attr))
+ self.output = self.apply_template()
+
+ def apply_template(self):
+ with open(self.document.settings.template, encoding='utf-8') as fp:
+ template = fp.read()
+ subs = self.interpolation_dict()
+ return template % subs
+
+ def interpolation_dict(self):
+ subs = {}
+ settings = self.document.settings
+ for attr in self.visitor_attributes:
+ subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
+ subs['encoding'] = settings.output_encoding
+ subs['version'] = docutils.__version__
+ return subs
+
+ def assemble_parts(self):
+ writers.Writer.assemble_parts(self)
+ for part in self.visitor_attributes:
+ self.parts[part] = ''.join(getattr(self, part))
+
+
+class HTMLTranslator(nodes.NodeVisitor):
+
+ """
+ Generic Docutils to HTML translator.
+
+ See the `html4css1` and `html5_polyglot` writers for full featured
+ HTML writers.
+
+ .. IMPORTANT::
+ The `visit_*` and `depart_*` methods use a
+ heterogeneous stack, `self.context`.
+ When subclassing, make sure to be consistent in its use!
+
+ Examples for robust coding:
+
+ a) Override both `visit_*` and `depart_*` methods, don't call the
+ parent functions.
+
+ b) Extend both and unconditionally call the parent functions::
+
+ def visit_example(self, node):
+ if foo:
+ self.body.append('<div class="foo">')
+ html4css1.HTMLTranslator.visit_example(self, node)
+
+ def depart_example(self, node):
+ html4css1.HTMLTranslator.depart_example(self, node)
+ if foo:
+ self.body.append('</div>')
+
+ c) Extend both, calling the parent functions under the same
+ conditions::
+
+ def visit_example(self, node):
+ if foo:
+ self.body.append('<div class="foo">\n')
+ else: # call the parent method
+ _html_base.HTMLTranslator.visit_example(self, node)
+
+ def depart_example(self, node):
+ if foo:
+ self.body.append('</div>\n')
+ else: # call the parent method
+ _html_base.HTMLTranslator.depart_example(self, node)
+
+ d) Extend one method (call the parent), but don't otherwise use the
+ `self.context` stack::
+
+ def depart_example(self, node):
+ _html_base.HTMLTranslator.depart_example(self, node)
+ if foo:
+ # implementation-specific code
+ # that does not use `self.context`
+ self.body.append('</div>\n')
+
+ This way, changes in stack use will not bite you.
+ """
+
+ doctype = '<!DOCTYPE html>\n'
+ doctype_mathml = doctype
+
+ head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
+ ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
+ content_type = '<meta charset="%s" />\n'
+ generator = (
+ f'<meta name="generator" content="Docutils {docutils.__version__}: '
+ 'https://docutils.sourceforge.io/" />\n')
+ # `starttag()` arguments for the main document (HTML5 uses <main>)
+ documenttag_args = {'tagname': 'div', 'CLASS': 'document'}
+
+ # Template for the MathJax script in the header:
+ mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
+
+ mathjax_url = 'file:/usr/share/javascript/mathjax/MathJax.js'
+ """
+ URL of the MathJax javascript library.
+
+ The MathJax library ought to be installed on the same
+ server as the rest of the deployed site files and specified
+ in the `math-output` setting appended to "mathjax".
+ See `Docutils Configuration`__.
+
+ __ https://docutils.sourceforge.io/docs/user/config.html#math-output
+
+ The fallback tries a local MathJax installation at
+ ``/usr/share/javascript/mathjax/MathJax.js``.
+ """
+
+ stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
+ embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
+ words_and_spaces = re.compile(r'[^ \n]+| +|\n')
+ # wrap point inside word:
+ in_word_wrap_point = re.compile(r'.+\W\W.+|[-?].+')
+ lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
+
+ special_characters = {ord('&'): '&amp;',
+ ord('<'): '&lt;',
+ ord('"'): '&quot;',
+ ord('>'): '&gt;',
+ ord('@'): '&#64;', # may thwart address harvesters
+ }
+ """Character references for characters with a special meaning in HTML."""
+
+ videotypes = ('video/mp4', 'video/webm', 'video/ogg')
+ """MIME types supported by the HTML5 <video> element."""
+
+ def __init__(self, document):
+ nodes.NodeVisitor.__init__(self, document)
+ # process settings
+ self.settings = settings = document.settings
+ self.language = languages.get_language(
+ settings.language_code, document.reporter)
+ self.initial_header_level = int(settings.initial_header_level)
+ # image_loading (only defined for HTML5 writer)
+ _image_loading_default = 'link'
+ # convert legacy setting embed_images:
+ if getattr(settings, 'embed_images', None) is not None:
+ if settings.embed_images:
+ _image_loading_default = 'embed'
+ warnings.warn('The configuration setting "embed_images"\n'
+ ' will be removed in Docutils 2.0. '
+ f'Use "image_loading: {_image_loading_default}".',
+ FutureWarning, stacklevel=8)
+ self.image_loading = getattr(settings,
+ 'image_loading', _image_loading_default)
+ # backwards compatibiltiy: validate/convert programatically set strings
+ if isinstance(self.settings.math_output, str):
+ self.settings.math_output = frontend.validate_math_output(
+ self.settings.math_output)
+ (self.math_output,
+ self.math_options) = self.settings.math_output
+
+ # set up "parts" (cf. docs/api/publisher.html#publish-parts-details)
+ #
+ self.body = [] # equivalent to `fragment`, ≠ `html_body`
+ self.body_prefix = ['</head>\n<body>\n'] # + optional header
+ self.body_pre_docinfo = [] # document heading (title and subtitle)
+ self.body_suffix = ['</body>\n</html>\n'] # + optional footer
+ self.docinfo = []
+ self.footer = []
+ self.fragment = [] # main content of the document ("naked" body)
+ self.head = []
+ self.head_prefix = [] # everything up to and including <head>
+ self.header = []
+ self.html_body = []
+ self.html_head = [self.content_type] # charset not interpolated
+ self.html_prolog = []
+ self.html_subtitle = []
+ self.html_title = []
+ self.meta = [self.generator]
+ self.stylesheet = [self.stylesheet_call(path)
+ for path in utils.get_stylesheet_list(settings)]
+ self.title = []
+ self.subtitle = []
+ if settings.xml_declaration:
+ self.head_prefix.append(
+ utils.xml_declaration(settings.output_encoding))
+ self.html_prolog.append(
+ utils.xml_declaration('%s')) # encoding not interpolated
+ if (settings.output_encoding
+ and settings.output_encoding.lower() != 'unicode'):
+ self.meta.insert(0, self.content_type % settings.output_encoding)
+
+ # bookkeeping attributes; reflect state of translator
+ #
+ self.context = []
+ """Heterogeneous stack.
+
+ Used by visit_* and depart_* functions in conjunction with the tree
+ traversal. Make sure that the pops correspond to the pushes.
+ """
+ self.section_level = 0
+ self.colspecs = []
+ self.compact_p = True
+ self.compact_simple = False
+ self.compact_field_list = False
+ self.in_docinfo = False
+ self.in_sidebar = False
+ self.in_document_title = 0 # len(self.body) or 0
+ self.in_mailto = False
+ self.author_in_authors = False # for html4css1
+ self.math_header = []
+ self.messages = []
+ """Queue of system_message nodes (writing issues).
+
+ Call `report_messages()` in `depart_*_block()` methods to clean up!
+ """
+
+ def astext(self):
+ return ''.join(self.head_prefix + self.head
+ + self.stylesheet + self.body_prefix
+ + self.body_pre_docinfo + self.docinfo
+ + self.body + self.body_suffix)
+
+ def attval(self, text,
+ whitespace=re.compile('[\n\r\t\v\f]')):
+ """Cleanse, HTML encode, and return attribute value text."""
+ encoded = self.encode(whitespace.sub(' ', text))
+ if self.in_mailto and self.settings.cloak_email_addresses:
+ # Cloak at-signs ("%40") and periods with HTML entities.
+ encoded = encoded.replace('%40', '&#37;&#52;&#48;')
+ encoded = encoded.replace('.', '&#46;')
+ return encoded
+
+ def cloak_email(self, addr):
+ """Try to hide the link text of a email link from harversters."""
+ # Surround at-signs and periods with <span> tags. ("@" has
+ # already been encoded to "&#64;" by the `encode` method.)
+ addr = addr.replace('&#64;', '<span>&#64;</span>')
+ return addr.replace('.', '<span>&#46;</span>')
+
+ def cloak_mailto(self, uri):
+ """Try to hide a mailto: URL from harvesters."""
+ # Encode "@" using a URL octet reference (see RFC 1738).
+ # Further cloaking with HTML entities will be done in the
+ # `attval` function.
+ return uri.replace('@', '%40')
+
+ def encode(self, text):
+ """Encode special characters in `text` & return."""
+ # Use only named entities known in both XML and HTML
+ # other characters are automatically encoded "by number" if required.
+ # @@@ A codec to do these and all other HTML entities would be nice.
+ text = str(text)
+ return text.translate(self.special_characters)
+
+ def image_size(self, node):
+ # Determine the image size from the node arguments or the image file.
+ # Return a size declaration suitable as "style" argument value,
+ # e.g., ``'width: 4px; height: 2em;'``.
+ # TODO: consider feature-request #102?
+ size = [node.get('width', None), node.get('height', None)]
+ if 'scale' in node:
+ if 'width' not in node or 'height' not in node:
+ # try reading size from image file
+ reading_problems = []
+ uri = node['uri']
+ if not PIL:
+ reading_problems.append('Requires Python Imaging Library.')
+ if mimetypes.guess_type(uri)[0] in self.videotypes:
+ reading_problems.append('PIL cannot read video images.')
+ if not self.settings.file_insertion_enabled:
+ reading_problems.append('Reading external files disabled.')
+ if not reading_problems:
+ try:
+ imagepath = self.uri2imagepath(uri)
+ with PIL.Image.open(imagepath) as img:
+ imgsize = img.size
+ except (ValueError, OSError, UnicodeEncodeError) as err:
+ reading_problems.append(str(err))
+ else:
+ self.settings.record_dependencies.add(
+ imagepath.replace('\\', '/'))
+ if reading_problems:
+ msg = ['Cannot scale image!',
+ f'Could not get size from "{uri}":',
+ *reading_problems]
+ self.messages.append(self.document.reporter.warning(
+ '\n '.join(msg), base_node=node))
+ else:
+ for i in range(2):
+ size[i] = size[i] or '%dpx' % imgsize[i]
+ # scale provided/determined size values:
+ factor = float(node['scale']) / 100
+ for i in range(2):
+ if size[i]:
+ match = re.match(r'([0-9.]+)(\S*)$', size[i])
+ size[i] = '%s%s' % (factor * float(match.group(1)),
+ match.group(2))
+ size_declarations = []
+ for i, dimension in enumerate(('width', 'height')):
+ if size[i]:
+ # Interpret unitless values as pixels:
+ if re.match(r'^[0-9.]+$', size[i]):
+ size[i] += 'px'
+ size_declarations.append(f'{dimension}: {size[i]};')
+ return ' '.join(size_declarations)
+
+ def prepare_svg(self, node, imagedata, size_declaration):
+ # Edit `imagedata` for embedding as SVG image.
+ # Use ElementTree to add node attributes.
+ # ET also removes comments and preamble code.
+ #
+ # Provisional:
+ # interface and behaviour may change without notice.
+
+ # SVG namespace
+ svg_ns = {'': 'http://www.w3.org/2000/svg',
+ 'xlink': 'http://www.w3.org/1999/xlink'}
+ # don't add SVG namespace to all elements
+ ET.register_namespace('', svg_ns[''])
+ ET.register_namespace('xlink', svg_ns['xlink'])
+ try:
+ svg = ET.fromstring(imagedata.decode('utf-8'))
+ except ET.ParseError as err:
+ self.messages.append(self.document.reporter.error(
+ f'Cannot parse SVG image "{node["uri"]}":\n {err}',
+ base_node=node))
+ return imagedata.decode('utf-8')
+ # apply image node attributes:
+ if size_declaration: # append to style, replacing width & height
+ declarations = [d.strip() for d in svg.get('style', '').split(';')]
+ declarations = [d for d in declarations
+ if d
+ and not d.startswith('width')
+ and not d.startswith('height')]
+ svg.set('style', '; '.join(declarations+[size_declaration]))
+ if node['classes'] or 'align' in node:
+ classes = svg.get('class', '').split()
+ classes += node.get('classes', [])
+ if 'align' in node:
+ classes.append(f'align-{node["align"]}')
+ svg.set('class', ' '.join(classes))
+ if 'alt' in node and svg.find('title', svg_ns) is None:
+ svg_title = ET.Element('title')
+ svg_title.text = node['alt']
+ svg.insert(0, svg_title)
+ return ET.tostring(svg, encoding='unicode')
+
+ def stylesheet_call(self, path, adjust_path=None):
+ """Return code to reference or embed stylesheet file `path`"""
+ if adjust_path is None:
+ adjust_path = bool(self.settings.stylesheet_path)
+ if self.settings.embed_stylesheet:
+ try:
+ with open(path, encoding='utf-8') as f:
+ content = f.read()
+ except OSError as err:
+ msg = f'Cannot embed stylesheet: {err}'
+ self.document.reporter.error(msg)
+ return '<--- %s --->\n' % msg
+ else:
+ self.settings.record_dependencies.add(path)
+ return self.embedded_stylesheet % content
+ # else link to style file:
+ if adjust_path:
+ # rewrite path relative to output (cf. config.html#stylesheet-path)
+ path = utils.relative_path(self.settings._destination, path)
+ return self.stylesheet_link % self.encode(path)
+
+ def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
+ """
+ Construct and return a start tag given a node (id & class attributes
+ are extracted), tag name, and optional attributes.
+ """
+ tagname = tagname.lower()
+ prefix = []
+ atts = {}
+ for (name, value) in attributes.items():
+ atts[name.lower()] = value
+ classes = atts.pop('classes', [])
+ languages = []
+ # unify class arguments and move language specification
+ for cls in node.get('classes', []) + atts.pop('class', '').split():
+ if cls.startswith('language-'):
+ languages.append(cls[9:])
+ elif cls.strip() and cls not in classes:
+ classes.append(cls)
+ if languages:
+ # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
+ atts[self.lang_attribute] = languages[0]
+ # filter classes that are processed by the writer:
+ internal = ('colwidths-auto', 'colwidths-given', 'colwidths-grid')
+ if isinstance(node, nodes.table):
+ classes = [cls for cls in classes if cls not in internal]
+ if classes:
+ atts['class'] = ' '.join(classes)
+ assert 'id' not in atts
+ ids = node.get('ids', [])
+ ids.extend(atts.pop('ids', []))
+ if ids:
+ atts['id'] = ids[0]
+ for id in ids[1:]:
+ # Add empty "span" elements for additional IDs. Note
+ # that we cannot use empty "a" elements because there
+ # may be targets inside of references, but nested "a"
+ # elements aren't allowed in XHTML (even if they do
+ # not all have a "href" attribute).
+ if empty or isinstance(node, (nodes.Sequential,
+ nodes.docinfo,
+ nodes.table)):
+ # Insert target right in front of element.
+ prefix.append('<span id="%s"></span>' % id)
+ else:
+ # Non-empty tag. Place the auxiliary <span> tag
+ # *inside* the element, as the first child.
+ suffix += '<span id="%s"></span>' % id
+ attlist = sorted(atts.items())
+ parts = [tagname]
+ for name, value in attlist:
+ # value=None was used for boolean attributes without
+ # value, but this isn't supported by XHTML.
+ assert value is not None
+ if isinstance(value, list):
+ values = [str(v) for v in value]
+ parts.append('%s="%s"' % (name.lower(),
+ self.attval(' '.join(values))))
+ else:
+ parts.append('%s="%s"' % (name.lower(),
+ self.attval(str(value))))
+ if empty:
+ infix = ' /'
+ else:
+ infix = ''
+ return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
+
+ def emptytag(self, node, tagname, suffix='\n', **attributes):
+ """Construct and return an XML-compatible empty tag."""
+ return self.starttag(node, tagname, suffix, empty=True, **attributes)
+
+ def report_messages(self, node):
+ if isinstance(node.parent, (nodes.system_message, nodes.entry)):
+ return
+ while self.messages:
+ message = self.messages.pop(0)
+ if self.settings.report_level <= message['level']:
+ message.walkabout(self)
+
+ def set_class_on_child(self, node, class_, index=0):
+ """
+ Set class `class_` on the visible child no. index of `node`.
+ Do nothing if node has fewer children than `index`.
+ """
+ children = [n for n in node if not isinstance(n, nodes.Invisible)]
+ try:
+ child = children[index]
+ except IndexError:
+ return
+ child['classes'].append(class_)
+
+ def uri2imagepath(self, uri):
+ """Get filesystem path corresponding to an URI.
+
+ The image directive expects an image URI. Some writers require the
+ corresponding image path to read the image size from the file or to
+ embed the image in the output.
+
+ Absolute URIs consider the "root_prefix" setting.
+
+ In order to work in the output document, relative image URIs relate
+ to the output directory. For access by the writer, the corresponding
+ image path must be relative to the current working directory.
+
+ Provisional: the function's location, interface and behaviour
+ may change without advance warning.
+ """
+ destination = self.settings._destination or ''
+ uri_parts = urllib.parse.urlparse(uri)
+ if uri_parts.scheme not in ('', 'file'):
+ raise ValueError('Can only read local images.')
+ imagepath = urllib.request.url2pathname(uri_parts.path)
+ if imagepath.startswith('/'):
+ root_prefix = Path(self.settings.root_prefix)
+ imagepath = (root_prefix/imagepath[1:]).as_posix()
+ elif not os.path.isabs(imagepath): # exclude absolute Windows paths
+ destdir = os.path.abspath(os.path.dirname(destination))
+ imagepath = utils.relative_path(None,
+ os.path.join(destdir, imagepath))
+ return imagepath
+
+ def visit_Text(self, node):
+ text = node.astext()
+ encoded = self.encode(text)
+ if self.in_mailto and self.settings.cloak_email_addresses:
+ encoded = self.cloak_email(encoded)
+ self.body.append(encoded)
+
+ def depart_Text(self, node):
+ pass
+
+ def visit_abbreviation(self, node):
+ # @@@ implementation incomplete ("title" attribute)
+ self.body.append(self.starttag(node, 'abbr', ''))
+
+ def depart_abbreviation(self, node):
+ self.body.append('</abbr>')
+
+ def visit_acronym(self, node):
+ # @@@ implementation incomplete ("title" attribute)
+ self.body.append(self.starttag(node, 'acronym', ''))
+
+ def depart_acronym(self, node):
+ self.body.append('</acronym>')
+
+ def visit_address(self, node):
+ self.visit_docinfo_item(node, 'address', meta=False)
+ self.body.append(self.starttag(node, 'pre',
+ suffix='', CLASS='address'))
+
+ def depart_address(self, node):
+ self.body.append('\n</pre>\n')
+ self.depart_docinfo_item()
+
+ def visit_admonition(self, node):
+ self.body.append(self.starttag(node, 'aside', classes=['admonition']))
+
+ def depart_admonition(self, node=None):
+ self.body.append('</aside>\n')
+
+ attribution_formats = {'dash': ('\u2014', ''),
+ 'parentheses': ('(', ')'),
+ 'parens': ('(', ')'),
+ 'none': ('', '')}
+
+ def visit_attribution(self, node):
+ prefix, suffix = self.attribution_formats[self.settings.attribution]
+ self.context.append(suffix)
+ self.body.append(
+ self.starttag(node, 'p', prefix, CLASS='attribution'))
+
+ def depart_attribution(self, node):
+ self.body.append(self.context.pop() + '</p>\n')
+
+ def visit_author(self, node):
+ if not isinstance(node.parent, nodes.authors):
+ self.visit_docinfo_item(node, 'author')
+ self.body.append('<p>')
+
+ def depart_author(self, node):
+ self.body.append('</p>')
+ if isinstance(node.parent, nodes.authors):
+ self.body.append('\n')
+ else:
+ self.depart_docinfo_item()
+
+ def visit_authors(self, node):
+ self.visit_docinfo_item(node, 'authors')
+
+ def depart_authors(self, node):
+ self.depart_docinfo_item()
+
+ def visit_block_quote(self, node):
+ self.body.append(self.starttag(node, 'blockquote'))
+
+ def depart_block_quote(self, node):
+ self.body.append('</blockquote>\n')
+
+ def check_simple_list(self, node):
+ """Check for a simple list that can be rendered compactly."""
+ visitor = SimpleListChecker(self.document)
+ try:
+ node.walk(visitor)
+ except nodes.NodeFound:
+ return False
+ else:
+ return True
+
+ # Compact lists
+ # ------------
+ # Include definition lists and field lists (in addition to ordered
+ # and unordered lists) in the test if a list is "simple" (cf. the
+ # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
+ # the end of this file).
+
+ def is_compactable(self, node):
+ # explicit class arguments have precedence
+ if 'compact' in node['classes']:
+ return True
+ if 'open' in node['classes']:
+ return False
+ # check config setting:
+ if (isinstance(node, (nodes.field_list, nodes.definition_list))
+ and not self.settings.compact_field_lists):
+ return False
+ if (isinstance(node, (nodes.enumerated_list, nodes.bullet_list))
+ and not self.settings.compact_lists):
+ return False
+ # Table of Contents:
+ if 'contents' in node.parent['classes']:
+ return True
+ # check the list items:
+ return self.check_simple_list(node)
+
+ def visit_bullet_list(self, node):
+ atts = {}
+ old_compact_simple = self.compact_simple
+ self.context.append((self.compact_simple, self.compact_p))
+ self.compact_p = None
+ self.compact_simple = self.is_compactable(node)
+ if self.compact_simple and not old_compact_simple:
+ atts['class'] = 'simple'
+ self.body.append(self.starttag(node, 'ul', **atts))
+
+ def depart_bullet_list(self, node):
+ self.compact_simple, self.compact_p = self.context.pop()
+ self.body.append('</ul>\n')
+
+ def visit_caption(self, node):
+ self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
+
+ def depart_caption(self, node):
+ self.body.append('</p>\n')
+
+ # Use semantic tag and DPub role (HTML4 uses a table)
+ def visit_citation(self, node):
+ # role 'doc-bibloentry' requires wrapping in an element with
+ # role 'list' and an element with role 'doc-bibliography'
+ # https://www.w3.org/TR/dpub-aria-1.0/#doc-biblioentry)
+ if not isinstance(node.previous_sibling(), type(node)):
+ self.body.append('<div role="list" class="citation-list">\n')
+ self.body.append(self.starttag(node, 'div', classes=[node.tagname],
+ role="doc-biblioentry"))
+
+ def depart_citation(self, node):
+ self.body.append('</div>\n')
+ if not isinstance(node.next_node(descend=False, siblings=True),
+ type(node)):
+ self.body.append('</div>\n')
+
+ # Use DPub role (overwritten in HTML4)
+ def visit_citation_reference(self, node):
+ href = '#'
+ if 'refid' in node:
+ href += node['refid']
+ elif 'refname' in node:
+ href += self.document.nameids[node['refname']]
+ # else: # TODO system message (or already in the transform)?
+ # 'Citation reference missing.'
+ self.body.append(self.starttag(node, 'a', suffix='[', href=href,
+ classes=['citation-reference'],
+ role='doc-biblioref'))
+
+ def depart_citation_reference(self, node):
+ self.body.append(']</a>')
+
+ def visit_classifier(self, node):
+ self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
+
+ def depart_classifier(self, node):
+ self.body.append('</span>')
+ self.depart_term(node) # close the term element after last classifier
+
+ def visit_colspec(self, node):
+ self.colspecs.append(node)
+ # "stubs" list is an attribute of the tgroup element:
+ node.parent.stubs.append(node.attributes.get('stub'))
+
+ def depart_colspec(self, node):
+ # write out <colgroup> when all colspecs are processed
+ if isinstance(node.next_node(descend=False, siblings=True),
+ nodes.colspec):
+ return
+ if 'colwidths-auto' in node.parent.parent['classes'] or (
+ 'colwidths-grid' not in self.settings.table_style
+ and 'colwidths-given' not in node.parent.parent['classes']):
+ return
+ self.body.append(self.starttag(node, 'colgroup'))
+ total_width = sum(node['colwidth'] for node in self.colspecs)
+ for node in self.colspecs:
+ colwidth = node['colwidth'] / total_width
+ self.body.append(self.emptytag(node, 'col',
+ style=f'width: {colwidth:.1%}'))
+ self.body.append('</colgroup>\n')
+
+ def visit_comment(self, node,
+ sub=re.compile('-(?=-)').sub):
+ """Escape double-dashes in comment text."""
+ self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
+ # Content already processed:
+ raise nodes.SkipNode
+
+ def visit_compound(self, node):
+ self.body.append(self.starttag(node, 'div', CLASS='compound'))
+
+ def depart_compound(self, node):
+ self.body.append('</div>\n')
+
+ def visit_container(self, node):
+ self.body.append(self.starttag(node, 'div',
+ CLASS='docutils container'))
+
+ def depart_container(self, node):
+ self.body.append('</div>\n')
+
+ def visit_contact(self, node):
+ self.visit_docinfo_item(node, 'contact', meta=False)
+
+ def depart_contact(self, node):
+ self.depart_docinfo_item()
+
+ def visit_copyright(self, node):
+ self.visit_docinfo_item(node, 'copyright')
+
+ def depart_copyright(self, node):
+ self.depart_docinfo_item()
+
+ def visit_date(self, node):
+ self.visit_docinfo_item(node, 'date')
+
+ def depart_date(self, node):
+ self.depart_docinfo_item()
+
+ def visit_decoration(self, node):
+ pass
+
+ def depart_decoration(self, node):
+ pass
+
+ def visit_definition(self, node):
+ if 'details' not in node.parent.parent['classes']:
+ self.body.append(self.starttag(node, 'dd', ''))
+
+ def depart_definition(self, node):
+ if 'details' not in node.parent.parent['classes']:
+ self.body.append('</dd>\n')
+
+ def visit_definition_list(self, node):
+ if 'details' in node['classes']:
+ self.body.append(self.starttag(node, 'div'))
+ else:
+ classes = ['simple'] if self.is_compactable(node) else []
+ self.body.append(self.starttag(node, 'dl', classes=classes))
+
+ def depart_definition_list(self, node):
+ if 'details' in node['classes']:
+ self.body.append('</div>\n')
+ else:
+ self.body.append('</dl>\n')
+
+ # Use a "details" disclosure element if parent has "class" arg "details".
+ def visit_definition_list_item(self, node):
+ if 'details' in node.parent['classes']:
+ atts = {}
+ if "open" in node.parent['classes']:
+ atts['open'] = 'open'
+ self.body.append(self.starttag(node, 'details', **atts))
+
+ def depart_definition_list_item(self, node):
+ if 'details' in node.parent['classes']:
+ self.body.append('</details>\n')
+
+ def visit_description(self, node):
+ self.body.append(self.starttag(node, 'dd', ''))
+
+ def depart_description(self, node):
+ self.body.append('</dd>\n')
+
+ def visit_docinfo(self, node):
+ self.context.append(len(self.body))
+ classes = ['docinfo']
+ if self.is_compactable(node):
+ classes.append('simple')
+ self.body.append(self.starttag(node, 'dl', classes=classes))
+
+ def depart_docinfo(self, node):
+ self.body.append('</dl>\n')
+ start = self.context.pop()
+ self.docinfo = self.body[start:]
+ self.body = []
+
+ def visit_docinfo_item(self, node, name, meta=True):
+ if meta:
+ self.meta.append(f'<meta name="{name}" '
+ f'content="{self.attval(node.astext())}" />\n')
+ self.body.append(f'<dt class="{name}">{self.language.labels[name]}'
+ '<span class="colon">:</span></dt>\n')
+ self.body.append(self.starttag(node, 'dd', '', CLASS=name))
+
+ def depart_docinfo_item(self):
+ self.body.append('</dd>\n')
+
+ def visit_doctest_block(self, node):
+ self.body.append(self.starttag(node, 'pre', suffix='',
+ classes=['code', 'python', 'doctest']))
+
+ def depart_doctest_block(self, node):
+ self.body.append('\n</pre>\n')
+
+ def visit_document(self, node):
+ title = (node.get('title') or os.path.basename(node['source'])
+ or 'untitled Docutils document')
+ self.head.append(f'<title>{self.encode(title)}</title>\n')
+
+ def depart_document(self, node):
+ self.head_prefix.extend([self.doctype,
+ self.head_prefix_template %
+ {'lang': self.settings.language_code}])
+ self.html_prolog.append(self.doctype)
+ self.head = self.meta[:] + self.head
+ if 'name="dcterms.' in ''.join(self.meta):
+ self.head.append('<link rel="schema.dcterms"'
+ ' href="http://purl.org/dc/terms/"/>')
+ if self.math_header:
+ if self.math_output == 'mathjax':
+ self.head.extend(self.math_header)
+ else:
+ self.stylesheet.extend(self.math_header)
+ # skip content-type meta tag with interpolated charset value:
+ self.html_head.extend(self.head[1:])
+ self.body_prefix.append(self.starttag(node, **self.documenttag_args))
+ self.body_suffix.insert(0, f'</{self.documenttag_args["tagname"]}>\n')
+ self.fragment.extend(self.body) # self.fragment is the "naked" body
+ self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
+ + self.docinfo + self.body
+ + self.body_suffix[:-1])
+ assert not self.context, f'len(context) = {len(self.context)}'
+
+ def visit_emphasis(self, node):
+ self.body.append(self.starttag(node, 'em', ''))
+
+ def depart_emphasis(self, node):
+ self.body.append('</em>')
+
+ def visit_entry(self, node):
+ atts = {'classes': []}
+ if isinstance(node.parent.parent, nodes.thead):
+ atts['classes'].append('head')
+ if node.parent.parent.parent.stubs[node.parent.column]:
+ # "stubs" list is an attribute of the tgroup element
+ atts['classes'].append('stub')
+ if atts['classes']:
+ tagname = 'th'
+ else:
+ tagname = 'td'
+ node.parent.column += 1
+ if 'morerows' in node:
+ atts['rowspan'] = node['morerows'] + 1
+ if 'morecols' in node:
+ atts['colspan'] = node['morecols'] + 1
+ node.parent.column += node['morecols']
+ self.body.append(self.starttag(node, tagname, '', **atts))
+ self.context.append('</%s>\n' % tagname.lower())
+
+ def depart_entry(self, node):
+ self.body.append(self.context.pop())
+
+ def visit_enumerated_list(self, node):
+ atts = {'classes': []}
+ if 'start' in node:
+ atts['start'] = node['start']
+ if 'enumtype' in node:
+ atts['classes'].append(node['enumtype'])
+ if self.is_compactable(node):
+ atts['classes'].append('simple')
+ self.body.append(self.starttag(node, 'ol', **atts))
+
+ def depart_enumerated_list(self, node):
+ self.body.append('</ol>\n')
+
+ def visit_field_list(self, node):
+ atts = {}
+ classes = node.setdefault('classes', [])
+ for i, cls in enumerate(classes):
+ if cls.startswith('field-indent-'):
+ try:
+ indent_length = length_or_percentage_or_unitless(
+ cls[13:], 'px')
+ except ValueError:
+ break
+ atts['style'] = '--field-indent: %s;' % indent_length
+ classes.pop(i)
+ break
+ classes.append('field-list')
+ if self.is_compactable(node):
+ classes.append('simple')
+ self.body.append(self.starttag(node, 'dl', **atts))
+
+ def depart_field_list(self, node):
+ self.body.append('</dl>\n')
+
+ def visit_field(self, node):
+ # Insert children (<field_name> and <field_body>) directly.
+ # Transfer "id" attribute to the <field_name> child node.
+ for child in node:
+ if isinstance(child, nodes.field_name):
+ child['ids'].extend(node['ids'])
+
+ def depart_field(self, node):
+ pass
+
+ # as field is ignored, pass class arguments to field-name and field-body:
+ def visit_field_name(self, node):
+ self.body.append(self.starttag(node, 'dt', '',
+ classes=node.parent['classes']))
+
+ def depart_field_name(self, node):
+ self.body.append('<span class="colon">:</span></dt>\n')
+
+ def visit_field_body(self, node):
+ self.body.append(self.starttag(node, 'dd', '',
+ classes=node.parent['classes']))
+ # prevent misalignment of following content if the field is empty:
+ if not node.children:
+ self.body.append('<p></p>')
+
+ def depart_field_body(self, node):
+ self.body.append('</dd>\n')
+
+ def visit_figure(self, node):
+ atts = {'class': 'figure'}
+ if node.get('width'):
+ atts['style'] = 'width: %s' % node['width']
+ if node.get('align'):
+ atts['class'] += " align-" + node['align']
+ self.body.append(self.starttag(node, 'div', **atts))
+
+ def depart_figure(self, node):
+ self.body.append('</div>\n')
+
+ def visit_footer(self, node):
+ self.context.append(len(self.body))
+
+ def depart_footer(self, node):
+ start = self.context.pop()
+ footer = [self.starttag(node, 'div', CLASS='footer'),
+ '<hr class="footer" />\n']
+ footer.extend(self.body[start:])
+ footer.append('\n</div>\n')
+ self.footer.extend(footer)
+ self.body_suffix[:0] = footer
+ del self.body[start:]
+
+ def visit_footnote(self, node):
+ # No native HTML element: use <aside> with ARIA role
+ # (html4css1 uses tables).
+ # Wrap groups of footnotes for easier styling.
+ label_style = self.settings.footnote_references # brackets/superscript
+ if not isinstance(node.previous_sibling(), type(node)):
+ self.body.append(f'<aside class="footnote-list {label_style}">\n')
+ self.body.append(self.starttag(node, 'aside',
+ classes=[node.tagname, label_style],
+ role="doc-footnote"))
+
+ def depart_footnote(self, node):
+ self.body.append('</aside>\n')
+ if not isinstance(node.next_node(descend=False, siblings=True),
+ type(node)):
+ self.body.append('</aside>\n')
+
+ def visit_footnote_reference(self, node):
+ href = '#' + node['refid']
+ classes = [self.settings.footnote_references]
+ self.body.append(self.starttag(node, 'a', suffix='', classes=classes,
+ role='doc-noteref', href=href))
+ self.body.append('<span class="fn-bracket">[</span>')
+
+ def depart_footnote_reference(self, node):
+ self.body.append('<span class="fn-bracket">]</span>')
+ self.body.append('</a>')
+
+ # Docutils-generated text: put section numbers in a span for CSS styling:
+ def visit_generated(self, node):
+ if 'sectnum' in node['classes']:
+ # get section number (strip trailing no-break-spaces)
+ sectnum = node.astext().rstrip(' ')
+ self.body.append('<span class="sectnum">%s </span>'
+ % self.encode(sectnum))
+ # Content already processed:
+ raise nodes.SkipNode
+
+ def depart_generated(self, node):
+ pass
+
+ def visit_header(self, node):
+ self.context.append(len(self.body))
+
+ def depart_header(self, node):
+ start = self.context.pop()
+ header = [self.starttag(node, 'div', CLASS='header')]
+ header.extend(self.body[start:])
+ header.append('\n<hr class="header"/>\n</div>\n')
+ self.body_prefix.extend(header)
+ self.header.extend(header)
+ del self.body[start:]
+
+ def visit_image(self, node):
+ # reference/embed images (still images and videos)
+ uri = node['uri']
+ alt = node.get('alt', uri)
+ mimetype = mimetypes.guess_type(uri)[0]
+ element = '' # the HTML element (including potential children)
+ atts = {} # attributes for the HTML tag
+ # alignment is handled by CSS rules
+ if 'align' in node:
+ atts['class'] = 'align-%s' % node['align']
+ # set size with "style" attribute (more universal, accepts dimensions)
+ size_declaration = self.image_size(node)
+ if size_declaration:
+ atts['style'] = size_declaration
+
+ # ``:loading:`` option (embed, link, lazy), default from setting,
+ # exception: only embed videos if told via directive option
+ loading = 'link' if mimetype in self.videotypes else self.image_loading
+ loading = node.get('loading', loading)
+ if loading == 'lazy':
+ atts['loading'] = 'lazy'
+ elif loading == 'embed':
+ try:
+ imagepath = self.uri2imagepath(uri)
+ with open(imagepath, 'rb') as imagefile:
+ imagedata = imagefile.read()
+ except (ValueError, OSError) as err:
+ self.messages.append(self.document.reporter.error(
+ f'Cannot embed image "{uri}":\n {err}', base_node=node))
+ # TODO: get external files with urllib.request (cf. odtwriter)?
+ else:
+ self.settings.record_dependencies.add(imagepath)
+ if mimetype == 'image/svg+xml':
+ element = self.prepare_svg(node, imagedata,
+ size_declaration)
+ else:
+ data64 = base64.b64encode(imagedata).decode()
+ uri = f'data:{mimetype};base64,{data64}'
+
+ # No newlines around inline images (but all images may be nested
+ # in a `reference` node which is a `TextElement` instance):
+ if (not isinstance(node.parent, nodes.TextElement)
+ or isinstance(node.parent, nodes.reference)
+ and not isinstance(node.parent.parent, nodes.TextElement)):
+ suffix = '\n'
+ else:
+ suffix = ''
+
+ if mimetype in self.videotypes:
+ atts['title'] = alt
+ if 'controls' in node['classes']:
+ node['classes'].remove('controls')
+ atts['controls'] = 'controls'
+ element = (self.starttag(node, "video", suffix, src=uri, **atts)
+ + f'<a href="{node["uri"]}">{alt}</a>{suffix}'
+ + f'</video>{suffix}')
+ elif mimetype == 'application/x-shockwave-flash':
+ atts['type'] = mimetype
+ element = (self.starttag(node, 'object', '', data=uri, **atts)
+ + f'{alt}</object>{suffix}')
+ elif element: # embedded SVG, see above
+ element += suffix
+ else:
+ atts['alt'] = alt
+ element = self.emptytag(node, 'img', suffix, src=uri, **atts)
+ self.body.append(element)
+ if suffix: # block-element
+ self.report_messages(node)
+
+ def depart_image(self, node):
+ pass
+
+ def visit_inline(self, node):
+ self.body.append(self.starttag(node, 'span', ''))
+
+ def depart_inline(self, node):
+ self.body.append('</span>')
+
+ # footnote and citation labels:
+ def visit_label(self, node):
+ self.body.append('<span class="label">')
+ self.body.append('<span class="fn-bracket">[</span>')
+ # footnote/citation backrefs:
+ if self.settings.footnote_backlinks:
+ backrefs = node.parent.get('backrefs', [])
+ if len(backrefs) == 1:
+ self.body.append('<a role="doc-backlink"'
+ ' href="#%s">' % backrefs[0])
+
+ def depart_label(self, node):
+ backrefs = []
+ if self.settings.footnote_backlinks:
+ backrefs = node.parent.get('backrefs', backrefs)
+ if len(backrefs) == 1:
+ self.body.append('</a>')
+ self.body.append('<span class="fn-bracket">]</span></span>\n')
+ if len(backrefs) > 1:
+ backlinks = ['<a role="doc-backlink" href="#%s">%s</a>' % (ref, i)
+ for (i, ref) in enumerate(backrefs, 1)]
+ self.body.append('<span class="backrefs">(%s)</span>\n'
+ % ','.join(backlinks))
+
+ def visit_legend(self, node):
+ self.body.append(self.starttag(node, 'div', CLASS='legend'))
+
+ def depart_legend(self, node):
+ self.body.append('</div>\n')
+
+ def visit_line(self, node):
+ self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
+ if not len(node):
+ self.body.append('<br />')
+
+ def depart_line(self, node):
+ self.body.append('</div>\n')
+
+ def visit_line_block(self, node):
+ self.body.append(self.starttag(node, 'div', CLASS='line-block'))
+
+ def depart_line_block(self, node):
+ self.body.append('</div>\n')
+
+ def visit_list_item(self, node):
+ self.body.append(self.starttag(node, 'li', ''))
+
+ def depart_list_item(self, node):
+ self.body.append('</li>\n')
+
+ # inline literal
+ def visit_literal(self, node):
+ # special case: "code" role
+ classes = node['classes']
+ if 'code' in classes:
+ # filter 'code' from class arguments
+ classes.pop(classes.index('code'))
+ self.body.append(self.starttag(node, 'code', ''))
+ return
+ self.body.append(
+ self.starttag(node, 'span', '', CLASS='docutils literal'))
+ text = node.astext()
+ if not isinstance(node.parent, nodes.literal_block):
+ text = text.replace('\n', ' ')
+ # Protect text like ``--an-option`` and the regular expression
+ # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
+ for token in self.words_and_spaces.findall(text):
+ if token.strip() and self.in_word_wrap_point.search(token):
+ self.body.append('<span class="pre">%s</span>'
+ % self.encode(token))
+ else:
+ self.body.append(self.encode(token))
+ self.body.append('</span>')
+ raise nodes.SkipNode # content already processed
+
+ def depart_literal(self, node):
+ # skipped unless literal element is from "code" role:
+ self.body.append('</code>')
+
+ def visit_literal_block(self, node):
+ self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
+ if 'code' in node['classes']:
+ self.body.append('<code>')
+
+ def depart_literal_block(self, node):
+ if 'code' in node['classes']:
+ self.body.append('</code>')
+ self.body.append('</pre>\n')
+
+ # Mathematics:
+ # As there is no native HTML math support, we provide alternatives
+ # for the math-output: LaTeX and MathJax simply wrap the content,
+ # HTML and MathML also convert the math_code.
+ # HTML element:
+ math_tags = { # format: (inline, block, [class arguments])
+ 'html': ('span', 'div', ['formula']),
+ 'latex': ('tt', 'pre', ['math']),
+ 'mathjax': ('span', 'div', ['math']),
+ 'mathml': ('', 'div', []),
+ 'problematic': ('span', 'pre', ['math', 'problematic']),
+ }
+
+ def visit_math(self, node):
+ # Also called from `visit_math_block()`:
+ is_block = isinstance(node, nodes.math_block)
+ format = self.math_output
+ math_code = node.astext().translate(unichar2tex.uni2tex_table)
+
+ # preamble code and conversion
+ if format == 'html':
+ if self.math_options and not self.math_header:
+ self.math_header = [
+ self.stylesheet_call(utils.find_file_in_dirs(
+ s, self.settings.stylesheet_dirs), adjust_path=True)
+ for s in self.math_options.split(',')]
+ math2html.DocumentParameters.displaymode = is_block
+ # TODO: fix display mode in matrices and fractions
+ math_code = wrap_math_code(math_code, is_block)
+ math_code = math2html.math2html(math_code)
+ elif format == 'latex':
+ math_code = self.encode(math_code)
+ elif format == 'mathjax':
+ if not self.math_header:
+ if self.math_options:
+ self.mathjax_url = self.math_options
+ else:
+ self.document.reporter.warning(
+ 'No MathJax URL specified, using local fallback '
+ '(see config.html).', base_node=node)
+ # append MathJax configuration
+ # (input LaTeX with AMS, output common HTML):
+ if '?' not in self.mathjax_url:
+ self.mathjax_url += '?config=TeX-AMS_CHTML'
+ self.math_header = [self.mathjax_script % self.mathjax_url]
+ if is_block:
+ math_code = wrap_math_code(math_code, is_block)
+ else:
+ math_code = rf'\({math_code}\)'
+ math_code = self.encode(math_code)
+ elif format == 'mathml':
+ if 'XHTML 1' in self.doctype:
+ self.doctype = self.doctype_mathml
+ self.content_type = self.content_type_mathml
+ if self.math_options:
+ converter = getattr(tex2mathml_extern, self.math_options)
+ else:
+ converter = latex2mathml.tex2mathml
+ try:
+ math_code = converter(math_code, as_block=is_block)
+ except (MathError, OSError) as err:
+ details = getattr(err, 'details', [])
+ self.messages.append(self.document.reporter.warning(
+ err, *details, base_node=node))
+ math_code = self.encode(node.astext())
+ if self.settings.report_level <= 2:
+ format = 'problematic'
+ else:
+ format = 'latex'
+ if isinstance(err, OSError):
+ # report missing converter only once
+ self.math_output = format
+
+ # append to document body
+ tag = self.math_tags[format][is_block]
+ suffix = '\n' if is_block else ''
+ if tag:
+ self.body.append(self.starttag(node, tag, suffix=suffix,
+ classes=self.math_tags[format][2]))
+ self.body.extend([math_code, suffix])
+ if tag:
+ self.body.append(f'</{tag}>{suffix}')
+ # Content already processed:
+ raise nodes.SkipChildren
+
+ def depart_math(self, node):
+ pass
+
+ def visit_math_block(self, node):
+ self.visit_math(node)
+
+ def depart_math_block(self, node):
+ self.report_messages(node)
+
+ # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
+ # HTML5/polyglot recommends using both
+ def visit_meta(self, node):
+ self.meta.append(self.emptytag(node, 'meta',
+ **node.non_default_attributes()))
+
+ def depart_meta(self, node):
+ pass
+
+ def visit_option(self, node):
+ self.body.append(self.starttag(node, 'span', '', CLASS='option'))
+
+ def depart_option(self, node):
+ self.body.append('</span>')
+ if isinstance(node.next_node(descend=False, siblings=True),
+ nodes.option):
+ self.body.append(', ')
+
+ def visit_option_argument(self, node):
+ self.body.append(node.get('delimiter', ' '))
+ self.body.append(self.starttag(node, 'var', ''))
+
+ def depart_option_argument(self, node):
+ self.body.append('</var>')
+
+ def visit_option_group(self, node):
+ self.body.append(self.starttag(node, 'dt', ''))
+ self.body.append('<kbd>')
+
+ def depart_option_group(self, node):
+ self.body.append('</kbd></dt>\n')
+
+ def visit_option_list(self, node):
+ self.body.append(
+ self.starttag(node, 'dl', CLASS='option-list'))
+
+ def depart_option_list(self, node):
+ self.body.append('</dl>\n')
+
+ def visit_option_list_item(self, node):
+ pass
+
+ def depart_option_list_item(self, node):
+ pass
+
+ def visit_option_string(self, node):
+ pass
+
+ def depart_option_string(self, node):
+ pass
+
+ def visit_organization(self, node):
+ self.visit_docinfo_item(node, 'organization')
+
+ def depart_organization(self, node):
+ self.depart_docinfo_item()
+
+ # Do not omit <p> tags
+ # --------------------
+ #
+ # The HTML4CSS1 writer does this to "produce
+ # visually compact lists (less vertical whitespace)". This writer
+ # relies on CSS rules for visual compactness.
+ #
+ # * In XHTML 1.1, e.g., a <blockquote> element may not contain
+ # character data, so you cannot drop the <p> tags.
+ # * Keeping simple paragraphs in the field_body enables a CSS
+ # rule to start the field-body on a new line if the label is too long
+ # * it makes the code simpler.
+ #
+ # TODO: omit paragraph tags in simple table cells?
+
+ def visit_paragraph(self, node):
+ self.body.append(self.starttag(node, 'p', ''))
+
+ def depart_paragraph(self, node):
+ self.body.append('</p>')
+ if not (isinstance(node.parent, (nodes.list_item, nodes.entry))
+ and (len(node.parent) == 1)):
+ self.body.append('\n')
+ self.report_messages(node)
+
+ def visit_problematic(self, node):
+ if node.hasattr('refid'):
+ self.body.append('<a href="#%s">' % node['refid'])
+ self.context.append('</a>')
+ else:
+ self.context.append('')
+ self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
+
+ def depart_problematic(self, node):
+ self.body.append('</span>')
+ self.body.append(self.context.pop())
+
+ def visit_raw(self, node):
+ if 'html' in node.get('format', '').split():
+ if isinstance(node.parent, nodes.TextElement):
+ tagname = 'span'
+ else:
+ tagname = 'div'
+ if node['classes']:
+ self.body.append(self.starttag(node, tagname, suffix=''))
+ self.body.append(node.astext())
+ if node['classes']:
+ self.body.append('</%s>' % tagname)
+ # Keep non-HTML raw text out of output:
+ raise nodes.SkipNode
+
+ def visit_reference(self, node):
+ atts = {'classes': ['reference']}
+ suffix = ''
+ if 'refuri' in node:
+ atts['href'] = node['refuri']
+ if (self.settings.cloak_email_addresses
+ and atts['href'].startswith('mailto:')):
+ atts['href'] = self.cloak_mailto(atts['href'])
+ self.in_mailto = True
+ atts['classes'].append('external')
+ else:
+ assert 'refid' in node, \
+ 'References must have "refuri" or "refid" attribute.'
+ atts['href'] = '#' + node['refid']
+ atts['classes'].append('internal')
+ if len(node) == 1 and isinstance(node[0], nodes.image):
+ atts['classes'].append('image-reference')
+ if not isinstance(node.parent, nodes.TextElement):
+ suffix = '\n'
+ self.body.append(self.starttag(node, 'a', suffix, **atts))
+
+ def depart_reference(self, node):
+ self.body.append('</a>')
+ if not isinstance(node.parent, nodes.TextElement):
+ self.body.append('\n')
+ self.in_mailto = False
+
+ def visit_revision(self, node):
+ self.visit_docinfo_item(node, 'revision', meta=False)
+
+ def depart_revision(self, node):
+ self.depart_docinfo_item()
+
+ def visit_row(self, node):
+ self.body.append(self.starttag(node, 'tr', ''))
+ node.column = 0
+
+ def depart_row(self, node):
+ self.body.append('</tr>\n')
+
+ def visit_rubric(self, node):
+ self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
+
+ def depart_rubric(self, node):
+ self.body.append('</p>\n')
+
+ def visit_section(self, node):
+ self.section_level += 1
+ self.body.append(
+ self.starttag(node, 'div', CLASS='section'))
+
+ def depart_section(self, node):
+ self.section_level -= 1
+ self.body.append('</div>\n')
+
+ # TODO: use the new HTML5 element <aside>
+ def visit_sidebar(self, node):
+ self.body.append(
+ self.starttag(node, 'div', CLASS='sidebar'))
+ self.in_sidebar = True
+
+ def depart_sidebar(self, node):
+ self.body.append('</div>\n')
+ self.in_sidebar = False
+
+ def visit_status(self, node):
+ self.visit_docinfo_item(node, 'status', meta=False)
+
+ def depart_status(self, node):
+ self.depart_docinfo_item()
+
+ def visit_strong(self, node):
+ self.body.append(self.starttag(node, 'strong', ''))
+
+ def depart_strong(self, node):
+ self.body.append('</strong>')
+
+ def visit_subscript(self, node):
+ self.body.append(self.starttag(node, 'sub', ''))
+
+ def depart_subscript(self, node):
+ self.body.append('</sub>')
+
+ def visit_substitution_definition(self, node):
+ """Internal only."""
+ raise nodes.SkipNode
+
+ def visit_substitution_reference(self, node):
+ self.unimplemented_visit(node)
+
+ # h1–h6 elements must not be used to markup subheadings, subtitles,
+ # alternative titles and taglines unless intended to be the heading for a
+ # new section or subsection.
+ # -- http://www.w3.org/TR/html51/sections.html#headings-and-sections
+ def visit_subtitle(self, node):
+ if isinstance(node.parent, nodes.sidebar):
+ classes = ['sidebar-subtitle']
+ elif isinstance(node.parent, nodes.document):
+ classes = ['subtitle']
+ self.in_document_title = len(self.body) + 1
+ elif isinstance(node.parent, nodes.section):
+ classes = ['section-subtitle']
+ self.body.append(self.starttag(node, 'p', '', classes=classes))
+
+ def depart_subtitle(self, node):
+ self.body.append('</p>\n')
+ if isinstance(node.parent, nodes.document):
+ self.subtitle = self.body[self.in_document_title:-1]
+ self.in_document_title = 0
+ self.body_pre_docinfo.extend(self.body)
+ self.html_subtitle.extend(self.body)
+ del self.body[:]
+
+ def visit_superscript(self, node):
+ self.body.append(self.starttag(node, 'sup', ''))
+
+ def depart_superscript(self, node):
+ self.body.append('</sup>')
+
+ def visit_system_message(self, node):
+ self.body.append(self.starttag(node, 'aside', CLASS='system-message'))
+ self.body.append('<p class="system-message-title">')
+ backref_text = ''
+ if len(node['backrefs']):
+ backrefs = node['backrefs']
+ if len(backrefs) == 1:
+ backref_text = ('; <em><a href="#%s">backlink</a></em>'
+ % backrefs[0])
+ else:
+ i = 1
+ backlinks = []
+ for backref in backrefs:
+ backlinks.append('<a href="#%s">%s</a>' % (backref, i))
+ i += 1
+ backref_text = ('; <em>backlinks: %s</em>'
+ % ', '.join(backlinks))
+ if node.hasattr('line'):
+ line = ', line %s' % node['line']
+ else:
+ line = ''
+ self.body.append('System Message: %s/%s '
+ '(<span class="docutils literal">%s</span>%s)%s</p>\n'
+ % (node['type'], node['level'],
+ self.encode(node['source']), line, backref_text))
+
+ def depart_system_message(self, node):
+ self.body.append('</aside>\n')
+
+ def visit_table(self, node):
+ atts = {'classes': self.settings.table_style.replace(',', ' ').split()}
+ if 'align' in node:
+ atts['classes'].append('align-%s' % node['align'])
+ if 'width' in node:
+ atts['style'] = 'width: %s;' % node['width']
+ tag = self.starttag(node, 'table', **atts)
+ self.body.append(tag)
+
+ def depart_table(self, node):
+ self.body.append('</table>\n')
+ self.report_messages(node)
+
+ def visit_target(self, node):
+ if ('refuri' not in node
+ and 'refid' not in node
+ and 'refname' not in node):
+ self.body.append(self.starttag(node, 'span', '', CLASS='target'))
+ self.context.append('</span>')
+ else:
+ self.context.append('')
+
+ def depart_target(self, node):
+ self.body.append(self.context.pop())
+
+ # no hard-coded vertical alignment in table body
+ def visit_tbody(self, node):
+ self.body.append(self.starttag(node, 'tbody'))
+
+ def depart_tbody(self, node):
+ self.body.append('</tbody>\n')
+
+ def visit_term(self, node):
+ if 'details' in node.parent.parent['classes']:
+ self.body.append(self.starttag(node, 'summary', suffix=''))
+ else:
+ # The parent node (definition_list_item) is omitted in HTML.
+ self.body.append(self.starttag(node, 'dt', suffix='',
+ classes=node.parent['classes'],
+ ids=node.parent['ids']))
+
+ def depart_term(self, node):
+ # Nest (optional) classifier(s) in the <dt> element
+ if node.next_node(nodes.classifier, descend=False, siblings=True):
+ return # skip (depart_classifier() calls this function again)
+ if 'details' in node.parent.parent['classes']:
+ self.body.append('</summary>\n')
+ else:
+ self.body.append('</dt>\n')
+
+ def visit_tgroup(self, node):
+ self.colspecs = []
+ node.stubs = []
+
+ def depart_tgroup(self, node):
+ pass
+
+ def visit_thead(self, node):
+ self.body.append(self.starttag(node, 'thead'))
+
+ def depart_thead(self, node):
+ self.body.append('</thead>\n')
+
+ def section_title_tags(self, node):
+ atts = {}
+ h_level = self.section_level + self.initial_header_level - 1
+ # Only 6 heading levels have dedicated HTML tags.
+ tagname = 'h%i' % min(h_level, 6)
+ if h_level > 6:
+ atts['aria-level'] = h_level
+ start_tag = self.starttag(node, tagname, '', **atts)
+ if node.hasattr('refid'):
+ atts = {}
+ atts['class'] = 'toc-backref'
+ atts['role'] = 'doc-backlink' # HTML5 only
+ atts['href'] = '#' + node['refid']
+ start_tag += self.starttag(nodes.reference(), 'a', '', **atts)
+ close_tag = '</a></%s>\n' % tagname
+ else:
+ close_tag = '</%s>\n' % tagname
+ return start_tag, close_tag
+
+ def visit_title(self, node):
+ close_tag = '</p>\n'
+ if isinstance(node.parent, nodes.topic):
+ # TODO: use role="heading" or <h1>? (HTML5 only)
+ self.body.append(
+ self.starttag(node, 'p', '', CLASS='topic-title'))
+ if (self.settings.toc_backlinks
+ and 'contents' in node.parent['classes']):
+ self.body.append('<a class="reference internal" href="#top">')
+ close_tag = '</a></p>\n'
+ elif isinstance(node.parent, nodes.sidebar):
+ # TODO: use role="heading" or <h1>? (HTML5 only)
+ self.body.append(
+ self.starttag(node, 'p', '', CLASS='sidebar-title'))
+ elif isinstance(node.parent, nodes.Admonition):
+ self.body.append(
+ self.starttag(node, 'p', '', CLASS='admonition-title'))
+ elif isinstance(node.parent, nodes.table):
+ self.body.append(self.starttag(node, 'caption', ''))
+ close_tag = '</caption>\n'
+ elif isinstance(node.parent, nodes.document):
+ self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
+ close_tag = '</h1>\n'
+ self.in_document_title = len(self.body)
+ else:
+ assert isinstance(node.parent, nodes.section)
+ # Get correct heading and evt. backlink tags
+ start_tag, close_tag = self.section_title_tags(node)
+ self.body.append(start_tag)
+ self.context.append(close_tag)
+
+ def depart_title(self, node):
+ self.body.append(self.context.pop())
+ if self.in_document_title:
+ self.title = self.body[self.in_document_title:-1]
+ self.in_document_title = 0
+ self.body_pre_docinfo.extend(self.body)
+ self.html_title.extend(self.body)
+ del self.body[:]
+
+ def visit_title_reference(self, node):
+ self.body.append(self.starttag(node, 'cite', ''))
+
+ def depart_title_reference(self, node):
+ self.body.append('</cite>')
+
+ def visit_topic(self, node):
+ self.body.append(self.starttag(node, 'div', CLASS='topic'))
+
+ def depart_topic(self, node):
+ self.body.append('</div>\n')
+
+ def visit_transition(self, node):
+ self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
+
+ def depart_transition(self, node):
+ pass
+
+ def visit_version(self, node):
+ self.visit_docinfo_item(node, 'version', meta=False)
+
+ def depart_version(self, node):
+ self.depart_docinfo_item()
+
+ def unimplemented_visit(self, node):
+ raise NotImplementedError('visiting unimplemented node type: %s'
+ % node.__class__.__name__)
+
+
+class SimpleListChecker(nodes.GenericNodeVisitor):
+
+ """
+ Raise `nodes.NodeFound` if non-simple list item is encountered.
+
+ Here "simple" means a list item containing nothing other than a single
+ paragraph, a simple list, or a paragraph followed by a simple list.
+
+ This version also checks for simple field lists and docinfo.
+ """
+
+ def default_visit(self, node):
+ raise nodes.NodeFound
+
+ def visit_list_item(self, node):
+ children = [child for child in node.children
+ if not isinstance(child, nodes.Invisible)]
+ if (children and isinstance(children[0], nodes.paragraph)
+ and (isinstance(children[-1], nodes.bullet_list)
+ or isinstance(children[-1], nodes.enumerated_list)
+ or isinstance(children[-1], nodes.field_list))):
+ children.pop()
+ if len(children) <= 1:
+ return
+ else:
+ raise nodes.NodeFound
+
+ def pass_node(self, node):
+ pass
+
+ def ignore_node(self, node):
+ # ignore nodes that are never complex (can contain only inline nodes)
+ raise nodes.SkipNode
+
+ # Paragraphs and text
+ visit_Text = ignore_node
+ visit_paragraph = ignore_node
+
+ # Lists
+ visit_bullet_list = pass_node
+ visit_enumerated_list = pass_node
+ visit_docinfo = pass_node
+
+ # Docinfo nodes:
+ visit_author = ignore_node
+ visit_authors = visit_list_item
+ visit_address = visit_list_item
+ visit_contact = pass_node
+ visit_copyright = ignore_node
+ visit_date = ignore_node
+ visit_organization = ignore_node
+ visit_status = ignore_node
+ visit_version = visit_list_item
+
+ # Definition list:
+ visit_definition_list = pass_node
+ visit_definition_list_item = pass_node
+ visit_term = ignore_node
+ visit_classifier = pass_node
+ visit_definition = visit_list_item
+
+ # Field list:
+ visit_field_list = pass_node
+ visit_field = pass_node
+ # the field body corresponds to a list item
+ visit_field_body = visit_list_item
+ visit_field_name = ignore_node
+
+ # Invisible nodes should be ignored.
+ visit_comment = ignore_node
+ visit_substitution_definition = ignore_node
+ visit_target = ignore_node
+ visit_pending = ignore_node
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/docutils_xml.py b/.venv/lib/python3.12/site-packages/docutils/writers/docutils_xml.py
new file mode 100644
index 00000000..f4169295
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/docutils_xml.py
@@ -0,0 +1,187 @@
+# $Id: docutils_xml.py 9502 2023-12-14 22:39:08Z milde $
+# Author: David Goodger, Paul Tremblay, Guenter Milde
+# Maintainer: docutils-develop@lists.sourceforge.net
+# Copyright: This module has been placed in the public domain.
+
+"""
+Simple document tree Writer, writes Docutils XML according to
+https://docutils.sourceforge.io/docs/ref/docutils.dtd.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from io import StringIO
+import xml.sax.saxutils
+
+import docutils
+from docutils import frontend, nodes, writers, utils
+
+
+class RawXmlError(docutils.ApplicationError):
+ pass
+
+
+class Writer(writers.Writer):
+
+ supported = ('xml',)
+ """Formats this writer supports."""
+
+ settings_spec = (
+ '"Docutils XML" Writer Options',
+ None,
+ (('Generate XML with newlines before and after tags.',
+ ['--newlines'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ ('Generate XML with indents and newlines.',
+ ['--indents'], # TODO use integer value for number of spaces?
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ ('Omit the XML declaration. Use with caution.',
+ ['--no-xml-declaration'],
+ {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
+ 'validator': frontend.validate_boolean}),
+ ('Omit the DOCTYPE declaration.',
+ ['--no-doctype'],
+ {'dest': 'doctype_declaration', 'default': 1,
+ 'action': 'store_false', 'validator': frontend.validate_boolean}),))
+
+ settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
+
+ config_section = 'docutils_xml writer'
+ config_section_dependencies = ('writers',)
+
+ output = None
+ """Final translated form of `document`."""
+
+ def __init__(self):
+ writers.Writer.__init__(self)
+ self.translator_class = XMLTranslator
+
+ def translate(self):
+ self.visitor = visitor = self.translator_class(self.document)
+ self.document.walkabout(visitor)
+ self.output = ''.join(visitor.output)
+
+
+class XMLTranslator(nodes.GenericNodeVisitor):
+
+ # TODO: add stylesheet options similar to HTML and LaTeX writers?
+ # xml_stylesheet = '<?xml-stylesheet type="text/xsl" href="%s"?>\n'
+ doctype = (
+ '<!DOCTYPE document PUBLIC'
+ ' "+//IDN docutils.sourceforge.net//DTD Docutils Generic//EN//XML"'
+ ' "http://docutils.sourceforge.net/docs/ref/docutils.dtd">\n')
+ generator = '<!-- Generated by Docutils %s -->\n'
+
+ xmlparser = xml.sax.make_parser()
+ """SAX parser instance to check/extract raw XML."""
+ xmlparser.setFeature(
+ "http://xml.org/sax/features/external-general-entities", True)
+
+ def __init__(self, document):
+ nodes.NodeVisitor.__init__(self, document)
+
+ # Reporter
+ self.warn = self.document.reporter.warning
+ self.error = self.document.reporter.error
+
+ # Settings
+ self.settings = settings = document.settings
+ self.indent = self.newline = ''
+ if settings.newlines:
+ self.newline = '\n'
+ if settings.indents:
+ self.newline = '\n'
+ self.indent = ' ' # TODO make this configurable?
+ self.level = 0 # indentation level
+ self.in_simple = 0 # level of nesting inside mixed-content elements
+ self.fixed_text = 0 # level of nesting inside FixedText elements
+
+ # Output
+ self.output = []
+ if settings.xml_declaration:
+ self.output.append(utils.xml_declaration(settings.output_encoding))
+ if settings.doctype_declaration:
+ self.output.append(self.doctype)
+ self.output.append(self.generator % docutils.__version__)
+
+ # initialize XML parser
+ self.the_handle = TestXml()
+ self.xmlparser.setContentHandler(self.the_handle)
+
+ # generic visit and depart methods
+ # --------------------------------
+
+ simple_nodes = (nodes.TextElement, nodes.meta,
+ nodes.image, nodes.colspec, nodes.transition)
+
+ def default_visit(self, node):
+ """Default node visit method."""
+ if not self.in_simple:
+ self.output.append(self.indent*self.level)
+ self.output.append(node.starttag(xml.sax.saxutils.quoteattr))
+ self.level += 1
+ # `nodes.literal` is not an instance of FixedTextElement by design,
+ # see docs/ref/rst/restructuredtext.html#inline-literals
+ if isinstance(node, (nodes.FixedTextElement, nodes.literal)):
+ self.fixed_text += 1
+ if isinstance(node, self.simple_nodes):
+ self.in_simple += 1
+ if not self.in_simple:
+ self.output.append(self.newline)
+
+ def default_departure(self, node):
+ """Default node depart method."""
+ self.level -= 1
+ if not self.in_simple:
+ self.output.append(self.indent*self.level)
+ self.output.append(node.endtag())
+ if isinstance(node, (nodes.FixedTextElement, nodes.literal)):
+ self.fixed_text -= 1
+ if isinstance(node, self.simple_nodes):
+ self.in_simple -= 1
+ if not self.in_simple:
+ self.output.append(self.newline)
+
+ # specific visit and depart methods
+ # ---------------------------------
+
+ def visit_Text(self, node):
+ text = xml.sax.saxutils.escape(node.astext())
+ # indent text if we are not in a FixedText element:
+ if not self.fixed_text:
+ text = text.replace('\n', '\n'+self.indent*self.level)
+ self.output.append(text)
+
+ def depart_Text(self, node):
+ pass
+
+ def visit_raw(self, node):
+ if 'xml' not in node.get('format', '').split():
+ # skip other raw content?
+ # raise nodes.SkipNode
+ self.default_visit(node)
+ return
+ # wrap in <raw> element
+ self.default_visit(node) # or not?
+ xml_string = node.astext()
+ self.output.append(xml_string)
+ self.default_departure(node) # or not?
+ # Check validity of raw XML:
+ try:
+ self.xmlparser.parse(StringIO(xml_string))
+ except xml.sax._exceptions.SAXParseException:
+ col_num = self.the_handle.locator.getColumnNumber()
+ line_num = self.the_handle.locator.getLineNumber()
+ srcline = node.line
+ if not isinstance(node.parent, nodes.TextElement):
+ srcline += 2 # directive content start line
+ msg = 'Invalid raw XML in column %d, line offset %d:\n%s' % (
+ col_num, line_num, node.astext())
+ self.warn(msg, source=node.source, line=srcline+line_num-1)
+ raise nodes.SkipNode # content already processed
+
+
+class TestXml(xml.sax.handler.ContentHandler):
+
+ def setDocumentLocator(self, locator):
+ self.locator = locator
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/__init__.py
new file mode 100644
index 00000000..799d30e4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/__init__.py
@@ -0,0 +1,955 @@
+# $Id: __init__.py 9558 2024-03-11 17:48:52Z milde $
+# Author: David Goodger
+# Maintainer: docutils-develop@lists.sourceforge.net
+# Copyright: This module has been placed in the public domain.
+
+"""
+Simple HyperText Markup Language document tree Writer.
+
+The output conforms to the XHTML version 1.0 Transitional DTD
+(*almost* strict). The output contains a minimum of formatting
+information. The cascading style sheet "html4css1.css" is required
+for proper viewing with a modern graphical browser.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import os.path
+import re
+
+from docutils import frontend, nodes, writers
+from docutils.writers import _html_base
+from docutils.writers._html_base import PIL
+
+
+class Writer(writers._html_base.Writer):
+
+ supported = ('html', 'html4', 'html4css1', 'xhtml', 'xhtml10')
+ """Formats this writer supports."""
+
+ default_stylesheets = ['html4css1.css']
+ default_stylesheet_dirs = ['.',
+ os.path.abspath(os.path.dirname(__file__)),
+ os.path.abspath(os.path.join(
+ os.path.dirname(os.path.dirname(__file__)),
+ 'html5_polyglot')) # for math.css
+ ]
+ default_template = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), 'template.txt')
+
+ # use a copy of the parent spec with some modifications
+ settings_spec = frontend.filter_settings_spec(
+ writers._html_base.Writer.settings_spec,
+ template=(
+ 'Template file. (UTF-8 encoded, default: "%s")' % default_template,
+ ['--template'],
+ {'default': default_template, 'metavar': '<file>'}),
+ stylesheet_path=(
+ 'Comma separated list of stylesheet paths. '
+ 'Relative paths are expanded if a matching file is found in '
+ 'the --stylesheet-dirs. With --link-stylesheet, '
+ 'the path is rewritten relative to the output HTML file. '
+ '(default: "%s")' % ','.join(default_stylesheets),
+ ['--stylesheet-path'],
+ {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
+ 'validator': frontend.validate_comma_separated_list,
+ 'default': default_stylesheets}),
+ stylesheet_dirs=(
+ 'Comma-separated list of directories where stylesheets are found. '
+ 'Used by --stylesheet-path when expanding relative path '
+ 'arguments. (default: "%s")' % ','.join(default_stylesheet_dirs),
+ ['--stylesheet-dirs'],
+ {'metavar': '<dir[,dir,...]>',
+ 'validator': frontend.validate_comma_separated_list,
+ 'default': default_stylesheet_dirs}),
+ initial_header_level=(
+ 'Specify the initial header level. Does not affect document '
+ 'title & subtitle (see --no-doc-title). (default: 1 for "<h1>")',
+ ['--initial-header-level'],
+ {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
+ 'metavar': '<level>'}),
+ xml_declaration=(
+ 'Prepend an XML declaration (default). ',
+ ['--xml-declaration'],
+ {'default': True, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ )
+ settings_spec = settings_spec + (
+ 'HTML4 Writer Options',
+ '',
+ (('Specify the maximum width (in characters) for one-column field '
+ 'names. Longer field names will span an entire row of the table '
+ 'used to render the field list. Default is 14 characters. '
+ 'Use 0 for "no limit".',
+ ['--field-name-limit'],
+ {'default': 14, 'metavar': '<level>',
+ 'validator': frontend.validate_nonnegative_int}),
+ ('Specify the maximum width (in characters) for options in option '
+ 'lists. Longer options will span an entire row of the table used '
+ 'to render the option list. Default is 14 characters. '
+ 'Use 0 for "no limit".',
+ ['--option-limit'],
+ {'default': 14, 'metavar': '<level>',
+ 'validator': frontend.validate_nonnegative_int}),
+ )
+ )
+
+ config_section = 'html4css1 writer'
+
+ def __init__(self):
+ self.parts = {}
+ self.translator_class = HTMLTranslator
+
+
+class HTMLTranslator(writers._html_base.HTMLTranslator):
+ """
+ The html4css1 writer has been optimized to produce visually compact
+ lists (less vertical whitespace). HTML's mixed content models
+ allow list items to contain "<li><p>body elements</p></li>" or
+ "<li>just text</li>" or even "<li>text<p>and body
+ elements</p>combined</li>", each with different effects. It would
+ be best to stick with strict body elements in list items, but they
+ affect vertical spacing in older browsers (although they really
+ shouldn't).
+ The html5_polyglot writer solves this using CSS2.
+
+ Here is an outline of the optimization:
+
+ - Check for and omit <p> tags in "simple" lists: list items
+ contain either a single paragraph, a nested simple list, or a
+ paragraph followed by a nested simple list. This means that
+ this list can be compact:
+
+ - Item 1.
+ - Item 2.
+
+ But this list cannot be compact:
+
+ - Item 1.
+
+ This second paragraph forces space between list items.
+
+ - Item 2.
+
+ - In non-list contexts, omit <p> tags on a paragraph if that
+ paragraph is the only child of its parent (footnotes & citations
+ are allowed a label first).
+
+ - Regardless of the above, in definitions, table cells, field bodies,
+ option descriptions, and list items, mark the first child with
+ 'class="first"' and the last child with 'class="last"'. The stylesheet
+ sets the margins (top & bottom respectively) to 0 for these elements.
+
+ The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
+ option) disables list whitespace optimization.
+ """
+
+ # The following definitions are required for display in browsers limited
+ # to CSS1 or backwards compatible behaviour of the writer:
+
+ doctype = (
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
+ ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
+
+ content_type = ('<meta http-equiv="Content-Type"'
+ ' content="text/html; charset=%s" />\n')
+ content_type_mathml = ('<meta http-equiv="Content-Type"'
+ ' content="application/xhtml+xml; charset=%s" />\n')
+
+ # encode also non-breaking space
+ special_characters = _html_base.HTMLTranslator.special_characters.copy()
+ special_characters[0xa0] = '&nbsp;'
+
+ # use character reference for dash (not valid in HTML5)
+ attribution_formats = {'dash': ('&mdash;', ''),
+ 'parentheses': ('(', ')'),
+ 'parens': ('(', ')'),
+ 'none': ('', '')}
+
+ # ersatz for first/last pseudo-classes missing in CSS1
+ def set_first_last(self, node):
+ self.set_class_on_child(node, 'first', 0)
+ self.set_class_on_child(node, 'last', -1)
+
+ # add newline after opening tag
+ def visit_address(self, node):
+ self.visit_docinfo_item(node, 'address', meta=False)
+ self.body.append(self.starttag(node, 'pre', CLASS='address'))
+
+ def depart_address(self, node):
+ self.body.append('\n</pre>\n')
+ self.depart_docinfo_item()
+
+ # ersatz for first/last pseudo-classes
+ def visit_admonition(self, node):
+ node['classes'].insert(0, 'admonition')
+ self.body.append(self.starttag(node, 'div'))
+ self.set_first_last(node)
+
+ def depart_admonition(self, node=None):
+ self.body.append('</div>\n')
+
+ # author, authors: use <br> instead of paragraphs
+ def visit_author(self, node):
+ if isinstance(node.parent, nodes.authors):
+ if self.author_in_authors:
+ self.body.append('\n<br />')
+ else:
+ self.visit_docinfo_item(node, 'author')
+
+ def depart_author(self, node):
+ if isinstance(node.parent, nodes.authors):
+ self.author_in_authors = True
+ else:
+ self.depart_docinfo_item()
+
+ def visit_authors(self, node):
+ self.visit_docinfo_item(node, 'authors')
+ self.author_in_authors = False # initialize
+
+ def depart_authors(self, node):
+ self.depart_docinfo_item()
+
+ # use "width" argument instead of "style: 'width'":
+ def visit_colspec(self, node):
+ self.colspecs.append(node)
+ # "stubs" list is an attribute of the tgroup element:
+ node.parent.stubs.append(node.attributes.get('stub'))
+
+ def depart_colspec(self, node):
+ # write out <colgroup> when all colspecs are processed
+ if isinstance(node.next_node(descend=False, siblings=True),
+ nodes.colspec):
+ return
+ if ('colwidths-auto' in node.parent.parent['classes']
+ or ('colwidths-auto' in self.settings.table_style
+ and 'colwidths-given' not in node.parent.parent['classes'])):
+ return
+ total_width = sum(node['colwidth'] for node in self.colspecs)
+ self.body.append(self.starttag(node, 'colgroup'))
+ for node in self.colspecs:
+ colwidth = int(node['colwidth'] * 100.0 / total_width + 0.5)
+ self.body.append(self.emptytag(node, 'col',
+ width='%i%%' % colwidth))
+ self.body.append('</colgroup>\n')
+
+ # Compact lists:
+ # exclude definition lists and field lists (non-compact by default)
+
+ def is_compactable(self, node):
+ return ('compact' in node['classes']
+ or (self.settings.compact_lists
+ and 'open' not in node['classes']
+ and (self.compact_simple
+ or 'contents' in node.parent['classes']
+ # TODO: self.in_contents
+ or self.check_simple_list(node))))
+
+ # citations: Use table for bibliographic references.
+ def visit_citation(self, node):
+ self.body.append(self.starttag(node, 'table',
+ CLASS='docutils citation',
+ frame="void", rules="none"))
+ self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
+ '<tbody valign="top">\n'
+ '<tr>')
+ self.footnote_backrefs(node)
+
+ def depart_citation(self, node):
+ self.body.append('</td></tr>\n'
+ '</tbody>\n</table>\n')
+
+ def visit_citation_reference(self, node):
+ href = '#'
+ if 'refid' in node:
+ href += node['refid']
+ elif 'refname' in node:
+ href += self.document.nameids[node['refname']]
+ self.body.append(self.starttag(node, 'a', suffix='[', href=href,
+ classes=['citation-reference']))
+
+ def depart_citation_reference(self, node):
+ self.body.append(']</a>')
+
+ # insert classifier-delimiter (not required with CSS2)
+ def visit_classifier(self, node):
+ self.body.append(' <span class="classifier-delimiter">:</span> ')
+ self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
+
+ def depart_classifier(self, node):
+ self.body.append('</span>')
+ self.depart_term(node) # close the <dt> after last classifier
+
+ # ersatz for first/last pseudo-classes
+ def visit_compound(self, node):
+ self.body.append(self.starttag(node, 'div', CLASS='compound'))
+ if len(node) > 1:
+ node[0]['classes'].append('compound-first')
+ node[-1]['classes'].append('compound-last')
+ for child in node[1:-1]:
+ child['classes'].append('compound-middle')
+
+ def depart_compound(self, node):
+ self.body.append('</div>\n')
+
+ # ersatz for first/last pseudo-classes, no special handling of "details"
+ def visit_definition(self, node):
+ self.body.append(self.starttag(node, 'dd', ''))
+ self.set_first_last(node)
+
+ def depart_definition(self, node):
+ self.body.append('</dd>\n')
+
+ # don't add "simple" class value, no special handling of "details"
+ def visit_definition_list(self, node):
+ self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
+
+ def depart_definition_list(self, node):
+ self.body.append('</dl>\n')
+
+ # no special handling of "details"
+ def visit_definition_list_item(self, node):
+ pass
+
+ def depart_definition_list_item(self, node):
+ pass
+
+ # use a table for description lists
+ def visit_description(self, node):
+ self.body.append(self.starttag(node, 'td', ''))
+ self.set_first_last(node)
+
+ def depart_description(self, node):
+ self.body.append('</td>')
+
+ # use table for docinfo
+ def visit_docinfo(self, node):
+ self.context.append(len(self.body))
+ self.body.append(self.starttag(node, 'table',
+ CLASS='docinfo',
+ frame="void", rules="none"))
+ self.body.append('<col class="docinfo-name" />\n'
+ '<col class="docinfo-content" />\n'
+ '<tbody valign="top">\n')
+ self.in_docinfo = True
+
+ def depart_docinfo(self, node):
+ self.body.append('</tbody>\n</table>\n')
+ self.in_docinfo = False
+ start = self.context.pop()
+ self.docinfo = self.body[start:]
+ self.body = []
+
+ def visit_docinfo_item(self, node, name, meta=True):
+ if meta:
+ meta_tag = '<meta name="%s" content="%s" />\n' \
+ % (name, self.attval(node.astext()))
+ self.meta.append(meta_tag)
+ self.body.append(self.starttag(node, 'tr', ''))
+ self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
+ % self.language.labels[name])
+ if len(node):
+ if isinstance(node[0], nodes.Element):
+ node[0]['classes'].append('first')
+ if isinstance(node[-1], nodes.Element):
+ node[-1]['classes'].append('last')
+
+ def depart_docinfo_item(self):
+ self.body.append('</td></tr>\n')
+
+ # add newline after opening tag
+ def visit_doctest_block(self, node):
+ self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
+
+ def depart_doctest_block(self, node):
+ self.body.append('\n</pre>\n')
+
+ # insert an NBSP into empty cells, ersatz for first/last
+ def visit_entry(self, node):
+ writers._html_base.HTMLTranslator.visit_entry(self, node)
+ if len(node) == 0: # empty cell
+ self.body.append('&nbsp;')
+ self.set_first_last(node)
+
+ def depart_entry(self, node):
+ self.body.append(self.context.pop())
+
+ # ersatz for first/last pseudo-classes
+ def visit_enumerated_list(self, node):
+ """
+ The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
+ cannot be emulated in CSS1 (HTML 5 reincludes it).
+ """
+ atts = {}
+ if 'start' in node:
+ atts['start'] = node['start']
+ if 'enumtype' in node:
+ atts['class'] = node['enumtype']
+ # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
+ # single "format" attribute? Use CSS2?
+ old_compact_simple = self.compact_simple
+ self.context.append((self.compact_simple, self.compact_p))
+ self.compact_p = None
+ self.compact_simple = self.is_compactable(node)
+ if self.compact_simple and not old_compact_simple:
+ atts['class'] = (atts.get('class', '') + ' simple').strip()
+ self.body.append(self.starttag(node, 'ol', **atts))
+
+ def depart_enumerated_list(self, node):
+ self.compact_simple, self.compact_p = self.context.pop()
+ self.body.append('</ol>\n')
+
+ # use table for field-list:
+ def visit_field(self, node):
+ self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
+
+ def depart_field(self, node):
+ self.body.append('</tr>\n')
+
+ def visit_field_body(self, node):
+ self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
+ self.set_class_on_child(node, 'first', 0)
+ field = node.parent
+ if (self.compact_field_list
+ or isinstance(field.parent, nodes.docinfo)
+ or field.parent.index(field) == len(field.parent) - 1):
+ # If we are in a compact list, the docinfo, or if this is
+ # the last field of the field list, do not add vertical
+ # space after last element.
+ self.set_class_on_child(node, 'last', -1)
+
+ def depart_field_body(self, node):
+ self.body.append('</td>\n')
+
+ def visit_field_list(self, node):
+ self.context.append((self.compact_field_list, self.compact_p))
+ self.compact_p = None
+ if 'compact' in node['classes']:
+ self.compact_field_list = True
+ elif (self.settings.compact_field_lists
+ and 'open' not in node['classes']):
+ self.compact_field_list = True
+ if self.compact_field_list:
+ for field in node:
+ field_body = field[-1]
+ assert isinstance(field_body, nodes.field_body)
+ children = [n for n in field_body
+ if not isinstance(n, nodes.Invisible)]
+ if not (len(children) == 0
+ or len(children) == 1
+ and isinstance(children[0],
+ (nodes.paragraph, nodes.line_block))):
+ self.compact_field_list = False
+ break
+ self.body.append(self.starttag(node, 'table', frame='void',
+ rules='none',
+ CLASS='docutils field-list'))
+ self.body.append('<col class="field-name" />\n'
+ '<col class="field-body" />\n'
+ '<tbody valign="top">\n')
+
+ def depart_field_list(self, node):
+ self.body.append('</tbody>\n</table>\n')
+ self.compact_field_list, self.compact_p = self.context.pop()
+
+ def visit_field_name(self, node):
+ atts = {}
+ if self.in_docinfo:
+ atts['class'] = 'docinfo-name'
+ else:
+ atts['class'] = 'field-name'
+ if (self.settings.field_name_limit
+ and len(node.astext()) > self.settings.field_name_limit):
+ atts['colspan'] = 2
+ self.context.append('</tr>\n'
+ + self.starttag(node.parent, 'tr', '',
+ CLASS='field')
+ + '<td>&nbsp;</td>')
+ else:
+ self.context.append('')
+ self.body.append(self.starttag(node, 'th', '', **atts))
+
+ def depart_field_name(self, node):
+ self.body.append(':</th>')
+ self.body.append(self.context.pop())
+
+ # use table for footnote text
+ def visit_footnote(self, node):
+ self.body.append(self.starttag(node, 'table',
+ CLASS='docutils footnote',
+ frame="void", rules="none"))
+ self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
+ '<tbody valign="top">\n'
+ '<tr>')
+ self.footnote_backrefs(node)
+
+ def footnote_backrefs(self, node):
+ backlinks = []
+ backrefs = node['backrefs']
+ if self.settings.footnote_backlinks and backrefs:
+ if len(backrefs) == 1:
+ self.context.append('')
+ self.context.append('</a>')
+ self.context.append('<a class="fn-backref" href="#%s">'
+ % backrefs[0])
+ else:
+ for (i, backref) in enumerate(backrefs, 1):
+ backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
+ % (backref, i))
+ self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
+ self.context += ['', '']
+ else:
+ self.context.append('')
+ self.context += ['', '']
+ # If the node does not only consist of a label.
+ if len(node) > 1:
+ # If there are preceding backlinks, we do not set class
+ # 'first', because we need to retain the top-margin.
+ if not backlinks:
+ node[1]['classes'].append('first')
+ node[-1]['classes'].append('last')
+
+ def depart_footnote(self, node):
+ self.body.append('</td></tr>\n'
+ '</tbody>\n</table>\n')
+
+ # insert markers in text (pseudo-classes are not supported in CSS1):
+ def visit_footnote_reference(self, node):
+ href = '#' + node['refid']
+ format = self.settings.footnote_references
+ if format == 'brackets':
+ suffix = '['
+ self.context.append(']')
+ else:
+ assert format == 'superscript'
+ suffix = '<sup>'
+ self.context.append('</sup>')
+ self.body.append(self.starttag(node, 'a', suffix,
+ CLASS='footnote-reference', href=href))
+
+ def depart_footnote_reference(self, node):
+ self.body.append(self.context.pop() + '</a>')
+
+ # just pass on generated text
+ def visit_generated(self, node):
+ pass
+
+ # Backwards-compatibility implementation:
+ # * Do not use <video>,
+ # * don't embed images,
+ # * use <object> instead of <img> for SVG.
+ # (SVG not supported by IE up to version 8,
+ # html4css1 strives for IE6 compatibility.)
+ object_image_types = {'.svg': 'image/svg+xml',
+ '.swf': 'application/x-shockwave-flash',
+ '.mp4': 'video/mp4',
+ '.webm': 'video/webm',
+ '.ogg': 'video/ogg',
+ }
+
+ def visit_image(self, node):
+ atts = {}
+ uri = node['uri']
+ ext = os.path.splitext(uri)[1].lower()
+ if ext in self.object_image_types:
+ atts['data'] = uri
+ atts['type'] = self.object_image_types[ext]
+ else:
+ atts['src'] = uri
+ atts['alt'] = node.get('alt', uri)
+ # image size
+ if 'width' in node:
+ atts['width'] = node['width']
+ if 'height' in node:
+ atts['height'] = node['height']
+ if 'scale' in node:
+ if (PIL and ('width' not in node or 'height' not in node)
+ and self.settings.file_insertion_enabled):
+ imagepath = self.uri2imagepath(uri)
+ try:
+ with PIL.Image.open(imagepath) as img:
+ img_size = img.size
+ except (OSError, UnicodeEncodeError):
+ pass # TODO: warn/info?
+ else:
+ self.settings.record_dependencies.add(
+ imagepath.replace('\\', '/'))
+ if 'width' not in atts:
+ atts['width'] = '%dpx' % img_size[0]
+ if 'height' not in atts:
+ atts['height'] = '%dpx' % img_size[1]
+ for att_name in 'width', 'height':
+ if att_name in atts:
+ match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
+ assert match
+ atts[att_name] = '%s%s' % (
+ float(match.group(1)) * (float(node['scale']) / 100),
+ match.group(2))
+ style = []
+ for att_name in 'width', 'height':
+ if att_name in atts:
+ if re.match(r'^[0-9.]+$', atts[att_name]):
+ # Interpret unitless values as pixels.
+ atts[att_name] += 'px'
+ style.append('%s: %s;' % (att_name, atts[att_name]))
+ del atts[att_name]
+ if style:
+ atts['style'] = ' '.join(style)
+ # No newlines around inline images.
+ if (not isinstance(node.parent, nodes.TextElement)
+ or isinstance(node.parent, nodes.reference)
+ and not isinstance(node.parent.parent, nodes.TextElement)):
+ suffix = '\n'
+ else:
+ suffix = ''
+ if 'align' in node:
+ atts['class'] = 'align-%s' % node['align']
+ if ext in self.object_image_types:
+ # do NOT use an empty tag: incorrect rendering in browsers
+ self.body.append(self.starttag(node, 'object', '', **atts)
+ + node.get('alt', uri) + '</object>' + suffix)
+ else:
+ self.body.append(self.emptytag(node, 'img', suffix, **atts))
+
+ def depart_image(self, node):
+ pass
+
+ # use table for footnote text,
+ # context added in footnote_backrefs.
+ def visit_label(self, node):
+ self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
+ CLASS='label'))
+
+ def depart_label(self, node):
+ self.body.append(f']{self.context.pop()}</td><td>{self.context.pop()}')
+
+ # ersatz for first/last pseudo-classes
+ def visit_list_item(self, node):
+ self.body.append(self.starttag(node, 'li', ''))
+ if len(node):
+ node[0]['classes'].append('first')
+
+ def depart_list_item(self, node):
+ self.body.append('</li>\n')
+
+ # use <tt> (not supported by HTML5),
+ # cater for limited styling options in CSS1 using hard-coded NBSPs
+ def visit_literal(self, node):
+ # special case: "code" role
+ classes = node['classes']
+ if 'code' in classes:
+ # filter 'code' from class arguments
+ node['classes'] = [cls for cls in classes if cls != 'code']
+ self.body.append(self.starttag(node, 'code', ''))
+ return
+ self.body.append(
+ self.starttag(node, 'tt', '', CLASS='docutils literal'))
+ text = node.astext()
+ for token in self.words_and_spaces.findall(text):
+ if token.strip():
+ # Protect text like "--an-option" and the regular expression
+ # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
+ if self.in_word_wrap_point.search(token):
+ self.body.append('<span class="pre">%s</span>'
+ % self.encode(token))
+ else:
+ self.body.append(self.encode(token))
+ elif token in ('\n', ' '):
+ # Allow breaks at whitespace:
+ self.body.append(token)
+ else:
+ # Protect runs of multiple spaces; the last space can wrap:
+ self.body.append('&nbsp;' * (len(token) - 1) + ' ')
+ self.body.append('</tt>')
+ # Content already processed:
+ raise nodes.SkipNode
+
+ def depart_literal(self, node):
+ # skipped unless literal element is from "code" role:
+ self.body.append('</code>')
+
+ # add newline after wrapper tags, don't use <code> for code
+ def visit_literal_block(self, node):
+ self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
+
+ def depart_literal_block(self, node):
+ self.body.append('\n</pre>\n')
+
+ # use table for option list
+ def visit_option_group(self, node):
+ atts = {}
+ if (self.settings.option_limit
+ and len(node.astext()) > self.settings.option_limit):
+ atts['colspan'] = 2
+ self.context.append('</tr>\n<tr><td>&nbsp;</td>')
+ else:
+ self.context.append('')
+ self.body.append(
+ self.starttag(node, 'td', CLASS='option-group', **atts))
+ self.body.append('<kbd>')
+ self.context.append(0) # count number of options
+
+ def depart_option_group(self, node):
+ self.context.pop()
+ self.body.append('</kbd></td>\n')
+ self.body.append(self.context.pop())
+
+ def visit_option_list(self, node):
+ self.body.append(
+ self.starttag(node, 'table', CLASS='docutils option-list',
+ frame="void", rules="none"))
+ self.body.append('<col class="option" />\n'
+ '<col class="description" />\n'
+ '<tbody valign="top">\n')
+
+ def depart_option_list(self, node):
+ self.body.append('</tbody>\n</table>\n')
+
+ def visit_option_list_item(self, node):
+ self.body.append(self.starttag(node, 'tr', ''))
+
+ def depart_option_list_item(self, node):
+ self.body.append('</tr>\n')
+
+ # Omit <p> tags to produce visually compact lists (less vertical
+ # whitespace) as CSS styling requires CSS2.
+ def should_be_compact_paragraph(self, node):
+ """
+ Determine if the <p> tags around paragraph ``node`` can be omitted.
+ """
+ if (isinstance(node.parent, nodes.document)
+ or isinstance(node.parent, nodes.compound)):
+ # Never compact paragraphs in document or compound.
+ return False
+ for key, value in node.attlist():
+ if (node.is_not_default(key)
+ and not (key == 'classes'
+ and value in ([], ['first'],
+ ['last'], ['first', 'last']))):
+ # Attribute which needs to survive.
+ return False
+ first = isinstance(node.parent[0], nodes.label) # skip label
+ for child in node.parent.children[first:]:
+ # only first paragraph can be compact
+ if isinstance(child, nodes.Invisible):
+ continue
+ if child is node:
+ break
+ return False
+ parent_length = len([n for n in node.parent if not isinstance(
+ n, (nodes.Invisible, nodes.label))])
+ if (self.compact_simple
+ or self.compact_field_list
+ or self.compact_p and parent_length == 1):
+ return True
+ return False
+
+ def visit_paragraph(self, node):
+ if self.should_be_compact_paragraph(node):
+ self.context.append('')
+ else:
+ self.body.append(self.starttag(node, 'p', ''))
+ self.context.append('</p>\n')
+
+ def depart_paragraph(self, node):
+ self.body.append(self.context.pop())
+ self.report_messages(node)
+
+ # ersatz for first/last pseudo-classes
+ def visit_sidebar(self, node):
+ self.body.append(
+ self.starttag(node, 'div', CLASS='sidebar'))
+ self.set_first_last(node)
+ self.in_sidebar = True
+
+ def depart_sidebar(self, node):
+ self.body.append('</div>\n')
+ self.in_sidebar = False
+
+ # <sub> not allowed in <pre>
+ def visit_subscript(self, node):
+ if isinstance(node.parent, nodes.literal_block):
+ self.body.append(self.starttag(node, 'span', '',
+ CLASS='subscript'))
+ else:
+ self.body.append(self.starttag(node, 'sub', ''))
+
+ def depart_subscript(self, node):
+ if isinstance(node.parent, nodes.literal_block):
+ self.body.append('</span>')
+ else:
+ self.body.append('</sub>')
+
+ # Use <h*> for subtitles (deprecated in HTML 5)
+ def visit_subtitle(self, node):
+ if isinstance(node.parent, nodes.sidebar):
+ self.body.append(self.starttag(node, 'p', '',
+ CLASS='sidebar-subtitle'))
+ self.context.append('</p>\n')
+ elif isinstance(node.parent, nodes.document):
+ self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
+ self.context.append('</h2>\n')
+ self.in_document_title = len(self.body)
+ elif isinstance(node.parent, nodes.section):
+ tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
+ self.body.append(
+ self.starttag(node, tag, '', CLASS='section-subtitle')
+ + self.starttag({}, 'span', '', CLASS='section-subtitle'))
+ self.context.append('</span></%s>\n' % tag)
+
+ def depart_subtitle(self, node):
+ self.body.append(self.context.pop())
+ if self.in_document_title:
+ self.subtitle = self.body[self.in_document_title:-1]
+ self.in_document_title = 0
+ self.body_pre_docinfo.extend(self.body)
+ self.html_subtitle.extend(self.body)
+ del self.body[:]
+
+ # <sup> not allowed in <pre> in HTML 4
+ def visit_superscript(self, node):
+ if isinstance(node.parent, nodes.literal_block):
+ self.body.append(self.starttag(node, 'span', '',
+ CLASS='superscript'))
+ else:
+ self.body.append(self.starttag(node, 'sup', ''))
+
+ def depart_superscript(self, node):
+ if isinstance(node.parent, nodes.literal_block):
+ self.body.append('</span>')
+ else:
+ self.body.append('</sup>')
+
+ # <tt> element deprecated in HTML 5
+ def visit_system_message(self, node):
+ self.body.append(self.starttag(node, 'div', CLASS='system-message'))
+ self.body.append('<p class="system-message-title">')
+ backref_text = ''
+ if len(node['backrefs']):
+ backrefs = node['backrefs']
+ if len(backrefs) == 1:
+ backref_text = ('; <em><a href="#%s">backlink</a></em>'
+ % backrefs[0])
+ else:
+ i = 1
+ backlinks = []
+ for backref in backrefs:
+ backlinks.append('<a href="#%s">%s</a>' % (backref, i))
+ i += 1
+ backref_text = ('; <em>backlinks: %s</em>'
+ % ', '.join(backlinks))
+ if node.hasattr('line'):
+ line = ', line %s' % node['line']
+ else:
+ line = ''
+ self.body.append('System Message: %s/%s '
+ '(<tt class="docutils">%s</tt>%s)%s</p>\n'
+ % (node['type'], node['level'],
+ self.encode(node['source']), line, backref_text))
+
+ def depart_system_message(self, node):
+ self.body.append('</div>\n')
+
+ # "hard coded" border setting
+ def visit_table(self, node):
+ self.context.append(self.compact_p)
+ self.compact_p = True
+ atts = {'border': 1}
+ classes = ['docutils', self.settings.table_style]
+ if 'align' in node:
+ classes.append('align-%s' % node['align'])
+ if 'width' in node:
+ atts['style'] = 'width: %s' % node['width']
+ self.body.append(
+ self.starttag(node, 'table', CLASS=' '.join(classes), **atts))
+
+ def depart_table(self, node):
+ self.compact_p = self.context.pop()
+ self.body.append('</table>\n')
+
+ # hard-coded vertical alignment
+ def visit_tbody(self, node):
+ self.body.append(self.starttag(node, 'tbody', valign='top'))
+
+ def depart_tbody(self, node):
+ self.body.append('</tbody>\n')
+
+ # no special handling of "details" in definition list
+ def visit_term(self, node):
+ self.body.append(self.starttag(node, 'dt', '',
+ classes=node.parent['classes'],
+ ids=node.parent['ids']))
+
+ def depart_term(self, node):
+ # Nest (optional) classifier(s) in the <dt> element
+ if node.next_node(nodes.classifier, descend=False, siblings=True):
+ return # skip (depart_classifier() calls this function again)
+ self.body.append('</dt>\n')
+
+ # hard-coded vertical alignment
+ def visit_thead(self, node):
+ self.body.append(self.starttag(node, 'thead', valign='bottom'))
+
+ def depart_thead(self, node):
+ self.body.append('</thead>\n')
+
+ # auxiliary method, called by visit_title()
+ # "with-subtitle" class, no ARIA roles
+ def section_title_tags(self, node):
+ classes = []
+ h_level = self.section_level + self.initial_header_level - 1
+ if (len(node.parent) >= 2
+ and isinstance(node.parent[1], nodes.subtitle)):
+ classes.append('with-subtitle')
+ if h_level > 6:
+ classes.append('h%i' % h_level)
+ tagname = 'h%i' % min(h_level, 6)
+ start_tag = self.starttag(node, tagname, '', classes=classes)
+ if node.hasattr('refid'):
+ atts = {}
+ atts['class'] = 'toc-backref'
+ atts['href'] = '#' + node['refid']
+ start_tag += self.starttag({}, 'a', '', **atts)
+ close_tag = '</a></%s>\n' % tagname
+ else:
+ close_tag = '</%s>\n' % tagname
+ return start_tag, close_tag
+
+
+class SimpleListChecker(writers._html_base.SimpleListChecker):
+
+ """
+ Raise `nodes.NodeFound` if non-simple list item is encountered.
+
+ Here "simple" means a list item containing nothing other than a single
+ paragraph, a simple list, or a paragraph followed by a simple list.
+ """
+
+ def visit_list_item(self, node):
+ children = []
+ for child in node.children:
+ if not isinstance(child, nodes.Invisible):
+ children.append(child)
+ if (children and isinstance(children[0], nodes.paragraph)
+ and (isinstance(children[-1], nodes.bullet_list)
+ or isinstance(children[-1], nodes.enumerated_list))):
+ children.pop()
+ if len(children) <= 1:
+ return
+ else:
+ raise nodes.NodeFound
+
+ # def visit_bullet_list(self, node):
+ # pass
+
+ # def visit_enumerated_list(self, node):
+ # pass
+
+ def visit_paragraph(self, node):
+ raise nodes.SkipNode
+
+ def visit_definition_list(self, node):
+ raise nodes.NodeFound
+
+ def visit_docinfo(self, node):
+ raise nodes.NodeFound
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/html4css1.css b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/html4css1.css
new file mode 100644
index 00000000..1d0d3e7c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/html4css1.css
@@ -0,0 +1,350 @@
+/*
+:Author: David Goodger (goodger@python.org)
+:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
+:Copyright: This stylesheet has been placed in the public domain.
+
+Default cascading style sheet for the HTML output of Docutils.
+Despite the name, some widely supported CSS2 features are used.
+
+See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
+customize this style sheet.
+*/
+
+/* used to remove borders from tables and images */
+.borderless, table.borderless td, table.borderless th {
+ border: 0 }
+
+table.borderless td, table.borderless th {
+ /* Override padding for "table.docutils td" with "! important".
+ The right padding separates the table cells. */
+ padding: 0 0.5em 0 0 ! important }
+
+.first {
+ /* Override more specific margin styles with "! important". */
+ margin-top: 0 ! important }
+
+.last, .with-subtitle {
+ margin-bottom: 0 ! important }
+
+.hidden {
+ display: none }
+
+.subscript {
+ vertical-align: sub;
+ font-size: smaller }
+
+.superscript {
+ vertical-align: super;
+ font-size: smaller }
+
+a.toc-backref {
+ text-decoration: none ;
+ color: black }
+
+blockquote.epigraph {
+ margin: 2em 5em ; }
+
+dl.docutils dd {
+ margin-bottom: 0.5em }
+
+object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
+ overflow: hidden;
+}
+
+/* Uncomment (and remove this text!) to get bold-faced definition list terms
+dl.docutils dt {
+ font-weight: bold }
+*/
+
+div.abstract {
+ margin: 2em 5em }
+
+div.abstract p.topic-title {
+ font-weight: bold ;
+ text-align: center }
+
+div.admonition, div.attention, div.caution, div.danger, div.error,
+div.hint, div.important, div.note, div.tip, div.warning {
+ margin: 2em ;
+ border: medium outset ;
+ padding: 1em }
+
+div.admonition p.admonition-title, div.hint p.admonition-title,
+div.important p.admonition-title, div.note p.admonition-title,
+div.tip p.admonition-title {
+ font-weight: bold ;
+ font-family: sans-serif }
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title, .code .error {
+ color: red ;
+ font-weight: bold ;
+ font-family: sans-serif }
+
+/* Uncomment (and remove this text!) to get reduced vertical space in
+ compound paragraphs.
+div.compound .compound-first, div.compound .compound-middle {
+ margin-bottom: 0.5em }
+
+div.compound .compound-last, div.compound .compound-middle {
+ margin-top: 0.5em }
+*/
+
+div.dedication {
+ margin: 2em 5em ;
+ text-align: center ;
+ font-style: italic }
+
+div.dedication p.topic-title {
+ font-weight: bold ;
+ font-style: normal }
+
+div.figure {
+ margin-left: 2em ;
+ margin-right: 2em }
+
+div.footer, div.header {
+ clear: both;
+ font-size: smaller }
+
+div.line-block {
+ display: block ;
+ margin-top: 1em ;
+ margin-bottom: 1em }
+
+div.line-block div.line-block {
+ margin-top: 0 ;
+ margin-bottom: 0 ;
+ margin-left: 1.5em }
+
+div.sidebar {
+ margin: 0 0 0.5em 1em ;
+ border: medium outset ;
+ padding: 1em ;
+ background-color: #ffffee ;
+ width: 40% ;
+ float: right ;
+ clear: right }
+
+div.sidebar p.rubric {
+ font-family: sans-serif ;
+ font-size: medium }
+
+div.system-messages {
+ margin: 5em }
+
+div.system-messages h1 {
+ color: red }
+
+div.system-message {
+ border: medium outset ;
+ padding: 1em }
+
+div.system-message p.system-message-title {
+ color: red ;
+ font-weight: bold }
+
+div.topic {
+ margin: 2em }
+
+h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
+h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
+ margin-top: 0.4em }
+
+h1.title {
+ text-align: center }
+
+h2.subtitle {
+ text-align: center }
+
+hr.docutils {
+ width: 75% }
+
+img.align-left, .figure.align-left, object.align-left, table.align-left {
+ clear: left ;
+ float: left ;
+ margin-right: 1em }
+
+img.align-right, .figure.align-right, object.align-right, table.align-right {
+ clear: right ;
+ float: right ;
+ margin-left: 1em }
+
+img.align-center, .figure.align-center, object.align-center {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+table.align-center {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.align-left {
+ text-align: left }
+
+.align-center {
+ clear: both ;
+ text-align: center }
+
+.align-right {
+ text-align: right }
+
+/* reset inner alignment in figures */
+div.align-right {
+ text-align: inherit }
+
+/* div.align-center * { */
+/* text-align: left } */
+
+.align-top {
+ vertical-align: top }
+
+.align-middle {
+ vertical-align: middle }
+
+.align-bottom {
+ vertical-align: bottom }
+
+ol.simple, ul.simple {
+ margin-bottom: 1em }
+
+ol.arabic {
+ list-style: decimal }
+
+ol.loweralpha {
+ list-style: lower-alpha }
+
+ol.upperalpha {
+ list-style: upper-alpha }
+
+ol.lowerroman {
+ list-style: lower-roman }
+
+ol.upperroman {
+ list-style: upper-roman }
+
+p.attribution {
+ text-align: right ;
+ margin-left: 50% }
+
+p.caption {
+ font-style: italic }
+
+p.credits {
+ font-style: italic ;
+ font-size: smaller }
+
+p.label {
+ white-space: nowrap }
+
+p.rubric {
+ font-weight: bold ;
+ font-size: larger ;
+ color: maroon ;
+ text-align: center }
+
+p.sidebar-title {
+ font-family: sans-serif ;
+ font-weight: bold ;
+ font-size: larger }
+
+p.sidebar-subtitle {
+ font-family: sans-serif ;
+ font-weight: bold }
+
+p.topic-title {
+ font-weight: bold }
+
+pre.address {
+ margin-bottom: 0 ;
+ margin-top: 0 ;
+ font: inherit }
+
+pre.literal-block, pre.doctest-block, pre.math, pre.code {
+ margin-left: 2em ;
+ margin-right: 2em }
+
+pre.code .ln { color: gray; } /* line numbers */
+pre.code, code { background-color: #eeeeee }
+pre.code .comment, code .comment { color: #5C6576 }
+pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
+pre.code .literal.string, code .literal.string { color: #0C5404 }
+pre.code .name.builtin, code .name.builtin { color: #352B84 }
+pre.code .deleted, code .deleted { background-color: #DEB0A1}
+pre.code .inserted, code .inserted { background-color: #A3D289}
+
+span.classifier {
+ font-family: sans-serif ;
+ font-style: oblique }
+
+span.classifier-delimiter {
+ font-family: sans-serif ;
+ font-weight: bold }
+
+span.interpreted {
+ font-family: sans-serif }
+
+span.option {
+ white-space: nowrap }
+
+span.pre {
+ white-space: pre }
+
+span.problematic, pre.problematic {
+ color: red }
+
+span.section-subtitle {
+ /* font-size relative to parent (h1..h6 element) */
+ font-size: 80% }
+
+table.citation {
+ border-left: solid 1px gray;
+ margin-left: 1px }
+
+table.docinfo {
+ margin: 2em 4em }
+
+table.docutils {
+ margin-top: 0.5em ;
+ margin-bottom: 0.5em }
+
+table.footnote {
+ border-left: solid 1px black;
+ margin-left: 1px }
+
+table.docutils td, table.docutils th,
+table.docinfo td, table.docinfo th {
+ padding-left: 0.5em ;
+ padding-right: 0.5em ;
+ vertical-align: top }
+
+table.docutils th.field-name, table.docinfo th.docinfo-name {
+ font-weight: bold ;
+ text-align: left ;
+ white-space: nowrap ;
+ padding-left: 0 }
+
+/* "booktabs" style (no vertical lines) */
+table.docutils.booktabs {
+ border: 0px;
+ border-top: 2px solid;
+ border-bottom: 2px solid;
+ border-collapse: collapse;
+}
+table.docutils.booktabs * {
+ border: 0px;
+}
+table.docutils.booktabs th {
+ border-bottom: thin solid;
+ text-align: left;
+}
+
+h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
+h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
+ font-size: 100% }
+
+ul.auto-toc {
+ list-style-type: none }
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/template.txt b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/template.txt
new file mode 100644
index 00000000..2591bce3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html4css1/template.txt
@@ -0,0 +1,8 @@
+%(head_prefix)s
+%(head)s
+%(stylesheet)s
+%(body_prefix)s
+%(body_pre_docinfo)s
+%(docinfo)s
+%(body)s
+%(body_suffix)s
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/__init__.py
new file mode 100644
index 00000000..c9bdf66c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/__init__.py
@@ -0,0 +1,393 @@
+# $Id: __init__.py 9539 2024-02-17 10:36:51Z milde $
+# :Author: Günter Milde <milde@users.sf.net>
+# Based on the html4css1 writer by David Goodger.
+# :Maintainer: docutils-develop@lists.sourceforge.net
+# :Copyright: © 2005, 2009, 2015 Günter Milde,
+# portions from html4css1 © David Goodger.
+# :License: Released under the terms of the `2-Clause BSD license`_, in short:
+#
+# Copying and distribution of this file, with or without modification,
+# are permitted in any medium without royalty provided the copyright
+# notice and this notice are preserved.
+# This file is offered as-is, without any warranty.
+#
+# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+
+# Use "best practice" as recommended by the W3C:
+# http://www.w3.org/2009/cheatsheet/
+
+"""
+Plain HyperText Markup Language document tree Writer.
+
+The output conforms to the `HTML 5` specification.
+
+The cascading style sheet "minimal.css" is required for proper viewing,
+the style sheet "plain.css" improves reading experience.
+"""
+__docformat__ = 'reStructuredText'
+
+from pathlib import Path
+
+from docutils import frontend, nodes
+from docutils.writers import _html_base
+
+
+class Writer(_html_base.Writer):
+
+ supported = ('html5', 'xhtml', 'html')
+ """Formats this writer supports."""
+
+ default_stylesheets = ['minimal.css', 'plain.css']
+ default_stylesheet_dirs = ['.', str(Path(__file__).parent)]
+ default_template = Path(__file__).parent / 'template.txt'
+
+ # use a copy of the parent spec with some modifications
+ settings_spec = frontend.filter_settings_spec(
+ _html_base.Writer.settings_spec,
+ template=(
+ f'Template file. (UTF-8 encoded, default: "{default_template}")',
+ ['--template'],
+ {'default': default_template, 'metavar': '<file>'}),
+ stylesheet_path=(
+ 'Comma separated list of stylesheet paths. '
+ 'Relative paths are expanded if a matching file is found in '
+ 'the --stylesheet-dirs. With --link-stylesheet, '
+ 'the path is rewritten relative to the output HTML file. '
+ '(default: "%s")' % ','.join(default_stylesheets),
+ ['--stylesheet-path'],
+ {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
+ 'validator': frontend.validate_comma_separated_list,
+ 'default': default_stylesheets}),
+ stylesheet_dirs=(
+ 'Comma-separated list of directories where stylesheets are found. '
+ 'Used by --stylesheet-path when expanding relative path '
+ 'arguments. (default: "%s")' % ','.join(default_stylesheet_dirs),
+ ['--stylesheet-dirs'],
+ {'metavar': '<dir[,dir,...]>',
+ 'validator': frontend.validate_comma_separated_list,
+ 'default': default_stylesheet_dirs}),
+ initial_header_level=(
+ 'Specify the initial header level. Does not affect document '
+ 'title & subtitle (see --no-doc-title). (default: 2 for "<h2>")',
+ ['--initial-header-level'],
+ {'choices': '1 2 3 4 5 6'.split(), 'default': '2',
+ 'metavar': '<level>'}),
+ no_xml_declaration=(
+ 'Omit the XML declaration (default).',
+ ['--no-xml-declaration'],
+ {'dest': 'xml_declaration', 'action': 'store_false'}),
+ )
+ settings_spec = settings_spec + (
+ 'HTML5 Writer Options',
+ '',
+ ((frontend.SUPPRESS_HELP, # Obsoleted by "--image-loading"
+ ['--embed-images'],
+ {'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ (frontend.SUPPRESS_HELP, # Obsoleted by "--image-loading"
+ ['--link-images'],
+ {'dest': 'embed_images', 'action': 'store_false'}),
+ ('Suggest at which point images should be loaded: '
+ '"embed", "link" (default), or "lazy".',
+ ['--image-loading'],
+ {'choices': ('embed', 'link', 'lazy'),
+ # 'default': 'link' # default set in _html_base.py
+ }),
+ ('Append a self-link to section headings.',
+ ['--section-self-link'],
+ {'default': False, 'action': 'store_true'}),
+ ('Do not append a self-link to section headings. (default)',
+ ['--no-section-self-link'],
+ {'dest': 'section_self_link', 'action': 'store_false'}),
+ )
+ )
+
+ config_section = 'html5 writer'
+
+ def __init__(self):
+ self.parts = {}
+ self.translator_class = HTMLTranslator
+
+
+class HTMLTranslator(_html_base.HTMLTranslator):
+ """
+ This writer generates `polyglot markup`: HTML5 that is also valid XML.
+
+ Safe subclassing: when overriding, treat ``visit_*`` and ``depart_*``
+ methods as a unit to prevent breaks due to internal changes. See the
+ docstring of docutils.writers._html_base.HTMLTranslator for details
+ and examples.
+ """
+
+ # self.starttag() arguments for the main document
+ documenttag_args = {'tagname': 'main'}
+
+ # add meta tag to fix rendering in mobile browsers
+ def __init__(self, document):
+ super().__init__(document)
+ self.meta.append('<meta name="viewport" '
+ 'content="width=device-width, initial-scale=1" />\n')
+
+ # <acronym> tag obsolete in HTML5. Use the <abbr> tag instead.
+ def visit_acronym(self, node):
+ # @@@ implementation incomplete ("title" attribute)
+ self.body.append(self.starttag(node, 'abbr', ''))
+
+ def depart_acronym(self, node):
+ self.body.append('</abbr>')
+
+ # no standard meta tag name in HTML5, use separate "author" meta tags
+ # https://www.w3.org/TR/html5/document-metadata.html#standard-metadata-names
+ def visit_authors(self, node):
+ self.visit_docinfo_item(node, 'authors', meta=False)
+ for subnode in node:
+ self.meta.append('<meta name="author" content='
+ f'"{self.attval(subnode.astext())}" />\n')
+
+ def depart_authors(self, node):
+ self.depart_docinfo_item()
+
+ # use the <figcaption> semantic tag.
+ def visit_caption(self, node):
+ if isinstance(node.parent, nodes.figure):
+ self.body.append('<figcaption>\n')
+ self.body.append(self.starttag(node, 'p', ''))
+
+ def depart_caption(self, node):
+ self.body.append('</p>\n')
+ # <figcaption> is closed in depart_figure(), as legend may follow.
+
+ # use HTML block-level tags if matching class value found
+ supported_block_tags = {'ins', 'del'}
+
+ def visit_container(self, node):
+ # If there is exactly one of the "supported block tags" in
+ # the list of class values, use it as tag name:
+ classes = node['classes']
+ tags = [cls for cls in classes
+ if cls in self.supported_block_tags]
+ if len(tags) == 1:
+ node.html5tagname = tags[0]
+ classes.remove(tags[0])
+ else:
+ node.html5tagname = 'div'
+ self.body.append(self.starttag(node, node.html5tagname,
+ CLASS='docutils container'))
+
+ def depart_container(self, node):
+ self.body.append(f'</{node.html5tagname}>\n')
+ del node.html5tagname
+
+ # no standard meta tag name in HTML5, use dcterms.rights
+ # see https://wiki.whatwg.org/wiki/MetaExtensions
+ def visit_copyright(self, node):
+ self.visit_docinfo_item(node, 'copyright', meta=False)
+ self.meta.append('<meta name="dcterms.rights" '
+ f'content="{self.attval(node.astext())}" />\n')
+
+ def depart_copyright(self, node):
+ self.depart_docinfo_item()
+
+ # no standard meta tag name in HTML5, use dcterms.date
+ def visit_date(self, node):
+ self.visit_docinfo_item(node, 'date', meta=False)
+ self.meta.append('<meta name="dcterms.date" '
+ f'content="{self.attval(node.astext())}" />\n')
+
+ def depart_date(self, node):
+ self.depart_docinfo_item()
+
+ # use new HTML5 <figure> and <figcaption> elements
+ def visit_figure(self, node):
+ atts = {}
+ if node.get('width'):
+ atts['style'] = f"width: {node['width']}"
+ if node.get('align'):
+ atts['class'] = f"align-{node['align']}"
+ self.body.append(self.starttag(node, 'figure', **atts))
+
+ def depart_figure(self, node):
+ if len(node) > 1:
+ self.body.append('</figcaption>\n')
+ self.body.append('</figure>\n')
+
+ # use HTML5 <footer> element
+ def visit_footer(self, node):
+ self.context.append(len(self.body))
+
+ def depart_footer(self, node):
+ start = self.context.pop()
+ footer = [self.starttag(node, 'footer')]
+ footer.extend(self.body[start:])
+ footer.append('</footer>\n')
+ self.footer.extend(footer)
+ self.body_suffix[:0] = footer
+ del self.body[start:]
+
+ # use HTML5 <header> element
+ def visit_header(self, node):
+ self.context.append(len(self.body))
+
+ def depart_header(self, node):
+ start = self.context.pop()
+ header = [self.starttag(node, 'header')]
+ header.extend(self.body[start:])
+ header.append('</header>\n')
+ self.body_prefix.extend(header)
+ self.header.extend(header)
+ del self.body[start:]
+
+ # use HTML text-level tags if matching class value found
+ supported_inline_tags = {'code', 'kbd', 'dfn', 'samp', 'var',
+ 'bdi', 'del', 'ins', 'mark', 'small',
+ 'b', 'i', 'q', 's', 'u'}
+
+ # Use `supported_inline_tags` if found in class values
+ def visit_inline(self, node):
+ classes = node['classes']
+ node.html5tagname = 'span'
+ # Special handling for "code" directive content
+ if (isinstance(node.parent, nodes.literal_block)
+ and 'code' in node.parent.get('classes')
+ or isinstance(node.parent, nodes.literal)
+ and getattr(node.parent, 'html5tagname', None) == 'code'):
+ if classes == ['ln']:
+ # line numbers are not part of the "fragment of computer code"
+ if self.body[-1] == '<code>':
+ del self.body[-1]
+ else:
+ self.body.append('</code>')
+ node.html5tagname = 'small'
+ else:
+ tags = [cls for cls in self.supported_inline_tags
+ if cls in classes]
+ if len(tags):
+ node.html5tagname = tags[0]
+ classes.remove(node.html5tagname)
+ self.body.append(self.starttag(node, node.html5tagname, ''))
+
+ def depart_inline(self, node):
+ self.body.append(f'</{node.html5tagname}>')
+ if (node.html5tagname == 'small' and node.get('classes') == ['ln']
+ and isinstance(node.parent, nodes.literal_block)):
+ self.body.append(f'<code data-lineno="{node.astext()}">')
+ del node.html5tagname
+
+ # place inside HTML5 <figcaption> element (together with caption)
+ def visit_legend(self, node):
+ if not isinstance(node.parent[1], nodes.caption):
+ self.body.append('<figcaption>\n')
+ self.body.append(self.starttag(node, 'div', CLASS='legend'))
+
+ def depart_legend(self, node):
+ self.body.append('</div>\n')
+ # <figcaption> closed in visit_figure()
+
+ # use HTML5 text-level tags if matching class value found
+ def visit_literal(self, node):
+ classes = node['classes']
+ html5tagname = 'span'
+ tags = [cls for cls in self.supported_inline_tags
+ if cls in classes]
+ if len(tags):
+ html5tagname = tags[0]
+ classes.remove(html5tagname)
+ if html5tagname == 'code':
+ node.html5tagname = html5tagname
+ self.body.append(self.starttag(node, html5tagname, ''))
+ return
+ self.body.append(
+ self.starttag(node, html5tagname, '', CLASS='docutils literal'))
+ text = node.astext()
+ # remove hard line breaks (except if in a parsed-literal block)
+ if not isinstance(node.parent, nodes.literal_block):
+ text = text.replace('\n', ' ')
+ # Protect text like ``--an-option`` and the regular expression
+ # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
+ for token in self.words_and_spaces.findall(text):
+ if token.strip() and self.in_word_wrap_point.search(token):
+ self.body.append(
+ f'<span class="pre">{self.encode(token)}</span>')
+ else:
+ self.body.append(self.encode(token))
+ self.body.append(f'</{html5tagname}>')
+ # Content already processed:
+ raise nodes.SkipNode
+
+ def depart_literal(self, node):
+ # skipped unless literal element is from "code" role:
+ self.depart_inline(node)
+
+ # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
+ # HTML5/polyglot recommends using both
+ def visit_meta(self, node):
+ if node.hasattr('lang'):
+ node['xml:lang'] = node['lang']
+ self.meta.append(self.emptytag(node, 'meta',
+ **node.non_default_attributes()))
+
+ def depart_meta(self, node):
+ pass
+
+ # no standard meta tag name in HTML5
+ def visit_organization(self, node):
+ self.visit_docinfo_item(node, 'organization', meta=False)
+
+ def depart_organization(self, node):
+ self.depart_docinfo_item()
+
+ # use the new HTML5 element <section>
+ def visit_section(self, node):
+ self.section_level += 1
+ self.body.append(
+ self.starttag(node, 'section'))
+
+ def depart_section(self, node):
+ self.section_level -= 1
+ self.body.append('</section>\n')
+
+ # use the new HTML5 element <aside>
+ def visit_sidebar(self, node):
+ self.body.append(
+ self.starttag(node, 'aside', CLASS='sidebar'))
+ self.in_sidebar = True
+
+ def depart_sidebar(self, node):
+ self.body.append('</aside>\n')
+ self.in_sidebar = False
+
+ # Use new HTML5 element <aside> or <nav>
+ # Add class value to <body>, if there is a ToC in the document
+ # (see responsive.css how this is used for a navigation sidebar).
+ def visit_topic(self, node):
+ atts = {'classes': ['topic']}
+ if 'contents' in node['classes']:
+ node.html5tagname = 'nav'
+ del atts['classes']
+ if isinstance(node.parent, nodes.document):
+ atts['role'] = 'doc-toc'
+ self.body_prefix[0] = '</head>\n<body class="with-toc">\n'
+ elif 'abstract' in node['classes']:
+ node.html5tagname = 'div'
+ atts['role'] = 'doc-abstract'
+ elif 'dedication' in node['classes']:
+ node.html5tagname = 'div'
+ atts['role'] = 'doc-dedication'
+ else:
+ node.html5tagname = 'aside'
+ self.body.append(self.starttag(node, node.html5tagname, **atts))
+
+ def depart_topic(self, node):
+ self.body.append(f'</{node.html5tagname}>\n')
+ del node.html5tagname
+
+ # append self-link
+ def section_title_tags(self, node):
+ start_tag, close_tag = super().section_title_tags(node)
+ ids = node.parent['ids']
+ if (ids and getattr(self.settings, 'section_self_link', None)
+ and not isinstance(node.parent, nodes.document)):
+ self_link = ('<a class="self-link" title="link to this section"'
+ f' href="#{ids[0]}"></a>')
+ close_tag = close_tag.replace('</h', self_link + '</h')
+ return start_tag, close_tag
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/italic-field-names.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/italic-field-names.css
new file mode 100644
index 00000000..75908529
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/italic-field-names.css
@@ -0,0 +1,26 @@
+/* italic-field-name.css: */
+/* Alternative style for Docutils field-lists */
+
+/* :Copyright: © 2023 Günter Milde. */
+/* :License: Released under the terms of the `2-Clause BSD license`_, */
+/* in short: */
+/* */
+/* Copying and distribution of this file, with or without modification, */
+/* are permitted in any medium without royalty provided the copyright */
+/* notice and this notice are preserved. */
+/* */
+/* This file is offered as-is, without any warranty. */
+/* */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause */
+
+/* In many contexts, a **bold** field name is too heavy styling. */
+/* Use *italic* instead:: */
+
+dl.field-list > dt {
+ font-weight: normal;
+ font-style: italic;
+}
+dl.field-list > dt > .colon {
+ font-style: normal;
+ padding-left: 0.05ex;
+}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/math.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/math.css
new file mode 100644
index 00000000..eb1ba72e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/math.css
@@ -0,0 +1,332 @@
+/*
+* math2html: convert LaTeX equations to HTML output.
+*
+* Copyright (C) 2009,2010 Alex Fernández
+* 2021 Günter Milde
+*
+* Released under the terms of the `2-Clause BSD license'_, in short:
+* Copying and distribution of this file, with or without modification,
+* are permitted in any medium without royalty provided the copyright
+* notice and this notice are preserved.
+* This file is offered as-is, without any warranty.
+*
+* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause
+*
+* Based on eLyXer: convert LyX source files to HTML output.
+* http://elyxer.nongnu.org/
+*
+*
+* CSS file for LaTeX formulas.
+*
+* References: http://www.zipcon.net/~swhite/docs/math/math.html
+* http://www.cs.tut.fi/~jkorpela/math/
+*/
+
+/* Formulas */
+.formula {
+ text-align: center;
+ margin: 1.2em 0;
+ line-height: 1.4;
+}
+span.formula {
+ white-space: nowrap;
+}
+div.formula {
+ padding: 0.5ex;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* Basic features */
+a.eqnumber {
+ display: inline-block;
+ float: right;
+ clear: right;
+ font-weight: bold;
+}
+span.unknown {
+ color: #800000;
+}
+span.ignored, span.arraydef {
+ display: none;
+}
+.phantom {
+ visibility: hidden;
+}
+.formula i {
+ letter-spacing: 0.1ex;
+}
+
+/* Alignment */
+.align-l {
+ text-align: left;
+}
+.align-r {
+ text-align: right;
+}
+.align-c {
+ text-align: center;
+}
+
+/* Structures */
+span.hspace {
+ display: inline-block;
+}
+span.overline, span.bar {
+ text-decoration: overline;
+}
+.fraction, .fullfraction, .textfraction {
+ display: inline-block;
+ vertical-align: middle;
+ text-align: center;
+}
+span.formula .fraction,
+.textfraction,
+span.smallmatrix {
+ font-size: 80%;
+ line-height: 1;
+}
+span.numerator {
+ display: block;
+ line-height: 1;
+}
+span.denominator {
+ display: block;
+ line-height: 1;
+ padding: 0ex;
+ border-top: thin solid;
+}
+.formula sub, .formula sup {
+ font-size: 80%;
+}
+sup.numerator, sup.unit {
+ vertical-align: 80%;
+}
+sub.denominator, sub.unit {
+ vertical-align: -20%;
+}
+span.smallsymbol {
+ font-size: 75%;
+ line-height: 75%;
+}
+span.boldsymbol {
+ font-weight: bold;
+}
+span.sqrt {
+ display: inline-block;
+ vertical-align: middle;
+ padding: 0.1ex;
+}
+sup.root {
+ position: relative;
+ left: 1.4ex;
+}
+span.radical {
+ display: inline-block;
+ padding: 0ex;
+ /* font-size: 160%; for DejaVu, not required with STIX */
+ line-height: 100%;
+ vertical-align: top;
+ vertical-align: middle;
+}
+
+span.root {
+ display: inline-block;
+ border-top: thin solid;
+ padding: 0ex;
+ vertical-align: middle;
+}
+div.formula .bigoperator,
+.displaystyle .bigoperator,
+.displaystyle .bigoperator {
+ line-height: 120%;
+ font-size: 140%;
+ padding-right: 0.2ex;
+}
+span.fraction .bigoperator,
+span.scriptstyle .bigoperator {
+ line-height: inherit;
+ font-size: inherit;
+ padding-right: 0;
+}
+span.bigdelimiter {
+ display: inline-block;
+}
+span.bigdelimiter.size1 {
+ transform: scale(1, 1.2);
+ line-height: 1.2;
+}
+span.bigdelimiter.size2 {
+ transform: scale(1, 1.62);
+ line-height: 1.62%;
+
+}
+span.bigdelimiter.size3 {
+ transform: scale(1, 2.05);
+ line-height: 2.05%;
+}
+span.bigdelimiter.size4 {
+ transform: scale(1, 2.47);
+ line-height: 2.47%;
+}
+/* vertically stacked sub and superscript */
+span.scripts {
+ display: inline-table;
+ vertical-align: middle;
+ padding-right: 0.2ex;
+}
+.script {
+ display: table-row;
+ text-align: left;
+ line-height: 150%;
+}
+span.limits {
+ display: inline-table;
+ vertical-align: middle;
+}
+.limit {
+ display: table-row;
+ line-height: 99%;
+}
+sup.limit, sub.limit {
+ line-height: 100%;
+}
+span.embellished,
+span.embellished > .base {
+ display: inline-block;
+}
+span.embellished > sup,
+span.embellished > sub {
+ display: inline-block;
+ font-size: 100%;
+ position: relative;
+ bottom: 0.3em;
+ width: 0px;
+}
+span.embellished > sub {
+ top: 0.4em;
+}
+
+/* Environments */
+span.array, span.bracketcases, span.binomial, span.environment {
+ display: inline-table;
+ text-align: center;
+ vertical-align: middle;
+}
+span.arrayrow, span.binomrow {
+ display: table-row;
+ padding: 0;
+ border: 0;
+}
+span.arraycell, span.bracket, span.case, span.binomcell, span.environmentcell {
+ display: table-cell;
+ padding: 0ex 0.2ex;
+ line-height: 1; /* 99%; */
+ border: 0ex;
+}
+.environment.align > .arrayrow > .arraycell.align-l {
+ padding-right: 2em;
+}
+
+/* Inline binomials */
+span.binom {
+ display: inline-block;
+ vertical-align: middle;
+ text-align: center;
+ font-size: 80%;
+}
+span.binomstack {
+ display: block;
+ padding: 0em;
+}
+
+/* Over- and underbraces */
+span.overbrace {
+ border-top: 2pt solid;
+}
+span.underbrace {
+ border-bottom: 2pt solid;
+}
+
+/* Stackrel */
+span.stackrel {
+ display: inline-block;
+ text-align: center;
+}
+span.upstackrel {
+ display: block;
+ padding: 0em;
+ font-size: 80%;
+ line-height: 64%;
+ position: relative;
+ top: 0.15em;
+
+}
+span.downstackrel {
+ display: block;
+ vertical-align: bottom;
+ padding: 0em;
+}
+
+/* Fonts */
+.formula {
+ font-family: STIX, "DejaVu Serif", "DejaVu Math TeX Gyre", serif;
+}
+span.radical, /* ensure correct size of square-root sign */
+span.integral { /* upright integral signs for better alignment of indices */
+ font-family: "STIXIntegralsUp", STIX;
+ /* font-size: 115%; match apparent size with DejaVu */
+}
+span.bracket {
+ /* some "STIX" and "DejaVu Math TeX Gyre" bracket pieces don't fit */
+ font-family: "DejaVu Serif", serif;
+}
+span.mathsf, span.textsf {
+ font-family: sans-serif;
+}
+span.mathrm, span.textrm {
+ font-family: STIX, "DejaVu Serif", "DejaVu Math TeX Gyre", serif;
+}
+span.mathtt, span.texttt {
+ font-family: monospace;
+}
+span.text, span.textnormal,
+span.mathsf, span.mathtt, span.mathrm {
+ font-style: normal;
+}
+span.fraktur {
+ font-family: "Lucida Blackletter", eufm10, blackletter;
+}
+span.blackboard {
+ font-family: Blackboard, msbm10, serif;
+}
+span.scriptfont {
+ font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
+ font-style: italic;
+}
+span.mathscr {
+ font-family: MathJax_Script, rsfs10, cursive;
+ font-style: italic;
+}
+span.textsc {
+ font-variant: small-caps;
+}
+span.textsl {
+ font-style: oblique;
+}
+
+/* Colors */
+span.colorbox {
+ display: inline-block;
+ padding: 5px;
+}
+span.fbox {
+ display: inline-block;
+ border: thin solid black;
+ padding: 2px;
+}
+span.boxed, span.framebox {
+ display: inline-block;
+ border: thin solid black;
+ padding: 5px;
+}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/minimal.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/minimal.css
new file mode 100644
index 00000000..66f0658d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/minimal.css
@@ -0,0 +1,293 @@
+/* Minimal style sheet for the HTML output of Docutils. */
+/* */
+/* :Author: Günter Milde, based on html4css1.css by David Goodger */
+/* :Id: $Id: minimal.css 9545 2024-02-17 10:37:56Z milde $ */
+/* :Copyright: © 2015, 2021 Günter Milde. */
+/* :License: Released under the terms of the `2-Clause BSD license`_, */
+/* in short: */
+/* */
+/* Copying and distribution of this file, with or without modification, */
+/* are permitted in any medium without royalty provided the copyright */
+/* notice and this notice are preserved. */
+/* */
+/* This file is offered as-is, without any warranty. */
+/* */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause */
+
+/* This CSS3 stylesheet defines rules for Docutils elements without */
+/* HTML equivalent. It is required to make the document semantics visible. */
+/* */
+/* .. _validates: http://jigsaw.w3.org/css-validator/validator$link */
+
+/* titles */
+p.topic-title,
+p.admonition-title,
+p.system-message-title {
+ font-weight: bold;
+}
+p.sidebar-title,
+p.rubric {
+ font-weight: bold;
+ font-size: larger;
+}
+p.rubric {
+ color: maroon;
+}
+p.subtitle,
+p.section-subtitle,
+p.sidebar-subtitle {
+ font-weight: bold;
+ margin-top: -0.5em;
+}
+h1 + p.subtitle {
+ font-size: 1.6em;
+}
+a.toc-backref {
+ color: inherit;
+ text-decoration: none;
+}
+
+/* Warnings, Errors */
+.system-messages h2,
+.system-message-title,
+pre.problematic,
+span.problematic {
+ color: red;
+}
+
+/* Inline Literals */
+.docutils.literal {
+ font-family: monospace;
+ white-space: pre-wrap;
+}
+/* do not wrap at hyphens and similar: */
+.literal > span.pre { white-space: nowrap; }
+
+/* keep line-breaks (\n) visible */
+.pre-wrap { white-space: pre-wrap; }
+
+/* Lists */
+
+/* compact and simple lists: no margin between items */
+.simple li, .simple ul, .simple ol,
+.compact li, .compact ul, .compact ol,
+.simple > li p, dl.simple > dd,
+.compact > li p, dl.compact > dd {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+/* Nested Paragraphs */
+p:first-child { margin-top: 0; }
+p:last-child { margin-bottom: 0; }
+details > p:last-child { margin-bottom: 1em; }
+
+/* Table of Contents */
+.contents ul.auto-toc { /* section numbers present */
+ list-style-type: none;
+}
+
+/* Enumerated Lists */
+ol.arabic { list-style: decimal }
+ol.loweralpha { list-style: lower-alpha }
+ol.upperalpha { list-style: upper-alpha }
+ol.lowerroman { list-style: lower-roman }
+ol.upperroman { list-style: upper-roman }
+
+/* Definition Lists and Derivatives */
+dt .classifier { font-style: italic }
+dt .classifier:before {
+ font-style: normal;
+ margin: 0.5em;
+ content: ":";
+}
+/* Field Lists and similar */
+/* bold field name, content starts on the same line */
+dl.field-list,
+dl.option-list,
+dl.docinfo {
+ display: flow-root;
+}
+dl.field-list > dt,
+dl.option-list > dt,
+dl.docinfo > dt {
+ font-weight: bold;
+ clear: left;
+ float: left;
+ margin: 0;
+ padding: 0;
+ padding-right: 0.25em;
+}
+/* Offset for field content (corresponds to the --field-name-limit option) */
+dl.field-list > dd,
+dl.option-list > dd,
+dl.docinfo > dd {
+ margin-left: 9em; /* ca. 14 chars in the test examples, fit all Docinfo fields */
+}
+/* start nested lists on new line */
+dd > dl:first-child,
+dd > ul:first-child,
+dd > ol:first-child {
+ clear: left;
+}
+/* start field-body on a new line after long field names */
+dl.field-list > dd > *:first-child,
+dl.option-list > dd > *:first-child
+{
+ display: inline-block;
+ width: 100%;
+ margin: 0;
+}
+
+/* Bibliographic Fields (docinfo) */
+dl.docinfo pre.address {
+ font: inherit;
+ margin: 0.5em 0;
+}
+dl.docinfo > dd.authors > p { margin: 0; }
+
+/* Option Lists */
+dl.option-list > dt { font-weight: normal; }
+span.option { white-space: nowrap; }
+
+/* Footnotes and Citations */
+
+.footnote, .citation { margin: 1em 0; } /* default paragraph skip (Firefox) */
+/* hanging indent */
+.citation { padding-left: 2em; }
+.footnote { padding-left: 1.7em; }
+.footnote.superscript { padding-left: 1.0em; }
+.citation > .label { margin-left: -2em; }
+.footnote > .label { margin-left: -1.7em; }
+.footnote.superscript > .label { margin-left: -1.0em; }
+
+.footnote > .label + *,
+.citation > .label + * {
+ display: inline-block;
+ margin-top: 0;
+ vertical-align: top;
+}
+.footnote > .backrefs + *,
+.citation > .backrefs + * {
+ margin-top: 0;
+}
+.footnote > .label + p, .footnote > .backrefs + p,
+.citation > .label + p, .citation > .backrefs + p {
+ display: inline;
+ vertical-align: inherit;
+}
+
+.backrefs { user-select: none; }
+.backrefs > a { font-style: italic; }
+
+/* superscript footnotes */
+a[role="doc-noteref"].superscript,
+.footnote.superscript > .label,
+.footnote.superscript > .backrefs {
+ vertical-align: super;
+ font-size: smaller;
+ line-height: 1;
+}
+a[role="doc-noteref"].superscript > .fn-bracket,
+.footnote.superscript > .label > .fn-bracket {
+ /* hide brackets in display but leave for copy/paste */
+ display: inline-block;
+ width: 0;
+ overflow: hidden;
+}
+[role="doc-noteref"].superscript + [role="doc-noteref"].superscript {
+ padding-left: 0.15em; /* separate consecutive footnote references */
+ /* TODO: unfortunately, "+" also selects with text between the references. */
+}
+
+/* Alignment */
+.align-left {
+ text-align: left;
+ margin-right: auto;
+}
+.align-center {
+ text-align: center;
+ margin-left: auto;
+ margin-right: auto;
+}
+.align-right {
+ text-align: right;
+ margin-left: auto;
+}
+.align-top { vertical-align: top; }
+.align-middle { vertical-align: middle; }
+.align-bottom { vertical-align: bottom; }
+
+/* reset inner alignment in figures and tables */
+figure.align-left, figure.align-right,
+table.align-left, table.align-center, table.align-right {
+ text-align: inherit;
+}
+
+/* Text Blocks */
+.topic { margin: 1em 2em; }
+.sidebar,
+.admonition,
+.system-message {
+ margin: 1em 2em;
+ border: thin solid;
+ padding: 0.5em 1em;
+}
+div.line-block { display: block; }
+div.line-block div.line-block, pre { margin-left: 2em; }
+
+/* Code line numbers: dropped when copying text from the page */
+pre.code .ln { display: none; }
+pre.code code:before {
+ content: attr(data-lineno); /* …, none) fallback not supported by any browser */
+ color: gray;
+}
+
+/* Tables */
+table {
+ border-collapse: collapse;
+}
+td, th {
+ border: thin solid silver;
+ padding: 0 1ex;
+}
+.borderless td, .borderless th {
+ border: 0;
+ padding: 0;
+ padding-right: 0.5em /* separate table cells */
+}
+
+table > caption, figcaption {
+ text-align: left;
+ margin-top: 0.2em;
+ margin-bottom: 0.2em;
+}
+table.captionbelow {
+ caption-side: bottom;
+}
+
+/* MathML (see "math.css" for --math-output=HTML) */
+math .boldsymbol { font-weight: bold; }
+math.boxed, math .boxed {padding: 0.25em; border: thin solid; }
+/* style table similar to AMS "align" or "aligned" environment: */
+mtable.cases > mtr > mtd { text-align: left; }
+mtable.ams-align > mtr > mtd { padding-left: 0; padding-right: 0; }
+mtable.ams-align > mtr > mtd:nth-child(2n) { text-align: left; }
+mtable.ams-align > mtr > mtd:nth-child(2n+1) { text-align: right; }
+mtable.ams-align > mtr > mtd:nth-child(2n+3) { padding-left: 2em; }
+.mathscr mi, mi.mathscr {
+ font-family: STIX, XITSMathJax_Script, rsfs10,
+ "Asana Math", Garamond, cursive;
+}
+
+/* Document Header and Footer */
+header { border-bottom: 1px solid black; }
+footer { border-top: 1px solid black; }
+
+/* Images are block-level by default in Docutils */
+/* New HTML5 block elements: set display for older browsers */
+img, svg, header, footer, main, aside, nav, section, figure, video, details {
+ display: block;
+}
+svg { width: auto; height: auto; } /* enable scaling of SVG images */
+/* inline images */
+p img, p svg, p video { display: inline; }
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/plain.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/plain.css
new file mode 100644
index 00000000..f0f089bb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/plain.css
@@ -0,0 +1,307 @@
+/* CSS31_ style sheet for the output of Docutils HTML writers. */
+/* Rules for easy reading and pre-defined style variants. */
+/* */
+/* :Author: Günter Milde, based on html4css1.css by David Goodger */
+/* :Id: $Id: plain.css 9615 2024-04-06 13:28:15Z milde $ */
+/* :Copyright: © 2015 Günter Milde. */
+/* :License: Released under the terms of the `2-Clause BSD license`_, */
+/* in short: */
+/* */
+/* Copying and distribution of this file, with or without modification, */
+/* are permitted in any medium without royalty provided the copyright */
+/* notice and this notice are preserved. */
+/* */
+/* This file is offered as-is, without any warranty. */
+/* */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause */
+/* .. _CSS3: https://www.w3.org/Style/CSS/ */
+
+
+/* Document Structure */
+/* ****************** */
+
+/* "page layout" */
+body {
+ margin: 0;
+ background-color: #dbdbdb;
+ --field-indent: 9em; /* default indent of fields in field lists */
+}
+main, footer, header {
+ line-height:1.6;
+ /* avoid long lines --> better reading */
+ /* optimum is 45…75 characters/line <http://webtypography.net/2.1.2> */
+ /* OTOH: lines should not be too short because of missing hyphenation, */
+ max-width: 50rem;
+ padding: 1px 2%; /* 1px on top avoids grey bar above title (mozilla) */
+ margin: auto;
+}
+main {
+ counter-reset: table figure;
+ background-color: white;
+}
+footer, header {
+ font-size: smaller;
+ padding: 0.5em 2%;
+ border: none;
+}
+
+/* Table of Contents */
+ul.auto-toc > li > p {
+ padding-left: 1em;
+ text-indent: -1em;
+}
+nav.contents ul {
+ padding-left: 1em;
+}
+main > nav.contents ul ul ul ul:not(.auto-toc) {
+ list-style-type: '\2B29\ ';
+}
+main > nav.contents ul ul ul ul ul:not(.auto-toc) {
+ list-style-type: '\2B1D\ ';
+}
+
+/* Transitions */
+hr.docutils {
+ width: 80%;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ clear: both;
+}
+
+/* Paragraphs */
+
+/* vertical space (parskip) */
+p, ol, ul, dl, li,
+.footnote, .citation,
+div > math,
+table {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+h1, h2, h3, h4, h5, h6,
+dd, details > p:last-child {
+ margin-bottom: 0.5em;
+}
+
+/* Lists */
+/* ===== */
+
+/* Definition Lists */
+/* Indent lists nested in definition lists */
+dd > ul:only-child, dd > ol:only-child { padding-left: 1em; }
+
+/* Description Lists */
+/* styled like in most dictionaries, encyclopedias etc. */
+dl.description {
+ display: flow-root;
+}
+dl.description > dt {
+ font-weight: bold;
+ clear: left;
+ float: left;
+ margin: 0;
+ padding: 0;
+ padding-right: 0.3em;
+}
+dl.description > dd:after {
+ display: table;
+ content: "";
+ clear: left; /* clearfix for empty descriptions */
+}
+
+/* Field Lists */
+
+dl.field-list > dd,
+dl.docinfo > dd {
+ margin-left: var(--field-indent); /* adapted in media queries or HTML */
+}
+
+/* example for custom field-name width */
+dl.field-list.narrow > dd {
+ --field-indent: 5em;
+}
+/* run-in: start field-body on same line after long field names */
+dl.field-list.run-in > dd p {
+ display: block;
+}
+
+/* Bibliographic Fields */
+
+/* generally, bibliographic fields use dl.docinfo */
+/* but dedication and abstract are placed into divs */
+div.abstract p.topic-title {
+ text-align: center;
+}
+div.dedication {
+ margin: 2em 5em;
+ text-align: center;
+ font-style: italic;
+}
+div.dedication p.topic-title {
+ font-style: normal;
+}
+
+/* disclosures */
+details { padding-left: 1em; }
+summary { margin-left: -1em; }
+
+/* Text Blocks */
+/* =========== */
+
+/* Literal Blocks */
+pre.literal-block, pre.doctest-block,
+pre.math, pre.code {
+ font-family: monospace;
+}
+
+/* Block Quotes and Topics */
+bockquote { margin: 1em 2em; }
+blockquote p.attribution,
+.topic p.attribution {
+ text-align: right;
+ margin-left: 20%;
+}
+
+/* Tables */
+/* ====== */
+
+/* th { vertical-align: bottom; } */
+
+table tr { text-align: left; }
+
+/* "booktabs" style (no vertical lines) */
+table.booktabs {
+ border: 0;
+ border-top: 2px solid;
+ border-bottom: 2px solid;
+ border-collapse: collapse;
+}
+table.booktabs * {
+ border: 0;
+}
+table.booktabs th {
+ border-bottom: thin solid;
+}
+
+/* numbered tables (counter defined in div.document) */
+table.numbered > caption:before {
+ counter-increment: table;
+ content: "Table " counter(table) ": ";
+ font-weight: bold;
+}
+
+/* Explicit Markup Blocks */
+/* ====================== */
+
+/* Footnotes and Citations */
+/* ----------------------- */
+
+/* line on the left */
+.footnote-list {
+ border-left: solid thin;
+ padding-left: 0.25em;
+}
+
+/* Directives */
+/* ---------- */
+
+/* Body Elements */
+/* ~~~~~~~~~~~~~ */
+
+/* Images and Figures */
+
+/* let content flow to the side of aligned images and figures */
+figure.align-left,
+img.align-left,
+svg.align-left,
+video.align-left,
+div.align-left,
+object.align-left {
+ clear: left;
+ float: left;
+ margin-right: 1em;
+}
+figure.align-right,
+img.align-right,
+svg.align-right,
+video.align-right,
+div.align-right,
+object.align-right {
+ clear: right;
+ float: right;
+ margin-left: 1em;
+}
+/* Stop floating sidebars, images and figures */
+h1, h2, h3, h4, footer, header { clear: both; }
+
+/* Numbered figures */
+figure.numbered > figcaption > p:before {
+ counter-increment: figure;
+ content: "Figure " counter(figure) ": ";
+ font-weight: bold;
+}
+
+/* Admonitions and System Messages */
+.caution p.admonition-title,
+.attention p.admonition-title,
+.danger p.admonition-title,
+.error p.admonition-title,
+.warning p.admonition-title,
+div.error {
+ color: red;
+}
+
+/* Sidebar */
+/* Move right. In a layout with fixed margins, */
+/* it can be moved into the margin. */
+aside.sidebar {
+ width: 30%;
+ max-width: 26em;
+ float: right;
+ clear: right;
+ margin-left: 1em;
+ margin-right: -1%;
+ background-color: #fffffa;
+}
+
+
+/* Code */
+pre.code { padding: 0.7ex }
+pre.code, code { background-color: #eeeeee }
+/* basic highlighting: for a complete scheme, see */
+/* https://docutils.sourceforge.io/sandbox/stylesheets/ */
+pre.code .comment, code .comment { color: #5C6576 }
+pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
+pre.code .literal.string, code .literal.string { color: #0C5404 }
+pre.code .name.builtin, code .name.builtin { color: #352B84 }
+pre.code .deleted, code .deleted { background-color: #DEB0A1}
+pre.code .inserted, code .inserted { background-color: #A3D289}
+
+
+/* Epigraph */
+/* Highlights */
+/* Pull-Quote */
+/* Compound Paragraph */
+/* Container */
+
+/* Inline Markup */
+/* ============= */
+
+sup, sub { line-height: 0.8; } /* do not add leading for lines with sup/sub */
+
+/* Inline Literals */
+/* possible values: normal, nowrap, pre, pre-wrap, pre-line */
+/* span.docutils.literal { white-space: pre-wrap; } */
+
+/* Hyperlink References */
+a { text-decoration: none; }
+
+/* External Targets */
+/* span.target.external */
+/* Internal Targets */
+/* span.target.internal */
+/* Footnote References */
+/* a[role="doc-noteref"] */
+/* Citation References */
+/* a.citation-reference */
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/responsive.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/responsive.css
new file mode 100644
index 00000000..234fa90b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/responsive.css
@@ -0,0 +1,486 @@
+/* CSS3_ style sheet for the output of Docutils HTML5 writer. */
+/* Generic responsive design for all screen sizes. */
+/* */
+/* :Author: Günter Milde */
+/* */
+/* :Id: $Id: responsive.css 9615 2024-04-06 13:28:15Z milde $ */
+/* :Copyright: © 2021 Günter Milde. */
+/* :License: Released under the terms of the `2-Clause BSD license`_, */
+/* in short: */
+/* */
+/* Copying and distribution of this file, with or without modification, */
+/* are permitted in any medium without royalty provided the copyright */
+/* notice and this notice are preserved. */
+/* */
+/* This file is offered as-is, without any warranty. */
+/* */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause */
+/* .. _CSS3: https://www.w3.org/Style/CSS/ */
+
+/* Note: */
+/* This style sheet is provisional: */
+/* the API is not settled and may change with any minor Docutils version. */
+
+
+
+/* General Settings */
+/* ================ */
+
+
+* { box-sizing: border-box; }
+
+body {
+ background-color: #fafaf6;
+ margin: auto;
+ --field-indent: 6.6em; /* indent of fields in field lists */
+ --sidebar-margin-right: 0; /* adapted in media queries below */
+}
+main {
+ counter-reset: figure table;
+}
+body > * {
+ background-color: white;
+ line-height: 1.6;
+ padding: 0.5rem calc(29% - 7.2rem); /* go from 5% to 15% (8.15em/54em) */
+ margin: auto;
+ max-width: 100rem;
+}
+sup, sub { /* avoid additional inter-line space for lines with sup/sub */
+ line-height: 1;
+}
+
+/* Vertical Space (Parskip) */
+p, ol, ul, dl, li,
+.topic,
+.footnote, .citation,
+div > math,
+table {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+h1, h2, h3, h4, h5, h6,
+dl > dd, details > p:last-child {
+ margin-bottom: 0.5em;
+}
+
+/* Indented Blocks */
+blockquote, figure, .topic {
+ margin: 1em 2%;
+ padding-left: 1em;
+}
+div.line-block div.line-block,
+pre, dd, dl.option-list {
+ margin-left: calc(2% + 1em);
+}
+
+/* Object styling */
+/* ============== */
+
+footer, header {
+ font-size: small;
+}
+
+/* Frontmatter */
+div.dedication {
+ padding: 0;
+ margin: 1.4em 0;
+ font-style: italic;
+ font-size: large;
+}
+.dedication p.topic-title {
+ display: none;
+}
+
+blockquote p.attribution,
+.topic p.attribution {
+ text-align: right;
+}
+
+/* Table of Contents */
+nav.contents ul {
+ padding-left: 1em;
+}
+ul.auto-toc > li > p { /* hanging indent */
+ padding-left: 1em;
+ text-indent: -1em;
+}
+main > nav.contents ul:not(.auto-toc) {
+ list-style-type: square;
+}
+main > nav.contents ul ul:not(.auto-toc) {
+ list-style-type: disc;
+}
+main > nav.contents ul ul ul:not(.auto-toc) {
+ list-style-type: '\2B29\ ';
+}
+main > nav.contents ul ul ul ul:not(.auto-toc) {
+ list-style-type: '\2B1D\ ';
+}
+main > nav.contents ul ul ul ul ul:not(.auto-toc) {
+ list-style-type: '\2B2A\ ';
+}
+nav.contents ul > li::marker {
+ color: grey;
+}
+
+/* Transitions */
+hr {
+ margin: 1em 10%;
+}
+
+/* Lists */
+
+dl.field-list.narrow, dl.docinfo, dl.option-list {
+ --field-indent: 2.4em;
+}
+
+ul, ol {
+ padding-left: 1.1em; /* indent by bullet width (Firefox, DejaVu fonts) */
+}
+dl.field-list > dd,
+dl.docinfo > dd {
+ margin-left: var(--field-indent); /* adapted in media queries or HTML */
+}
+dl.option-list > dd {
+ margin-left: 20%;
+}
+/* run-in: start field-body on same line after long field names */
+dl.field-list.run-in > dd p {
+ display: block;
+}
+/* "description style" like in most dictionaries, encyclopedias etc. */
+dl.description {
+ display: flow-root;
+}
+dl.description > dt {
+ clear: left;
+ float: left;
+ margin: 0;
+ padding: 0;
+ padding-right: 0.3em;
+ font-weight: bold;
+}
+dl.description > dd:after {
+ display: table;
+ content: "";
+ clear: left; /* clearfix for empty descriptions */
+}
+/* start lists nested in description/field lists on new line */
+dd > dl:first-child,
+dd > ul:first-child,
+dd > ol:first-child {
+ clear: left;
+}
+
+/* disclosures */
+details { padding-left: 1em; }
+summary { margin-left: -1em; }
+
+/* Footnotes and Citations */
+.footnote {
+ font-size: small;
+}
+
+/* Images, Figures, and Tables */
+figcaption,
+table > caption {
+ /* font-size: small; */
+ font-style: italic;
+}
+figcaption > .legend {
+ font-size: small;
+ font-style: initial;
+}
+figure.numbered > figcaption > p:before {
+ counter-increment: figure;
+ content: "Figure " counter(figure) ": ";
+ font-weight: bold;
+ font-style: initial;
+}
+
+table tr {
+ text-align: left;
+ vertical-align: baseline;
+}
+table.booktabs { /* "booktabs" style (no vertical lines) */
+ border-top: 2px solid;
+ border-bottom: 2px solid;
+}
+table.booktabs * {
+ border: 0;
+}
+table.booktabs th {
+ border-bottom: thin solid;
+}
+table.numbered > caption:before {
+ counter-increment: table;
+ content: "Table " counter(table) ": ";
+ font-weight: bold;
+ font-style: initial;
+}
+
+/* Admonitions and System Messages */
+.admonition,
+div.system-message {
+ border: thin solid silver;
+ margin: 1em 2%;
+ padding: 0.5em 1em;
+}
+.caution p.admonition-title,
+.attention p.admonition-title,
+.danger p.admonition-title,
+.warning p.admonition-title,
+div.error {
+ color: maroon;
+}
+div.system-message > p > span.literal {
+ overflow-wrap: break-word;
+}
+
+/* Literal and Code */
+pre.literal-block, pre.doctest{
+ padding: 0.2em;
+ overflow-x: auto;
+}
+.literal-block, .doctest, span.literal {
+ background-color: #f6f9f8;
+}
+.system-message span.literal {
+ background-color: inherit;
+}
+
+/* basic highlighting: for a complete scheme, see */
+/* https://docutils.sourceforge.io/sandbox/stylesheets/ */
+pre.code .comment, code .comment { color: #5C6576 }
+pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
+pre.code .literal.string, code .literal.string { color: #0C5404 }
+pre.code .name.builtin, code .name.builtin { color: #352B84 }
+pre.code .deleted, code .deleted { background-color: #DEB0A1}
+pre.code .inserted, code .inserted { background-color: #A3D289}
+
+/* Hyperlink References */
+a {
+ text-decoration: none; /* for chromium */
+ /* Wrap links at any place, if this is the only way to prevent overflow */
+ overflow-wrap: break-word;
+}
+.contents a, a.toc-backref, a.citation-reference {
+ overflow-wrap: inherit;
+}
+/* Undecorated Links (see also minimal.css) */
+/* a.citation-reference, */
+.citation a.fn-backref {
+ color: inherit;
+}
+a:hover {
+ text-decoration: underline;
+}
+*:hover > a.toc-backref:after,
+.topic-title:hover > a:after {
+ content: " \2191"; /* ↑ UPWARDS ARROW */
+ color: grey;
+}
+*:hover > a.self-link:after {
+ content: "\1F517"; /* LINK SYMBOL */
+ color: grey;
+ font-size: smaller;
+ margin-left: 0.2em;
+}
+/* highlight specific targets of the current URL */
+section:target > h2, section:target > h3, section:target > h4,
+section:target > h5, section:target > h6,
+span:target + h2, span:target + h3, span:target + h4,
+span:target + h5, span:target + h6,
+dt:target, span:target,
+.contents :target,
+.contents:target > .topic-title,
+[role="doc-biblioentry"]:target > .label,
+[role="doc-biblioref"]:target,
+[role="note"]:target, /* Docutils 0.18 ... 0.19 */
+[role="doc-footnote"]:target, /* Docutils >= 0.20 */
+[role="doc-noteref"]:target {
+ background-color: #d2e6ec;
+}
+
+/* Block Alignment */
+/* Let content flow to the side of aligned images and figures */
+
+/* no floats around this elements */
+footer, header, hr,
+h1, h2, h3 {
+ clear: both;
+}
+
+img.align-left,
+svg.align-left,
+video.align-left,
+figure.align-left,
+div.align-left,
+table.align-left {
+ margin-left: 0;
+ padding-left: 0;
+ margin-right: 0.5em;
+ clear: left;
+ float: left;
+}
+img.align-right,
+svg.align-right,
+video.align-right,
+figure.align-right,
+div.align-right,
+table.align-right {
+ margin-left: 0.5em;
+ margin-right: 0;
+ clear: right;
+ float: right;
+}
+
+/* Margin Elements */
+/* see below for screen size dependent rules */
+.sidebar,
+.marginal,
+.admonition.marginal {
+ max-width: 40%;
+ border: none;
+ background-color: #efefea;
+ margin: 0.5em var(--sidebar-margin-right) 0.5em 1em;
+ padding: 0.5em;
+ padding-left: 0.7em;
+ clear: right;
+ float: right;
+ font-size: small;
+}
+.sidebar {
+ width: 40%;
+}
+
+/* Adaptive page layout */
+/* ==================== */
+
+@media (max-width: 30em) {
+ /* Smaller margins and no floating elements for small screens */
+ /* (main text less than 40 characters/line) */
+ body > * {
+ padding: 0.5rem 5%;
+ line-height: 1.4
+ }
+ .sidebar,
+ .marginal,
+ .admonition.marginal {
+ width: auto;
+ max-width: 100%;
+ float: none;
+ }
+ dl.option-list,
+ pre {
+ margin-left: 0;
+ }
+ body {
+ --field-indent: 4em;
+ }
+ pre, pre * {
+ font-size: 0.9em;
+ /* overflow: auto; */
+ }
+}
+
+@media (min-width: 54em) {
+ /* Move ToC to the left */
+ /* Main text width before: 70% ≙ 35em ≙ 75…95 chrs (Dejavu/Times) */
+ /* after: ≳ 30em ≙ 54…70 chrs (Dejavu/Times) */
+ body.with-toc {
+ padding-left: 8%;
+ }
+ body.with-toc > * {
+ margin-left: 0;
+ padding-left: 22rem; /* fallback for webkit */
+ padding-left: min(22%, 22rem);
+ padding-right: 7%;
+ }
+ main > nav.contents { /* global ToC */
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: min(25%, 25em);
+ height: 100vh;
+ margin: 0;
+ background-color: #fafaf6;
+ padding: 1em 2% 0 2%;
+ overflow: auto;
+ }
+ main > nav.contents > * {
+ padding-left: 0;
+ line-height: 1.4;
+ }
+ main > nav.contents a {
+ color: inherit;
+ }
+}
+
+@media (min-width: 70em) {
+ body {
+ --field-indent: 9em;
+ }
+}
+
+@media (min-width: 77em) {
+ /* Move marginalia to 6rem from right border */
+ /* .sidebar, */
+ /* .marginal, */
+ /* .admonition.marginal { */
+ /* margin-right: calc(6rem - 15%); */
+ /* } */
+ /* BUG: margin is calculated for break point width */
+ /* workaround: variable + many breakpoints */
+ body > * {
+ padding-left: 18%;
+ padding-right: 28%; /* fallback for webkit */
+ padding-right: min(28%, 28rem);
+ --sidebar-margin-right: -20rem;
+ }
+ /* limit main text to ~ 50em ≙ 85…100 characters DejaVu rsp. …120 Times */
+ body.with-toc > * {
+ padding-left: min(22%, 22rem);
+ padding-right: calc(78% - 50rem); /* fallback for webkit */
+ padding-right: min(78% - 50rem, 28rem);
+ --sidebar-margin-right: 0;
+ }
+}
+
+@media (min-width: 85em) {
+ body.with-toc > * {
+ --sidebar-margin-right: -9rem;
+ }
+}
+
+@media (min-width: 90em) {
+ /* move marginalia into the margin */
+ body > * {
+ padding-left: min(22%, 22rem);
+ --sidebar-margin-right: -23rem;
+ }
+ body.with-toc > * {
+ --sidebar-margin-right: -14rem;
+ }
+}
+
+@media (min-width: 99em) {
+ /* move marginalia out of main text area */
+ body.with-toc > * {
+ --sidebar-margin-right: -20rem;
+ }
+ body > *, body.with-toc > * { /* for webkit */
+ padding-left: 22rem;
+ padding-right: 28rem;
+ }
+ .admonition.marginal,
+ .marginal {
+ width: 40%; /* make marginal figures, ... "full width" */
+ }
+}
+
+@media (min-width: 104em) {
+ body.with-toc > * {
+ --sidebar-margin-right: -23rem;
+ }
+}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/template.txt b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/template.txt
new file mode 100644
index 00000000..2591bce3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/template.txt
@@ -0,0 +1,8 @@
+%(head_prefix)s
+%(head)s
+%(stylesheet)s
+%(body_prefix)s
+%(body_pre_docinfo)s
+%(docinfo)s
+%(body)s
+%(body_suffix)s
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/tuftig.css b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/tuftig.css
new file mode 100644
index 00000000..cdedfded
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/html5_polyglot/tuftig.css
@@ -0,0 +1,566 @@
+/* CSS3_ style sheet for the output of Docutils HTML writers. */
+/* Rules inspired by Edward Tufte's layout design. */
+/* */
+/* :Author: Günter Milde */
+/* based on tufte.css_ by Dave Liepmann */
+/* and the tufte-latex_ package. */
+/* */
+/* :Id: $Id: tuftig.css 9503 2023-12-16 22:37:59Z milde $ */
+/* :Copyright: © 2020 Günter Milde. */
+/* :License: Released under the terms of the `2-Clause BSD license`_, */
+/* in short: */
+/* */
+/* Copying and distribution of this file, with or without modification, */
+/* are permitted in any medium without royalty provided the copyright */
+/* notice and this notice are preserved. */
+/* */
+/* This file is offered as-is, without any warranty. */
+/* */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause */
+/* .. _CSS3: https://www.w3.org/Style/CSS/ */
+/* .. _tufte.css: https://edwardtufte.github.io/tufte-css/ */
+/* .. _tufte-latex_: https://www.ctan.org/pkg/tufte-latex */
+
+
+/* General Settings */
+/* ================ */
+
+body {
+ font-family: Georgia, serif;
+ background-color: #fafaf6;
+ font-size: 1.2em;
+ line-height: 1.4;
+ margin: auto;
+}
+main {
+ counter-reset: figure table;
+}
+main, header, footer {
+ padding: 0.5em 5%;
+ background-color: #fefef8;
+ max-width: 100rem;
+}
+
+/* Spacing */
+
+/* vertical space (parskip) */
+p, ol, ul, dl, li,
+h1, h2, h3, h4, h5, h6,
+div.line-block,
+.topic,
+.footnote, .citation,
+table {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+dl > dd {
+ margin-bottom: 0.5em;
+}
+/* exceptions */
+p:first-child {
+ margin-top: 0;
+}
+p:last-child {
+ margin-bottom: 0;
+}
+
+/* Indented Blocks */
+blockquote,
+.topic {
+ /* background-color: Honeydew; */
+ margin: 0.5em 2%;
+ padding-left: 1em;
+}
+div.line-block div.line-block,
+dl.option-list,
+figure > img,
+pre.literal-block, pre.math,
+pre.doctest-block, pre.code {
+ /* background-color: LightCyan; */
+ margin-left: calc(2% + 1em);
+}
+
+/* Object styling */
+/* ============== */
+
+footer, header {
+ font-size: smaller;
+}
+
+/* Titles and Headings */
+
+h2, h3, h4, p.subtitle, p.section-subtitle,
+p.topic-title, p.sidebar-title, p.sidebar-subtitle {
+ font-weight: normal;
+ font-style: italic;
+ text-align: left;
+}
+.sectnum {
+ font-style: normal;
+}
+
+h1.title {
+ text-align: left;
+ margin-top: 2.4em;
+ margin-bottom: 2em;
+ font-size: 2.4em;
+}
+h1 + p.subtitle {
+ margin-top: -2em;
+ margin-bottom: 2em;
+ font-size: 2.0em;
+}
+section {
+ margin-top: 2em;
+}
+h2, .contents > p.topic-title {
+ font-size: 2.2em;
+}
+h2 + p.section-subtitle {
+ font-size: 1.6em;
+}
+h3 {
+ font-size: 1.2em;
+}
+h3 + p.section-subtitle {
+ font-size: 1.1em;
+}
+h4 {
+ font-size: 1em;
+}
+p.section-subtitle {
+ font-size: 1em;
+}
+
+/* Dedication and Abstract */
+div.dedication {
+ padding: 0;
+ margin-left: 0;
+ font-style: italic;
+ font-size: 1.2em;
+}
+/* div.abstract p.topic-title, */
+div.dedication p.topic-title {
+ display: none;
+}
+
+/* Attribution */
+blockquote p.attribution,
+.topic p.attribution {
+ text-align: right;
+}
+
+/* Table of Contents */
+nav.contents {
+ padding: 0;
+ font-style: italic;
+}
+ul.auto-toc > li > p {
+ padding-left: 1em;
+ text-indent: -1em;
+}
+nav.contents ul {
+ padding-left: 1em;
+}
+
+
+/* Transitions */
+hr {
+ border: 0;
+ border-top: 1px solid #ccc;
+ margin: 1em 10%;
+}
+
+/* Lists */
+/* Less indent per level */
+ul, ol {
+ padding-left: 1.1em;
+}
+dd {
+ margin-left: 1.5em;
+}
+dd > dl:first-child,
+dd > ul:first-child,
+dd > ol:first-child {
+ /* lists nested in definition/description/field lists */
+ clear: left;
+}
+
+dl.field-list > dd,
+dl.docinfo > dd,
+dl.option-list > dd {
+ margin-left: 4em;
+}
+/* example for custom field-name width */
+dl.field-list.narrow > dd {
+ margin-left: 3em;
+}
+/* run-in: start field-body on same line after long field names */
+dl.field-list.run-in > dd p {
+ display: block;
+}
+/* italic field name */
+dl.description > dt,
+dl.field-list > dt,
+dl.docinfo > dt {
+ font-weight: normal;
+ font-style: italic;
+}
+
+/* "description style" like in most dictionaries, encyclopedias etc. */
+dl.description > dt {
+ clear: left;
+ float: left;
+ margin: 0;
+ padding: 0;
+ padding-right: 0.5em;
+}
+dl.description > dd:after {
+ display: block;
+ content: "";
+ clear: both;
+}
+
+/* Citation list (style as description list) */
+.citation-list,
+.footnote-list {
+ display: contents;
+}
+.citation {
+ padding-left: 1.5em;
+}
+.citation .label {
+ margin-left: -1.5em;
+}
+
+/* Images and Figures */
+/* Caption to the left (if there is space) or below: */
+figure {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ margin: 0.5em 2%;
+ padding-left: 1em;
+}
+figure > img,
+figure.fullwidth > img {
+ margin: 0 0.5em 0.5em 0;
+ padding: 0;
+}
+figcaption {
+ font-size: 0.8em;
+}
+.fullwidth > figcaption {
+ font-size: inherit;
+}
+figure.numbered > figcaption > p:before {
+ counter-increment: figure;
+ content: "Figure " counter(figure) ": ";
+}
+
+/* Tables */
+table tr {
+ text-align: left;
+}
+/* th { vertical-align: bottom; } */
+/* "booktabs" style (no vertical lines) */
+table.booktabs {
+ border-top: 2px solid;
+ border-bottom: 2px solid;
+}
+table.booktabs * {
+ border: 0;
+}
+table.booktabs th {
+ border-bottom: thin solid;
+}
+table.numbered > caption:before {
+ counter-increment: table;
+ content: "Table " counter(table) ": ";
+}
+
+/* Admonitions and System Messages */
+.admonition, .system-message {
+ border-style: solid;
+ border-color: silver;
+ border-width: thin;
+ margin: 1em 0;
+ padding: 0.5em;
+}
+.caution p.admonition-title,
+.attention p.admonition-title,
+.danger p.admonition-title,
+.warning p.admonition-title,
+div.error {
+ color: maroon;
+}
+
+/* Literal and Code */
+pre.literal-block, pre.doctest-block,
+pre.math, pre.code {
+ /* font-family: Consolas, "Liberation Mono", Menlo, monospace; */
+ /* font-size: 0.9em; */
+ overflow: auto;
+}
+/* basic highlighting: for a complete scheme, see */
+/* https://docutils.sourceforge.io/sandbox/stylesheets/ */
+pre.code .comment, code .comment { color: #5C6576 }
+pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
+pre.code .literal.string, code .literal.string { color: #0C5404 }
+pre.code .name.builtin, code .name.builtin { color: #352B84 }
+pre.code .deleted, code .deleted { background-color: #DEB0A1}
+pre.code .inserted, code .inserted { background-color: #A3D289}
+
+.sans {
+ font-family: "Gill Sans", "Gill Sans MT", Calibri, "Lucida Sans", "Noto Sans", sans-serif;
+ letter-spacing: .02em;
+}
+
+/* Hyperlink References */
+/* underline that clears descenders */
+a {
+ color: inherit;
+}
+a:link {
+ text-decoration: underline;
+ /* text-decoration-skip-ink: auto; nonstandard selector */
+}
+/* undecorated links */
+.contents a:link, a.toc-backref:link, a.image-reference:link,
+a[role="doc-noteref"]:link, a[role="doc-backlink"]:link, .backrefs a:link,
+a.citation-reference:link,
+a[href^="#system-message"] {
+ text-decoration: none;
+}
+a:link:hover {
+ text-decoration: underline;
+}
+
+/* Block Alignment */
+/* Let content flow to the side of aligned images and figures */
+/* (does not work if the image/figure is a grid element). */
+
+/* no floats around this elements */
+footer, header,
+hr.docutils,
+h1, h2, h3, .contents > p.topic-title,
+.fullwidth {
+ clear: both;
+}
+
+img.align-left,
+svg.align-left,
+video.align-left,
+figure.align-left,
+div.align-left,
+table.align-left {
+ margin-left: 0;
+ padding-left: 0;
+ padding-right: 0.5em;
+ clear: left;
+ float: left;
+}
+figure.align-left > img {
+ margin-left: 0;
+ padding-left: 0;
+}
+
+img.align-right,
+svg.align-right,
+video.align-right,
+div.align-right {
+ padding-left: 0.5em;
+ clear: right;
+ float: right;
+}
+figure.align-right {
+ clear: right;
+ float: right;
+}
+figure.align-right > img {
+ justify-self: right;
+ padding: 0;
+}
+table.align-right {
+ margin-right: 2.5%;
+}
+
+figure.align-center {
+ align-content: center;
+ justify-content: center;
+}
+figure.align-center > img {
+ padding-left: 0;
+ justify-self: center;
+}
+
+/* Margin Elements */
+/* see below for screen size dependent rules */
+aside.sidebar,
+.marginal,
+.admonition.marginal,
+.topic.marginal {
+ background-color: #efefea;
+ box-sizing: border-box;
+ margin-left: 2%;
+ margin-right: 0;
+ padding: 0.5em;
+ font-size: 0.8em;
+}
+aside.sidebar {
+ background-color: inherit;
+}
+figure.marginal > figcaption {
+ font-size: 1em;
+}
+.footnote {
+ font-size: smaller;
+ overflow: auto;
+}
+
+/* Adaptive page layout */
+
+/* no floating for very small Screens */
+/* (main text up to ca. 40 characters/line) */
+@media (min-width: 35em) {
+ main, header, footer {
+ padding: 0.5em calc(15% - 3rem);
+ line-height: 1.6
+ }
+ aside.sidebar,
+ .marginal,
+ .admonition.marginal,
+ .topic.marginal {
+ max-width: 45%;
+ float: right;
+ clear: right;
+ }
+ dl.field-list > dd,
+ dl.docinfo > dd {
+ margin-left: 6em;
+ }
+ dl.option-list > dd {
+ margin-left: 6em;
+ }
+}
+
+/* 2 column layout with wide margin */
+@media (min-width: 65em) {
+ /* use the same grid for main, all sections, and figures */
+ main, section {
+ display: grid;
+ grid-template-columns: [content] minmax(0, 6fr)
+ [margin] 3fr [end];
+ grid-column-gap: calc(3em + 1%);
+ }
+ main > section, section > section {
+ grid-column: 1 / end;
+ }
+ main, header, footer {
+ padding-right: 5%; /* less padding right of margin-column */
+ }
+ section > figure {
+ display: contents; /* to place caption in the margin */
+ }
+ /* Main text elements */
+ main > *, section > *,
+ figure > img,
+ .footnote.align-left, /* override the placement in the margin */
+ .citation.align-left {
+ grid-column: content;
+ }
+ .citation.align-left {
+ font-size: 1em;
+ padding-left: 1.5em;
+ }
+ .citation.align-left .label {
+ margin-left: -1.5em;
+ }
+ figure > img { /* indent */
+ margin: 0.5em 2%;
+ padding-left: 1em;
+ }
+
+ /* Margin Elements */
+ /* Sidebar, Footnotes, Citations, Captions */
+ aside.sidebar,
+ .citation,
+ .footnote,
+ figcaption,
+ /* table > caption, does not work :(*/
+ .marginal,
+ .admonition.marginal,
+ .topic.marginal {
+ /* color: red; */
+ grid-column: margin;
+ width: auto;
+ max-width: 55em;
+ margin: 0.5em 0;
+ border: none;
+ padding: 0;
+ font-size: 0.8em;
+ text-align: initial; /* overwrite align-* */
+ background-color: inherit;
+ }
+ .admonition.marginal {
+ padding: 0.5em;
+ }
+ figure.marginal {
+ display: block;
+ margin: 0.5em 0;
+ }
+ .citation,
+ .footnote {
+ padding-left: 0;
+ }
+ .citation .label,
+ .footnote .label {
+ margin-left: 0;
+ }
+
+ /* Fullwidth Elements */
+ h1.title, p.subtitle,
+ dl.docinfo,
+ div.abstract,
+ div.dedication,
+ nav.contents,
+ aside.system-message,
+ pre,
+ .fullwidth,
+ .fullwidth img,
+ .fullwidth figcaption {
+ /* background-color: Linen; */
+ grid-column: content / end;
+ margin-right: calc(10% - 3rem);
+ max-width: 55em;
+ }
+}
+
+/* 3 column layout */
+
+@media (min-width: 100em) {
+ main, header, footer {
+ padding-left: 30%;
+ }
+ main > nav.contents {
+ position: fixed;
+ top: 0;
+ left: 0;
+ box-sizing: border-box;
+ width: 25%;
+ height: 100vh;
+ margin: 0;
+ background-color: #fafaf6;
+ padding: 5.5em 2%;
+ overflow: auto;
+ }
+ main > nav.contents > * {
+ padding-left: 0;
+ }
+}
+
+/* wrap URLs */
+/* a:link { */
+/* white-space: normal; */
+/* hyphens: none; */
+/* } */
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/__init__.py
new file mode 100644
index 00000000..d1960a79
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/__init__.py
@@ -0,0 +1,3323 @@
+# $Id: __init__.py 9581 2024-03-17 23:31:04Z milde $
+# Author: Engelbert Gruber, Günter Milde
+# Maintainer: docutils-develop@lists.sourceforge.net
+# Copyright: This module has been placed in the public domain.
+
+"""LaTeX2e document tree Writer."""
+
+__docformat__ = 'reStructuredText'
+
+# code contributions from several people included, thanks to all.
+# some named: David Abrahams, Julien Letessier, Lele Gaifax, and others.
+#
+# convention deactivate code by two # i.e. ##.
+
+from pathlib import Path
+import re
+import string
+from urllib.request import url2pathname
+import warnings
+try:
+ import roman
+except ImportError:
+ import docutils.utils.roman as roman
+
+from docutils import frontend, nodes, languages, writers, utils
+from docutils.transforms import writer_aux
+from docutils.utils.math import pick_math_environment, unichar2tex
+
+LATEX_WRITER_DIR = Path(__file__).parent
+
+
+class Writer(writers.Writer):
+
+ supported = ('latex', 'latex2e')
+ """Formats this writer supports."""
+
+ default_template = 'default.tex'
+ default_template_path = LATEX_WRITER_DIR
+ default_preamble = ('% PDF Standard Fonts\n'
+ '\\usepackage{mathptmx} % Times\n'
+ '\\usepackage[scaled=.90]{helvet}\n'
+ '\\usepackage{courier}')
+ table_style_values = [ # TODO: align-left, align-center, align-right, ??
+ 'booktabs', 'borderless', 'colwidths-auto',
+ 'nolines', 'standard']
+
+ settings_spec = (
+ 'LaTeX-Specific Options',
+ None,
+ (('Specify LaTeX documentclass. Default: "article".',
+ ['--documentclass'],
+ {'default': 'article', }),
+ ('Specify document options. Multiple options can be given, '
+ 'separated by commas. Default: "a4paper".',
+ ['--documentoptions'],
+ {'default': 'a4paper', }),
+ ('Format for footnote references: one of "superscript" or '
+ '"brackets". Default: "superscript".',
+ ['--footnote-references'],
+ {'choices': ['superscript', 'brackets'], 'default': 'superscript',
+ 'metavar': '<format>',
+ 'overrides': 'trim_footnote_reference_space'}),
+ ('Use \\cite command for citations. (future default)',
+ ['--use-latex-citations'],
+ {'default': None, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Use figure floats for citations '
+ '(might get mixed with real figures). (provisional default)',
+ ['--figure-citations'],
+ {'dest': 'use_latex_citations', 'action': 'store_false',
+ 'validator': frontend.validate_boolean}),
+ ('Format for block quote attributions: one of "dash" (em-dash '
+ 'prefix), "parentheses"/"parens", or "none". Default: "dash".',
+ ['--attribution'],
+ {'choices': ['dash', 'parentheses', 'parens', 'none'],
+ 'default': 'dash', 'metavar': '<format>'}),
+ ('Specify LaTeX packages/stylesheets. '
+ 'A style is referenced with "\\usepackage" if extension is '
+ '".sty" or omitted and with "\\input" else. '
+ ' Overrides previous --stylesheet and --stylesheet-path settings.',
+ ['--stylesheet'],
+ {'default': '', 'metavar': '<file[,file,...]>',
+ 'overrides': 'stylesheet_path',
+ 'validator': frontend.validate_comma_separated_list}),
+ ('Comma separated list of LaTeX packages/stylesheets. '
+ 'Relative paths are expanded if a matching file is found in '
+ 'the --stylesheet-dirs. With --link-stylesheet, '
+ 'the path is rewritten relative to the output *.tex file. ',
+ ['--stylesheet-path'],
+ {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
+ 'validator': frontend.validate_comma_separated_list}),
+ ('Link to the stylesheet(s) in the output file. (default)',
+ ['--link-stylesheet'],
+ {'dest': 'embed_stylesheet', 'action': 'store_false'}),
+ ('Embed the stylesheet(s) in the output file. '
+ 'Stylesheets must be accessible during processing. ',
+ ['--embed-stylesheet'],
+ {'default': False, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Comma-separated list of directories where stylesheets are found. '
+ 'Used by --stylesheet-path when expanding relative path arguments. '
+ 'Default: ".".',
+ ['--stylesheet-dirs'],
+ {'metavar': '<dir[,dir,...]>',
+ 'validator': frontend.validate_comma_separated_list,
+ 'default': ['.']}),
+ ('Customization by LaTeX code in the preamble. '
+ 'Default: select PDF standard fonts (Times, Helvetica, Courier).',
+ ['--latex-preamble'],
+ {'default': default_preamble}),
+ ('Specify the template file. Default: "%s".' % default_template,
+ ['--template'],
+ {'default': default_template, 'metavar': '<file>'}),
+ ('Table of contents by LaTeX. (default)',
+ ['--use-latex-toc'],
+ {'default': True, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Table of contents by Docutils (without page numbers).',
+ ['--use-docutils-toc'],
+ {'dest': 'use_latex_toc', 'action': 'store_false',
+ 'validator': frontend.validate_boolean}),
+ ('Add parts on top of the section hierarchy.',
+ ['--use-part-section'],
+ {'default': False, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Attach author and date to the document info table. (default)',
+ ['--use-docutils-docinfo'],
+ {'dest': 'use_latex_docinfo', 'action': 'store_false',
+ 'validator': frontend.validate_boolean}),
+ ('Attach author and date to the document title.',
+ ['--use-latex-docinfo'],
+ {'default': False, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ("Typeset abstract as topic. (default)",
+ ['--topic-abstract'],
+ {'dest': 'use_latex_abstract', 'action': 'store_false',
+ 'validator': frontend.validate_boolean}),
+ ("Use LaTeX abstract environment for the document's abstract.",
+ ['--use-latex-abstract'],
+ {'default': False, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Color of any hyperlinks embedded in text. '
+ 'Default: "blue" (use "false" to disable).',
+ ['--hyperlink-color'], {'default': 'blue'}),
+ ('Additional options to the "hyperref" package.',
+ ['--hyperref-options'], {'default': ''}),
+ ('Enable compound enumerators for nested enumerated lists '
+ '(e.g. "1.2.a.ii").',
+ ['--compound-enumerators'],
+ {'default': False, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Disable compound enumerators for nested enumerated lists. '
+ '(default)',
+ ['--no-compound-enumerators'],
+ {'action': 'store_false', 'dest': 'compound_enumerators'}),
+ ('Enable section ("." subsection ...) prefixes for compound '
+ 'enumerators. This has no effect without --compound-enumerators.',
+ ['--section-prefix-for-enumerators'],
+ {'default': None, 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Disable section prefixes for compound enumerators. (default)',
+ ['--no-section-prefix-for-enumerators'],
+ {'action': 'store_false', 'dest': 'section_prefix_for_enumerators'}),
+ ('Set the separator between section number and enumerator '
+ 'for compound enumerated lists. Default: "-".',
+ ['--section-enumerator-separator'],
+ {'default': '-', 'metavar': '<char>'}),
+ ('When possible, use the specified environment for literal-blocks. '
+ 'Default: "" (fall back to "alltt").',
+ ['--literal-block-env'],
+ {'default': ''}),
+ ('Deprecated alias for "--literal-block-env=verbatim".',
+ ['--use-verbatim-when-possible'],
+ {'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Table style. "standard" with horizontal and vertical lines, '
+ '"booktabs" (LaTeX booktabs style) only horizontal lines '
+ 'above and below the table and below the header, or "borderless". '
+ 'Default: "standard"',
+ ['--table-style'],
+ {'default': ['standard'],
+ 'metavar': '<format>',
+ 'action': 'append',
+ 'validator': frontend.validate_comma_separated_list,
+ 'choices': table_style_values}),
+ ('LaTeX graphicx package option. '
+ 'Possible values are "dvipdfmx", "dvips", "dvisvgm", '
+ '"luatex", "pdftex", and "xetex".'
+ 'Default: "".',
+ ['--graphicx-option'],
+ {'default': ''}),
+ ('LaTeX font encoding. '
+ 'Possible values are "", "T1" (default), "OT1", "LGR,T1" or '
+ 'any other combination of options to the `fontenc` package. ',
+ ['--font-encoding'],
+ {'default': 'T1'}),
+ ('Per default the latex-writer puts the reference title into '
+ 'hyperreferences. Specify "ref*" or "pageref*" to get the section '
+ 'number or the page number.',
+ ['--reference-label'],
+ {'default': ''}),
+ ('Specify style and database(s) for bibtex, for example '
+ '"--use-bibtex=unsrt,mydb1,mydb2". Provisional!',
+ ['--use-bibtex'],
+ {'default': '',
+ 'metavar': '<style,bibfile[,bibfile,...]>',
+ 'validator': frontend.validate_comma_separated_list}),
+ ('Use legacy functions with class value list for '
+ '\\DUtitle and \\DUadmonition.',
+ ['--legacy-class-functions'],
+ {'default': False,
+ 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Use \\DUrole and "DUclass" wrappers for class values. '
+ 'Place admonition content in an environment. (default)',
+ ['--new-class-functions'],
+ {'dest': 'legacy_class_functions',
+ 'action': 'store_false',
+ 'validator': frontend.validate_boolean}),
+ ('Use legacy algorithm to determine table column widths. '
+ '(provisional default)',
+ ['--legacy-column-widths'],
+ {'default': None,
+ 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ('Use new algorithm to determine table column widths. '
+ '(future default)',
+ ['--new-column-widths'],
+ {'dest': 'legacy_column_widths',
+ 'action': 'store_false',
+ 'validator': frontend.validate_boolean}),
+ # TODO: implement "latex footnotes" alternative
+ ('Footnotes with numbers/symbols by Docutils. (default) '
+ '(The alternative, --latex-footnotes, is not implemented yet.)',
+ ['--docutils-footnotes'],
+ {'default': True,
+ 'action': 'store_true',
+ 'validator': frontend.validate_boolean}),
+ ),
+ )
+
+ relative_path_settings = ('template',)
+ settings_defaults = {'sectnum_depth': 0} # updated by SectNum transform
+ config_section = 'latex2e writer'
+ config_section_dependencies = ('writers', 'latex writers')
+
+ head_parts = ('head_prefix', 'requirements', 'latex_preamble',
+ 'stylesheet', 'fallbacks', 'pdfsetup', 'titledata')
+ visitor_attributes = head_parts + ('title', 'subtitle',
+ 'body_pre_docinfo', 'docinfo',
+ 'dedication', 'abstract', 'body')
+
+ output = None
+ """Final translated form of `document`."""
+
+ def __init__(self):
+ writers.Writer.__init__(self)
+ self.translator_class = LaTeXTranslator
+
+ def get_transforms(self):
+ # Override parent method to add latex-specific transforms
+ return super().get_transforms() + [
+ # Convert specific admonitions to generic one
+ writer_aux.Admonitions,
+ # TODO: footnote collection transform
+ ]
+
+ def translate(self):
+ visitor = self.translator_class(self.document)
+ self.document.walkabout(visitor)
+ # copy parts
+ for part in self.visitor_attributes:
+ setattr(self, part, getattr(visitor, part))
+ # get template string from file
+ templatepath = Path(self.document.settings.template)
+ if not templatepath.exists():
+ templatepath = self.default_template_path / templatepath.name
+ template = templatepath.read_text(encoding='utf-8')
+ # fill template
+ self.assemble_parts() # create dictionary of parts
+ self.output = string.Template(template).substitute(self.parts)
+
+ def assemble_parts(self):
+ """Assemble the `self.parts` dictionary of output fragments."""
+ writers.Writer.assemble_parts(self)
+ for part in self.visitor_attributes:
+ lines = getattr(self, part)
+ if part in self.head_parts:
+ if lines:
+ lines.append('') # to get a trailing newline
+ self.parts[part] = '\n'.join(lines)
+ else:
+ # body contains inline elements, so join without newline
+ self.parts[part] = ''.join(lines)
+
+
+class Babel:
+ """Language specifics for LaTeX."""
+
+ # TeX (babel) language names:
+ # ! not all of these are supported by Docutils!
+ #
+ # based on LyX' languages file with adaptions to `BCP 47`_
+ # (https://www.rfc-editor.org/rfc/bcp/bcp47.txt) and
+ # http://www.tug.org/TUGboat/Articles/tb29-3/tb93miklavec.pdf
+ # * the key without subtags is the default
+ # * case is ignored
+ # cf. https://docutils.sourceforge.io/docs/howto/i18n.html
+ # https://www.w3.org/International/articles/language-tags/
+ # and http://www.iana.org/assignments/language-subtag-registry
+ language_codes = {
+ # code TeX/Babel-name comment
+ 'af': 'afrikaans',
+ 'ar': 'arabic',
+ # 'be': 'belarusian',
+ 'bg': 'bulgarian',
+ 'br': 'breton',
+ 'ca': 'catalan',
+ # 'cop': 'coptic',
+ 'cs': 'czech',
+ 'cy': 'welsh',
+ 'da': 'danish',
+ 'de': 'ngerman', # new spelling (de_1996)
+ 'de-1901': 'german', # old spelling
+ 'de-AT': 'naustrian',
+ 'de-AT-1901': 'austrian',
+ 'dsb': 'lowersorbian',
+ 'el': 'greek', # monotonic (el-monoton)
+ 'el-polyton': 'polutonikogreek',
+ 'en': 'english', # TeX' default language
+ 'en-AU': 'australian',
+ 'en-CA': 'canadian',
+ 'en-GB': 'british',
+ 'en-NZ': 'newzealand',
+ 'en-US': 'american',
+ 'eo': 'esperanto',
+ 'es': 'spanish',
+ 'et': 'estonian',
+ 'eu': 'basque',
+ # 'fa': 'farsi',
+ 'fi': 'finnish',
+ 'fr': 'french',
+ 'fr-CA': 'canadien',
+ 'ga': 'irish', # Irish Gaelic
+ # 'grc': # Ancient Greek
+ 'grc-ibycus': 'ibycus', # Ibycus encoding
+ 'gl': 'galician',
+ 'he': 'hebrew',
+ 'hr': 'croatian',
+ 'hsb': 'uppersorbian',
+ 'hu': 'magyar',
+ 'ia': 'interlingua',
+ 'id': 'bahasai', # Bahasa (Indonesian)
+ 'is': 'icelandic',
+ 'it': 'italian',
+ 'ja': 'japanese',
+ 'kk': 'kazakh',
+ 'la': 'latin',
+ 'lt': 'lithuanian',
+ 'lv': 'latvian',
+ 'mn': 'mongolian', # Mongolian, Cyrillic (mn-cyrl)
+ 'ms': 'bahasam', # Bahasa (Malay)
+ 'nb': 'norsk', # Norwegian Bokmal
+ 'nl': 'dutch',
+ 'nn': 'nynorsk', # Norwegian Nynorsk
+ 'no': 'norsk', # Norwegian (Bokmal)
+ 'pl': 'polish',
+ 'pt': 'portuges',
+ 'pt-BR': 'brazil',
+ 'ro': 'romanian',
+ 'ru': 'russian',
+ 'se': 'samin', # North Sami
+ 'sh-Cyrl': 'serbianc', # Serbo-Croatian, Cyrillic
+ 'sh-Latn': 'serbian', # Serbo-Croatian, Latin (cf. 'hr')
+ 'sk': 'slovak',
+ 'sl': 'slovene',
+ 'sq': 'albanian',
+ 'sr': 'serbianc', # Serbian, Cyrillic (contributed)
+ 'sr-Latn': 'serbian', # Serbian, Latin script
+ 'sv': 'swedish',
+ # 'th': 'thai',
+ 'tr': 'turkish',
+ 'uk': 'ukrainian',
+ 'vi': 'vietnam',
+ # zh-Latn: Chinese Pinyin
+ }
+ # normalize (downcase) keys
+ language_codes = {k.lower(): v for k, v in language_codes.items()}
+
+ warn_msg = 'Language "%s" not supported by LaTeX (babel)'
+
+ # "Active characters" are shortcuts that start a LaTeX macro and may need
+ # escaping for literals use. Characters that prevent literal use (e.g.
+ # starting accent macros like "a -> ä) will be deactivated if one of the
+ # defining languages is used in the document.
+ # Special cases:
+ # ~ (tilde) -- used in estonian, basque, galician, and old versions of
+ # spanish -- cannot be deactivated as it denotes a no-break space macro,
+ # " (straight quote) -- used in albanian, austrian, basque
+ # brazil, bulgarian, catalan, czech, danish, dutch, estonian,
+ # finnish, galician, german, icelandic, italian, latin, naustrian,
+ # ngerman, norsk, nynorsk, polish, portuges, russian, serbian, slovak,
+ # slovene, spanish, swedish, ukrainian, and uppersorbian --
+ # is escaped as ``\textquotedbl``.
+ active_chars = {
+ # TeX/Babel-name: active characters to deactivate
+ # 'breton': ':;!?' # ensure whitespace
+ # 'esperanto': '^',
+ # 'estonian': '~"`',
+ # 'french': ':;!?' # ensure whitespace
+ 'galician': '.<>', # also '~"'
+ # 'magyar': '`', # for special hyphenation cases
+ 'spanish': '.<>', # old versions also '~'
+ # 'turkish': ':!=' # ensure whitespace
+ }
+
+ def __init__(self, language_code, reporter=None):
+ self.reporter = reporter
+ self.language = self.language_name(language_code)
+ self.otherlanguages = {}
+
+ def __call__(self):
+ """Return the babel call with correct options and settings"""
+ languages = sorted(self.otherlanguages.keys())
+ languages.append(self.language or 'english')
+ self.setup = [r'\usepackage[%s]{babel}' % ','.join(languages)]
+ # Deactivate "active characters"
+ shorthands = []
+ for c in ''.join(self.active_chars.get(lng, '') for lng in languages):
+ if c not in shorthands:
+ shorthands.append(c)
+ if shorthands:
+ self.setup.append(r'\AtBeginDocument{\shorthandoff{%s}}'
+ % ''.join(shorthands))
+ # Including '~' in shorthandoff prevents its use as no-break space
+ if 'galician' in languages:
+ self.setup.append(r'\deactivatetilden % restore ~ in Galician')
+ if 'estonian' in languages:
+ self.setup.extend([r'\makeatletter',
+ r' \addto\extrasestonian{\bbl@deactivate{~}}',
+ r'\makeatother'])
+ if 'basque' in languages:
+ self.setup.extend([r'\makeatletter',
+ r' \addto\extrasbasque{\bbl@deactivate{~}}',
+ r'\makeatother'])
+ if (languages[-1] == 'english'
+ and 'french' in self.otherlanguages.keys()):
+ self.setup += ['% Prevent side-effects if French hyphenation '
+ 'patterns are not loaded:',
+ r'\frenchbsetup{StandardLayout}',
+ r'\AtBeginDocument{\selectlanguage{%s}'
+ r'\noextrasfrench}' % self.language]
+ return '\n'.join(self.setup)
+
+ def language_name(self, language_code):
+ """Return TeX language name for `language_code`"""
+ for tag in utils.normalize_language_tag(language_code):
+ try:
+ return self.language_codes[tag]
+ except KeyError:
+ pass
+ if self.reporter is not None:
+ self.reporter.warning(self.warn_msg % language_code)
+ return ''
+
+ def get_language(self):
+ # Obsolete, kept for backwards compatibility with Sphinx
+ return self.language
+
+
+# Building blocks for the latex preamble
+# --------------------------------------
+
+class SortableDict(dict):
+ """Dictionary with additional sorting methods
+
+ Tip: use key starting with with '_' for sorting before small letters
+ and with '~' for sorting after small letters.
+ """
+ def sortedkeys(self):
+ """Return sorted list of keys"""
+ return sorted(self.keys())
+
+ def sortedvalues(self):
+ """Return list of values sorted by keys"""
+ return [self[key] for key in self.sortedkeys()]
+
+
+# PreambleCmds
+# `````````````
+# A container for LaTeX code snippets that can be
+# inserted into the preamble if required in the document.
+#
+# .. The package 'makecmds' would enable shorter definitions using the
+# \providelength and \provideenvironment commands.
+# However, it is pretty non-standard (texlive-latex-extra).
+
+class PreambleCmds:
+ """Building blocks for the latex preamble."""
+
+
+# Requirements and Setup
+
+PreambleCmds.color = r"""\usepackage{color}"""
+
+PreambleCmds.float = r"""\usepackage{float} % extended float configuration
+\floatplacement{figure}{H} % place figures here definitely"""
+
+PreambleCmds.linking = r"""%% hyperlinks:
+\ifthenelse{\isundefined{\hypersetup}}{
+ \usepackage[%s]{hyperref}
+ \usepackage{bookmark}
+ \urlstyle{same} %% normal text font (alternatives: tt, rm, sf)
+}{}"""
+
+PreambleCmds.minitoc = r"""%% local table of contents
+\usepackage{minitoc}"""
+
+PreambleCmds.table = r"""\usepackage{longtable,ltcaption,array}
+\setlength{\extrarowheight}{2pt}
+\newlength{\DUtablewidth} % internal use in tables"""
+
+PreambleCmds.table_columnwidth = (
+ r'\newcommand{\DUcolumnwidth}[1]'
+ r'{\dimexpr#1\DUtablewidth-2\tabcolsep\relax}')
+
+PreambleCmds.textcomp = r"""\usepackage{textcomp} % text symbol macros"""
+# TODO? Options [force,almostfull] prevent spurious error messages,
+# see de.comp.text.tex/2005-12/msg01855
+
+# backwards compatibility definitions
+
+PreambleCmds.abstract_legacy = r"""
+% abstract title
+\providecommand*{\DUtitleabstract}[1]{\centerline{\textbf{#1}}}"""
+
+# see https://sourceforge.net/p/docutils/bugs/339/
+PreambleCmds.admonition_legacy = r"""
+% admonition (specially marked topic)
+\providecommand{\DUadmonition}[2][class-arg]{%
+ % try \DUadmonition#1{#2}:
+ \ifcsname DUadmonition#1\endcsname%
+ \csname DUadmonition#1\endcsname{#2}%
+ \else
+ \begin{center}
+ \fbox{\parbox{0.9\linewidth}{#2}}
+ \end{center}
+ \fi
+}"""
+
+PreambleCmds.error_legacy = r"""
+% error admonition title
+\providecommand*{\DUtitleerror}[1]{\DUtitle{\color{red}#1}}"""
+
+PreambleCmds.title_legacy = r"""
+% title for topics, admonitions, unsupported section levels, and sidebar
+\providecommand*{\DUtitle}[2][class-arg]{%
+ % call \DUtitle#1{#2} if it exists:
+ \ifcsname DUtitle#1\endcsname%
+ \csname DUtitle#1\endcsname{#2}%
+ \else
+ \smallskip\noindent\textbf{#2}\smallskip%
+ \fi
+}"""
+
+PreambleCmds.toc_list = r"""
+\providecommand*{\DUCLASScontents}{%
+ \renewenvironment{itemize}%
+ {\begin{list}{}{\setlength{\partopsep}{0pt}
+ \setlength{\parsep}{0pt}}
+ }%
+ {\end{list}}%
+}"""
+
+PreambleCmds.ttem = r"""
+% character width in monospaced font
+\newlength{\ttemwidth}
+\settowidth{\ttemwidth}{\ttfamily M}"""
+
+## PreambleCmds.caption = r"""% configure caption layout
+## \usepackage{caption}
+## \captionsetup{singlelinecheck=false}% no exceptions for one-liners"""
+
+
+# Definitions from docutils.sty::
+
+def _read_block(fp):
+ block = [next(fp)] # first line (empty)
+ for line in fp:
+ if not line.strip():
+ break
+ block.append(line)
+ return ''.join(block).rstrip()
+
+
+with open(LATEX_WRITER_DIR/'docutils.sty', encoding='utf-8') as fp:
+ for line in fp:
+ line = line.strip('% \n')
+ if not line.endswith('::'):
+ continue
+ block_name = line.rstrip(':')
+ if not block_name:
+ continue
+ definitions = _read_block(fp)
+ if block_name in ('color', 'float', 'table', 'textcomp'):
+ definitions = definitions.strip()
+ # print('Block: `%s`'% block_name)
+ # print(definitions)
+ setattr(PreambleCmds, block_name, definitions)
+
+
+# LaTeX encoding maps
+# -------------------
+# ::
+
+class CharMaps:
+ """LaTeX representations for active and Unicode characters."""
+
+ # characters that need escaping even in `alltt` environments:
+ alltt = {
+ ord('\\'): '\\textbackslash{}',
+ ord('{'): '\\{',
+ ord('}'): '\\}',
+ }
+ # characters that normally need escaping:
+ special = {
+ ord('#'): '\\#',
+ ord('$'): '\\$',
+ ord('%'): '\\%',
+ ord('&'): '\\&',
+ ord('~'): '\\textasciitilde{}',
+ ord('_'): '\\_',
+ ord('^'): '\\textasciicircum{}',
+ # straight double quotes are 'active' in many languages
+ ord('"'): '\\textquotedbl{}',
+ # Square brackets are ordinary chars and cannot be escaped with '\',
+ # so we put them in a group '{[}'. (Alternative: ensure that all
+ # macros with optional arguments are terminated with {} and text
+ # inside any optional argument is put in a group ``[{text}]``).
+ # Commands with optional args inside an optional arg must be put in a
+ # group, e.g. ``\item[{\hyperref[label]{text}}]``.
+ ord('['): '{[}',
+ ord(']'): '{]}',
+ # the soft hyphen is unknown in 8-bit text
+ # and not properly handled by XeTeX
+ 0x00AD: '\\-', # SOFT HYPHEN
+ }
+ # Unicode chars that are not recognized by LaTeX's utf8 encoding
+ unsupported_unicode = {
+ # TODO: ensure white space also at the beginning of a line?
+ # 0x00A0: '\\leavevmode\\nobreak\\vadjust{}~'
+ 0x2000: '\\enskip', # EN QUAD
+ 0x2001: '\\quad', # EM QUAD
+ 0x2002: '\\enskip', # EN SPACE
+ 0x2003: '\\quad', # EM SPACE
+ 0x2008: '\\,', # PUNCTUATION SPACE
+ 0x200b: '\\hspace{0pt}', # ZERO WIDTH SPACE
+ 0x202F: '\\,', # NARROW NO-BREAK SPACE
+ # 0x02d8: '\\\u{ }', # BREVE
+ 0x2011: '\\hbox{-}', # NON-BREAKING HYPHEN
+ 0x212b: '\\AA', # ANGSTROM SIGN
+ 0x21d4: '\\ensuremath{\\Leftrightarrow}', # LEFT RIGHT DOUBLE ARROW
+ 0x2260: '\\ensuremath{\\neq}', # NOT EQUAL TO
+ 0x2261: '\\ensuremath{\\equiv}', # IDENTICAL TO
+ 0x2264: '\\ensuremath{\\le}', # LESS-THAN OR EQUAL TO
+ 0x2265: '\\ensuremath{\\ge}', # GREATER-THAN OR EQUAL TO
+ # Docutils footnote symbols:
+ 0x2660: '\\ensuremath{\\spadesuit}',
+ 0x2663: '\\ensuremath{\\clubsuit}',
+ 0xfb00: 'ff', # LATIN SMALL LIGATURE FF
+ 0xfb01: 'fi', # LATIN SMALL LIGATURE FI
+ 0xfb02: 'fl', # LATIN SMALL LIGATURE FL
+ 0xfb03: 'ffi', # LATIN SMALL LIGATURE FFI
+ 0xfb04: 'ffl', # LATIN SMALL LIGATURE FFL
+ }
+ # Unicode chars that are recognized by LaTeX's utf8 encoding
+ utf8_supported_unicode = {
+ 0x00A0: '~', # NO-BREAK SPACE
+ 0x00AB: '\\guillemotleft{}', # LEFT-POINTING DOUBLE ANGLE QUOTATION
+ 0x00bb: '\\guillemotright{}', # RIGHT-POINTING DOUBLE ANGLE QUOTATION
+ 0x200C: '\\textcompwordmark{}', # ZERO WIDTH NON-JOINER
+ 0x2013: '\\textendash{}',
+ 0x2014: '\\textemdash{}',
+ 0x2018: '\\textquoteleft{}',
+ 0x2019: '\\textquoteright{}',
+ 0x201A: '\\quotesinglbase{}', # SINGLE LOW-9 QUOTATION MARK
+ 0x201C: '\\textquotedblleft{}',
+ 0x201D: '\\textquotedblright{}',
+ 0x201E: '\\quotedblbase{}', # DOUBLE LOW-9 QUOTATION MARK
+ 0x2030: '\\textperthousand{}', # PER MILLE SIGN
+ 0x2031: '\\textpertenthousand{}', # PER TEN THOUSAND SIGN
+ 0x2039: '\\guilsinglleft{}',
+ 0x203A: '\\guilsinglright{}',
+ 0x2423: '\\textvisiblespace{}', # OPEN BOX
+ 0x2020: '\\dag{}',
+ 0x2021: '\\ddag{}',
+ 0x2026: '\\dots{}',
+ 0x2122: '\\texttrademark{}',
+ }
+ # recognized with 'utf8', if textcomp is loaded
+ textcomp = {
+ # Latin-1 Supplement
+ 0x00a2: '\\textcent{}', # ¢ CENT SIGN
+ 0x00a4: '\\textcurrency{}', # ¤ CURRENCY SYMBOL
+ 0x00a5: '\\textyen{}', # ¥ YEN SIGN
+ 0x00a6: '\\textbrokenbar{}', # ¦ BROKEN BAR
+ 0x00a7: '\\textsection{}', # § SECTION SIGN
+ 0x00a8: '\\textasciidieresis{}', # ¨ DIAERESIS
+ 0x00a9: '\\textcopyright{}', # © COPYRIGHT SIGN
+ 0x00aa: '\\textordfeminine{}', # ª FEMININE ORDINAL INDICATOR
+ 0x00ac: '\\textlnot{}', # ¬ NOT SIGN
+ 0x00ae: '\\textregistered{}', # ® REGISTERED SIGN
+ 0x00af: '\\textasciimacron{}', # ¯ MACRON
+ 0x00b0: '\\textdegree{}', # ° DEGREE SIGN
+ 0x00b1: '\\textpm{}', # ± PLUS-MINUS SIGN
+ 0x00b2: '\\texttwosuperior{}', # ² SUPERSCRIPT TWO
+ 0x00b3: '\\textthreesuperior{}', # ³ SUPERSCRIPT THREE
+ 0x00b4: '\\textasciiacute{}', # ´ ACUTE ACCENT
+ 0x00b5: '\\textmu{}', # µ MICRO SIGN
+ 0x00b6: '\\textparagraph{}', # ¶ PILCROW SIGN # != \textpilcrow
+ 0x00b9: '\\textonesuperior{}', # ¹ SUPERSCRIPT ONE
+ 0x00ba: '\\textordmasculine{}', # º MASCULINE ORDINAL INDICATOR
+ 0x00bc: '\\textonequarter{}', # 1/4 FRACTION
+ 0x00bd: '\\textonehalf{}', # 1/2 FRACTION
+ 0x00be: '\\textthreequarters{}', # 3/4 FRACTION
+ 0x00d7: '\\texttimes{}', # × MULTIPLICATION SIGN
+ 0x00f7: '\\textdiv{}', # ÷ DIVISION SIGN
+ # others
+ 0x0192: '\\textflorin{}', # LATIN SMALL LETTER F WITH HOOK
+ 0x02b9: '\\textasciiacute{}', # MODIFIER LETTER PRIME
+ 0x02ba: '\\textacutedbl{}', # MODIFIER LETTER DOUBLE PRIME
+ 0x2016: '\\textbardbl{}', # DOUBLE VERTICAL LINE
+ 0x2022: '\\textbullet{}', # BULLET
+ 0x2032: '\\textasciiacute{}', # PRIME
+ 0x2033: '\\textacutedbl{}', # DOUBLE PRIME
+ 0x2035: '\\textasciigrave{}', # REVERSED PRIME
+ 0x2036: '\\textgravedbl{}', # REVERSED DOUBLE PRIME
+ 0x203b: '\\textreferencemark{}', # REFERENCE MARK
+ 0x203d: '\\textinterrobang{}', # INTERROBANG
+ 0x2044: '\\textfractionsolidus{}', # FRACTION SLASH
+ 0x2045: '\\textlquill{}', # LEFT SQUARE BRACKET WITH QUILL
+ 0x2046: '\\textrquill{}', # RIGHT SQUARE BRACKET WITH QUILL
+ 0x2052: '\\textdiscount{}', # COMMERCIAL MINUS SIGN
+ 0x20a1: '\\textcolonmonetary{}', # COLON SIGN
+ 0x20a3: '\\textfrenchfranc{}', # FRENCH FRANC SIGN
+ 0x20a4: '\\textlira{}', # LIRA SIGN
+ 0x20a6: '\\textnaira{}', # NAIRA SIGN
+ 0x20a9: '\\textwon{}', # WON SIGN
+ 0x20ab: '\\textdong{}', # DONG SIGN
+ 0x20ac: '\\texteuro{}', # EURO SIGN
+ 0x20b1: '\\textpeso{}', # PESO SIGN
+ 0x20b2: '\\textguarani{}', # GUARANI SIGN
+ 0x2103: '\\textcelsius{}', # DEGREE CELSIUS
+ 0x2116: '\\textnumero{}', # NUMERO SIGN
+ 0x2117: '\\textcircledP{}', # SOUND RECORDING COPYRIGHT
+ 0x211e: '\\textrecipe{}', # PRESCRIPTION TAKE
+ 0x2120: '\\textservicemark{}', # SERVICE MARK
+ 0x2122: '\\texttrademark{}', # TRADE MARK SIGN
+ 0x2126: '\\textohm{}', # OHM SIGN
+ 0x2127: '\\textmho{}', # INVERTED OHM SIGN
+ 0x212e: '\\textestimated{}', # ESTIMATED SYMBOL
+ 0x2190: '\\textleftarrow{}', # LEFTWARDS ARROW
+ 0x2191: '\\textuparrow{}', # UPWARDS ARROW
+ 0x2192: '\\textrightarrow{}', # RIGHTWARDS ARROW
+ 0x2193: '\\textdownarrow{}', # DOWNWARDS ARROW
+ 0x2212: '\\textminus{}', # MINUS SIGN
+ 0x2217: '\\textasteriskcentered{}', # ASTERISK OPERATOR
+ 0x221a: '\\textsurd{}', # SQUARE ROOT
+ 0x2422: '\\textblank{}', # BLANK SYMBOL
+ 0x25e6: '\\textopenbullet{}', # WHITE BULLET
+ 0x25ef: '\\textbigcircle{}', # LARGE CIRCLE
+ 0x266a: '\\textmusicalnote{}', # EIGHTH NOTE
+ 0x26ad: '\\textmarried{}', # MARRIAGE SYMBOL
+ 0x26ae: '\\textdivorced{}', # DIVORCE SYMBOL
+ 0x27e8: '\\textlangle{}', # MATHEMATICAL LEFT ANGLE BRACKET
+ 0x27e9: '\\textrangle{}', # MATHEMATICAL RIGHT ANGLE BRACKET
+ }
+ # Unicode chars that require a feature/package to render
+ pifont = {
+ 0x2665: '\\ding{170}', # black heartsuit
+ 0x2666: '\\ding{169}', # black diamondsuit
+ 0x2713: '\\ding{51}', # check mark
+ 0x2717: '\\ding{55}', # check mark
+ }
+ # TODO: greek alphabet ... ?
+ # see also LaTeX codec
+ # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/252124
+ # and unimap.py from TeXML
+
+
+class DocumentClass:
+ """Details of a LaTeX document class."""
+
+ def __init__(self, document_class, with_part=False):
+ self.document_class = document_class
+ self._with_part = with_part
+ self.sections = ['section', 'subsection', 'subsubsection',
+ 'paragraph', 'subparagraph']
+ if self.document_class in ('book', 'memoir', 'report',
+ 'scrbook', 'scrreprt'):
+ self.sections.insert(0, 'chapter')
+ if self._with_part:
+ self.sections.insert(0, 'part')
+
+ def section(self, level):
+ """Return the LaTeX section name for section `level`.
+
+ The name depends on the specific document class.
+ Level is 1,2,3..., as level 0 is the title.
+ """
+ if level <= len(self.sections):
+ return self.sections[level-1]
+ # unsupported levels
+ return 'DUtitle'
+
+ def latex_section_depth(self, depth):
+ """
+ Return LaTeX equivalent of Docutils section level `depth`.
+
+ Given the value of the ``:depth:`` option of the "contents" or
+ "sectnum" directive, return the corresponding value for the
+ LaTeX ``tocdepth`` or ``secnumdepth`` counters.
+ """
+ depth = min(depth, len(self.sections)) # limit to supported levels
+ if 'chapter' in self.sections:
+ depth -= 1
+ if self.sections[0] == 'part':
+ depth -= 1
+ return depth
+
+
+class Table:
+ """Manage a table while traversing.
+
+ Table style might be
+
+ :standard: horizontal and vertical lines
+ :booktabs: only horizontal lines (requires "booktabs" LaTeX package)
+ :borderless: no borders around table cells
+ :nolines: alias for borderless
+
+ :colwidths-auto: column widths determined by LaTeX
+ """
+ def __init__(self, translator, latex_type):
+ self._translator = translator
+ self._latex_type = latex_type
+ self.legacy_column_widths = False
+
+ self.close()
+ self._colwidths = []
+ self._rowspan = []
+ self._in_thead = 0
+
+ def open(self):
+ self._open = True
+ self._col_specs = []
+ self.caption = []
+ self._attrs = {}
+ self._in_head = False # maybe context with search
+
+ def close(self):
+ self._open = False
+ self._col_specs = None
+ self.caption = []
+ self._attrs = {}
+ self.stubs = []
+ self.colwidths_auto = False
+
+ def is_open(self):
+ return self._open
+
+ def set_table_style(self, node, settings):
+ self.legacy_column_widths = settings.legacy_column_widths
+ if 'align' in node:
+ self.set('align', node['align'])
+ # TODO: elif 'align' in classes/settings.table-style:
+ # self.set('align', ...)
+ borders = [cls.replace('nolines', 'borderless')
+ for cls in (['standard']
+ + settings.table_style
+ + node['classes'])
+ if cls in ('standard', 'booktabs', 'borderless', 'nolines')]
+ self.borders = borders[-1]
+ self.colwidths_auto = (('colwidths-auto' in node['classes']
+ or 'colwidths-auto' in settings.table_style)
+ and 'colwidths-given' not in node['classes']
+ and 'width' not in node)
+
+ def get_latex_type(self):
+ if self._latex_type == 'longtable' and not self.caption:
+ # do not advance the "table" counter (requires "ltcaption" package)
+ return 'longtable*'
+ return self._latex_type
+
+ def set(self, attr, value):
+ self._attrs[attr] = value
+
+ def get(self, attr):
+ if attr in self._attrs:
+ return self._attrs[attr]
+ return None
+
+ def get_vertical_bar(self):
+ if self.borders == 'standard':
+ return '|'
+ return ''
+
+ def get_opening(self, width=r'\linewidth'):
+ align_map = {'left': '[l]',
+ 'center': '[c]',
+ 'right': '[r]',
+ None: ''}
+ align = align_map.get(self.get('align'))
+ latex_type = self.get_latex_type()
+ if align and latex_type not in ("longtable", "longtable*"):
+ opening = [r'\noindent\makebox[\linewidth]%s{%%' % (align,),
+ r'\begin{%s}' % (latex_type,)]
+ else:
+ opening = [r'\begin{%s}%s' % (latex_type, align)]
+ if not self.colwidths_auto:
+ if self.borders == 'standard' and not self.legacy_column_widths:
+ opening.insert(-1, r'\setlength{\DUtablewidth}'
+ r'{\dimexpr%s-%i\arrayrulewidth\relax}%%'
+ % (width, len(self._col_specs)+1))
+ else:
+ opening.insert(-1, r'\setlength{\DUtablewidth}{%s}%%' % width)
+ return '\n'.join(opening)
+
+ def get_closing(self):
+ closing = []
+ if self.borders == 'booktabs':
+ closing.append(r'\bottomrule')
+ # elif self.borders == 'standard':
+ # closing.append(r'\hline')
+ closing.append(r'\end{%s}' % self.get_latex_type())
+ if (self.get('align')
+ and self.get_latex_type() not in ("longtable", "longtable*")):
+ closing.append('}')
+ return '\n'.join(closing)
+
+ def visit_colspec(self, node):
+ self._col_specs.append(node)
+ # "stubs" list is an attribute of the tgroup element:
+ self.stubs.append(node.attributes.get('stub'))
+
+ def get_colspecs(self, node):
+ """Return column specification for longtable.
+ """
+ bar = self.get_vertical_bar()
+ self._rowspan = [0] * len(self._col_specs)
+ if self.colwidths_auto:
+ self._colwidths = []
+ latex_colspecs = ['l'] * len(self._col_specs)
+ elif self.legacy_column_widths:
+ # use old algorithm for backwards compatibility
+ width = 80 # assumed standard line length
+ factor = 0.93 # do not make it full linewidth
+ # first see if we get too wide.
+ total_width = sum(node['colwidth']+1 for node in self._col_specs)
+ if total_width > width:
+ factor *= width / total_width
+ self._colwidths = [(factor * (node['colwidth']+1)/width)
+ + 0.005 for node in self._col_specs]
+ latex_colspecs = ['p{%.3f\\DUtablewidth}' % colwidth
+ for colwidth in self._colwidths]
+ else:
+ # No of characters corresponding to table width = 100%
+ # Characters/line with LaTeX article, A4, Times, default margins
+ # depends on character: M: 40, A: 50, x: 70, i: 120.
+ norm_length = 40
+ # Allowance to prevent unpadded columns like
+ # === ==
+ # ABC DE
+ # === ==
+ # getting too narrow:
+ if 'colwidths-given' not in node.parent.parent['classes']:
+ allowance = 1
+ else:
+ allowance = 0 # "widths" option specified, use exact ratio
+ self._colwidths = [(node['colwidth']+allowance)/norm_length
+ for node in self._col_specs]
+ total_width = sum(self._colwidths)
+ # Limit to 100%, force 100% if table width is specified:
+ if total_width > 1 or 'width' in node.parent.parent.attributes:
+ self._colwidths = [colwidth/total_width
+ for colwidth in self._colwidths]
+ latex_colspecs = ['p{\\DUcolumnwidth{%.3f}}' % colwidth
+ for colwidth in self._colwidths]
+ return bar + bar.join(latex_colspecs) + bar
+
+ def get_column_width(self):
+ """Return columnwidth for current cell (not multicell)."""
+ try:
+ if self.legacy_column_widths:
+ return '%.2f\\DUtablewidth'%self._colwidths[self._cell_in_row]
+ return '\\DUcolumnwidth{%.2f}'%self._colwidths[self._cell_in_row]
+ except IndexError:
+ return '*'
+
+ def get_multicolumn_width(self, start, len_):
+ """Return sum of columnwidths for multicell."""
+ try:
+ multicol_width = sum(self._colwidths[start + co]
+ for co in range(len_))
+ if self.legacy_column_widths:
+ return 'p{%.2f\\DUtablewidth}' % multicol_width
+ return 'p{\\DUcolumnwidth{%.3f}}' % multicol_width
+ except IndexError:
+ return 'l'
+
+ def get_caption(self):
+ """Deprecated. Will be removed in Docutils 0.22."""
+ warnings.warn('`writers.latex2e.Table.get_caption()` is obsolete'
+ ' and will be removed in Docutils 0.22.',
+ DeprecationWarning, stacklevel=2)
+
+ if not self.caption:
+ return ''
+ caption = ''.join(self.caption)
+ if 1 == self._translator.thead_depth():
+ return r'\caption{%s}\\' '\n' % caption
+ return r'\caption[]{%s (... continued)}\\' '\n' % caption
+
+ def need_recurse(self):
+ if self._latex_type == 'longtable':
+ return 1 == self._translator.thead_depth()
+ return 0
+
+ def visit_thead(self):
+ self._in_thead += 1
+ if self.borders == 'standard':
+ return ['\\hline\n']
+ elif self.borders == 'booktabs':
+ return ['\\toprule\n']
+ return []
+
+ def depart_thead(self):
+ a = []
+ ## if self.borders == 'standard':
+ ## a.append('\\hline\n')
+ if self.borders == 'booktabs':
+ a.append('\\midrule\n')
+ if self._latex_type == 'longtable':
+ if 1 == self._translator.thead_depth():
+ a.append('\\endfirsthead\n')
+ else:
+ n_c = len(self._col_specs)
+ a.append('\\endhead\n')
+ # footer on all but last page (if it fits):
+ twidth = sum(node['colwidth']+2 for node in self._col_specs)
+ if twidth > 30 or (twidth > 12 and not self.colwidths_auto):
+ a.append(r'\multicolumn{%d}{%s}'
+ % (n_c, self.get_multicolumn_width(0, n_c))
+ + r'{\raggedleft\ldots continued on next page}\\'
+ + '\n')
+ a.append('\\endfoot\n\\endlastfoot\n')
+ # for longtable one could add firsthead, foot and lastfoot
+ self._in_thead -= 1
+ return a
+
+ def visit_row(self):
+ self._cell_in_row = 0
+
+ def depart_row(self):
+ res = [' \\\\\n']
+ self._cell_in_row = None # remove cell counter
+ for i in range(len(self._rowspan)):
+ if self._rowspan[i] > 0:
+ self._rowspan[i] -= 1
+
+ if self.borders == 'standard':
+ rowspans = [i+1 for i in range(len(self._rowspan))
+ if self._rowspan[i] <= 0]
+ if len(rowspans) == len(self._rowspan):
+ res.append('\\hline\n')
+ else:
+ cline = ''
+ rowspans.reverse()
+ # TODO merge clines
+ while True:
+ try:
+ c_start = rowspans.pop()
+ except IndexError:
+ break
+ cline += '\\cline{%d-%d}\n' % (c_start, c_start)
+ res.append(cline)
+ return res
+
+ def set_rowspan(self, cell, value):
+ try:
+ self._rowspan[cell] = value
+ except IndexError:
+ pass
+
+ def get_rowspan(self, cell):
+ try:
+ return self._rowspan[cell]
+ except IndexError:
+ return 0
+
+ def get_entry_number(self):
+ return self._cell_in_row
+
+ def visit_entry(self):
+ self._cell_in_row += 1
+
+ def is_stub_column(self):
+ if len(self.stubs) >= self._cell_in_row:
+ return self.stubs[self._cell_in_row]
+ return False
+
+
+class LaTeXTranslator(nodes.NodeVisitor):
+ """
+ Generate code for 8-bit LaTeX from a Docutils document tree.
+
+ See the docstring of docutils.writers._html_base.HTMLTranslator for
+ notes on and examples of safe subclassing.
+ """
+
+ # When options are given to the documentclass, latex will pass them
+ # to other packages, as done with babel.
+ # Dummy settings might be taken from document settings
+
+ # Generate code for typesetting with 8-bit latex/pdflatex vs.
+ # xelatex/lualatex engine. Overwritten by the XeTeX writer
+ is_xetex = False
+
+ # Config setting defaults
+ # -----------------------
+
+ # TODO: use mixins for different implementations.
+ # list environment for docinfo. else tabularx
+ ## use_optionlist_for_docinfo = False # TODO: NOT YET IN USE
+
+ # Use compound enumerations (1.A.1.)
+ compound_enumerators = False
+
+ # If using compound enumerations, include section information.
+ section_prefix_for_enumerators = False
+
+ # This is the character that separates the section ("." subsection ...)
+ # prefix from the regular list enumerator.
+ section_enumerator_separator = '-'
+
+ # Auxiliary variables
+ # -------------------
+
+ has_latex_toc = False # is there a toc in the doc? (needed by minitoc)
+ section_level = 0
+
+ # Flags to encode():
+ # inside citation reference labels underscores dont need to be escaped
+ inside_citation_reference_label = False
+ verbatim = False # do not encode
+ insert_non_breaking_blanks = False # replace blanks by "~"
+ insert_newline = False # add latex newline commands
+ literal = False # literal text (block or inline)
+ alltt = False # inside `alltt` environment
+
+ def __init__(self, document, babel_class=Babel):
+ super().__init__(document)
+ # Reporter
+ # ~~~~~~~~
+ self.warn = self.document.reporter.warning
+ self.error = self.document.reporter.error
+
+ # Settings
+ # ~~~~~~~~
+ self.settings = settings = document.settings
+ # warn of deprecated settings and changing defaults:
+ if settings.use_latex_citations is None and not settings.use_bibtex:
+ settings.use_latex_citations = False
+ warnings.warn('The default for the setting "use_latex_citations" '
+ 'will change to "True" in Docutils 1.0.',
+ FutureWarning, stacklevel=7)
+ if settings.legacy_column_widths is None:
+ settings.legacy_column_widths = True
+ warnings.warn('The default for the setting "legacy_column_widths" '
+ 'will change to "False" in Docutils 1.0.)',
+ FutureWarning, stacklevel=7)
+ if settings.use_verbatim_when_possible is not None:
+ warnings.warn(
+ 'The configuration setting "use_verbatim_when_possible" '
+ 'will be removed in Docutils 2.0. '
+ 'Use "literal_block_env: verbatim".',
+ FutureWarning, stacklevel=7)
+
+ self.latex_encoding = self.to_latex_encoding(settings.output_encoding)
+ self.use_latex_toc = settings.use_latex_toc
+ self.use_latex_docinfo = settings.use_latex_docinfo
+ self.use_latex_citations = settings.use_latex_citations
+ self.reference_label = settings.reference_label
+ self.hyperlink_color = settings.hyperlink_color
+ self.compound_enumerators = settings.compound_enumerators
+ self.font_encoding = getattr(settings, 'font_encoding', '')
+ self.section_prefix_for_enumerators = (
+ settings.section_prefix_for_enumerators)
+ self.section_enumerator_separator = (
+ settings.section_enumerator_separator.replace('_', r'\_'))
+ # literal blocks:
+ self.literal_block_env = ''
+ self.literal_block_options = ''
+ if settings.literal_block_env:
+ (none,
+ self.literal_block_env,
+ self.literal_block_options,
+ none) = re.split(r'(\w+)(.*)', settings.literal_block_env)
+ elif settings.use_verbatim_when_possible:
+ self.literal_block_env = 'verbatim'
+
+ if settings.use_bibtex:
+ self.use_latex_citations = True
+ self.bibtex = settings.use_bibtex
+ # language module for Docutils-generated text
+ # (labels, bibliographic_fields, and author_separators)
+ self.language_module = languages.get_language(settings.language_code,
+ document.reporter)
+ self.babel = babel_class(settings.language_code, document.reporter)
+ self.author_separator = self.language_module.author_separators[0]
+ d_options = [settings.documentoptions]
+ if self.babel.language not in ('english', ''):
+ d_options.append(self.babel.language)
+ self.documentoptions = ','.join(filter(None, d_options))
+ self.d_class = DocumentClass(settings.documentclass,
+ settings.use_part_section)
+ # graphic package options:
+ if settings.graphicx_option == '':
+ self.graphicx_package = r'\usepackage{graphicx}'
+ else:
+ self.graphicx_package = (r'\usepackage[%s]{graphicx}' %
+ settings.graphicx_option)
+ # footnotes: TODO: implement LaTeX footnotes
+ self.docutils_footnotes = settings.docutils_footnotes
+
+ # Output collection stacks
+ # ~~~~~~~~~~~~~~~~~~~~~~~~
+
+ # Document parts
+ self.head_prefix = [r'\documentclass[%s]{%s}' %
+ (self.documentoptions,
+ settings.documentclass)]
+ self.requirements = SortableDict() # made a list in depart_document()
+ self.requirements['__static'] = r'\usepackage{ifthen}'
+ self.latex_preamble = [settings.latex_preamble]
+ self.fallbacks = SortableDict() # made a list in depart_document()
+ self.pdfsetup = [] # PDF properties (hyperref package)
+ self.title = []
+ self.subtitle = []
+ self.titledata = [] # \title, \author, \date
+ ## self.body_prefix = ['\\begin{document}\n']
+ self.body_pre_docinfo = [] # \maketitle
+ self.docinfo = []
+ self.dedication = []
+ self.abstract = []
+ self.body = []
+ ## self.body_suffix = ['\\end{document}\n']
+
+ self.context = []
+ """Heterogeneous stack.
+
+ Used by visit_* and depart_* functions in conjunction with the tree
+ traversal. Make sure that the pops correspond to the pushes."""
+
+ # Title metadata:
+ self.title_labels = []
+ self.subtitle_labels = []
+ # (if use_latex_docinfo: collects lists of
+ # author/organization/contact/address lines)
+ self.author_stack = []
+ self.date = []
+
+ # PDF properties: pdftitle, pdfauthor
+ self.pdfauthor = []
+ self.pdfinfo = []
+ if settings.language_code != 'en':
+ self.pdfinfo.append(' pdflang={%s},'%settings.language_code)
+
+ # Stack of section counters so that we don't have to use_latex_toc.
+ # This will grow and shrink as processing occurs.
+ # Initialized for potential first-level sections.
+ self._section_number = [0]
+
+ # The current stack of enumerations so that we can expand
+ # them into a compound enumeration.
+ self._enumeration_counters = []
+ # The maximum number of enumeration counters we've used.
+ # If we go beyond this number, we need to create a new
+ # counter; otherwise, just reuse an old one.
+ self._max_enumeration_counters = 0
+
+ self._bibitems = []
+
+ # object for a table while processing.
+ self.table_stack = []
+ self.active_table = Table(self, 'longtable')
+
+ # Where to collect the output of visitor methods (default: body)
+ self.out = self.body
+ self.out_stack = [] # stack of output collectors
+
+ # Process settings
+ # ~~~~~~~~~~~~~~~~
+ # Encodings:
+ # Docutils' output-encoding => TeX input encoding
+ if self.latex_encoding not in ('ascii', 'unicode', 'utf8'):
+ self.requirements['_inputenc'] = (r'\usepackage[%s]{inputenc}'
+ % self.latex_encoding)
+ # TeX font encoding
+ if not self.is_xetex:
+ if self.font_encoding:
+ self.requirements['_fontenc'] = (r'\usepackage[%s]{fontenc}' %
+ self.font_encoding)
+ # ensure \textquotedbl is defined:
+ for enc in self.font_encoding.split(','):
+ enc = enc.strip()
+ if enc == 'OT1':
+ self.requirements['_textquotedblOT1'] = (
+ r'\DeclareTextSymbol{\textquotedbl}{OT1}{`\"}')
+ elif enc not in ('T1', 'T2A', 'T2B', 'T2C', 'T4', 'T5'):
+ self.requirements['_textquotedbl'] = (
+ r'\DeclareTextSymbolDefault{\textquotedbl}{T1}')
+ # page layout with typearea (if there are relevant document options)
+ if (settings.documentclass.find('scr') == -1
+ and (self.documentoptions.find('DIV') != -1
+ or self.documentoptions.find('BCOR') != -1)):
+ self.requirements['typearea'] = r'\usepackage{typearea}'
+
+ # Stylesheets
+ # (the name `self.stylesheet` is singular because only one
+ # stylesheet was supported before Docutils 0.6).
+ stylesheet_list = utils.get_stylesheet_list(settings)
+ self.fallback_stylesheet = 'docutils' in stylesheet_list
+ if self.fallback_stylesheet:
+ stylesheet_list.remove('docutils')
+ if settings.legacy_class_functions:
+ # docutils.sty is incompatible with legacy functions
+ self.fallback_stylesheet = False
+ else:
+ # require a minimal version:
+ self.fallbacks['docutils.sty'] = (
+ r'\usepackage{docutils}[2020/08/28]')
+
+ self.stylesheet = [self.stylesheet_call(path)
+ for path in stylesheet_list]
+
+ # PDF setup
+ if self.hyperlink_color.lower() in ('0', 'false', ''):
+ self.hyperref_options = ''
+ else:
+ self.hyperref_options = ('colorlinks=true,'
+ f'linkcolor={self.hyperlink_color},'
+ f'urlcolor={self.hyperlink_color}')
+ if settings.hyperref_options:
+ self.hyperref_options += ',' + settings.hyperref_options
+
+ # LaTeX Toc
+ # include all supported sections in toc and PDF bookmarks
+ # (or use documentclass-default (as currently))?
+
+ # Section numbering
+ if settings.sectnum_xform: # section numbering by Docutils
+ PreambleCmds.secnumdepth = r'\setcounter{secnumdepth}{0}'
+ else: # section numbering by LaTeX:
+ secnumdepth = settings.sectnum_depth
+ # Possible values of settings.sectnum_depth:
+ # None "sectnum" directive without depth arg -> LaTeX default
+ # 0 no "sectnum" directive -> no section numbers
+ # >0 value of "depth" argument -> translate to LaTeX levels:
+ # -1 part (0 with "article" document class)
+ # 0 chapter (missing in "article" document class)
+ # 1 section
+ # 2 subsection
+ # 3 subsubsection
+ # 4 paragraph
+ # 5 subparagraph
+ if secnumdepth is not None:
+ PreambleCmds.secnumdepth = (
+ r'\setcounter{secnumdepth}{%d}'
+ % self.d_class.latex_section_depth(secnumdepth))
+ # start with specified number:
+ if (hasattr(settings, 'sectnum_start')
+ and settings.sectnum_start != 1):
+ self.requirements['sectnum_start'] = (
+ r'\setcounter{%s}{%d}' % (self.d_class.sections[0],
+ settings.sectnum_start-1))
+ # TODO: currently ignored (configure in a stylesheet):
+ ## settings.sectnum_prefix
+ ## settings.sectnum_suffix
+
+ # Auxiliary Methods
+ # -----------------
+
+ def stylesheet_call(self, path):
+ """Return code to reference or embed stylesheet file `path`"""
+ path = Path(path)
+ # is it a package (no extension or *.sty) or "normal" tex code:
+ is_package = path.suffix in ('.sty', '')
+ # Embed content of style file:
+ if self.settings.embed_stylesheet:
+ if is_package:
+ path = path.with_suffix('.sty') # ensure extension
+ try:
+ content = path.read_text(encoding='utf-8')
+ except OSError as err:
+ msg = f'Cannot embed stylesheet:\n {err}'.replace('\\\\', '/')
+ self.document.reporter.error(msg)
+ return '% ' + msg.replace('\n', '\n% ')
+ else:
+ self.settings.record_dependencies.add(path.as_posix())
+ if is_package:
+ # allow '@' in macro names:
+ content = (f'\\makeatletter\n{content}\n\\makeatother')
+ return (f'% embedded stylesheet: {path.as_posix()}\n'
+ f'{content}')
+ # Link to style file:
+ if is_package:
+ path = path.parent / path.stem # drop extension
+ cmd = r'\usepackage{%s}'
+ else:
+ cmd = r'\input{%s}'
+ if self.settings.stylesheet_path:
+ # adapt path relative to output (cf. config.html#stylesheet-path)
+ return cmd % utils.relative_path(self.settings._destination, path)
+ return cmd % path.as_posix()
+
+ def to_latex_encoding(self, docutils_encoding):
+ """Translate docutils encoding name into LaTeX's.
+
+ Default method is remove "-" and "_" chars from docutils_encoding.
+ """
+ tr = {'iso-8859-1': 'latin1', # west european
+ 'iso-8859-2': 'latin2', # east european
+ 'iso-8859-3': 'latin3', # esperanto, maltese
+ 'iso-8859-4': 'latin4', # north european
+ 'iso-8859-5': 'iso88595', # cyrillic (ISO)
+ 'iso-8859-9': 'latin5', # turkish
+ 'iso-8859-15': 'latin9', # latin9, update to latin1.
+ 'mac_cyrillic': 'maccyr', # cyrillic (on Mac)
+ 'windows-1251': 'cp1251', # cyrillic (on Windows)
+ 'koi8-r': 'koi8-r', # cyrillic (Russian)
+ 'koi8-u': 'koi8-u', # cyrillic (Ukrainian)
+ 'windows-1250': 'cp1250', #
+ 'windows-1252': 'cp1252', #
+ 'us-ascii': 'ascii', # ASCII (US)
+ # unmatched encodings
+ # '': 'applemac',
+ # '': 'ansinew', # windows 3.1 ansi
+ # '': 'ascii', # ASCII encoding for the range 32--127.
+ # '': 'cp437', # dos latin us
+ # '': 'cp850', # dos latin 1
+ # '': 'cp852', # dos latin 2
+ # '': 'decmulti',
+ # '': 'latin10',
+ # 'iso-8859-6': '' # arabic
+ # 'iso-8859-7': '' # greek
+ # 'iso-8859-8': '' # hebrew
+ # 'iso-8859-10': '' # latin6, more complete iso-8859-4
+ }
+ encoding = docutils_encoding.lower() # normalize case
+ encoding = encoding.split(':')[0] # strip the error handler
+ if encoding in tr:
+ return tr[encoding]
+ # drop HYPHEN or LOW LINE from "latin_1", "utf-8" and similar
+ return encoding.replace('_', '').replace('-', '')
+
+ def language_label(self, docutil_label):
+ return self.language_module.labels[docutil_label]
+
+ def encode(self, text):
+ """Return text with 'problematic' characters escaped.
+
+ * Escape the special printing characters ``# $ % & ~ _ ^ \\ { }``,
+ square brackets ``[ ]``, double quotes and (in OT1) ``< | >``.
+ * Translate non-supported Unicode characters.
+ * Separate ``-`` (and more in literal text) to prevent input ligatures.
+ """
+ if self.verbatim:
+ return text
+ # Set up the translation table:
+ table = CharMaps.alltt.copy()
+ if not self.alltt:
+ table.update(CharMaps.special)
+ # keep the underscore in citation references
+ if self.inside_citation_reference_label and not self.alltt:
+ del table[ord('_')]
+ # Workarounds for OT1 font-encoding
+ if self.font_encoding in ['OT1', ''] and not self.is_xetex:
+ # * out-of-order characters in cmtt
+ if self.literal:
+ # replace underscore by underlined blank,
+ # because this has correct width.
+ table[ord('_')] = '\\underline{~}'
+ # the backslash doesn't work, so we use a mirrored slash.
+ # \reflectbox is provided by graphicx:
+ self.requirements['graphicx'] = self.graphicx_package
+ table[ord('\\')] = '\\reflectbox{/}'
+ # * ``< | >`` come out as different chars (except for cmtt):
+ else:
+ table[ord('|')] = '\\textbar{}'
+ table[ord('<')] = '\\textless{}'
+ table[ord('>')] = '\\textgreater{}'
+ if self.insert_non_breaking_blanks:
+ table[ord(' ')] = '~'
+ # tab chars may occur in included files (literal or code)
+ # quick-and-dirty replacement with spaces
+ # (for better results use `--literal-block-env=lstlisting`)
+ table[ord('\t')] = '~' * self.settings.tab_width
+ # Unicode replacements for 8-bit tex engines (not required with XeTeX)
+ if not self.is_xetex:
+ if not self.latex_encoding.startswith('utf8'):
+ table.update(CharMaps.unsupported_unicode)
+ table.update(CharMaps.utf8_supported_unicode)
+ table.update(CharMaps.textcomp)
+ table.update(CharMaps.pifont)
+ # Characters that require a feature/package to render
+ for ch in text:
+ cp = ord(ch)
+ if cp in CharMaps.textcomp and not self.fallback_stylesheet:
+ self.requirements['textcomp'] = PreambleCmds.textcomp
+ elif cp in CharMaps.pifont:
+ self.requirements['pifont'] = '\\usepackage{pifont}'
+ # preamble-definitions for unsupported Unicode characters
+ elif (self.latex_encoding == 'utf8'
+ and cp in CharMaps.unsupported_unicode):
+ self.requirements['_inputenc'+str(cp)] = (
+ '\\DeclareUnicodeCharacter{%04X}{%s}'
+ % (cp, CharMaps.unsupported_unicode[cp]))
+ text = text.translate(table)
+
+ # Break up input ligatures e.g. '--' to '-{}-'.
+ if not self.is_xetex: # Not required with xetex/luatex
+ separate_chars = '-'
+ # In monospace-font, we also separate ',,', '``' and "''" and some
+ # other characters which can't occur in non-literal text.
+ if self.literal:
+ separate_chars += ',`\'"<>'
+ for char in separate_chars * 2:
+ # Do it twice ("* 2") because otherwise we would replace
+ # '---' by '-{}--'.
+ text = text.replace(char + char, char + '{}' + char)
+
+ # Literal line breaks (in address or literal blocks):
+ if self.insert_newline:
+ lines = text.split('\n')
+ # Add a protected space to blank lines (except the last)
+ # to avoid ``! LaTeX Error: There's no line here to end.``
+ for i, line in enumerate(lines[:-1]):
+ if not line.lstrip():
+ lines[i] += '~'
+ text = (r'\\' + '\n').join(lines)
+ if self.literal and not self.insert_non_breaking_blanks:
+ # preserve runs of spaces but allow wrapping
+ text = text.replace(' ', ' ~')
+ return text
+
+ def attval(self, text,
+ whitespace=re.compile('[\n\r\t\v\f]')):
+ """Cleanse, encode, and return attribute value text."""
+ return self.encode(whitespace.sub(' ', text))
+
+ # TODO: is this used anywhere? -> update (use template) or delete
+ ## def astext(self):
+ ## """Assemble document parts and return as string."""
+ ## head = '\n'.join(self.head_prefix + self.stylesheet + self.head)
+ ## body = ''.join(self.body_prefix + self.body + self.body_suffix)
+ ## return head + '\n' + body
+
+ def is_inline(self, node):
+ """Check whether a node represents an inline or block-level element"""
+ return isinstance(node.parent, nodes.TextElement)
+
+ def append_hypertargets(self, node):
+ """Append hypertargets for all ids of `node`"""
+ # hypertarget places the anchor at the target's baseline,
+ # so we raise it explicitly
+ self.out.append('%\n'.join('\\raisebox{1em}{\\hypertarget{%s}{}}' %
+ id for id in node['ids']))
+
+ def ids_to_labels(self, node, set_anchor=True, protect=False,
+ newline=False):
+ """Return list of label definitions for all ids of `node`
+
+ If `set_anchor` is True, an anchor is set with \\phantomsection.
+ If `protect` is True, the \\label cmd is made robust.
+ If `newline` is True, a newline is added if there are labels.
+ """
+ prefix = '\\protect' if protect else ''
+ labels = [prefix + '\\label{%s}' % id for id in node['ids']]
+ if set_anchor and labels:
+ labels.insert(0, '\\phantomsection')
+ if newline and labels:
+ labels.append('\n')
+ return labels
+
+ def set_align_from_classes(self, node):
+ """Convert ``align-*`` class arguments into alignment args."""
+ # separate:
+ align = [cls for cls in node['classes'] if cls.startswith('align-')]
+ if align:
+ node['align'] = align[-1].replace('align-', '')
+ node['classes'] = [cls for cls in node['classes']
+ if not cls.startswith('align-')]
+
+ def insert_align_declaration(self, node, default=None):
+ align = node.get('align', default)
+ if align == 'left':
+ self.out.append('\\raggedright\n')
+ elif align == 'center':
+ self.out.append('\\centering\n')
+ elif align == 'right':
+ self.out.append('\\raggedleft\n')
+
+ def duclass_open(self, node):
+ """Open a group and insert declarations for class values."""
+ if not isinstance(node.parent, nodes.compound):
+ self.out.append('\n')
+ for cls in node['classes']:
+ if cls.startswith('language-'):
+ language = self.babel.language_name(cls[9:])
+ if language:
+ self.babel.otherlanguages[language] = True
+ self.out.append('\\begin{selectlanguage}{%s}\n' % language)
+ elif (isinstance(node, nodes.table)
+ and cls in Writer.table_style_values + ['colwidths-given']):
+ pass
+ else:
+ if not self.fallback_stylesheet:
+ self.fallbacks['DUclass'] = PreambleCmds.duclass
+ self.out.append('\\begin{DUclass}{%s}\n' % cls)
+
+ def duclass_close(self, node):
+ """Close a group of class declarations."""
+ for cls in reversed(node['classes']):
+ if cls.startswith('language-'):
+ language = self.babel.language_name(cls[9:])
+ if language:
+ self.out.append('\\end{selectlanguage}\n')
+ elif (isinstance(node, nodes.table)
+ and cls in Writer.table_style_values + ['colwidths-given']):
+ pass
+ else:
+ if not self.fallback_stylesheet:
+ self.fallbacks['DUclass'] = PreambleCmds.duclass
+ self.out.append('\\end{DUclass}\n')
+
+ def push_output_collector(self, new_out):
+ self.out_stack.append(self.out)
+ self.out = new_out
+
+ def pop_output_collector(self):
+ self.out = self.out_stack.pop()
+
+ def term_postfix(self, node):
+ """
+ Return LaTeX code required between term or field name and content.
+
+ In a LaTeX "description" environment (used for definition
+ lists and non-docinfo field lists), a ``\\leavevmode``
+ between an item's label and content ensures the correct
+ placement of certain block constructs.
+ """
+ for child in node:
+ if not isinstance(child, (nodes.Invisible, nodes.footnote,
+ nodes.citation)):
+ break
+ else:
+ return ''
+ if isinstance(child, (nodes.container, nodes.compound)):
+ return self.term_postfix(child)
+ if isinstance(child, nodes.image):
+ return '\\leavevmode\n' # Images get an additional newline.
+ if not isinstance(child, (nodes.paragraph, nodes.math_block)):
+ return '\\leavevmode'
+ return ''
+
+ # Visitor methods
+ # ---------------
+
+ def visit_Text(self, node):
+ self.out.append(self.encode(node.astext()))
+
+ def depart_Text(self, node):
+ pass
+
+ def visit_abbreviation(self, node):
+ node['classes'].insert(0, 'abbreviation')
+ self.visit_inline(node)
+
+ def depart_abbreviation(self, node):
+ self.depart_inline(node)
+
+ def visit_acronym(self, node):
+ node['classes'].insert(0, 'acronym')
+ self.visit_inline(node)
+
+ def depart_acronym(self, node):
+ self.depart_inline(node)
+
+ def visit_address(self, node):
+ self.visit_docinfo_item(node, 'address')
+
+ def depart_address(self, node):
+ self.depart_docinfo_item(node)
+
+ def visit_admonition(self, node):
+ # strip the generic 'admonition' from the list of classes
+ node['classes'] = [cls for cls in node['classes']
+ if cls != 'admonition']
+ if self.settings.legacy_class_functions:
+ self.fallbacks['admonition'] = PreambleCmds.admonition_legacy
+ if 'error' in node['classes']:
+ self.fallbacks['error'] = PreambleCmds.error_legacy
+ self.out.append('\n\\DUadmonition[%s]{'%','.join(node['classes']))
+ return
+ if not self.fallback_stylesheet:
+ self.fallbacks['admonition'] = PreambleCmds.admonition
+ if 'error' in node['classes'] and not self.fallback_stylesheet:
+ self.fallbacks['error'] = PreambleCmds.error
+ self.duclass_open(node)
+ self.out.append('\\begin{DUadmonition}')
+
+ def depart_admonition(self, node):
+ if self.settings.legacy_class_functions:
+ self.out.append('}\n')
+ return
+ self.out.append('\\end{DUadmonition}\n')
+ self.duclass_close(node)
+
+ def visit_author(self, node):
+ self.pdfauthor.append(self.attval(node.astext()))
+ self.visit_docinfo_item(node, 'author')
+
+ def depart_author(self, node):
+ self.depart_docinfo_item(node)
+
+ def visit_authors(self, node):
+ # not used: visit_author is called anyway for each author.
+ pass
+
+ def depart_authors(self, node):
+ pass
+
+ def visit_block_quote(self, node):
+ self.duclass_open(node)
+ self.out.append('\\begin{quote}')
+
+ def depart_block_quote(self, node):
+ self.out.append('\\end{quote}\n')
+ self.duclass_close(node)
+
+ def visit_bullet_list(self, node):
+ self.duclass_open(node)
+ self.out.append('\\begin{itemize}')
+
+ def depart_bullet_list(self, node):
+ self.out.append('\\end{itemize}\n')
+ self.duclass_close(node)
+
+ def visit_superscript(self, node):
+ self.out.append(r'\textsuperscript{')
+ self.visit_inline(node)
+
+ def depart_superscript(self, node):
+ self.depart_inline(node)
+ self.out.append('}')
+
+ def visit_subscript(self, node):
+ self.out.append(r'\textsubscript{')
+ self.visit_inline(node)
+
+ def depart_subscript(self, node):
+ self.depart_inline(node)
+ self.out.append('}')
+
+ def visit_caption(self, node):
+ self.out.append('\n\\caption{')
+
+ def depart_caption(self, node):
+ self.out.append('}\n')
+
+ def visit_title_reference(self, node):
+ if not self.fallback_stylesheet:
+ self.fallbacks['titlereference'] = PreambleCmds.titlereference
+ self.out.append(r'\DUroletitlereference{')
+ self.visit_inline(node)
+
+ def depart_title_reference(self, node):
+ self.depart_inline(node)
+ self.out.append('}')
+
+ def visit_citation(self, node):
+ if self.use_latex_citations:
+ self.push_output_collector([])
+ else:
+ # self.requirements['~fnt_floats'] = PreambleCmds.footnote_floats
+ self.out.append(r'\begin{figure}[b]')
+ self.append_hypertargets(node)
+
+ def depart_citation(self, node):
+ if self.use_latex_citations:
+ # TODO: normalize label
+ label = self.out[0]
+ text = ''.join(self.out[1:])
+ self._bibitems.append([label, text])
+ self.pop_output_collector()
+ else:
+ self.out.append('\\end{figure}\n')
+
+ def visit_citation_reference(self, node):
+ if self.bibtex:
+ self._bibitems.append([node.astext()])
+ if self.use_latex_citations:
+ if not self.inside_citation_reference_label:
+ self.out.append(r'\cite{')
+ self.inside_citation_reference_label = True
+ else:
+ assert self.out[-1] in (' ', '\n'),\
+ 'unexpected non-whitespace while in reference label'
+ del self.out[-1]
+ else:
+ href = ''
+ if 'refid' in node:
+ href = node['refid']
+ elif 'refname' in node:
+ href = self.document.nameids[node['refname']]
+ self.out.append('\\hyperlink{%s}{[' % href)
+
+ def depart_citation_reference(self, node):
+ # TODO: normalize labels
+ if self.use_latex_citations:
+ followup_citation = False
+ # check for a following citation separated by a space or newline
+ sibling = node.next_node(descend=False, siblings=True)
+ if (isinstance(sibling, nodes.Text)
+ and sibling.astext() in (' ', '\n')):
+ sibling2 = sibling.next_node(descend=False, siblings=True)
+ if isinstance(sibling2, nodes.citation_reference):
+ followup_citation = True
+ if followup_citation:
+ self.out.append(',')
+ else:
+ self.out.append('}')
+ self.inside_citation_reference_label = False
+ else:
+ self.out.append(']}')
+
+ def visit_classifier(self, node):
+ self.out.append('(\\textbf{')
+
+ def depart_classifier(self, node):
+ self.out.append('})')
+ if node.next_node(nodes.term, descend=False, siblings=True):
+ self.out.append('\n')
+
+ def visit_colspec(self, node):
+ self.active_table.visit_colspec(node)
+
+ def depart_colspec(self, node):
+ pass
+
+ def visit_comment(self, node):
+ if not isinstance(node.parent, nodes.compound):
+ self.out.append('\n')
+ # Precede every line with a comment sign, wrap in newlines
+ self.out.append('%% %s\n' % node.astext().replace('\n', '\n% '))
+ raise nodes.SkipNode
+
+ def depart_comment(self, node):
+ pass
+
+ def visit_compound(self, node):
+ if isinstance(node.parent, nodes.compound):
+ self.out.append('\n')
+ node['classes'].insert(0, 'compound')
+ self.duclass_open(node)
+
+ def depart_compound(self, node):
+ self.duclass_close(node)
+
+ def visit_contact(self, node):
+ self.visit_docinfo_item(node, 'contact')
+
+ def depart_contact(self, node):
+ self.depart_docinfo_item(node)
+
+ def visit_container(self, node):
+ self.duclass_open(node)
+
+ def depart_container(self, node):
+ self.duclass_close(node)
+
+ def visit_copyright(self, node):
+ self.visit_docinfo_item(node, 'copyright')
+
+ def depart_copyright(self, node):
+ self.depart_docinfo_item(node)
+
+ def visit_date(self, node):
+ self.visit_docinfo_item(node, 'date')
+
+ def depart_date(self, node):
+ self.depart_docinfo_item(node)
+
+ def visit_decoration(self, node):
+ # header and footer
+ pass
+
+ def depart_decoration(self, node):
+ pass
+
+ def visit_definition(self, node):
+ pass
+
+ def depart_definition(self, node):
+ pass
+
+ def visit_definition_list(self, node):
+ self.duclass_open(node)
+ self.out.append('\\begin{description}\n')
+
+ def depart_definition_list(self, node):
+ self.out.append('\\end{description}\n')
+ self.duclass_close(node)
+
+ def visit_definition_list_item(self, node):
+ pass
+
+ def depart_definition_list_item(self, node):
+ if node.next_node(descend=False, siblings=True) is not None:
+ self.out.append('\n') # TODO: just pass?
+
+ def visit_description(self, node):
+ self.out.append(' ')
+
+ def depart_description(self, node):
+ pass
+
+ def visit_docinfo(self, node):
+ self.push_output_collector(self.docinfo)
+
+ def depart_docinfo(self, node):
+ self.pop_output_collector()
+ # Some itmes (e.g. author) end up at other places
+ if self.docinfo:
+ # tabularx: automatic width of columns, no page breaks allowed.
+ self.requirements['tabularx'] = r'\usepackage{tabularx}'
+ if not self.fallback_stylesheet:
+ self.fallbacks['_providelength'] = PreambleCmds.providelength
+ self.fallbacks['docinfo'] = PreambleCmds.docinfo
+ #
+ self.docinfo.insert(0, '\n% Docinfo\n'
+ '\\begin{center}\n'
+ '\\begin{tabularx}{\\DUdocinfowidth}{lX}\n')
+ self.docinfo.append('\\end{tabularx}\n'
+ '\\end{center}\n')
+
+ def visit_docinfo_item(self, node, name):
+ if self.use_latex_docinfo:
+ if name in ('author', 'organization', 'contact', 'address'):
+ # We attach these to the last author. If any of them precedes
+ # the first author, put them in a separate "author" group
+ # (in lack of better semantics).
+ if name == 'author' or not self.author_stack:
+ self.author_stack.append([])
+ if name == 'address': # newlines are meaningful
+ self.insert_newline = True
+ text = self.encode(node.astext())
+ self.insert_newline = False
+ else:
+ text = self.attval(node.astext())
+ self.author_stack[-1].append(text)
+ raise nodes.SkipNode
+ elif name == 'date':
+ self.date.append(self.attval(node.astext()))
+ raise nodes.SkipNode
+ self.out.append('\\textbf{%s}: &\n\t' % self.language_label(name))
+ if name == 'address':
+ self.insert_newline = True
+ self.out.append('{\\raggedright\n')
+ self.context.append(' } \\\\\n')
+ else:
+ self.context.append(' \\\\\n')
+
+ def depart_docinfo_item(self, node):
+ self.out.append(self.context.pop())
+ # for address we did set insert_newline
+ self.insert_newline = False
+
+ def visit_doctest_block(self, node):
+ self.visit_literal_block(node)
+
+ def depart_doctest_block(self, node):
+ self.depart_literal_block(node)
+
+ def visit_document(self, node):
+ # titled document?
+ if (self.use_latex_docinfo or len(node)
+ and isinstance(node[0], nodes.title)):
+ protect = (self.settings.documentclass == 'memoir')
+ self.title_labels += self.ids_to_labels(node, set_anchor=False,
+ protect=protect)
+
+ def depart_document(self, node):
+ # Complete "parts" with information gained from walkabout
+ # * language setup
+ if (self.babel.otherlanguages
+ or self.babel.language not in ('', 'english')):
+ self.requirements['babel'] = self.babel()
+ # * conditional requirements (before style sheet)
+ self.requirements = self.requirements.sortedvalues()
+ # * coditional fallback definitions (after style sheet)
+ self.fallbacks = self.fallbacks.sortedvalues()
+ # * PDF properties
+ self.pdfsetup.append(PreambleCmds.linking % self.hyperref_options)
+ if self.pdfauthor:
+ authors = self.author_separator.join(self.pdfauthor)
+ self.pdfinfo.append(' pdfauthor={%s}' % authors)
+ if self.pdfinfo:
+ self.pdfsetup += [r'\hypersetup{'] + self.pdfinfo + ['}']
+ # * title (including author(s) and date if using "latex_docinfo")
+ if self.title or (self.use_latex_docinfo
+ and (self.author_stack or self.date)):
+ self.make_title() # see below
+ # * bibliography
+ if self._bibitems:
+ self.append_bibliogaphy() # see below
+ # * make sure to generate a toc file if needed for local contents:
+ if 'minitoc' in self.requirements and not self.has_latex_toc:
+ self.out.append('\n\\faketableofcontents % for local ToCs\n')
+
+ def make_title(self):
+ # Auxiliary function called by `self.depart_document()`.
+ #
+ # Append ``\title``, ``\author``, and ``\date`` to "titledata".
+ # (We need all three, even if empty, to prevent errors
+ # and/or automatic display of the current date by \maketitle.)
+ # Append ``\maketitle`` to "body_pre_docinfo" parts.
+ #
+ # \title
+ title_arg = [''.join(self.title)] # ensure len == 1
+ if self.title:
+ title_arg += self.title_labels
+ if self.subtitle:
+ title_arg += [r'\\',
+ r'\DUdocumentsubtitle{%s}' % ''.join(self.subtitle),
+ ] + self.subtitle_labels
+ self.titledata.append(r'\title{%s}' % '%\n '.join(title_arg))
+ # \author
+ author_arg = ['\\\\\n'.join(author_entry)
+ for author_entry in self.author_stack]
+ self.titledata.append(r'\author{%s}' %
+ ' \\and\n'.join(author_arg))
+ # \date
+ self.titledata.append(r'\date{%s}' % ', '.join(self.date))
+ # \maketitle
+ # Must be in the document body. We add it to `body_pre_docinfo`
+ # to allow templates to put `titledata` into the document preamble.
+ self.body_pre_docinfo.append('\\maketitle\n')
+
+ def append_bibliogaphy(self):
+ # Add bibliography at end of document.
+ # TODO insertion point should be configurable.
+ # Auxiliary function called by `depart_document`.
+ if self.bibtex:
+ self.out.append('\n\\bibliographystyle{%s}\n' % self.bibtex[0])
+ self.out.append('\\bibliography{%s}\n' % ','.join(self.bibtex[1:]))
+ elif self.use_latex_citations:
+ # TODO: insert citations at point of definition.
+ widest_label = ''
+ for bibitem in self._bibitems:
+ if len(widest_label) < len(bibitem[0]):
+ widest_label = bibitem[0]
+ self.out.append('\n\\begin{thebibliography}{%s}\n' %
+ widest_label)
+ for bibitem in self._bibitems:
+ # cite_key: underscores must not be escaped
+ cite_key = bibitem[0].replace(r'\_', '_')
+ self.out.append('\\bibitem[%s]{%s}{%s}\n' %
+ (bibitem[0], cite_key, bibitem[1]))
+ self.out.append('\\end{thebibliography}\n')
+
+ def visit_emphasis(self, node):
+ self.out.append('\\emph{')
+ self.visit_inline(node)
+
+ def depart_emphasis(self, node):
+ self.depart_inline(node)
+ self.out.append('}')
+
+ # Append column delimiters and advance column counter,
+ # if the current cell is a multi-row continuation."""
+ def insert_additional_table_colum_delimiters(self):
+ while self.active_table.get_rowspan(
+ self.active_table.get_entry_number()):
+ self.out.append(' & ')
+ self.active_table.visit_entry() # increment cell count
+
+ def visit_entry(self, node):
+ # cell separation
+ if self.active_table.get_entry_number() == 0:
+ self.insert_additional_table_colum_delimiters()
+ else:
+ self.out.append(' & ')
+
+ # multirow, multicolumn
+ if 'morerows' in node and 'morecols' in node:
+ raise NotImplementedError('Cells that span multiple rows *and* '
+ 'columns currently not supported '
+ 'by the LaTeX writer')
+ # TODO: should be possible with LaTeX, see e.g.
+ # http://texblog.org/2012/12/21/multi-column-and-multi-row-cells-in-latex-tables/
+ # multirow in LaTeX simply will enlarge the cell over several rows
+ # (the following n if n is positive, the former if negative).
+ if 'morerows' in node:
+ self.requirements['multirow'] = r'\usepackage{multirow}'
+ mrows = node['morerows'] + 1
+ self.active_table.set_rowspan(
+ self.active_table.get_entry_number(), mrows)
+ self.out.append('\\multirow{%d}{%s}{' %
+ (mrows, self.active_table.get_column_width()))
+ self.context.append('}')
+ elif 'morecols' in node:
+ # the vertical bar before column is missing if it is the first
+ # column. the one after always.
+ if self.active_table.get_entry_number() == 0:
+ bar1 = self.active_table.get_vertical_bar()
+ else:
+ bar1 = ''
+ mcols = node['morecols'] + 1
+ self.out.append('\\multicolumn{%d}{%s%s%s}{' %
+ (mcols,
+ bar1,
+ self.active_table.get_multicolumn_width(
+ self.active_table.get_entry_number(), mcols),
+ self.active_table.get_vertical_bar()))
+ self.context.append('}')
+ else:
+ self.context.append('')
+
+ # bold header/stub-column
+ if len(node) and (isinstance(node.parent.parent, nodes.thead)
+ or self.active_table.is_stub_column()):
+ self.out.append('\\textbf{')
+ self.context.append('}')
+ else:
+ self.context.append('')
+
+ # if line ends with '{', mask line break
+ if (not self.active_table.colwidths_auto
+ and self.out[-1].endswith("{")
+ and node.astext()):
+ self.out.append("%")
+
+ self.active_table.visit_entry() # increment cell count
+
+ def depart_entry(self, node):
+ self.out.append(self.context.pop()) # header / not header
+ self.out.append(self.context.pop()) # multirow/column
+ # insert extra "&"s, if following rows are spanned from above:
+ self.insert_additional_table_colum_delimiters()
+
+ def visit_row(self, node):
+ self.active_table.visit_row()
+
+ def depart_row(self, node):
+ self.out.extend(self.active_table.depart_row())
+
+ def visit_enumerated_list(self, node):
+ # enumeration styles:
+ types = {'': '',
+ 'arabic': 'arabic',
+ 'loweralpha': 'alph',
+ 'upperalpha': 'Alph',
+ 'lowerroman': 'roman',
+ 'upperroman': 'Roman'}
+ # default LaTeX enumeration labels:
+ default_labels = [
+ # (präfix, enumtype, suffix)
+ ('', 'arabic', '.'), # 1.
+ ('(', 'alph', ')'), # (a)
+ ('', 'roman', '.'), # i.
+ ('', 'Alph', '.')] # A.
+
+ prefix = ''
+ if self.compound_enumerators:
+ if (self.section_prefix_for_enumerators and self.section_level
+ and not self._enumeration_counters):
+ prefix = '.'.join(str(n) for n in
+ self._section_number[:self.section_level]
+ ) + self.section_enumerator_separator
+ if self._enumeration_counters:
+ prefix += self._enumeration_counters[-1]
+ prefix += node.get('prefix', '')
+ enumtype = types[node.get('enumtype', 'arabic')]
+ suffix = node.get('suffix', '.')
+
+ enum_level = len(self._enumeration_counters)+1
+ counter_name = 'enum' + roman.toRoman(enum_level).lower()
+ label = r'%s\%s{%s}%s' % (prefix, enumtype, counter_name, suffix)
+ self._enumeration_counters.append(label)
+
+ self.duclass_open(node)
+ if enum_level <= 4:
+ self.out.append('\\begin{enumerate}')
+ if (prefix, enumtype, suffix) != default_labels[enum_level-1]:
+ self.out.append('\n\\renewcommand{\\label%s}{%s}' %
+ (counter_name, label))
+ else:
+ self.fallbacks[counter_name] = '\\newcounter{%s}' % counter_name
+ self.out.append('\\begin{list}')
+ self.out.append('{%s}' % label)
+ self.out.append('{\\usecounter{%s}}' % counter_name)
+ if 'start' in node:
+ self.out.append('\n\\setcounter{%s}{%d}' %
+ (counter_name, node['start']-1))
+
+ def depart_enumerated_list(self, node):
+ if len(self._enumeration_counters) <= 4:
+ self.out.append('\\end{enumerate}\n')
+ else:
+ self.out.append('\\end{list}\n')
+ self.duclass_close(node)
+ self._enumeration_counters.pop()
+
+ def visit_field(self, node):
+ # output is done in field_body, field_name
+ pass
+
+ def depart_field(self, node):
+ pass
+
+ def visit_field_body(self, node):
+ if not isinstance(node.parent.parent, nodes.docinfo):
+ self.out.append(self.term_postfix(node))
+
+ def depart_field_body(self, node):
+ if self.out is self.docinfo:
+ self.out.append(r'\\'+'\n')
+
+ def visit_field_list(self, node):
+ self.duclass_open(node)
+ if self.out is not self.docinfo:
+ if not self.fallback_stylesheet:
+ self.fallbacks['fieldlist'] = PreambleCmds.fieldlist
+ self.out.append('\\begin{DUfieldlist}')
+
+ def depart_field_list(self, node):
+ if self.out is not self.docinfo:
+ self.out.append('\\end{DUfieldlist}\n')
+ self.duclass_close(node)
+
+ def visit_field_name(self, node):
+ if self.out is self.docinfo:
+ self.out.append('\\textbf{')
+ else:
+ # Commands with optional args inside an optional arg must be put
+ # in a group, e.g. ``\item[{\hyperref[label]{text}}]``.
+ self.out.append('\n\\item[{')
+
+ def depart_field_name(self, node):
+ if self.out is self.docinfo:
+ self.out.append('}: &')
+ else:
+ self.out.append(':}]')
+
+ def visit_figure(self, node):
+ self.requirements['float'] = PreambleCmds.float
+ self.duclass_open(node)
+ # The 'align' attribute sets the "outer alignment",
+ # for "inner alignment" use LaTeX default alignment (similar to HTML)
+ alignment = node.attributes.get('align', 'center')
+ if alignment != 'center':
+ # The LaTeX "figure" environment always uses the full linewidth,
+ # so "outer alignment" is ignored. Just write a comment.
+ # TODO: use the wrapfigure environment?
+ self.out.append('\\begin{figure} %% align = "%s"\n' % alignment)
+ else:
+ self.out.append('\\begin{figure}\n')
+ self.out += self.ids_to_labels(node, newline=True)
+
+ def depart_figure(self, node):
+ self.out.append('\\end{figure}\n')
+ self.duclass_close(node)
+
+ def visit_footer(self, node):
+ self.push_output_collector([])
+ self.out.append(r'\newcommand{\DUfooter}{')
+
+ def depart_footer(self, node):
+ self.out.append('}')
+ self.requirements['~footer'] = ''.join(self.out)
+ self.pop_output_collector()
+
+ def visit_footnote(self, node):
+ try:
+ backref = node['backrefs'][0]
+ except IndexError:
+ backref = node['ids'][0] # no backref, use self-ref instead
+ if self.docutils_footnotes:
+ if not self.fallback_stylesheet:
+ self.fallbacks['footnotes'] = PreambleCmds.footnotes
+ num = node[0].astext()
+ if self.settings.footnote_references == 'brackets':
+ num = '[%s]' % num
+ self.out.append('%%\n\\DUfootnotetext{%s}{%s}{%s}{' %
+ (node['ids'][0], backref, self.encode(num)))
+ if node['ids'] == node['names']:
+ self.out += self.ids_to_labels(node)
+ # prevent spurious whitespace if footnote starts with paragraph:
+ if len(node) > 1 and isinstance(node[1], nodes.paragraph):
+ self.out.append('%')
+ # TODO: "real" LaTeX \footnote{}s (see visit_footnotes_reference())
+
+ def depart_footnote(self, node):
+ self.out.append('}\n')
+
+ def visit_footnote_reference(self, node):
+ href = ''
+ if 'refid' in node:
+ href = node['refid']
+ elif 'refname' in node:
+ href = self.document.nameids[node['refname']]
+ # if not self.docutils_footnotes:
+ # # TODO: insert footnote content at (or near) this place
+ # # see also docs/dev/todo.txt
+ # try:
+ # referenced_node = self.document.ids[node['refid']]
+ # except (AttributeError, KeyError):
+ # self.document.reporter.error(
+ # 'unresolved footnote-reference %s' % node)
+ # print('footnote-ref to %s' % referenced_node)
+ format = self.settings.footnote_references
+ if format == 'brackets':
+ self.append_hypertargets(node)
+ self.out.append('\\hyperlink{%s}{[' % href)
+ self.context.append(']}')
+ else:
+ if not self.fallback_stylesheet:
+ self.fallbacks['footnotes'] = PreambleCmds.footnotes
+ self.out.append(r'\DUfootnotemark{%s}{%s}{' %
+ (node['ids'][0], href))
+ self.context.append('}')
+
+ def depart_footnote_reference(self, node):
+ self.out.append(self.context.pop())
+
+ # footnote/citation label
+ def label_delim(self, node, bracket, superscript):
+ if isinstance(node.parent, nodes.footnote):
+ raise nodes.SkipNode
+ else:
+ assert isinstance(node.parent, nodes.citation)
+ if not self.use_latex_citations:
+ self.out.append(bracket)
+
+ def visit_label(self, node):
+ """footnote or citation label: in brackets or as superscript"""
+ self.label_delim(node, '[', '\\textsuperscript{')
+
+ def depart_label(self, node):
+ self.label_delim(node, ']', '}')
+
+ # elements generated by the framework e.g. section numbers.
+ def visit_generated(self, node):
+ pass
+
+ def depart_generated(self, node):
+ pass
+
+ def visit_header(self, node):
+ self.push_output_collector([])
+ self.out.append(r'\newcommand{\DUheader}{')
+
+ def depart_header(self, node):
+ self.out.append('}')
+ self.requirements['~header'] = ''.join(self.out)
+ self.pop_output_collector()
+
+ def to_latex_length(self, length_str, pxunit=None):
+ """Convert `length_str` with rst length to LaTeX length
+ """
+ if pxunit is not None:
+ warnings.warn(
+ 'The optional argument `pxunit` '
+ 'of LaTeXTranslator.to_latex_length() is ignored '
+ 'and will be removed in Docutils 0.21 or later',
+ DeprecationWarning, stacklevel=2)
+ match = re.match(r'(\d*\.?\d*)\s*(\S*)', length_str)
+ if not match:
+ return length_str
+ value, unit = match.groups()[:2]
+ # no unit or "DTP" points (called 'bp' in TeX):
+ if unit in ('', 'pt'):
+ length_str = '%sbp' % value
+ # percentage: relate to current line width
+ elif unit == '%':
+ length_str = '%.3f\\linewidth' % (float(value)/100.0)
+ elif self.is_xetex and unit == 'px':
+ # XeTeX does not know the length unit px.
+ # Use \pdfpxdimen, the macro to set the value of 1 px in pdftex.
+ # This way, configuring works the same for pdftex and xetex.
+ if not self.fallback_stylesheet:
+ self.fallbacks['_providelength'] = PreambleCmds.providelength
+ self.fallbacks['px'] = '\n\\DUprovidelength{\\pdfpxdimen}{1bp}\n'
+ length_str = r'%s\pdfpxdimen' % value
+ return length_str
+
+ def visit_image(self, node):
+ self.requirements['graphicx'] = self.graphicx_package
+ attrs = node.attributes
+ # Convert image URI to a local file path
+ imagepath = url2pathname(attrs['uri']).replace('\\', '/')
+ # alignment defaults:
+ if 'align' not in attrs:
+ # Set default align of image in a figure to 'center'
+ if isinstance(node.parent, nodes.figure):
+ attrs['align'] = 'center'
+ self.set_align_from_classes(node)
+ # pre- and postfix (prefix inserted in reverse order)
+ pre = []
+ post = []
+ include_graphics_options = []
+ align_codes = {
+ # inline images: by default latex aligns the bottom.
+ 'bottom': ('', ''),
+ 'middle': (r'\raisebox{-0.5\height}{', '}'),
+ 'top': (r'\raisebox{-\height}{', '}'),
+ # block level images:
+ 'center': (r'\noindent\makebox[\linewidth][c]{', '}'),
+ 'left': (r'\noindent{', r'\hfill}'),
+ 'right': (r'\noindent{\hfill', '}'),
+ }
+ if 'align' in attrs:
+ # TODO: warn or ignore non-applicable alignment settings?
+ try:
+ align_code = align_codes[attrs['align']]
+ pre.append(align_code[0])
+ post.append(align_code[1])
+ except KeyError:
+ pass # TODO: warn?
+ if 'height' in attrs:
+ include_graphics_options.append(
+ 'height=%s' % self.to_latex_length(attrs['height']))
+ if 'scale' in attrs:
+ include_graphics_options.append(
+ 'scale=%f' % (attrs['scale'] / 100.0))
+ if 'width' in attrs:
+ include_graphics_options.append(
+ 'width=%s' % self.to_latex_length(attrs['width']))
+ if not (self.is_inline(node)
+ or isinstance(node.parent, (nodes.figure, nodes.compound))):
+ pre.append('\n')
+ if not (self.is_inline(node)
+ or isinstance(node.parent, nodes.figure)):
+ post.append('\n')
+ pre.reverse()
+ self.out.extend(pre)
+ options = ''
+ if include_graphics_options:
+ options = '[%s]' % (','.join(include_graphics_options))
+ self.out.append('\\includegraphics%s{%s}' % (options, imagepath))
+ self.out.extend(post)
+
+ def depart_image(self, node):
+ self.out += self.ids_to_labels(node, newline=True)
+
+ def visit_inline(self, node): # <span>, i.e. custom roles
+ for cls in node['classes']:
+ if cls.startswith('language-'):
+ language = self.babel.language_name(cls[9:])
+ if language:
+ self.babel.otherlanguages[language] = True
+ self.out.append(r'\foreignlanguage{%s}{' % language)
+ else:
+ if not self.fallback_stylesheet:
+ self.fallbacks['inline'] = PreambleCmds.inline
+ self.out.append(r'\DUrole{%s}{' % cls)
+
+ def depart_inline(self, node):
+ self.out.append('}' * len(node['classes']))
+
+ def visit_legend(self, node):
+ if not self.fallback_stylesheet:
+ self.fallbacks['legend'] = PreambleCmds.legend
+ self.out.append('\\begin{DUlegend}')
+
+ def depart_legend(self, node):
+ self.out.append('\\end{DUlegend}\n')
+
+ def visit_line(self, node):
+ self.out.append(r'\item[] ')
+
+ def depart_line(self, node):
+ self.out.append('\n')
+
+ def visit_line_block(self, node):
+ if not self.fallback_stylesheet:
+ self.fallbacks['_providelength'] = PreambleCmds.providelength
+ self.fallbacks['lineblock'] = PreambleCmds.lineblock
+ self.set_align_from_classes(node)
+ if isinstance(node.parent, nodes.line_block):
+ self.out.append('\\item[]\n'
+ '\\begin{DUlineblock}{\\DUlineblockindent}\n')
+ # nested line-blocks cannot be given class arguments
+ else:
+ self.duclass_open(node)
+ self.out.append('\\begin{DUlineblock}{0em}\n')
+ self.insert_align_declaration(node)
+
+ def depart_line_block(self, node):
+ self.out.append('\\end{DUlineblock}\n')
+ self.duclass_close(node)
+
+ def visit_list_item(self, node):
+ self.out.append('\n\\item ')
+
+ def depart_list_item(self, node):
+ pass
+
+ def visit_literal(self, node):
+ self.literal = True
+ if ('code' in node['classes']
+ and self.settings.syntax_highlight != 'none'):
+ self.requirements['color'] = PreambleCmds.color
+ if not self.fallback_stylesheet:
+ self.fallbacks['code'] = PreambleCmds.highlight_rules
+ self.out.append('\\texttt{')
+ self.visit_inline(node)
+
+ def depart_literal(self, node):
+ self.literal = False
+ self.depart_inline(node)
+ self.out.append('}')
+
+ # Literal blocks are used for '::'-prefixed literal-indented
+ # blocks of text, where the inline markup is not recognized,
+ # but are also the product of the "parsed-literal" directive,
+ # where the markup is respected.
+ #
+ # In both cases, we want to use a typewriter/monospaced typeface.
+ # For "real" literal-blocks, we can use \verbatim, while for all
+ # the others we must use \ttfamily and \raggedright.
+ #
+ # We can distinguish between the two kinds by the number of
+ # siblings that compose this node: if it is composed by a
+ # single element, it's either
+ # * a real one,
+ # * a parsed-literal that does not contain any markup, or
+ # * a parsed-literal containing just one markup construct.
+ def is_plaintext(self, node):
+ """Check whether a node can be typeset verbatim"""
+ return (len(node) == 1) and isinstance(node[0], nodes.Text)
+
+ def visit_literal_block(self, node):
+ """Render a literal block.
+
+ Corresponding rST elements: literal block, parsed-literal, code.
+ """
+ packages = {'lstlisting': r'\usepackage{listings}' '\n'
+ r'\lstset{xleftmargin=\leftmargin}',
+ 'listing': r'\usepackage{moreverb}',
+ 'Verbatim': r'\usepackage{fancyvrb}',
+ 'verbatimtab': r'\usepackage{moreverb}'}
+
+ literal_env = self.literal_block_env
+
+ # Check, if it is possible to use a literal-block environment
+ _plaintext = self.is_plaintext(node)
+ _in_table = self.active_table.is_open()
+ # TODO: fails if normal text precedes the literal block.
+ # Check parent node instead?
+ _autowidth_table = _in_table and self.active_table.colwidths_auto
+ _no_env_nodes = (nodes.footnote, nodes.sidebar)
+ if self.settings.legacy_class_functions:
+ _no_env_nodes += (nodes.admonition, nodes.system_message)
+ _use_env = _plaintext and not isinstance(node.parent, _no_env_nodes)
+ _use_listings = (literal_env == 'lstlisting') and _use_env
+
+ # Labels and classes:
+ self.duclass_open(node)
+ self.out += self.ids_to_labels(node, newline=True)
+ # Highlight code?
+ if (not _plaintext
+ and 'code' in node['classes']
+ and self.settings.syntax_highlight != 'none'):
+ self.requirements['color'] = PreambleCmds.color
+ if not self.fallback_stylesheet:
+ self.fallbacks['code'] = PreambleCmds.highlight_rules
+ # Wrap?
+ if _in_table and _use_env and not _autowidth_table:
+ # Wrap in minipage to prevent extra vertical space
+ # with alltt and verbatim-like environments:
+ self.fallbacks['ttem'] = PreambleCmds.ttem
+ self.out.append(
+ '\\begin{minipage}{%d\\ttemwidth}\n' %
+ (max(len(line) for line in node.astext().split('\n'))))
+ self.context.append('\n\\end{minipage}\n')
+ elif not _in_table and not _use_listings:
+ # Wrap in quote to set off vertically and indent
+ self.out.append('\\begin{quote}\n')
+ self.context.append('\n\\end{quote}\n')
+ else:
+ self.context.append('\n')
+
+ # Use verbatim-like environment, if defined and possible
+ # (in an auto-width table, only listings works):
+ if literal_env and _use_env and (not _autowidth_table
+ or _use_listings):
+ try:
+ self.requirements['literal_block'] = packages[literal_env]
+ except KeyError:
+ pass
+ self.verbatim = True
+ if _in_table and _use_listings:
+ self.out.append('\\lstset{xleftmargin=0pt}\n')
+ self.out.append('\\begin{%s}%s\n' %
+ (literal_env, self.literal_block_options))
+ self.context.append('\n\\end{%s}' % literal_env)
+ elif _use_env and not _autowidth_table:
+ self.alltt = True
+ self.requirements['alltt'] = r'\usepackage{alltt}'
+ self.out.append('\\begin{alltt}\n')
+ self.context.append('\n\\end{alltt}')
+ else:
+ self.literal = True
+ self.insert_newline = True
+ self.insert_non_breaking_blanks = True
+ # \raggedright ensures leading blanks are respected but
+ # leads to additional leading vspace if the first line
+ # of the block is overfull :-(
+ self.out.append('\\ttfamily\\raggedright\n')
+ self.context.append('')
+
+ def depart_literal_block(self, node):
+ self.insert_non_breaking_blanks = False
+ self.insert_newline = False
+ self.literal = False
+ self.verbatim = False
+ self.alltt = False
+ self.out.append(self.context.pop())
+ self.out.append(self.context.pop())
+ self.duclass_close(node)
+
+ def visit_meta(self, node):
+ name = node.attributes.get('name')
+ content = node.attributes.get('content')
+ if not name or not content:
+ return
+ if name in ('author', 'creator', 'keywords', 'subject', 'title'):
+ # fields with dedicated hyperref options:
+ self.pdfinfo.append(' pdf%s={%s},'%(name, content))
+ elif name == 'producer':
+ self.pdfinfo.append(' addtopdfproducer={%s},'%content)
+ else:
+ # generic interface (case sensitive!)
+ # TODO: filter irrelevant nodes ("http-equiv", ...)?
+ self.pdfinfo.append(' pdfinfo={%s={%s}},'%(name, content))
+
+ def depart_meta(self, node):
+ pass
+
+ def visit_math(self, node, math_env='$'):
+ """math role"""
+ self.visit_inline(node)
+ self.requirements['amsmath'] = r'\usepackage{amsmath}'
+ math_code = node.astext().translate(unichar2tex.uni2tex_table)
+ if math_env == '$':
+ if self.alltt:
+ wrapper = ['\\(', '\\)']
+ else:
+ wrapper = ['$', '$']
+ else:
+ labels = self.ids_to_labels(node, set_anchor=False, newline=True)
+ wrapper = ['%%\n\\begin{%s}\n' % math_env,
+ '\n',
+ ''.join(labels),
+ '\\end{%s}' % math_env]
+ wrapper.insert(1, math_code)
+ self.out.extend(wrapper)
+ self.depart_inline(node)
+ # Content already processed:
+ raise nodes.SkipNode
+
+ def depart_math(self, node):
+ pass # never reached
+
+ def visit_math_block(self, node):
+ math_env = pick_math_environment(node.astext())
+ self.visit_math(node, math_env=math_env)
+
+ def depart_math_block(self, node):
+ pass # never reached
+
+ def visit_option(self, node):
+ if self.context[-1]:
+ # this is not the first option
+ self.out.append(', ')
+
+ def depart_option(self, node):
+ # flag that the first option is done.
+ self.context[-1] += 1
+
+ def visit_option_argument(self, node):
+ """Append the delimiter between an option and its argument to body."""
+ self.out.append(node.get('delimiter', ' '))
+
+ def depart_option_argument(self, node):
+ pass
+
+ def visit_option_group(self, node):
+ self.out.append('\\item[')
+ # flag for first option
+ self.context.append(0)
+
+ def depart_option_group(self, node):
+ self.context.pop() # the flag
+ self.out.append('] ')
+
+ def visit_option_list(self, node):
+ if not self.fallback_stylesheet:
+ self.fallbacks['_providelength'] = PreambleCmds.providelength
+ self.fallbacks['optionlist'] = PreambleCmds.optionlist
+ self.duclass_open(node)
+ self.out.append('\\begin{DUoptionlist}\n')
+
+ def depart_option_list(self, node):
+ self.out.append('\\end{DUoptionlist}\n')
+ self.duclass_close(node)
+
+ def visit_option_list_item(self, node):
+ pass
+
+ def depart_option_list_item(self, node):
+ pass
+
+ def visit_option_string(self, node):
+ ## self.out.append(self.starttag(node, 'span', '', CLASS='option'))
+ pass
+
+ def depart_option_string(self, node):
+ ## self.out.append('</span>')
+ pass
+
+ def visit_organization(self, node):
+ self.visit_docinfo_item(node, 'organization')
+
+ def depart_organization(self, node):
+ self.depart_docinfo_item(node)
+
+ def visit_paragraph(self, node):
+ # insert blank line, unless
+ # * the paragraph is first in a list item, compound, or container
+ # * follows a non-paragraph node in a compound,
+ # * is in a table with auto-width columns
+ index = node.parent.index(node)
+ if index == 0 and isinstance(node.parent,
+ (nodes.list_item, nodes.description,
+ nodes.compound, nodes.container)):
+ pass
+ elif (index > 0
+ and isinstance(node.parent, nodes.compound)
+ and not isinstance(node.parent[index - 1],
+ (nodes.paragraph, nodes.compound))):
+ pass
+ elif self.active_table.colwidths_auto:
+ if index == 1: # second paragraph
+ self.warn('LaTeX merges paragraphs in tables '
+ 'with auto-sized columns!', base_node=node)
+ if index > 0:
+ self.out.append('\n')
+ else:
+ self.out.append('\n')
+ self.out += self.ids_to_labels(node, newline=True)
+ self.visit_inline(node)
+
+ def depart_paragraph(self, node):
+ self.depart_inline(node)
+ if not self.active_table.colwidths_auto:
+ self.out.append('\n')
+
+ def visit_problematic(self, node):
+ self.requirements['color'] = PreambleCmds.color
+ self.out.append('%\n')
+ self.append_hypertargets(node)
+ self.out.append(r'\hyperlink{%s}{\textbf{\color{red}' % node['refid'])
+
+ def depart_problematic(self, node):
+ self.out.append('}}')
+
+ def visit_raw(self, node):
+ if 'latex' not in node.get('format', '').split():
+ raise nodes.SkipNode
+ if not (self.is_inline(node)
+ or isinstance(node.parent, nodes.compound)):
+ self.out.append('\n')
+ self.visit_inline(node)
+ # append "as-is" skipping any LaTeX-encoding
+ self.verbatim = True
+
+ def depart_raw(self, node):
+ self.verbatim = False
+ self.depart_inline(node)
+ if not self.is_inline(node):
+ self.out.append('\n')
+
+ def has_unbalanced_braces(self, string):
+ """Test whether there are unmatched '{' or '}' characters."""
+ level = 0
+ for ch in string:
+ if ch == '{':
+ level += 1
+ if ch == '}':
+ level -= 1
+ if level < 0:
+ return True
+ return level != 0
+
+ def visit_reference(self, node):
+ # We need to escape #, \, and % if we use the URL in a command.
+ special_chars = {ord('#'): '\\#',
+ ord('%'): '\\%',
+ ord('\\'): '\\\\',
+ }
+ # external reference (URL)
+ if 'refuri' in node:
+ href = str(node['refuri']).translate(special_chars)
+ # problematic chars double caret and unbalanced braces:
+ if href.find('^^') != -1 or self.has_unbalanced_braces(href):
+ self.error(
+ f'External link "{href}" not supported by LaTeX.\n'
+ ' (Must not contain "^^" or unbalanced braces.)')
+ if node['refuri'] == node.astext():
+ self.out.append(r'\url{%s}' % href)
+ raise nodes.SkipNode
+ self.out.append(r'\href{%s}{' % href)
+ return
+ # internal reference
+ if 'refid' in node:
+ href = node['refid']
+ elif 'refname' in node:
+ href = self.document.nameids[node['refname']]
+ else:
+ raise AssertionError('Unknown reference.')
+ if not self.is_inline(node):
+ self.out.append('\n')
+ self.out.append('\\hyperref[%s]{' % href)
+ if self.reference_label:
+ # TODO: don't use \hyperref if self.reference_label is True
+ self.out.append('\\%s{%s}}' %
+ (self.reference_label, href.replace('#', '')))
+ raise nodes.SkipNode
+
+ def depart_reference(self, node):
+ self.out.append('}')
+ if not self.is_inline(node):
+ self.out.append('\n')
+
+ def visit_revision(self, node):
+ self.visit_docinfo_item(node, 'revision')
+
+ def depart_revision(self, node):
+ self.depart_docinfo_item(node)
+
+ def visit_rubric(self, node):
+ if not self.fallback_stylesheet:
+ self.fallbacks['rubric'] = PreambleCmds.rubric
+ # class wrapper would interfere with ``\section*"`` type commands
+ # (spacing/indent of first paragraph)
+ self.out.append('\n\\DUrubric{')
+
+ def depart_rubric(self, node):
+ self.out.append('}\n')
+
+ def visit_section(self, node):
+ self.section_level += 1
+ # Initialize counter for potential subsections:
+ self._section_number.append(0)
+ # Counter for this section's level (initialized by parent section):
+ self._section_number[self.section_level - 1] += 1
+
+ def depart_section(self, node):
+ # Remove counter for potential subsections:
+ self._section_number.pop()
+ self.section_level -= 1
+
+ def visit_sidebar(self, node):
+ self.duclass_open(node)
+ self.requirements['color'] = PreambleCmds.color
+ if not self.fallback_stylesheet:
+ self.fallbacks['sidebar'] = PreambleCmds.sidebar
+ self.out.append('\\DUsidebar{')
+
+ def depart_sidebar(self, node):
+ self.out.append('}\n')
+ self.duclass_close(node)
+
+ attribution_formats = {'dash': ('—', ''), # EM DASH
+ 'parentheses': ('(', ')'),
+ 'parens': ('(', ')'),
+ 'none': ('', '')}
+
+ def visit_attribution(self, node):
+ prefix, suffix = self.attribution_formats[self.settings.attribution]
+ self.out.append('\\nopagebreak\n\n\\raggedleft ')
+ self.out.append(prefix)
+ self.context.append(suffix)
+
+ def depart_attribution(self, node):
+ self.out.append(self.context.pop() + '\n')
+
+ def visit_status(self, node):
+ self.visit_docinfo_item(node, 'status')
+
+ def depart_status(self, node):
+ self.depart_docinfo_item(node)
+
+ def visit_strong(self, node):
+ self.out.append('\\textbf{')
+ self.visit_inline(node)
+
+ def depart_strong(self, node):
+ self.depart_inline(node)
+ self.out.append('}')
+
+ def visit_substitution_definition(self, node):
+ raise nodes.SkipNode
+
+ def visit_substitution_reference(self, node):
+ self.unimplemented_visit(node)
+
+ def visit_subtitle(self, node):
+ if isinstance(node.parent, nodes.document):
+ self.push_output_collector(self.subtitle)
+ if not self.fallback_stylesheet:
+ self.fallbacks['documentsubtitle'] = PreambleCmds.documentsubtitle # noqa:E501
+ protect = (self.settings.documentclass == 'memoir')
+ self.subtitle_labels += self.ids_to_labels(node, set_anchor=False,
+ protect=protect)
+ # section subtitle: "starred" (no number, not in ToC)
+ elif isinstance(node.parent, nodes.section):
+ self.out.append(r'\%s*{' %
+ self.d_class.section(self.section_level + 1))
+ else:
+ if not self.fallback_stylesheet:
+ self.fallbacks['subtitle'] = PreambleCmds.subtitle
+ self.out.append('\n\\DUsubtitle{')
+
+ def depart_subtitle(self, node):
+ if isinstance(node.parent, nodes.document):
+ self.pop_output_collector()
+ else:
+ self.out.append('}\n')
+
+ def visit_system_message(self, node):
+ self.requirements['color'] = PreambleCmds.color
+ if not self.fallback_stylesheet:
+ self.fallbacks['title'] = PreambleCmds.title
+ if self.settings.legacy_class_functions:
+ self.fallbacks['title'] = PreambleCmds.title_legacy
+ node['classes'] = ['system-message']
+ self.visit_admonition(node)
+ if self.settings.legacy_class_functions:
+ self.out.append('\n\\DUtitle[system-message]{system-message\n')
+ else:
+ self.out.append('\n\\DUtitle{system-message\n')
+ self.append_hypertargets(node)
+ try:
+ line = ', line~%s' % node['line']
+ except KeyError:
+ line = ''
+ self.out.append('}\n\n{\\color{red}%s/%s} in \\texttt{%s}%s\n' %
+ (node['type'], node['level'],
+ self.encode(node['source']), line))
+ if len(node['backrefs']) == 1:
+ self.out.append('\n\\hyperlink{%s}{' % node['backrefs'][0])
+ self.context.append('}')
+ else:
+ backrefs = ['\\hyperlink{%s}{%d}' % (href, i+1)
+ for (i, href) in enumerate(node['backrefs'])]
+ self.context.append('backrefs: ' + ' '.join(backrefs))
+
+ def depart_system_message(self, node):
+ self.out.append(self.context.pop())
+ self.depart_admonition(node)
+
+ def visit_table(self, node):
+ self.duclass_open(node)
+ self.requirements['table'] = PreambleCmds.table
+ if not self.settings.legacy_column_widths:
+ self.requirements['table1'] = PreambleCmds.table_columnwidth
+ if self.active_table.is_open():
+ self.table_stack.append(self.active_table)
+ # nesting longtable does not work (e.g. 2007-04-18)
+ self.active_table = Table(self, 'tabular')
+ # A longtable moves before \paragraph and \subparagraph
+ # section titles if it immediately follows them:
+ if (self.active_table._latex_type == 'longtable'
+ and isinstance(node.parent, nodes.section)
+ and node.parent.index(node) == 1
+ and self.d_class.section(
+ self.section_level).find('paragraph') != -1):
+ self.out.append('\\leavevmode')
+ self.active_table.open()
+ self.active_table.set_table_style(node, self.settings)
+ if self.active_table.borders == 'booktabs':
+ self.requirements['booktabs'] = r'\usepackage{booktabs}'
+ self.push_output_collector([])
+
+ def depart_table(self, node):
+ # wrap content in the right environment:
+ content = self.out
+ self.pop_output_collector()
+ try:
+ width = self.to_latex_length(node['width'])
+ except KeyError:
+ width = r'\linewidth'
+ # Insert hyperlabel and anchor before the table
+ # if it has no caption/title.
+ # See visit_thead() for tables with caption.
+ if not self.active_table.caption:
+ self.out.extend(self.ids_to_labels(
+ node, set_anchor=len(self.table_stack) != 1,
+ newline=True))
+ # TODO: Don't use a longtable or add \noindent before
+ # the next paragraph, when in a "compound paragraph".
+ # Start a new line or a new paragraph?
+ # if (isinstance(node.parent, nodes.compound)
+ # and self._latex_type != 'longtable')?
+ self.out.append(self.active_table.get_opening(width))
+ self.out += content
+ self.out.append(self.active_table.get_closing() + '\n')
+ self.active_table.close()
+ if len(self.table_stack) > 0:
+ self.active_table = self.table_stack.pop()
+ self.duclass_close(node)
+
+ def visit_target(self, node):
+ # Skip indirect targets:
+ if ('refuri' in node # external hyperlink
+ or 'refid' in node # resolved internal link
+ or 'refname' in node): # unresolved internal link
+ ## self.out.append('%% %s\n' % node) # for debugging
+ return
+ self.out.append('%\n')
+ # do we need an anchor (\phantomsection)?
+ set_anchor = not isinstance(node.parent, (nodes.caption, nodes.title))
+ # TODO: where else can/must we omit the \phantomsection?
+ self.out += self.ids_to_labels(node, set_anchor)
+
+ def depart_target(self, node):
+ pass
+
+ def visit_tbody(self, node):
+ # BUG write preamble if not yet done (colspecs not [])
+ # for tables without heads.
+ if not self.active_table.get('preamble written'):
+ self.visit_thead(node)
+ self.depart_thead(None)
+
+ def depart_tbody(self, node):
+ pass
+
+ def visit_term(self, node):
+ """definition list term"""
+ # Commands with optional args inside an optional arg must be put
+ # in a group, e.g. ``\item[{\hyperref[label]{text}}]``.
+ self.out.append('\\item[{')
+
+ def depart_term(self, node):
+ self.out.append('}] ')
+ # Do we need a \leavevmode (line break if the field body begins
+ # with a list or environment)?
+ next_node = node.next_node(descend=False, siblings=True)
+ if isinstance(next_node, nodes.term):
+ self.out.append('\n')
+ elif not isinstance(next_node, nodes.classifier):
+ self.out.append(self.term_postfix(next_node))
+
+ def visit_tgroup(self, node):
+ pass
+
+ def depart_tgroup(self, node):
+ pass
+
+ _thead_depth = 0
+
+ def thead_depth(self):
+ return self._thead_depth
+
+ def visit_thead(self, node):
+ self._thead_depth += 1
+ if 1 == self.thead_depth():
+ self.out.append('{%s}\n' % self.active_table.get_colspecs(node))
+ self.active_table.set('preamble written', 1)
+ if self.active_table.caption:
+ if self._thead_depth == 1:
+ pre = [r'\caption{']
+ post = self.ids_to_labels(node.parent.parent, False) + [r'}\\']
+ else:
+ pre = [r'\caption[]{']
+ post = [r' (... continued)}\\']
+ self.out.extend(pre + self.active_table.caption + post + ['\n'])
+ self.out.extend(self.active_table.visit_thead())
+
+ def depart_thead(self, node):
+ if node is not None:
+ self.out.extend(self.active_table.depart_thead())
+ if self.active_table.need_recurse():
+ node.walkabout(self)
+ self._thead_depth -= 1
+
+ def visit_title(self, node):
+ """Append section and other titles."""
+ # Document title
+ if isinstance(node.parent, nodes.document):
+ self.push_output_collector(self.title)
+ self.context.append('')
+ self.pdfinfo.append(' pdftitle={%s},' %
+ self.encode(node.astext()))
+ # Topic titles (topic, admonition, sidebar)
+ elif (isinstance(node.parent, nodes.topic)
+ or isinstance(node.parent, nodes.admonition)
+ or isinstance(node.parent, nodes.sidebar)):
+ classes = node.parent['classes'] or [node.parent.tagname]
+ if self.settings.legacy_class_functions:
+ self.fallbacks['title'] = PreambleCmds.title_legacy
+ self.out.append('\n\\DUtitle[%s]{' % ','.join(classes))
+ else:
+ if not self.fallback_stylesheet:
+ self.fallbacks['title'] = PreambleCmds.title
+ self.out.append('\n\\DUtitle{')
+ self.context.append('}\n')
+ # Table caption
+ elif isinstance(node.parent, nodes.table):
+ self.push_output_collector(self.active_table.caption)
+ self.context.append('')
+ # Section title
+ else:
+ if hasattr(PreambleCmds, 'secnumdepth'):
+ self.requirements['secnumdepth'] = PreambleCmds.secnumdepth
+ level = self.section_level
+ section_name = self.d_class.section(level)
+ self.out.append('\n\n')
+ if level > len(self.d_class.sections):
+ # section level not supported by LaTeX
+ if self.settings.legacy_class_functions:
+ self.fallbacks['title'] = PreambleCmds.title_legacy
+ section_name += '[section%s]' % roman.toRoman(level)
+ else:
+ if not self.fallback_stylesheet:
+ self.fallbacks['title'] = PreambleCmds.title
+ self.fallbacks['DUclass'] = PreambleCmds.duclass
+ self.out.append('\\begin{DUclass}{section%s}\n'
+ % roman.toRoman(level))
+
+ # System messages heading in red:
+ if 'system-messages' in node.parent['classes']:
+ self.requirements['color'] = PreambleCmds.color
+ section_title = self.encode(node.astext())
+ self.out.append(r'\%s[%s]{\color{red}' % (
+ section_name, section_title))
+ else:
+ self.out.append(r'\%s{' % section_name)
+
+ # label and ToC entry:
+ bookmark = ['']
+ # add sections with unsupported level to toc and pdfbookmarks?
+ ## if level > len(self.d_class.sections):
+ ## section_title = self.encode(node.astext())
+ ## bookmark.append(r'\addcontentsline{toc}{%s}{%s}' %
+ ## (section_name, section_title))
+ bookmark += self.ids_to_labels(node.parent, set_anchor=False)
+ self.context.append('%\n '.join(bookmark) + '%\n}\n')
+ if (level > len(self.d_class.sections)
+ and not self.settings.legacy_class_functions):
+ self.context[-1] += '\\end{DUclass}\n'
+ # MAYBE postfix paragraph and subparagraph with \leavevmode to
+ # ensure floats stay in the section and text starts on a new line.
+
+ def depart_title(self, node):
+ self.out.append(self.context.pop())
+ if isinstance(node.parent, (nodes.table, nodes.document)):
+ self.pop_output_collector()
+
+ def visit_contents(self, node):
+ """Write the table of contents.
+
+ Called from visit_topic() for "contents" topics.
+ """
+ # requirements/setup for local ToC with package "minitoc",
+ if self.use_latex_toc and 'local' in node['classes']:
+ section_name = self.d_class.section(self.section_level)
+ # minitoc only supports "part" and toplevel sections
+ minitoc_names = {'part': 'part',
+ 'chapter': 'mini',
+ 'section': 'sect'}
+ if 'chapter' in self.d_class.sections:
+ del minitoc_names['section']
+ try:
+ mtc_name = minitoc_names[section_name]
+ except KeyError:
+ self.warn('Skipping local ToC at "%s" level.\n'
+ ' Feature not supported with option "use-latex-toc"'
+ % section_name, base_node=node)
+ raise nodes.SkipNode
+
+ # labels and PDF bookmark (sidebar entry)
+ self.out.append('\n') # start new paragraph
+ if node['names']: # don't add labels just for auto-ids
+ self.out += self.ids_to_labels(node, newline=True)
+ if (isinstance(node.next_node(), nodes.title)
+ and 'local' not in node['classes']
+ and self.settings.documentclass != 'memoir'):
+ self.out.append('\\pdfbookmark[%d]{%s}{%s}\n' %
+ (self.section_level+1,
+ node.next_node().astext(),
+ node.get('ids', ['contents'])[0]))
+
+ # Docutils generated contents list (no page numbers)
+ if not self.use_latex_toc:
+ self.fallbacks['toc-list'] = PreambleCmds.toc_list
+ self.duclass_open(node)
+ return
+
+ # ToC by LaTeX
+ depth = node.get('depth', 0)
+ maxdepth = len(self.d_class.sections)
+ if isinstance(node.next_node(), nodes.title):
+ title = self.encode(node[0].astext())
+ else:
+ title = ''
+ if 'local' in node['classes']:
+ # use the "minitoc" package
+ self.requirements['minitoc'] = PreambleCmds.minitoc
+ self.requirements['minitoc-'+mtc_name] = r'\do%stoc'%mtc_name
+ self.requirements['minitoc-%s-depth' % mtc_name] = (
+ r'\mtcsetdepth{%stoc}{%d}' % (mtc_name, maxdepth))
+ # "depth" option: Docutils stores a relative depth while
+ # minitoc expects an absolute depth!:
+ offset = {'sect': 1, 'mini': 0, 'part': 0}
+ if 'chapter' in self.d_class.sections:
+ offset['part'] = -1
+ if depth:
+ self.out.append('\\setcounter{%stocdepth}{%d}' %
+ (mtc_name, depth + offset[mtc_name]))
+ # title:
+ self.out.append('\\mtcsettitle{%stoc}{%s}\n' % (mtc_name, title))
+ # the toc-generating command:
+ self.out.append('\\%stoc\n' % mtc_name)
+ else:
+ if depth:
+ self.out.append('\\setcounter{tocdepth}{%d}\n'
+ % self.d_class.latex_section_depth(depth))
+ if title != 'Contents':
+ self.out.append('\\renewcommand{\\contentsname}{%s}\n' % title)
+ self.out.append('\\tableofcontents\n')
+ self.has_latex_toc = True
+ # ignore rest of node content
+ raise nodes.SkipNode
+
+ def visit_topic(self, node):
+ # Topic nodes can be generic topic, abstract, dedication, or ToC.
+ # table of contents:
+ if 'contents' in node['classes']:
+ self.visit_contents(node)
+ elif ('abstract' in node['classes']
+ and self.settings.use_latex_abstract):
+ self.push_output_collector(self.abstract)
+ self.out.append('\\begin{abstract}')
+ if isinstance(node.next_node(), nodes.title):
+ node.pop(0) # LaTeX provides its own title
+ else:
+ # special topics:
+ if 'abstract' in node['classes']:
+ if not self.fallback_stylesheet:
+ self.fallbacks['abstract'] = PreambleCmds.abstract
+ if self.settings.legacy_class_functions:
+ self.fallbacks['abstract'] = PreambleCmds.abstract_legacy
+ self.push_output_collector(self.abstract)
+ elif 'dedication' in node['classes']:
+ if not self.fallback_stylesheet:
+ self.fallbacks['dedication'] = PreambleCmds.dedication
+ self.push_output_collector(self.dedication)
+ else:
+ node['classes'].insert(0, 'topic')
+ self.visit_block_quote(node)
+
+ def depart_topic(self, node):
+ if ('abstract' in node['classes']
+ and self.settings.use_latex_abstract):
+ self.out.append('\\end{abstract}\n')
+ elif 'contents' in node['classes']:
+ self.duclass_close(node)
+ else:
+ self.depart_block_quote(node)
+ if ('abstract' in node['classes']
+ or 'dedication' in node['classes']):
+ self.pop_output_collector()
+
+ def visit_transition(self, node):
+ if not self.fallback_stylesheet:
+ self.fallbacks['transition'] = PreambleCmds.transition
+ self.out.append('\n%' + '_' * 75 + '\n')
+ self.out.append('\\DUtransition\n')
+
+ def depart_transition(self, node):
+ pass
+
+ def visit_version(self, node):
+ self.visit_docinfo_item(node, 'version')
+
+ def depart_version(self, node):
+ self.depart_docinfo_item(node)
+
+ def unimplemented_visit(self, node):
+ raise NotImplementedError('visiting unimplemented node type: %s' %
+ node.__class__.__name__)
+
+# def unknown_visit(self, node):
+# def default_visit(self, node):
+
+# vim: set ts=4 et ai :
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/default.tex b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/default.tex
new file mode 100644
index 00000000..86552ab6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/default.tex
@@ -0,0 +1,14 @@
+$head_prefix% generated by Docutils <https://docutils.sourceforge.io/>
+\usepackage{cmap} % fix search and cut-and-paste in Acrobat
+$requirements
+%%% Custom LaTeX preamble
+$latex_preamble
+%%% User specified packages and stylesheets
+$stylesheet
+%%% Fallback definitions for Docutils-specific commands
+$fallbacks
+$pdfsetup
+%%% Body
+\begin{document}
+$titledata$body_pre_docinfo$docinfo$dedication$abstract$body
+\end{document}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/docutils.sty b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/docutils.sty
new file mode 100644
index 00000000..52386bb9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/docutils.sty
@@ -0,0 +1,223 @@
+%% docutils.sty: macros for Docutils LaTeX output.
+%%
+%% Copyright © 2020 Günter Milde
+%% Released under the terms of the `2-Clause BSD license`, in short:
+%%
+%% Copying and distribution of this file, with or without modification,
+%% are permitted in any medium without royalty provided the copyright
+%% notice and this notice are preserved.
+%% This file is offered as-is, without any warranty.
+
+% .. include:: README.md
+%
+% Implementation
+% ==============
+%
+% ::
+
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{docutils}
+ [2021/05/18 macros for Docutils LaTeX output]
+
+% Helpers
+% -------
+%
+% duclass::
+
+% class handling for environments (block-level elements)
+% \begin{DUclass}{spam} tries \DUCLASSspam and
+% \end{DUclass}{spam} tries \endDUCLASSspam
+\ifx\DUclass\undefined % poor man's "provideenvironment"
+ \newenvironment{DUclass}[1]%
+ {% "#1" does not work in end-part of environment.
+ \def\DocutilsClassFunctionName{DUCLASS#1}
+ \csname \DocutilsClassFunctionName \endcsname}%
+ {\csname end\DocutilsClassFunctionName \endcsname}%
+\fi
+
+% providelength::
+
+% Provide a length variable and set default, if it is new
+\providecommand*{\DUprovidelength}[2]{
+ \ifthenelse{\isundefined{#1}}{\newlength{#1}\setlength{#1}{#2}}{}
+}
+
+
+% Configuration defaults
+% ----------------------
+%
+% See `Docutils LaTeX Writer`_ for details.
+%
+% abstract::
+
+\providecommand*{\DUCLASSabstract}{
+ \renewcommand{\DUtitle}[1]{\centerline{\textbf{##1}}}
+}
+
+% dedication::
+
+% special topic for dedications
+\providecommand*{\DUCLASSdedication}{%
+ \renewenvironment{quote}{\begin{center}}{\end{center}}%
+}
+
+% TODO: add \em to set dedication text in italics?
+%
+% docinfo::
+
+% width of docinfo table
+\DUprovidelength{\DUdocinfowidth}{0.9\linewidth}
+
+% error::
+
+\providecommand*{\DUCLASSerror}{\color{red}}
+
+% highlight_rules::
+
+% basic code highlight:
+\providecommand*\DUrolecomment[1]{\textcolor[rgb]{0.40,0.40,0.40}{#1}}
+\providecommand*\DUroledeleted[1]{\textcolor[rgb]{0.40,0.40,0.40}{#1}}
+\providecommand*\DUrolekeyword[1]{\textbf{#1}}
+\providecommand*\DUrolestring[1]{\textit{#1}}
+
+% Elements
+% --------
+%
+% Definitions for unknown or to-be-configured Docutils elements.
+%
+% admonition::
+
+% admonition environment (specially marked topic)
+\ifx\DUadmonition\undefined % poor man's "provideenvironment"
+ \newbox{\DUadmonitionbox}
+ \newenvironment{DUadmonition}%
+ {\begin{center}
+ \begin{lrbox}{\DUadmonitionbox}
+ \begin{minipage}{0.9\linewidth}
+ }%
+ { \end{minipage}
+ \end{lrbox}
+ \fbox{\usebox{\DUadmonitionbox}}
+ \end{center}
+ }
+\fi
+
+% fieldlist::
+
+% field list environment (for separate configuration of `field lists`)
+\ifthenelse{\isundefined{\DUfieldlist}}{
+ \newenvironment{DUfieldlist}%
+ {\quote\description}
+ {\enddescription\endquote}
+}{}
+
+% footnotes::
+
+% numerical or symbol footnotes with hyperlinks and backlinks
+\providecommand*{\DUfootnotemark}[3]{%
+ \raisebox{1em}{\hypertarget{#1}{}}%
+ \hyperlink{#2}{\textsuperscript{#3}}%
+}
+\providecommand{\DUfootnotetext}[4]{%
+ \begingroup%
+ \renewcommand{\thefootnote}{%
+ \protect\raisebox{1em}{\protect\hypertarget{#1}{}}%
+ \protect\hyperlink{#2}{#3}}%
+ \footnotetext{#4}%
+ \endgroup%
+}
+
+% inline::
+
+% custom inline roles: \DUrole{#1}{#2} tries \DUrole#1{#2}
+\providecommand*{\DUrole}[2]{%
+ \ifcsname DUrole#1\endcsname%
+ \csname DUrole#1\endcsname{#2}%
+ \else%
+ #2%
+ \fi%
+}
+
+% legend::
+
+% legend environment (in figures and formal tables)
+\ifthenelse{\isundefined{\DUlegend}}{
+ \newenvironment{DUlegend}{\small}{}
+}{}
+
+% lineblock::
+
+% line block environment
+\DUprovidelength{\DUlineblockindent}{2.5em}
+\ifthenelse{\isundefined{\DUlineblock}}{
+ \newenvironment{DUlineblock}[1]{%
+ \list{}{\setlength{\partopsep}{\parskip}
+ \addtolength{\partopsep}{\baselineskip}
+ \setlength{\topsep}{0pt}
+ \setlength{\itemsep}{0.15\baselineskip}
+ \setlength{\parsep}{0pt}
+ \setlength{\leftmargin}{#1}}
+ \raggedright
+ }
+ {\endlist}
+}{}
+
+% optionlist::
+
+% list of command line options
+\providecommand*{\DUoptionlistlabel}[1]{\bfseries #1 \hfill}
+\DUprovidelength{\DUoptionlistindent}{3cm}
+\ifthenelse{\isundefined{\DUoptionlist}}{
+ \newenvironment{DUoptionlist}{%
+ \list{}{\setlength{\labelwidth}{\DUoptionlistindent}
+ \setlength{\rightmargin}{1cm}
+ \setlength{\leftmargin}{\rightmargin}
+ \addtolength{\leftmargin}{\labelwidth}
+ \addtolength{\leftmargin}{\labelsep}
+ \renewcommand{\makelabel}{\DUoptionlistlabel}}
+ }
+ {\endlist}
+}{}
+
+% rubric::
+
+% informal heading
+\providecommand*{\DUrubric}[1]{\subsubsection*{\emph{#1}}}
+
+% sidebar::
+
+% text outside the main text flow
+\providecommand{\DUsidebar}[1]{%
+ \begin{center}
+ \colorbox[gray]{0.80}{\parbox{0.9\linewidth}{#1}}
+ \end{center}
+}
+
+% title::
+
+% title for topics, admonitions, unsupported section levels, and sidebar
+\providecommand*{\DUtitle}[1]{%
+ \smallskip\noindent\textbf{#1}\smallskip}
+
+% subtitle::
+
+% subtitle (for sidebar)
+\providecommand*{\DUsubtitle}[1]{\par\emph{#1}\smallskip}
+
+% documentsubtitle::
+
+% subtitle (in document title)
+\providecommand*{\DUdocumentsubtitle}[1]{{\large #1}}
+
+% titlereference::
+
+% titlereference standard role
+\providecommand*{\DUroletitlereference}[1]{\textsl{#1}}
+
+% transition::
+
+% transition (break / fancybreak / anonymous section)
+\providecommand*{\DUtransition}{%
+ \hspace*{\fill}\hrulefill\hspace*{\fill}
+ \vskip 0.5\baselineskip
+}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlepage.tex b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlepage.tex
new file mode 100644
index 00000000..278fba80
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlepage.tex
@@ -0,0 +1,19 @@
+% generated by Docutils <https://docutils.sourceforge.io/>
+$head_prefix
+\usepackage{cmap} % fix search and cut-and-paste in Acrobat
+$requirements
+%%% Custom LaTeX preamble
+$latex_preamble
+%%% User specified packages and stylesheets
+$stylesheet
+%%% Fallback definitions for Docutils-specific commands
+$fallbacks$pdfsetup
+$titledata
+%%% Body
+\begin{document}
+\begin{titlepage}
+$body_pre_docinfo$docinfo$dedication$abstract
+\thispagestyle{empty}
+\end{titlepage}
+$body
+\end{document}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlingpage.tex b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlingpage.tex
new file mode 100644
index 00000000..1e96806d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/titlingpage.tex
@@ -0,0 +1,18 @@
+% generated by Docutils <https://docutils.sourceforge.io/>
+$head_prefix
+$requirements
+%%% Custom LaTeX preamble
+$latex_preamble
+%%% User specified packages and stylesheets
+$stylesheet
+%%% Fallback definitions for Docutils-specific commands
+$fallbacks$pdfsetup
+$titledata
+%%% Body
+\begin{document}
+\begin{titlingpage}
+\thispagestyle{empty}
+$body_pre_docinfo$docinfo$dedication$abstract
+\end{titlingpage}
+$body
+\end{document}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/xelatex.tex b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/xelatex.tex
new file mode 100644
index 00000000..4d802805
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/latex2e/xelatex.tex
@@ -0,0 +1,21 @@
+$head_prefix% generated by Docutils <https://docutils.sourceforge.io/>
+% rubber: set program xelatex
+\usepackage{fontspec}
+% \defaultfontfeatures{Scale=MatchLowercase}
+% straight double quotes (defined T1 but missing in TU):
+\ifdefined \UnicodeEncodingName
+ \DeclareTextCommand{\textquotedbl}{\UnicodeEncodingName}{%
+ {\addfontfeatures{RawFeature=-tlig,Mapping=}\char34}}%
+\fi
+$requirements
+%%% Custom LaTeX preamble
+$latex_preamble
+%%% User specified packages and stylesheets
+$stylesheet
+%%% Fallback definitions for Docutils-specific commands
+$fallbacks$pdfsetup
+$titledata
+%%% Body
+\begin{document}
+$body_pre_docinfo$docinfo$dedication$abstract$body
+\end{document}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/manpage.py b/.venv/lib/python3.12/site-packages/docutils/writers/manpage.py
new file mode 100644
index 00000000..9c0ab479
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/manpage.py
@@ -0,0 +1,1214 @@
+# $Id: manpage.py 9610 2024-04-03 17:29:36Z grubert $
+# Author: Engelbert Gruber <grubert@users.sourceforge.net>
+# Copyright: This module is put into the public domain.
+
+"""
+Simple man page writer for reStructuredText.
+
+Man pages (short for "manual pages") contain system documentation on unix-like
+systems. The pages are grouped in numbered sections:
+
+ 1 executable programs and shell commands
+ 2 system calls
+ 3 library functions
+ 4 special files
+ 5 file formats
+ 6 games
+ 7 miscellaneous
+ 8 system administration
+
+Man pages are written *troff*, a text file formatting system.
+
+See https://www.tldp.org/HOWTO/Man-Page for a start.
+
+Man pages have no subsection only parts.
+Standard parts
+
+ NAME ,
+ SYNOPSIS ,
+ DESCRIPTION ,
+ OPTIONS ,
+ FILES ,
+ SEE ALSO ,
+ BUGS ,
+
+and
+
+ AUTHOR .
+
+A unix-like system keeps an index of the DESCRIPTIONs, which is accessible
+by the command whatis or apropos.
+
+"""
+
+__docformat__ = 'reStructuredText'
+
+import re
+
+from docutils import nodes, writers, languages
+try:
+ import roman
+except ImportError:
+ import docutils.utils.roman as roman
+
+FIELD_LIST_INDENT = 7
+DEFINITION_LIST_INDENT = 7
+OPTION_LIST_INDENT = 7
+BLOCKQOUTE_INDENT = 3.5
+LITERAL_BLOCK_INDENT = 3.5
+
+# Define two macros so man/roff can calculate the
+# indent/unindent margins by itself
+MACRO_DEF = (r""".
+.nr rst2man-indent-level 0
+.
+.de1 rstReportMargin
+\\$1 \\n[an-margin]
+level \\n[rst2man-indent-level]
+level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
+-
+\\n[rst2man-indent0]
+\\n[rst2man-indent1]
+\\n[rst2man-indent2]
+..
+.de1 INDENT
+.\" .rstReportMargin pre:
+. RS \\$1
+. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
+. nr rst2man-indent-level +1
+.\" .rstReportMargin post:
+..
+.de UNINDENT
+. RE
+.\" indent \\n[an-margin]
+.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.nr rst2man-indent-level -1
+.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
+..
+""")
+
+
+class Writer(writers.Writer):
+
+ supported = ('manpage',)
+ """Formats this writer supports."""
+
+ output = None
+ """Final translated form of `document`."""
+
+ def __init__(self):
+ writers.Writer.__init__(self)
+ self.translator_class = Translator
+
+ def translate(self):
+ visitor = self.translator_class(self.document)
+ self.document.walkabout(visitor)
+ self.output = visitor.astext()
+
+
+class Table:
+ def __init__(self):
+ self._rows = []
+ self._options = ['box', 'center']
+ self._tab_char = '\t'
+ self._coldefs = []
+
+ def new_row(self):
+ self._rows.append([])
+
+ def append_separator(self, separator):
+ """Append the separator for table head."""
+ self._rows.append([separator])
+
+ def append_cell(self, cell_lines):
+ """cell_lines is an array of lines"""
+ start = 0
+ if len(cell_lines) > 0 and cell_lines[0] == '.sp\n':
+ start = 1
+ self._rows[-1].append(cell_lines[start:])
+ if len(self._coldefs) < len(self._rows[-1]):
+ self._coldefs.append('l')
+
+ def _minimize_cell(self, cell_lines):
+ """Remove leading and trailing blank and ``.sp`` lines"""
+ while cell_lines and cell_lines[0] in ('\n', '.sp\n'):
+ del cell_lines[0]
+ while cell_lines and cell_lines[-1] in ('\n', '.sp\n'):
+ del cell_lines[-1]
+
+ def as_list(self):
+ text = ['.TS\n']
+ text.append(' '.join(self._options) + ';\n')
+ text.append('%s.\n' % ('|'.join(self._coldefs)))
+ for row in self._rows:
+ # row = array of cells. cell = array of lines.
+ text.append('T{\n')
+ for i in range(len(row)):
+ cell = row[i]
+ self._minimize_cell(cell)
+ text.extend(cell)
+ if not text[-1].endswith('\n'):
+ text[-1] += '\n'
+ if i < len(row)-1:
+ text.append('T}'+self._tab_char+'T{\n')
+ else:
+ text.append('T}\n')
+ text.append('_\n') # line between rows
+ text.pop() # pop last "line between"
+ text.append('.TE\n')
+ return text
+
+
+class Translator(nodes.NodeVisitor):
+ """"""
+
+ words_and_spaces = re.compile(r'\S+| +|\n')
+ possibly_a_roff_command = re.compile(r'\.\w')
+ document_start = """Man page generated from reStructuredText."""
+ # TODO add "from docutils 0.21rc1."
+
+ def __init__(self, document):
+ nodes.NodeVisitor.__init__(self, document)
+ self.settings = settings = document.settings
+ lcode = settings.language_code
+ self.language = languages.get_language(lcode, document.reporter)
+ self.head = []
+ self.body = []
+ self.foot = []
+ self.section_level = 0
+ self.context = []
+ self.topic_class = ''
+ self.colspecs = []
+ self.compact_p = 1
+ self.compact_simple = None
+ # the list style "*" bullet or "#" numbered
+ self._list_char = []
+ # writing the header .TH and .SH NAME is postboned after
+ # docinfo.
+ self._docinfo = {
+ "title": "", "title_upper": "",
+ "subtitle": "",
+ "manual_section": "", "manual_group": "",
+ "author": [],
+ "date": "",
+ "copyright": "",
+ "version": "",
+ }
+ self._docinfo_keys = [] # a list to keep the sequence as in source.
+ self._docinfo_names = {} # to get name from text not normalized.
+ self._in_docinfo = None
+ self._field_name = None
+ self._active_table = None
+ self._has_a_table = False # is there a table in this document
+ self._in_literal = False
+ self.header_written = 0
+ self._line_block = 0
+ self.authors = []
+ self.section_level = 0
+ self._indent = [0]
+ # central definition of simple processing rules
+ # what to output on : visit, depart
+ # Do not use paragraph requests ``.PP`` because these set indentation.
+ # use ``.sp``. Remove superfluous ``.sp`` in ``astext``.
+ #
+ # Fonts are put on a stack, the top one is used.
+ # ``.ft P`` or ``\\fP`` pop from stack.
+ # But ``.BI`` seams to fill stack with BIBIBIBIB...
+ # ``B`` bold, ``I`` italic, ``R`` roman should be available.
+ #
+ # Requests start wit a dot ``.`` or the no-break control character,
+ # a neutral apostrophe ``'`` suppresses the break implied by some
+ # requests.
+ self.defs = {
+ 'indent': ('.INDENT %.1f\n', '.UNINDENT\n'),
+ 'definition_list_item': ('.TP', ''), # par. with hanging tag
+ 'field_name': ('.TP\n.B ', '\n'),
+ 'literal': ('\\fB', '\\fP'),
+ 'literal_block': ('.sp\n.EX\n', '\n.EE\n'),
+
+ 'option_list_item': ('.TP\n', ''),
+
+ 'reference': (r'\fI\%', r'\fP'),
+ 'emphasis': ('\\fI', '\\fP'),
+ 'strong': ('\\fB', '\\fP'),
+ 'title_reference': ('\\fI', '\\fP'),
+
+ 'topic-title': ('.SS ',),
+ 'sidebar-title': ('.SS ',),
+
+ 'problematic': ('\n.nf\n', '\n.fi\n'),
+ }
+ # NOTE do not specify the newline before a dot-command, but ensure
+ # it is there.
+
+ def comment_begin(self, text):
+ """Return commented version of the passed text WITHOUT end of
+ line/comment."""
+ prefix = '.\\" '
+ out_text = ''.join([(prefix + in_line + '\n')
+ for in_line in text.split('\n')])
+ return out_text
+
+ def comment(self, text):
+ """Return commented version of the passed text."""
+ return self.comment_begin(text)+'.\n'
+
+ def ensure_eol(self):
+ """Ensure the last line in body is terminated by new line."""
+ if len(self.body) > 0 and self.body[-1][-1] != '\n':
+ self.body.append('\n')
+
+ def astext(self):
+ """Return the final formatted document as a string."""
+ if not self.header_written:
+ # ensure we get a ".TH" as viewers require it.
+ self.append_header()
+ # filter body
+ for i in range(len(self.body)-1, 0, -1):
+ # remove superfluous vertical gaps.
+ if self.body[i] == '.sp\n':
+ if self.body[i - 1][:4] in ('.BI ', '.IP '):
+ self.body[i] = '.\n'
+ elif (self.body[i - 1][:3] == '.B '
+ and self.body[i - 2][:4] == '.TP\n'):
+ self.body[i] = '.\n'
+ elif (self.body[i - 1] == '\n'
+ and not self.possibly_a_roff_command.match(
+ self.body[i - 2])
+ and (self.body[i - 3][:7] == '.TP\n.B '
+ or self.body[i - 3][:4] == '\n.B ')
+ ):
+ self.body[i] = '.\n'
+ return ''.join(self.head + self.body + self.foot)
+
+ def deunicode(self, text):
+ text = text.replace('\xa0', '\\ ')
+ text = text.replace('\u2020', '\\(dg')
+ return text
+
+ def visit_Text(self, node):
+ text = node.astext()
+ text = text.replace('\\', '\\e')
+ replace_pairs = [
+ ('-', '\\-'),
+ ('\'', '\\(aq'),
+ ('´', "\\'"),
+ ('`', '\\(ga'),
+ ('"', '\\(dq'), # double quotes are a problem on macro lines
+ ]
+ for (in_char, out_markup) in replace_pairs:
+ text = text.replace(in_char, out_markup)
+ # unicode
+ text = self.deunicode(text)
+ # prevent interpretation of "." at line start
+ if text.startswith('.'):
+ text = '\\&' + text
+ if self._in_literal:
+ text = text.replace('\n.', '\n\\&.')
+ self.body.append(text)
+
+ def depart_Text(self, node):
+ pass
+
+ def list_start(self, node):
+ class EnumChar:
+ enum_style = {
+ 'bullet': '\\(bu',
+ 'emdash': '\\(em',
+ }
+
+ def __init__(self, style):
+ self._style = style
+ if 'start' in node:
+ self._cnt = node['start'] - 1
+ else:
+ self._cnt = 0
+ self._indent = 2
+ if style == 'arabic':
+ # indentation depends on number of children
+ # and start value.
+ self._indent = len(str(len(node.children)))
+ self._indent += len(str(self._cnt)) + 1
+ elif style == 'loweralpha':
+ self._cnt += ord('a') - 1
+ self._indent = 3
+ elif style == 'upperalpha':
+ self._cnt += ord('A') - 1
+ self._indent = 3
+ elif style.endswith('roman'):
+ self._indent = 5
+
+ def __next__(self):
+ if self._style == 'bullet':
+ return self.enum_style[self._style]
+ elif self._style == 'emdash':
+ return self.enum_style[self._style]
+ self._cnt += 1
+ # TODO add prefix postfix
+ if self._style == 'arabic':
+ return "%d." % self._cnt
+ elif self._style in ('loweralpha', 'upperalpha'):
+ return "%c." % self._cnt
+ elif self._style.endswith('roman'):
+ res = roman.toRoman(self._cnt) + '.'
+ if self._style.startswith('upper'):
+ return res.upper()
+ return res.lower()
+ else:
+ return "%d." % self._cnt
+
+ def get_width(self):
+ return self._indent
+
+ def __repr__(self):
+ return 'enum_style-%s' % list(self._style)
+
+ if 'enumtype' in node:
+ self._list_char.append(EnumChar(node['enumtype']))
+ else:
+ self._list_char.append(EnumChar('bullet'))
+ if len(self._list_char) > 1:
+ # indent nested lists
+ self.indent(self._list_char[-2].get_width())
+ else:
+ self.indent(self._list_char[-1].get_width())
+
+ def list_end(self):
+ self.dedent()
+ self._list_char.pop()
+
+ def header(self):
+ th = (".TH \"%(title_upper)s\" \"%(manual_section)s\""
+ " \"%(date)s\" \"%(version)s\"") % self._docinfo
+ if self._docinfo["manual_group"]:
+ th += " \"%(manual_group)s\"" % self._docinfo
+ th += "\n"
+ sh_tmpl = (".SH NAME\n"
+ "%(title)s \\- %(subtitle)s\n")
+ return th + sh_tmpl % self._docinfo
+
+ def append_header(self):
+ """append header with .TH and .SH NAME"""
+ # NOTE before everything
+ # .TH title_upper section date source manual
+ # BUT macros before .TH for whatis database generators.
+ if self.header_written:
+ return
+ self.head.append(MACRO_DEF)
+ self.head.append(self.header())
+ self.header_written = 1
+
+ def visit_address(self, node):
+ self.visit_docinfo_item(node, 'address')
+
+ def depart_address(self, node):
+ pass
+
+ def visit_admonition(self, node, name=None):
+ #
+ # Make admonitions a simple block quote
+ # with a strong heading
+ #
+ # Using .IP/.RE doesn't preserve indentation
+ # when admonitions contain bullets, literal,
+ # and/or block quotes.
+ #
+ if name:
+ # .. admonition:: has no name
+ self.body.append('.sp\n')
+ name = '%s%s:%s\n' % (
+ self.defs['strong'][0],
+ self.language.labels.get(name, name).upper(),
+ self.defs['strong'][1],
+ )
+ self.body.append(name)
+ self.visit_block_quote(node)
+
+ def depart_admonition(self, node):
+ self.depart_block_quote(node)
+
+ def visit_attention(self, node):
+ self.visit_admonition(node, 'attention')
+
+ depart_attention = depart_admonition
+
+ def visit_docinfo_item(self, node, name):
+ if name == 'author':
+ self._docinfo[name].append(node.astext())
+ else:
+ self._docinfo[name] = node.astext()
+ self._docinfo_keys.append(name)
+ raise nodes.SkipNode
+
+ def depart_docinfo_item(self, node):
+ pass
+
+ def visit_author(self, node):
+ self.visit_docinfo_item(node, 'author')
+
+ depart_author = depart_docinfo_item
+
+ def visit_authors(self, node):
+ # _author is called anyway.
+ pass
+
+ def depart_authors(self, node):
+ pass
+
+ def visit_block_quote(self, node):
+ # BUG/HACK: indent always uses the _last_ indentation,
+ # thus we need two of them.
+ self.indent(BLOCKQOUTE_INDENT)
+ self.indent(0)
+
+ def depart_block_quote(self, node):
+ self.dedent()
+ self.dedent()
+
+ def visit_bullet_list(self, node):
+ self.list_start(node)
+
+ def depart_bullet_list(self, node):
+ self.list_end()
+
+ def visit_caption(self, node):
+ pass
+
+ def depart_caption(self, node):
+ pass
+
+ def visit_caution(self, node):
+ self.visit_admonition(node, 'caution')
+
+ depart_caution = depart_admonition
+
+ def visit_citation(self, node):
+ num = node.astext().split(None, 1)[0]
+ num = num.strip()
+ self.body.append('.IP [%s] 5\n' % num)
+
+ def depart_citation(self, node):
+ pass
+
+ def visit_citation_reference(self, node):
+ self.body.append('['+node.astext()+']')
+ raise nodes.SkipNode
+
+ def visit_classifier(self, node):
+ self.body.append('(')
+
+ def depart_classifier(self, node):
+ self.body.append(')')
+ self.depart_term(node) # close the term element after last classifier
+
+ def visit_colspec(self, node):
+ self.colspecs.append(node)
+
+ def depart_colspec(self, node):
+ pass
+
+ def write_colspecs(self):
+ self.body.append("%s.\n" % ('L '*len(self.colspecs)))
+
+ def visit_comment(self, node,
+ sub=re.compile('-(?=-)').sub):
+ self.body.append(self.comment(node.astext()))
+ raise nodes.SkipNode
+
+ def visit_contact(self, node):
+ self.visit_docinfo_item(node, 'contact')
+
+ depart_contact = depart_docinfo_item
+
+ def visit_container(self, node):
+ pass
+
+ def depart_container(self, node):
+ pass
+
+ def visit_compound(self, node):
+ pass
+
+ def depart_compound(self, node):
+ pass
+
+ def visit_copyright(self, node):
+ self.visit_docinfo_item(node, 'copyright')
+
+ def visit_danger(self, node):
+ self.visit_admonition(node, 'danger')
+
+ depart_danger = depart_admonition
+
+ def visit_date(self, node):
+ self.visit_docinfo_item(node, 'date')
+
+ def visit_decoration(self, node):
+ pass
+
+ def depart_decoration(self, node):
+ pass
+
+ def visit_definition(self, node):
+ pass
+
+ def depart_definition(self, node):
+ pass
+
+ def visit_definition_list(self, node):
+ self.indent(DEFINITION_LIST_INDENT)
+
+ def depart_definition_list(self, node):
+ self.dedent()
+
+ def visit_definition_list_item(self, node):
+ self.body.append(self.defs['definition_list_item'][0])
+
+ def depart_definition_list_item(self, node):
+ self.body.append(self.defs['definition_list_item'][1])
+
+ def visit_description(self, node):
+ pass
+
+ def depart_description(self, node):
+ pass
+
+ def visit_docinfo(self, node):
+ self._in_docinfo = 1
+
+ def depart_docinfo(self, node):
+ self._in_docinfo = None
+ # NOTE nothing should be written before this
+ self.append_header()
+
+ def visit_doctest_block(self, node):
+ self.body.append(self.defs['literal_block'][0])
+ self._in_literal = True
+
+ def depart_doctest_block(self, node):
+ self._in_literal = False
+ self.body.append(self.defs['literal_block'][1])
+
+ def visit_document(self, node):
+ # no blank line between comment and header.
+ self.head.append(self.comment(self.document_start).rstrip()+'\n')
+ # writing header is postponed
+ self.header_written = 0
+
+ def depart_document(self, node):
+ if self._docinfo['author']:
+ self.body.append('.SH AUTHOR\n%s\n'
+ % ', '.join(self._docinfo['author']))
+ skip = ('author', 'copyright', 'date',
+ 'manual_group', 'manual_section',
+ 'subtitle',
+ 'title', 'title_upper', 'version')
+ for name in self._docinfo_keys:
+ if name == 'address':
+ self.body.append("\n%s:\n%s%s.nf\n%s\n.fi\n%s%s" % (
+ self.language.labels.get(name, name),
+ self.defs['indent'][0] % 0,
+ self.defs['indent'][0] % BLOCKQOUTE_INDENT,
+ self._docinfo[name],
+ self.defs['indent'][1],
+ self.defs['indent'][1]))
+ elif name not in skip:
+ if name in self._docinfo_names:
+ label = self._docinfo_names[name]
+ else:
+ label = self.language.labels.get(name, name)
+ self.body.append("\n%s: %s\n" % (label, self._docinfo[name]))
+ if self._docinfo['copyright']:
+ self.body.append('.SH COPYRIGHT\n%s\n'
+ % self._docinfo['copyright'])
+ self.body.append(self.comment('Generated by docutils manpage writer.'))
+
+ def visit_emphasis(self, node):
+ self.body.append(self.defs['emphasis'][0])
+
+ def depart_emphasis(self, node):
+ self.body.append(self.defs['emphasis'][1])
+
+ def visit_entry(self, node):
+ # a cell in a table row
+ if 'morerows' in node:
+ self.document.reporter.warning(
+ '"table row spanning" not supported', base_node=node)
+ if 'morecols' in node:
+ self.document.reporter.warning(
+ '"table cell spanning" not supported', base_node=node)
+ self.context.append(len(self.body))
+
+ def depart_entry(self, node):
+ start = self.context.pop()
+ self._active_table.append_cell(self.body[start:])
+ del self.body[start:]
+
+ def visit_enumerated_list(self, node):
+ self.list_start(node)
+
+ def depart_enumerated_list(self, node):
+ self.list_end()
+
+ def visit_error(self, node):
+ self.visit_admonition(node, 'error')
+
+ depart_error = depart_admonition
+
+ def visit_field(self, node):
+ pass
+
+ def depart_field(self, node):
+ pass
+
+ def visit_field_body(self, node):
+ if self._in_docinfo:
+ name_normalized = self._field_name.lower().replace(" ", "_")
+ self._docinfo_names[name_normalized] = self._field_name
+ self.visit_docinfo_item(node, name_normalized)
+ raise nodes.SkipNode
+
+ def depart_field_body(self, node):
+ pass
+
+ def visit_field_list(self, node):
+ self.indent(FIELD_LIST_INDENT)
+
+ def depart_field_list(self, node):
+ self.dedent()
+
+ def visit_field_name(self, node):
+ if self._in_docinfo:
+ self._field_name = node.astext()
+ raise nodes.SkipNode
+ else:
+ self.body.append(self.defs['field_name'][0])
+
+ def depart_field_name(self, node):
+ self.body.append(self.defs['field_name'][1])
+
+ def visit_figure(self, node):
+ self.indent(2.5)
+ self.indent(0)
+
+ def depart_figure(self, node):
+ self.dedent()
+ self.dedent()
+
+ def visit_footer(self, node):
+ self.document.reporter.warning('"footer" not supported',
+ base_node=node)
+ # avoid output the link to document source
+ raise nodes.SkipNode
+
+ def depart_footer(self, node):
+ pass
+
+ def visit_footnote(self, node):
+ num, text = node.astext().split(None, 1)
+ num = num.strip()
+ self.body.append('.IP [%s] 5\n' % self.deunicode(num))
+
+ def depart_footnote(self, node):
+ pass
+
+ def footnote_backrefs(self, node):
+ self.document.reporter.warning('"footnote_backrefs" not supported',
+ base_node=node)
+
+ def visit_footnote_reference(self, node):
+ self.body.append('['+self.deunicode(node.astext())+']')
+ raise nodes.SkipNode
+
+ def depart_footnote_reference(self, node):
+ pass
+
+ def visit_generated(self, node):
+ pass
+
+ def depart_generated(self, node):
+ pass
+
+ def visit_header(self, node):
+ raise NotImplementedError(node.astext())
+
+ def depart_header(self, node):
+ pass
+
+ def visit_hint(self, node):
+ self.visit_admonition(node, 'hint')
+
+ depart_hint = depart_admonition
+
+ def visit_subscript(self, node):
+ self.body.append('\\s-2\\d')
+
+ def depart_subscript(self, node):
+ self.body.append('\\u\\s0')
+
+ def visit_superscript(self, node):
+ self.body.append('\\s-2\\u')
+
+ def depart_superscript(self, node):
+ self.body.append('\\d\\s0')
+
+ def visit_attribution(self, node):
+ self.body.append('\\(em ')
+
+ def depart_attribution(self, node):
+ self.body.append('\n')
+
+ def visit_image(self, node):
+ self.document.reporter.warning('"image" not supported',
+ base_node=node)
+ text = []
+ if 'alt' in node.attributes:
+ text.append(node.attributes['alt'])
+ if 'uri' in node.attributes:
+ text.append(node.attributes['uri'])
+ self.body.append('[image: %s]\n' % ('/'.join(text)))
+ raise nodes.SkipNode
+
+ def visit_important(self, node):
+ self.visit_admonition(node, 'important')
+
+ depart_important = depart_admonition
+
+ def visit_inline(self, node):
+ pass
+
+ def depart_inline(self, node):
+ pass
+
+ def visit_label(self, node):
+ # footnote and citation
+ if (isinstance(node.parent, nodes.footnote)
+ or isinstance(node.parent, nodes.citation)):
+ raise nodes.SkipNode
+ self.document.reporter.warning('"unsupported "label"',
+ base_node=node)
+ self.body.append('[')
+
+ def depart_label(self, node):
+ self.body.append(']\n')
+
+ def visit_legend(self, node):
+ pass
+
+ def depart_legend(self, node):
+ pass
+
+ # WHAT should we use .INDENT, .UNINDENT ?
+ def visit_line_block(self, node):
+ self._line_block += 1
+ if self._line_block == 1:
+ # TODO: separate inline blocks from previous paragraphs
+ # see http://hg.intevation.org/mercurial/crew/rev/9c142ed9c405
+ # self.body.append('.sp\n')
+ # but it does not work for me.
+ self.body.append('.nf\n')
+ else:
+ self.body.append('.in +2\n')
+
+ def depart_line_block(self, node):
+ self._line_block -= 1
+ if self._line_block == 0:
+ self.body.append('.fi\n')
+ self.body.append('.sp\n')
+ else:
+ self.body.append('.in -2\n')
+
+ def visit_line(self, node):
+ pass
+
+ def depart_line(self, node):
+ self.body.append('\n')
+
+ def visit_list_item(self, node):
+ # man 7 man argues to use ".IP" instead of ".TP"
+ self.body.append('.IP %s %d\n' % (
+ next(self._list_char[-1]),
+ self._list_char[-1].get_width(),))
+
+ def depart_list_item(self, node):
+ pass
+
+ def visit_literal(self, node):
+ self.body.append(self.defs['literal'][0])
+
+ def depart_literal(self, node):
+ self.body.append(self.defs['literal'][1])
+
+ def visit_literal_block(self, node):
+ # BUG/HACK: indent always uses the _last_ indentation,
+ # thus we need two of them.
+ self.indent(LITERAL_BLOCK_INDENT)
+ self.indent(0)
+ self.body.append(self.defs['literal_block'][0])
+ self._in_literal = True
+
+ def depart_literal_block(self, node):
+ self._in_literal = False
+ self.body.append(self.defs['literal_block'][1])
+ self.dedent()
+ self.dedent()
+
+ def visit_math(self, node):
+ self.document.reporter.warning('"math" role not supported',
+ base_node=node)
+ self.visit_literal(node)
+
+ def depart_math(self, node):
+ self.depart_literal(node)
+
+ def visit_math_block(self, node):
+ self.document.reporter.warning('"math" directive not supported',
+ base_node=node)
+ self.visit_literal_block(node)
+
+ def depart_math_block(self, node):
+ self.depart_literal_block(node)
+
+ # <meta> shall become an optional standard node:
+ # def visit_meta(self, node):
+ # raise NotImplementedError(node.astext())
+
+ # def depart_meta(self, node):
+ # pass
+
+ def visit_note(self, node):
+ self.visit_admonition(node, 'note')
+
+ depart_note = depart_admonition
+
+ def indent(self, by=0.5):
+ # if we are in a section ".SH" there already is a .RS
+ step = self._indent[-1]
+ self._indent.append(by)
+ self.body.append(self.defs['indent'][0] % step)
+
+ def dedent(self):
+ self._indent.pop()
+ self.body.append(self.defs['indent'][1])
+
+ def visit_option_list(self, node):
+ self.indent(OPTION_LIST_INDENT)
+
+ def depart_option_list(self, node):
+ self.dedent()
+
+ def visit_option_list_item(self, node):
+ # one item of the list
+ self.body.append(self.defs['option_list_item'][0])
+
+ def depart_option_list_item(self, node):
+ self.body.append(self.defs['option_list_item'][1])
+
+ def visit_option_group(self, node):
+ # as one option could have several forms it is a group
+ # options without parameter bold only, .B, -v
+ # options with parameter bold italic, .BI, -f file
+ #
+ # we do not know if .B or .BI, blind guess:
+ self.context.append('.B ') # Add blank for sphinx (docutils/bugs/380)
+ self.context.append(len(self.body)) # to be able to insert later
+ self.context.append(0) # option counter
+
+ def depart_option_group(self, node):
+ self.context.pop() # the counter
+ start_position = self.context.pop()
+ text = self.body[start_position:]
+ del self.body[start_position:]
+ self.body.append('%s%s\n' % (self.context.pop(), ''.join(text)))
+
+ def visit_option(self, node):
+ # each form of the option will be presented separately
+ if self.context[-1] > 0:
+ if self.context[-3] == '.BI':
+ self.body.append('\\fR,\\fB ')
+ else:
+ self.body.append('\\fP,\\fB ')
+ if self.context[-3] == '.BI':
+ self.body.append('\\')
+ self.body.append(' ')
+
+ def depart_option(self, node):
+ self.context[-1] += 1
+
+ def visit_option_string(self, node):
+ # do not know if .B or .BI
+ pass
+
+ def depart_option_string(self, node):
+ pass
+
+ def visit_option_argument(self, node):
+ self.context[-3] = '.BI' # bold/italic alternate
+ if node['delimiter'] != ' ':
+ self.body.append('\\fB%s ' % node['delimiter'])
+ elif self.body[len(self.body)-1].endswith('='):
+ # a blank only means no blank in output, just changing font
+ self.body.append(' ')
+ else:
+ # blank backslash blank, switch font then a blank
+ self.body.append(' \\ ')
+
+ def depart_option_argument(self, node):
+ pass
+
+ def visit_organization(self, node):
+ self.visit_docinfo_item(node, 'organization')
+
+ def depart_organization(self, node):
+ pass
+
+ def first_child(self, node):
+ first = isinstance(node.parent[0], nodes.label) # skip label
+ for child in node.parent.children[first:]:
+ if isinstance(child, nodes.Invisible):
+ continue
+ if child is node:
+ return 1
+ break
+ return 0
+
+ def visit_paragraph(self, node):
+ # ``.PP`` : Start standard indented paragraph.
+ # ``.LP`` : Start block paragraph, all except the first.
+ # ``.P [type]`` : Start paragraph type.
+ # NOTE do not use paragraph starts because they reset indentation.
+ # ``.sp`` is only vertical space
+ self.ensure_eol()
+ if not self.first_child(node):
+ self.body.append('.sp\n')
+ # set in literal to escape dots after a new-line-character
+ self._in_literal = True
+
+ def depart_paragraph(self, node):
+ self._in_literal = False
+ self.body.append('\n')
+
+ def visit_problematic(self, node):
+ self.body.append(self.defs['problematic'][0])
+
+ def depart_problematic(self, node):
+ self.body.append(self.defs['problematic'][1])
+
+ def visit_raw(self, node):
+ if 'manpage' in node.get('format', '').split():
+ self.body.append(node.astext() + "\n")
+ # Keep non-manpage raw text out of output:
+ raise nodes.SkipNode
+
+ def visit_reference(self, node):
+ """E.g. link or email address."""
+ # .UR and .UE macros in roff use OSC8 escape sequences
+ # which are not supported everywhere yet
+ # therefore make the markup ourself
+ if 'refuri' in node:
+ # if content has the "email" do not output "mailto:email"
+ if node['refuri'].endswith(node.astext()):
+ self.body.append(" <")
+ #TODO elif 'refid' in node:
+
+ def depart_reference(self, node):
+ if 'refuri' in node:
+ # if content has the "email" do not output "mailto:email"
+ if node['refuri'].endswith(node.astext()):
+ self.body.append("> ")
+ else:
+ self.body.append(" <%s>\n" % node['refuri'])
+ #TODO elif 'refid' in node:
+
+ def visit_revision(self, node):
+ self.visit_docinfo_item(node, 'revision')
+
+ depart_revision = depart_docinfo_item
+
+ def visit_row(self, node):
+ self._active_table.new_row()
+
+ def depart_row(self, node):
+ pass
+
+ def visit_section(self, node):
+ self.section_level += 1
+
+ def depart_section(self, node):
+ self.section_level -= 1
+
+ def visit_status(self, node):
+ self.visit_docinfo_item(node, 'status')
+
+ depart_status = depart_docinfo_item
+
+ def visit_strong(self, node):
+ self.body.append(self.defs['strong'][0])
+
+ def depart_strong(self, node):
+ self.body.append(self.defs['strong'][1])
+
+ def visit_substitution_definition(self, node):
+ """Internal only."""
+ raise nodes.SkipNode
+
+ def visit_substitution_reference(self, node):
+ self.document.reporter.warning(
+ '"substitution_reference" not supported', base_node=node)
+
+ def visit_subtitle(self, node):
+ if isinstance(node.parent, nodes.sidebar):
+ self.body.append(self.defs['strong'][0])
+ elif isinstance(node.parent, nodes.document):
+ self.visit_docinfo_item(node, 'subtitle')
+ elif isinstance(node.parent, nodes.section):
+ self.body.append(self.defs['strong'][0])
+
+ def depart_subtitle(self, node):
+ # document subtitle calls SkipNode
+ self.body.append(self.defs['strong'][1]+'\n.PP\n')
+
+ def visit_system_message(self, node):
+ # TODO add report_level
+ # if node['level'] < self.document.reporter['writer'].report_level:
+ # Level is too low to display:
+ # raise nodes.SkipNode
+ attr = {}
+ if node.hasattr('id'):
+ attr['name'] = node['id']
+ if node.hasattr('line'):
+ line = ', line %s' % node['line']
+ else:
+ line = ''
+ self.body.append('.IP "System Message: %s/%s (%s:%s)"\n'
+ % (node['type'], node['level'], node['source'], line))
+
+ def depart_system_message(self, node):
+ pass
+
+ def visit_table(self, node):
+ self._active_table = Table()
+ if not self._has_a_table:
+ self._has_a_table = True
+ # the comment to hint that preprocessor tbl should be called
+ self.head.insert(0, "'\\\" t\n") # ``'\" t`` + newline
+
+ def depart_table(self, node):
+ self.ensure_eol()
+ self.body.extend(self._active_table.as_list())
+ self._active_table = None
+
+ def visit_target(self, node):
+ # targets are in-document hyper targets, without any use for man-pages.
+ raise nodes.SkipNode
+
+ def visit_tbody(self, node):
+ pass
+
+ def depart_tbody(self, node):
+ pass
+
+ def visit_term(self, node):
+ self.body.append('\n.B ')
+
+ def depart_term(self, node):
+ _next = node.next_node(None, descend=False, siblings=True)
+ # Nest (optional) classifier(s) in the <term> element
+ if isinstance(_next, nodes.classifier):
+ self.body.append(' ')
+ return # skip (depart_classifier() calls this function again)
+ if isinstance(_next, nodes.term):
+ self.body.append('\n.TQ')
+ else:
+ self.body.append('\n')
+
+ def visit_tgroup(self, node):
+ pass
+
+ def depart_tgroup(self, node):
+ pass
+
+ def visit_thead(self, node):
+ # MAYBE double line '='
+ pass
+
+ def depart_thead(self, node):
+ # MAYBE double line '='
+ pass
+
+ def visit_tip(self, node):
+ self.visit_admonition(node, 'tip')
+
+ depart_tip = depart_admonition
+
+ def visit_title(self, node):
+ if isinstance(node.parent, nodes.topic):
+ self.body.append(self.defs['topic-title'][0])
+ elif isinstance(node.parent, nodes.sidebar):
+ self.body.append(self.defs['sidebar-title'][0])
+ elif isinstance(node.parent, nodes.admonition):
+ self.body.append('.IP "')
+ elif self.section_level == 0:
+ self._docinfo['title'] = node.astext()
+ # document title for .TH
+ self._docinfo['title_upper'] = node.astext().upper()
+ raise nodes.SkipNode
+ elif self.section_level == 1:
+ self.body.append('.SH %s\n'%self.deunicode(node.astext().upper()))
+ raise nodes.SkipNode
+ else:
+ self.body.append('.SS ')
+
+ def depart_title(self, node):
+ if isinstance(node.parent, nodes.admonition):
+ self.body.append('"')
+ self.body.append('\n')
+
+ def visit_title_reference(self, node):
+ """inline citation reference"""
+ self.body.append(self.defs['title_reference'][0])
+
+ def depart_title_reference(self, node):
+ self.body.append(self.defs['title_reference'][1])
+
+ def visit_topic(self, node):
+ pass
+
+ def depart_topic(self, node):
+ pass
+
+ def visit_sidebar(self, node):
+ pass
+
+ def depart_sidebar(self, node):
+ pass
+
+ def visit_rubric(self, node):
+ pass
+
+ def depart_rubric(self, node):
+ self.body.append('\n')
+
+ def visit_transition(self, node):
+ # .PP Begin a new paragraph and reset prevailing indent.
+ # .sp N leaves N lines of blank space.
+ # .ce centers the next line
+ self.body.append('\n.sp\n.ce\n----\n')
+
+ def depart_transition(self, node):
+ self.body.append('\n.ce 0\n.sp\n')
+
+ def visit_version(self, node):
+ self.visit_docinfo_item(node, 'version')
+
+ def visit_warning(self, node):
+ self.visit_admonition(node, 'warning')
+
+ depart_warning = depart_admonition
+
+ def unimplemented_visit(self, node):
+ raise NotImplementedError('visiting unimplemented node type: %s'
+ % node.__class__.__name__)
+
+# vim: set fileencoding=utf-8 et ts=4 ai :
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/null.py b/.venv/lib/python3.12/site-packages/docutils/writers/null.py
new file mode 100644
index 00000000..db4c6720
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/null.py
@@ -0,0 +1,25 @@
+# $Id: null.py 9352 2023-04-17 20:26:41Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+A do-nothing Writer.
+
+`self.output` will change from ``None`` to the empty string
+in Docutils 0.22.
+"""
+
+from docutils import writers
+
+
+class Writer(writers.UnfilteredWriter):
+
+ supported = ('null',)
+ """Formats this writer supports."""
+
+ config_section = 'null writer'
+ config_section_dependencies = ('writers',)
+
+ def translate(self):
+ # output = None # TODO in 0.22
+ pass
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py
new file mode 100644
index 00000000..c538af34
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/__init__.py
@@ -0,0 +1,3461 @@
+# $Id: __init__.py 9541 2024-02-17 10:37:13Z milde $
+# Author: Dave Kuhlman <dkuhlman@davekuhlman.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Open Document Format (ODF) Writer.
+
+This module is provisional:
+the API is not settled and may change with any minor Docutils version.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from configparser import ConfigParser
+import copy
+from io import StringIO
+import itertools
+import locale
+import os
+import os.path
+from pathlib import Path
+import re
+import subprocess
+import tempfile
+import time
+import urllib
+import weakref
+from xml.etree import ElementTree as etree
+from xml.dom import minidom
+import zipfile
+
+import docutils
+from docutils import frontend, nodes, utils, writers, languages
+from docutils.parsers.rst.directives.images import PIL # optional
+from docutils.readers import standalone
+from docutils.transforms import references
+
+# Import pygments and odtwriter pygments formatters if possible.
+try:
+ import pygments
+ import pygments.lexers
+ from .pygmentsformatter import (OdtPygmentsProgFormatter,
+ OdtPygmentsLaTeXFormatter)
+except (ImportError, SyntaxError):
+ pygments = None
+
+# import warnings
+# warnings.warn('importing IPShellEmbed', UserWarning)
+# from IPython.Shell import IPShellEmbed
+# args = ['-pdb', '-pi1', 'In <\\#>: ', '-pi2', ' .\\D.: ',
+# '-po', 'Out<\\#>: ', '-nosep']
+# ipshell = IPShellEmbed(args,
+# banner = 'Entering IPython. Press Ctrl-D to exit.',
+# exit_msg = 'Leaving Interpreter, back to program.')
+
+VERSION = '1.0a'
+
+IMAGE_NAME_COUNTER = itertools.count()
+
+
+#
+# ElementTree does not support getparent method (lxml does).
+# This wrapper class and the following support functions provide
+# that support for the ability to get the parent of an element.
+#
+_parents = weakref.WeakKeyDictionary()
+if isinstance(etree.Element, type):
+ _ElementInterface = etree.Element
+else:
+ _ElementInterface = etree._ElementInterface
+
+
+class _ElementInterfaceWrapper(_ElementInterface):
+ def __init__(self, tag, attrib=None):
+ _ElementInterface.__init__(self, tag, attrib)
+ _parents[self] = None
+
+ def setparent(self, parent):
+ _parents[self] = parent
+
+ def getparent(self):
+ return _parents[self]
+
+
+#
+# Constants and globals
+
+SPACES_PATTERN = re.compile(r'( +)')
+TABS_PATTERN = re.compile(r'(\t+)')
+FILL_PAT1 = re.compile(r'^ +')
+FILL_PAT2 = re.compile(r' {2,}')
+
+TABLESTYLEPREFIX = 'rststyle-table-'
+TABLENAMEDEFAULT = '%s0' % TABLESTYLEPREFIX
+TABLEPROPERTYNAMES = (
+ 'border', 'border-top', 'border-left',
+ 'border-right', 'border-bottom', )
+
+GENERATOR_DESC = 'Docutils.org/odf_odt'
+
+NAME_SPACE_1 = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'
+
+CONTENT_NAMESPACE_DICT = CNSD = {
+ # 'office:version': '1.0',
+ 'chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
+ 'dc': 'http://purl.org/dc/elements/1.1/',
+ 'dom': 'http://www.w3.org/2001/xml-events',
+ 'dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
+ 'draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
+ 'fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
+ 'form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
+ 'math': 'http://www.w3.org/1998/Math/MathML',
+ 'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+ 'number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
+ 'office': NAME_SPACE_1,
+ 'ooo': 'http://openoffice.org/2004/office',
+ 'oooc': 'http://openoffice.org/2004/calc',
+ 'ooow': 'http://openoffice.org/2004/writer',
+ 'presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
+
+ 'script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
+ 'style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
+ 'svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
+ 'table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
+ 'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
+ 'xforms': 'http://www.w3.org/2002/xforms',
+ 'xlink': 'http://www.w3.org/1999/xlink',
+ 'xsd': 'http://www.w3.org/2001/XMLSchema',
+ 'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
+}
+
+STYLES_NAMESPACE_DICT = SNSD = {
+ # 'office:version': '1.0',
+ 'chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
+ 'dc': 'http://purl.org/dc/elements/1.1/',
+ 'dom': 'http://www.w3.org/2001/xml-events',
+ 'dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
+ 'draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
+ 'fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
+ 'form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
+ 'math': 'http://www.w3.org/1998/Math/MathML',
+ 'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+ 'number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
+ 'office': NAME_SPACE_1,
+ 'presentation': 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
+ 'ooo': 'http://openoffice.org/2004/office',
+ 'oooc': 'http://openoffice.org/2004/calc',
+ 'ooow': 'http://openoffice.org/2004/writer',
+ 'script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
+ 'style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
+ 'svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
+ 'table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
+ 'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
+ 'xlink': 'http://www.w3.org/1999/xlink',
+}
+
+MANIFEST_NAMESPACE_DICT = MANNSD = {
+ 'manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
+}
+
+META_NAMESPACE_DICT = METNSD = {
+ # 'office:version': '1.0',
+ 'dc': 'http://purl.org/dc/elements/1.1/',
+ 'meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+ 'office': NAME_SPACE_1,
+ 'ooo': 'http://openoffice.org/2004/office',
+ 'xlink': 'http://www.w3.org/1999/xlink',
+}
+
+# Attribute dictionaries for use with ElementTree, which
+# does not support use of nsmap parameter on Element() and SubElement().
+
+CONTENT_NAMESPACE_ATTRIB = {
+ # 'office:version': '1.0',
+ 'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
+ 'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
+ 'xmlns:dom': 'http://www.w3.org/2001/xml-events',
+ 'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
+ 'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
+ 'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
+ 'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
+ 'xmlns:math': 'http://www.w3.org/1998/Math/MathML',
+ 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+ 'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
+ 'xmlns:office': NAME_SPACE_1,
+ 'xmlns:presentation':
+ 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
+ 'xmlns:ooo': 'http://openoffice.org/2004/office',
+ 'xmlns:oooc': 'http://openoffice.org/2004/calc',
+ 'xmlns:ooow': 'http://openoffice.org/2004/writer',
+ 'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
+ 'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
+ 'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
+ 'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
+ 'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
+ 'xmlns:xforms': 'http://www.w3.org/2002/xforms',
+ 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
+ 'xmlns:xsd': 'http://www.w3.org/2001/XMLSchema',
+ 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
+}
+
+STYLES_NAMESPACE_ATTRIB = {
+ # 'office:version': '1.0',
+ 'xmlns:chart': 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
+ 'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
+ 'xmlns:dom': 'http://www.w3.org/2001/xml-events',
+ 'xmlns:dr3d': 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
+ 'xmlns:draw': 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
+ 'xmlns:fo': 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
+ 'xmlns:form': 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
+ 'xmlns:math': 'http://www.w3.org/1998/Math/MathML',
+ 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+ 'xmlns:number': 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0',
+ 'xmlns:office': NAME_SPACE_1,
+ 'xmlns:presentation':
+ 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
+ 'xmlns:ooo': 'http://openoffice.org/2004/office',
+ 'xmlns:oooc': 'http://openoffice.org/2004/calc',
+ 'xmlns:ooow': 'http://openoffice.org/2004/writer',
+ 'xmlns:script': 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
+ 'xmlns:style': 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
+ 'xmlns:svg': 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
+ 'xmlns:table': 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
+ 'xmlns:text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
+ 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
+}
+
+MANIFEST_NAMESPACE_ATTRIB = {
+ 'xmlns:manifest': 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
+}
+
+META_NAMESPACE_ATTRIB = {
+ # 'office:version': '1.0',
+ 'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
+ 'xmlns:meta': 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
+ 'xmlns:office': NAME_SPACE_1,
+ 'xmlns:ooo': 'http://openoffice.org/2004/office',
+ 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
+}
+
+
+#
+# Functions
+#
+
+#
+# ElementTree support functions.
+# In order to be able to get the parent of elements, must use these
+# instead of the functions with same name provided by ElementTree.
+#
+def Element(tag, attrib=None, nsmap=None, nsdict=CNSD):
+ if attrib is None:
+ attrib = {}
+ tag, attrib = fix_ns(tag, attrib, nsdict)
+ return _ElementInterfaceWrapper(tag, attrib)
+
+
+def SubElement(parent, tag, attrib=None, nsmap=None, nsdict=CNSD):
+ if attrib is None:
+ attrib = {}
+ tag, attrib = fix_ns(tag, attrib, nsdict)
+ el = _ElementInterfaceWrapper(tag, attrib)
+ parent.append(el)
+ el.setparent(parent)
+ return el
+
+
+def fix_ns(tag, attrib, nsdict):
+ nstag = add_ns(tag, nsdict)
+ nsattrib = {}
+ for key, val in list(attrib.items()):
+ nskey = add_ns(key, nsdict)
+ nsattrib[nskey] = val
+ return nstag, nsattrib
+
+
+def add_ns(tag, nsdict=CNSD):
+ return tag
+
+
+def ToString(et):
+ outstream = StringIO()
+ et.write(outstream, encoding="unicode")
+ s1 = outstream.getvalue()
+ outstream.close()
+ return s1
+
+
+def escape_cdata(text):
+ text = text.replace("&", "&amp;")
+ text = text.replace("<", "&lt;")
+ text = text.replace(">", "&gt;")
+ ascii = ''
+ for char in text:
+ if ord(char) >= ord("\x7f"):
+ ascii += "&#x%X;" % (ord(char), )
+ else:
+ ascii += char
+ return ascii
+
+
+#
+# Classes
+#
+
+
+class TableStyle:
+ def __init__(self, border=None, backgroundcolor=None):
+ self.border = border
+ self.backgroundcolor = backgroundcolor
+
+ def get_border_(self):
+ return self.border_
+
+ def set_border_(self, border):
+ self.border_ = border
+
+ border = property(get_border_, set_border_)
+
+ def get_backgroundcolor_(self):
+ return self.backgroundcolor_
+
+ def set_backgroundcolor_(self, backgroundcolor):
+ self.backgroundcolor_ = backgroundcolor
+ backgroundcolor = property(get_backgroundcolor_, set_backgroundcolor_)
+
+
+BUILTIN_DEFAULT_TABLE_STYLE = TableStyle(
+ border='0.0007in solid #000000')
+
+
+#
+# Information about the indentation level for lists nested inside
+# other contexts, e.g. dictionary lists.
+class ListLevel:
+ def __init__(self, level, sibling_level=True, nested_level=True):
+ self.level = level
+ self.sibling_level = sibling_level
+ self.nested_level = nested_level
+
+ def set_sibling(self, sibling_level):
+ self.sibling_level = sibling_level
+
+ def get_sibling(self):
+ return self.sibling_level
+
+ def set_nested(self, nested_level):
+ self.nested_level = nested_level
+
+ def get_nested(self):
+ return self.nested_level
+
+ def set_level(self, level):
+ self.level = level
+
+ def get_level(self):
+ return self.level
+
+
+class Writer(writers.Writer):
+
+ MIME_TYPE = 'application/vnd.oasis.opendocument.text'
+ EXTENSION = '.odt'
+
+ supported = ('odt', )
+ """Formats this writer supports."""
+
+ default_stylesheet = 'styles' + EXTENSION
+
+ default_stylesheet_path = utils.relative_path(
+ os.path.join(os.getcwd(), 'dummy'),
+ os.path.join(os.path.dirname(__file__), default_stylesheet))
+
+ default_template = 'template.txt'
+
+ default_template_path = utils.relative_path(
+ os.path.join(os.getcwd(), 'dummy'),
+ os.path.join(os.path.dirname(__file__), default_template))
+
+ settings_spec = (
+ 'ODF-Specific Options.',
+ None,
+ (
+ ('Specify a stylesheet. '
+ 'Default: "%s"' % default_stylesheet_path,
+ ['--stylesheet'],
+ {
+ 'default': default_stylesheet_path,
+ 'dest': 'stylesheet'
+ }),
+ ('Specify an ODF-specific configuration/mapping file '
+ 'relative to the current working directory.',
+ ['--odf-config-file'],
+ {'metavar': '<file>'}),
+ ('Obfuscate email addresses to confuse harvesters.',
+ ['--cloak-email-addresses'],
+ {'default': False,
+ 'action': 'store_true',
+ 'dest': 'cloak_email_addresses',
+ 'validator': frontend.validate_boolean}),
+ ('Do not obfuscate email addresses.',
+ ['--no-cloak-email-addresses'],
+ {'default': False,
+ 'action': 'store_false',
+ 'dest': 'cloak_email_addresses',
+ 'validator': frontend.validate_boolean}),
+ ('Specify the thickness of table borders in thousands of a cm. '
+ 'Default is 35.',
+ ['--table-border-thickness'],
+ {'default': None,
+ 'metavar': '<int>',
+ 'validator': frontend.validate_nonnegative_int}),
+ ('Add syntax highlighting in literal code blocks.',
+ ['--add-syntax-highlighting'],
+ {'default': False,
+ 'action': 'store_true',
+ 'dest': 'add_syntax_highlighting',
+ 'validator': frontend.validate_boolean}),
+ ('Do not add syntax highlighting in '
+ 'literal code blocks. (default)',
+ ['--no-syntax-highlighting'],
+ {'default': False,
+ 'action': 'store_false',
+ 'dest': 'add_syntax_highlighting',
+ 'validator': frontend.validate_boolean}),
+ ('Create sections for headers. (default)',
+ ['--create-sections'],
+ {'default': True,
+ 'action': 'store_true',
+ 'dest': 'create_sections',
+ 'validator': frontend.validate_boolean}),
+ ('Do not create sections for headers.',
+ ['--no-sections'],
+ {'default': True,
+ 'action': 'store_false',
+ 'dest': 'create_sections',
+ 'validator': frontend.validate_boolean}),
+ ('Create links.',
+ ['--create-links'],
+ {'default': False,
+ 'action': 'store_true',
+ 'dest': 'create_links',
+ 'validator': frontend.validate_boolean}),
+ ('Do not create links. (default)',
+ ['--no-links'],
+ {'default': False,
+ 'action': 'store_false',
+ 'dest': 'create_links',
+ 'validator': frontend.validate_boolean}),
+ ('Generate endnotes at end of document, not footnotes '
+ 'at bottom of page.',
+ ['--endnotes-end-doc'],
+ {'default': False,
+ 'action': 'store_true',
+ 'dest': 'endnotes_end_doc',
+ 'validator': frontend.validate_boolean}),
+ ('Generate footnotes at bottom of page, not endnotes '
+ 'at end of document. (default)',
+ ['--no-endnotes-end-doc'],
+ {'default': False,
+ 'action': 'store_false',
+ 'dest': 'endnotes_end_doc',
+ 'validator': frontend.validate_boolean}),
+ ('Generate a bullet list table of contents, '
+ 'not a native ODF table of contents.',
+ ['--generate-list-toc'],
+ {'action': 'store_false',
+ 'dest': 'generate_oowriter_toc',
+ 'validator': frontend.validate_boolean}),
+ ('Generate a native ODF table of contents, '
+ 'not a bullet list. (default)',
+ ['--generate-oowriter-toc'],
+ {'default': True,
+ 'action': 'store_true',
+ 'dest': 'generate_oowriter_toc',
+ 'validator': frontend.validate_boolean}),
+ ('Specify the contents of an custom header line. '
+ 'See ODF/ODT writer documentation for details '
+ 'about special field character sequences.',
+ ['--custom-odt-header'],
+ {'default': '',
+ 'dest': 'custom_header',
+ 'metavar': '<custom header>'}),
+ ('Specify the contents of an custom footer line. '
+ 'See ODF/ODT writer documentation for details.',
+ ['--custom-odt-footer'],
+ {'default': '',
+ 'dest': 'custom_footer',
+ 'metavar': '<custom footer>'}),
+ )
+ )
+
+ settings_defaults = {
+ 'output_encoding_error_handler': 'xmlcharrefreplace',
+ }
+
+ relative_path_settings = ('odf_config_file', 'stylesheet',)
+
+ config_section = 'odf_odt writer'
+ config_section_dependencies = ('writers',)
+
+ def __init__(self):
+ writers.Writer.__init__(self)
+ self.translator_class = ODFTranslator
+
+ def translate(self):
+ self.settings = self.document.settings
+ self.visitor = self.translator_class(self.document)
+ self.visitor.retrieve_styles(self.EXTENSION)
+ self.document.walkabout(self.visitor)
+ self.visitor.add_doc_title()
+ self.assemble_my_parts()
+ self.output = self.parts['whole']
+
+ def assemble_my_parts(self):
+ """Assemble the `self.parts` dictionary. Extend in subclasses.
+ """
+ writers.Writer.assemble_parts(self)
+ f = tempfile.NamedTemporaryFile()
+ zfile = zipfile.ZipFile(f, 'w', zipfile.ZIP_DEFLATED)
+ self.write_zip_str(
+ zfile, 'mimetype', self.MIME_TYPE,
+ compress_type=zipfile.ZIP_STORED)
+ content = self.visitor.content_astext()
+ self.write_zip_str(zfile, 'content.xml', content)
+ s1 = self.create_manifest()
+ self.write_zip_str(zfile, 'META-INF/manifest.xml', s1)
+ s1 = self.create_meta()
+ self.write_zip_str(zfile, 'meta.xml', s1)
+ s1 = self.get_stylesheet()
+ # Set default language in document to be generated.
+ # Language is specified by the -l/--language command line option.
+ # The format is described in BCP 47. If region is omitted, we use
+ # local.normalize(ll) to obtain a region.
+ language_code = None
+ region_code = None
+ if self.visitor.language_code:
+ language_ids = self.visitor.language_code.replace('_', '-')
+ language_ids = language_ids.split('-')
+ # first tag is primary language tag
+ language_code = language_ids[0].lower()
+ # 2-letter region subtag may follow in 2nd or 3rd position
+ for subtag in language_ids[1:]:
+ if len(subtag) == 2 and subtag.isalpha():
+ region_code = subtag.upper()
+ break
+ elif len(subtag) == 1:
+ break # 1-letter tag is never before valid region tag
+ if region_code is None:
+ try:
+ rcode = locale.normalize(language_code)
+ except NameError:
+ rcode = language_code
+ rcode = rcode.split('_')
+ if len(rcode) > 1:
+ rcode = rcode[1].split('.')
+ region_code = rcode[0]
+ if region_code is None:
+ self.document.reporter.warning(
+ 'invalid language-region.\n'
+ ' Could not find region with locale.normalize().\n'
+ ' Please specify both language and region (ll-RR).\n'
+ ' Examples: es-MX (Spanish, Mexico),\n'
+ ' en-AU (English, Australia).')
+ # Update the style ElementTree with the language and region.
+ # Note that we keep a reference to the modified node because
+ # it is possible that ElementTree will throw away the Python
+ # representation of the updated node if we do not.
+ updated, new_dom_styles, updated_node = self.update_stylesheet(
+ self.visitor.get_dom_stylesheet(), language_code, region_code)
+ if updated:
+ s1 = etree.tostring(new_dom_styles)
+ self.write_zip_str(zfile, 'styles.xml', s1)
+ self.store_embedded_files(zfile)
+ self.copy_from_stylesheet(zfile)
+ zfile.close()
+ f.seek(0)
+ whole = f.read()
+ f.close()
+ self.parts['whole'] = whole
+ self.parts['encoding'] = self.document.settings.output_encoding
+ self.parts['version'] = docutils.__version__
+
+ def update_stylesheet(self, stylesheet_root, language_code, region_code):
+ """Update xml style sheet element with language and region/country."""
+ updated = False
+ modified_nodes = set()
+ if language_code is not None or region_code is not None:
+ n1 = stylesheet_root.find(
+ '{urn:oasis:names:tc:opendocument:xmlns:office:1.0}'
+ 'styles')
+ if n1 is None:
+ raise RuntimeError(
+ "Cannot find 'styles' element in styles.odt/styles.xml")
+ n2_nodes = n1.findall(
+ '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
+ 'default-style')
+ if not n2_nodes:
+ raise RuntimeError(
+ "Cannot find 'default-style' "
+ "element in styles.xml")
+ for node in n2_nodes:
+ family = node.attrib.get(
+ '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
+ 'family')
+ if family == 'paragraph' or family == 'graphic':
+ n3 = node.find(
+ '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
+ 'text-properties')
+ if n3 is None:
+ raise RuntimeError(
+ "Cannot find 'text-properties' "
+ "element in styles.xml")
+ if language_code is not None:
+ n3.attrib[
+ '{urn:oasis:names:tc:opendocument:xmlns:'
+ 'xsl-fo-compatible:1.0}language'] = language_code
+ n3.attrib[
+ '{urn:oasis:names:tc:opendocument:xmlns:'
+ 'style:1.0}language-complex'] = language_code
+ updated = True
+ modified_nodes.add(n3)
+ if region_code is not None:
+ n3.attrib[
+ '{urn:oasis:names:tc:opendocument:xmlns:'
+ 'xsl-fo-compatible:1.0}country'] = region_code
+ n3.attrib[
+ '{urn:oasis:names:tc:opendocument:xmlns:'
+ 'style:1.0}country-complex'] = region_code
+ updated = True
+ modified_nodes.add(n3)
+ return updated, stylesheet_root, modified_nodes
+
+ def write_zip_str(
+ self, zfile, name, bytes, compress_type=zipfile.ZIP_DEFLATED):
+ localtime = time.localtime(time.time())
+ zinfo = zipfile.ZipInfo(name, localtime)
+ # Add some standard UNIX file access permissions (-rw-r--r--).
+ zinfo.external_attr = (0x81a4 & 0xFFFF) << 16
+ zinfo.compress_type = compress_type
+ zfile.writestr(zinfo, bytes)
+
+ def store_embedded_files(self, zfile):
+ embedded_files = self.visitor.get_embedded_file_list()
+ for source, destination in embedded_files:
+ if source is None:
+ continue
+ try:
+ zfile.write(source, destination)
+ except OSError:
+ self.document.reporter.warning(
+ "Can't open file %s." % (source, ))
+
+ def get_settings(self):
+ """
+ modeled after get_stylesheet
+ """
+ stylespath = self.settings.stylesheet
+ zfile = zipfile.ZipFile(stylespath, 'r')
+ s1 = zfile.read('settings.xml')
+ zfile.close()
+ return s1
+
+ def get_stylesheet(self):
+ """Get the stylesheet from the visitor.
+ Ask the visitor to setup the page.
+ """
+ return self.visitor.setup_page()
+
+ def copy_from_stylesheet(self, outzipfile):
+ """Copy images, settings, etc from the stylesheet doc into target doc.
+ """
+ stylespath = self.settings.stylesheet
+ inzipfile = zipfile.ZipFile(stylespath, 'r')
+ # Copy the styles.
+ s1 = inzipfile.read('settings.xml')
+ self.write_zip_str(outzipfile, 'settings.xml', s1)
+ # Copy the images.
+ namelist = inzipfile.namelist()
+ for name in namelist:
+ if name.startswith('Pictures/'):
+ imageobj = inzipfile.read(name)
+ outzipfile.writestr(name, imageobj)
+ inzipfile.close()
+
+ def assemble_parts(self):
+ pass
+
+ def create_manifest(self):
+ root = Element(
+ 'manifest:manifest',
+ attrib=MANIFEST_NAMESPACE_ATTRIB,
+ nsdict=MANIFEST_NAMESPACE_DICT,
+ )
+ doc = etree.ElementTree(root)
+ SubElement(root, 'manifest:file-entry', attrib={
+ 'manifest:media-type': self.MIME_TYPE,
+ 'manifest:full-path': '/',
+ }, nsdict=MANNSD)
+ SubElement(root, 'manifest:file-entry', attrib={
+ 'manifest:media-type': 'text/xml',
+ 'manifest:full-path': 'content.xml',
+ }, nsdict=MANNSD)
+ SubElement(root, 'manifest:file-entry', attrib={
+ 'manifest:media-type': 'text/xml',
+ 'manifest:full-path': 'styles.xml',
+ }, nsdict=MANNSD)
+ SubElement(root, 'manifest:file-entry', attrib={
+ 'manifest:media-type': 'text/xml',
+ 'manifest:full-path': 'settings.xml',
+ }, nsdict=MANNSD)
+ SubElement(root, 'manifest:file-entry', attrib={
+ 'manifest:media-type': 'text/xml',
+ 'manifest:full-path': 'meta.xml',
+ }, nsdict=MANNSD)
+ s1 = ToString(doc)
+ doc = minidom.parseString(s1)
+ return doc.toprettyxml(' ')
+
+ def create_meta(self):
+ root = Element(
+ 'office:document-meta',
+ attrib=META_NAMESPACE_ATTRIB,
+ nsdict=META_NAMESPACE_DICT,
+ )
+ doc = etree.ElementTree(root)
+ root = SubElement(root, 'office:meta', nsdict=METNSD)
+ el1 = SubElement(root, 'meta:generator', nsdict=METNSD)
+ el1.text = 'Docutils/rst2odf.py/%s' % (VERSION, )
+ s1 = os.environ.get('USER', '')
+ el1 = SubElement(root, 'meta:initial-creator', nsdict=METNSD)
+ el1.text = s1
+ s2 = time.strftime('%Y-%m-%dT%H:%M:%S', time.localtime())
+ el1 = SubElement(root, 'meta:creation-date', nsdict=METNSD)
+ el1.text = s2
+ el1 = SubElement(root, 'dc:creator', nsdict=METNSD)
+ el1.text = s1
+ el1 = SubElement(root, 'dc:date', nsdict=METNSD)
+ el1.text = s2
+ el1 = SubElement(root, 'dc:language', nsdict=METNSD)
+ el1.text = 'en-US'
+ el1 = SubElement(root, 'meta:editing-cycles', nsdict=METNSD)
+ el1.text = '1'
+ el1 = SubElement(root, 'meta:editing-duration', nsdict=METNSD)
+ el1.text = 'PT00M01S'
+ title = self.visitor.get_title()
+ el1 = SubElement(root, 'dc:title', nsdict=METNSD)
+ if title:
+ el1.text = title
+ else:
+ el1.text = '[no title]'
+ for prop, value in self.visitor.get_meta_dict().items():
+ # 'keywords', 'description', and 'subject' have their own fields:
+ if prop == 'keywords':
+ keywords = re.split(', *', value)
+ for keyword in keywords:
+ el1 = SubElement(root, 'meta:keyword', nsdict=METNSD)
+ el1.text = keyword
+ elif prop == 'description':
+ el1 = SubElement(root, 'dc:description', nsdict=METNSD)
+ el1.text = value
+ elif prop == 'subject':
+ el1 = SubElement(root, 'dc:subject', nsdict=METNSD)
+ el1.text = value
+ else: # Store remaining properties as custom/user-defined
+ el1 = SubElement(root, 'meta:user-defined',
+ attrib={'meta:name': prop}, nsdict=METNSD)
+ el1.text = value
+ s1 = ToString(doc)
+ # doc = minidom.parseString(s1)
+ # s1 = doc.toprettyxml(' ')
+ return s1
+
+
+# class ODFTranslator(nodes.SparseNodeVisitor):
+class ODFTranslator(nodes.GenericNodeVisitor):
+
+ used_styles = (
+ 'attribution', 'blockindent', 'blockquote', 'blockquote-bulletitem',
+ 'blockquote-bulletlist', 'blockquote-enumitem', 'blockquote-enumlist',
+ 'bulletitem', 'bulletlist',
+ 'caption', 'legend',
+ 'centeredtextbody', 'codeblock', 'codeblock-indented',
+ 'codeblock-classname', 'codeblock-comment', 'codeblock-functionname',
+ 'codeblock-keyword', 'codeblock-name', 'codeblock-number',
+ 'codeblock-operator', 'codeblock-string', 'emphasis', 'enumitem',
+ 'enumlist', 'epigraph', 'epigraph-bulletitem', 'epigraph-bulletlist',
+ 'epigraph-enumitem', 'epigraph-enumlist', 'footer',
+ 'footnote', 'citation',
+ 'header', 'highlights', 'highlights-bulletitem',
+ 'highlights-bulletlist', 'highlights-enumitem', 'highlights-enumlist',
+ 'horizontalline', 'inlineliteral', 'quotation', 'rubric',
+ 'strong', 'table-title', 'textbody', 'tocbulletlist', 'tocenumlist',
+ 'title',
+ 'subtitle',
+ 'heading1',
+ 'heading2',
+ 'heading3',
+ 'heading4',
+ 'heading5',
+ 'heading6',
+ 'heading7',
+ 'admon-attention-hdr',
+ 'admon-attention-body',
+ 'admon-caution-hdr',
+ 'admon-caution-body',
+ 'admon-danger-hdr',
+ 'admon-danger-body',
+ 'admon-error-hdr',
+ 'admon-error-body',
+ 'admon-generic-hdr',
+ 'admon-generic-body',
+ 'admon-hint-hdr',
+ 'admon-hint-body',
+ 'admon-important-hdr',
+ 'admon-important-body',
+ 'admon-note-hdr',
+ 'admon-note-body',
+ 'admon-tip-hdr',
+ 'admon-tip-body',
+ 'admon-warning-hdr',
+ 'admon-warning-body',
+ 'tableoption',
+ 'tableoption.%c', 'tableoption.%c%d', 'Table%d', 'Table%d.%c',
+ 'Table%d.%c%d',
+ 'lineblock1',
+ 'lineblock2',
+ 'lineblock3',
+ 'lineblock4',
+ 'lineblock5',
+ 'lineblock6',
+ 'image', 'figureframe',
+ )
+
+ def __init__(self, document):
+ # nodes.SparseNodeVisitor.__init__(self, document)
+ nodes.GenericNodeVisitor.__init__(self, document)
+ self.settings = document.settings
+ self.language_code = self.settings.language_code
+ self.language = languages.get_language(
+ self.language_code,
+ document.reporter)
+ self.format_map = {}
+ if self.settings.odf_config_file:
+ parser = ConfigParser()
+ parser.read(self.settings.odf_config_file)
+ for rststyle, format in parser.items("Formats"):
+ if rststyle not in self.used_styles:
+ self.document.reporter.warning(
+ 'Style "%s" is not a style used by odtwriter.' % (
+ rststyle, ))
+ self.format_map[rststyle] = format
+ self.section_level = 0
+ self.section_count = 0
+ # Create ElementTree content and styles documents.
+ root = Element(
+ 'office:document-content',
+ attrib=CONTENT_NAMESPACE_ATTRIB,
+ )
+ self.content_tree = etree.ElementTree(element=root)
+ self.current_element = root
+ SubElement(root, 'office:scripts')
+ SubElement(root, 'office:font-face-decls')
+ el = SubElement(root, 'office:automatic-styles')
+ self.automatic_styles = el
+ el = SubElement(root, 'office:body')
+ el = self.generate_content_element(el)
+ self.current_element = el
+ self.body_text_element = el
+ self.paragraph_style_stack = [self.rststyle('textbody'), ]
+ self.list_style_stack = []
+ self.table_count = 0
+ self.column_count = ord('A') - 1
+ self.trace_level = -1
+ self.optiontablestyles_generated = False
+ self.field_name = None
+ self.field_element = None
+ self.title = None
+ self.image_count = 0
+ self.image_style_count = 0
+ self.image_dict = {}
+ self.embedded_file_list = []
+ self.syntaxhighlighting = 1
+ self.syntaxhighlight_lexer = 'python'
+ self.header_content = []
+ self.footer_content = []
+ self.in_header = False
+ self.in_footer = False
+ self.blockstyle = ''
+ self.in_table_of_contents = False
+ self.table_of_content_index_body = None
+ self.list_level = 0
+ self.def_list_level = 0
+ self.footnote_ref_dict = {}
+ self.footnote_list = []
+ self.footnote_chars_idx = 0
+ self.footnote_level = 0
+ self.pending_ids = []
+ self.in_paragraph = False
+ self.found_doc_title = False
+ self.bumped_list_level_stack = []
+ self.meta_dict = {}
+ self.line_block_level = 0
+ self.line_indent_level = 0
+ self.citation_id = None
+ self.style_index = 0 # use to form unique style names
+ self.str_stylesheet = ''
+ self.str_stylesheetcontent = ''
+ self.dom_stylesheet = None
+ self.table_styles = None
+ self.in_citation = False
+
+ # Keep track of nested styling classes
+ self.inline_style_count_stack = []
+
+ def get_str_stylesheet(self):
+ return self.str_stylesheet
+
+ def retrieve_styles(self, extension):
+ """Retrieve the stylesheet from either a .xml file or from
+ a .odt (zip) file. Return the content as a string.
+ """
+ s2 = None
+ stylespath = self.settings.stylesheet
+ ext = os.path.splitext(stylespath)[1]
+ if ext == '.xml':
+ with open(stylespath, 'r', encoding='utf-8') as stylesfile:
+ s1 = stylesfile.read()
+ elif ext == extension:
+ zfile = zipfile.ZipFile(stylespath, 'r')
+ s1 = zfile.read('styles.xml')
+ s2 = zfile.read('content.xml')
+ zfile.close()
+ else:
+ raise RuntimeError('stylesheet path (%s) must be %s or '
+ '.xml file' % (stylespath, extension))
+ self.str_stylesheet = s1
+ self.str_stylesheetcontent = s2
+ self.dom_stylesheet = etree.fromstring(self.str_stylesheet)
+ self.dom_stylesheetcontent = etree.fromstring(
+ self.str_stylesheetcontent)
+ self.table_styles = self.extract_table_styles(s2)
+
+ def extract_table_styles(self, styles_str):
+ root = etree.fromstring(styles_str)
+ table_styles = {}
+ auto_styles = root.find(
+ '{%s}automatic-styles' % (CNSD['office'], ))
+ for stylenode in auto_styles:
+ name = stylenode.get('{%s}name' % (CNSD['style'], ))
+ tablename = name.split('.')[0]
+ family = stylenode.get('{%s}family' % (CNSD['style'], ))
+ if name.startswith(TABLESTYLEPREFIX):
+ tablestyle = table_styles.get(tablename)
+ if tablestyle is None:
+ tablestyle = TableStyle()
+ table_styles[tablename] = tablestyle
+ if family == 'table':
+ properties = stylenode.find(
+ '{%s}table-properties' % (CNSD['style'], ))
+ property = properties.get(
+ '{%s}%s' % (CNSD['fo'], 'background-color', ))
+ if property is not None and property != 'none':
+ tablestyle.backgroundcolor = property
+ elif family == 'table-cell':
+ properties = stylenode.find(
+ '{%s}table-cell-properties' % (CNSD['style'], ))
+ if properties is not None:
+ border = self.get_property(properties)
+ if border is not None:
+ tablestyle.border = border
+ return table_styles
+
+ def get_property(self, stylenode):
+ border = None
+ for propertyname in TABLEPROPERTYNAMES:
+ border = stylenode.get('{%s}%s' % (CNSD['fo'], propertyname, ))
+ if border is not None and border != 'none':
+ return border
+ return border
+
+ def add_doc_title(self):
+ text = self.settings.title
+ if text:
+ self.title = text
+ if not self.found_doc_title:
+ el = Element('text:p', attrib={
+ 'text:style-name': self.rststyle('title'),
+ })
+ el.text = text
+ self.body_text_element.insert(0, el)
+ el = self.find_first_text_p(self.body_text_element)
+ if el is not None:
+ self.attach_page_style(el)
+
+ def find_first_text_p(self, el):
+ """Search the generated doc and return the first <text:p> element.
+ """
+ if el.tag == 'text:p' or el.tag == 'text:h':
+ return el
+ else:
+ for child in el:
+ el1 = self.find_first_text_p(child)
+ if el1 is not None:
+ return el1
+ return None
+
+ def attach_page_style(self, el):
+ """Attach the default page style.
+
+ Create an automatic-style that refers to the current style
+ of this element and that refers to the default page style.
+ """
+ current_style = el.get('text:style-name')
+ style_name = 'P1003'
+ el1 = SubElement(
+ self.automatic_styles, 'style:style', attrib={
+ 'style:name': style_name,
+ 'style:master-page-name': "rststyle-pagedefault",
+ 'style:family': "paragraph",
+ }, nsdict=SNSD)
+ if current_style:
+ el1.set('style:parent-style-name', current_style)
+ el.set('text:style-name', style_name)
+
+ def rststyle(self, name, parameters=()):
+ """
+ Returns the style name to use for the given style.
+
+ If `parameters` is given `name` must contain a matching number of
+ ``%`` and is used as a format expression with `parameters` as
+ the value.
+ """
+ name1 = name % parameters
+ return self.format_map.get(name1, 'rststyle-%s' % name1)
+
+ def generate_content_element(self, root):
+ return SubElement(root, 'office:text')
+
+ def setup_page(self):
+ self.setup_paper(self.dom_stylesheet)
+ if (len(self.header_content) > 0
+ or len(self.footer_content) > 0
+ or self.settings.custom_header
+ or self.settings.custom_footer):
+ self.add_header_footer(self.dom_stylesheet)
+ return etree.tostring(self.dom_stylesheet)
+
+ def get_dom_stylesheet(self):
+ return self.dom_stylesheet
+
+ def setup_paper(self, root_el):
+ # TODO: only call paperconf, if it is actually used
+ # (i.e. page size removed from "styles.odt" with rst2odt_prepstyles.py
+ # cf. conditional in walk() below)?
+ try:
+ dimensions = subprocess.check_output(('paperconf', '-s'),
+ stderr=subprocess.STDOUT)
+ w, h = (float(s) for s in dimensions.split())
+ except (subprocess.CalledProcessError, FileNotFoundError, ValueError):
+ self.document.reporter.info(
+ 'Cannot use `paperconf`, defaulting to Letter.')
+ w, h = 612, 792 # default to Letter
+
+ def walk(el):
+ if el.tag == "{%s}page-layout-properties" % SNSD["style"] and \
+ "{%s}page-width" % SNSD["fo"] not in el.attrib:
+ el.attrib["{%s}page-width" % SNSD["fo"]] = "%.3fpt" % w
+ el.attrib["{%s}page-height" % SNSD["fo"]] = "%.3fpt" % h
+ el.attrib["{%s}margin-left" % SNSD["fo"]] = \
+ el.attrib["{%s}margin-right" % SNSD["fo"]] = \
+ "%.3fpt" % (.1 * w)
+ el.attrib["{%s}margin-top" % SNSD["fo"]] = \
+ el.attrib["{%s}margin-bottom" % SNSD["fo"]] = \
+ "%.3fpt" % (.1 * h)
+ else:
+ for subel in el:
+ walk(subel)
+ walk(root_el)
+
+ def add_header_footer(self, root_el):
+ automatic_styles = root_el.find(
+ '{%s}automatic-styles' % SNSD['office'])
+ path = '{%s}master-styles' % (NAME_SPACE_1, )
+ master_el = root_el.find(path)
+ if master_el is None:
+ return
+ path = '{%s}master-page' % (SNSD['style'], )
+ master_el_container = master_el.findall(path)
+ master_el = None
+ target_attrib = '{%s}name' % (SNSD['style'], )
+ target_name = self.rststyle('pagedefault')
+ for el in master_el_container:
+ if el.get(target_attrib) == target_name:
+ master_el = el
+ break
+ if master_el is None:
+ return
+ el1 = master_el
+ if self.header_content or self.settings.custom_header:
+ el2 = SubElement(
+ el1, 'style:header',
+ attrib=STYLES_NAMESPACE_ATTRIB,
+ nsdict=STYLES_NAMESPACE_DICT,
+ )
+ for el in self.header_content:
+ attrkey = add_ns('text:style-name', nsdict=SNSD)
+ el.attrib[attrkey] = self.rststyle('header')
+ el2.append(el)
+ if self.settings.custom_header:
+ self.create_custom_headfoot(
+ el2,
+ self.settings.custom_header, 'header', automatic_styles)
+ if self.footer_content or self.settings.custom_footer:
+ el2 = SubElement(
+ el1, 'style:footer',
+ attrib=STYLES_NAMESPACE_ATTRIB,
+ nsdict=STYLES_NAMESPACE_DICT,
+ )
+ for el in self.footer_content:
+ attrkey = add_ns('text:style-name', nsdict=SNSD)
+ el.attrib[attrkey] = self.rststyle('footer')
+ el2.append(el)
+ if self.settings.custom_footer:
+ self.create_custom_headfoot(
+ el2,
+ self.settings.custom_footer, 'footer', automatic_styles)
+
+ code_none, code_field, code_text = list(range(3))
+ field_pat = re.compile(r'%(..?)%')
+
+ def create_custom_headfoot(
+ self, parent, text, style_name, automatic_styles):
+ parent = SubElement(parent, 'text:p', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ })
+ current_element = None
+ field_iter = self.split_field_specifiers_iter(text)
+ for item in field_iter:
+ if item[0] == ODFTranslator.code_field:
+ if item[1] not in (
+ 'p', 'P',
+ 't1', 't2', 't3', 't4',
+ 'd1', 'd2', 'd3', 'd4', 'd5',
+ 's', 't', 'a'):
+ msg = 'bad field spec: %%%s%%' % (item[1], )
+ raise RuntimeError(msg)
+ el1 = self.make_field_element(
+ parent,
+ item[1], style_name, automatic_styles)
+ if el1 is None:
+ msg = 'bad field spec: %%%s%%' % (item[1], )
+ raise RuntimeError(msg)
+ else:
+ current_element = el1
+ else:
+ if current_element is None:
+ parent.text = item[1]
+ else:
+ current_element.tail = item[1]
+
+ def make_field_element(self, parent, text, style_name, automatic_styles):
+ if text == 'p':
+ el1 = SubElement(parent, 'text:page-number', attrib={
+ # 'text:style-name': self.rststyle(style_name),
+ 'text:select-page': 'current',
+ })
+ elif text == 'P':
+ el1 = SubElement(parent, 'text:page-count', attrib={
+ # 'text:style-name': self.rststyle(style_name),
+ })
+ elif text == 't1':
+ self.style_index += 1
+ el1 = SubElement(parent, 'text:time', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ 'text:fixed': 'true',
+ 'style:data-style-name':
+ 'rst-time-style-%d' % self.style_index,
+ })
+ el2 = SubElement(automatic_styles, 'number:time-style', attrib={
+ 'style:name': 'rst-time-style-%d' % self.style_index,
+ 'xmlns:number': SNSD['number'],
+ 'xmlns:style': SNSD['style'],
+ })
+ el3 = SubElement(el2, 'number:hours', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ':'
+ el3 = SubElement(el2, 'number:minutes', attrib={
+ 'number:style': 'long',
+ })
+ elif text == 't2':
+ self.style_index += 1
+ el1 = SubElement(parent, 'text:time', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ 'text:fixed': 'true',
+ 'style:data-style-name':
+ 'rst-time-style-%d' % self.style_index,
+ })
+ el2 = SubElement(automatic_styles, 'number:time-style', attrib={
+ 'style:name': 'rst-time-style-%d' % self.style_index,
+ 'xmlns:number': SNSD['number'],
+ 'xmlns:style': SNSD['style'],
+ })
+ el3 = SubElement(el2, 'number:hours', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ':'
+ el3 = SubElement(el2, 'number:minutes', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ':'
+ el3 = SubElement(el2, 'number:seconds', attrib={
+ 'number:style': 'long',
+ })
+ elif text == 't3':
+ self.style_index += 1
+ el1 = SubElement(parent, 'text:time', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ 'text:fixed': 'true',
+ 'style:data-style-name':
+ 'rst-time-style-%d' % self.style_index,
+ })
+ el2 = SubElement(automatic_styles, 'number:time-style', attrib={
+ 'style:name': 'rst-time-style-%d' % self.style_index,
+ 'xmlns:number': SNSD['number'],
+ 'xmlns:style': SNSD['style'],
+ })
+ el3 = SubElement(el2, 'number:hours', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ':'
+ el3 = SubElement(el2, 'number:minutes', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ' '
+ el3 = SubElement(el2, 'number:am-pm')
+ elif text == 't4':
+ self.style_index += 1
+ el1 = SubElement(parent, 'text:time', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ 'text:fixed': 'true',
+ 'style:data-style-name':
+ 'rst-time-style-%d' % self.style_index,
+ })
+ el2 = SubElement(automatic_styles, 'number:time-style', attrib={
+ 'style:name': 'rst-time-style-%d' % self.style_index,
+ 'xmlns:number': SNSD['number'],
+ 'xmlns:style': SNSD['style'],
+ })
+ el3 = SubElement(el2, 'number:hours', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ':'
+ el3 = SubElement(el2, 'number:minutes', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ':'
+ el3 = SubElement(el2, 'number:seconds', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ' '
+ el3 = SubElement(el2, 'number:am-pm')
+ elif text == 'd1':
+ self.style_index += 1
+ el1 = SubElement(parent, 'text:date', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ 'style:data-style-name':
+ 'rst-date-style-%d' % self.style_index,
+ })
+ el2 = SubElement(automatic_styles, 'number:date-style', attrib={
+ 'style:name': 'rst-date-style-%d' % self.style_index,
+ 'number:automatic-order': 'true',
+ 'xmlns:number': SNSD['number'],
+ 'xmlns:style': SNSD['style'],
+ })
+ el3 = SubElement(el2, 'number:month', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = '/'
+ el3 = SubElement(el2, 'number:day', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = '/'
+ el3 = SubElement(el2, 'number:year')
+ elif text == 'd2':
+ self.style_index += 1
+ el1 = SubElement(parent, 'text:date', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ 'style:data-style-name':
+ 'rst-date-style-%d' % self.style_index,
+ })
+ el2 = SubElement(automatic_styles, 'number:date-style', attrib={
+ 'style:name': 'rst-date-style-%d' % self.style_index,
+ 'number:automatic-order': 'true',
+ 'xmlns:number': SNSD['number'],
+ 'xmlns:style': SNSD['style'],
+ })
+ el3 = SubElement(el2, 'number:month', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = '/'
+ el3 = SubElement(el2, 'number:day', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = '/'
+ el3 = SubElement(el2, 'number:year', attrib={
+ 'number:style': 'long',
+ })
+ elif text == 'd3':
+ self.style_index += 1
+ el1 = SubElement(parent, 'text:date', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ 'style:data-style-name':
+ 'rst-date-style-%d' % self.style_index,
+ })
+ el2 = SubElement(automatic_styles, 'number:date-style', attrib={
+ 'style:name': 'rst-date-style-%d' % self.style_index,
+ 'number:automatic-order': 'true',
+ 'xmlns:number': SNSD['number'],
+ 'xmlns:style': SNSD['style'],
+ })
+ el3 = SubElement(el2, 'number:month', attrib={
+ 'number:textual': 'true',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ' '
+ el3 = SubElement(el2, 'number:day', attrib={})
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ', '
+ el3 = SubElement(el2, 'number:year', attrib={
+ 'number:style': 'long',
+ })
+ elif text == 'd4':
+ self.style_index += 1
+ el1 = SubElement(parent, 'text:date', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ 'style:data-style-name':
+ 'rst-date-style-%d' % self.style_index,
+ })
+ el2 = SubElement(automatic_styles, 'number:date-style', attrib={
+ 'style:name': 'rst-date-style-%d' % self.style_index,
+ 'number:automatic-order': 'true',
+ 'xmlns:number': SNSD['number'],
+ 'xmlns:style': SNSD['style'],
+ })
+ el3 = SubElement(el2, 'number:month', attrib={
+ 'number:textual': 'true',
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ' '
+ el3 = SubElement(el2, 'number:day', attrib={})
+ el3 = SubElement(el2, 'number:text')
+ el3.text = ', '
+ el3 = SubElement(el2, 'number:year', attrib={
+ 'number:style': 'long',
+ })
+ elif text == 'd5':
+ self.style_index += 1
+ el1 = SubElement(parent, 'text:date', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ 'style:data-style-name':
+ 'rst-date-style-%d' % self.style_index,
+ })
+ el2 = SubElement(automatic_styles, 'number:date-style', attrib={
+ 'style:name': 'rst-date-style-%d' % self.style_index,
+ 'xmlns:number': SNSD['number'],
+ 'xmlns:style': SNSD['style'],
+ })
+ el3 = SubElement(el2, 'number:year', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = '-'
+ el3 = SubElement(el2, 'number:month', attrib={
+ 'number:style': 'long',
+ })
+ el3 = SubElement(el2, 'number:text')
+ el3.text = '-'
+ el3 = SubElement(el2, 'number:day', attrib={
+ 'number:style': 'long',
+ })
+ elif text == 's':
+ el1 = SubElement(parent, 'text:subject', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ })
+ elif text == 't':
+ el1 = SubElement(parent, 'text:title', attrib={
+ 'text:style-name': self.rststyle(style_name),
+ })
+ elif text == 'a':
+ el1 = SubElement(parent, 'text:author-name', attrib={
+ 'text:fixed': 'false',
+ })
+ else:
+ el1 = None
+ return el1
+
+ def split_field_specifiers_iter(self, text):
+ pos1 = 0
+ while True:
+ mo = ODFTranslator.field_pat.search(text, pos1)
+ if mo:
+ pos2 = mo.start()
+ if pos2 > pos1:
+ yield ODFTranslator.code_text, text[pos1:pos2]
+ yield ODFTranslator.code_field, mo.group(1)
+ pos1 = mo.end()
+ else:
+ break
+ trailing = text[pos1:]
+ if trailing:
+ yield ODFTranslator.code_text, trailing
+
+ def astext(self):
+ root = self.content_tree.getroot()
+ et = etree.ElementTree(root)
+ return ToString(et)
+
+ def content_astext(self):
+ return self.astext()
+
+ def set_title(self, title):
+ self.title = title
+
+ def get_title(self):
+ return self.title
+
+ def set_embedded_file_list(self, embedded_file_list):
+ self.embedded_file_list = embedded_file_list
+
+ def get_embedded_file_list(self):
+ return self.embedded_file_list
+
+ def get_meta_dict(self):
+ return self.meta_dict
+
+ def process_footnotes(self):
+ for node, el1 in self.footnote_list:
+ backrefs = node.attributes.get('backrefs', [])
+ first = True
+ for ref in backrefs:
+ el2 = self.footnote_ref_dict.get(ref)
+ if el2 is not None:
+ if first:
+ first = False
+ el3 = copy.deepcopy(el1)
+ el2.append(el3)
+ else:
+ if len(el2) > 0: # and 'id' in el2.attrib:
+ child = el2[0]
+ ref1 = child.text
+ attribkey = add_ns('text:id', nsdict=SNSD)
+ id1 = el2.get(attribkey, 'footnote-error')
+ if id1 is None:
+ id1 = ''
+ tag = add_ns('text:note-ref', nsdict=SNSD)
+ el2.tag = tag
+ if self.settings.endnotes_end_doc:
+ note_class = 'endnote'
+ else:
+ note_class = 'footnote'
+ el2.attrib.clear()
+ attribkey = add_ns('text:note-class', nsdict=SNSD)
+ el2.attrib[attribkey] = note_class
+ attribkey = add_ns('text:ref-name', nsdict=SNSD)
+ el2.attrib[attribkey] = id1
+ attribkey = add_ns(
+ 'text:reference-format', nsdict=SNSD)
+ el2.attrib[attribkey] = 'page'
+ el2.text = ref1
+
+ #
+ # Utility methods
+
+ def append_child(self, tag, attrib=None, parent=None):
+ if parent is None:
+ parent = self.current_element
+ return SubElement(parent, tag, attrib)
+
+ def append_p(self, style, text=None):
+ result = self.append_child('text:p', attrib={
+ 'text:style-name': self.rststyle(style)})
+ self.append_pending_ids(result)
+ if text is not None:
+ result.text = text
+ return result
+
+ def append_pending_ids(self, el):
+ if self.settings.create_links:
+ for id in self.pending_ids:
+ SubElement(el, 'text:reference-mark', attrib={
+ 'text:name': id})
+ self.pending_ids = []
+
+ def set_current_element(self, el):
+ self.current_element = el
+
+ def set_to_parent(self):
+ self.current_element = self.current_element.getparent()
+
+ def generate_labeled_block(self, node, label):
+ label = '%s:' % (self.language.labels[label], )
+ el = self.append_p('textbody')
+ el1 = SubElement(
+ el, 'text:span',
+ attrib={'text:style-name': self.rststyle('strong')})
+ el1.text = label
+ return self.append_p('blockindent')
+
+ def generate_labeled_line(self, node, label):
+ label = '%s:' % (self.language.labels[label], )
+ el = self.append_p('textbody')
+ el1 = SubElement(
+ el, 'text:span',
+ attrib={'text:style-name': self.rststyle('strong')})
+ el1.text = label
+ el1.tail = node.astext()
+ return el
+
+ def encode(self, text):
+ return text.replace('\n', " ")
+
+ #
+ # Visitor functions
+ #
+ # In alphabetic order, more or less.
+ # See docutils.docutils.nodes.node_class_names.
+ #
+
+ def dispatch_visit(self, node):
+ """Override to catch basic attributes which many nodes have."""
+ self.handle_basic_atts(node)
+ nodes.GenericNodeVisitor.dispatch_visit(self, node)
+
+ def handle_basic_atts(self, node):
+ if isinstance(node, nodes.Element) and node['ids']:
+ self.pending_ids += node['ids']
+
+ def default_visit(self, node):
+ self.document.reporter.warning('missing visit_%s' % (node.tagname, ))
+
+ def default_departure(self, node):
+ self.document.reporter.warning('missing depart_%s' % (node.tagname, ))
+
+ def visit_Text(self, node):
+ # Skip nodes whose text has been processed in parent nodes.
+ if isinstance(node.parent, docutils.nodes.literal_block):
+ return
+ text = node.astext()
+ # Are we in mixed content? If so, add the text to the
+ # etree tail of the previous sibling element.
+ if len(self.current_element) > 0:
+ if self.current_element[-1].tail:
+ self.current_element[-1].tail += text
+ else:
+ self.current_element[-1].tail = text
+ else:
+ if self.current_element.text:
+ self.current_element.text += text
+ else:
+ self.current_element.text = text
+
+ def depart_Text(self, node):
+ pass
+
+ #
+ # Pre-defined fields
+ #
+
+ def visit_address(self, node):
+ el = self.generate_labeled_block(node, 'address')
+ self.set_current_element(el)
+
+ def depart_address(self, node):
+ self.set_to_parent()
+
+ def visit_author(self, node):
+ if isinstance(node.parent, nodes.authors):
+ el = self.append_p('blockindent')
+ else:
+ el = self.generate_labeled_block(node, 'author')
+ self.set_current_element(el)
+
+ def depart_author(self, node):
+ self.set_to_parent()
+
+ def visit_authors(self, node):
+ label = '%s:' % (self.language.labels['authors'], )
+ el = self.append_p('textbody')
+ el1 = SubElement(
+ el, 'text:span',
+ attrib={'text:style-name': self.rststyle('strong')})
+ el1.text = label
+
+ def depart_authors(self, node):
+ pass
+
+ def visit_contact(self, node):
+ el = self.generate_labeled_block(node, 'contact')
+ self.set_current_element(el)
+
+ def depart_contact(self, node):
+ self.set_to_parent()
+
+ def visit_copyright(self, node):
+ el = self.generate_labeled_block(node, 'copyright')
+ self.set_current_element(el)
+
+ def depart_copyright(self, node):
+ self.set_to_parent()
+
+ def visit_date(self, node):
+ self.generate_labeled_line(node, 'date')
+
+ def depart_date(self, node):
+ pass
+
+ def visit_organization(self, node):
+ el = self.generate_labeled_block(node, 'organization')
+ self.set_current_element(el)
+
+ def depart_organization(self, node):
+ self.set_to_parent()
+
+ def visit_status(self, node):
+ el = self.generate_labeled_block(node, 'status')
+ self.set_current_element(el)
+
+ def depart_status(self, node):
+ self.set_to_parent()
+
+ def visit_revision(self, node):
+ self.generate_labeled_line(node, 'revision')
+
+ def depart_revision(self, node):
+ pass
+
+ def visit_version(self, node):
+ self.generate_labeled_line(node, 'version')
+ # self.set_current_element(el)
+
+ def depart_version(self, node):
+ # self.set_to_parent()
+ pass
+
+ def visit_attribution(self, node):
+ self.append_p('attribution', node.astext())
+
+ def depart_attribution(self, node):
+ pass
+
+ def visit_block_quote(self, node):
+ if 'epigraph' in node.attributes['classes']:
+ self.paragraph_style_stack.append(self.rststyle('epigraph'))
+ self.blockstyle = self.rststyle('epigraph')
+ elif 'highlights' in node.attributes['classes']:
+ self.paragraph_style_stack.append(self.rststyle('highlights'))
+ self.blockstyle = self.rststyle('highlights')
+ else:
+ self.paragraph_style_stack.append(self.rststyle('blockquote'))
+ self.blockstyle = self.rststyle('blockquote')
+ self.line_indent_level += 1
+
+ def depart_block_quote(self, node):
+ self.paragraph_style_stack.pop()
+ self.blockstyle = ''
+ self.line_indent_level -= 1
+
+ def visit_bullet_list(self, node):
+ self.list_level += 1
+ if self.in_table_of_contents:
+ if self.settings.generate_oowriter_toc:
+ pass
+ else:
+ if 'classes' in node and \
+ 'auto-toc' in node.attributes['classes']:
+ el = SubElement(self.current_element, 'text:list', attrib={
+ 'text:style-name': self.rststyle('tocenumlist'),
+ })
+ self.list_style_stack.append(self.rststyle('enumitem'))
+ else:
+ el = SubElement(self.current_element, 'text:list', attrib={
+ 'text:style-name': self.rststyle('tocbulletlist'),
+ })
+ self.list_style_stack.append(self.rststyle('bulletitem'))
+ self.set_current_element(el)
+ else:
+ if self.blockstyle == self.rststyle('blockquote'):
+ el = SubElement(self.current_element, 'text:list', attrib={
+ 'text:style-name': self.rststyle('blockquote-bulletlist'),
+ })
+ self.list_style_stack.append(
+ self.rststyle('blockquote-bulletitem'))
+ elif self.blockstyle == self.rststyle('highlights'):
+ el = SubElement(self.current_element, 'text:list', attrib={
+ 'text:style-name': self.rststyle('highlights-bulletlist'),
+ })
+ self.list_style_stack.append(
+ self.rststyle('highlights-bulletitem'))
+ elif self.blockstyle == self.rststyle('epigraph'):
+ el = SubElement(self.current_element, 'text:list', attrib={
+ 'text:style-name': self.rststyle('epigraph-bulletlist'),
+ })
+ self.list_style_stack.append(
+ self.rststyle('epigraph-bulletitem'))
+ else:
+ el = SubElement(self.current_element, 'text:list', attrib={
+ 'text:style-name': self.rststyle('bulletlist'),
+ })
+ self.list_style_stack.append(self.rststyle('bulletitem'))
+ self.set_current_element(el)
+
+ def depart_bullet_list(self, node):
+ if self.in_table_of_contents:
+ if self.settings.generate_oowriter_toc:
+ pass
+ else:
+ self.set_to_parent()
+ self.list_style_stack.pop()
+ else:
+ self.set_to_parent()
+ self.list_style_stack.pop()
+ self.list_level -= 1
+
+ def visit_caption(self, node):
+ raise nodes.SkipChildren()
+
+ def depart_caption(self, node):
+ pass
+
+ def visit_comment(self, node):
+ el = self.append_p('textbody')
+ el1 = SubElement(el, 'office:annotation', attrib={})
+ el2 = SubElement(el1, 'dc:creator', attrib={})
+ s1 = os.environ.get('USER', '')
+ el2.text = s1
+ el2 = SubElement(el1, 'text:p', attrib={})
+ el2.text = node.astext()
+
+ def depart_comment(self, node):
+ pass
+
+ def visit_compound(self, node):
+ # The compound directive currently receives no special treatment.
+ pass
+
+ def depart_compound(self, node):
+ pass
+
+ def visit_container(self, node):
+ styles = node.attributes.get('classes', ())
+ if len(styles) > 0:
+ self.paragraph_style_stack.append(self.rststyle(styles[0]))
+
+ def depart_container(self, node):
+ styles = node.attributes.get('classes', ())
+ if len(styles) > 0:
+ self.paragraph_style_stack.pop()
+
+ def visit_decoration(self, node):
+ pass
+
+ def depart_decoration(self, node):
+ pass
+
+ def visit_definition_list(self, node):
+ self.def_list_level += 1
+ if self.list_level > 5:
+ raise RuntimeError(
+ 'max definition list nesting level exceeded')
+
+ def depart_definition_list(self, node):
+ self.def_list_level -= 1
+
+ def visit_definition_list_item(self, node):
+ pass
+
+ def depart_definition_list_item(self, node):
+ pass
+
+ def visit_term(self, node):
+ el = self.append_p('deflist-term-%d' % self.def_list_level)
+ el.text = node.astext()
+ self.set_current_element(el)
+ raise nodes.SkipChildren()
+
+ def depart_term(self, node):
+ self.set_to_parent()
+
+ def visit_definition(self, node):
+ self.paragraph_style_stack.append(
+ self.rststyle('deflist-def-%d' % self.def_list_level))
+ self.bumped_list_level_stack.append(ListLevel(1))
+
+ def depart_definition(self, node):
+ self.paragraph_style_stack.pop()
+ self.bumped_list_level_stack.pop()
+
+ def visit_classifier(self, node):
+ if len(self.current_element) > 0:
+ el = self.current_element[-1]
+ el1 = SubElement(
+ el, 'text:span',
+ attrib={'text:style-name': self.rststyle('emphasis')})
+ el1.text = ' (%s)' % (node.astext(), )
+
+ def depart_classifier(self, node):
+ pass
+
+ def visit_document(self, node):
+ pass
+
+ def depart_document(self, node):
+ self.process_footnotes()
+
+ def visit_docinfo(self, node):
+ self.section_level += 1
+ self.section_count += 1
+ if self.settings.create_sections:
+ el = self.append_child(
+ 'text:section', attrib={
+ 'text:name': 'Section%d' % self.section_count,
+ 'text:style-name': 'Sect%d' % self.section_level,
+ }
+ )
+ self.set_current_element(el)
+
+ def depart_docinfo(self, node):
+ self.section_level -= 1
+ if self.settings.create_sections:
+ self.set_to_parent()
+
+ def visit_emphasis(self, node):
+ el = SubElement(
+ self.current_element, 'text:span',
+ attrib={'text:style-name': self.rststyle('emphasis')})
+ self.set_current_element(el)
+
+ def depart_emphasis(self, node):
+ self.set_to_parent()
+
+ def visit_enumerated_list(self, node):
+ el1 = self.current_element
+ if self.blockstyle == self.rststyle('blockquote'):
+ el2 = SubElement(el1, 'text:list', attrib={
+ 'text:style-name': self.rststyle('blockquote-enumlist'),
+ })
+ self.list_style_stack.append(self.rststyle('blockquote-enumitem'))
+ elif self.blockstyle == self.rststyle('highlights'):
+ el2 = SubElement(el1, 'text:list', attrib={
+ 'text:style-name': self.rststyle('highlights-enumlist'),
+ })
+ self.list_style_stack.append(self.rststyle('highlights-enumitem'))
+ elif self.blockstyle == self.rststyle('epigraph'):
+ el2 = SubElement(el1, 'text:list', attrib={
+ 'text:style-name': self.rststyle('epigraph-enumlist'),
+ })
+ self.list_style_stack.append(self.rststyle('epigraph-enumitem'))
+ else:
+ liststylename = 'enumlist-%s' % (node.get('enumtype', 'arabic'), )
+ el2 = SubElement(el1, 'text:list', attrib={
+ 'text:style-name': self.rststyle(liststylename),
+ })
+ self.list_style_stack.append(self.rststyle('enumitem'))
+ self.set_current_element(el2)
+
+ def depart_enumerated_list(self, node):
+ self.set_to_parent()
+ self.list_style_stack.pop()
+
+ def visit_list_item(self, node):
+ # If we are in a "bumped" list level, then wrap this
+ # list in an outer lists in order to increase the
+ # indentation level.
+ if self.in_table_of_contents:
+ if self.settings.generate_oowriter_toc:
+ self.paragraph_style_stack.append(
+ self.rststyle('contents-%d' % (self.list_level, )))
+ else:
+ el1 = self.append_child('text:list-item')
+ self.set_current_element(el1)
+ else:
+ el1 = self.append_child('text:list-item')
+ el3 = el1
+ if len(self.bumped_list_level_stack) > 0:
+ level_obj = self.bumped_list_level_stack[-1]
+ if level_obj.get_sibling():
+ level_obj.set_nested(False)
+ for level_obj1 in self.bumped_list_level_stack:
+ for idx in range(level_obj1.get_level()):
+ el2 = self.append_child('text:list', parent=el3)
+ el3 = self.append_child(
+ 'text:list-item', parent=el2)
+ self.paragraph_style_stack.append(self.list_style_stack[-1])
+ self.set_current_element(el3)
+
+ def depart_list_item(self, node):
+ if self.in_table_of_contents:
+ if self.settings.generate_oowriter_toc:
+ self.paragraph_style_stack.pop()
+ else:
+ self.set_to_parent()
+ else:
+ if len(self.bumped_list_level_stack) > 0:
+ level_obj = self.bumped_list_level_stack[-1]
+ if level_obj.get_sibling():
+ level_obj.set_nested(True)
+ for level_obj1 in self.bumped_list_level_stack:
+ for idx in range(level_obj1.get_level()):
+ self.set_to_parent()
+ self.set_to_parent()
+ self.paragraph_style_stack.pop()
+ self.set_to_parent()
+
+ def visit_header(self, node):
+ self.in_header = True
+
+ def depart_header(self, node):
+ self.in_header = False
+
+ def visit_footer(self, node):
+ self.in_footer = True
+
+ def depart_footer(self, node):
+ self.in_footer = False
+
+ def visit_field(self, node):
+ pass
+
+ def depart_field(self, node):
+ pass
+
+ def visit_field_list(self, node):
+ pass
+
+ def depart_field_list(self, node):
+ pass
+
+ def visit_field_name(self, node):
+ el = self.append_p('textbody')
+ el1 = SubElement(
+ el, 'text:span',
+ attrib={'text:style-name': self.rststyle('strong')})
+ el1.text = node.astext()
+
+ def depart_field_name(self, node):
+ pass
+
+ def visit_field_body(self, node):
+ self.paragraph_style_stack.append(self.rststyle('blockindent'))
+
+ def depart_field_body(self, node):
+ self.paragraph_style_stack.pop()
+
+ def visit_figure(self, node):
+ pass
+
+ def depart_figure(self, node):
+ pass
+
+ def visit_footnote(self, node):
+ self.footnote_level += 1
+ self.save_footnote_current = self.current_element
+ el1 = Element('text:note-body')
+ self.current_element = el1
+ self.footnote_list.append((node, el1))
+ if isinstance(node, docutils.nodes.citation):
+ self.paragraph_style_stack.append(self.rststyle('citation'))
+ else:
+ self.paragraph_style_stack.append(self.rststyle('footnote'))
+
+ def depart_footnote(self, node):
+ self.paragraph_style_stack.pop()
+ self.current_element = self.save_footnote_current
+ self.footnote_level -= 1
+
+ footnote_chars = [
+ '*', '**', '***',
+ '++', '+++',
+ '##', '###',
+ '@@', '@@@',
+ ]
+
+ def visit_footnote_reference(self, node):
+ if self.footnote_level <= 0:
+ id = node.attributes['ids'][0]
+ refid = node.attributes.get('refid')
+ if refid is None:
+ refid = ''
+ if self.settings.endnotes_end_doc:
+ note_class = 'endnote'
+ else:
+ note_class = 'footnote'
+ el1 = self.append_child('text:note', attrib={
+ 'text:id': '%s' % (refid, ),
+ 'text:note-class': note_class,
+ })
+ note_auto = str(node.attributes.get('auto', 1))
+ if isinstance(node, docutils.nodes.citation_reference):
+ citation = '[%s]' % node.astext()
+ el2 = SubElement(el1, 'text:note-citation', attrib={
+ 'text:label': citation,
+ })
+ el2.text = citation
+ elif note_auto == '1':
+ el2 = SubElement(el1, 'text:note-citation', attrib={
+ 'text:label': node.astext(),
+ })
+ el2.text = node.astext()
+ elif note_auto == '*':
+ if self.footnote_chars_idx >= len(
+ ODFTranslator.footnote_chars):
+ self.footnote_chars_idx = 0
+ footnote_char = ODFTranslator.footnote_chars[
+ self.footnote_chars_idx]
+ self.footnote_chars_idx += 1
+ el2 = SubElement(el1, 'text:note-citation', attrib={
+ 'text:label': footnote_char,
+ })
+ el2.text = footnote_char
+ self.footnote_ref_dict[id] = el1
+ raise nodes.SkipChildren()
+
+ def depart_footnote_reference(self, node):
+ pass
+
+ def visit_citation(self, node):
+ self.in_citation = True
+ for id in node.attributes['ids']:
+ self.citation_id = id
+ break
+ self.paragraph_style_stack.append(self.rststyle('blockindent'))
+ self.bumped_list_level_stack.append(ListLevel(1))
+
+ def depart_citation(self, node):
+ self.citation_id = None
+ self.paragraph_style_stack.pop()
+ self.bumped_list_level_stack.pop()
+ self.in_citation = False
+
+ def visit_citation_reference(self, node):
+ if self.settings.create_links:
+ id = node.attributes['refid']
+ el = self.append_child('text:reference-ref', attrib={
+ 'text:ref-name': '%s' % (id, ),
+ 'text:reference-format': 'text',
+ })
+ el.text = '['
+ self.set_current_element(el)
+ elif self.current_element.text is None:
+ self.current_element.text = '['
+ else:
+ self.current_element.text += '['
+
+ def depart_citation_reference(self, node):
+ self.current_element.text += ']'
+ if self.settings.create_links:
+ self.set_to_parent()
+
+ def visit_label(self, node):
+ if isinstance(node.parent, docutils.nodes.footnote):
+ raise nodes.SkipChildren()
+ elif self.citation_id is not None:
+ el = self.append_p('textbody')
+ self.set_current_element(el)
+ if self.settings.create_links:
+ el0 = SubElement(el, 'text:span')
+ el0.text = '['
+ self.append_child('text:reference-mark-start', attrib={
+ 'text:name': '%s' % (self.citation_id, ),
+ })
+ else:
+ el.text = '['
+
+ def depart_label(self, node):
+ if isinstance(node.parent, docutils.nodes.footnote):
+ pass
+ elif self.citation_id is not None:
+ if self.settings.create_links:
+ self.append_child('text:reference-mark-end', attrib={
+ 'text:name': '%s' % (self.citation_id, ),
+ })
+ el0 = SubElement(self.current_element, 'text:span')
+ el0.text = ']'
+ else:
+ self.current_element.text += ']'
+ self.set_to_parent()
+
+ def visit_generated(self, node):
+ pass
+
+ def depart_generated(self, node):
+ pass
+
+ def check_file_exists(self, path):
+ if os.path.exists(path):
+ return 1
+ else:
+ return 0
+
+ def visit_image(self, node):
+ # Capture the image file.
+ source = node['uri']
+ uri_parts = urllib.parse.urlparse(source)
+ if uri_parts.scheme in ('', 'file'):
+ source = urllib.parse.unquote(uri_parts.path)
+ if source.startswith('/'):
+ root_prefix = Path(self.settings.root_prefix)
+ source = (root_prefix/source[1:]).as_posix()
+ else:
+ # adapt relative paths
+ docsource, line = utils.get_source_line(node)
+ if docsource:
+ dirname = os.path.dirname(docsource)
+ if dirname:
+ source = os.path.join(dirname, source)
+ if not self.check_file_exists(source):
+ self.document.reporter.warning(
+ f'Cannot find image file "{source}".')
+ return
+ if source in self.image_dict:
+ filename, destination = self.image_dict[source]
+ else:
+ self.image_count += 1
+ filename = os.path.split(source)[1]
+ destination = 'Pictures/1%08x%s' % (self.image_count, filename)
+ if uri_parts.scheme in ('', 'file'):
+ spec = (os.path.abspath(source), destination,)
+ else:
+ try:
+ with urllib.request.urlopen(source) as imgfile:
+ content = imgfile.read()
+ except urllib.error.URLError as err:
+ self.document.reporter.warning(
+ f'Cannot open image URL "{source}". {err}')
+ return
+ with tempfile.NamedTemporaryFile('wb',
+ delete=False) as imgfile2:
+ imgfile2.write(content)
+ source = imgfile2.name
+ spec = (source, destination,)
+ self.embedded_file_list.append(spec)
+ self.image_dict[source] = (source, destination,)
+ # Is this a figure (containing an image) or just a plain image?
+ if self.in_paragraph:
+ el1 = self.current_element
+ else:
+ el1 = SubElement(
+ self.current_element, 'text:p',
+ attrib={'text:style-name': self.rststyle('textbody')})
+ el2 = el1
+ if isinstance(node.parent, docutils.nodes.figure):
+ el3, el4, el5, caption = self.generate_figure(
+ node, source,
+ destination, el2)
+ attrib = {}
+ el6, width = self.generate_image(
+ node, source, destination,
+ el5, attrib)
+ if caption is not None:
+ el6.tail = caption
+ else: # if isinstance(node.parent, docutils.nodes.image):
+ self.generate_image(node, source, destination, el2)
+
+ def depart_image(self, node):
+ pass
+
+ def get_image_width_height(self, node, attr):
+ size = None
+ unit = None
+ if attr in node.attributes:
+ size = node.attributes[attr]
+ size = size.strip()
+ # For conversion factors, see:
+ # http://www.unitconversion.org/unit_converter/typography-ex.html
+ try:
+ if size.endswith('%'):
+ if attr == 'height':
+ # Percentage allowed for width but not height.
+ raise ValueError('percentage not allowed for height')
+ size = size.rstrip(' %')
+ size = float(size) / 100.0
+ unit = '%'
+ else:
+ size, unit = self.convert_to_cm(size)
+ except ValueError as exp:
+ self.document.reporter.warning(
+ 'Invalid %s for image: "%s". '
+ 'Error: "%s".' % (
+ attr, node.attributes[attr], exp))
+ return size, unit
+
+ def convert_to_cm(self, size):
+ """Convert various units to centimeters.
+
+ Note that a call to this method should be wrapped in:
+ try: except ValueError:
+ """
+ size = size.strip()
+ if size.endswith('px'):
+ size = float(size[:-2]) * 0.026 # convert px to cm
+ elif size.endswith('in'):
+ size = float(size[:-2]) * 2.54 # convert in to cm
+ elif size.endswith('pt'):
+ size = float(size[:-2]) * 0.035 # convert pt to cm
+ elif size.endswith('pc'):
+ size = float(size[:-2]) * 2.371 # convert pc to cm
+ elif size.endswith('mm'):
+ size = float(size[:-2]) * 0.1 # convert mm to cm
+ elif size.endswith('cm'):
+ size = float(size[:-2])
+ else:
+ raise ValueError('unknown unit type')
+ unit = 'cm'
+ return size, unit
+
+ def get_image_scale(self, node):
+ if 'scale' in node.attributes:
+ scale = node.attributes['scale']
+ try:
+ scale = int(scale)
+ except ValueError:
+ self.document.reporter.warning(
+ 'Invalid scale for image: "%s"' % (
+ node.attributes['scale'], ))
+ if scale < 1: # or scale > 100:
+ self.document.reporter.warning(
+ 'scale out of range (%s), using 1.' % (scale, ))
+ scale = 1
+ scale = scale * 0.01
+ else:
+ scale = 1.0
+ return scale
+
+ def get_image_scaled_width_height(self, node, source):
+ """Return the image size in centimeters adjusted by image attrs."""
+ scale = self.get_image_scale(node)
+ width, width_unit = self.get_image_width_height(node, 'width')
+ height, _ = self.get_image_width_height(node, 'height')
+ dpi = (72, 72)
+ if PIL is not None and source in self.image_dict:
+ filename, destination = self.image_dict[source]
+ with PIL.Image.open(filename, 'r') as img:
+ img_size = img.size
+ dpi = img.info.get('dpi', dpi)
+ # dpi information can be (xdpi, ydpi) or xydpi
+ try:
+ iter(dpi)
+ except TypeError:
+ dpi = (dpi, dpi)
+ else:
+ img_size = None
+ if width is None or height is None:
+ if img_size is None:
+ raise RuntimeError(
+ 'image size not fully specified and PIL not installed')
+ if width is None:
+ width = img_size[0]
+ width = float(width) * 0.026 # convert px to cm
+ if height is None:
+ height = img_size[1]
+ height = float(height) * 0.026 # convert px to cm
+ if width_unit == '%':
+ factor = width
+ image_width = img_size[0]
+ image_width = float(image_width) * 0.026 # convert px to cm
+ image_height = img_size[1]
+ image_height = float(image_height) * 0.026 # convert px to cm
+ line_width = self.get_page_width()
+ width = factor * line_width
+ factor = (factor * line_width) / image_width
+ height = factor * image_height
+ width *= scale
+ height *= scale
+ width = '%.2fcm' % width
+ height = '%.2fcm' % height
+ return width, height
+
+ def get_page_width(self):
+ """Return the document's page width in centimeters."""
+ root = self.get_dom_stylesheet()
+ nodes = root.iterfind(
+ './/{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
+ 'page-layout/'
+ '{urn:oasis:names:tc:opendocument:xmlns:style:1.0}'
+ 'page-layout-properties')
+ width = None
+ for node in nodes:
+ page_width = node.get(
+ '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
+ 'page-width')
+ margin_left = node.get(
+ '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
+ 'margin-left')
+ margin_right = node.get(
+ '{urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0}'
+ 'margin-right')
+ if (page_width is None
+ or margin_left is None
+ or margin_right is None):
+ continue
+ try:
+ page_width, _ = self.convert_to_cm(page_width)
+ margin_left, _ = self.convert_to_cm(margin_left)
+ margin_right, _ = self.convert_to_cm(margin_right)
+ except ValueError:
+ self.document.reporter.warning(
+ 'Stylesheet file contains invalid page width '
+ 'or margin size.')
+ width = page_width - margin_left - margin_right
+ if width is None:
+ # We can't find the width in styles, so we make a guess.
+ # Use a width of 6 in = 15.24 cm.
+ width = 15.24
+ return width
+
+ def generate_figure(self, node, source, destination, current_element):
+ caption = None
+ width, height = self.get_image_scaled_width_height(node, source)
+ for node1 in node.parent.children:
+ if node1.tagname == 'caption':
+ caption = node1.astext()
+ self.image_style_count += 1
+ #
+ # Add the style for the caption.
+ if caption is not None:
+ attrib = {
+ 'style:class': 'extra',
+ 'style:family': 'paragraph',
+ 'style:name': 'Caption',
+ 'style:parent-style-name': 'Standard',
+ }
+ el1 = SubElement(self.automatic_styles, 'style:style',
+ attrib=attrib, nsdict=SNSD)
+ attrib = {
+ 'fo:margin-bottom': '0.0835in',
+ 'fo:margin-top': '0.0835in',
+ 'text:line-number': '0',
+ 'text:number-lines': 'false',
+ }
+ SubElement(el1, 'style:paragraph-properties',
+ attrib=attrib, nsdict=SNSD)
+ attrib = {
+ 'fo:font-size': '12pt',
+ 'fo:font-style': 'italic',
+ 'style:font-name': 'Times',
+ 'style:font-name-complex': 'Lucidasans1',
+ 'style:font-size-asian': '12pt',
+ 'style:font-size-complex': '12pt',
+ 'style:font-style-asian': 'italic',
+ 'style:font-style-complex': 'italic',
+ }
+ SubElement(el1, 'style:text-properties',
+ attrib=attrib, nsdict=SNSD)
+ style_name = 'rstframestyle%d' % self.image_style_count
+ draw_name = 'graphics%d' % next(IMAGE_NAME_COUNTER)
+ # Add the styles
+ attrib = {
+ 'style:name': style_name,
+ 'style:family': 'graphic',
+ 'style:parent-style-name': self.rststyle('figureframe'),
+ }
+ el1 = SubElement(self.automatic_styles,
+ 'style:style', attrib=attrib, nsdict=SNSD)
+ attrib = {}
+ wrap = False
+ classes = node.parent.attributes.get('classes')
+ if classes and 'wrap' in classes:
+ wrap = True
+ if wrap:
+ attrib['style:wrap'] = 'dynamic'
+ else:
+ attrib['style:wrap'] = 'none'
+ SubElement(el1, 'style:graphic-properties',
+ attrib=attrib, nsdict=SNSD)
+ attrib = {
+ 'draw:style-name': style_name,
+ 'draw:name': draw_name,
+ 'text:anchor-type': 'paragraph',
+ 'draw:z-index': '0',
+ }
+ attrib['svg:width'] = width
+ el3 = SubElement(current_element, 'draw:frame', attrib=attrib)
+ attrib = {}
+ el4 = SubElement(el3, 'draw:text-box', attrib=attrib)
+ attrib = {
+ 'text:style-name': self.rststyle('caption'),
+ }
+ el5 = SubElement(el4, 'text:p', attrib=attrib)
+ return el3, el4, el5, caption
+
+ def generate_image(self, node, source, destination, current_element,
+ frame_attrs=None):
+ width, height = self.get_image_scaled_width_height(node, source)
+ self.image_style_count += 1
+ style_name = 'rstframestyle%d' % self.image_style_count
+ # Add the style.
+ attrib = {
+ 'style:name': style_name,
+ 'style:family': 'graphic',
+ 'style:parent-style-name': self.rststyle('image'),
+ }
+ el1 = SubElement(self.automatic_styles,
+ 'style:style', attrib=attrib, nsdict=SNSD)
+ halign = None
+ valign = None
+ if 'align' in node.attributes:
+ align = node.attributes['align'].split()
+ for val in align:
+ if val in ('left', 'center', 'right'):
+ halign = val
+ elif val in ('top', 'middle', 'bottom'):
+ valign = val
+ if frame_attrs is None:
+ attrib = {
+ 'style:vertical-pos': 'top',
+ 'style:vertical-rel': 'paragraph',
+ 'style:horizontal-rel': 'paragraph',
+ 'style:mirror': 'none',
+ 'fo:clip': 'rect(0cm 0cm 0cm 0cm)',
+ 'draw:luminance': '0%',
+ 'draw:contrast': '0%',
+ 'draw:red': '0%',
+ 'draw:green': '0%',
+ 'draw:blue': '0%',
+ 'draw:gamma': '100%',
+ 'draw:color-inversion': 'false',
+ 'draw:image-opacity': '100%',
+ 'draw:color-mode': 'standard',
+ }
+ else:
+ attrib = frame_attrs
+ if halign is not None:
+ attrib['style:horizontal-pos'] = halign
+ if valign is not None:
+ attrib['style:vertical-pos'] = valign
+ # If there is a classes/wrap directive or we are
+ # inside a table, add a no-wrap style.
+ wrap = False
+ classes = node.attributes.get('classes')
+ if classes and 'wrap' in classes:
+ wrap = True
+ if wrap:
+ attrib['style:wrap'] = 'dynamic'
+ else:
+ attrib['style:wrap'] = 'none'
+ # If we are inside a table, add a no-wrap style.
+ if self.is_in_table(node):
+ attrib['style:wrap'] = 'none'
+ SubElement(el1, 'style:graphic-properties',
+ attrib=attrib, nsdict=SNSD)
+ draw_name = 'graphics%d' % next(IMAGE_NAME_COUNTER)
+ # Add the content.
+ # el = SubElement(current_element, 'text:p',
+ # attrib={'text:style-name': self.rststyle('textbody')})
+ attrib = {
+ 'draw:style-name': style_name,
+ 'draw:name': draw_name,
+ 'draw:z-index': '1',
+ }
+ if isinstance(node.parent, nodes.TextElement):
+ attrib['text:anchor-type'] = 'as-char' # vds
+ else:
+ attrib['text:anchor-type'] = 'paragraph'
+ attrib['svg:width'] = width
+ attrib['svg:height'] = height
+ el1 = SubElement(current_element, 'draw:frame', attrib=attrib)
+ SubElement(el1, 'draw:image', attrib={
+ 'xlink:href': '%s' % (destination, ),
+ 'xlink:type': 'simple',
+ 'xlink:show': 'embed',
+ 'xlink:actuate': 'onLoad',
+ })
+ return el1, width
+
+ def is_in_table(self, node):
+ node1 = node.parent
+ while node1:
+ if isinstance(node1, docutils.nodes.entry):
+ return True
+ node1 = node1.parent
+ return False
+
+ def visit_legend(self, node):
+ if isinstance(node.parent, docutils.nodes.figure):
+ el1 = self.current_element[-1]
+ el1 = el1[0][0]
+ self.current_element = el1
+ self.paragraph_style_stack.append(self.rststyle('legend'))
+
+ def depart_legend(self, node):
+ if isinstance(node.parent, docutils.nodes.figure):
+ self.paragraph_style_stack.pop()
+ self.set_to_parent()
+ self.set_to_parent()
+ self.set_to_parent()
+
+ def visit_line_block(self, node):
+ self.line_indent_level += 1
+ self.line_block_level += 1
+
+ def depart_line_block(self, node):
+ self.line_indent_level -= 1
+ self.line_block_level -= 1
+
+ def visit_line(self, node):
+ style = 'lineblock%d' % self.line_indent_level
+ el1 = SubElement(self.current_element, 'text:p',
+ attrib={'text:style-name': self.rststyle(style), })
+ self.current_element = el1
+
+ def depart_line(self, node):
+ self.set_to_parent()
+
+ def visit_literal(self, node):
+ el = SubElement(
+ self.current_element, 'text:span',
+ attrib={'text:style-name': self.rststyle('inlineliteral')})
+ self.set_current_element(el)
+
+ def depart_literal(self, node):
+ self.set_to_parent()
+
+ def visit_inline(self, node):
+ styles = node.attributes.get('classes', ())
+ if styles:
+ el = self.current_element
+ for inline_style in styles:
+ el = SubElement(el, 'text:span',
+ attrib={'text:style-name':
+ self.rststyle(inline_style)})
+ count = len(styles)
+ else:
+ # No style was specified so use a default style (old code
+ # crashed if no style was given)
+ el = SubElement(self.current_element, 'text:span')
+ count = 1
+
+ self.set_current_element(el)
+ self.inline_style_count_stack.append(count)
+
+ def depart_inline(self, node):
+ count = self.inline_style_count_stack.pop()
+ for x in range(count):
+ self.set_to_parent()
+
+ def _calculate_code_block_padding(self, line):
+ count = 0
+ matchobj = SPACES_PATTERN.match(line)
+ if matchobj:
+ pad = matchobj.group()
+ count = len(pad)
+ else:
+ matchobj = TABS_PATTERN.match(line)
+ if matchobj:
+ pad = matchobj.group()
+ count = len(pad) * 8
+ return count
+
+ def _add_syntax_highlighting(self, insource, language):
+ lexer = pygments.lexers.get_lexer_by_name(language, stripall=True)
+ if language in ('latex', 'tex'):
+ fmtr = OdtPygmentsLaTeXFormatter(
+ lambda name, parameters=():
+ self.rststyle(name, parameters),
+ escape_function=escape_cdata)
+ else:
+ fmtr = OdtPygmentsProgFormatter(
+ lambda name, parameters=():
+ self.rststyle(name, parameters),
+ escape_function=escape_cdata)
+ return pygments.highlight(insource, lexer, fmtr)
+
+ def fill_line(self, line):
+ line = FILL_PAT1.sub(self.fill_func1, line)
+ return FILL_PAT2.sub(self.fill_func2, line)
+
+ def fill_func1(self, matchobj):
+ spaces = matchobj.group(0)
+ return '<text:s text:c="%d"/>' % (len(spaces), )
+
+ def fill_func2(self, matchobj):
+ spaces = matchobj.group(0)
+ return ' <text:s text:c="%d"/>' % (len(spaces) - 1, )
+
+ def visit_literal_block(self, node):
+ if len(self.paragraph_style_stack) > 1:
+ wrapper1 = '<text:p text:style-name="%s">%%s</text:p>' % (
+ self.rststyle('codeblock-indented'), )
+ else:
+ wrapper1 = '<text:p text:style-name="%s">%%s</text:p>' % (
+ self.rststyle('codeblock'), )
+ source = node.astext()
+ if (pygments and self.settings.add_syntax_highlighting):
+ language = node.get('language', 'python')
+ source = self._add_syntax_highlighting(source, language)
+ else:
+ source = escape_cdata(source)
+ lines = source.split('\n')
+ # If there is an empty last line, remove it.
+ if lines[-1] == '':
+ del lines[-1]
+ lines1 = ['<wrappertag1 xmlns:text="urn:oasis:names:tc:'
+ 'opendocument:xmlns:text:1.0">']
+ my_lines = []
+ for my_line in lines:
+ my_line = self.fill_line(my_line)
+ my_line = my_line.replace("&#10;", "\n")
+ my_lines.append(my_line)
+ my_lines_str = '<text:line-break/>'.join(my_lines)
+ my_lines_str2 = wrapper1 % (my_lines_str, )
+ lines1.append(my_lines_str2)
+ lines1.append('</wrappertag1>')
+ s1 = ''.join(lines1)
+ s1 = s1.encode("utf-8")
+ el1 = etree.fromstring(s1)
+ for child in el1:
+ self.current_element.append(child)
+
+ def depart_literal_block(self, node):
+ pass
+
+ visit_doctest_block = visit_literal_block
+ depart_doctest_block = depart_literal_block
+
+ # placeholder for math (see docs/dev/todo.txt)
+ def visit_math(self, node):
+ self.document.reporter.warning('"math" role not supported',
+ base_node=node)
+ self.visit_literal(node)
+
+ def depart_math(self, node):
+ self.depart_literal(node)
+
+ def visit_math_block(self, node):
+ self.document.reporter.warning('"math" directive not supported',
+ base_node=node)
+ self.visit_literal_block(node)
+
+ def depart_math_block(self, node):
+ self.depart_literal_block(node)
+
+ def visit_meta(self, node):
+ name = node.attributes.get('name')
+ content = node.attributes.get('content')
+ if name is not None and content is not None:
+ self.meta_dict[name] = content
+
+ def depart_meta(self, node):
+ pass
+
+ def visit_option_list(self, node):
+ table_name = 'tableoption'
+ #
+ # Generate automatic styles
+ if not self.optiontablestyles_generated:
+ self.optiontablestyles_generated = True
+ el = SubElement(self.automatic_styles, 'style:style', attrib={
+ 'style:name': self.rststyle(table_name),
+ 'style:family': 'table'}, nsdict=SNSD)
+ el1 = SubElement(el, 'style:table-properties', attrib={
+ 'style:width': '17.59cm',
+ 'table:align': 'left',
+ 'style:shadow': 'none'}, nsdict=SNSD)
+ el = SubElement(self.automatic_styles, 'style:style', attrib={
+ 'style:name': self.rststyle('%s.%%c' % table_name, ('A', )),
+ 'style:family': 'table-column'}, nsdict=SNSD)
+ el1 = SubElement(el, 'style:table-column-properties', attrib={
+ 'style:column-width': '4.999cm'}, nsdict=SNSD)
+ el = SubElement(self.automatic_styles, 'style:style', attrib={
+ 'style:name': self.rststyle('%s.%%c' % table_name, ('B', )),
+ 'style:family': 'table-column'}, nsdict=SNSD)
+ el1 = SubElement(el, 'style:table-column-properties', attrib={
+ 'style:column-width': '12.587cm'}, nsdict=SNSD)
+ el = SubElement(self.automatic_styles, 'style:style', attrib={
+ 'style:name': self.rststyle(
+ '%s.%%c%%d' % table_name, ('A', 1, )),
+ 'style:family': 'table-cell'}, nsdict=SNSD)
+ el1 = SubElement(el, 'style:table-cell-properties', attrib={
+ 'fo:background-color': 'transparent',
+ 'fo:padding': '0.097cm',
+ 'fo:border-left': '0.035cm solid #000000',
+ 'fo:border-right': 'none',
+ 'fo:border-top': '0.035cm solid #000000',
+ 'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD)
+ el2 = SubElement(el1, 'style:background-image', nsdict=SNSD)
+ el = SubElement(self.automatic_styles, 'style:style', attrib={
+ 'style:name': self.rststyle(
+ '%s.%%c%%d' % table_name, ('B', 1, )),
+ 'style:family': 'table-cell'}, nsdict=SNSD)
+ el1 = SubElement(el, 'style:table-cell-properties', attrib={
+ 'fo:padding': '0.097cm',
+ 'fo:border': '0.035cm solid #000000'}, nsdict=SNSD)
+ el = SubElement(self.automatic_styles, 'style:style', attrib={
+ 'style:name': self.rststyle(
+ '%s.%%c%%d' % table_name, ('A', 2, )),
+ 'style:family': 'table-cell'}, nsdict=SNSD)
+ el1 = SubElement(el, 'style:table-cell-properties', attrib={
+ 'fo:padding': '0.097cm',
+ 'fo:border-left': '0.035cm solid #000000',
+ 'fo:border-right': 'none',
+ 'fo:border-top': 'none',
+ 'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD)
+ el = SubElement(self.automatic_styles, 'style:style', attrib={
+ 'style:name': self.rststyle(
+ '%s.%%c%%d' % table_name, ('B', 2, )),
+ 'style:family': 'table-cell'}, nsdict=SNSD)
+ el1 = SubElement(el, 'style:table-cell-properties', attrib={
+ 'fo:padding': '0.097cm',
+ 'fo:border-left': '0.035cm solid #000000',
+ 'fo:border-right': '0.035cm solid #000000',
+ 'fo:border-top': 'none',
+ 'fo:border-bottom': '0.035cm solid #000000'}, nsdict=SNSD)
+ #
+ # Generate table data
+ el = self.append_child('table:table', attrib={
+ 'table:name': self.rststyle(table_name),
+ 'table:style-name': self.rststyle(table_name),
+ })
+ el1 = SubElement(el, 'table:table-column', attrib={
+ 'table:style-name': self.rststyle(
+ '%s.%%c' % table_name, ('A', ))})
+ el1 = SubElement(el, 'table:table-column', attrib={
+ 'table:style-name': self.rststyle(
+ '%s.%%c' % table_name, ('B', ))})
+ el1 = SubElement(el, 'table:table-header-rows')
+ el2 = SubElement(el1, 'table:table-row')
+ el3 = SubElement(el2, 'table:table-cell', attrib={
+ 'table:style-name': self.rststyle(
+ '%s.%%c%%d' % table_name, ('A', 1, )),
+ 'office:value-type': 'string'})
+ el4 = SubElement(el3, 'text:p', attrib={
+ 'text:style-name': 'Table_20_Heading'})
+ el4.text = 'Option'
+ el3 = SubElement(el2, 'table:table-cell', attrib={
+ 'table:style-name': self.rststyle(
+ '%s.%%c%%d' % table_name, ('B', 1, )),
+ 'office:value-type': 'string'})
+ el4 = SubElement(el3, 'text:p', attrib={
+ 'text:style-name': 'Table_20_Heading'})
+ el4.text = 'Description'
+ self.set_current_element(el)
+
+ def depart_option_list(self, node):
+ self.set_to_parent()
+
+ def visit_option_list_item(self, node):
+ el = self.append_child('table:table-row')
+ self.set_current_element(el)
+
+ def depart_option_list_item(self, node):
+ self.set_to_parent()
+
+ def visit_option_group(self, node):
+ el = self.append_child('table:table-cell', attrib={
+ 'table:style-name': 'Table%d.A2' % self.table_count,
+ 'office:value-type': 'string',
+ })
+ self.set_current_element(el)
+
+ def depart_option_group(self, node):
+ self.set_to_parent()
+
+ def visit_option(self, node):
+ el = self.append_child('text:p', attrib={
+ 'text:style-name': 'Table_20_Contents'})
+ el.text = node.astext()
+
+ def depart_option(self, node):
+ pass
+
+ def visit_option_string(self, node):
+ pass
+
+ def depart_option_string(self, node):
+ pass
+
+ def visit_option_argument(self, node):
+ pass
+
+ def depart_option_argument(self, node):
+ pass
+
+ def visit_description(self, node):
+ el = self.append_child('table:table-cell', attrib={
+ 'table:style-name': 'Table%d.B2' % self.table_count,
+ 'office:value-type': 'string',
+ })
+ el1 = SubElement(el, 'text:p', attrib={
+ 'text:style-name': 'Table_20_Contents'})
+ el1.text = node.astext()
+ raise nodes.SkipChildren()
+
+ def depart_description(self, node):
+ pass
+
+ def visit_paragraph(self, node):
+ self.in_paragraph = True
+ if self.in_header:
+ el = self.append_p('header')
+ elif self.in_footer:
+ el = self.append_p('footer')
+ else:
+ style_name = self.paragraph_style_stack[-1]
+ el = self.append_child(
+ 'text:p',
+ attrib={'text:style-name': style_name})
+ self.append_pending_ids(el)
+ self.set_current_element(el)
+
+ def depart_paragraph(self, node):
+ self.in_paragraph = False
+ self.set_to_parent()
+ if self.in_header:
+ self.header_content.append(self.current_element[-1])
+ self.current_element.remove(self.current_element[-1])
+ elif self.in_footer:
+ self.footer_content.append(self.current_element[-1])
+ self.current_element.remove(self.current_element[-1])
+
+ def visit_problematic(self, node):
+ pass
+
+ def depart_problematic(self, node):
+ pass
+
+ def visit_raw(self, node):
+ if 'format' in node.attributes:
+ formats = node.attributes['format']
+ formatlist = formats.split()
+ if 'odt' in formatlist:
+ rawstr = node.astext()
+ attrstr = ' '.join(
+ '%s="%s"' % (k, v, )
+ for k, v in list(CONTENT_NAMESPACE_ATTRIB.items()))
+ contentstr = '<stuff %s>%s</stuff>' % (attrstr, rawstr, )
+ contentstr = contentstr.encode("utf-8")
+ content = etree.fromstring(contentstr)
+ if len(content) > 0:
+ el1 = content[0]
+ if self.in_header:
+ pass
+ elif self.in_footer:
+ pass
+ else:
+ self.current_element.append(el1)
+ raise nodes.SkipChildren()
+
+ def depart_raw(self, node):
+ if self.in_header:
+ pass
+ elif self.in_footer:
+ pass
+ else:
+ pass
+
+ def visit_reference(self, node):
+ # text = node.astext()
+ if self.settings.create_links:
+ if 'refuri' in node:
+ href = node['refuri']
+ if (self.settings.cloak_email_addresses
+ and href.startswith('mailto:')):
+ href = self.cloak_mailto(href)
+ el = self.append_child('text:a', attrib={
+ 'xlink:href': '%s' % href,
+ 'xlink:type': 'simple',
+ })
+ self.set_current_element(el)
+ elif 'refid' in node:
+ if self.settings.create_links:
+ href = node['refid']
+ el = self.append_child('text:reference-ref', attrib={
+ 'text:ref-name': '%s' % href,
+ 'text:reference-format': 'text',
+ })
+ else:
+ self.document.reporter.warning(
+ 'References must have "refuri" or "refid" attribute.')
+ if (self.in_table_of_contents
+ and len(node.children) >= 1
+ and isinstance(node.children[0], docutils.nodes.generated)):
+ node.remove(node.children[0])
+
+ def depart_reference(self, node):
+ if self.settings.create_links:
+ if 'refuri' in node:
+ self.set_to_parent()
+
+ def visit_rubric(self, node):
+ style_name = self.rststyle('rubric')
+ classes = node.get('classes')
+ if classes:
+ class1 = classes[0]
+ if class1:
+ style_name = class1
+ el = SubElement(self.current_element, 'text:h', attrib={
+ # 'text:outline-level': '%d' % section_level,
+ # 'text:style-name': 'Heading_20_%d' % section_level,
+ 'text:style-name': style_name,
+ })
+ text = node.astext()
+ el.text = self.encode(text)
+
+ def depart_rubric(self, node):
+ pass
+
+ def visit_section(self, node, move_ids=1):
+ self.section_level += 1
+ self.section_count += 1
+ if self.settings.create_sections:
+ el = self.append_child('text:section', attrib={
+ 'text:name': 'Section%d' % self.section_count,
+ 'text:style-name': 'Sect%d' % self.section_level,
+ })
+ self.set_current_element(el)
+
+ def depart_section(self, node):
+ self.section_level -= 1
+ if self.settings.create_sections:
+ self.set_to_parent()
+
+ def visit_strong(self, node):
+ el = SubElement(self.current_element, 'text:span',
+ attrib={'text:style-name': self.rststyle('strong')})
+ self.set_current_element(el)
+
+ def depart_strong(self, node):
+ self.set_to_parent()
+
+ def visit_substitution_definition(self, node):
+ raise nodes.SkipChildren()
+
+ def depart_substitution_definition(self, node):
+ pass
+
+ def visit_system_message(self, node):
+ pass
+
+ def depart_system_message(self, node):
+ pass
+
+ def get_table_style(self, node):
+ table_style = None
+ table_name = None
+ str_classes = node.get('classes')
+ if str_classes is not None:
+ for str_class in str_classes:
+ if str_class.startswith(TABLESTYLEPREFIX):
+ table_name = str_class
+ break
+ if table_name is not None:
+ table_style = self.table_styles.get(table_name)
+ if table_style is None:
+ # If we can't find the table style, issue warning
+ # and use the default table style.
+ self.document.reporter.warning(
+ 'Can\'t find table style "%s". Using default.' % (
+ table_name, ))
+ table_name = TABLENAMEDEFAULT
+ table_style = self.table_styles.get(table_name)
+ if table_style is None:
+ # If we can't find the default table style, issue a warning
+ # and use a built-in default style.
+ self.document.reporter.warning(
+ 'Can\'t find default table style "%s". '
+ 'Using built-in default.' % (
+ table_name, ))
+ table_style = BUILTIN_DEFAULT_TABLE_STYLE
+ else:
+ table_name = TABLENAMEDEFAULT
+ table_style = self.table_styles.get(table_name)
+ if table_style is None:
+ # If we can't find the default table style, issue a warning
+ # and use a built-in default style.
+ self.document.reporter.warning(
+ 'Can\'t find default table style "%s". '
+ 'Using built-in default.' % (
+ table_name, ))
+ table_style = BUILTIN_DEFAULT_TABLE_STYLE
+ return table_style
+
+ def visit_table(self, node):
+ self.table_count += 1
+ table_style = self.get_table_style(node)
+ table_name = '%s%%d' % TABLESTYLEPREFIX
+ el1 = SubElement(self.automatic_styles, 'style:style', attrib={
+ 'style:name': self.rststyle(
+ '%s' % table_name, (self.table_count, )),
+ 'style:family': 'table',
+ }, nsdict=SNSD)
+ if table_style.backgroundcolor is None:
+ SubElement(el1, 'style:table-properties', attrib={
+ # 'style:width': '17.59cm',
+ # 'table:align': 'margins',
+ 'table:align': 'left',
+ 'fo:margin-top': '0in',
+ 'fo:margin-bottom': '0.10in',
+ }, nsdict=SNSD)
+ else:
+ SubElement(el1, 'style:table-properties', attrib={
+ # 'style:width': '17.59cm',
+ 'table:align': 'margins',
+ 'fo:margin-top': '0in',
+ 'fo:margin-bottom': '0.10in',
+ 'fo:background-color': table_style.backgroundcolor,
+ }, nsdict=SNSD)
+ # We use a single cell style for all cells in this table.
+ # That's probably not correct, but seems to work.
+ el2 = SubElement(self.automatic_styles, 'style:style', attrib={
+ 'style:name': self.rststyle(
+ '%s.%%c%%d' % table_name, (self.table_count, 'A', 1, )),
+ 'style:family': 'table-cell',
+ }, nsdict=SNSD)
+ thickness = self.settings.table_border_thickness
+ if thickness is None:
+ line_style1 = table_style.border
+ else:
+ line_style1 = '0.%03dcm solid #000000' % (thickness, )
+ SubElement(el2, 'style:table-cell-properties', attrib={
+ 'fo:padding': '0.049cm',
+ 'fo:border-left': line_style1,
+ 'fo:border-right': line_style1,
+ 'fo:border-top': line_style1,
+ 'fo:border-bottom': line_style1,
+ }, nsdict=SNSD)
+ title = None
+ for child in node.children:
+ if child.tagname == 'title':
+ title = child.astext()
+ break
+ if title is not None:
+ self.append_p('table-title', title)
+ else:
+ pass
+ el4 = SubElement(self.current_element, 'table:table', attrib={
+ 'table:name': self.rststyle(
+ '%s' % table_name, (self.table_count, )),
+ 'table:style-name': self.rststyle(
+ '%s' % table_name, (self.table_count, )),
+ })
+ self.set_current_element(el4)
+ self.current_table_style = el1
+ self.table_width = 0.0
+
+ def depart_table(self, node):
+ attribkey = add_ns('style:width', nsdict=SNSD)
+ attribval = '%.4fin' % (self.table_width, )
+ el1 = self.current_table_style
+ el2 = el1[0]
+ el2.attrib[attribkey] = attribval
+ self.set_to_parent()
+
+ def visit_tgroup(self, node):
+ self.column_count = ord('A') - 1
+
+ def depart_tgroup(self, node):
+ pass
+
+ def visit_colspec(self, node):
+ self.column_count += 1
+ colspec_name = self.rststyle(
+ '%s%%d.%%s' % TABLESTYLEPREFIX,
+ (self.table_count, chr(self.column_count), )
+ )
+ colwidth = node['colwidth'] / 12.0
+ el1 = SubElement(self.automatic_styles, 'style:style', attrib={
+ 'style:name': colspec_name,
+ 'style:family': 'table-column',
+ }, nsdict=SNSD)
+ SubElement(el1, 'style:table-column-properties',
+ attrib={'style:column-width': '%.4fin' % colwidth},
+ nsdict=SNSD)
+ self.append_child('table:table-column',
+ attrib={'table:style-name': colspec_name, })
+ self.table_width += colwidth
+
+ def depart_colspec(self, node):
+ pass
+
+ def visit_thead(self, node):
+ el = self.append_child('table:table-header-rows')
+ self.set_current_element(el)
+ self.in_thead = True
+ self.paragraph_style_stack.append('Table_20_Heading')
+
+ def depart_thead(self, node):
+ self.set_to_parent()
+ self.in_thead = False
+ self.paragraph_style_stack.pop()
+
+ def visit_row(self, node):
+ self.column_count = ord('A') - 1
+ el = self.append_child('table:table-row')
+ self.set_current_element(el)
+
+ def depart_row(self, node):
+ self.set_to_parent()
+
+ def visit_entry(self, node):
+ self.column_count += 1
+ cellspec_name = self.rststyle(
+ '%s%%d.%%c%%d' % TABLESTYLEPREFIX,
+ (self.table_count, 'A', 1, )
+ )
+ attrib = {
+ 'table:style-name': cellspec_name,
+ 'office:value-type': 'string',
+ }
+ morecols = node.get('morecols', 0)
+ if morecols > 0:
+ attrib['table:number-columns-spanned'] = '%d' % (morecols + 1,)
+ self.column_count += morecols
+ morerows = node.get('morerows', 0)
+ if morerows > 0:
+ attrib['table:number-rows-spanned'] = '%d' % (morerows + 1,)
+ el1 = self.append_child('table:table-cell', attrib=attrib)
+ self.set_current_element(el1)
+
+ def depart_entry(self, node):
+ self.set_to_parent()
+
+ def visit_tbody(self, node):
+ pass
+
+ def depart_tbody(self, node):
+ pass
+
+ def visit_target(self, node):
+ #
+ # I don't know how to implement targets in ODF.
+ # How do we create a target in oowriter? A cross-reference?
+ if ('refuri' not in node
+ and 'refid' not in node
+ and 'refname' not in node):
+ pass
+ else:
+ pass
+
+ def depart_target(self, node):
+ pass
+
+ def visit_title(self, node, move_ids=1, title_type='title'):
+ if isinstance(node.parent, docutils.nodes.section):
+ section_level = self.section_level
+ if section_level > 7:
+ self.document.reporter.warning(
+ 'Heading/section levels greater than 7 not supported.')
+ self.document.reporter.warning(
+ ' Reducing to heading level 7 for heading: "%s"' % (
+ node.astext(), ))
+ section_level = 7
+ el1 = self.append_child(
+ 'text:h', attrib={
+ 'text:outline-level': '%d' % section_level,
+ # 'text:style-name': 'Heading_20_%d' % section_level,
+ 'text:style-name': self.rststyle(
+ 'heading%d', (section_level, )),
+ })
+ self.append_pending_ids(el1)
+ self.set_current_element(el1)
+ elif isinstance(node.parent, docutils.nodes.document):
+ # text = self.settings.title
+ # else:
+ # text = node.astext()
+ el1 = SubElement(self.current_element, 'text:p', attrib={
+ 'text:style-name': self.rststyle(title_type),
+ })
+ self.append_pending_ids(el1)
+ text = node.astext()
+ self.title = text
+ self.found_doc_title = True
+ self.set_current_element(el1)
+
+ def depart_title(self, node):
+ if (isinstance(node.parent, docutils.nodes.section)
+ or isinstance(node.parent, docutils.nodes.document)):
+ self.set_to_parent()
+
+ def visit_subtitle(self, node, move_ids=1):
+ self.visit_title(node, move_ids, title_type='subtitle')
+
+ def depart_subtitle(self, node):
+ self.depart_title(node)
+
+ def visit_title_reference(self, node):
+ el = self.append_child('text:span', attrib={
+ 'text:style-name': self.rststyle('quotation')})
+ el.text = self.encode(node.astext())
+ raise nodes.SkipChildren()
+
+ def depart_title_reference(self, node):
+ pass
+
+ def generate_table_of_content_entry_template(self, el1):
+ for idx in range(1, 11):
+ el2 = SubElement(
+ el1,
+ 'text:table-of-content-entry-template',
+ attrib={
+ 'text:outline-level': "%d" % (idx, ),
+ 'text:style-name': self.rststyle('contents-%d' % (idx, )),
+ })
+ SubElement(el2, 'text:index-entry-chapter')
+ SubElement(el2, 'text:index-entry-text')
+ SubElement(el2, 'text:index-entry-tab-stop', attrib={
+ 'style:leader-char': ".",
+ 'style:type': "right",
+ })
+ SubElement(el2, 'text:index-entry-page-number')
+
+ def find_title_label(self, node, class_type, label_key):
+ label = ''
+ title_node = None
+ for child in node.children:
+ if isinstance(child, class_type):
+ title_node = child
+ break
+ if title_node is not None:
+ label = title_node.astext()
+ else:
+ label = self.language.labels[label_key]
+ return label
+
+ def visit_topic(self, node):
+ if 'classes' in node.attributes:
+ if 'contents' in node.attributes['classes']:
+ label = self.find_title_label(
+ node, docutils.nodes.title, 'contents')
+ if self.settings.generate_oowriter_toc:
+ el1 = self.append_child('text:table-of-content', attrib={
+ 'text:name': 'Table of Contents1',
+ 'text:protected': 'true',
+ 'text:style-name': 'Sect1',
+ })
+ el2 = SubElement(
+ el1,
+ 'text:table-of-content-source',
+ attrib={
+ 'text:outline-level': '10',
+ })
+ el3 = SubElement(el2, 'text:index-title-template', attrib={
+ 'text:style-name': 'Contents_20_Heading',
+ })
+ el3.text = label
+ self.generate_table_of_content_entry_template(el2)
+ el4 = SubElement(el1, 'text:index-body')
+ el5 = SubElement(el4, 'text:index-title')
+ el6 = SubElement(el5, 'text:p', attrib={
+ 'text:style-name': self.rststyle('contents-heading'),
+ })
+ el6.text = label
+ self.save_current_element = self.current_element
+ self.table_of_content_index_body = el4
+ self.set_current_element(el4)
+ else:
+ el = self.append_p('horizontalline')
+ el = self.append_p('centeredtextbody')
+ el1 = SubElement(
+ el, 'text:span',
+ attrib={'text:style-name': self.rststyle('strong')})
+ el1.text = label
+ self.in_table_of_contents = True
+ elif 'abstract' in node.attributes['classes']:
+ el = self.append_p('horizontalline')
+ el = self.append_p('centeredtextbody')
+ el1 = SubElement(
+ el, 'text:span',
+ attrib={'text:style-name': self.rststyle('strong')})
+ label = self.find_title_label(
+ node, docutils.nodes.title,
+ 'abstract')
+ el1.text = label
+ elif 'dedication' in node.attributes['classes']:
+ el = self.append_p('horizontalline')
+ el = self.append_p('centeredtextbody')
+ el1 = SubElement(
+ el, 'text:span',
+ attrib={'text:style-name': self.rststyle('strong')})
+ label = self.find_title_label(
+ node, docutils.nodes.title,
+ 'dedication')
+ el1.text = label
+
+ def depart_topic(self, node):
+ if 'classes' in node.attributes:
+ if 'contents' in node.attributes['classes']:
+ if self.settings.generate_oowriter_toc:
+ self.update_toc_page_numbers(
+ self.table_of_content_index_body)
+ self.set_current_element(self.save_current_element)
+ else:
+ self.append_p('horizontalline')
+ self.in_table_of_contents = False
+
+ def update_toc_page_numbers(self, el):
+ collection = []
+ self.update_toc_collect(el, 0, collection)
+ self.update_toc_add_numbers(collection)
+
+ def update_toc_collect(self, el, level, collection):
+ collection.append((level, el))
+ level += 1
+ for child_el in el:
+ if child_el.tag != 'text:index-body':
+ self.update_toc_collect(child_el, level, collection)
+
+ def update_toc_add_numbers(self, collection):
+ for level, el1 in collection:
+ if (el1.tag == 'text:p'
+ and el1.text != 'Table of Contents'):
+ el2 = SubElement(el1, 'text:tab')
+ el2.tail = '9999'
+
+ def visit_transition(self, node):
+ self.append_p('horizontalline')
+
+ def depart_transition(self, node):
+ pass
+
+ #
+ # Admonitions
+ #
+ def visit_warning(self, node):
+ self.generate_admonition(node, 'warning')
+
+ def depart_warning(self, node):
+ self.paragraph_style_stack.pop()
+
+ def visit_attention(self, node):
+ self.generate_admonition(node, 'attention')
+
+ depart_attention = depart_warning
+
+ def visit_caution(self, node):
+ self.generate_admonition(node, 'caution')
+
+ depart_caution = depart_warning
+
+ def visit_danger(self, node):
+ self.generate_admonition(node, 'danger')
+
+ depart_danger = depart_warning
+
+ def visit_error(self, node):
+ self.generate_admonition(node, 'error')
+
+ depart_error = depart_warning
+
+ def visit_hint(self, node):
+ self.generate_admonition(node, 'hint')
+
+ depart_hint = depart_warning
+
+ def visit_important(self, node):
+ self.generate_admonition(node, 'important')
+
+ depart_important = depart_warning
+
+ def visit_note(self, node):
+ self.generate_admonition(node, 'note')
+
+ depart_note = depart_warning
+
+ def visit_tip(self, node):
+ self.generate_admonition(node, 'tip')
+
+ depart_tip = depart_warning
+
+ def visit_admonition(self, node):
+ title = None
+ for child in node.children:
+ if child.tagname == 'title':
+ title = child.astext()
+ if title is None:
+ classes1 = node.get('classes')
+ if classes1:
+ title = classes1[0]
+ self.generate_admonition(node, 'generic', title)
+
+ depart_admonition = depart_warning
+
+ def generate_admonition(self, node, label, title=None):
+ if hasattr(self.language, 'labels'):
+ translated_label = self.language.labels.get(label, label)
+ else:
+ translated_label = label
+ el1 = SubElement(self.current_element, 'text:p', attrib={
+ 'text:style-name': self.rststyle(
+ 'admon-%s-hdr', (label, )),
+ })
+ if title:
+ el1.text = title
+ else:
+ el1.text = '%s!' % (translated_label.capitalize(), )
+ s1 = self.rststyle('admon-%s-body', (label, ))
+ self.paragraph_style_stack.append(s1)
+
+ #
+ # Roles (e.g. subscript, superscript, strong, ...
+ #
+ def visit_subscript(self, node):
+ el = self.append_child('text:span', attrib={
+ 'text:style-name': 'rststyle-subscript',
+ })
+ self.set_current_element(el)
+
+ def depart_subscript(self, node):
+ self.set_to_parent()
+
+ def visit_superscript(self, node):
+ el = self.append_child('text:span', attrib={
+ 'text:style-name': 'rststyle-superscript',
+ })
+ self.set_current_element(el)
+
+ def depart_superscript(self, node):
+ self.set_to_parent()
+
+ def visit_abbreviation(self, node):
+ pass
+
+ def depart_abbreviation(self, node):
+ pass
+
+ def visit_acronym(self, node):
+ pass
+
+ def depart_acronym(self, node):
+ pass
+
+ def visit_sidebar(self, node):
+ pass
+
+ def depart_sidebar(self, node):
+ pass
+
+
+# Use an own reader to modify transformations done.
+class Reader(standalone.Reader):
+
+ def get_transforms(self):
+ transforms = super().get_transforms()
+ if not self.settings.create_links:
+ transforms.remove(references.DanglingReferences)
+ return transforms
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/prepstyles.py b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/prepstyles.py
new file mode 100755
index 00000000..b59490f2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/prepstyles.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+
+# $Id: prepstyles.py 9386 2023-05-16 14:49:31Z milde $
+# Author: Dave Kuhlman <dkuhlman@rexx.com>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Adapt a word-processor-generated styles.odt for odtwriter use:
+
+Drop page size specifications from styles.xml in STYLE_FILE.odt.
+See https://docutils.sourceforge.io/docs/user/odt.html#page-size
+"""
+
+# Author: Michael Schutte <michi@uiae.at>
+
+from xml.etree import ElementTree as etree
+
+import sys
+import zipfile
+from tempfile import mkstemp
+import shutil
+import os
+
+NAMESPACES = {
+ "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
+ "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
+}
+
+
+def prepstyle(filename):
+
+ zin = zipfile.ZipFile(filename)
+ styles = zin.open("styles.xml")
+
+ root = None
+ # some extra effort to preserve namespace prefixes
+ for event, elem in etree.iterparse(styles, events=("start", "start-ns")):
+ if event == "start-ns":
+ etree.register_namespace(elem[0], elem[1])
+ elif event == "start":
+ if root is None:
+ root = elem
+
+ styles.close()
+
+ for el in root.findall(".//style:page-layout-properties",
+ namespaces=NAMESPACES):
+ for attr in list(el.attrib):
+ if attr.startswith("{%s}" % NAMESPACES["fo"]):
+ del el.attrib[attr]
+
+ tempname = mkstemp()
+ zout = zipfile.ZipFile(os.fdopen(tempname[0], "wb"), "w",
+ zipfile.ZIP_DEFLATED)
+
+ for item in zin.infolist():
+ if item.filename == "styles.xml":
+ zout.writestr(item, etree.tostring(root, encoding="UTF-8"))
+ else:
+ zout.writestr(item, zin.read(item.filename))
+
+ zout.close()
+ zin.close()
+ shutil.move(tempname[1], filename)
+
+
+def main():
+ args = sys.argv[1:]
+ if len(args) != 1 or args[0] in ('-h', '--help'):
+ print(__doc__, file=sys.stderr)
+ print("Usage: %s STYLE_FILE.odt\n" % sys.argv[0], file=sys.stderr)
+ sys.exit(1)
+ filename = args[0]
+ prepstyle(filename)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/pygmentsformatter.py b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/pygmentsformatter.py
new file mode 100644
index 00000000..7880651b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/pygmentsformatter.py
@@ -0,0 +1,109 @@
+# $Id: pygmentsformatter.py 9015 2022-03-03 22:15:00Z milde $
+# Author: Dave Kuhlman <dkuhlman@rexx.com>
+# Copyright: This module has been placed in the public domain.
+
+"""
+
+Additional support for Pygments formatter.
+
+"""
+
+
+import pygments
+import pygments.formatter
+
+
+class OdtPygmentsFormatter(pygments.formatter.Formatter):
+ def __init__(self, rststyle_function, escape_function):
+ pygments.formatter.Formatter.__init__(self)
+ self.rststyle_function = rststyle_function
+ self.escape_function = escape_function
+
+ def rststyle(self, name, parameters=()):
+ return self.rststyle_function(name, parameters)
+
+
+class OdtPygmentsProgFormatter(OdtPygmentsFormatter):
+ def format(self, tokensource, outfile):
+ tokenclass = pygments.token.Token
+ for ttype, value in tokensource:
+ value = self.escape_function(value)
+ if ttype == tokenclass.Keyword:
+ s2 = self.rststyle('codeblock-keyword')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ elif ttype == tokenclass.Literal.String:
+ s2 = self.rststyle('codeblock-string')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ elif ttype in (
+ tokenclass.Literal.Number.Integer,
+ tokenclass.Literal.Number.Integer.Long,
+ tokenclass.Literal.Number.Float,
+ tokenclass.Literal.Number.Hex,
+ tokenclass.Literal.Number.Oct,
+ tokenclass.Literal.Number,
+ ):
+ s2 = self.rststyle('codeblock-number')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ elif ttype == tokenclass.Operator:
+ s2 = self.rststyle('codeblock-operator')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ elif ttype == tokenclass.Comment:
+ s2 = self.rststyle('codeblock-comment')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ elif ttype == tokenclass.Name.Class:
+ s2 = self.rststyle('codeblock-classname')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ elif ttype == tokenclass.Name.Function:
+ s2 = self.rststyle('codeblock-functionname')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ elif ttype == tokenclass.Name:
+ s2 = self.rststyle('codeblock-name')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ else:
+ s1 = value
+ outfile.write(s1)
+
+
+class OdtPygmentsLaTeXFormatter(OdtPygmentsFormatter):
+ def format(self, tokensource, outfile):
+ tokenclass = pygments.token.Token
+ for ttype, value in tokensource:
+ value = self.escape_function(value)
+ if ttype == tokenclass.Keyword:
+ s2 = self.rststyle('codeblock-keyword')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ elif ttype in (tokenclass.Literal.String,
+ tokenclass.Literal.String.Backtick,
+ ):
+ s2 = self.rststyle('codeblock-string')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ elif ttype == tokenclass.Name.Attribute:
+ s2 = self.rststyle('codeblock-operator')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ elif ttype == tokenclass.Comment:
+ if value[-1] == '\n':
+ s2 = self.rststyle('codeblock-comment')
+ s1 = '<text:span text:style-name="%s">%s</text:span>\n' % \
+ (s2, value[:-1], )
+ else:
+ s2 = self.rststyle('codeblock-comment')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ elif ttype == tokenclass.Name.Builtin:
+ s2 = self.rststyle('codeblock-name')
+ s1 = '<text:span text:style-name="%s">%s</text:span>' % \
+ (s2, value, )
+ else:
+ s1 = value
+ outfile.write(s1)
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/styles.odt b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/styles.odt
new file mode 100644
index 00000000..e17b0072
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/odf_odt/styles.odt
Binary files differ
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/__init__.py
new file mode 100644
index 00000000..dfde2e47
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/__init__.py
@@ -0,0 +1,101 @@
+# $Id: __init__.py 9541 2024-02-17 10:37:13Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+PEP HTML Writer.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import os
+import os.path
+
+from docutils import frontend, nodes, utils
+from docutils.writers import html4css1
+
+
+class Writer(html4css1.Writer):
+
+ default_stylesheet = 'pep.css'
+
+ default_stylesheet_path = utils.relative_path(
+ os.path.join(os.getcwd(), 'dummy'),
+ os.path.join(os.path.dirname(__file__), default_stylesheet))
+
+ default_template = 'template.txt'
+
+ default_template_path = utils.relative_path(
+ os.path.join(os.getcwd(), 'dummy'),
+ os.path.join(os.path.dirname(__file__), default_template))
+
+ settings_spec = html4css1.Writer.settings_spec + (
+ 'PEP/HTML Writer Options',
+ 'For the PEP/HTML writer, the default value for the --stylesheet-path '
+ 'option is "%s", and the default value for --template is "%s". '
+ 'See HTML Writer Options above.'
+ % (default_stylesheet_path, default_template_path),
+ (('Python\'s home URL. Default is "https://www.python.org".',
+ ['--python-home'],
+ {'default': 'https://www.python.org', 'metavar': '<URL>'}),
+ ('Home URL prefix for PEPs. Default is "." (current directory).',
+ ['--pep-home'],
+ {'default': '.', 'metavar': '<URL>'}),
+ # For testing.
+ (frontend.SUPPRESS_HELP,
+ ['--no-random'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),))
+
+ settings_default_overrides = {'stylesheet_path': default_stylesheet_path,
+ 'template': default_template_path}
+ relative_path_settings = ('template',)
+ config_section = 'pep_html writer'
+ config_section_dependencies = ('writers', 'html writers',
+ 'html4css1 writer')
+
+ def __init__(self):
+ html4css1.Writer.__init__(self)
+ self.translator_class = HTMLTranslator
+
+ def interpolation_dict(self):
+ subs = html4css1.Writer.interpolation_dict(self)
+ settings = self.document.settings
+ pyhome = settings.python_home
+ subs['pyhome'] = pyhome
+ subs['pephome'] = settings.pep_home
+ if pyhome == '..':
+ subs['pepindex'] = '.'
+ else:
+ subs['pepindex'] = pyhome + '/dev/peps'
+ index = self.document.first_child_matching_class(nodes.field_list)
+ header = self.document[index]
+ self.pepnum = header[0][1].astext()
+ subs['pep'] = self.pepnum
+ if settings.no_random:
+ subs['banner'] = 0
+ else:
+ import random
+ subs['banner'] = random.randrange(64)
+ try:
+ subs['pepnum'] = '%04i' % int(self.pepnum)
+ except ValueError:
+ subs['pepnum'] = self.pepnum
+ self.title = header[1][1].astext()
+ subs['title'] = self.title
+ subs['body'] = ''.join(
+ self.body_pre_docinfo + self.docinfo + self.body)
+ return subs
+
+ def assemble_parts(self):
+ html4css1.Writer.assemble_parts(self)
+ self.parts['title'] = [self.title]
+ self.parts['pepnum'] = self.pepnum
+
+
+class HTMLTranslator(html4css1.HTMLTranslator):
+
+ def depart_field_list(self, node):
+ html4css1.HTMLTranslator.depart_field_list(self, node)
+ if 'rfc2822' in node['classes']:
+ self.body.append('<hr />\n')
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/pep.css b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/pep.css
new file mode 100644
index 00000000..5231cd1a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/pep.css
@@ -0,0 +1,344 @@
+/*
+:Author: David Goodger
+:Contact: goodger@python.org
+:date: $Date: 2022-01-29 23:26:10 +0100 (Sa, 29. Jän 2022) $
+:version: $Revision: 8995 $
+:copyright: This stylesheet has been placed in the public domain.
+
+Default cascading style sheet for the PEP HTML output of Docutils.
+*/
+
+/* "! important" is used here to override other ``margin-top`` and
+ ``margin-bottom`` styles that are later in the stylesheet or
+ more specific. See http://www.w3.org/TR/CSS1#the-cascade */
+.first {
+ margin-top: 0 ! important }
+
+.last, .with-subtitle {
+ margin-bottom: 0 ! important }
+
+.hidden {
+ display: none }
+
+.navigation {
+ width: 100% ;
+ background: #99ccff ;
+ margin-top: 0px ;
+ margin-bottom: 0px }
+
+.navigation .navicon {
+ width: 150px ;
+ height: 35px }
+
+.navigation .textlinks {
+ padding-left: 1em ;
+ text-align: left }
+
+.navigation td, .navigation th {
+ padding-left: 0em ;
+ padding-right: 0em ;
+ vertical-align: middle }
+
+.rfc2822 {
+ margin-top: 0.5em ;
+ margin-left: 0.5em ;
+ margin-right: 0.5em ;
+ margin-bottom: 0em }
+
+.rfc2822 td {
+ text-align: left }
+
+.rfc2822 th.field-name {
+ text-align: right ;
+ font-family: sans-serif ;
+ padding-right: 0.5em ;
+ font-weight: bold ;
+ margin-bottom: 0em }
+
+a.toc-backref {
+ text-decoration: none ;
+ color: black }
+
+blockquote.epigraph {
+ margin: 2em 5em ; }
+
+body {
+ margin: 0px ;
+ margin-bottom: 1em ;
+ padding: 0px }
+
+dl.docutils dd {
+ margin-bottom: 0.5em }
+
+div.section {
+ margin-left: 1em ;
+ margin-right: 1em ;
+ margin-bottom: 1.5em }
+
+div.section div.section {
+ margin-left: 0em ;
+ margin-right: 0em ;
+ margin-top: 1.5em }
+
+div.abstract {
+ margin: 2em 5em }
+
+div.abstract p.topic-title {
+ font-weight: bold ;
+ text-align: center }
+
+div.admonition, div.attention, div.caution, div.danger, div.error,
+div.hint, div.important, div.note, div.tip, div.warning {
+ margin: 2em ;
+ border: medium outset ;
+ padding: 1em }
+
+div.admonition p.admonition-title, div.hint p.admonition-title,
+div.important p.admonition-title, div.note p.admonition-title,
+div.tip p.admonition-title {
+ font-weight: bold ;
+ font-family: sans-serif }
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title {
+ color: red ;
+ font-weight: bold ;
+ font-family: sans-serif }
+
+/* Uncomment (and remove this text!) to get reduced vertical space in
+ compound paragraphs.
+div.compound .compound-first, div.compound .compound-middle {
+ margin-bottom: 0.5em }
+
+div.compound .compound-last, div.compound .compound-middle {
+ margin-top: 0.5em }
+*/
+
+div.dedication {
+ margin: 2em 5em ;
+ text-align: center ;
+ font-style: italic }
+
+div.dedication p.topic-title {
+ font-weight: bold ;
+ font-style: normal }
+
+div.figure {
+ margin-left: 2em ;
+ margin-right: 2em }
+
+div.footer, div.header {
+ clear: both;
+ font-size: smaller }
+
+div.footer {
+ margin-left: 1em ;
+ margin-right: 1em }
+
+div.line-block {
+ display: block ;
+ margin-top: 1em ;
+ margin-bottom: 1em }
+
+div.line-block div.line-block {
+ margin-top: 0 ;
+ margin-bottom: 0 ;
+ margin-left: 1.5em }
+
+div.sidebar {
+ margin-left: 1em ;
+ border: medium outset ;
+ padding: 1em ;
+ background-color: #ffffee ;
+ width: 40% ;
+ float: right ;
+ clear: right }
+
+div.sidebar p.rubric {
+ font-family: sans-serif ;
+ font-size: medium }
+
+div.system-messages {
+ margin: 5em }
+
+div.system-messages h1 {
+ color: red }
+
+div.system-message {
+ border: medium outset ;
+ padding: 1em }
+
+div.system-message p.system-message-title {
+ color: red ;
+ font-weight: bold }
+
+div.topic {
+ margin: 2em }
+
+h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
+h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
+ margin-top: 0.4em }
+
+h1 {
+ font-family: sans-serif ;
+ font-size: large }
+
+h2 {
+ font-family: sans-serif ;
+ font-size: medium }
+
+h3 {
+ font-family: sans-serif ;
+ font-size: small }
+
+h4 {
+ font-family: sans-serif ;
+ font-style: italic ;
+ font-size: small }
+
+h5 {
+ font-family: sans-serif;
+ font-size: x-small }
+
+h6 {
+ font-family: sans-serif;
+ font-style: italic ;
+ font-size: x-small }
+
+hr.docutils {
+ width: 75% }
+
+img.align-left {
+ clear: left }
+
+img.align-right {
+ clear: right }
+
+img.borderless {
+ border: 0 }
+
+ol.simple, ul.simple {
+ margin-bottom: 1em }
+
+ol.arabic {
+ list-style: decimal }
+
+ol.loweralpha {
+ list-style: lower-alpha }
+
+ol.upperalpha {
+ list-style: upper-alpha }
+
+ol.lowerroman {
+ list-style: lower-roman }
+
+ol.upperroman {
+ list-style: upper-roman }
+
+p.attribution {
+ text-align: right ;
+ margin-left: 50% }
+
+p.caption {
+ font-style: italic }
+
+p.credits {
+ font-style: italic ;
+ font-size: smaller }
+
+p.label {
+ white-space: nowrap }
+
+p.rubric {
+ font-weight: bold ;
+ font-size: larger ;
+ color: maroon ;
+ text-align: center }
+
+p.sidebar-title {
+ font-family: sans-serif ;
+ font-weight: bold ;
+ font-size: larger }
+
+p.sidebar-subtitle {
+ font-family: sans-serif ;
+ font-weight: bold }
+
+p.topic-title {
+ font-family: sans-serif ;
+ font-weight: bold }
+
+pre.address {
+ margin-bottom: 0 ;
+ margin-top: 0 ;
+ font-family: serif ;
+ font-size: 100% }
+
+pre.literal-block, pre.doctest-block {
+ margin-left: 2em ;
+ margin-right: 2em }
+
+span.classifier {
+ font-family: sans-serif ;
+ font-style: oblique }
+
+span.classifier-delimiter {
+ font-family: sans-serif ;
+ font-weight: bold }
+
+span.interpreted {
+ font-family: sans-serif }
+
+span.option {
+ white-space: nowrap }
+
+span.option-argument {
+ font-style: italic }
+
+span.pre {
+ white-space: pre }
+
+span.problematic {
+ color: red }
+
+span.section-subtitle {
+ /* font-size relative to parent (h1..h6 element) */
+ font-size: 80% }
+
+table.citation {
+ border-left: solid 1px gray;
+ margin-left: 1px }
+
+table.docinfo {
+ margin: 2em 4em }
+
+table.docutils {
+ margin-top: 0.5em ;
+ margin-bottom: 0.5em }
+
+table.footnote {
+ border-left: solid 1px black;
+ margin-left: 1px }
+
+table.docutils td, table.docutils th,
+table.docinfo td, table.docinfo th {
+ padding-left: 0.5em ;
+ padding-right: 0.5em ;
+ vertical-align: top }
+
+td.num {
+ text-align: right }
+
+th.field-name {
+ font-weight: bold ;
+ text-align: left ;
+ white-space: nowrap ;
+ padding-left: 0 }
+
+h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
+h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
+ font-size: 100% }
+
+ul.auto-toc {
+ list-style-type: none }
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/template.txt b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/template.txt
new file mode 100644
index 00000000..e8cd351c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/pep_html/template.txt
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="%(encoding)s"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<!--
+This HTML is auto-generated. DO NOT EDIT THIS FILE! If you are writing a new
+PEP, see http://peps.python.org/pep-0001 for instructions and links
+to templates. DO NOT USE THIS HTML FILE AS YOUR TEMPLATE!
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
+ <meta name="generator" content="Docutils %(version)s: https://docutils.sourceforge.io/" />
+ <title>PEP %(pep)s - %(title)s</title>
+ %(stylesheet)s
+</head>
+<body bgcolor="white">
+<div class="header">
+<strong>Python Enhancement Proposals</strong>
+| <a href="%(pyhome)s/">Python</a>
+&raquo; <a href="https://peps.python.org/pep-0000/">PEP Index</a>
+&raquo; PEP %(pep)s &ndash; %(title)s
+<hr class="header"/>
+</div>
+<div class="document">
+%(body)s
+%(body_suffix)s
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/pseudoxml.py b/.venv/lib/python3.12/site-packages/docutils/writers/pseudoxml.py
new file mode 100644
index 00000000..0e238a88
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/pseudoxml.py
@@ -0,0 +1,40 @@
+# $Id: pseudoxml.py 9043 2022-03-11 12:09:16Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Simple internal document tree Writer, writes indented pseudo-XML.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from docutils import writers, frontend
+
+
+class Writer(writers.Writer):
+
+ supported = ('pseudoxml', 'pprint', 'pformat')
+ """Formats this writer supports."""
+
+ settings_spec = (
+ '"Docutils pseudo-XML" Writer Options',
+ None,
+ (('Pretty-print <#text> nodes.',
+ ['--detailed'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ )
+ )
+
+ config_section = 'pseudoxml writer'
+ config_section_dependencies = ('writers',)
+
+ output = None
+ """Final translated form of `document`."""
+
+ def translate(self):
+ self.output = self.document.pformat()
+
+ def supports(self, format):
+ """This writer supports all format-specific elements."""
+ return True
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/__init__.py
new file mode 100644
index 00000000..7014de33
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/__init__.py
@@ -0,0 +1,353 @@
+# $Id: __init__.py 9542 2024-02-17 10:37:23Z milde $
+# Authors: Chris Liechti <cliechti@gmx.net>;
+# David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+S5/HTML Slideshow Writer.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import sys
+import os
+import re
+import docutils
+from docutils import frontend, nodes, utils
+from docutils.writers import html4css1
+
+themes_dir_path = utils.relative_path(
+ os.path.join(os.getcwd(), 'dummy'),
+ os.path.join(os.path.dirname(__file__), 'themes'))
+
+
+def find_theme(name):
+ # Where else to look for a theme?
+ # Check working dir? Destination dir? Config dir? Plugins dir?
+ path = os.path.join(themes_dir_path, name)
+ if not os.path.isdir(path):
+ raise docutils.ApplicationError(
+ 'Theme directory not found: %r (path: %r)' % (name, path))
+ return path
+
+
+class Writer(html4css1.Writer):
+
+ settings_spec = html4css1.Writer.settings_spec + (
+ 'S5 Slideshow Specific Options',
+ 'For the S5/HTML writer, the --no-toc-backlinks option '
+ '(defined in General Docutils Options above) is the default, '
+ 'and should not be changed.',
+ (('Specify an installed S5 theme by name. Overrides --theme-url. '
+ 'The default theme name is "default". The theme files will be '
+ 'copied into a "ui/<theme>" directory, in the same directory as the '
+ 'destination file (output HTML). Note that existing theme files '
+ 'will not be overwritten (unless --overwrite-theme-files is used).',
+ ['--theme'],
+ {'default': 'default', 'metavar': '<name>',
+ 'overrides': 'theme_url'}),
+ ('Specify an S5 theme URL. The destination file (output HTML) will '
+ 'link to this theme; nothing will be copied. Overrides --theme.',
+ ['--theme-url'],
+ {'metavar': '<URL>', 'overrides': 'theme'}),
+ ('Allow existing theme files in the ``ui/<theme>`` directory to be '
+ 'overwritten. The default is not to overwrite theme files.',
+ ['--overwrite-theme-files'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ ('Keep existing theme files in the ``ui/<theme>`` directory; do not '
+ 'overwrite any. This is the default.',
+ ['--keep-theme-files'],
+ {'dest': 'overwrite_theme_files', 'action': 'store_false'}),
+ ('Set the initial view mode to "slideshow" [default] or "outline".',
+ ['--view-mode'],
+ {'choices': ['slideshow', 'outline'], 'default': 'slideshow',
+ 'metavar': '<mode>'}),
+ ('Normally hide the presentation controls in slideshow mode. '
+ 'This is the default.',
+ ['--hidden-controls'],
+ {'action': 'store_true', 'default': True,
+ 'validator': frontend.validate_boolean}),
+ ('Always show the presentation controls in slideshow mode. '
+ 'The default is to hide the controls.',
+ ['--visible-controls'],
+ {'dest': 'hidden_controls', 'action': 'store_false'}),
+ ('Enable the current slide indicator ("1 / 15"). '
+ 'The default is to disable it.',
+ ['--current-slide'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ ('Disable the current slide indicator. This is the default.',
+ ['--no-current-slide'],
+ {'dest': 'current_slide', 'action': 'store_false'}),))
+
+ settings_default_overrides = {'toc_backlinks': 0}
+
+ config_section = 's5_html writer'
+ config_section_dependencies = ('writers', 'html writers',
+ 'html4css1 writer')
+
+ def __init__(self):
+ html4css1.Writer.__init__(self)
+ self.translator_class = S5HTMLTranslator
+
+
+class S5HTMLTranslator(html4css1.HTMLTranslator):
+
+ s5_stylesheet_template = """\
+<!-- configuration parameters -->
+<meta name="defaultView" content="%(view_mode)s" />
+<meta name="controlVis" content="%(control_visibility)s" />
+<!-- style sheet links -->
+<script src="%(path)s/slides.js" type="text/javascript"></script>
+<link rel="stylesheet" href="%(path)s/slides.css"
+ type="text/css" media="projection" id="slideProj" />
+<link rel="stylesheet" href="%(path)s/outline.css"
+ type="text/css" media="screen" id="outlineStyle" />
+<link rel="stylesheet" href="%(path)s/print.css"
+ type="text/css" media="print" id="slidePrint" />
+<link rel="stylesheet" href="%(path)s/opera.css"
+ type="text/css" media="projection" id="operaFix" />\n"""
+ # The script element must go in front of the link elements to
+ # avoid a flash of unstyled content (FOUC), reproducible with
+ # Firefox.
+
+ disable_current_slide = """
+<style type="text/css">
+#currentSlide {display: none;}
+</style>\n"""
+
+ layout_template = """\
+<div class="layout">
+<div id="controls"></div>
+<div id="currentSlide"></div>
+<div id="header">
+%(header)s
+</div>
+<div id="footer">
+%(title)s%(footer)s
+</div>
+</div>\n"""
+# <div class="topleft"></div>
+# <div class="topright"></div>
+# <div class="bottomleft"></div>
+# <div class="bottomright"></div>
+
+ default_theme = 'default'
+ """Name of the default theme."""
+
+ base_theme_file = '__base__'
+ """Name of the file containing the name of the base theme."""
+
+ direct_theme_files = (
+ 'slides.css', 'outline.css', 'print.css', 'opera.css', 'slides.js')
+ """Names of theme files directly linked to in the output HTML"""
+
+ indirect_theme_files = (
+ 's5-core.css', 'framing.css', 'pretty.css')
+ """Names of files used indirectly; imported or used by files in
+ `direct_theme_files`."""
+
+ required_theme_files = indirect_theme_files + direct_theme_files
+ """Names of mandatory theme files."""
+
+ def __init__(self, *args):
+ html4css1.HTMLTranslator.__init__(self, *args)
+ # insert S5-specific stylesheet and script stuff:
+ self.theme_file_path = None
+ try:
+ self.setup_theme()
+ except docutils.ApplicationError as e:
+ self.document.reporter.warning(e)
+ view_mode = self.document.settings.view_mode
+ control_visibility = ('visible', 'hidden')[self.document.settings
+ .hidden_controls]
+ self.stylesheet.append(self.s5_stylesheet_template
+ % {'path': self.theme_file_path,
+ 'view_mode': view_mode,
+ 'control_visibility': control_visibility})
+ if not self.document.settings.current_slide:
+ self.stylesheet.append(self.disable_current_slide)
+ self.meta.append('<meta name="version" content="S5 1.1" />\n')
+ self.s5_footer = []
+ self.s5_header = []
+ self.section_count = 0
+ self.theme_files_copied = None
+
+ def setup_theme(self):
+ if self.document.settings.theme:
+ self.copy_theme()
+ elif self.document.settings.theme_url:
+ self.theme_file_path = self.document.settings.theme_url
+ else:
+ raise docutils.ApplicationError(
+ 'No theme specified for S5/HTML writer.')
+
+ def copy_theme(self):
+ """
+ Locate & copy theme files.
+
+ A theme may be explicitly based on another theme via a '__base__'
+ file. The default base theme is 'default'. Files are accumulated
+ from the specified theme, any base themes, and 'default'.
+ """
+ settings = self.document.settings
+ path = find_theme(settings.theme)
+ theme_paths = [path]
+ self.theme_files_copied = {}
+ required_files_copied = {}
+ # This is a link (URL) in HTML, so we use "/", not os.sep:
+ self.theme_file_path = 'ui/%s' % settings.theme
+ if not settings.output:
+ raise docutils.ApplicationError(
+ 'Output path not specified, you may need to copy'
+ ' the S5 theme files "by hand" or set the "--output" option.')
+ dest = os.path.join(
+ os.path.dirname(settings.output), 'ui', settings.theme)
+ if not os.path.isdir(dest):
+ os.makedirs(dest)
+ default = False
+ while path:
+ for f in os.listdir(path): # copy all files from each theme
+ if f == self.base_theme_file:
+ continue # ... except the "__base__" file
+ if (self.copy_file(f, path, dest)
+ and f in self.required_theme_files):
+ required_files_copied[f] = True
+ if default:
+ break # "default" theme has no base theme
+ # Find the "__base__" file in theme directory:
+ base_theme_file = os.path.join(path, self.base_theme_file)
+ # If it exists, read it and record the theme path:
+ if os.path.isfile(base_theme_file):
+ with open(base_theme_file, encoding='utf-8') as f:
+ lines = f.readlines()
+ for line in lines:
+ line = line.strip()
+ if line and not line.startswith('#'):
+ path = find_theme(line)
+ if path in theme_paths: # check for duplicates/cycles
+ path = None # if found, use default base
+ else:
+ theme_paths.append(path)
+ break
+ else: # no theme name found
+ path = None # use default base
+ else: # no base theme file found
+ path = None # use default base
+ if not path:
+ path = find_theme(self.default_theme)
+ theme_paths.append(path)
+ default = True
+ if len(required_files_copied) != len(self.required_theme_files):
+ # Some required files weren't found & couldn't be copied.
+ required = list(self.required_theme_files)
+ for f in required_files_copied.keys():
+ required.remove(f)
+ raise docutils.ApplicationError(
+ 'Theme files not found: %s'
+ % ', '.join('%r' % f for f in required))
+
+ files_to_skip_pattern = re.compile(r'~$|\.bak$|#$|\.cvsignore$')
+
+ def copy_file(self, name, source_dir, dest_dir):
+ """
+ Copy file `name` from `source_dir` to `dest_dir`.
+ Return True if the file exists in either `source_dir` or `dest_dir`.
+ """
+ source = os.path.join(source_dir, name)
+ dest = os.path.join(dest_dir, name)
+ if dest in self.theme_files_copied:
+ return True
+ else:
+ self.theme_files_copied[dest] = True
+ if os.path.isfile(source):
+ if self.files_to_skip_pattern.search(source):
+ return None
+ settings = self.document.settings
+ if os.path.exists(dest) and not settings.overwrite_theme_files:
+ settings.record_dependencies.add(dest)
+ else:
+ with open(source, 'rb') as src_file:
+ src_data = src_file.read()
+ with open(dest, 'wb') as dest_file:
+ dest_dir = dest_dir.replace(os.sep, '/')
+ dest_file.write(src_data.replace(
+ b'ui/default',
+ dest_dir[dest_dir.rfind('ui/'):].encode(
+ sys.getfilesystemencoding())))
+ settings.record_dependencies.add(source)
+ return True
+ if os.path.isfile(dest):
+ return True
+
+ def depart_document(self, node):
+ self.head_prefix.extend([self.doctype,
+ self.head_prefix_template %
+ {'lang': self.settings.language_code}])
+ self.html_prolog.append(self.doctype)
+ self.head = self.meta[:] + self.head
+ if self.math_header:
+ if self.math_output == 'mathjax':
+ self.head.extend(self.math_header)
+ else:
+ self.stylesheet.extend(self.math_header)
+ # skip content-type meta tag with interpolated charset value:
+ self.html_head.extend(self.head[1:])
+ self.fragment.extend(self.body)
+ # special S5 code up to the next comment line
+ header = ''.join(self.s5_header)
+ footer = ''.join(self.s5_footer)
+ title = ''.join(self.html_title).replace('<h1 class="title">', '<h1>')
+ layout = self.layout_template % {'header': header,
+ 'title': title,
+ 'footer': footer}
+ self.body_prefix.extend(layout)
+ self.body_prefix.append('<div class="presentation">\n')
+ self.body_prefix.append(
+ self.starttag({'classes': ['slide'], 'ids': ['slide0']}, 'div'))
+ if not self.section_count:
+ self.body.append('</div>\n')
+ #
+ self.body_suffix.insert(0, '</div>\n')
+ self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
+ + self.docinfo + self.body
+ + self.body_suffix[:-1])
+
+ def depart_footer(self, node):
+ start = self.context.pop()
+ self.s5_footer.append('<h2>')
+ self.s5_footer.extend(self.body[start:])
+ self.s5_footer.append('</h2>')
+ del self.body[start:]
+
+ def depart_header(self, node):
+ start = self.context.pop()
+ header = ['<div id="header">\n']
+ header.extend(self.body[start:])
+ header.append('\n</div>\n')
+ del self.body[start:]
+ self.s5_header.extend(header)
+
+ def visit_section(self, node):
+ if not self.section_count:
+ self.body.append('\n</div>\n')
+ self.section_count += 1
+ self.section_level += 1
+ if self.section_level > 1:
+ # dummy for matching div's
+ self.body.append(self.starttag(node, 'div', CLASS='section'))
+ else:
+ self.body.append(self.starttag(node, 'div', CLASS='slide'))
+
+ def visit_subtitle(self, node):
+ if isinstance(node.parent, nodes.section):
+ level = self.section_level + self.initial_header_level - 1
+ if level == 1:
+ level = 2
+ tag = 'h%s' % level
+ self.body.append(self.starttag(node, tag, ''))
+ self.context.append('</%s>\n' % tag)
+ else:
+ html4css1.HTMLTranslator.visit_subtitle(self, node)
+
+ def visit_title(self, node):
+ html4css1.HTMLTranslator.visit_title(self, node)
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/README.txt b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/README.txt
new file mode 100644
index 00000000..605d08f5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/README.txt
@@ -0,0 +1,6 @@
+Except where otherwise noted, all files in this
+directory have been released into the Public Domain.
+
+These files are based on files from S5 1.1, released into the Public
+Domain by Eric Meyer. For further details, please see
+http://www.meyerweb.com/eric/tools/s5/credits.html.
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/__base__ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/__base__
new file mode 100644
index 00000000..f08be9ad
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/__base__
@@ -0,0 +1,2 @@
+# base theme of this theme:
+big-white
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/framing.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/framing.css
new file mode 100644
index 00000000..a945abbc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/framing.css
@@ -0,0 +1,25 @@
+/* The following styles size, place, and layer the slide components.
+ Edit these if you want to change the overall slide layout.
+ The commented lines can be uncommented (and modified, if necessary)
+ to help you with the rearrangement process. */
+
+/* target = 1024x768 */
+
+div#header, div#footer, .slide {width: 100%; top: 0; left: 0;}
+div#header {top: 0; z-index: 1;}
+div#footer {display:none;}
+.slide {top: 0; width: 92%; padding: 0.1em 4% 4%; z-index: 2;}
+/* list-style: none;} */
+div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0;}
+#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em;
+ z-index: 10;}
+html>body #currentSlide {position: fixed;}
+
+/*
+div#header {background: #FCC;}
+div#footer {background: #CCF;}
+div#controls {background: #BBD;}
+div#currentSlide {background: #FFC;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/pretty.css
new file mode 100644
index 00000000..85f07cf0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-black/pretty.css
@@ -0,0 +1,109 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: black; color: white;}
+:link, :visited {text-decoration: none; color: cyan;}
+#controls :active {color: #888 !important;}
+#controls :focus {outline: 1px dotted #CCC;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+.slide {font-size: 3em; font-family: sans-serif; font-weight: bold;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font-size: 120%;}
+.slide h2 {font-size: 110%;}
+.slide h3 {font-size: 105%;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+ width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+ border: none; color: #888; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+ background: black; color: #CCC;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #AAA;
+ font-family: sans-serif; font-weight: bold;}
+
+#slide0 h1 {position: static; margin: 0 0 0.5em; padding-top: 0.3em; top: 0;
+ font-size: 150%; white-space: normal; background: transparent;}
+#slide0 h2 {font: 110%; font-style: italic; color: gray;}
+#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #FCC;}
+
+.incremental, .incremental *, .incremental *:after {
+ color: black; visibility: visible; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: lime;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-size: 150%;}
+.big {font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 0.5em 0 0.5em 1em;}
+
+div.sidebar {background-color: black;}
+
+pre.literal-block, pre.doctest-block {background-color: black;}
+
+tt.docutils {background-color: black;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/framing.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/framing.css
new file mode 100644
index 00000000..45f123f3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/framing.css
@@ -0,0 +1,24 @@
+/* This file has been placed in the public domain. */
+/* The following styles size, place, and layer the slide components.
+ Edit these if you want to change the overall slide layout.
+ The commented lines can be uncommented (and modified, if necessary)
+ to help you with the rearrangement process. */
+
+/* target = 1024x768 */
+
+div#header, div#footer, .slide {width: 100%; top: 0; left: 0;}
+div#footer {display:none;}
+.slide {top: 0; width: 92%; padding: 0.25em 4% 4%; z-index: 2;}
+div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0;}
+#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em;
+ z-index: 10;}
+html>body #currentSlide {position: fixed;}
+
+/*
+div#header {background: #FCC;}
+div#footer {background: #CCF;}
+div#controls {background: #BBD;}
+div#currentSlide {background: #FFC;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/pretty.css
new file mode 100644
index 00000000..68fe863a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/big-white/pretty.css
@@ -0,0 +1,107 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: white; color: black;}
+:link, :visited {text-decoration: none; color: #00C;}
+#controls :active {color: #88A !important;}
+#controls :focus {outline: 1px dotted #227;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+.slide {font-size: 3em; font-family: sans-serif; font-weight: bold;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font-size: 120%;}
+.slide h2 {font-size: 110%;}
+.slide h3 {font-size: 105%;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+ width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+ border: none; color: #005; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+ background: #DDD; color: #227;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #444;
+ font-family: sans-serif; font-weight: bold;}
+
+#slide0 h1 {position: static; margin: 0 0 0.5em; padding-top: 0.3em; top: 0;
+ font-size: 150%; white-space: normal; background: transparent;}
+#slide0 h2 {font: 110%; font-style: italic; color: gray;}
+#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #77B;}
+
+.incremental, .incremental *, .incremental *:after {
+ color: white; visibility: visible; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: green;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-size: 150%;}
+.big {font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 0.5em 0 0.5em 1em;}
+
+pre.literal-block, pre.doctest-block {background-color: white;}
+
+tt.docutils {background-color: white;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/framing.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/framing.css
new file mode 100644
index 00000000..b19b1f04
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/framing.css
@@ -0,0 +1,25 @@
+/* This file has been placed in the public domain. */
+/* The following styles size, place, and layer the slide components.
+ Edit these if you want to change the overall slide layout.
+ The commented lines can be uncommented (and modified, if necessary)
+ to help you with the rearrangement process. */
+
+/* target = 1024x768 */
+
+div#header, div#footer, .slide {width: 100%; top: 0; left: 0;}
+div#header {position: fixed; top: 0; height: 3em; z-index: 1;}
+div#footer {top: auto; bottom: 0; height: 2.5em; z-index: 5;}
+.slide {top: 0; width: 92%; padding: 2.5em 4% 4%; z-index: 2;}
+div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0;}
+#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em;
+ z-index: 10;}
+html>body #currentSlide {position: fixed;}
+
+/*
+div#header {background: #FCC;}
+div#footer {background: #CCF;}
+div#controls {background: #BBD;}
+div#currentSlide {background: #FFC;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/opera.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/opera.css
new file mode 100644
index 00000000..c9d1148b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/opera.css
@@ -0,0 +1,8 @@
+/* This file has been placed in the public domain. */
+/* DO NOT CHANGE THESE unless you really want to break Opera Show */
+.slide {
+ visibility: visible !important;
+ position: static !important;
+ page-break-before: always;
+}
+#slide0 {page-break-before: avoid;}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/outline.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/outline.css
new file mode 100644
index 00000000..fa767e22
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/outline.css
@@ -0,0 +1,16 @@
+/* This file has been placed in the public domain. */
+/* Don't change this unless you want the layout stuff to show up in the
+ outline view! */
+
+.layout div, #footer *, #controlForm * {display: none;}
+#footer, #controls, #controlForm, #navLinks, #toggle {
+ display: block; visibility: visible; margin: 0; padding: 0;}
+#toggle {float: right; padding: 0.5em;}
+html>body #toggle {position: fixed; top: 0; right: 0;}
+
+/* making the outline look pretty-ish */
+
+#slide0 h1, #slide0 h2, #slide0 h3, #slide0 h4 {border: none; margin: 0;}
+#toggle {border: 1px solid; border-width: 0 0 1px 1px; background: #FFF;}
+
+.outline {display: inline ! important;}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/pretty.css
new file mode 100644
index 00000000..7d48fff5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/pretty.css
@@ -0,0 +1,120 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: white; color: black;}
+/* Replace the background style above with the style below (and again for
+ div#header) for a graphic: */
+/* background: white url(bodybg.gif) -16px 0 no-repeat; */
+:link, :visited {text-decoration: none; color: #00C;}
+#controls :active {color: #88A !important;}
+#controls :focus {outline: 1px dotted #227;}
+h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+div#header, div#footer {background: #005; color: #AAB; font-family: sans-serif;}
+/* background: #005 url(bodybg.gif) -16px 0 no-repeat; */
+div#footer {font-size: 0.5em; font-weight: bold; padding: 1em 0;}
+#footer h1 {display: block; padding: 0 1em;}
+#footer h2 {display: block; padding: 0.8em 1em 0;}
+
+.slide {font-size: 1.2em;}
+.slide h1 {position: absolute; top: 0.45em; z-index: 1;
+ margin: 0; padding-left: 0.7em; white-space: nowrap;
+ font: bold 150% sans-serif; color: #DDE; background: #005;}
+.slide h2 {font: bold 120%/1em sans-serif; padding-top: 0.5em;}
+.slide h3 {font: bold 100% sans-serif; padding-top: 0.5em;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+ width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+ background: #005; border: none; color: #779; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+ background: #DDD; color: #227;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #449;
+ font-family: sans-serif; font-weight: bold;}
+
+#slide0 {padding-top: 1.5em}
+#slide0 h1 {position: static; margin: 1em 0 0; padding: 0; color: #000;
+ font: bold 2em sans-serif; white-space: normal; background: transparent;}
+#slide0 h2 {font: bold italic 1em sans-serif; margin: 0.25em;}
+#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #77B;}
+
+.incremental, .incremental *, .incremental *:after {visibility: visible;
+ color: white; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: green;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;}
+.big {font-family: sans-serif; font-weight: bold; font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 1em 0 0.5em 2em;}
+
+pre.literal-block, pre.doctest-block {background-color: white;}
+
+tt.docutils {background-color: white;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/print.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/print.css
new file mode 100644
index 00000000..9d057cc8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/print.css
@@ -0,0 +1,24 @@
+/* This file has been placed in the public domain. */
+/* The following rule is necessary to have all slides appear in print!
+ DO NOT REMOVE IT! */
+.slide, ul {page-break-inside: avoid; visibility: visible !important;}
+h1 {page-break-after: avoid;}
+
+body {font-size: 12pt; background: white;}
+* {color: black;}
+
+#slide0 h1 {font-size: 200%; border: none; margin: 0.5em 0 0.25em;}
+#slide0 h3 {margin: 0; padding: 0;}
+#slide0 h4 {margin: 0 0 0.5em; padding: 0;}
+#slide0 {margin-bottom: 3em;}
+
+#header {display: none;}
+#footer h1 {margin: 0; border-bottom: 1px solid; color: gray;
+ font-style: italic;}
+#footer h2, #controls {display: none;}
+
+.print {display: inline ! important;}
+
+/* The following rule keeps the layout stuff out of print.
+ Remove at your own risk! */
+.layout, .layout * {display: none !important;}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/s5-core.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/s5-core.css
new file mode 100644
index 00000000..62e1b7b1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/s5-core.css
@@ -0,0 +1,11 @@
+/* This file has been placed in the public domain. */
+/* Do not edit or override these styles!
+ The system will likely break if you do. */
+
+div#header, div#footer, div#controls, .slide {position: absolute;}
+html>body div#header, html>body div#footer,
+ html>body div#controls, html>body .slide {position: fixed;}
+.handout {display: none;}
+.layout {display: block;}
+.slide, .hideme, .incremental {visibility: hidden;}
+#slide0 {visibility: visible;}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.css
new file mode 100644
index 00000000..82bdc0ee
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.css
@@ -0,0 +1,10 @@
+/* This file has been placed in the public domain. */
+
+/* required to make the slide show run at all */
+@import url(s5-core.css);
+
+/* sets basic placement and size of slide components */
+@import url(framing.css);
+
+/* styles that make the slides look good */
+@import url(pretty.css);
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.js b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.js
new file mode 100644
index 00000000..cd0e0e43
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/default/slides.js
@@ -0,0 +1,558 @@
+// S5 v1.1 slides.js -- released into the Public Domain
+// Modified for Docutils (https://docutils.sourceforge.io) by David Goodger
+//
+// Please see http://www.meyerweb.com/eric/tools/s5/credits.html for
+// information about all the wonderful and talented contributors to this code!
+
+var undef;
+var slideCSS = '';
+var snum = 0;
+var smax = 1;
+var slideIDs = new Array();
+var incpos = 0;
+var number = undef;
+var s5mode = true;
+var defaultView = 'slideshow';
+var controlVis = 'visible';
+
+var isIE = navigator.appName == 'Microsoft Internet Explorer' ? 1 : 0;
+var isOp = navigator.userAgent.indexOf('Opera') > -1 ? 1 : 0;
+var isGe = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('Safari') < 1 ? 1 : 0;
+
+function hasClass(object, className) {
+ if (!object.className) return false;
+ return (object.className.search('(^|\\s)' + className + '(\\s|$)') != -1);
+}
+
+function hasValue(object, value) {
+ if (!object) return false;
+ return (object.search('(^|\\s)' + value + '(\\s|$)') != -1);
+}
+
+function removeClass(object,className) {
+ if (!object) return;
+ object.className = object.className.replace(new RegExp('(^|\\s)'+className+'(\\s|$)'), RegExp.$1+RegExp.$2);
+}
+
+function addClass(object,className) {
+ if (!object || hasClass(object, className)) return;
+ if (object.className) {
+ object.className += ' '+className;
+ } else {
+ object.className = className;
+ }
+}
+
+function GetElementsWithClassName(elementName,className) {
+ var allElements = document.getElementsByTagName(elementName);
+ var elemColl = new Array();
+ for (var i = 0; i< allElements.length; i++) {
+ if (hasClass(allElements[i], className)) {
+ elemColl[elemColl.length] = allElements[i];
+ }
+ }
+ return elemColl;
+}
+
+function isParentOrSelf(element, id) {
+ if (element == null || element.nodeName=='BODY') return false;
+ else if (element.id == id) return true;
+ else return isParentOrSelf(element.parentNode, id);
+}
+
+function nodeValue(node) {
+ var result = "";
+ if (node.nodeType == 1) {
+ var children = node.childNodes;
+ for (var i = 0; i < children.length; ++i) {
+ result += nodeValue(children[i]);
+ }
+ }
+ else if (node.nodeType == 3) {
+ result = node.nodeValue;
+ }
+ return(result);
+}
+
+function slideLabel() {
+ var slideColl = GetElementsWithClassName('*','slide');
+ var list = document.getElementById('jumplist');
+ smax = slideColl.length;
+ for (var n = 0; n < smax; n++) {
+ var obj = slideColl[n];
+
+ var did = 'slide' + n.toString();
+ if (obj.getAttribute('id')) {
+ slideIDs[n] = obj.getAttribute('id');
+ }
+ else {
+ obj.setAttribute('id',did);
+ slideIDs[n] = did;
+ }
+ if (isOp) continue;
+
+ var otext = '';
+ var menu = obj.firstChild;
+ if (!menu) continue; // to cope with empty slides
+ while (menu && menu.nodeType == 3) {
+ menu = menu.nextSibling;
+ }
+ if (!menu) continue; // to cope with slides with only text nodes
+
+ var menunodes = menu.childNodes;
+ for (var o = 0; o < menunodes.length; o++) {
+ otext += nodeValue(menunodes[o]);
+ }
+ list.options[list.length] = new Option(n + ' : ' + otext, n);
+ }
+}
+
+function currentSlide() {
+ var cs;
+ var footer_nodes;
+ var vis = 'visible';
+ if (document.getElementById) {
+ cs = document.getElementById('currentSlide');
+ footer_nodes = document.getElementById('footer').childNodes;
+ } else {
+ cs = document.currentSlide;
+ footer = document.footer.childNodes;
+ }
+ cs.innerHTML = '<span id="csHere">' + snum + '<\/span> ' +
+ '<span id="csSep">\/<\/span> ' +
+ '<span id="csTotal">' + (smax-1) + '<\/span>';
+ if (snum == 0) {
+ vis = 'hidden';
+ }
+ cs.style.visibility = vis;
+ for (var i = 0; i < footer_nodes.length; i++) {
+ if (footer_nodes[i].nodeType == 1) {
+ footer_nodes[i].style.visibility = vis;
+ }
+ }
+}
+
+function go(step) {
+ if (document.getElementById('slideProj').disabled || step == 0) return;
+ var jl = document.getElementById('jumplist');
+ var cid = slideIDs[snum];
+ var ce = document.getElementById(cid);
+ if (incrementals[snum].length > 0) {
+ for (var i = 0; i < incrementals[snum].length; i++) {
+ removeClass(incrementals[snum][i], 'current');
+ removeClass(incrementals[snum][i], 'incremental');
+ }
+ }
+ if (step != 'j') {
+ snum += step;
+ lmax = smax - 1;
+ if (snum > lmax) snum = lmax;
+ if (snum < 0) snum = 0;
+ } else
+ snum = parseInt(jl.value);
+ var nid = slideIDs[snum];
+ var ne = document.getElementById(nid);
+ if (!ne) {
+ ne = document.getElementById(slideIDs[0]);
+ snum = 0;
+ }
+ if (step < 0) {incpos = incrementals[snum].length} else {incpos = 0;}
+ if (incrementals[snum].length > 0 && incpos == 0) {
+ for (var i = 0; i < incrementals[snum].length; i++) {
+ if (hasClass(incrementals[snum][i], 'current'))
+ incpos = i + 1;
+ else
+ addClass(incrementals[snum][i], 'incremental');
+ }
+ }
+ if (incrementals[snum].length > 0 && incpos > 0)
+ addClass(incrementals[snum][incpos - 1], 'current');
+ ce.style.visibility = 'hidden';
+ ne.style.visibility = 'visible';
+ jl.selectedIndex = snum;
+ currentSlide();
+ number = 0;
+}
+
+function goTo(target) {
+ if (target >= smax || target == snum) return;
+ go(target - snum);
+}
+
+function subgo(step) {
+ if (step > 0) {
+ removeClass(incrementals[snum][incpos - 1],'current');
+ removeClass(incrementals[snum][incpos], 'incremental');
+ addClass(incrementals[snum][incpos],'current');
+ incpos++;
+ } else {
+ incpos--;
+ removeClass(incrementals[snum][incpos],'current');
+ addClass(incrementals[snum][incpos], 'incremental');
+ addClass(incrementals[snum][incpos - 1],'current');
+ }
+}
+
+function toggle() {
+ var slideColl = GetElementsWithClassName('*','slide');
+ var slides = document.getElementById('slideProj');
+ var outline = document.getElementById('outlineStyle');
+ if (!slides.disabled) {
+ slides.disabled = true;
+ outline.disabled = false;
+ s5mode = false;
+ fontSize('1em');
+ for (var n = 0; n < smax; n++) {
+ var slide = slideColl[n];
+ slide.style.visibility = 'visible';
+ }
+ } else {
+ slides.disabled = false;
+ outline.disabled = true;
+ s5mode = true;
+ fontScale();
+ for (var n = 0; n < smax; n++) {
+ var slide = slideColl[n];
+ slide.style.visibility = 'hidden';
+ }
+ slideColl[snum].style.visibility = 'visible';
+ }
+}
+
+function showHide(action) {
+ var obj = GetElementsWithClassName('*','hideme')[0];
+ switch (action) {
+ case 's': obj.style.visibility = 'visible'; break;
+ case 'h': obj.style.visibility = 'hidden'; break;
+ case 'k':
+ if (obj.style.visibility != 'visible') {
+ obj.style.visibility = 'visible';
+ } else {
+ obj.style.visibility = 'hidden';
+ }
+ break;
+ }
+}
+
+// 'keys' code adapted from MozPoint (http://mozpoint.mozdev.org/)
+function keys(key) {
+ if (!key) {
+ key = event;
+ key.which = key.keyCode;
+ }
+ if (key.which == 84) {
+ toggle();
+ return;
+ }
+ if (s5mode) {
+ switch (key.which) {
+ case 10: // return
+ case 13: // enter
+ if (window.event && isParentOrSelf(window.event.srcElement, 'controls')) return;
+ if (key.target && isParentOrSelf(key.target, 'controls')) return;
+ if(number != undef) {
+ goTo(number);
+ break;
+ }
+ case 32: // spacebar
+ case 34: // page down
+ case 39: // rightkey
+ case 40: // downkey
+ if(number != undef) {
+ go(number);
+ } else if (!incrementals[snum] || incpos >= incrementals[snum].length) {
+ go(1);
+ } else {
+ subgo(1);
+ }
+ break;
+ case 33: // page up
+ case 37: // leftkey
+ case 38: // upkey
+ if(number != undef) {
+ go(-1 * number);
+ } else if (!incrementals[snum] || incpos <= 0) {
+ go(-1);
+ } else {
+ subgo(-1);
+ }
+ break;
+ case 36: // home
+ goTo(0);
+ break;
+ case 35: // end
+ goTo(smax-1);
+ break;
+ case 67: // c
+ showHide('k');
+ break;
+ }
+ if (key.which < 48 || key.which > 57) {
+ number = undef;
+ } else {
+ if (window.event && isParentOrSelf(window.event.srcElement, 'controls')) return;
+ if (key.target && isParentOrSelf(key.target, 'controls')) return;
+ number = (((number != undef) ? number : 0) * 10) + (key.which - 48);
+ }
+ }
+ return false;
+}
+
+function clicker(e) {
+ number = undef;
+ var target;
+ if (window.event) {
+ target = window.event.srcElement;
+ e = window.event;
+ } else target = e.target;
+ if (target.href != null || hasValue(target.rel, 'external') || isParentOrSelf(target, 'controls') || isParentOrSelf(target,'embed') || isParentOrSelf(target, 'object')) return true;
+ if (!e.which || e.which == 1) {
+ if (!incrementals[snum] || incpos >= incrementals[snum].length) {
+ go(1);
+ } else {
+ subgo(1);
+ }
+ }
+}
+
+function findSlide(hash) {
+ var target = document.getElementById(hash);
+ if (target) {
+ for (var i = 0; i < slideIDs.length; i++) {
+ if (target.id == slideIDs[i]) return i;
+ }
+ }
+ return null;
+}
+
+function slideJump() {
+ if (window.location.hash == null || window.location.hash == '') {
+ currentSlide();
+ return;
+ }
+ if (window.location.hash == null) return;
+ var dest = null;
+ dest = findSlide(window.location.hash.slice(1));
+ if (dest == null) {
+ dest = 0;
+ }
+ go(dest - snum);
+}
+
+function fixLinks() {
+ var thisUri = window.location.href;
+ thisUri = thisUri.slice(0, thisUri.length - window.location.hash.length);
+ var aelements = document.getElementsByTagName('A');
+ for (var i = 0; i < aelements.length; i++) {
+ var a = aelements[i].href;
+ var slideID = a.match('\#.+');
+ if ((slideID) && (slideID[0].slice(0,1) == '#')) {
+ var dest = findSlide(slideID[0].slice(1));
+ if (dest != null) {
+ if (aelements[i].addEventListener) {
+ aelements[i].addEventListener("click", new Function("e",
+ "if (document.getElementById('slideProj').disabled) return;" +
+ "go("+dest+" - snum); " +
+ "if (e.preventDefault) e.preventDefault();"), true);
+ } else if (aelements[i].attachEvent) {
+ aelements[i].attachEvent("onclick", new Function("",
+ "if (document.getElementById('slideProj').disabled) return;" +
+ "go("+dest+" - snum); " +
+ "event.returnValue = false;"));
+ }
+ }
+ }
+ }
+}
+
+function externalLinks() {
+ if (!document.getElementsByTagName) return;
+ var anchors = document.getElementsByTagName('a');
+ for (var i=0; i<anchors.length; i++) {
+ var anchor = anchors[i];
+ if (anchor.getAttribute('href') && hasValue(anchor.rel, 'external')) {
+ anchor.target = '_blank';
+ addClass(anchor,'external');
+ }
+ }
+}
+
+function createControls() {
+ var controlsDiv = document.getElementById("controls");
+ if (!controlsDiv) return;
+ var hider = ' onmouseover="showHide(\'s\');" onmouseout="showHide(\'h\');"';
+ var hideDiv, hideList = '';
+ if (controlVis == 'hidden') {
+ hideDiv = hider;
+ } else {
+ hideList = hider;
+ }
+ controlsDiv.innerHTML = '<form action="#" id="controlForm"' + hideDiv + '>' +
+ '<div id="navLinks">' +
+ '<a accesskey="t" id="toggle" href="javascript:toggle();">&#216;<\/a>' +
+ '<a accesskey="z" id="prev" href="javascript:go(-1);">&laquo;<\/a>' +
+ '<a accesskey="x" id="next" href="javascript:go(1);">&raquo;<\/a>' +
+ '<div id="navList"' + hideList + '><select id="jumplist" onchange="go(\'j\');"><\/select><\/div>' +
+ '<\/div><\/form>';
+ if (controlVis == 'hidden') {
+ var hidden = document.getElementById('navLinks');
+ } else {
+ var hidden = document.getElementById('jumplist');
+ }
+ addClass(hidden,'hideme');
+}
+
+function fontScale() { // causes layout problems in FireFox that get fixed if browser's Reload is used; same may be true of other Gecko-based browsers
+ if (!s5mode) return false;
+ var vScale = 22; // both yield 32 (after rounding) at 1024x768
+ var hScale = 32; // perhaps should auto-calculate based on theme's declared value?
+ if (window.innerHeight) {
+ var vSize = window.innerHeight;
+ var hSize = window.innerWidth;
+ } else if (document.documentElement.clientHeight) {
+ var vSize = document.documentElement.clientHeight;
+ var hSize = document.documentElement.clientWidth;
+ } else if (document.body.clientHeight) {
+ var vSize = document.body.clientHeight;
+ var hSize = document.body.clientWidth;
+ } else {
+ var vSize = 700; // assuming 1024x768, minus chrome and such
+ var hSize = 1024; // these do not account for kiosk mode or Opera Show
+ }
+ var newSize = Math.min(Math.round(vSize/vScale),Math.round(hSize/hScale));
+ fontSize(newSize + 'px');
+ if (isGe) { // hack to counter incremental reflow bugs
+ var obj = document.getElementsByTagName('body')[0];
+ obj.style.display = 'none';
+ obj.style.display = 'block';
+ }
+}
+
+function fontSize(value) {
+ if (!(s5ss = document.getElementById('s5ss'))) {
+ if (!isIE) {
+ document.getElementsByTagName('head')[0].appendChild(s5ss = document.createElement('style'));
+ s5ss.setAttribute('media','screen, projection');
+ s5ss.setAttribute('id','s5ss');
+ } else {
+ document.createStyleSheet();
+ document.s5ss = document.styleSheets[document.styleSheets.length - 1];
+ }
+ }
+ if (!isIE) {
+ while (s5ss.lastChild) s5ss.removeChild(s5ss.lastChild);
+ s5ss.appendChild(document.createTextNode('body {font-size: ' + value + ' !important;}'));
+ } else {
+ document.s5ss.addRule('body','font-size: ' + value + ' !important;');
+ }
+}
+
+function notOperaFix() {
+ slideCSS = document.getElementById('slideProj').href;
+ var slides = document.getElementById('slideProj');
+ var outline = document.getElementById('outlineStyle');
+ slides.setAttribute('media','screen');
+ outline.disabled = true;
+ if (isGe) {
+ slides.setAttribute('href','null'); // Gecko fix
+ slides.setAttribute('href',slideCSS); // Gecko fix
+ }
+ if (isIE && document.styleSheets && document.styleSheets[0]) {
+ document.styleSheets[0].addRule('img', 'behavior: url(ui/default/iepngfix.htc)');
+ document.styleSheets[0].addRule('div', 'behavior: url(ui/default/iepngfix.htc)');
+ document.styleSheets[0].addRule('.slide', 'behavior: url(ui/default/iepngfix.htc)');
+ }
+}
+
+function getIncrementals(obj) {
+ var incrementals = new Array();
+ if (!obj)
+ return incrementals;
+ var children = obj.childNodes;
+ for (var i = 0; i < children.length; i++) {
+ var child = children[i];
+ if (hasClass(child, 'incremental')) {
+ if (child.nodeName == 'OL' || child.nodeName == 'UL') {
+ removeClass(child, 'incremental');
+ for (var j = 0; j < child.childNodes.length; j++) {
+ if (child.childNodes[j].nodeType == 1) {
+ addClass(child.childNodes[j], 'incremental');
+ }
+ }
+ } else {
+ incrementals[incrementals.length] = child;
+ removeClass(child,'incremental');
+ }
+ }
+ if (hasClass(child, 'show-first')) {
+ if (child.nodeName == 'OL' || child.nodeName == 'UL') {
+ removeClass(child, 'show-first');
+ if (child.childNodes[isGe].nodeType == 1) {
+ removeClass(child.childNodes[isGe], 'incremental');
+ }
+ } else {
+ incrementals[incrementals.length] = child;
+ }
+ }
+ incrementals = incrementals.concat(getIncrementals(child));
+ }
+ return incrementals;
+}
+
+function createIncrementals() {
+ var incrementals = new Array();
+ for (var i = 0; i < smax; i++) {
+ incrementals[i] = getIncrementals(document.getElementById(slideIDs[i]));
+ }
+ return incrementals;
+}
+
+function defaultCheck() {
+ var allMetas = document.getElementsByTagName('meta');
+ for (var i = 0; i< allMetas.length; i++) {
+ if (allMetas[i].name == 'defaultView') {
+ defaultView = allMetas[i].content;
+ }
+ if (allMetas[i].name == 'controlVis') {
+ controlVis = allMetas[i].content;
+ }
+ }
+}
+
+// Key trap fix, new function body for trap()
+function trap(e) {
+ if (!e) {
+ e = event;
+ e.which = e.keyCode;
+ }
+ try {
+ modifierKey = e.ctrlKey || e.altKey || e.metaKey;
+ }
+ catch(e) {
+ modifierKey = false;
+ }
+ return modifierKey || e.which == 0;
+}
+
+function startup() {
+ defaultCheck();
+ if (!isOp) createControls();
+ slideLabel();
+ fixLinks();
+ externalLinks();
+ fontScale();
+ if (!isOp) {
+ notOperaFix();
+ incrementals = createIncrementals();
+ slideJump();
+ if (defaultView == 'outline') {
+ toggle();
+ }
+ document.onkeyup = keys;
+ document.onkeypress = trap;
+ document.onclick = clicker;
+ }
+}
+
+window.onload = startup;
+window.onresize = function(){setTimeout('fontScale()', 50);}
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/__base__ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/__base__
new file mode 100644
index 00000000..401b621b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/__base__
@@ -0,0 +1,2 @@
+# base theme of this theme:
+medium-white
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/pretty.css
new file mode 100644
index 00000000..81df4bc1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-black/pretty.css
@@ -0,0 +1,115 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: black; color: white;}
+:link, :visited {text-decoration: none; color: cyan;}
+#controls :active {color: #888 !important;}
+#controls :focus {outline: 1px dotted #CCC;}
+h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+div#footer {font-family: sans-serif; color: #AAA;
+ font-size: 0.5em; font-weight: bold; padding: 1em 0;}
+#footer h1 {display: block; padding: 0 1em;}
+#footer h2 {display: block; padding: 0.8em 1em 0;}
+
+.slide {font-size: 1.75em;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font: bold 150% sans-serif;}
+.slide h2 {font: bold 125% sans-serif; padding-top: 0.5em;}
+.slide h3 {font: bold 110% sans-serif; padding-top: 0.5em;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+ width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+ border: none; color: #888; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+ background: black; color: #CCC;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #AAA;
+ font-family: sans-serif; font-weight: bold;}
+
+#slide0 h1 {position: static; margin: 0 0 0.5em; padding-top: 1em; top: 0;
+ font: bold 150% sans-serif; white-space: normal; background: transparent;}
+#slide0 h2 {font: bold italic 125% sans-serif; color: gray;}
+#slide0 h3 {margin-top: 1.5em; font: bold 110% sans-serif;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #FCC;}
+
+.incremental, .incremental *, .incremental *:after {
+ color: black; visibility: visible; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: lime;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;}
+.big {font-family: sans-serif; font-weight: bold; font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 0.5em 0 0.5em 1em;}
+
+div.sidebar {background-color: black;}
+
+pre.literal-block, pre.doctest-block {background-color: black;}
+
+tt.docutils {background-color: black;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/framing.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/framing.css
new file mode 100644
index 00000000..ebb8a573
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/framing.css
@@ -0,0 +1,24 @@
+/* This file has been placed in the public domain. */
+/* The following styles size, place, and layer the slide components.
+ Edit these if you want to change the overall slide layout.
+ The commented lines can be uncommented (and modified, if necessary)
+ to help you with the rearrangement process. */
+
+/* target = 1024x768 */
+
+div#header, div#footer, .slide {width: 100%; top: 0; left: 0;}
+div#footer {top: auto; bottom: 0; height: 2.5em; z-index: 5;}
+.slide {top: 0; width: 92%; padding: 0.75em 4% 0 4%; z-index: 2;}
+div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0;}
+#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em;
+ z-index: 10;}
+html>body #currentSlide {position: fixed;}
+
+/*
+div#header {background: #FCC;}
+div#footer {background: #CCF;}
+div#controls {background: #BBD;}
+div#currentSlide {background: #FFC;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/pretty.css
new file mode 100644
index 00000000..1c9fafdf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/medium-white/pretty.css
@@ -0,0 +1,113 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: white; color: black;}
+:link, :visited {text-decoration: none; color: #00C;}
+#controls :active {color: #888 !important;}
+#controls :focus {outline: 1px dotted #222;}
+h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+div#footer {font-family: sans-serif; color: #444;
+ font-size: 0.5em; font-weight: bold; padding: 1em 0;}
+#footer h1 {display: block; padding: 0 1em;}
+#footer h2 {display: block; padding: 0.8em 1em 0;}
+
+.slide {font-size: 1.75em;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font: bold 150% sans-serif;}
+.slide h2 {font: bold 125% sans-serif; padding-top: 0.5em;}
+.slide h3 {font: bold 110% sans-serif; padding-top: 0.5em;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+ width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+ border: none; color: #888; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+ background: #DDD; color: #222;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #444;
+ font-family: sans-serif; font-weight: bold;}
+
+#slide0 h1 {position: static; margin: 0 0 0.5em; padding-top: 1em; top: 0;
+ font: bold 150% sans-serif; white-space: normal; background: transparent;}
+#slide0 h2 {font: bold italic 125% sans-serif; color: gray;}
+#slide0 h3 {margin-top: 1.5em; font: bold 110% sans-serif;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #77B;}
+
+.incremental, .incremental *, .incremental *:after {
+ color: white; visibility: visible; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: green;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;}
+.big {font-family: sans-serif; font-weight: bold; font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 0.5em 0 0.5em 1em;}
+
+pre.literal-block, pre.doctest-block {background-color: white;}
+
+tt.docutils {background-color: white;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/__base__ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/__base__
new file mode 100644
index 00000000..67f4db2b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/__base__
@@ -0,0 +1,2 @@
+# base theme of this theme:
+small-white
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/pretty.css
new file mode 100644
index 00000000..5524e12e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-black/pretty.css
@@ -0,0 +1,116 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: black; color: white;}
+:link, :visited {text-decoration: none; color: cyan;}
+#controls :active {color: #888 !important;}
+#controls :focus {outline: 1px dotted #CCC;}
+h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+div#footer {font-family: sans-serif; color: #AAA;
+ font-size: 0.5em; font-weight: bold; padding: 1em 0;}
+#footer h1 {display: block; padding: 0 1em;}
+#footer h2 {display: block; padding: 0.8em 1em 0;}
+
+.slide {font-size: 1.2em;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font: bold 150% sans-serif;}
+.slide h2 {font: bold 120% sans-serif; padding-top: 0.5em;}
+.slide h3 {font: bold 100% sans-serif; padding-top: 0.5em;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+ width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+ border: none; color: #888; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+ background: black; color: #CCC;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #AAA;
+ font-family: sans-serif; font-weight: bold;}
+
+#slide0 {padding-top: 0em}
+#slide0 h1 {position: static; margin: 1em 0 0; padding: 0;
+ font: bold 2em sans-serif; white-space: normal; background: transparent;}
+#slide0 h2 {font: bold italic 1em sans-serif; margin: 0.25em;}
+#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #FCC;}
+
+.incremental, .incremental *, .incremental *:after {
+ color: black; visibility: visible; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: lime;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;}
+.big {font-family: sans-serif; font-weight: bold; font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 1em 0 0.5em 2em;}
+
+div.sidebar {background-color: black;}
+
+pre.literal-block, pre.doctest-block {background-color: black;}
+
+tt.docutils {background-color: black;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/framing.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/framing.css
new file mode 100644
index 00000000..f6578749
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/framing.css
@@ -0,0 +1,24 @@
+/* This file has been placed in the public domain. */
+/* The following styles size, place, and layer the slide components.
+ Edit these if you want to change the overall slide layout.
+ The commented lines can be uncommented (and modified, if necessary)
+ to help you with the rearrangement process. */
+
+/* target = 1024x768 */
+
+div#header, div#footer, .slide {width: 100%; top: 0; left: 0;}
+div#footer {top: auto; bottom: 0; height: 2.5em; z-index: 5;}
+.slide {top: 0; width: 92%; padding: 1em 4% 0 4%; z-index: 2;}
+div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0;}
+#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em;
+ z-index: 10;}
+html>body #currentSlide {position: fixed;}
+
+/*
+div#header {background: #FCC;}
+div#footer {background: #CCF;}
+div#controls {background: #BBD;}
+div#currentSlide {background: #FFC;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/pretty.css b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/pretty.css
new file mode 100644
index 00000000..edf4cb5e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/s5_html/themes/small-white/pretty.css
@@ -0,0 +1,114 @@
+/* This file has been placed in the public domain. */
+/* Following are the presentation styles -- edit away! */
+
+html, body {margin: 0; padding: 0;}
+body {background: white; color: black;}
+:link, :visited {text-decoration: none; color: #00C;}
+#controls :active {color: #888 !important;}
+#controls :focus {outline: 1px dotted #222;}
+h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;}
+
+blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;}
+blockquote p {margin: 0;}
+
+kbd {font-weight: bold; font-size: 1em;}
+sup {font-size: smaller; line-height: 1px;}
+
+.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 90%;}
+.slide ul ul li {list-style: square;}
+.slide img.leader {display: block; margin: 0 auto;}
+.slide tt {font-size: 90%;}
+
+div#footer {font-family: sans-serif; color: #444;
+ font-size: 0.5em; font-weight: bold; padding: 1em 0;}
+#footer h1 {display: block; padding: 0 1em;}
+#footer h2 {display: block; padding: 0.8em 1em 0;}
+
+.slide {font-size: 1.2em;}
+.slide h1 {padding-top: 0; z-index: 1; margin: 0; font: bold 150% sans-serif;}
+.slide h2 {font: bold 120% sans-serif; padding-top: 0.5em;}
+.slide h3 {font: bold 100% sans-serif; padding-top: 0.5em;}
+h1 abbr {font-variant: small-caps;}
+
+div#controls {position: absolute; left: 50%; bottom: 0;
+ width: 50%; text-align: right; font: bold 0.9em sans-serif;}
+html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;}
+div#controls form {position: absolute; bottom: 0; right: 0; width: 100%;
+ margin: 0; padding: 0;}
+#controls #navLinks a {padding: 0; margin: 0 0.5em;
+ border: none; color: #888; cursor: pointer;}
+#controls #navList {height: 1em;}
+#controls #navList #jumplist {position: absolute; bottom: 0; right: 0;
+ background: #DDD; color: #222;}
+
+#currentSlide {text-align: center; font-size: 0.5em; color: #444;
+ font-family: sans-serif; font-weight: bold;}
+
+#slide0 {padding-top: 0em}
+#slide0 h1 {position: static; margin: 1em 0 0; padding: 0;
+ font: bold 2em sans-serif; white-space: normal; background: transparent;}
+#slide0 h2 {font: bold italic 1em sans-serif; margin: 0.25em;}
+#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;}
+#slide0 h4 {margin-top: 0; font-size: 1em;}
+
+ul.urls {list-style: none; display: inline; margin: 0;}
+.urls li {display: inline; margin: 0;}
+.external {border-bottom: 1px dotted gray;}
+html>body .external {border-bottom: none;}
+.external:after {content: " \274F"; font-size: smaller; color: #77B;}
+
+.incremental, .incremental *, .incremental *:after {
+ color: white; visibility: visible; border: 0; border: 0;}
+img.incremental {visibility: hidden;}
+.slide .current {color: green;}
+
+.slide-display {display: inline ! important;}
+
+.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;}
+.big {font-family: sans-serif; font-weight: bold; font-size: 120%;}
+.small {font-size: 75%;}
+.tiny {font-size: 50%;}
+.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;}
+.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;}
+
+.maroon {color: maroon;}
+.red {color: red;}
+.magenta {color: magenta;}
+.fuchsia {color: fuchsia;}
+.pink {color: #FAA;}
+.orange {color: orange;}
+.yellow {color: yellow;}
+.lime {color: lime;}
+.green {color: green;}
+.olive {color: olive;}
+.teal {color: teal;}
+.cyan {color: cyan;}
+.aqua {color: aqua;}
+.blue {color: blue;}
+.navy {color: navy;}
+.purple {color: purple;}
+.black {color: black;}
+.gray {color: gray;}
+.silver {color: silver;}
+.white {color: white;}
+
+.left {text-align: left ! important;}
+.center {text-align: center ! important;}
+.right {text-align: right ! important;}
+
+.animation {position: relative; margin: 1em 0; padding: 0;}
+.animation img {position: absolute;}
+
+/* Docutils-specific overrides */
+
+.slide table.docinfo {margin: 1em 0 0.5em 2em;}
+
+pre.literal-block, pre.doctest-block {background-color: white;}
+
+tt.docutils {background-color: white;}
+
+/* diagnostics */
+/*
+li:after {content: " [" attr(class) "]"; color: #F88;}
+div:before {content: "[" attr(class) "]"; color: #F88;}
+*/
diff --git a/.venv/lib/python3.12/site-packages/docutils/writers/xetex/__init__.py b/.venv/lib/python3.12/site-packages/docutils/writers/xetex/__init__.py
new file mode 100644
index 00000000..a7bad3fd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/writers/xetex/__init__.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+# :Author: Günter Milde <milde@users.sf.net>
+# :Revision: $Revision: 9293 $
+# :Date: $Date: 2022-12-01 22:13:54 +0100 (Do, 01. Dez 2022) $
+# :Copyright: © 2010 Günter Milde.
+# :License: Released under the terms of the `2-Clause BSD license`_, in short:
+#
+# Copying and distribution of this file, with or without modification,
+# are permitted in any medium without royalty provided the copyright
+# notice and this notice are preserved.
+# This file is offered as-is, without any warranty.
+#
+# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+
+"""
+XeLaTeX document tree Writer.
+
+A variant of Docutils' standard 'latex2e' writer producing LaTeX output
+suited for processing with the Unicode-aware TeX engines
+LuaTeX and XeTeX.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from docutils import frontend
+from docutils.writers import latex2e
+
+
+class Writer(latex2e.Writer):
+ """A writer for Unicode-aware LaTeX variants (XeTeX, LuaTeX)"""
+
+ supported = ('latex', 'tex', 'xetex', 'xelatex', 'luatex', 'lualatex')
+ """Formats this writer supports."""
+
+ default_template = 'xelatex.tex'
+ default_preamble = """\
+% Linux Libertine (free, wide coverage, not only for Linux)
+\\setmainfont{Linux Libertine O}
+\\setsansfont{Linux Biolinum O}
+\\setmonofont[HyphenChar=None,Scale=MatchLowercase]{DejaVu Sans Mono}"""
+
+ config_section = 'xetex writer'
+ config_section_dependencies = ('writers', 'latex writers')
+
+ # use a copy of the parent spec with some modifications:
+ settings_spec = frontend.filter_settings_spec(
+ latex2e.Writer.settings_spec,
+ # removed settings
+ 'font_encoding',
+ # changed settings:
+ template=('Template file. Default: "%s".' % default_template,
+ ['--template'],
+ {'default': default_template, 'metavar': '<file>'}),
+ latex_preamble=('Customization by LaTeX code in the preamble. '
+ 'Default: select "Linux Libertine" fonts.',
+ ['--latex-preamble'],
+ {'default': default_preamble}),
+ )
+
+ def __init__(self):
+ latex2e.Writer.__init__(self)
+ self.settings_defaults.update({'fontencoding': ''}) # use default (TU)
+ self.translator_class = XeLaTeXTranslator
+
+
+class Babel(latex2e.Babel):
+ """Language specifics for XeTeX.
+
+ Use `polyglossia` instead of `babel` and adapt settings.
+ """
+ language_codes = latex2e.Babel.language_codes.copy()
+ # Additionally supported or differently named languages:
+ language_codes.update({
+ # code Polyglossia-name comment
+ 'cop': 'coptic',
+ 'de': 'german', # new spelling (de_1996)
+ 'de-1901': 'ogerman', # old spelling
+ 'dv': 'divehi', # Maldivian
+ 'dsb': 'lsorbian',
+ 'el-polyton': 'polygreek',
+ 'fa': 'farsi',
+ 'grc': 'ancientgreek',
+ 'ko': 'korean',
+ 'hsb': 'usorbian',
+ 'sh-Cyrl': 'serbian', # Serbo-Croatian, Cyrillic script
+ 'sh-Latn': 'croatian', # Serbo-Croatian, Latin script
+ 'sq': 'albanian',
+ 'sr': 'serbian', # Cyrillic script (sr-Cyrl)
+ 'th': 'thai',
+ 'vi': 'vietnamese',
+ # zh-Latn: ??? # Chinese Pinyin
+ })
+ # normalize (downcase) keys
+ language_codes = {k.lower(): v for k, v in language_codes.items()}
+
+ # Languages without Polyglossia support:
+ for key in ('af', # 'afrikaans',
+ 'de-AT', # 'naustrian',
+ 'de-AT-1901', # 'austrian',
+ # TODO: use variant=... for English variants
+ 'en-CA', # 'canadian',
+ 'en-GB', # 'british',
+ 'en-NZ', # 'newzealand',
+ 'en-US', # 'american',
+ 'fr-CA', # 'canadien',
+ 'grc-ibycus', # 'ibycus', (Greek Ibycus encoding)
+ 'sr-Latn', # 'serbian script=latin'
+ ):
+ del language_codes[key.lower()]
+
+ def __init__(self, language_code, reporter):
+ self.language_code = language_code
+ self.reporter = reporter
+ self.language = self.language_name(language_code)
+ self.otherlanguages = {}
+ self.warn_msg = 'Language "%s" not supported by Polyglossia.'
+ self.quote_index = 0
+ self.quotes = ('"', '"')
+ # language dependent configuration:
+ # double quotes are "active" in some languages (e.g. German).
+ self.literal_double_quote = '"' # TODO: use \textquotedbl ?
+
+ def __call__(self):
+ setup = [r'\usepackage{polyglossia}',
+ r'\setdefaultlanguage{%s}' % self.language]
+ if self.otherlanguages:
+ setup.append(r'\setotherlanguages{%s}' %
+ ','.join(sorted(self.otherlanguages.keys())))
+ return '\n'.join(setup)
+
+
+class XeLaTeXTranslator(latex2e.LaTeXTranslator):
+ """
+ Generate code for LaTeX using Unicode fonts (XeLaTex or LuaLaTeX).
+
+ See the docstring of docutils.writers._html_base.HTMLTranslator for
+ notes on and examples of safe subclassing.
+ """
+
+ def __init__(self, document):
+ self.is_xetex = True # typeset with XeTeX or LuaTeX engine
+ latex2e.LaTeXTranslator.__init__(self, document, Babel)
+ if self.latex_encoding == 'utf8':
+ self.requirements.pop('_inputenc', None)
+ else:
+ self.requirements['_inputenc'] = (r'\XeTeXinputencoding %s '
+ % self.latex_encoding)