aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/docutils
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/docutils')
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/__init__.py291
-rwxr-xr-x.venv/lib/python3.12/site-packages/docutils/__main__.py96
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/core.py780
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/docutils.conf5
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/examples.py99
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/frontend.py1065
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/io.py637
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/__init__.py83
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/af.py58
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/ar.py60
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/ca.py65
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/cs.py60
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/da.py61
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/de.py58
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/en.py60
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/eo.py61
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/es.py58
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/fa.py60
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/fi.py60
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/fr.py58
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/gl.py62
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/he.py62
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/it.py58
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/ja.py60
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/ka.py58
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/ko.py60
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/lt.py60
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/lv.py59
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/nl.py60
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/pl.py60
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/pt_br.py60
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/ru.py58
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/sk.py58
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/sv.py59
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/uk.py58
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/zh_cn.py62
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/languages/zh_tw.py61
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/nodes.py2301
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/__init__.py92
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/commonmark_wrapper.py56
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/null.py20
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/recommonmark_wrapper.py147
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/__init__.py413
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/__init__.py466
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/admonitions.py101
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/body.py305
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/html.py21
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/images.py173
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/misc.py642
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/parts.py126
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/references.py29
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/tables.py538
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/README.txt17
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsa.txt162
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsb.txt126
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsc.txt29
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsn.txt96
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamso.txt62
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsr.txt191
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isobox.txt46
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isocyr1.txt73
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isocyr2.txt32
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isodia.txt20
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk1.txt55
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk2.txt26
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk3.txt52
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk4-wide.txt49
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk4.txt8
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isolat1.txt68
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isolat2.txt128
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomfrk-wide.txt58
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomfrk.txt11
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomopf-wide.txt32
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomopf.txt13
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomscr-wide.txt58
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomscr.txt17
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isonum.txt82
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isopub.txt90
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isotech.txt168
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlalias.txt554
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlextra-wide.txt113
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlextra.txt87
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/s5defs.txt68
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-lat1.txt102
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-special.txt37
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-symbol.txt130
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/__init__.py40
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/af.py108
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ar.py99
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ca.py130
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/cs.py110
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/da.py114
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/de.py107
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/en.py114
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/eo.py119
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/es.py123
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fa.py102
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fi.py98
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fr.py108
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/gl.py106
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/he.py110
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/it.py99
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ja.py119
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ka.py90
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ko.py111
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/lt.py109
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/lv.py108
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/nl.py114
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/pl.py101
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/pt_br.py110
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ru.py90
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/sk.py96
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/sv.py96
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/uk.py91
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/zh_cn.py104
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/zh_tw.py109
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/roles.py439
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/states.py3145
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/parsers/rst/tableparser.py539
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/readers/__init__.py113
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/readers/doctree.py46
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/readers/pep.py48
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/readers/standalone.py65
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/statemachine.py1525
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/transforms/__init__.py185
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/transforms/components.py54
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/transforms/frontmatter.py540
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/transforms/misc.py144
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/transforms/parts.py176
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/transforms/peps.py308
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/transforms/references.py924
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/transforms/universal.py335
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/transforms/writer_aux.py99
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/__init__.py861
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/code_analyzer.py136
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/error_reporting.py222
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/__init__.py73
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/latex2mathml.py1252
-rwxr-xr-x.venv/lib/python3.12/site-packages/docutils/utils/math/math2html.py3165
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/mathalphabet2unichar.py892
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/mathml_elements.py478
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py261
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/tex2unichar.py730
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/math/unichar2tex.py808
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/punctuation_chars.py123
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/roman.py154
-rwxr-xr-x.venv/lib/python3.12/site-packages/docutils/utils/smartquotes.py1004
-rw-r--r--.venv/lib/python3.12/site-packages/docutils/utils/urischemes.py138
-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
202 files changed, 52224 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/docutils/__init__.py b/.venv/lib/python3.12/site-packages/docutils/__init__.py
new file mode 100644
index 00000000..16af4108
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/__init__.py
@@ -0,0 +1,291 @@
+# $Id: __init__.py 9649 2024-04-23 18:54:26Z grubert $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+This is the Docutils (Python Documentation Utilities) package.
+
+Package Structure
+=================
+
+Modules:
+
+- __init__.py: Contains component base classes, exception classes, and
+ Docutils version information.
+
+- core.py: Contains the ``Publisher`` class and ``publish_*()`` convenience
+ functions.
+
+- frontend.py: Runtime settings (command-line interface, configuration files)
+ processing, for Docutils front-ends.
+
+- io.py: Provides a uniform API for low-level input and output.
+
+- nodes.py: Docutils document tree (doctree) node class library.
+
+- statemachine.py: A finite state machine specialized for
+ regular-expression-based text filters.
+
+Subpackages:
+
+- languages: Language-specific mappings of terms.
+
+- parsers: Syntax-specific input parser modules or packages.
+
+- readers: Context-specific input handlers which understand the data
+ source and manage a parser.
+
+- transforms: Modules used by readers and writers to modify
+ the Docutils document tree.
+
+- utils: Contains the ``Reporter`` system warning class and miscellaneous
+ utilities used by readers, writers, and transforms.
+
+ utils/urischemes.py: Contains a complete mapping of known URI addressing
+ scheme names to descriptions.
+
+- utils/math: Contains functions for conversion of mathematical notation
+ between different formats (LaTeX, MathML, text, ...).
+
+- writers: Format-specific output translators.
+"""
+
+from collections import namedtuple
+
+__docformat__ = 'reStructuredText'
+
+__version__ = '0.21.2'
+"""Docutils version identifier (complies with PEP 440)::
+
+ major.minor[.micro][releaselevel[serial]][.dev]
+
+For version comparison operations, use `__version_info__` (see, below)
+rather than parsing the text of `__version__`.
+
+https://docutils.sourceforge.io/docs/dev/policies.html#version-identification
+"""
+
+__version_details__ = ''
+"""Optional extra version details (e.g. 'snapshot 2005-05-29, r3410').
+
+For development and release status, use `__version__ and `__version_info__`.
+"""
+
+
+class VersionInfo(namedtuple('VersionInfo',
+ 'major minor micro releaselevel serial release')):
+
+ def __new__(cls, major=0, minor=0, micro=0,
+ releaselevel='final', serial=0, release=True):
+ releaselevels = ('alpha', 'beta', 'candidate', 'final')
+ if releaselevel not in releaselevels:
+ raise ValueError('releaselevel must be one of %r.'
+ % (releaselevels, ))
+ if releaselevel == 'final':
+ if not release:
+ raise ValueError('releaselevel "final" must not be used '
+ 'with development versions (leads to wrong '
+ 'version ordering of the related __version__')
+ # cf. https://peps.python.org/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering # noqa
+ if serial != 0:
+ raise ValueError('"serial" must be 0 for final releases')
+
+ return super().__new__(cls, major, minor, micro,
+ releaselevel, serial, release)
+
+ def __lt__(self, other):
+ if isinstance(other, tuple):
+ other = VersionInfo(*other)
+ return tuple.__lt__(self, other)
+
+ def __gt__(self, other):
+ if isinstance(other, tuple):
+ other = VersionInfo(*other)
+ return tuple.__gt__(self, other)
+
+ def __le__(self, other):
+ if isinstance(other, tuple):
+ other = VersionInfo(*other)
+ return tuple.__le__(self, other)
+
+ def __ge__(self, other):
+ if isinstance(other, tuple):
+ other = VersionInfo(*other)
+ return tuple.__ge__(self, other)
+
+
+__version_info__ = VersionInfo(
+ major=0,
+ minor=21,
+ micro=2,
+ releaselevel='final', # one of 'alpha', 'beta', 'candidate', 'final'
+ serial=0, # pre-release number (0 for final releases and snapshots)
+ release=True # True for official releases and pre-releases
+ )
+"""Comprehensive version information tuple.
+
+https://docutils.sourceforge.io/docs/dev/policies.html#version-identification
+"""
+
+
+class ApplicationError(Exception): pass
+class DataError(ApplicationError): pass
+
+
+class SettingsSpec:
+
+ """
+ Runtime setting specification base class.
+
+ SettingsSpec subclass objects used by `docutils.frontend.OptionParser`.
+ """
+
+ # TODO: replace settings_specs with a new data structure
+ # Backwards compatiblity:
+ # Drop-in components:
+ # Sphinx supplies settings_spec in the current format in some places
+ # Myst parser provides a settings_spec tuple
+ #
+ # Sphinx reads a settings_spec in order to set a default value
+ # in writers/html.py:59
+ # https://github.com/sphinx-doc/sphinx/blob/4.x/sphinx/writers/html.py
+ # This should be changed (before retiring the old format)
+ # to use `settings_default_overrides` instead.
+ settings_spec = ()
+ """Runtime settings specification. Override in subclasses.
+
+ Defines runtime settings and associated command-line options, as used by
+ `docutils.frontend.OptionParser`. This is a tuple of:
+
+ - Option group title (string or `None` which implies no group, just a list
+ of single options).
+
+ - Description (string or `None`).
+
+ - A sequence of option tuples. Each consists of:
+
+ - Help text (string)
+
+ - List of option strings (e.g. ``['-Q', '--quux']``).
+
+ - Dictionary of keyword arguments sent to the OptionParser/OptionGroup
+ ``add_option`` method.
+
+ Runtime setting names are derived implicitly from long option names
+ ('--a-setting' becomes ``settings.a_setting``) or explicitly from the
+ 'dest' keyword argument.
+
+ Most settings will also have a 'validator' keyword & function. The
+ validator function validates setting values (from configuration files
+ and command-line option arguments) and converts them to appropriate
+ types. For example, the ``docutils.frontend.validate_boolean``
+ function, **required by all boolean settings**, converts true values
+ ('1', 'on', 'yes', and 'true') to 1 and false values ('0', 'off',
+ 'no', 'false', and '') to 0. Validators need only be set once per
+ setting. See the `docutils.frontend.validate_*` functions.
+
+ See the optparse docs for more details.
+
+ - More triples of group title, description, options, as many times as
+ needed. Thus, `settings_spec` tuples can be simply concatenated.
+ """
+
+ settings_defaults = None
+ """A dictionary of defaults for settings not in `settings_spec` (internal
+ settings, intended to be inaccessible by command-line and config file).
+ Override in subclasses."""
+
+ settings_default_overrides = None
+ """A dictionary of auxiliary defaults, to override defaults for settings
+ defined in other components' `setting_specs`. Override in subclasses."""
+
+ relative_path_settings = ()
+ """Settings containing filesystem paths. Override in subclasses.
+ Settings listed here are to be interpreted relative to the current working
+ directory."""
+
+ config_section = None
+ """The name of the config file section specific to this component
+ (lowercase, no brackets). Override in subclasses."""
+
+ config_section_dependencies = None
+ """A list of names of config file sections that are to be applied before
+ `config_section`, in order (from general to specific). In other words,
+ the settings in `config_section` are to be overlaid on top of the settings
+ from these sections. The "general" section is assumed implicitly.
+ Override in subclasses."""
+
+
+class TransformSpec:
+ """
+ Runtime transform specification base class.
+
+ Provides the interface to register "transforms" and helper functions
+ to resolve references with a `docutils.transforms.Transformer`.
+
+ https://docutils.sourceforge.io/docs/ref/transforms.html
+ """
+
+ def get_transforms(self):
+ """Transforms required by this class. Override in subclasses."""
+ if self.default_transforms != ():
+ import warnings
+ warnings.warn('TransformSpec: the "default_transforms" attribute '
+ 'will be removed in Docutils 2.0.\n'
+ 'Use get_transforms() method instead.',
+ DeprecationWarning)
+ return list(self.default_transforms)
+ return []
+
+ # Deprecated; for compatibility.
+ default_transforms = ()
+
+ unknown_reference_resolvers = ()
+ """List of functions to try to resolve unknown references.
+
+ Unknown references have a 'refname' attribute which doesn't correspond
+ to any target in the document. Called when the transforms in
+ `docutils.transforms.references` are unable to find a correct target.
+
+ The list should contain functions which will try to resolve unknown
+ references, with the following signature::
+
+ def reference_resolver(node):
+ '''Returns boolean: true if resolved, false if not.'''
+
+ If the function is able to resolve the reference, it should also remove
+ the 'refname' attribute and mark the node as resolved::
+
+ del node['refname']
+ node.resolved = 1
+
+ Each function must have a "priority" attribute which will affect the order
+ the unknown_reference_resolvers are run::
+
+ reference_resolver.priority = 100
+
+ This hook is provided for 3rd party extensions.
+ Example use case: the `MoinMoin - ReStructured Text Parser`
+ in ``sandbox/mmgilbe/rst.py``.
+ """
+
+
+class Component(SettingsSpec, TransformSpec):
+
+ """Base class for Docutils components."""
+
+ component_type = None
+ """Name of the component type ('reader', 'parser', 'writer'). Override in
+ subclasses."""
+
+ supported = ()
+ """Name and aliases for this component. Override in subclasses."""
+
+ def supports(self, format):
+ """
+ Is `format` supported by this component?
+
+ To be used by transforms to ask the dependent component if it supports
+ a certain input context or output format.
+ """
+ return format in self.supported
diff --git a/.venv/lib/python3.12/site-packages/docutils/__main__.py b/.venv/lib/python3.12/site-packages/docutils/__main__.py
new file mode 100755
index 00000000..ce614891
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/__main__.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+# :Copyright: © 2020, 2022 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
+#
+# Revision: $Revision: 9107 $
+# Date: $Date: 2022-07-06 15:59:57 +0200 (Mi, 06. Jul 2022) $
+
+"""Generic command line interface for the `docutils` package.
+
+See also
+https://docs.python.org/3/library/__main__.html#main-py-in-python-packages
+"""
+
+import argparse
+import locale
+import sys
+
+import docutils
+from docutils.core import Publisher, publish_cmdline, default_description
+
+
+class CliSettingsSpec(docutils.SettingsSpec):
+ """Runtime settings & command-line options for the generic CLI.
+
+ Configurable reader, parser, and writer components.
+
+ The "--writer" default will change to 'html' in Docutils 2.0
+ when 'html' becomes an alias for the current value 'html5'.
+ """
+
+ settings_spec = (
+ 'Docutils Application Options',
+ 'Reader, writer, and parser settings influence the available options. '
+ ' Example: use `--help --writer=latex` to see LaTeX writer options. ',
+ # options: ('help text', [<option strings>], {<keyword arguments>})
+ (('Reader name (currently: "%default").',
+ ['--reader'], {'default': 'standalone', 'metavar': '<reader>'}),
+ ('Parser name (currently: "%default").',
+ ['--parser'], {'default': 'rst', 'metavar': '<parser>'}),
+ ('Writer name (currently: "%default").',
+ ['--writer'], {'default': 'html5', 'metavar': '<writer>'}),
+ )
+ )
+ config_section = 'docutils application'
+ config_section_dependencies = ('docutils-cli application', # back-compat
+ 'applications')
+
+
+def main():
+ """Generic command line interface for the Docutils Publisher.
+ """
+ locale.setlocale(locale.LC_ALL, '')
+
+ description = ('Convert documents into useful formats. '
+ + default_description)
+
+ # Update component selection from config file(s)
+ components = Publisher().get_settings(settings_spec=CliSettingsSpec)
+
+ # Update component selection from command-line
+ argparser = argparse.ArgumentParser(add_help=False, allow_abbrev=False)
+ argparser.add_argument('--reader', default=components.reader)
+ argparser.add_argument('--parser', default=components.parser)
+ argparser.add_argument('--writer', default=components.writer)
+ # other options are parsed in a second pass via `publish_cmdline()`
+ (args, remainder) = argparser.parse_known_args()
+ # Ensure the current component selections are shown in help:
+ CliSettingsSpec.settings_default_overrides = args.__dict__
+
+ try:
+ publish_cmdline(reader_name=args.reader,
+ parser_name=args.parser,
+ writer_name=args.writer,
+ settings_spec=CliSettingsSpec,
+ description=description,
+ argv=remainder)
+ except ImportError as error:
+ print('%s.' % error, file=sys.stderr)
+ if '--traceback' in remainder:
+ raise
+ else:
+ print('Use "--traceback" to show details.')
+
+
+if __name__ == '__main__':
+ if sys.argv[0].endswith('__main__.py'):
+ # fix "usage" message
+ sys.argv[0] = '%s -m docutils' % sys.executable
+ main()
diff --git a/.venv/lib/python3.12/site-packages/docutils/core.py b/.venv/lib/python3.12/site-packages/docutils/core.py
new file mode 100644
index 00000000..adf80759
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/core.py
@@ -0,0 +1,780 @@
+# $Id: core.py 9369 2023-05-02 23:04:27Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Calling the ``publish_*`` convenience functions (or instantiating a
+`Publisher` object) with component names will result in default
+behavior. For custom behavior (setting component options), create
+custom component objects first, and pass *them* to
+``publish_*``/`Publisher`. See `The Docutils Publisher`_.
+
+.. _The Docutils Publisher:
+ https://docutils.sourceforge.io/docs/api/publisher.html
+"""
+
+__docformat__ = 'reStructuredText'
+
+import locale
+import pprint
+import os
+import sys
+import warnings
+
+from docutils import (__version__, __version_details__, SettingsSpec,
+ io, utils, readers, writers)
+from docutils.frontend import OptionParser
+from docutils.readers import doctree
+
+
+class Publisher:
+
+ """
+ A facade encapsulating the high-level logic of a Docutils system.
+ """
+
+ def __init__(self, reader=None, parser=None, writer=None,
+ source=None, source_class=io.FileInput,
+ destination=None, destination_class=io.FileOutput,
+ settings=None):
+ """
+ Initial setup. If any of `reader`, `parser`, or `writer` are not
+ specified, ``set_components()`` or the corresponding ``set_...()``
+ method should be called with component names
+ (`set_reader` sets the parser as well).
+ """
+
+ self.document = None
+ """The document tree (`docutils.nodes` objects)."""
+
+ self.reader = reader
+ """A `docutils.readers.Reader` instance."""
+
+ self.parser = parser
+ """A `docutils.parsers.Parser` instance."""
+
+ self.writer = writer
+ """A `docutils.writers.Writer` instance."""
+
+ for component in 'reader', 'parser', 'writer':
+ assert not isinstance(getattr(self, component), str), (
+ 'passed string "%s" as "%s" parameter; pass an instance, '
+ 'or use the "%s_name" parameter instead (in '
+ 'docutils.core.publish_* convenience functions).'
+ % (getattr(self, component), component, component))
+
+ self.source = source
+ """The source of input data, a `docutils.io.Input` instance."""
+
+ self.source_class = source_class
+ """The class for dynamically created source objects."""
+
+ self.destination = destination
+ """The destination for docutils output, a `docutils.io.Output`
+ instance."""
+
+ self.destination_class = destination_class
+ """The class for dynamically created destination objects."""
+
+ self.settings = settings
+ """An object containing Docutils settings as instance attributes.
+ Set by `self.process_command_line()` or `self.get_settings()`."""
+
+ self._stderr = io.ErrorOutput()
+
+ def set_reader(self, reader_name, parser, parser_name):
+ """Set `self.reader` by name."""
+ reader_class = readers.get_reader_class(reader_name)
+ self.reader = reader_class(parser, parser_name)
+ self.parser = self.reader.parser
+
+ def set_writer(self, writer_name):
+ """Set `self.writer` by name."""
+ writer_class = writers.get_writer_class(writer_name)
+ self.writer = writer_class()
+
+ def set_components(self, reader_name, parser_name, writer_name):
+ if self.reader is None:
+ self.set_reader(reader_name, self.parser, parser_name)
+ if self.parser is None:
+ if self.reader.parser is None:
+ self.reader.set_parser(parser_name)
+ self.parser = self.reader.parser
+ if self.writer is None:
+ self.set_writer(writer_name)
+
+ def setup_option_parser(self, usage=None, description=None,
+ settings_spec=None, config_section=None,
+ **defaults):
+ warnings.warn('Publisher.setup_option_parser is deprecated, '
+ 'and will be removed in Docutils 0.21.',
+ DeprecationWarning, stacklevel=2)
+ if config_section:
+ if not settings_spec:
+ settings_spec = SettingsSpec()
+ settings_spec.config_section = config_section
+ parts = config_section.split()
+ if len(parts) > 1 and parts[-1] == 'application':
+ settings_spec.config_section_dependencies = ['applications']
+ # @@@ Add self.source & self.destination to components in future?
+ return OptionParser(
+ components=(self.parser, self.reader, self.writer, settings_spec),
+ defaults=defaults, read_config_files=True,
+ usage=usage, description=description)
+
+ def _setup_settings_parser(self, *args, **kwargs):
+ # Provisional: will change (docutils.frontend.OptionParser will
+ # be replaced by a parser based on arparse.ArgumentParser)
+ # and may be removed later.
+ with warnings.catch_warnings():
+ warnings.filterwarnings('ignore', category=DeprecationWarning)
+ return self.setup_option_parser(*args, **kwargs)
+
+ def get_settings(self, usage=None, description=None,
+ settings_spec=None, config_section=None, **defaults):
+ """
+ Return settings from components and config files.
+
+ Please set components first (`self.set_reader` & `self.set_writer`).
+ Use keyword arguments to override component defaults
+ (before updating from configuration files).
+
+ Calling this function also sets `self.settings` which makes
+ `self.publish()` skip parsing command line options.
+ """
+ option_parser = self._setup_settings_parser(
+ usage, description, settings_spec, config_section, **defaults)
+ self.settings = option_parser.get_default_values()
+ return self.settings
+
+ def process_programmatic_settings(self, settings_spec,
+ settings_overrides,
+ config_section):
+ if self.settings is None:
+ defaults = settings_overrides.copy() if settings_overrides else {}
+ # Propagate exceptions by default when used programmatically:
+ defaults.setdefault('traceback', True)
+ self.get_settings(settings_spec=settings_spec,
+ config_section=config_section,
+ **defaults)
+
+ def process_command_line(self, argv=None, usage=None, description=None,
+ settings_spec=None, config_section=None,
+ **defaults):
+ """
+ Parse command line arguments and set ``self.settings``.
+
+ Pass an empty sequence to `argv` to avoid reading `sys.argv`
+ (the default behaviour).
+
+ Set components first (`self.set_reader` & `self.set_writer`).
+ """
+ option_parser = self._setup_settings_parser(
+ usage, description, settings_spec, config_section, **defaults)
+ if argv is None:
+ argv = sys.argv[1:]
+ self.settings = option_parser.parse_args(argv)
+
+ def set_io(self, source_path=None, destination_path=None):
+ if self.source is None:
+ self.set_source(source_path=source_path)
+ if self.destination is None:
+ self.set_destination(destination_path=destination_path)
+
+ def set_source(self, source=None, source_path=None):
+ if source_path is None:
+ source_path = self.settings._source
+ else:
+ self.settings._source = source_path
+ self.source = self.source_class(
+ source=source, source_path=source_path,
+ encoding=self.settings.input_encoding,
+ error_handler=self.settings.input_encoding_error_handler)
+
+ def set_destination(self, destination=None, destination_path=None):
+ if destination_path is None:
+ if (self.settings.output and self.settings._destination
+ and self.settings.output != self.settings._destination):
+ raise SystemExit('The positional argument <destination> is '
+ 'obsoleted by the --output option. '
+ 'You cannot use them together.')
+ if self.settings.output == '-': # means stdout
+ self.settings.output = None
+ destination_path = (self.settings.output
+ or self.settings._destination)
+ self.settings._destination = destination_path
+ self.destination = self.destination_class(
+ destination=destination,
+ destination_path=destination_path,
+ encoding=self.settings.output_encoding,
+ error_handler=self.settings.output_encoding_error_handler)
+
+ def apply_transforms(self):
+ self.document.transformer.populate_from_components(
+ (self.source, self.reader, self.reader.parser, self.writer,
+ self.destination))
+ self.document.transformer.apply_transforms()
+
+ def publish(self, argv=None, usage=None, description=None,
+ settings_spec=None, settings_overrides=None,
+ config_section=None, enable_exit_status=False):
+ """
+ Process command line options and arguments (if `self.settings` not
+ already set), run `self.reader` and then `self.writer`. Return
+ `self.writer`'s output.
+ """
+ exit = None
+ try:
+ if self.settings is None:
+ self.process_command_line(
+ argv, usage, description, settings_spec, config_section,
+ **(settings_overrides or {}))
+ self.set_io()
+ self.prompt()
+ self.document = self.reader.read(self.source, self.parser,
+ self.settings)
+ self.apply_transforms()
+ output = self.writer.write(self.document, self.destination)
+ self.writer.assemble_parts()
+ except SystemExit as error:
+ exit = True
+ exit_status = error.code
+ except Exception as error:
+ if not self.settings: # exception too early to report nicely
+ raise
+ if self.settings.traceback: # Propagate exceptions?
+ self.debugging_dumps()
+ raise
+ self.report_Exception(error)
+ exit = True
+ exit_status = 1
+ self.debugging_dumps()
+ if (enable_exit_status and self.document
+ and (self.document.reporter.max_level
+ >= self.settings.exit_status_level)):
+ sys.exit(self.document.reporter.max_level + 10)
+ elif exit:
+ sys.exit(exit_status)
+ return output
+
+ def debugging_dumps(self):
+ if not self.document:
+ return
+ if self.settings.dump_settings:
+ print('\n::: Runtime settings:', file=self._stderr)
+ print(pprint.pformat(self.settings.__dict__), file=self._stderr)
+ if self.settings.dump_internals:
+ print('\n::: Document internals:', file=self._stderr)
+ print(pprint.pformat(self.document.__dict__), file=self._stderr)
+ if self.settings.dump_transforms:
+ print('\n::: Transforms applied:', file=self._stderr)
+ print(' (priority, transform class, pending node details, '
+ 'keyword args)', file=self._stderr)
+ print(pprint.pformat(
+ [(priority, '%s.%s' % (xclass.__module__, xclass.__name__),
+ pending and pending.details, kwargs)
+ for priority, xclass, pending, kwargs
+ in self.document.transformer.applied]), file=self._stderr)
+ if self.settings.dump_pseudo_xml:
+ print('\n::: Pseudo-XML:', file=self._stderr)
+ print(self.document.pformat().encode(
+ 'raw_unicode_escape'), file=self._stderr)
+
+ def prompt(self):
+ """Print info and prompt when waiting for input from a terminal."""
+ try:
+ if not (self.source.isatty() and self._stderr.isatty()):
+ return
+ except AttributeError:
+ return
+ eot_key = 'Ctrl+Z' if os.name == 'nt' else 'Ctrl+D'
+ in_format = ''
+ out_format = 'useful formats'
+ try:
+ in_format = self.parser.supported[0]
+ out_format = self.writer.supported[0]
+ except (AttributeError, IndexError):
+ pass
+ print(f'Docutils {__version__} <https://docutils.sourceforge.io>\n'
+ f'converting "{in_format}" into "{out_format}".\n'
+ f'Call with option "--help" for more info.\n'
+ f'.. Waiting for source text (finish with {eot_key} '
+ 'on an empty line):',
+ file=self._stderr)
+
+ def report_Exception(self, error):
+ if isinstance(error, utils.SystemMessage):
+ self.report_SystemMessage(error)
+ elif isinstance(error, UnicodeEncodeError):
+ self.report_UnicodeError(error)
+ elif isinstance(error, io.InputError):
+ self._stderr.write('Unable to open source file for reading:\n'
+ ' %s\n' % io.error_string(error))
+ elif isinstance(error, io.OutputError):
+ self._stderr.write(
+ 'Unable to open destination file for writing:\n'
+ ' %s\n' % io.error_string(error))
+ else:
+ print('%s' % io.error_string(error), file=self._stderr)
+ print(f"""\
+Exiting due to error. Use "--traceback" to diagnose.
+Please report errors to <docutils-users@lists.sourceforge.net>.
+Include "--traceback" output, Docutils version ({__version__}\
+{f' [{__version_details__}]' if __version_details__ else ''}),
+Python version ({sys.version.split()[0]}), your OS type & version, \
+and the command line used.""", file=self._stderr)
+
+ def report_SystemMessage(self, error):
+ print('Exiting due to level-%s (%s) system message.' % (
+ error.level, utils.Reporter.levels[error.level]),
+ file=self._stderr)
+
+ def report_UnicodeError(self, error):
+ data = error.object[error.start:error.end]
+ self._stderr.write(
+ '%s\n'
+ '\n'
+ 'The specified output encoding (%s) cannot\n'
+ 'handle all of the output.\n'
+ 'Try setting "--output-encoding-error-handler" to\n'
+ '\n'
+ '* "xmlcharrefreplace" (for HTML & XML output);\n'
+ ' the output will contain "%s" and should be usable.\n'
+ '* "backslashreplace" (for other output formats);\n'
+ ' look for "%s" in the output.\n'
+ '* "replace"; look for "?" in the output.\n'
+ '\n'
+ '"--output-encoding-error-handler" is currently set to "%s".\n'
+ '\n'
+ 'Exiting due to error. Use "--traceback" to diagnose.\n'
+ 'If the advice above doesn\'t eliminate the error,\n'
+ 'please report it to <docutils-users@lists.sourceforge.net>.\n'
+ 'Include "--traceback" output, Docutils version (%s),\n'
+ 'Python version (%s), your OS type & version, and the\n'
+ 'command line used.\n'
+ % (io.error_string(error),
+ self.settings.output_encoding,
+ data.encode('ascii', 'xmlcharrefreplace'),
+ data.encode('ascii', 'backslashreplace'),
+ self.settings.output_encoding_error_handler,
+ __version__, sys.version.split()[0]))
+
+
+default_usage = '%prog [options] [<source> [<destination>]]'
+default_description = (
+ 'Reads from <source> (default is stdin) '
+ 'and writes to <destination> (default is stdout). '
+ 'See https://docutils.sourceforge.io/docs/user/config.html '
+ 'for a detailed settings reference.')
+
+
+# TODO: or not to do? cf. https://clig.dev/#help
+#
+# Display output on success, but keep it brief.
+# Provide a -q option to suppress all non-essential output.
+#
+# Chain several args as input and use --output or redirection for output:
+# argparser.add_argument('source', nargs='+')
+#
+def publish_cmdline(reader=None, reader_name='standalone',
+ parser=None, parser_name='restructuredtext',
+ writer=None, writer_name='pseudoxml',
+ settings=None, settings_spec=None,
+ settings_overrides=None, config_section=None,
+ enable_exit_status=True, argv=None,
+ usage=default_usage, description=default_description):
+ """
+ Set up & run a `Publisher` for command-line-based file I/O (input and
+ output file paths taken automatically from the command line).
+ Also return the output as `str` or `bytes` (for binary output document
+ formats).
+
+ Parameters: see `publish_programmatically()` for the remainder.
+
+ - `argv`: Command-line argument list to use instead of ``sys.argv[1:]``.
+ - `usage`: Usage string, output if there's a problem parsing the command
+ line.
+ - `description`: Program description, output for the "--help" option
+ (along with command-line option descriptions).
+ """
+ publisher = Publisher(reader, parser, writer, settings=settings)
+ publisher.set_components(reader_name, parser_name, writer_name)
+ output = publisher.publish(
+ argv, usage, description, settings_spec, settings_overrides,
+ config_section=config_section, enable_exit_status=enable_exit_status)
+ return output
+
+
+def publish_file(source=None, source_path=None,
+ destination=None, destination_path=None,
+ reader=None, reader_name='standalone',
+ parser=None, parser_name='restructuredtext',
+ writer=None, writer_name='pseudoxml',
+ settings=None, settings_spec=None, settings_overrides=None,
+ config_section=None, enable_exit_status=False):
+ """
+ Set up & run a `Publisher` for programmatic use with file-like I/O.
+ Also return the output as `str` or `bytes` (for binary output document
+ formats).
+
+ Parameters: see `publish_programmatically()`.
+ """
+ output, publisher = publish_programmatically(
+ source_class=io.FileInput, source=source, source_path=source_path,
+ destination_class=io.FileOutput,
+ destination=destination, destination_path=destination_path,
+ reader=reader, reader_name=reader_name,
+ parser=parser, parser_name=parser_name,
+ writer=writer, writer_name=writer_name,
+ settings=settings, settings_spec=settings_spec,
+ settings_overrides=settings_overrides,
+ config_section=config_section,
+ enable_exit_status=enable_exit_status)
+ return output
+
+
+def publish_string(source, source_path=None, destination_path=None,
+ reader=None, reader_name='standalone',
+ parser=None, parser_name='restructuredtext',
+ writer=None, writer_name='pseudoxml',
+ settings=None, settings_spec=None,
+ settings_overrides=None, config_section=None,
+ enable_exit_status=False):
+ """
+ Set up & run a `Publisher` for programmatic use with string I/O.
+
+ Accepts a `bytes` or `str` instance as `source`.
+
+ The output is encoded according to the `output_encoding`_ setting;
+ the return value is a `bytes` instance (unless `output_encoding`_ is
+ "unicode", cf. `docutils.io.StringOutput.write()`).
+
+ Parameters: see `publish_programmatically()`.
+
+ This function is provisional because in Python 3 name and behaviour
+ no longer match.
+
+ .. _output_encoding:
+ https://docutils.sourceforge.io/docs/user/config.html#output-encoding
+ """
+ output, publisher = publish_programmatically(
+ source_class=io.StringInput, source=source, source_path=source_path,
+ destination_class=io.StringOutput,
+ destination=None, destination_path=destination_path,
+ reader=reader, reader_name=reader_name,
+ parser=parser, parser_name=parser_name,
+ writer=writer, writer_name=writer_name,
+ settings=settings, settings_spec=settings_spec,
+ settings_overrides=settings_overrides,
+ config_section=config_section,
+ enable_exit_status=enable_exit_status)
+ return output
+
+
+def publish_parts(source, source_path=None, source_class=io.StringInput,
+ destination_path=None,
+ reader=None, reader_name='standalone',
+ parser=None, parser_name='restructuredtext',
+ writer=None, writer_name='pseudoxml',
+ settings=None, settings_spec=None,
+ settings_overrides=None, config_section=None,
+ enable_exit_status=False):
+ """
+ Set up & run a `Publisher`, and return a dictionary of document parts.
+
+ Dictionary keys are the names of parts.
+ Dictionary values are `str` instances; encoding is up to the client,
+ e.g.::
+
+ parts = publish_parts(...)
+ body = parts['body'].encode(parts['encoding'], parts['errors'])
+
+ See the `API documentation`__ for details on the provided parts.
+
+ Parameters: see `publish_programmatically()`.
+
+ __ https://docutils.sourceforge.io/docs/api/publisher.html#publish-parts
+ """
+ output, publisher = publish_programmatically(
+ source=source, source_path=source_path, source_class=source_class,
+ destination_class=io.StringOutput,
+ destination=None, destination_path=destination_path,
+ reader=reader, reader_name=reader_name,
+ parser=parser, parser_name=parser_name,
+ writer=writer, writer_name=writer_name,
+ settings=settings, settings_spec=settings_spec,
+ settings_overrides=settings_overrides,
+ config_section=config_section,
+ enable_exit_status=enable_exit_status)
+ return publisher.writer.parts
+
+
+def publish_doctree(source, source_path=None,
+ source_class=io.StringInput,
+ reader=None, reader_name='standalone',
+ parser=None, parser_name='restructuredtext',
+ settings=None, settings_spec=None,
+ settings_overrides=None, config_section=None,
+ enable_exit_status=False):
+ """
+ Set up & run a `Publisher` for programmatic use. Return a document tree.
+
+ Parameters: see `publish_programmatically()`.
+ """
+ _output, publisher = publish_programmatically(
+ source=source, source_path=source_path,
+ source_class=source_class,
+ destination=None, destination_path=None,
+ destination_class=io.NullOutput,
+ reader=reader, reader_name=reader_name,
+ parser=parser, parser_name=parser_name,
+ writer=None, writer_name='null',
+ settings=settings, settings_spec=settings_spec,
+ settings_overrides=settings_overrides, config_section=config_section,
+ enable_exit_status=enable_exit_status)
+ return publisher.document
+
+
+def publish_from_doctree(document, destination_path=None,
+ writer=None, writer_name='pseudoxml',
+ settings=None, settings_spec=None,
+ settings_overrides=None, config_section=None,
+ enable_exit_status=False):
+ """
+ Set up & run a `Publisher` to render from an existing document tree
+ data structure. For programmatic use with string output
+ (`bytes` or `str`, cf. `publish_string()`).
+
+ Note that ``document.settings`` is overridden; if you want to use the
+ settings of the original `document`, pass ``settings=document.settings``.
+
+ Also, new `document.transformer` and `document.reporter` objects are
+ generated.
+
+ Parameters: `document` is a `docutils.nodes.document` object, an existing
+ document tree.
+
+ Other parameters: see `publish_programmatically()`.
+
+ This function is provisional because in Python 3 name and behaviour
+ of the `io.StringOutput` class no longer match.
+ """
+ reader = doctree.Reader(parser_name='null')
+ publisher = Publisher(reader, None, writer,
+ source=io.DocTreeInput(document),
+ destination_class=io.StringOutput,
+ settings=settings)
+ if not writer and writer_name:
+ publisher.set_writer(writer_name)
+ publisher.process_programmatic_settings(
+ settings_spec, settings_overrides, config_section)
+ publisher.set_destination(None, destination_path)
+ return publisher.publish(enable_exit_status=enable_exit_status)
+
+
+def publish_cmdline_to_binary(reader=None, reader_name='standalone',
+ parser=None, parser_name='restructuredtext',
+ writer=None, writer_name='pseudoxml',
+ settings=None,
+ settings_spec=None,
+ settings_overrides=None,
+ config_section=None,
+ enable_exit_status=True,
+ argv=None,
+ usage=default_usage,
+ description=default_description,
+ destination=None,
+ destination_class=io.BinaryFileOutput):
+ """
+ Set up & run a `Publisher` for command-line-based file I/O (input and
+ output file paths taken automatically from the command line).
+ Also return the output as `bytes`.
+
+ This is just like publish_cmdline, except that it uses
+ io.BinaryFileOutput instead of io.FileOutput.
+
+ Parameters: see `publish_programmatically()` for the remainder.
+
+ - `argv`: Command-line argument list to use instead of ``sys.argv[1:]``.
+ - `usage`: Usage string, output if there's a problem parsing the command
+ line.
+ - `description`: Program description, output for the "--help" option
+ (along with command-line option descriptions).
+ """
+ publisher = Publisher(reader, parser, writer, settings=settings,
+ destination_class=destination_class)
+ publisher.set_components(reader_name, parser_name, writer_name)
+ output = publisher.publish(
+ argv, usage, description, settings_spec, settings_overrides,
+ config_section=config_section, enable_exit_status=enable_exit_status)
+ return output
+
+
+def publish_programmatically(source_class, source, source_path,
+ destination_class, destination, destination_path,
+ reader, reader_name,
+ parser, parser_name,
+ writer, writer_name,
+ settings, settings_spec,
+ settings_overrides, config_section,
+ enable_exit_status):
+ """
+ Set up & run a `Publisher` for custom programmatic use.
+
+ Return the output (as `str` or `bytes`, depending on `destination_class`,
+ writer, and the "output_encoding" setting) and the Publisher object.
+
+ Applications should not need to call this function directly. If it does
+ seem to be necessary to call this function directly, please write to the
+ Docutils-develop mailing list
+ <https://docutils.sourceforge.io/docs/user/mailing-lists.html#docutils-develop>.
+
+ Parameters:
+
+ * `source_class` **required**: The class for dynamically created source
+ objects. Typically `io.FileInput` or `io.StringInput`.
+
+ * `source`: Type depends on `source_class`:
+
+ - If `source_class` is `io.FileInput`: Either a file-like object
+ (must have 'read' and 'close' methods), or ``None``
+ (`source_path` is opened). If neither `source` nor
+ `source_path` are supplied, `sys.stdin` is used.
+
+ - If `source_class` is `io.StringInput` **required**:
+ The input as either a `bytes` object (ensure the 'input_encoding'
+ setting matches its encoding) or a `str` object.
+
+ * `source_path`: Type depends on `source_class`:
+
+ - `io.FileInput`: Path to the input file, opened if no `source`
+ supplied.
+
+ - `io.StringInput`: Optional. Path to the file or name of the
+ object that produced `source`. Only used for diagnostic output.
+
+ * `destination_class` **required**: The class for dynamically created
+ destination objects. Typically `io.FileOutput` or `io.StringOutput`.
+
+ * `destination`: Type depends on `destination_class`:
+
+ - `io.FileOutput`: Either a file-like object (must have 'write' and
+ 'close' methods), or ``None`` (`destination_path` is opened). If
+ neither `destination` nor `destination_path` are supplied,
+ `sys.stdout` is used.
+
+ - `io.StringOutput`: Not used; pass ``None``.
+
+ * `destination_path`: Type depends on `destination_class`:
+
+ - `io.FileOutput`: Path to the output file. Opened if no `destination`
+ supplied.
+
+ - `io.StringOutput`: Path to the file or object which will receive the
+ output; optional. Used for determining relative paths (stylesheets,
+ source links, etc.).
+
+ * `reader`: A `docutils.readers.Reader` object.
+
+ * `reader_name`: Name or alias of the Reader class to be instantiated if
+ no `reader` supplied.
+
+ * `parser`: A `docutils.parsers.Parser` object.
+
+ * `parser_name`: Name or alias of the Parser class to be instantiated if
+ no `parser` supplied.
+
+ * `writer`: A `docutils.writers.Writer` object.
+
+ * `writer_name`: Name or alias of the Writer class to be instantiated if
+ no `writer` supplied.
+
+ * `settings`: A runtime settings (`docutils.frontend.Values`) object, for
+ dotted-attribute access to runtime settings. It's the end result of the
+ `SettingsSpec`, config file, and option processing. If `settings` is
+ passed, it's assumed to be complete and no further setting/config/option
+ processing is done.
+
+ * `settings_spec`: A `docutils.SettingsSpec` subclass or object. Provides
+ extra application-specific settings definitions independently of
+ components. In other words, the application becomes a component, and
+ its settings data is processed along with that of the other components.
+ Used only if no `settings` specified.
+
+ * `settings_overrides`: A dictionary containing application-specific
+ settings defaults that override the defaults of other components.
+ Used only if no `settings` specified.
+
+ * `config_section`: A string, the name of the configuration file section
+ for this application. Overrides the ``config_section`` attribute
+ defined by `settings_spec`. Used only if no `settings` specified.
+
+ * `enable_exit_status`: Boolean; enable exit status at end of processing?
+ """
+ publisher = Publisher(reader, parser, writer, settings=settings,
+ source_class=source_class,
+ destination_class=destination_class)
+ publisher.set_components(reader_name, parser_name, writer_name)
+ publisher.process_programmatic_settings(
+ settings_spec, settings_overrides, config_section)
+ publisher.set_source(source, source_path)
+ publisher.set_destination(destination, destination_path)
+ output = publisher.publish(enable_exit_status=enable_exit_status)
+ return output, publisher
+
+
+# "Entry points" with functionality of the "tools/rst2*.py" scripts
+# cf. https://packaging.python.org/en/latest/specifications/entry-points/
+
+def rst2something(writer, documenttype, doc_path=''):
+ # Helper function for the common parts of rst2...
+ # writer: writer name
+ # documenttype: output document type
+ # doc_path: documentation path (relative to the documentation root)
+ description = (
+ f'Generate {documenttype} documents '
+ 'from standalone reStructuredText sources '
+ f'<https://docutils.sourceforge.io/docs/{doc_path}>. '
+ + default_description)
+ locale.setlocale(locale.LC_ALL, '')
+ publish_cmdline(writer_name=writer, description=description)
+
+
+def rst2html():
+ rst2something('html', 'HTML', 'user/html.html#html')
+
+
+def rst2html4():
+ rst2something('html4', 'XHTML 1.1', 'user/html.html#html4css1')
+
+
+def rst2html5():
+ rst2something('html5', 'HTML5', 'user/html.html#html5-polyglot')
+
+
+def rst2latex():
+ rst2something('latex', 'LaTeX', 'user/latex.html')
+
+
+def rst2man():
+ rst2something('manpage', 'Unix manual (troff)', 'user/manpage.html')
+
+
+def rst2odt():
+ rst2something('odt', 'OpenDocument text (ODT)', 'user/odt.html')
+
+
+def rst2pseudoxml():
+ rst2something('pseudoxml', 'pseudo-XML (test)', 'ref/doctree.html')
+
+
+def rst2s5():
+ rst2something('s5', 'S5 HTML slideshow', 'user/slide-shows.html')
+
+
+def rst2xetex():
+ rst2something('xetex', 'LaTeX (XeLaTeX/LuaLaTeX)', 'user/latex.html')
+
+
+def rst2xml():
+ rst2something('xml', 'Docutils-native XML', 'ref/doctree.html')
diff --git a/.venv/lib/python3.12/site-packages/docutils/docutils.conf b/.venv/lib/python3.12/site-packages/docutils/docutils.conf
new file mode 100644
index 00000000..cdce8d62
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/docutils.conf
@@ -0,0 +1,5 @@
+# This configuration file is to prevent tools/buildhtml.py from
+# processing text files in and below this directory.
+
+[buildhtml application]
+prune: .
diff --git a/.venv/lib/python3.12/site-packages/docutils/examples.py b/.venv/lib/python3.12/site-packages/docutils/examples.py
new file mode 100644
index 00000000..c27ab70d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/examples.py
@@ -0,0 +1,99 @@
+# $Id: examples.py 9026 2022-03-04 15:57:13Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+This module contains practical examples of Docutils client code.
+
+Importing this module from client code is not recommended; its contents are
+subject to change in future Docutils releases. Instead, it is recommended
+that you copy and paste the parts you need into your own code, modifying as
+necessary.
+"""
+
+from docutils import core, io
+
+
+def html_parts(input_string, source_path=None, destination_path=None,
+ input_encoding='unicode', doctitle=True,
+ initial_header_level=1):
+ """
+ Given an input string, returns a dictionary of HTML document parts.
+
+ Dictionary keys are the names of parts, and values are Unicode strings;
+ encoding is up to the client.
+
+ Parameters:
+
+ - `input_string`: A multi-line text string; required.
+ - `source_path`: Path to the source file or object. Optional, but useful
+ for diagnostic output (system messages).
+ - `destination_path`: Path to the file or object which will receive the
+ output; optional. Used for determining relative paths (stylesheets,
+ source links, etc.).
+ - `input_encoding`: The encoding of `input_string`. If it is an encoded
+ 8-bit string, provide the correct encoding. If it is a Unicode string,
+ use "unicode", the default.
+ - `doctitle`: Disable the promotion of a lone top-level section title to
+ document title (and subsequent section title to document subtitle
+ promotion); enabled by default.
+ - `initial_header_level`: The initial level for header elements (e.g. 1
+ for "<h1>").
+ """
+ overrides = {'input_encoding': input_encoding,
+ 'doctitle_xform': doctitle,
+ 'initial_header_level': initial_header_level}
+ parts = core.publish_parts(
+ source=input_string, source_path=source_path,
+ destination_path=destination_path,
+ writer_name='html', settings_overrides=overrides)
+ return parts
+
+
+def html_body(input_string, source_path=None, destination_path=None,
+ input_encoding='unicode', output_encoding='unicode',
+ doctitle=True, initial_header_level=1):
+ """
+ Given an input string, returns an HTML fragment as a string.
+
+ The return value is the contents of the <body> element.
+
+ Parameters (see `html_parts()` for the remainder):
+
+ - `output_encoding`: The desired encoding of the output. If a Unicode
+ string is desired, use the default value of "unicode" .
+ """
+ parts = html_parts(
+ input_string=input_string, source_path=source_path,
+ destination_path=destination_path,
+ input_encoding=input_encoding, doctitle=doctitle,
+ initial_header_level=initial_header_level)
+ fragment = parts['html_body']
+ if output_encoding != 'unicode':
+ fragment = fragment.encode(output_encoding)
+ return fragment
+
+
+def internals(input_string, source_path=None, destination_path=None,
+ input_encoding='unicode', settings_overrides=None):
+ """
+ Return the document tree and publisher, for exploring Docutils internals.
+
+ Parameters: see `html_parts()`.
+ """
+ if settings_overrides:
+ overrides = settings_overrides.copy()
+ else:
+ overrides = {}
+ overrides['input_encoding'] = input_encoding
+ output, pub = core.publish_programmatically(
+ source_class=io.StringInput, source=input_string,
+ source_path=source_path,
+ destination_class=io.NullOutput, destination=None,
+ destination_path=destination_path,
+ reader=None, reader_name='standalone',
+ parser=None, parser_name='restructuredtext',
+ writer=None, writer_name='null',
+ settings=None, settings_spec=None, settings_overrides=overrides,
+ config_section=None, enable_exit_status=None)
+ return pub.writer.document, pub
diff --git a/.venv/lib/python3.12/site-packages/docutils/frontend.py b/.venv/lib/python3.12/site-packages/docutils/frontend.py
new file mode 100644
index 00000000..2499c628
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/frontend.py
@@ -0,0 +1,1065 @@
+# $Id: frontend.py 9540 2024-02-17 10:36:59Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Command-line and common processing for Docutils front-end tools.
+
+This module is provisional.
+Major changes will happen with the switch from the deprecated
+"optparse" module to "arparse".
+
+Applications should use the high-level API provided by `docutils.core`.
+See https://docutils.sourceforge.io/docs/api/runtime-settings.html.
+
+Exports the following classes:
+
+* `OptionParser`: Standard Docutils command-line processing.
+ Deprecated. Will be replaced by an ArgumentParser.
+* `Option`: Customized version of `optparse.Option`; validation support.
+ Deprecated. Will be removed.
+* `Values`: Runtime settings; objects are simple structs
+ (``object.attribute``). Supports cumulative list settings (attributes).
+ Deprecated. Will be removed.
+* `ConfigParser`: Standard Docutils config file processing.
+ Provisional. Details will change.
+
+Also exports the following functions:
+
+Interface function:
+ `get_default_settings()`. New in 0.19.
+
+Option callbacks:
+ `store_multiple()`, `read_config_file()`. Deprecated.
+
+Setting validators:
+ `validate_encoding()`, `validate_encoding_error_handler()`,
+ `validate_encoding_and_error_handler()`,
+ `validate_boolean()`, `validate_ternary()`,
+ `validate_nonnegative_int()`, `validate_threshold()`,
+ `validate_colon_separated_string_list()`,
+ `validate_comma_separated_list()`,
+ `validate_url_trailing_slash()`,
+ `validate_dependency_file()`,
+ `validate_strip_class()`
+ `validate_smartquotes_locales()`.
+
+ Provisional.
+
+Misc:
+ `make_paths_absolute()`, `filter_settings_spec()`. Provisional.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import codecs
+import configparser
+import optparse
+from optparse import SUPPRESS_HELP
+import os
+import os.path
+from pathlib import Path
+import sys
+import warnings
+
+import docutils
+from docutils import io, utils
+
+
+def store_multiple(option, opt, value, parser, *args, **kwargs):
+ """
+ Store multiple values in `parser.values`. (Option callback.)
+
+ Store `None` for each attribute named in `args`, and store the value for
+ each key (attribute name) in `kwargs`.
+ """
+ for attribute in args:
+ setattr(parser.values, attribute, None)
+ for key, value in kwargs.items():
+ setattr(parser.values, key, value)
+
+
+def read_config_file(option, opt, value, parser):
+ """
+ Read a configuration file during option processing. (Option callback.)
+ """
+ try:
+ new_settings = parser.get_config_file_settings(value)
+ except ValueError as err:
+ parser.error(err)
+ parser.values.update(new_settings, parser)
+
+
+def validate_encoding(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ # All arguments except `value` are ignored
+ # (kept for compatibility with "optparse" module).
+ # If there is only one positional argument, it is interpreted as `value`.
+ if value is None:
+ value = setting
+ if value == '':
+ return None # allow overwriting a config file value
+ try:
+ codecs.lookup(value)
+ except LookupError:
+ raise LookupError('setting "%s": unknown encoding: "%s"'
+ % (setting, value))
+ return value
+
+
+def validate_encoding_error_handler(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ # All arguments except `value` are ignored
+ # (kept for compatibility with "optparse" module).
+ # If there is only one positional argument, it is interpreted as `value`.
+ if value is None:
+ value = setting
+ try:
+ codecs.lookup_error(value)
+ except LookupError:
+ raise LookupError(
+ 'unknown encoding error handler: "%s" (choices: '
+ '"strict", "ignore", "replace", "backslashreplace", '
+ '"xmlcharrefreplace", and possibly others; see documentation for '
+ 'the Python ``codecs`` module)' % value)
+ return value
+
+
+def validate_encoding_and_error_handler(
+ setting, value, option_parser, config_parser=None, config_section=None):
+ """
+ Side-effect: if an error handler is included in the value, it is inserted
+ into the appropriate place as if it were a separate setting/option.
+ """
+ if ':' in value:
+ encoding, handler = value.split(':')
+ validate_encoding_error_handler(handler)
+ if config_parser:
+ config_parser.set(config_section, setting + '_error_handler',
+ handler)
+ else:
+ setattr(option_parser.values, setting + '_error_handler', handler)
+ else:
+ encoding = value
+ return validate_encoding(encoding)
+
+
+def validate_boolean(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ """Check/normalize boolean settings:
+ True: '1', 'on', 'yes', 'true'
+ False: '0', 'off', 'no','false', ''
+
+ All arguments except `value` are ignored
+ (kept for compatibility with "optparse" module).
+ If there is only one positional argument, it is interpreted as `value`.
+ """
+ if value is None:
+ value = setting
+ if isinstance(value, bool):
+ return value
+ try:
+ return OptionParser.booleans[value.strip().lower()]
+ except KeyError:
+ raise LookupError('unknown boolean value: "%s"' % value)
+
+
+def validate_ternary(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ """Check/normalize three-value settings:
+ True: '1', 'on', 'yes', 'true'
+ False: '0', 'off', 'no','false', ''
+ any other value: returned as-is.
+
+ All arguments except `value` are ignored
+ (kept for compatibility with "optparse" module).
+ If there is only one positional argument, it is interpreted as `value`.
+ """
+ if value is None:
+ value = setting
+ if isinstance(value, bool) or value is None:
+ return value
+ try:
+ return OptionParser.booleans[value.strip().lower()]
+ except KeyError:
+ return value
+
+
+def validate_nonnegative_int(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ # All arguments except `value` are ignored
+ # (kept for compatibility with "optparse" module).
+ # If there is only one positional argument, it is interpreted as `value`.
+ if value is None:
+ value = setting
+ value = int(value)
+ if value < 0:
+ raise ValueError('negative value; must be positive or zero')
+ return value
+
+
+def validate_threshold(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ # All arguments except `value` are ignored
+ # (kept for compatibility with "optparse" module).
+ # If there is only one positional argument, it is interpreted as `value`.
+ if value is None:
+ value = setting
+ try:
+ return int(value)
+ except ValueError:
+ try:
+ return OptionParser.thresholds[value.lower()]
+ except (KeyError, AttributeError):
+ raise LookupError('unknown threshold: %r.' % value)
+
+
+def validate_colon_separated_string_list(
+ setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ # All arguments except `value` are ignored
+ # (kept for compatibility with "optparse" module).
+ # If there is only one positional argument, it is interpreted as `value`.
+ if value is None:
+ value = setting
+ if not isinstance(value, list):
+ value = value.split(':')
+ else:
+ last = value.pop()
+ value.extend(last.split(':'))
+ return value
+
+
+def validate_comma_separated_list(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ """Check/normalize list arguments (split at "," and strip whitespace).
+
+ All arguments except `value` are ignored
+ (kept for compatibility with "optparse" module).
+ If there is only one positional argument, it is interpreted as `value`.
+ """
+ if value is None:
+ value = setting
+ # `value` may be ``bytes``, ``str``, or a ``list`` (when given as
+ # command line option and "action" is "append").
+ if not isinstance(value, list):
+ value = [value]
+ # this function is called for every option added to `value`
+ # -> split the last item and append the result:
+ last = value.pop()
+ items = [i.strip(' \t\n') for i in last.split(',') if i.strip(' \t\n')]
+ value.extend(items)
+ return value
+
+
+def validate_math_output(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ """Check "math-output" setting, return list with "format" and "options".
+
+ See also https://docutils.sourceforge.io/docs/user/config.html#math-output
+
+ Argument list for compatibility with "optparse" module.
+ All arguments except `value` are ignored.
+ If there is only one positional argument, it is interpreted as `value`.
+ """
+ if value is None:
+ value = setting
+
+ formats = ('html', 'latex', 'mathml', 'mathjax')
+ tex2mathml_converters = ('', 'latexml', 'ttm', 'blahtexml', 'pandoc')
+
+ if not value:
+ return []
+ values = value.split(maxsplit=1)
+ format = values[0].lower()
+ try:
+ options = values[1]
+ except IndexError:
+ options = ''
+ if format not in formats:
+ raise LookupError(f'Unknown math output format: "{value}",\n'
+ f' choose from {formats}.')
+ if format == 'mathml':
+ converter = options.lower()
+ if converter not in tex2mathml_converters:
+ raise LookupError(f'MathML converter "{options}" not supported,\n'
+ f' choose from {tex2mathml_converters}.')
+ options = converter
+ return [format, options]
+
+
+def validate_url_trailing_slash(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ # All arguments except `value` are ignored
+ # (kept for compatibility with "optparse" module).
+ # If there is only one positional argument, it is interpreted as `value`.
+ if value is None:
+ value = setting
+ if not value:
+ return './'
+ elif value.endswith('/'):
+ return value
+ else:
+ return value + '/'
+
+
+def validate_dependency_file(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ # All arguments except `value` are ignored
+ # (kept for compatibility with "optparse" module).
+ # If there is only one positional argument, it is interpreted as `value`.
+ if value is None:
+ value = setting
+ try:
+ return utils.DependencyList(value)
+ except OSError:
+ # TODO: warn/info?
+ return utils.DependencyList(None)
+
+
+def validate_strip_class(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ # All arguments except `value` are ignored
+ # (kept for compatibility with "optparse" module).
+ # If there is only one positional argument, it is interpreted as `value`.
+ if value is None:
+ value = setting
+ # value is a comma separated string list:
+ value = validate_comma_separated_list(value)
+ # validate list elements:
+ for cls in value:
+ normalized = docutils.nodes.make_id(cls)
+ if cls != normalized:
+ raise ValueError('Invalid class value %r (perhaps %r?)'
+ % (cls, normalized))
+ return value
+
+
+def validate_smartquotes_locales(setting, value=None, option_parser=None,
+ config_parser=None, config_section=None):
+ """Check/normalize a comma separated list of smart quote definitions.
+
+ Return a list of (language-tag, quotes) string tuples.
+
+ All arguments except `value` are ignored
+ (kept for compatibility with "optparse" module).
+ If there is only one positional argument, it is interpreted as `value`.
+ """
+ if value is None:
+ value = setting
+ # value is a comma separated string list:
+ value = validate_comma_separated_list(value)
+ # validate list elements
+ lc_quotes = []
+ for item in value:
+ try:
+ lang, quotes = item.split(':', 1)
+ except AttributeError:
+ # this function is called for every option added to `value`
+ # -> ignore if already a tuple:
+ lc_quotes.append(item)
+ continue
+ except ValueError:
+ raise ValueError('Invalid value "%s".'
+ ' Format is "<language>:<quotes>".'
+ % item.encode('ascii', 'backslashreplace'))
+ # parse colon separated string list:
+ quotes = quotes.strip()
+ multichar_quotes = quotes.split(':')
+ if len(multichar_quotes) == 4:
+ quotes = multichar_quotes
+ elif len(quotes) != 4:
+ raise ValueError('Invalid value "%s". Please specify 4 quotes\n'
+ ' (primary open/close; secondary open/close).'
+ % item.encode('ascii', 'backslashreplace'))
+ lc_quotes.append((lang, quotes))
+ return lc_quotes
+
+
+def make_paths_absolute(pathdict, keys, base_path=None):
+ """
+ Interpret filesystem path settings relative to the `base_path` given.
+
+ Paths are values in `pathdict` whose keys are in `keys`. Get `keys` from
+ `OptionParser.relative_path_settings`.
+ """
+ if base_path is None:
+ base_path = Path.cwd()
+ else:
+ base_path = Path(base_path)
+ for key in keys:
+ if key in pathdict:
+ value = pathdict[key]
+ if isinstance(value, list):
+ value = [str((base_path/path).resolve()) for path in value]
+ elif value:
+ value = str((base_path/value).resolve())
+ pathdict[key] = value
+
+
+def make_one_path_absolute(base_path, path):
+ # deprecated, will be removed
+ warnings.warn('frontend.make_one_path_absolute() will be removed '
+ 'in Docutils 0.23.', DeprecationWarning, stacklevel=2)
+ return os.path.abspath(os.path.join(base_path, path))
+
+
+def filter_settings_spec(settings_spec, *exclude, **replace):
+ """Return a copy of `settings_spec` excluding/replacing some settings.
+
+ `settings_spec` is a tuple of configuration settings
+ (cf. `docutils.SettingsSpec.settings_spec`).
+
+ Optional positional arguments are names of to-be-excluded settings.
+ Keyword arguments are option specification replacements.
+ (See the html4strict writer for an example.)
+ """
+ settings = list(settings_spec)
+ # every third item is a sequence of option tuples
+ for i in range(2, len(settings), 3):
+ newopts = []
+ for opt_spec in settings[i]:
+ # opt_spec is ("<help>", [<option strings>], {<keyword args>})
+ opt_name = [opt_string[2:].replace('-', '_')
+ for opt_string in opt_spec[1]
+ if opt_string.startswith('--')][0]
+ if opt_name in exclude:
+ continue
+ if opt_name in replace.keys():
+ newopts.append(replace[opt_name])
+ else:
+ newopts.append(opt_spec)
+ settings[i] = tuple(newopts)
+ return tuple(settings)
+
+
+class Values(optparse.Values):
+ """Storage for option values.
+
+ Updates list attributes by extension rather than by replacement.
+ Works in conjunction with the `OptionParser.lists` instance attribute.
+
+ Deprecated. Will be removed.
+ """
+
+ def __init__(self, *args, **kwargs):
+ warnings.warn('frontend.Values class will be removed '
+ 'in Docutils 0.21 or later.',
+ DeprecationWarning, stacklevel=2)
+ super().__init__(*args, **kwargs)
+ if getattr(self, 'record_dependencies', None) is None:
+ # Set up dummy dependency list.
+ self.record_dependencies = utils.DependencyList()
+
+ def update(self, other_dict, option_parser):
+ if isinstance(other_dict, Values):
+ other_dict = other_dict.__dict__
+ other_dict = dict(other_dict) # also works with ConfigParser sections
+ for setting in option_parser.lists.keys():
+ if hasattr(self, setting) and setting in other_dict:
+ value = getattr(self, setting)
+ if value:
+ value += other_dict[setting]
+ del other_dict[setting]
+ self._update_loose(other_dict)
+
+ def copy(self):
+ """Return a shallow copy of `self`."""
+ with warnings.catch_warnings():
+ warnings.filterwarnings('ignore', category=DeprecationWarning)
+ return self.__class__(defaults=self.__dict__)
+
+ def setdefault(self, name, default):
+ """Return ``self.name`` or ``default``.
+
+ If ``self.name`` is unset, set ``self.name = default``.
+ """
+ if getattr(self, name, None) is None:
+ setattr(self, name, default)
+ return getattr(self, name)
+
+
+class Option(optparse.Option):
+ """Add validation and override support to `optparse.Option`.
+
+ Deprecated. Will be removed.
+ """
+
+ ATTRS = optparse.Option.ATTRS + ['validator', 'overrides']
+
+ def __init__(self, *args, **kwargs):
+ warnings.warn('The frontend.Option class will be removed '
+ 'in Docutils 0.21 or later.',
+ DeprecationWarning, stacklevel=2)
+ super().__init__(*args, **kwargs)
+
+ def process(self, opt, value, values, parser):
+ """
+ Call the validator function on applicable settings and
+ evaluate the 'overrides' option.
+ Extends `optparse.Option.process`.
+ """
+ result = super().process(opt, value, values, parser)
+ setting = self.dest
+ if setting:
+ if self.validator:
+ value = getattr(values, setting)
+ try:
+ new_value = self.validator(setting, value, parser)
+ except Exception as err:
+ raise optparse.OptionValueError(
+ 'Error in option "%s":\n %s'
+ % (opt, io.error_string(err)))
+ setattr(values, setting, new_value)
+ if self.overrides:
+ setattr(values, self.overrides, None)
+ return result
+
+
+class OptionParser(optparse.OptionParser, docutils.SettingsSpec):
+ """
+ Settings parser for command-line and library use.
+
+ The `settings_spec` specification here and in other Docutils components
+ are merged to build the set of command-line options and runtime settings
+ for this process.
+
+ Common settings (defined below) and component-specific settings must not
+ conflict. Short options are reserved for common settings, and components
+ are restricted to using long options.
+
+ Deprecated.
+ Will be replaced by a subclass of `argparse.ArgumentParser`.
+ """
+
+ standard_config_files = [
+ '/etc/docutils.conf', # system-wide
+ './docutils.conf', # project-specific
+ '~/.docutils'] # user-specific
+ """Docutils configuration files, using ConfigParser syntax.
+
+ Filenames will be tilde-expanded later. Later files override earlier ones.
+ """
+
+ threshold_choices = 'info 1 warning 2 error 3 severe 4 none 5'.split()
+ """Possible inputs for for --report and --halt threshold values."""
+
+ thresholds = {'info': 1, 'warning': 2, 'error': 3, 'severe': 4, 'none': 5}
+ """Lookup table for --report and --halt threshold values."""
+
+ booleans = {'1': True, 'on': True, 'yes': True, 'true': True, '0': False,
+ 'off': False, 'no': False, 'false': False, '': False}
+ """Lookup table for boolean configuration file settings."""
+
+ default_error_encoding = (getattr(sys.stderr, 'encoding', None)
+ or io._locale_encoding # noqa
+ or 'ascii')
+
+ default_error_encoding_error_handler = 'backslashreplace'
+
+ settings_spec = (
+ 'General Docutils Options',
+ None,
+ (('Output destination name. Obsoletes the <destination> '
+ 'positional argument. Default: None (stdout).',
+ ['--output'], {'metavar': '<destination>'}),
+ ('Specify the document title as metadata.',
+ ['--title'], {'metavar': '<title>'}),
+ ('Include a "Generated by Docutils" credit and link.',
+ ['--generator', '-g'], {'action': 'store_true',
+ 'validator': validate_boolean}),
+ ('Do not include a generator credit.',
+ ['--no-generator'], {'action': 'store_false', 'dest': 'generator'}),
+ ('Include the date at the end of the document (UTC).',
+ ['--date', '-d'], {'action': 'store_const', 'const': '%Y-%m-%d',
+ 'dest': 'datestamp'}),
+ ('Include the time & date (UTC).',
+ ['--time', '-t'], {'action': 'store_const',
+ 'const': '%Y-%m-%d %H:%M UTC',
+ 'dest': 'datestamp'}),
+ ('Do not include a datestamp of any kind.',
+ ['--no-datestamp'], {'action': 'store_const', 'const': None,
+ 'dest': 'datestamp'}),
+ ('Base directory for absolute paths when reading '
+ 'from the local filesystem. Default "/".',
+ ['--root-prefix'],
+ {'default': '/', 'metavar': '<path>'}),
+ ('Include a "View document source" link.',
+ ['--source-link', '-s'], {'action': 'store_true',
+ 'validator': validate_boolean}),
+ ('Use <URL> for a source link; implies --source-link.',
+ ['--source-url'], {'metavar': '<URL>'}),
+ ('Do not include a "View document source" link.',
+ ['--no-source-link'],
+ {'action': 'callback', 'callback': store_multiple,
+ 'callback_args': ('source_link', 'source_url')}),
+ ('Link from section headers to TOC entries. (default)',
+ ['--toc-entry-backlinks'],
+ {'dest': 'toc_backlinks', 'action': 'store_const', 'const': 'entry',
+ 'default': 'entry'}),
+ ('Link from section headers to the top of the TOC.',
+ ['--toc-top-backlinks'],
+ {'dest': 'toc_backlinks', 'action': 'store_const', 'const': 'top'}),
+ ('Disable backlinks to the table of contents.',
+ ['--no-toc-backlinks'],
+ {'dest': 'toc_backlinks', 'action': 'store_false'}),
+ ('Link from footnotes/citations to references. (default)',
+ ['--footnote-backlinks'],
+ {'action': 'store_true', 'default': True,
+ 'validator': validate_boolean}),
+ ('Disable backlinks from footnotes and citations.',
+ ['--no-footnote-backlinks'],
+ {'dest': 'footnote_backlinks', 'action': 'store_false'}),
+ ('Enable section numbering by Docutils. (default)',
+ ['--section-numbering'],
+ {'action': 'store_true', 'dest': 'sectnum_xform',
+ 'default': True, 'validator': validate_boolean}),
+ ('Disable section numbering by Docutils.',
+ ['--no-section-numbering'],
+ {'action': 'store_false', 'dest': 'sectnum_xform'}),
+ ('Remove comment elements from the document tree.',
+ ['--strip-comments'],
+ {'action': 'store_true', 'validator': validate_boolean}),
+ ('Leave comment elements in the document tree. (default)',
+ ['--leave-comments'],
+ {'action': 'store_false', 'dest': 'strip_comments'}),
+ ('Remove all elements with classes="<class>" from the document tree. '
+ 'Warning: potentially dangerous; use with caution. '
+ '(Multiple-use option.)',
+ ['--strip-elements-with-class'],
+ {'action': 'append', 'dest': 'strip_elements_with_classes',
+ 'metavar': '<class>', 'validator': validate_strip_class}),
+ ('Remove all classes="<class>" attributes from elements in the '
+ 'document tree. Warning: potentially dangerous; use with caution. '
+ '(Multiple-use option.)',
+ ['--strip-class'],
+ {'action': 'append', 'dest': 'strip_classes',
+ 'metavar': '<class>', 'validator': validate_strip_class}),
+ ('Report system messages at or higher than <level>: "info" or "1", '
+ '"warning"/"2" (default), "error"/"3", "severe"/"4", "none"/"5"',
+ ['--report', '-r'], {'choices': threshold_choices, 'default': 2,
+ 'dest': 'report_level', 'metavar': '<level>',
+ 'validator': validate_threshold}),
+ ('Report all system messages. (Same as "--report=1".)',
+ ['--verbose', '-v'], {'action': 'store_const', 'const': 1,
+ 'dest': 'report_level'}),
+ ('Report no system messages. (Same as "--report=5".)',
+ ['--quiet', '-q'], {'action': 'store_const', 'const': 5,
+ 'dest': 'report_level'}),
+ ('Halt execution at system messages at or above <level>. '
+ 'Levels as in --report. Default: 4 (severe).',
+ ['--halt'], {'choices': threshold_choices, 'dest': 'halt_level',
+ 'default': 4, 'metavar': '<level>',
+ 'validator': validate_threshold}),
+ ('Halt at the slightest problem. Same as "--halt=info".',
+ ['--strict'], {'action': 'store_const', 'const': 1,
+ 'dest': 'halt_level'}),
+ ('Enable a non-zero exit status for non-halting system messages at '
+ 'or above <level>. Default: 5 (disabled).',
+ ['--exit-status'], {'choices': threshold_choices,
+ 'dest': 'exit_status_level',
+ 'default': 5, 'metavar': '<level>',
+ 'validator': validate_threshold}),
+ ('Enable debug-level system messages and diagnostics.',
+ ['--debug'], {'action': 'store_true',
+ 'validator': validate_boolean}),
+ ('Disable debug output. (default)',
+ ['--no-debug'], {'action': 'store_false', 'dest': 'debug'}),
+ ('Send the output of system messages to <file>.',
+ ['--warnings'], {'dest': 'warning_stream', 'metavar': '<file>'}),
+ ('Enable Python tracebacks when Docutils is halted.',
+ ['--traceback'], {'action': 'store_true', 'default': None,
+ 'validator': validate_boolean}),
+ ('Disable Python tracebacks. (default)',
+ ['--no-traceback'], {'dest': 'traceback', 'action': 'store_false'}),
+ ('Specify the encoding and optionally the '
+ 'error handler of input text. Default: <auto-detect>:strict.',
+ ['--input-encoding', '-i'],
+ {'metavar': '<name[:handler]>',
+ 'validator': validate_encoding_and_error_handler}),
+ ('Specify the error handler for undecodable characters. '
+ 'Choices: "strict" (default), "ignore", and "replace".',
+ ['--input-encoding-error-handler'],
+ {'default': 'strict', 'validator': validate_encoding_error_handler}),
+ ('Specify the text encoding and optionally the error handler for '
+ 'output. Default: utf-8:strict.',
+ ['--output-encoding', '-o'],
+ {'metavar': '<name[:handler]>', 'default': 'utf-8',
+ 'validator': validate_encoding_and_error_handler}),
+ ('Specify error handler for unencodable output characters; '
+ '"strict" (default), "ignore", "replace", '
+ '"xmlcharrefreplace", "backslashreplace".',
+ ['--output-encoding-error-handler'],
+ {'default': 'strict', 'validator': validate_encoding_error_handler}),
+ ('Specify text encoding and optionally error handler '
+ 'for error output. Default: %s:%s.'
+ % (default_error_encoding, default_error_encoding_error_handler),
+ ['--error-encoding', '-e'],
+ {'metavar': '<name[:handler]>', 'default': default_error_encoding,
+ 'validator': validate_encoding_and_error_handler}),
+ ('Specify the error handler for unencodable characters in '
+ 'error output. Default: %s.'
+ % default_error_encoding_error_handler,
+ ['--error-encoding-error-handler'],
+ {'default': default_error_encoding_error_handler,
+ 'validator': validate_encoding_error_handler}),
+ ('Specify the language (as BCP 47 language tag). Default: en.',
+ ['--language', '-l'], {'dest': 'language_code', 'default': 'en',
+ 'metavar': '<name>'}),
+ ('Write output file dependencies to <file>.',
+ ['--record-dependencies'],
+ {'metavar': '<file>', 'validator': validate_dependency_file,
+ 'default': None}), # default set in Values class
+ ('Read configuration settings from <file>, if it exists.',
+ ['--config'], {'metavar': '<file>', 'type': 'string',
+ 'action': 'callback', 'callback': read_config_file}),
+ ("Show this program's version number and exit.",
+ ['--version', '-V'], {'action': 'version'}),
+ ('Show this help message and exit.',
+ ['--help', '-h'], {'action': 'help'}),
+ # Typically not useful for non-programmatical use:
+ (SUPPRESS_HELP, ['--id-prefix'], {'default': ''}),
+ (SUPPRESS_HELP, ['--auto-id-prefix'], {'default': '%'}),
+ # Hidden options, for development use only:
+ (SUPPRESS_HELP, ['--dump-settings'], {'action': 'store_true'}),
+ (SUPPRESS_HELP, ['--dump-internals'], {'action': 'store_true'}),
+ (SUPPRESS_HELP, ['--dump-transforms'], {'action': 'store_true'}),
+ (SUPPRESS_HELP, ['--dump-pseudo-xml'], {'action': 'store_true'}),
+ (SUPPRESS_HELP, ['--expose-internal-attribute'],
+ {'action': 'append', 'dest': 'expose_internals',
+ 'validator': validate_colon_separated_string_list}),
+ (SUPPRESS_HELP, ['--strict-visitor'], {'action': 'store_true'}),
+ ))
+ """Runtime settings and command-line options common to all Docutils front
+ ends. Setting specs specific to individual Docutils components are also
+ used (see `populate_from_components()`)."""
+
+ settings_defaults = {'_disable_config': None,
+ '_source': None,
+ '_destination': None,
+ '_config_files': None}
+ """Defaults for settings without command-line option equivalents.
+
+ See https://docutils.sourceforge.io/docs/user/config.html#internal-settings
+ """
+
+ config_section = 'general'
+
+ version_template = ('%%prog (Docutils %s%s, Python %s, on %s)'
+ % (docutils.__version__,
+ docutils.__version_details__
+ and ' [%s]'%docutils.__version_details__ or '',
+ sys.version.split()[0], sys.platform))
+ """Default version message."""
+
+ def __init__(self, components=(), defaults=None, read_config_files=False,
+ *args, **kwargs):
+ """Set up OptionParser instance.
+
+ `components` is a list of Docutils components each containing a
+ ``.settings_spec`` attribute.
+ `defaults` is a mapping of setting default overrides.
+ """
+
+ self.lists = {}
+ """Set of list-type settings."""
+
+ self.config_files = []
+ """List of paths of applied configuration files."""
+
+ self.relative_path_settings = ['warning_stream'] # will be modified
+
+ warnings.warn('The frontend.OptionParser class will be replaced '
+ 'by a subclass of argparse.ArgumentParser '
+ 'in Docutils 0.21 or later.',
+ DeprecationWarning, stacklevel=2)
+ super().__init__(option_class=Option, add_help_option=None,
+ formatter=optparse.TitledHelpFormatter(width=78),
+ *args, **kwargs)
+ if not self.version:
+ self.version = self.version_template
+ self.components = (self, *components)
+ self.populate_from_components(self.components)
+ self.defaults.update(defaults or {})
+ if read_config_files and not self.defaults['_disable_config']:
+ try:
+ config_settings = self.get_standard_config_settings()
+ except ValueError as err:
+ self.error(err)
+ self.defaults.update(config_settings.__dict__)
+
+ def populate_from_components(self, components):
+ """Collect settings specification from components.
+
+ For each component, populate from the `SettingsSpec.settings_spec`
+ structure, then from the `SettingsSpec.settings_defaults` dictionary.
+ After all components have been processed, check for and populate from
+ each component's `SettingsSpec.settings_default_overrides` dictionary.
+ """
+ for component in components:
+ if component is None:
+ continue
+ settings_spec = component.settings_spec
+ self.relative_path_settings.extend(
+ component.relative_path_settings)
+ for i in range(0, len(settings_spec), 3):
+ title, description, option_spec = settings_spec[i:i+3]
+ if title:
+ group = optparse.OptionGroup(self, title, description)
+ self.add_option_group(group)
+ else:
+ group = self # single options
+ for (help_text, option_strings, kwargs) in option_spec:
+ option = group.add_option(help=help_text, *option_strings,
+ **kwargs)
+ if kwargs.get('action') == 'append':
+ self.lists[option.dest] = True
+ if component.settings_defaults:
+ self.defaults.update(component.settings_defaults)
+ for component in components:
+ if component and component.settings_default_overrides:
+ self.defaults.update(component.settings_default_overrides)
+
+ @classmethod
+ def get_standard_config_files(cls):
+ """Return list of config files, from environment or standard."""
+ if 'DOCUTILSCONFIG' in os.environ:
+ config_files = os.environ['DOCUTILSCONFIG'].split(os.pathsep)
+ else:
+ config_files = cls.standard_config_files
+ return [os.path.expanduser(f) for f in config_files if f.strip()]
+
+ def get_standard_config_settings(self):
+ with warnings.catch_warnings():
+ warnings.filterwarnings('ignore', category=DeprecationWarning)
+ settings = Values()
+ for filename in self.get_standard_config_files():
+ settings.update(self.get_config_file_settings(filename), self)
+ return settings
+
+ def get_config_file_settings(self, config_file):
+ """Returns a dictionary containing appropriate config file settings."""
+ config_parser = ConfigParser()
+ # parse config file, add filename if found and successfully read.
+ applied = set()
+ with warnings.catch_warnings():
+ warnings.filterwarnings('ignore', category=DeprecationWarning)
+ self.config_files += config_parser.read(config_file, self)
+ settings = Values()
+ for component in self.components:
+ if not component:
+ continue
+ for section in (tuple(component.config_section_dependencies or ())
+ + (component.config_section,)):
+ if section in applied:
+ continue
+ applied.add(section)
+ if config_parser.has_section(section):
+ settings.update(config_parser[section], self)
+ make_paths_absolute(settings.__dict__,
+ self.relative_path_settings,
+ os.path.dirname(config_file))
+ return settings.__dict__
+
+ def check_values(self, values, args):
+ """Store positional arguments as runtime settings."""
+ values._source, values._destination = self.check_args(args)
+ make_paths_absolute(values.__dict__, self.relative_path_settings)
+ values._config_files = self.config_files
+ return values
+
+ def check_args(self, args):
+ source = destination = None
+ if args:
+ source = args.pop(0)
+ if source == '-': # means stdin
+ source = None
+ if args:
+ destination = args.pop(0)
+ if destination == '-': # means stdout
+ destination = None
+ if args:
+ self.error('Maximum 2 arguments allowed.')
+ if source and source == destination:
+ self.error('Do not specify the same file for both source and '
+ 'destination. It will clobber the source file.')
+ return source, destination
+
+ def set_defaults_from_dict(self, defaults):
+ # deprecated, will be removed
+ warnings.warn('OptionParser.set_defaults_from_dict() will be removed '
+ 'in Docutils 0.22 or with the switch to ArgumentParser.',
+ DeprecationWarning, stacklevel=2)
+ self.defaults.update(defaults)
+
+ def get_default_values(self):
+ """Needed to get custom `Values` instances."""
+ with warnings.catch_warnings():
+ warnings.filterwarnings('ignore', category=DeprecationWarning)
+ defaults = Values(self.defaults)
+ defaults._config_files = self.config_files
+ return defaults
+
+ def get_option_by_dest(self, dest):
+ """
+ Get an option by its dest.
+
+ If you're supplying a dest which is shared by several options,
+ it is undefined which option of those is returned.
+
+ A KeyError is raised if there is no option with the supplied
+ dest.
+ """
+ for group in self.option_groups + [self]:
+ for option in group.option_list:
+ if option.dest == dest:
+ return option
+ raise KeyError('No option with dest == %r.' % dest)
+
+
+class ConfigParser(configparser.RawConfigParser):
+ """Parser for Docutils configuration files.
+
+ See https://docutils.sourceforge.io/docs/user/config.html.
+
+ Option key normalization includes conversion of '-' to '_'.
+
+ Config file encoding is "utf-8". Encoding errors are reported
+ and the affected file(s) skipped.
+
+ This class is provisional and will change in future versions.
+ """
+
+ old_settings = {
+ 'pep_stylesheet': ('pep_html writer', 'stylesheet'),
+ 'pep_stylesheet_path': ('pep_html writer', 'stylesheet_path'),
+ 'pep_template': ('pep_html writer', 'template')}
+ """{old setting: (new section, new setting)} mapping, used by
+ `handle_old_config`, to convert settings from the old [options] section.
+ """
+
+ old_warning = (
+ 'The "[option]" section is deprecated.\n'
+ 'Support for old-format configuration files will be removed in '
+ 'Docutils 2.0. Please revise your configuration files. '
+ 'See <https://docutils.sourceforge.io/docs/user/config.html>, '
+ 'section "Old-Format Configuration Files".')
+
+ not_utf8_error = """\
+Unable to read configuration file "%s": content not encoded as UTF-8.
+Skipping "%s" configuration file.
+"""
+
+ def read(self, filenames, option_parser=None):
+ # Currently, if a `docutils.frontend.OptionParser` instance is
+ # supplied, setting values are validated.
+ if option_parser is not None:
+ warnings.warn('frontend.ConfigParser.read(): parameter '
+ '"option_parser" will be removed '
+ 'in Docutils 0.21 or later.',
+ DeprecationWarning, stacklevel=2)
+ read_ok = []
+ if isinstance(filenames, str):
+ filenames = [filenames]
+ for filename in filenames:
+ # Config files are UTF-8-encoded:
+ try:
+ read_ok += super().read(filename, encoding='utf-8')
+ except UnicodeDecodeError:
+ sys.stderr.write(self.not_utf8_error % (filename, filename))
+ continue
+ if 'options' in self:
+ self.handle_old_config(filename)
+ if option_parser is not None:
+ self.validate_settings(filename, option_parser)
+ return read_ok
+
+ def handle_old_config(self, filename):
+ warnings.warn_explicit(self.old_warning, ConfigDeprecationWarning,
+ filename, 0)
+ options = self.get_section('options')
+ if not self.has_section('general'):
+ self.add_section('general')
+ for key, value in options.items():
+ if key in self.old_settings:
+ section, setting = self.old_settings[key]
+ if not self.has_section(section):
+ self.add_section(section)
+ else:
+ section = 'general'
+ setting = key
+ if not self.has_option(section, setting):
+ self.set(section, setting, value)
+ self.remove_section('options')
+
+ def validate_settings(self, filename, option_parser):
+ """
+ Call the validator function and implement overrides on all applicable
+ settings.
+ """
+ for section in self.sections():
+ for setting in self.options(section):
+ try:
+ option = option_parser.get_option_by_dest(setting)
+ except KeyError:
+ continue
+ if option.validator:
+ value = self.get(section, setting)
+ try:
+ new_value = option.validator(
+ setting, value, option_parser,
+ config_parser=self, config_section=section)
+ except Exception as err:
+ raise ValueError(f'Error in config file "{filename}", '
+ f'section "[{section}]":\n'
+ f' {io.error_string(err)}\n'
+ f' {setting} = {value}')
+ self.set(section, setting, new_value)
+ if option.overrides:
+ self.set(section, option.overrides, None)
+
+ def optionxform(self, optionstr):
+ """
+ Lowercase and transform '-' to '_'.
+
+ So the cmdline form of option names can be used in config files.
+ """
+ return optionstr.lower().replace('-', '_')
+
+ def get_section(self, section):
+ """
+ Return a given section as a dictionary.
+
+ Return empty dictionary if the section doesn't exist.
+
+ Deprecated. Use the configparser "Mapping Protocol Access" and
+ catch KeyError.
+ """
+ warnings.warn('frontend.OptionParser.get_section() '
+ 'will be removed in Docutils 0.21 or later.',
+ DeprecationWarning, stacklevel=2)
+ try:
+ return dict(self[section])
+ except KeyError:
+ return {}
+
+
+class ConfigDeprecationWarning(FutureWarning):
+ """Warning for deprecated configuration file features."""
+
+
+def get_default_settings(*components):
+ """Return default runtime settings for `components`.
+
+ Return a `frontend.Values` instance with defaults for generic Docutils
+ settings and settings from the `components` (`SettingsSpec` instances).
+
+ This corresponds to steps 1 and 2 in the `runtime settings priority`__.
+
+ __ https://docutils.sourceforge.io/docs/api/runtime-settings.html
+ #settings-priority
+ """
+ with warnings.catch_warnings():
+ warnings.filterwarnings('ignore', category=DeprecationWarning)
+ return OptionParser(components).get_default_values()
diff --git a/.venv/lib/python3.12/site-packages/docutils/io.py b/.venv/lib/python3.12/site-packages/docutils/io.py
new file mode 100644
index 00000000..6237c66a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/io.py
@@ -0,0 +1,637 @@
+# $Id: io.py 9427 2023-07-07 06:50:09Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+I/O classes provide a uniform API for low-level input and output. Subclasses
+exist for a variety of input/output mechanisms.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import codecs
+import locale
+import os
+import re
+import sys
+import warnings
+
+from docutils import TransformSpec
+
+
+# Guess the locale's preferred encoding.
+# If no valid guess can be made, _locale_encoding is set to `None`:
+#
+# TODO: check whether this is set correctly with every OS and Python version
+# or whether front-end tools need to call `locale.setlocale()`
+# before importing this module
+try:
+ # Return locale encoding also in UTF-8 mode
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ _locale_encoding = (locale.getlocale()[1]
+ or locale.getdefaultlocale()[1])
+ _locale_encoding = _locale_encoding.lower()
+except: # noqa any other problems determining the locale -> use None
+ _locale_encoding = None
+try:
+ codecs.lookup(_locale_encoding)
+except (LookupError, TypeError):
+ _locale_encoding = None
+
+
+class InputError(OSError): pass
+class OutputError(OSError): pass
+
+
+def check_encoding(stream, encoding):
+ """Test, whether the encoding of `stream` matches `encoding`.
+
+ Returns
+
+ :None: if `encoding` or `stream.encoding` are not a valid encoding
+ argument (e.g. ``None``) or `stream.encoding is missing.
+ :True: if the encoding argument resolves to the same value as `encoding`,
+ :False: if the encodings differ.
+ """
+ try:
+ return codecs.lookup(stream.encoding) == codecs.lookup(encoding)
+ except (LookupError, AttributeError, TypeError):
+ return None
+
+
+def error_string(err):
+ """Return string representation of Exception `err`.
+ """
+ return f'{err.__class__.__name__}: {err}'
+
+
+class Input(TransformSpec):
+ """
+ Abstract base class for input wrappers.
+
+ Docutils input objects must provide a `read()` method that
+ returns the source, typically as `str` instance.
+
+ Inheriting `TransformSpec` allows input objects to add
+ "transforms" and "unknown_reference_resolvers" to the "Transformer".
+ (Optional for custom input objects since Docutils 0.19.)
+ """
+
+ component_type = 'input'
+
+ default_source_path = None
+
+ def __init__(self, source=None, source_path=None, encoding=None,
+ error_handler='strict'):
+ self.encoding = encoding
+ """Text encoding for the input source."""
+
+ self.error_handler = error_handler
+ """Text decoding error handler."""
+
+ self.source = source
+ """The source of input data."""
+
+ self.source_path = source_path
+ """A text reference to the source."""
+
+ if not source_path:
+ self.source_path = self.default_source_path
+
+ self.successful_encoding = None
+ """The encoding that successfully decoded the source data."""
+
+ def __repr__(self):
+ return '%s: source=%r, source_path=%r' % (self.__class__, self.source,
+ self.source_path)
+
+ def read(self):
+ """Return input as `str`. Define in subclasses."""
+ raise NotImplementedError
+
+ def decode(self, data):
+ """
+ Decode `data` if required.
+
+ Return Unicode `str` instances unchanged (nothing to decode).
+
+ If `self.encoding` is None, determine encoding from data
+ or try UTF-8 and the locale's preferred encoding.
+ The client application should call ``locale.setlocale()`` at the
+ beginning of processing::
+
+ locale.setlocale(locale.LC_ALL, '')
+
+ Raise UnicodeError if unsuccessful.
+
+ Provisional: encoding detection will be removed in Docutils 1.0.
+ """
+ if self.encoding and self.encoding.lower() == 'unicode':
+ assert isinstance(data, str), ('input encoding is "unicode" '
+ 'but `data` is no `str` instance')
+ if isinstance(data, str):
+ # nothing to decode
+ return data
+ if self.encoding:
+ # We believe the user/application when the encoding is
+ # explicitly given.
+ encoding_candidates = [self.encoding]
+ else:
+ data_encoding = self.determine_encoding_from_data(data)
+ if data_encoding:
+ # `data` declares its encoding with "magic comment" or BOM,
+ encoding_candidates = [data_encoding]
+ else:
+ # Apply heuristics if the encoding is not specified.
+ # Start with UTF-8, because that only matches
+ # data that *IS* UTF-8:
+ encoding_candidates = ['utf-8']
+ # If UTF-8 fails, fall back to the locale's preferred encoding:
+ fallback = locale.getpreferredencoding(do_setlocale=False)
+ if fallback and fallback.lower() != 'utf-8':
+ encoding_candidates.append(fallback)
+ for enc in encoding_candidates:
+ try:
+ decoded = str(data, enc, self.error_handler)
+ self.successful_encoding = enc
+ return decoded
+ except (UnicodeError, LookupError) as err:
+ # keep exception instance for use outside of the "for" loop.
+ error = err
+ raise UnicodeError(
+ 'Unable to decode input data. Tried the following encodings: '
+ f'{", ".join(repr(enc) for enc in encoding_candidates)}.\n'
+ f'({error_string(error)})')
+
+ coding_slug = re.compile(br"coding[:=]\s*([-\w.]+)")
+ """Encoding declaration pattern."""
+
+ byte_order_marks = ((codecs.BOM_UTF32_BE, 'utf-32'),
+ (codecs.BOM_UTF32_LE, 'utf-32'),
+ (codecs.BOM_UTF8, 'utf-8-sig'),
+ (codecs.BOM_UTF16_BE, 'utf-16'),
+ (codecs.BOM_UTF16_LE, 'utf-16'),
+ )
+ """Sequence of (start_bytes, encoding) tuples for encoding detection.
+ The first bytes of input data are checked against the start_bytes strings.
+ A match indicates the given encoding."""
+
+ def determine_encoding_from_data(self, data):
+ """
+ Try to determine the encoding of `data` by looking *in* `data`.
+ Check for a byte order mark (BOM) or an encoding declaration.
+ """
+ # check for a byte order mark:
+ for start_bytes, encoding in self.byte_order_marks:
+ if data.startswith(start_bytes):
+ return encoding
+ # check for an encoding declaration pattern in first 2 lines of file:
+ for line in data.splitlines()[:2]:
+ match = self.coding_slug.search(line)
+ if match:
+ return match.group(1).decode('ascii')
+ return None
+
+ def isatty(self):
+ """Return True, if the input source is connected to a TTY device."""
+ try:
+ return self.source.isatty()
+ except AttributeError:
+ return False
+
+
+class Output(TransformSpec):
+ """
+ Abstract base class for output wrappers.
+
+ Docutils output objects must provide a `write()` method that
+ expects and handles one argument (the output).
+
+ Inheriting `TransformSpec` allows output objects to add
+ "transforms" and "unknown_reference_resolvers" to the "Transformer".
+ (Optional for custom output objects since Docutils 0.19.)
+ """
+
+ component_type = 'output'
+
+ default_destination_path = None
+
+ def __init__(self, destination=None, destination_path=None,
+ encoding=None, error_handler='strict'):
+ self.encoding = encoding
+ """Text encoding for the output destination."""
+
+ self.error_handler = error_handler or 'strict'
+ """Text encoding error handler."""
+
+ self.destination = destination
+ """The destination for output data."""
+
+ self.destination_path = destination_path
+ """A text reference to the destination."""
+
+ if not destination_path:
+ self.destination_path = self.default_destination_path
+
+ def __repr__(self):
+ return ('%s: destination=%r, destination_path=%r'
+ % (self.__class__, self.destination, self.destination_path))
+
+ def write(self, data):
+ """Write `data`. Define in subclasses."""
+ raise NotImplementedError
+
+ def encode(self, data):
+ """
+ Encode and return `data`.
+
+ If `data` is a `bytes` instance, it is returned unchanged.
+ Otherwise it is encoded with `self.encoding`.
+
+ Provisional: If `self.encoding` is set to the pseudo encoding name
+ "unicode", `data` must be a `str` instance and is returned unchanged.
+ """
+ if self.encoding and self.encoding.lower() == 'unicode':
+ assert isinstance(data, str), ('output encoding is "unicode" '
+ 'but `data` is no `str` instance')
+ return data
+ if not isinstance(data, str):
+ # Non-unicode (e.g. bytes) output.
+ return data
+ else:
+ return data.encode(self.encoding, self.error_handler)
+
+
+class ErrorOutput:
+ """
+ Wrapper class for file-like error streams with
+ failsafe de- and encoding of `str`, `bytes`, `unicode` and
+ `Exception` instances.
+ """
+
+ def __init__(self, destination=None, encoding=None,
+ encoding_errors='backslashreplace',
+ decoding_errors='replace'):
+ """
+ :Parameters:
+ - `destination`: a file-like object,
+ a string (path to a file),
+ `None` (write to `sys.stderr`, default), or
+ evaluating to `False` (write() requests are ignored).
+ - `encoding`: `destination` text encoding. Guessed if None.
+ - `encoding_errors`: how to treat encoding errors.
+ """
+ if destination is None:
+ destination = sys.stderr
+ elif not destination:
+ destination = False
+ # if `destination` is a file name, open it
+ elif isinstance(destination, str):
+ destination = open(destination, 'w')
+
+ self.destination = destination
+ """Where warning output is sent."""
+
+ self.encoding = (encoding or getattr(destination, 'encoding', None)
+ or _locale_encoding or 'ascii')
+ """The output character encoding."""
+
+ self.encoding_errors = encoding_errors
+ """Encoding error handler."""
+
+ self.decoding_errors = decoding_errors
+ """Decoding error handler."""
+
+ def write(self, data):
+ """
+ Write `data` to self.destination. Ignore, if self.destination is False.
+
+ `data` can be a `bytes`, `str`, or `Exception` instance.
+ """
+ if not self.destination:
+ return
+ if isinstance(data, Exception):
+ data = str(data)
+ try:
+ self.destination.write(data)
+ except UnicodeEncodeError:
+ self.destination.write(data.encode(self.encoding,
+ self.encoding_errors))
+ except TypeError:
+ if isinstance(data, str): # destination may expect bytes
+ self.destination.write(data.encode(self.encoding,
+ self.encoding_errors))
+ elif self.destination in (sys.stderr, sys.stdout):
+ # write bytes to raw stream
+ self.destination.buffer.write(data)
+ else:
+ self.destination.write(str(data, self.encoding,
+ self.decoding_errors))
+
+ def close(self):
+ """
+ Close the error-output stream.
+
+ Ignored if the destination is` sys.stderr` or `sys.stdout` or has no
+ close() method.
+ """
+ if self.destination in (sys.stdout, sys.stderr):
+ return
+ try:
+ self.destination.close()
+ except AttributeError:
+ pass
+
+ def isatty(self):
+ """Return True, if the destination is connected to a TTY device."""
+ try:
+ return self.destination.isatty()
+ except AttributeError:
+ return False
+
+
+class FileInput(Input):
+
+ """
+ Input for single, simple file-like objects.
+ """
+ def __init__(self, source=None, source_path=None,
+ encoding=None, error_handler='strict',
+ autoclose=True, mode='r'):
+ """
+ :Parameters:
+ - `source`: either a file-like object (which is read directly), or
+ `None` (which implies `sys.stdin` if no `source_path` given).
+ - `source_path`: a path to a file, which is opened for reading.
+ - `encoding`: the expected text encoding of the input file.
+ - `error_handler`: the encoding error handler to use.
+ - `autoclose`: close automatically after read (except when
+ `sys.stdin` is the source).
+ - `mode`: how the file is to be opened (see standard function
+ `open`). The default is read only ('r').
+ """
+ Input.__init__(self, source, source_path, encoding, error_handler)
+ self.autoclose = autoclose
+ self._stderr = ErrorOutput()
+
+ if source is None:
+ if source_path:
+ try:
+ self.source = open(source_path, mode,
+ encoding=self.encoding,
+ errors=self.error_handler)
+ except OSError as error:
+ raise InputError(error.errno, error.strerror, source_path)
+ else:
+ self.source = sys.stdin
+ elif check_encoding(self.source, self.encoding) is False:
+ # TODO: re-open, warn or raise error?
+ raise UnicodeError('Encoding clash: encoding given is "%s" '
+ 'but source is opened with encoding "%s".' %
+ (self.encoding, self.source.encoding))
+ if not source_path:
+ try:
+ self.source_path = self.source.name
+ except AttributeError:
+ pass
+
+ def read(self):
+ """
+ Read and decode a single file, return as `str`.
+ """
+ try:
+ if not self.encoding and hasattr(self.source, 'buffer'):
+ # read as binary data
+ data = self.source.buffer.read()
+ # decode with heuristics
+ data = self.decode(data)
+ # normalize newlines
+ data = '\n'.join(data.splitlines()+[''])
+ else:
+ data = self.source.read()
+ finally:
+ if self.autoclose:
+ self.close()
+ return data
+
+ def readlines(self):
+ """
+ Return lines of a single file as list of strings.
+ """
+ return self.read().splitlines(True)
+
+ def close(self):
+ if self.source is not sys.stdin:
+ self.source.close()
+
+
+class FileOutput(Output):
+
+ """Output for single, simple file-like objects."""
+
+ default_destination_path = '<file>'
+
+ mode = 'w'
+ """The mode argument for `open()`."""
+ # 'wb' for binary (e.g. OpenOffice) files (see also `BinaryFileOutput`).
+ # (Do not use binary mode ('wb') for text files, as this prevents the
+ # conversion of newlines to the system specific default.)
+
+ def __init__(self, destination=None, destination_path=None,
+ encoding=None, error_handler='strict', autoclose=True,
+ handle_io_errors=None, mode=None):
+ """
+ :Parameters:
+ - `destination`: either a file-like object (which is written
+ directly) or `None` (which implies `sys.stdout` if no
+ `destination_path` given).
+ - `destination_path`: a path to a file, which is opened and then
+ written.
+ - `encoding`: the text encoding of the output file.
+ - `error_handler`: the encoding error handler to use.
+ - `autoclose`: close automatically after write (except when
+ `sys.stdout` or `sys.stderr` is the destination).
+ - `handle_io_errors`: ignored, deprecated, will be removed.
+ - `mode`: how the file is to be opened (see standard function
+ `open`). The default is 'w', providing universal newline
+ support for text files.
+ """
+ Output.__init__(self, destination, destination_path,
+ encoding, error_handler)
+ self.opened = True
+ self.autoclose = autoclose
+ if handle_io_errors is not None:
+ warnings.warn('io.FileOutput: init argument "handle_io_errors" '
+ 'is ignored and will be removed in '
+ 'Docutils 2.0.', DeprecationWarning, stacklevel=2)
+ if mode is not None:
+ self.mode = mode
+ self._stderr = ErrorOutput()
+ if destination is None:
+ if destination_path:
+ self.opened = False
+ else:
+ self.destination = sys.stdout
+ elif ( # destination is file-type object -> check mode:
+ mode and hasattr(self.destination, 'mode')
+ and mode != self.destination.mode):
+ print('Warning: Destination mode "%s" differs from specified '
+ 'mode "%s"' % (self.destination.mode, mode),
+ file=self._stderr)
+ if not destination_path:
+ try:
+ self.destination_path = self.destination.name
+ except AttributeError:
+ pass
+
+ def open(self):
+ # Specify encoding
+ if 'b' not in self.mode:
+ kwargs = {'encoding': self.encoding,
+ 'errors': self.error_handler}
+ else:
+ kwargs = {}
+ try:
+ self.destination = open(self.destination_path, self.mode, **kwargs)
+ except OSError as error:
+ raise OutputError(error.errno, error.strerror,
+ self.destination_path)
+ self.opened = True
+
+ def write(self, data):
+ """Write `data` to a single file, also return it.
+
+ `data` can be a `str` or `bytes` instance.
+ If writing `bytes` fails, an attempt is made to write to
+ the low-level interface ``self.destination.buffer``.
+
+ If `data` is a `str` instance and `self.encoding` and
+ `self.destination.encoding` are set to different values, `data`
+ is encoded to a `bytes` instance using `self.encoding`.
+
+ Provisional: future versions may raise an error if `self.encoding`
+ and `self.destination.encoding` are set to different values.
+ """
+ if not self.opened:
+ self.open()
+ if (isinstance(data, str)
+ and check_encoding(self.destination, self.encoding) is False):
+ if os.linesep != '\n':
+ data = data.replace('\n', os.linesep) # fix endings
+ data = self.encode(data)
+
+ try:
+ self.destination.write(data)
+ except TypeError as err:
+ if isinstance(data, bytes):
+ try:
+ self.destination.buffer.write(data)
+ except AttributeError:
+ if check_encoding(self.destination,
+ self.encoding) is False:
+ raise ValueError(
+ f'Encoding of {self.destination_path} '
+ f'({self.destination.encoding}) differs \n'
+ f' from specified encoding ({self.encoding})')
+ else:
+ raise err
+ except (UnicodeError, LookupError) as err:
+ raise UnicodeError(
+ 'Unable to encode output data. output-encoding is: '
+ f'{self.encoding}.\n({error_string(err)})')
+ finally:
+ if self.autoclose:
+ self.close()
+ return data
+
+ def close(self):
+ if self.destination not in (sys.stdout, sys.stderr):
+ self.destination.close()
+ self.opened = False
+
+
+class BinaryFileOutput(FileOutput):
+ """
+ A version of docutils.io.FileOutput which writes to a binary file.
+ """
+ # Used by core.publish_cmdline_to_binary() which in turn is used by
+ # tools/rst2odt.py but not by core.rst2odt().
+ mode = 'wb'
+
+
+class StringInput(Input):
+ """Input from a `str` or `bytes` instance."""
+
+ default_source_path = '<string>'
+
+ def read(self):
+ """Return the source as `str` instance.
+
+ Decode, if required (see `Input.decode`).
+ """
+ return self.decode(self.source)
+
+
+class StringOutput(Output):
+ """Output to a `bytes` or `str` instance.
+
+ Provisional.
+ """
+
+ default_destination_path = '<string>'
+
+ def write(self, data):
+ """Store `data` in `self.destination`, and return it.
+
+ If `self.encoding` is set to the pseudo encoding name "unicode",
+ `data` must be a `str` instance and is stored/returned unchanged
+ (cf. `Output.encode`).
+
+ Otherwise, `data` can be a `bytes` or `str` instance and is
+ stored/returned as a `bytes` instance
+ (`str` data is encoded with `self.encode()`).
+
+ Attention: the `output_encoding`_ setting may affect the content
+ of the output (e.g. an encoding declaration in HTML or XML or the
+ representation of characters as LaTeX macro vs. literal character).
+ """
+ self.destination = self.encode(data)
+ return self.destination
+
+
+class NullInput(Input):
+
+ """Degenerate input: read nothing."""
+
+ default_source_path = 'null input'
+
+ def read(self):
+ """Return an empty string."""
+ return ''
+
+
+class NullOutput(Output):
+
+ """Degenerate output: write nothing."""
+
+ default_destination_path = 'null output'
+
+ def write(self, data):
+ """Do nothing, return None."""
+ pass
+
+
+class DocTreeInput(Input):
+
+ """
+ Adapter for document tree input.
+
+ The document tree must be passed in the ``source`` parameter.
+ """
+
+ default_source_path = 'doctree input'
+
+ def read(self):
+ """Return the document tree."""
+ return self.source
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/__init__.py b/.venv/lib/python3.12/site-packages/docutils/languages/__init__.py
new file mode 100644
index 00000000..1bf43129
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/__init__.py
@@ -0,0 +1,83 @@
+# $Id: __init__.py 9030 2022-03-05 23:28:32Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+# Internationalization details are documented in
+# <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+
+"""
+This package contains modules for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from importlib import import_module
+
+from docutils.utils import normalize_language_tag
+
+
+class LanguageImporter:
+ """Import language modules.
+
+ When called with a BCP 47 language tag, instances return a module
+ with localisations from `docutils.languages` or the PYTHONPATH.
+
+ If there is no matching module, warn (if a `reporter` is passed)
+ and fall back to English.
+ """
+ packages = ('docutils.languages.', '')
+ warn_msg = ('Language "%s" not supported: '
+ 'Docutils-generated text will be in English.')
+ fallback = 'en'
+ # TODO: use a dummy module returning empty strings?, configurable?
+
+ def __init__(self):
+ self.cache = {}
+
+ def import_from_packages(self, name, reporter=None):
+ """Try loading module `name` from `self.packages`."""
+ module = None
+ for package in self.packages:
+ try:
+ module = import_module(package+name)
+ self.check_content(module)
+ except (ImportError, AttributeError):
+ if reporter and module:
+ reporter.info(f'{module} is no complete '
+ 'Docutils language module.')
+ elif reporter:
+ reporter.info(f'Module "{package+name}" not found.')
+ continue
+ break
+ return module
+
+ def check_content(self, module):
+ """Check if we got a Docutils language module."""
+ if not (isinstance(module.labels, dict)
+ and isinstance(module.bibliographic_fields, dict)
+ and isinstance(module.author_separators, list)):
+ raise ImportError
+
+ def __call__(self, language_code, reporter=None):
+ try:
+ return self.cache[language_code]
+ except KeyError:
+ pass
+ for tag in normalize_language_tag(language_code):
+ tag = tag.replace('-', '_') # '-' not valid in module names
+ module = self.import_from_packages(tag, reporter)
+ if module is not None:
+ break
+ else:
+ if reporter:
+ reporter.warning(self.warn_msg % language_code)
+ if self.fallback:
+ module = self.import_from_packages(self.fallback)
+ if reporter and (language_code != 'en'):
+ reporter.info('Using %s for language "%s".'
+ % (module, language_code))
+ self.cache[language_code] = module
+ return module
+
+
+get_language = LanguageImporter()
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/af.py b/.venv/lib/python3.12/site-packages/docutils/languages/af.py
new file mode 100644
index 00000000..b78f3f7d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/af.py
@@ -0,0 +1,58 @@
+# $Id: af.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Jannie Hofmeyr <jhsh@sun.ac.za>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Afrikaans-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ 'author': 'Auteur',
+ 'authors': 'Auteurs',
+ 'organization': 'Organisasie',
+ 'address': 'Adres',
+ 'contact': 'Kontak',
+ 'version': 'Weergawe',
+ 'revision': 'Revisie',
+ 'status': 'Status',
+ 'date': 'Datum',
+ 'copyright': 'Kopiereg',
+ 'dedication': 'Opdrag',
+ 'abstract': 'Opsomming',
+ 'attention': 'Aandag!',
+ 'caution': 'Wees versigtig!',
+ 'danger': '!GEVAAR!',
+ 'error': 'Fout',
+ 'hint': 'Wenk',
+ 'important': 'Belangrik',
+ 'note': 'Nota',
+ 'tip': 'Tip', # hint and tip both have the same translation: wenk
+ 'warning': 'Waarskuwing',
+ 'contents': 'Inhoud'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ 'auteur': 'author',
+ 'auteurs': 'authors',
+ 'organisasie': 'organization',
+ 'adres': 'address',
+ 'kontak': 'contact',
+ 'weergawe': 'version',
+ 'revisie': 'revision',
+ 'status': 'status',
+ 'datum': 'date',
+ 'kopiereg': 'copyright',
+ 'opdrag': 'dedication',
+ 'opsomming': 'abstract'}
+"""Afrikaans (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/ar.py b/.venv/lib/python3.12/site-packages/docutils/languages/ar.py
new file mode 100644
index 00000000..d6aebd0b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/ar.py
@@ -0,0 +1,60 @@
+# $Id: fa.py 4564 2016-08-10 11:48:42Z
+# Author: Shahin <me@5hah.in>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Arabic-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'المؤلف',
+ 'authors': 'المؤلفون',
+ 'organization': 'التنظيم',
+ 'address': 'العنوان',
+ 'contact': 'اتصل',
+ 'version': 'نسخة',
+ 'revision': 'مراجعة',
+ 'status': 'الحالة',
+ 'date': 'تاریخ',
+ 'copyright': 'الحقوق',
+ 'dedication': 'إهداء',
+ 'abstract': 'ملخص',
+ 'attention': 'تنبيه',
+ 'caution': 'احتیاط',
+ 'danger': 'خطر',
+ 'error': 'خطأ',
+ 'hint': 'تلميح',
+ 'important': 'مهم',
+ 'note': 'ملاحظة',
+ 'tip': 'نصيحة',
+ 'warning': 'تحذير',
+ 'contents': 'المحتوى'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'مؤلف': 'author',
+ 'مؤلفون': 'authors',
+ 'التنظيم': 'organization',
+ 'العنوان': 'address',
+ 'اتصل': 'contact',
+ 'نسخة': 'version',
+ 'مراجعة': 'revision',
+ 'الحالة': 'status',
+ 'تاریخ': 'date',
+ 'الحقوق': 'copyright',
+ 'إهداء': 'dedication',
+ 'ملخص': 'abstract'}
+"""Arabic (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = ['؛', '،']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/ca.py b/.venv/lib/python3.12/site-packages/docutils/languages/ca.py
new file mode 100644
index 00000000..d5faf39a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/ca.py
@@ -0,0 +1,65 @@
+# $Id: ca.py 9457 2023-10-02 16:25:50Z milde $
+# Authors: Ivan Vilata i Balaguer <ivan@selidor.net>;
+# Antoni Bella Pérez <antonibella5@yahoo.com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation,
+# please read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+# These translations can be used without changes for
+# Valencian variant of Catalan (use language tag "ca-valencia").
+# Checked by a native speaker of Valentian.
+
+"""
+Catalan-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Autor',
+ 'authors': 'Autors',
+ 'organization': 'Organització',
+ 'address': 'Adreça',
+ 'contact': 'Contacte',
+ 'version': 'Versió',
+ 'revision': 'Revisió',
+ 'status': 'Estat',
+ 'date': 'Data',
+ 'copyright': 'Copyright',
+ 'dedication': 'Dedicatòria',
+ 'abstract': 'Resum',
+ 'attention': 'Atenció!',
+ 'caution': 'Compte!',
+ 'danger': 'PERILL!',
+ 'error': 'Error',
+ 'hint': 'Suggeriment',
+ 'important': 'Important',
+ 'note': 'Nota',
+ 'tip': 'Consell',
+ 'warning': 'Avís',
+ 'contents': 'Contingut'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'autor': 'author',
+ 'autors': 'authors',
+ 'organització': 'organization',
+ 'adreça': 'address',
+ 'contacte': 'contact',
+ 'versió': 'version',
+ 'revisió': 'revision',
+ 'estat': 'status',
+ 'data': 'date',
+ 'copyright': 'copyright',
+ 'dedicatòria': 'dedication',
+ 'resum': 'abstract'}
+"""Catalan (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/cs.py b/.venv/lib/python3.12/site-packages/docutils/languages/cs.py
new file mode 100644
index 00000000..7ca0ff58
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/cs.py
@@ -0,0 +1,60 @@
+# $Id: cs.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Marek Blaha <mb@dat.cz>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Czech-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Autor',
+ 'authors': 'Autoři',
+ 'organization': 'Organizace',
+ 'address': 'Adresa',
+ 'contact': 'Kontakt',
+ 'version': 'Verze',
+ 'revision': 'Revize',
+ 'status': 'Stav',
+ 'date': 'Datum',
+ 'copyright': 'Copyright',
+ 'dedication': 'Věnování',
+ 'abstract': 'Abstrakt',
+ 'attention': 'Pozor!',
+ 'caution': 'Opatrně!',
+ 'danger': '!NEBEZPEČÍ!',
+ 'error': 'Chyba',
+ 'hint': 'Rada',
+ 'important': 'Důležité',
+ 'note': 'Poznámka',
+ 'tip': 'Tip',
+ 'warning': 'Varování',
+ 'contents': 'Obsah'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'autor': 'author',
+ 'autoři': 'authors',
+ 'organizace': 'organization',
+ 'adresa': 'address',
+ 'kontakt': 'contact',
+ 'verze': 'version',
+ 'revize': 'revision',
+ 'stav': 'status',
+ 'datum': 'date',
+ 'copyright': 'copyright',
+ 'věnování': 'dedication',
+ 'abstrakt': 'abstract'}
+"""Czech (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/da.py b/.venv/lib/python3.12/site-packages/docutils/languages/da.py
new file mode 100644
index 00000000..d683b732
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/da.py
@@ -0,0 +1,61 @@
+# $Id: da.py 9030 2022-03-05 23:28:32Z milde $
+# Author: E D
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Danish-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Forfatter',
+ 'authors': 'Forfattere',
+ 'organization': 'Organisation',
+ 'address': 'Adresse',
+ 'contact': 'Kontakt',
+ 'version': 'Version',
+ 'revision': 'Revision',
+ 'status': 'Status',
+ 'date': 'Dato',
+ 'copyright': 'Copyright',
+ 'dedication': 'Dedikation',
+ 'abstract': 'Resumé',
+ 'attention': 'Giv agt!',
+ 'caution': 'Pas på!',
+ 'danger': '!FARE!',
+ 'error': 'Fejl',
+ 'hint': 'Vink',
+ 'important': 'Vigtigt',
+ 'note': 'Bemærk',
+ 'tip': 'Tips',
+ 'warning': 'Advarsel',
+ 'contents': 'Indhold'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'forfatter': 'author',
+ 'forfattere': 'authors',
+ 'organisation': 'organization',
+ 'adresse': 'address',
+ 'kontakt': 'contact',
+ 'version': 'version',
+ 'revision': 'revision',
+ 'status': 'status',
+ 'dato': 'date',
+ 'copyright': 'copyright',
+ 'dedikation': 'dedication',
+ 'resume': 'abstract',
+ 'resumé': 'abstract'}
+"""Danish (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/de.py b/.venv/lib/python3.12/site-packages/docutils/languages/de.py
new file mode 100644
index 00000000..0d6c82e7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/de.py
@@ -0,0 +1,58 @@
+# $Id: de.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Gunnar Schwant <g.schwant@gmx.de>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+German language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ 'author': 'Autor',
+ 'authors': 'Autoren',
+ 'organization': 'Organisation',
+ 'address': 'Adresse',
+ 'contact': 'Kontakt',
+ 'version': 'Version',
+ 'revision': 'Revision',
+ 'status': 'Status',
+ 'date': 'Datum',
+ 'dedication': 'Widmung',
+ 'copyright': 'Copyright',
+ 'abstract': 'Zusammenfassung',
+ 'attention': 'Achtung!',
+ 'caution': 'Vorsicht!',
+ 'danger': '!GEFAHR!',
+ 'error': 'Fehler',
+ 'hint': 'Hinweis',
+ 'important': 'Wichtig',
+ 'note': 'Bemerkung',
+ 'tip': 'Tipp',
+ 'warning': 'Warnung',
+ 'contents': 'Inhalt'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ 'autor': 'author',
+ 'autoren': 'authors',
+ 'organisation': 'organization',
+ 'adresse': 'address',
+ 'kontakt': 'contact',
+ 'version': 'version',
+ 'revision': 'revision',
+ 'status': 'status',
+ 'datum': 'date',
+ 'copyright': 'copyright',
+ 'widmung': 'dedication',
+ 'zusammenfassung': 'abstract'}
+"""German (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/en.py b/.venv/lib/python3.12/site-packages/docutils/languages/en.py
new file mode 100644
index 00000000..683411d1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/en.py
@@ -0,0 +1,60 @@
+# $Id: en.py 9030 2022-03-05 23:28:32Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+English-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Author',
+ 'authors': 'Authors',
+ 'organization': 'Organization',
+ 'address': 'Address',
+ 'contact': 'Contact',
+ 'version': 'Version',
+ 'revision': 'Revision',
+ 'status': 'Status',
+ 'date': 'Date',
+ 'copyright': 'Copyright',
+ 'dedication': 'Dedication',
+ 'abstract': 'Abstract',
+ 'attention': 'Attention!',
+ 'caution': 'Caution!',
+ 'danger': '!DANGER!',
+ 'error': 'Error',
+ 'hint': 'Hint',
+ 'important': 'Important',
+ 'note': 'Note',
+ 'tip': 'Tip',
+ 'warning': 'Warning',
+ 'contents': 'Contents'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'author': 'author',
+ 'authors': 'authors',
+ 'organization': 'organization',
+ 'address': 'address',
+ 'contact': 'contact',
+ 'version': 'version',
+ 'revision': 'revision',
+ 'status': 'status',
+ 'date': 'date',
+ 'copyright': 'copyright',
+ 'dedication': 'dedication',
+ 'abstract': 'abstract'}
+"""English (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/eo.py b/.venv/lib/python3.12/site-packages/docutils/languages/eo.py
new file mode 100644
index 00000000..32ea482e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/eo.py
@@ -0,0 +1,61 @@
+# $Id: eo.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Marcelo Huerta San Martin <richieadler@users.sourceforge.net>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Esperanto-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Aŭtoro',
+ 'authors': 'Aŭtoroj',
+ 'organization': 'Organizo',
+ 'address': 'Adreso',
+ 'contact': 'Kontakto',
+ 'version': 'Versio',
+ 'revision': 'Revido',
+ 'status': 'Stato',
+ 'date': 'Dato',
+ # 'copyright': 'Kopirajto',
+ 'copyright': 'Aŭtorrajto',
+ 'dedication': 'Dediĉo',
+ 'abstract': 'Resumo',
+ 'attention': 'Atentu!',
+ 'caution': 'Zorgu!',
+ 'danger': 'DANĜERO!',
+ 'error': 'Eraro',
+ 'hint': 'Spuro',
+ 'important': 'Grava',
+ 'note': 'Noto',
+ 'tip': 'Helpeto',
+ 'warning': 'Averto',
+ 'contents': 'Enhavo'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'aŭtoro': 'author',
+ 'aŭtoroj': 'authors',
+ 'organizo': 'organization',
+ 'adreso': 'address',
+ 'kontakto': 'contact',
+ 'versio': 'version',
+ 'revido': 'revision',
+ 'stato': 'status',
+ 'dato': 'date',
+ 'aŭtorrajto': 'copyright',
+ 'dediĉo': 'dedication',
+ 'resumo': 'abstract'}
+"""Esperanto (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/es.py b/.venv/lib/python3.12/site-packages/docutils/languages/es.py
new file mode 100644
index 00000000..2b66e7cd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/es.py
@@ -0,0 +1,58 @@
+# $Id: es.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Marcelo Huerta San Martín <richieadler@users.sourceforge.net>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Spanish-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ 'author': 'Autor',
+ 'authors': 'Autores',
+ 'organization': 'Organización',
+ 'address': 'Dirección',
+ 'contact': 'Contacto',
+ 'version': 'Versión',
+ 'revision': 'Revisión',
+ 'status': 'Estado',
+ 'date': 'Fecha',
+ 'copyright': 'Copyright',
+ 'dedication': 'Dedicatoria',
+ 'abstract': 'Resumen',
+ 'attention': '¡Atención!',
+ 'caution': '¡Precaución!',
+ 'danger': '¡PELIGRO!',
+ 'error': 'Error',
+ 'hint': 'Sugerencia',
+ 'important': 'Importante',
+ 'note': 'Nota',
+ 'tip': 'Consejo',
+ 'warning': 'Advertencia',
+ 'contents': 'Contenido'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ 'autor': 'author',
+ 'autores': 'authors',
+ 'organización': 'organization',
+ 'dirección': 'address',
+ 'contacto': 'contact',
+ 'versión': 'version',
+ 'revisión': 'revision',
+ 'estado': 'status',
+ 'fecha': 'date',
+ 'copyright': 'copyright',
+ 'dedicatoria': 'dedication',
+ 'resumen': 'abstract'}
+"""Spanish (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/fa.py b/.venv/lib/python3.12/site-packages/docutils/languages/fa.py
new file mode 100644
index 00000000..f25814d2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/fa.py
@@ -0,0 +1,60 @@
+# $Id: fa.py 4564 2016-08-10 11:48:42Z
+# Author: Shahin <me@5hah.in>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Persian-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'نویسنده',
+ 'authors': 'نویسندگان',
+ 'organization': 'سازمان',
+ 'address': 'آدرس',
+ 'contact': 'تماس',
+ 'version': 'نسخه',
+ 'revision': 'بازبینی',
+ 'status': 'وضعیت',
+ 'date': 'تاریخ',
+ 'copyright': 'کپی‌رایت',
+ 'dedication': 'تخصیص',
+ 'abstract': 'چکیده',
+ 'attention': 'توجه!',
+ 'caution': 'احتیاط!',
+ 'danger': 'خطر!',
+ 'error': 'خطا',
+ 'hint': 'راهنما',
+ 'important': 'مهم',
+ 'note': 'یادداشت',
+ 'tip': 'نکته',
+ 'warning': 'اخطار',
+ 'contents': 'محتوا'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'نویسنده': 'author',
+ 'نویسندگان': 'authors',
+ 'سازمان': 'organization',
+ 'آدرس': 'address',
+ 'تماس': 'contact',
+ 'نسخه': 'version',
+ 'بازبینی': 'revision',
+ 'وضعیت': 'status',
+ 'تاریخ': 'date',
+ 'کپی‌رایت': 'copyright',
+ 'تخصیص': 'dedication',
+ 'چکیده': 'abstract'}
+"""Persian (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = ['؛', '،']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/fi.py b/.venv/lib/python3.12/site-packages/docutils/languages/fi.py
new file mode 100644
index 00000000..2b401dba
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/fi.py
@@ -0,0 +1,60 @@
+# $Id: fi.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Asko Soukka <asko.soukka@iki.fi>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Finnish-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Tekijä',
+ 'authors': 'Tekijät',
+ 'organization': 'Yhteisö',
+ 'address': 'Osoite',
+ 'contact': 'Yhteystiedot',
+ 'version': 'Versio',
+ 'revision': 'Vedos',
+ 'status': 'Tila',
+ 'date': 'Päiväys',
+ 'copyright': 'Tekijänoikeudet',
+ 'dedication': 'Omistuskirjoitus',
+ 'abstract': 'Tiivistelmä',
+ 'attention': 'Huomio!',
+ 'caution': 'Varo!',
+ 'danger': '!VAARA!',
+ 'error': 'Virhe',
+ 'hint': 'Vihje',
+ 'important': 'Tärkeää',
+ 'note': 'Huomautus',
+ 'tip': 'Neuvo',
+ 'warning': 'Varoitus',
+ 'contents': 'Sisällys'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'tekijä': 'author',
+ 'tekijät': 'authors',
+ 'yhteisö': 'organization',
+ 'osoite': 'address',
+ 'yhteystiedot': 'contact',
+ 'versio': 'version',
+ 'vedos': 'revision',
+ 'tila': 'status',
+ 'päiväys': 'date',
+ 'tekijänoikeudet': 'copyright',
+ 'omistuskirjoitus': 'dedication',
+ 'tiivistelmä': 'abstract'}
+"""Finnish (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/fr.py b/.venv/lib/python3.12/site-packages/docutils/languages/fr.py
new file mode 100644
index 00000000..926455bc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/fr.py
@@ -0,0 +1,58 @@
+# $Id: fr.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Stefane Fermigier <sf@fermigier.com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+French-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ 'author': 'Auteur',
+ 'authors': 'Auteurs',
+ 'organization': 'Organisation',
+ 'address': 'Adresse',
+ 'contact': 'Contact',
+ 'version': 'Version',
+ 'revision': 'Révision',
+ 'status': 'Statut',
+ 'date': 'Date',
+ 'copyright': 'Copyright',
+ 'dedication': 'Dédicace',
+ 'abstract': 'Résumé',
+ 'attention': 'Attention!',
+ 'caution': 'Avertissement!',
+ 'danger': '!DANGER!',
+ 'error': 'Erreur',
+ 'hint': 'Indication',
+ 'important': 'Important',
+ 'note': 'Note',
+ 'tip': 'Astuce',
+ 'warning': 'Avis',
+ 'contents': 'Sommaire'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ 'auteur': 'author',
+ 'auteurs': 'authors',
+ 'organisation': 'organization',
+ 'adresse': 'address',
+ 'contact': 'contact',
+ 'version': 'version',
+ 'révision': 'revision',
+ 'statut': 'status',
+ 'date': 'date',
+ 'copyright': 'copyright',
+ 'dédicace': 'dedication',
+ 'résumé': 'abstract'}
+"""French (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/gl.py b/.venv/lib/python3.12/site-packages/docutils/languages/gl.py
new file mode 100644
index 00000000..f3864abf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/gl.py
@@ -0,0 +1,62 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision: 2224 $
+# Date: $Date: 2004-06-05 21:40:46 +0200 (Sat, 05 Jun 2004) $
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Galician-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Autor',
+ 'authors': 'Autores',
+ 'organization': 'Organización',
+ 'address': 'Enderezo',
+ 'contact': 'Contacto',
+ 'version': 'Versión',
+ 'revision': 'Revisión',
+ 'status': 'Estado',
+ 'date': 'Data',
+ 'copyright': 'Dereitos de copia',
+ 'dedication': 'Dedicatoria',
+ 'abstract': 'Abstract',
+ 'attention': 'Atención!',
+ 'caution': 'Advertencia!',
+ 'danger': 'PERIGO!',
+ 'error': 'Erro',
+ 'hint': 'Consello',
+ 'important': 'Importante',
+ 'note': 'Nota',
+ 'tip': 'Suxestión',
+ 'warning': 'Aviso',
+ 'contents': 'Contido'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'autor': 'author',
+ 'autores': 'authors',
+ 'organización': 'organization',
+ 'enderezo': 'address',
+ 'contacto': 'contact',
+ 'versión': 'version',
+ 'revisión': 'revision',
+ 'estado': 'status',
+ 'data': 'date',
+ 'dereitos de copia': 'copyright',
+ 'dedicatoria': 'dedication',
+ 'abstract': 'abstract'}
+"""Galician (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/he.py b/.venv/lib/python3.12/site-packages/docutils/languages/he.py
new file mode 100644
index 00000000..018cc01a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/he.py
@@ -0,0 +1,62 @@
+# Author: Meir Kriheli
+# Id: $Id: he.py 9452 2023-09-27 00:11:54Z milde $
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Hebrew-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'מחבר',
+ 'authors': 'מחברי',
+ 'organization': 'ארגון',
+ 'address': 'כתובת',
+ 'contact': 'איש קשר',
+ 'version': 'גרסה',
+ 'revision': 'מהדורה',
+ 'status': 'סטטוס',
+ 'date': 'תאריך',
+ 'copyright': 'זכויות שמורות',
+ 'dedication': 'הקדשה',
+ 'abstract': 'תקציר',
+ 'attention': 'תשומת לב',
+ 'caution': 'זהירות',
+ 'danger': 'סכנה',
+ 'error': 'שגיאה',
+ 'hint': 'רמז',
+ 'important': 'חשוב',
+ 'note': 'הערה',
+ 'tip': 'טיפ',
+ 'warning': 'אזהרה',
+ 'contents': 'תוכן',
+ }
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'מחבר': 'author',
+ 'מחברי': 'authors',
+ 'ארגון': 'organization',
+ 'כתובת': 'address',
+ 'איש קשר': 'contact',
+ 'גרסה': 'version',
+ 'מהדורה': 'revision',
+ 'סטטוס': 'status',
+ 'תאריך': 'date',
+ 'זכויות שמורות': 'copyright',
+ 'הקדשה': 'dedication',
+ 'תקציר': 'abstract',
+ }
+"""Hebrew to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/it.py b/.venv/lib/python3.12/site-packages/docutils/languages/it.py
new file mode 100644
index 00000000..798ccf95
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/it.py
@@ -0,0 +1,58 @@
+# $Id: it.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Nicola Larosa <docutils@tekNico.net>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Italian-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ 'author': 'Autore',
+ 'authors': 'Autori',
+ 'organization': 'Organizzazione',
+ 'address': 'Indirizzo',
+ 'contact': 'Contatti',
+ 'version': 'Versione',
+ 'revision': 'Revisione',
+ 'status': 'Status',
+ 'date': 'Data',
+ 'copyright': 'Copyright',
+ 'dedication': 'Dedica',
+ 'abstract': 'Riassunto',
+ 'attention': 'Attenzione!',
+ 'caution': 'Cautela!',
+ 'danger': '!PERICOLO!',
+ 'error': 'Errore',
+ 'hint': 'Suggerimento',
+ 'important': 'Importante',
+ 'note': 'Nota',
+ 'tip': 'Consiglio',
+ 'warning': 'Avvertenza',
+ 'contents': 'Indice'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ 'autore': 'author',
+ 'autori': 'authors',
+ 'organizzazione': 'organization',
+ 'indirizzo': 'address',
+ 'contatto': 'contact',
+ 'versione': 'version',
+ 'revisione': 'revision',
+ 'status': 'status',
+ 'data': 'date',
+ 'copyright': 'copyright',
+ 'dedica': 'dedication',
+ 'riassunto': 'abstract'}
+"""Italian (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/ja.py b/.venv/lib/python3.12/site-packages/docutils/languages/ja.py
new file mode 100644
index 00000000..b936e220
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/ja.py
@@ -0,0 +1,60 @@
+# $Id: ja.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Hisashi Morita <hisashim@kt.rim.or.jp>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Japanese-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': '著者',
+ 'authors': '著者',
+ 'organization': '組織',
+ 'address': '住所',
+ 'contact': '連絡先',
+ 'version': 'バージョン',
+ 'revision': 'リビジョン',
+ 'status': 'ステータス',
+ 'date': '日付',
+ 'copyright': '著作権',
+ 'dedication': '献辞',
+ 'abstract': '概要',
+ 'attention': '注目!',
+ 'caution': '注意!',
+ 'danger': '!危険!',
+ 'error': 'エラー',
+ 'hint': 'ヒント',
+ 'important': '重要',
+ 'note': '備考',
+ 'tip': '通報',
+ 'warning': '警告',
+ 'contents': '目次'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ '著者': 'author',
+ ' n/a': 'authors',
+ '組織': 'organization',
+ '住所': 'address',
+ '連絡先': 'contact',
+ 'バージョン': 'version',
+ 'リビジョン': 'revision',
+ 'ステータス': 'status',
+ '日付': 'date',
+ '著作権': 'copyright',
+ '献辞': 'dedication',
+ '概要': 'abstract'}
+"""Japanese (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/ka.py b/.venv/lib/python3.12/site-packages/docutils/languages/ka.py
new file mode 100644
index 00000000..352388d9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/ka.py
@@ -0,0 +1,58 @@
+# $Id: ka.py 9444 2023-08-23 12:02:41Z grubert $
+# Author: Temuri Doghonadze <temuri dot doghonadze at gmail dot com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Georgian-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ 'abstract': 'ანოტაცია',
+ 'address': 'მისამართი',
+ 'attention': 'ყურადღება!',
+ 'author': 'ავტორი',
+ 'authors': 'ავტორები',
+ 'caution': 'ფრთხილად!',
+ 'contact': 'კონტაქტი',
+ 'contents': 'შემცველობა',
+ 'copyright': 'საავტორო უფლებები',
+ 'danger': 'საშიშია!',
+ 'date': 'თარიღი',
+ 'dedication': 'მიძღვნა',
+ 'error': 'შეცდომა',
+ 'hint': 'რჩევა',
+ 'important': 'მნიშვნელოვანია',
+ 'note': 'შენიშვნა',
+ 'organization': 'ორგანიზაცია',
+ 'revision': 'რევიზია',
+ 'status': 'სტატუსი',
+ 'tip': 'მინიშნება',
+ 'version': 'ვერსია',
+ 'warning': 'გაფრთხილება'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ 'ანოტაცია': 'abstract',
+ 'მისამართი': 'address',
+ 'ავტორი': 'author',
+ 'ავტორები': 'authors',
+ 'კონტაქტი': 'contact',
+ 'საავტორო უფლებები': 'copyright',
+ 'თარიღი': 'date',
+ 'მიძღვნა': 'dedication',
+ 'ორგანიზაცია': 'organization',
+ 'რევიზია': 'revision',
+ 'სტატუსი': 'status',
+ 'ვერსია': 'version'}
+"""Georgian (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/ko.py b/.venv/lib/python3.12/site-packages/docutils/languages/ko.py
new file mode 100644
index 00000000..c7521d1f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/ko.py
@@ -0,0 +1,60 @@
+# $Id: ko.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Thomas SJ Kang <thomas.kangsj@ujuc.kr>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Korean-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': '저자',
+ 'authors': '저자들',
+ 'organization': '조직',
+ 'address': '주소',
+ 'contact': '연락처',
+ 'version': '버전',
+ 'revision': '리비전',
+ 'status': '상태',
+ 'date': '날짜',
+ 'copyright': '저작권',
+ 'dedication': '헌정',
+ 'abstract': '요약',
+ 'attention': '집중!',
+ 'caution': '주의!',
+ 'danger': '!위험!',
+ 'error': '오류',
+ 'hint': '실마리',
+ 'important': '중요한',
+ 'note': '비고',
+ 'tip': '팁',
+ 'warning': '경고',
+ 'contents': '목차'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ '저자': 'author',
+ '저자들': 'authors',
+ '조직': 'organization',
+ '주소': 'address',
+ '연락처': 'contact',
+ '버전': 'version',
+ '리비전': 'revision',
+ '상태': 'status',
+ '날짜': 'date',
+ '저작권': 'copyright',
+ '헌정': 'dedication',
+ '요약': 'abstract'}
+"""Korean to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/lt.py b/.venv/lib/python3.12/site-packages/docutils/languages/lt.py
new file mode 100644
index 00000000..14c90f26
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/lt.py
@@ -0,0 +1,60 @@
+# $Id: lt.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Dalius Dobravolskas <dalius.do...@gmail.com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Lithuanian language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Autorius',
+ 'authors': 'Autoriai',
+ 'organization': 'Organizacija',
+ 'address': 'Adresas',
+ 'contact': 'Kontaktas',
+ 'version': 'Versija',
+ 'revision': 'Revizija',
+ 'status': 'Būsena',
+ 'date': 'Data',
+ 'copyright': 'Autoriaus teisės',
+ 'dedication': 'Dedikacija',
+ 'abstract': 'Santrauka',
+ 'attention': 'Dėmesio!',
+ 'caution': 'Atsargiai!',
+ 'danger': '!PAVOJINGA!',
+ 'error': 'Klaida',
+ 'hint': 'Užuomina',
+ 'important': 'Svarbu',
+ 'note': 'Pastaba',
+ 'tip': 'Patarimas',
+ 'warning': 'Įspėjimas',
+ 'contents': 'Turinys'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'autorius': 'author',
+ 'autoriai': 'authors',
+ 'organizacija': 'organization',
+ 'adresas': 'address',
+ 'kontaktas': 'contact',
+ 'versija': 'version',
+ 'revizija': 'revision',
+ 'būsena': 'status',
+ 'data': 'date',
+ 'autoriaus teisės': 'copyright',
+ 'dedikacija': 'dedication',
+ 'santrauka': 'abstract'}
+"""Lithuanian (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/lv.py b/.venv/lib/python3.12/site-packages/docutils/languages/lv.py
new file mode 100644
index 00000000..812e95af
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/lv.py
@@ -0,0 +1,59 @@
+# $Id: lv.py 9030 2022-03-05 23:28:32Z milde $
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Latvian-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Autors',
+ 'authors': 'Autori',
+ 'organization': 'Organizācija',
+ 'address': 'Adrese',
+ 'contact': 'Kontakti',
+ 'version': 'Versija',
+ 'revision': 'Revīzija',
+ 'status': 'Statuss',
+ 'date': 'Datums',
+ 'copyright': 'Copyright',
+ 'dedication': 'Veltījums',
+ 'abstract': 'Atreferējums',
+ 'attention': 'Uzmanību!',
+ 'caution': 'Piesardzību!',
+ 'danger': '!BĪSTAMI!',
+ 'error': 'Kļūda',
+ 'hint': 'Ieteikums',
+ 'important': 'Svarīgi',
+ 'note': 'Piezīme',
+ 'tip': 'Padoms',
+ 'warning': 'Brīdinājums',
+ 'contents': 'Saturs'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'autors': 'author',
+ 'autori': 'authors',
+ 'organizācija': 'organization',
+ 'adrese': 'address',
+ 'kontakti': 'contact',
+ 'versija': 'version',
+ 'revīzija': 'revision',
+ 'statuss': 'status',
+ 'datums': 'date',
+ 'copyright': 'copyright',
+ 'veltījums': 'dedication',
+ 'atreferējums': 'abstract'}
+"""English (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/nl.py b/.venv/lib/python3.12/site-packages/docutils/languages/nl.py
new file mode 100644
index 00000000..53540e02
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/nl.py
@@ -0,0 +1,60 @@
+# $Id: nl.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Martijn Pieters <mjpieters@users.sourceforge.net>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Dutch-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Auteur',
+ 'authors': 'Auteurs',
+ 'organization': 'Organisatie',
+ 'address': 'Adres',
+ 'contact': 'Contact',
+ 'version': 'Versie',
+ 'revision': 'Revisie',
+ 'status': 'Status',
+ 'date': 'Datum',
+ 'copyright': 'Copyright',
+ 'dedication': 'Toewijding',
+ 'abstract': 'Samenvatting',
+ 'attention': 'Attentie!',
+ 'caution': 'Let op!',
+ 'danger': '!GEVAAR!',
+ 'error': 'Fout',
+ 'hint': 'Hint',
+ 'important': 'Belangrijk',
+ 'note': 'Opmerking',
+ 'tip': 'Tip',
+ 'warning': 'Waarschuwing',
+ 'contents': 'Inhoud'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'auteur': 'author',
+ 'auteurs': 'authors',
+ 'organisatie': 'organization',
+ 'adres': 'address',
+ 'contact': 'contact',
+ 'versie': 'version',
+ 'revisie': 'revision',
+ 'status': 'status',
+ 'datum': 'date',
+ 'copyright': 'copyright',
+ 'toewijding': 'dedication',
+ 'samenvatting': 'abstract'}
+"""Dutch (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/pl.py b/.venv/lib/python3.12/site-packages/docutils/languages/pl.py
new file mode 100644
index 00000000..606a4014
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/pl.py
@@ -0,0 +1,60 @@
+# $Id$
+# Author: Robert Wojciechowicz <rw@smsnet.pl>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Polish-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Autor',
+ 'authors': 'Autorzy',
+ 'organization': 'Organizacja',
+ 'address': 'Adres',
+ 'contact': 'Kontakt',
+ 'version': 'Wersja',
+ 'revision': 'Korekta',
+ 'status': 'Status',
+ 'date': 'Data',
+ 'copyright': 'Copyright',
+ 'dedication': 'Dedykacja',
+ 'abstract': 'Streszczenie',
+ 'attention': 'Uwaga!',
+ 'caution': 'Ostrożnie!',
+ 'danger': '!Niebezpieczeństwo!',
+ 'error': 'Błąd',
+ 'hint': 'Wskazówka',
+ 'important': 'Ważne',
+ 'note': 'Przypis',
+ 'tip': 'Rada',
+ 'warning': 'Ostrzeżenie',
+ 'contents': 'Treść'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'autor': 'author',
+ 'autorzy': 'authors',
+ 'organizacja': 'organization',
+ 'adres': 'address',
+ 'kontakt': 'contact',
+ 'wersja': 'version',
+ 'korekta': 'revision',
+ 'status': 'status',
+ 'data': 'date',
+ 'copyright': 'copyright',
+ 'dedykacja': 'dedication',
+ 'streszczenie': 'abstract'}
+"""Polish (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/pt_br.py b/.venv/lib/python3.12/site-packages/docutils/languages/pt_br.py
new file mode 100644
index 00000000..195a671b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/pt_br.py
@@ -0,0 +1,60 @@
+# $Id: pt_br.py 9452 2023-09-27 00:11:54Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Brazilian Portuguese-language mappings for language-dependent features.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': 'Autor',
+ 'authors': 'Autores',
+ 'organization': 'Organização',
+ 'address': 'Endereço',
+ 'contact': 'Contato',
+ 'version': 'Versão',
+ 'revision': 'Revisão',
+ 'status': 'Estado',
+ 'date': 'Data',
+ 'copyright': 'Copyright',
+ 'dedication': 'Dedicatória',
+ 'abstract': 'Resumo',
+ 'attention': 'Atenção!',
+ 'caution': 'Cuidado!',
+ 'danger': 'PERIGO!',
+ 'error': 'Erro',
+ 'hint': 'Sugestão',
+ 'important': 'Importante',
+ 'note': 'Nota',
+ 'tip': 'Dica',
+ 'warning': 'Aviso',
+ 'contents': 'Sumário'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'autor': 'author',
+ 'autores': 'authors',
+ 'organização': 'organization',
+ 'endereço': 'address',
+ 'contato': 'contact',
+ 'versão': 'version',
+ 'revisão': 'revision',
+ 'estado': 'status',
+ 'data': 'date',
+ 'copyright': 'copyright',
+ 'dedicatória': 'dedication',
+ 'resumo': 'abstract'}
+"""Brazilian Portuguese (lowcased) name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/ru.py b/.venv/lib/python3.12/site-packages/docutils/languages/ru.py
new file mode 100644
index 00000000..3741bd86
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/ru.py
@@ -0,0 +1,58 @@
+# $Id: ru.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Roman Suzi <rnd@onego.ru>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Russian-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ 'abstract': 'Аннотация',
+ 'address': 'Адрес',
+ 'attention': 'Внимание!',
+ 'author': 'Автор',
+ 'authors': 'Авторы',
+ 'caution': 'Осторожно!',
+ 'contact': 'Контакт',
+ 'contents': 'Содержание',
+ 'copyright': 'Права копирования',
+ 'danger': 'ОПАСНО!',
+ 'date': 'Дата',
+ 'dedication': 'Посвящение',
+ 'error': 'Ошибка',
+ 'hint': 'Совет',
+ 'important': 'Важно',
+ 'note': 'Примечание',
+ 'organization': 'Организация',
+ 'revision': 'Редакция',
+ 'status': 'Статус',
+ 'tip': 'Подсказка',
+ 'version': 'Версия',
+ 'warning': 'Предупреждение'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ 'аннотация': 'abstract',
+ 'адрес': 'address',
+ 'автор': 'author',
+ 'авторы': 'authors',
+ 'контакт': 'contact',
+ 'права копирования': 'copyright',
+ 'дата': 'date',
+ 'посвящение': 'dedication',
+ 'организация': 'organization',
+ 'редакция': 'revision',
+ 'статус': 'status',
+ 'версия': 'version'}
+"""Russian (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/sk.py b/.venv/lib/python3.12/site-packages/docutils/languages/sk.py
new file mode 100644
index 00000000..bb8c8109
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/sk.py
@@ -0,0 +1,58 @@
+# $Id: sk.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Miroslav Vasko <zemiak@zoznam.sk>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Slovak-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ 'author': 'Autor',
+ 'authors': 'Autori',
+ 'organization': 'Organizácia',
+ 'address': 'Adresa',
+ 'contact': 'Kontakt',
+ 'version': 'Verzia',
+ 'revision': 'Revízia',
+ 'status': 'Stav',
+ 'date': 'Dátum',
+ 'copyright': 'Copyright',
+ 'dedication': 'Venovanie',
+ 'abstract': 'Abstraktne',
+ 'attention': 'Pozor!',
+ 'caution': 'Opatrne!',
+ 'danger': '!NEBEZPEČENSTVO!',
+ 'error': 'Chyba',
+ 'hint': 'Rada',
+ 'important': 'Dôležité',
+ 'note': 'Poznámka',
+ 'tip': 'Tip',
+ 'warning': 'Varovanie',
+ 'contents': 'Obsah'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ 'autor': 'author',
+ 'autori': 'authors',
+ 'organizácia': 'organization',
+ 'adresa': 'address',
+ 'kontakt': 'contact',
+ 'verzia': 'version',
+ 'revízia': 'revision',
+ 'stav': 'status',
+ 'dátum': 'date',
+ 'copyright': 'copyright',
+ 'venovanie': 'dedication',
+ 'abstraktne': 'abstract'}
+"""Slovak (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/sv.py b/.venv/lib/python3.12/site-packages/docutils/languages/sv.py
new file mode 100644
index 00000000..a47ede58
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/sv.py
@@ -0,0 +1,59 @@
+# $Id: sv.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Adam Chodorowski <chodorowski@users.sourceforge.net>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Swedish language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ 'author': 'Författare',
+ 'authors': 'Författare',
+ 'organization': 'Organisation',
+ 'address': 'Adress',
+ 'contact': 'Kontakt',
+ 'version': 'Version',
+ 'revision': 'Revision',
+ 'status': 'Status',
+ 'date': 'Datum',
+ 'copyright': 'Copyright',
+ 'dedication': 'Dedikation',
+ 'abstract': 'Sammanfattning',
+ 'attention': 'Observera!',
+ 'caution': 'Akta!', # 'Varning' already used for 'warning'
+ 'danger': 'FARA!',
+ 'error': 'Fel',
+ 'hint': 'Vink',
+ 'important': 'Viktigt',
+ 'note': 'Notera',
+ 'tip': 'Tips',
+ 'warning': 'Varning',
+ 'contents': 'Innehåll'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # 'Author' and 'Authors' identical in Swedish; assume the plural:
+ 'författare': 'authors',
+ ' n/a': 'author', # removing leads to (spurious) test failure
+ 'organisation': 'organization',
+ 'adress': 'address',
+ 'kontakt': 'contact',
+ 'version': 'version',
+ 'revision': 'revision',
+ 'status': 'status',
+ 'datum': 'date',
+ 'copyright': 'copyright',
+ 'dedikation': 'dedication',
+ 'sammanfattning': 'abstract'}
+"""Swedish (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/uk.py b/.venv/lib/python3.12/site-packages/docutils/languages/uk.py
new file mode 100644
index 00000000..b591dacb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/uk.py
@@ -0,0 +1,58 @@
+# $Id: uk.py 9114 2022-07-28 17:06:10Z milde $
+# Author: Dmytro Kazanzhy <dkazanzhy@gmail.com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Ukrainian-language mappings for language-dependent features of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ 'abstract': 'Анотація',
+ 'address': 'Адреса',
+ 'attention': 'Увага!',
+ 'author': 'Автор',
+ 'authors': 'Автори',
+ 'caution': 'Обережно!',
+ 'contact': 'Контакт',
+ 'contents': 'Зміст',
+ 'copyright': 'Права копіювання',
+ 'danger': 'НЕБЕЗПЕЧНО!',
+ 'date': 'Дата',
+ 'dedication': 'Посвячення',
+ 'error': 'Помилка',
+ 'hint': 'Порада',
+ 'important': 'Важливо',
+ 'note': 'Примітка',
+ 'organization': 'Організація',
+ 'revision': 'Редакція',
+ 'status': 'Статус',
+ 'tip': 'Підказка',
+ 'version': 'Версія',
+ 'warning': 'Попередження'}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ 'анотація': 'abstract',
+ 'адреса': 'address',
+ 'автор': 'author',
+ 'автори': 'authors',
+ 'контакт': 'contact',
+ 'права копіювання': 'copyright',
+ 'дата': 'date',
+ 'посвячення': 'dedication',
+ 'організація': 'organization',
+ 'редакція': 'revision',
+ 'статус': 'status',
+ 'версія': 'version'}
+"""Ukrainian (lowcased) to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/zh_cn.py b/.venv/lib/python3.12/site-packages/docutils/languages/zh_cn.py
new file mode 100644
index 00000000..c2ff2e6a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/zh_cn.py
@@ -0,0 +1,62 @@
+# $Id: zh_cn.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Pan Junyong <panjy@zopechina.com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Simplified Chinese language mappings for language-dependent features
+of Docutils.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': '作者',
+ 'authors': '作者群',
+ 'organization': '组织',
+ 'address': '地址',
+ 'contact': '联系',
+ 'version': '版本',
+ 'revision': '修订',
+ 'status': '状态',
+ 'date': '日期',
+ 'copyright': '版权',
+ 'dedication': '献辞',
+ 'abstract': '摘要',
+ 'attention': '注意',
+ 'caution': '小心',
+ 'danger': '危险',
+ 'error': '错误',
+ 'hint': '提示',
+ 'important': '重要',
+ 'note': '注解',
+ 'tip': '技巧',
+ 'warning': '警告',
+ 'contents': '目录',
+}
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ '作者': 'author',
+ '作者群': 'authors',
+ '组织': 'organization',
+ '地址': 'address',
+ '联系': 'contact',
+ '版本': 'version',
+ '修订': 'revision',
+ '状态': 'status',
+ '时间': 'date',
+ '版权': 'copyright',
+ '献辞': 'dedication',
+ '摘要': 'abstract'}
+"""Simplified Chinese to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',', ';', ',', '、']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/languages/zh_tw.py b/.venv/lib/python3.12/site-packages/docutils/languages/zh_tw.py
new file mode 100644
index 00000000..cb59c50b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/languages/zh_tw.py
@@ -0,0 +1,61 @@
+# $Id: zh_tw.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Joe YS Jaw <joeysj@users.sourceforge.net>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Traditional Chinese language mappings for language-dependent features.
+"""
+
+__docformat__ = 'reStructuredText'
+
+labels = {
+ # fixed: language-dependent
+ 'author': '作者',
+ 'authors': '作者群',
+ 'organization': '組織',
+ 'address': '地址',
+ 'contact': '連絡',
+ 'version': '版本',
+ 'revision': '修訂',
+ 'status': '狀態',
+ 'date': '日期',
+ 'copyright': '版權',
+ 'dedication': '題獻',
+ 'abstract': '摘要',
+ 'attention': '注意!',
+ 'caution': '小心!',
+ 'danger': '!危險!',
+ 'error': '錯誤',
+ 'hint': '提示',
+ 'important': '重要',
+ 'note': '註釋',
+ 'tip': '秘訣',
+ 'warning': '警告',
+ 'contents': '目錄',
+ }
+"""Mapping of node class name to label text."""
+
+bibliographic_fields = {
+ # language-dependent: fixed
+ 'author (translation required)': 'author',
+ 'authors (translation required)': 'authors',
+ 'organization (translation required)': 'organization',
+ 'address (translation required)': 'address',
+ 'contact (translation required)': 'contact',
+ 'version (translation required)': 'version',
+ 'revision (translation required)': 'revision',
+ 'status (translation required)': 'status',
+ 'date (translation required)': 'date',
+ 'copyright (translation required)': 'copyright',
+ 'dedication (translation required)': 'dedication',
+ 'abstract (translation required)': 'abstract'}
+"""Traditional Chinese to canonical name mapping for bibliographic fields."""
+
+author_separators = [';', ',', ';', ',', '、']
+"""List of separator strings for the 'Authors' bibliographic field. Tried in
+order."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/nodes.py b/.venv/lib/python3.12/site-packages/docutils/nodes.py
new file mode 100644
index 00000000..63a38981
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/nodes.py
@@ -0,0 +1,2301 @@
+# $Id: nodes.py 9612 2024-04-05 23:07:13Z milde $
+# Author: David Goodger <goodger@python.org>
+# Maintainer: docutils-develop@lists.sourceforge.net
+# Copyright: This module has been placed in the public domain.
+
+"""
+Docutils document tree element class library.
+
+Classes in CamelCase are abstract base classes or auxiliary classes. The one
+exception is `Text`, for a text (PCDATA) node; uppercase is used to
+differentiate from element classes. Classes in lower_case_with_underscores
+are element classes, matching the XML element generic identifiers in the DTD_.
+
+The position of each node (the level at which it can occur) is significant and
+is represented by abstract base classes (`Root`, `Structural`, `Body`,
+`Inline`, etc.). Certain transformations will be easier because we can use
+``isinstance(node, base_class)`` to determine the position of the node in the
+hierarchy.
+
+.. _DTD: https://docutils.sourceforge.io/docs/ref/docutils.dtd
+"""
+
+__docformat__ = 'reStructuredText'
+
+from collections import Counter
+import re
+import sys
+import warnings
+import unicodedata
+# import xml.dom.minidom as dom # -> conditional import in Node.asdom()
+# and document.asdom()
+
+# import docutils.transforms # -> conditional import in document.__init__()
+
+
+# ==============================
+# Functional Node Base Classes
+# ==============================
+
+class Node:
+ """Abstract base class of nodes in a document tree."""
+
+ parent = None
+ """Back-reference to the Node immediately containing this Node."""
+
+ source = None
+ """Path or description of the input source which generated this Node."""
+
+ line = None
+ """The line number (1-based) of the beginning of this Node in `source`."""
+
+ _document = None
+
+ @property
+ def document(self):
+ """Return the `document` root node of the tree containing this Node.
+ """
+ try:
+ return self._document or self.parent.document
+ except AttributeError:
+ return None
+
+ @document.setter
+ def document(self, value):
+ self._document = value
+
+ def __bool__(self):
+ """
+ Node instances are always true, even if they're empty. A node is more
+ than a simple container. Its boolean "truth" does not depend on
+ having one or more subnodes in the doctree.
+
+ Use `len()` to check node length.
+ """
+ return True
+
+ def asdom(self, dom=None):
+ """Return a DOM **fragment** representation of this Node."""
+ if dom is None:
+ import xml.dom.minidom as dom
+ domroot = dom.Document()
+ return self._dom_node(domroot)
+
+ def pformat(self, indent=' ', level=0):
+ """
+ Return an indented pseudo-XML representation, for test purposes.
+
+ Override in subclasses.
+ """
+ raise NotImplementedError
+
+ def copy(self):
+ """Return a copy of self."""
+ raise NotImplementedError
+
+ def deepcopy(self):
+ """Return a deep copy of self (also copying children)."""
+ raise NotImplementedError
+
+ def astext(self):
+ """Return a string representation of this Node."""
+ raise NotImplementedError
+
+ def setup_child(self, child):
+ child.parent = self
+ if self.document:
+ child.document = self.document
+ if child.source is None:
+ child.source = self.document.current_source
+ if child.line is None:
+ child.line = self.document.current_line
+
+ def walk(self, visitor):
+ """
+ Traverse a tree of `Node` objects, calling the
+ `dispatch_visit()` method of `visitor` when entering each
+ node. (The `walkabout()` method is similar, except it also
+ calls the `dispatch_departure()` method before exiting each
+ node.)
+
+ This tree traversal supports limited in-place tree
+ modifications. Replacing one node with one or more nodes is
+ OK, as is removing an element. However, if the node removed
+ or replaced occurs after the current node, the old node will
+ still be traversed, and any new nodes will not.
+
+ Within ``visit`` methods (and ``depart`` methods for
+ `walkabout()`), `TreePruningException` subclasses may be raised
+ (`SkipChildren`, `SkipSiblings`, `SkipNode`, `SkipDeparture`).
+
+ Parameter `visitor`: A `NodeVisitor` object, containing a
+ ``visit`` implementation for each `Node` subclass encountered.
+
+ Return true if we should stop the traversal.
+ """
+ stop = False
+ visitor.document.reporter.debug(
+ 'docutils.nodes.Node.walk calling dispatch_visit for %s'
+ % self.__class__.__name__)
+ try:
+ try:
+ visitor.dispatch_visit(self)
+ except (SkipChildren, SkipNode):
+ return stop
+ except SkipDeparture: # not applicable; ignore
+ pass
+ children = self.children
+ try:
+ for child in children[:]:
+ if child.walk(visitor):
+ stop = True
+ break
+ except SkipSiblings:
+ pass
+ except StopTraversal:
+ stop = True
+ return stop
+
+ def walkabout(self, visitor):
+ """
+ Perform a tree traversal similarly to `Node.walk()` (which
+ see), except also call the `dispatch_departure()` method
+ before exiting each node.
+
+ Parameter `visitor`: A `NodeVisitor` object, containing a
+ ``visit`` and ``depart`` implementation for each `Node`
+ subclass encountered.
+
+ Return true if we should stop the traversal.
+ """
+ call_depart = True
+ stop = False
+ visitor.document.reporter.debug(
+ 'docutils.nodes.Node.walkabout calling dispatch_visit for %s'
+ % self.__class__.__name__)
+ try:
+ try:
+ visitor.dispatch_visit(self)
+ except SkipNode:
+ return stop
+ except SkipDeparture:
+ call_depart = False
+ children = self.children
+ try:
+ for child in children[:]:
+ if child.walkabout(visitor):
+ stop = True
+ break
+ except SkipSiblings:
+ pass
+ except SkipChildren:
+ pass
+ except StopTraversal:
+ stop = True
+ if call_depart:
+ visitor.document.reporter.debug(
+ 'docutils.nodes.Node.walkabout calling dispatch_departure '
+ 'for %s' % self.__class__.__name__)
+ visitor.dispatch_departure(self)
+ return stop
+
+ def _fast_findall(self, cls):
+ """Return iterator that only supports instance checks."""
+ if isinstance(self, cls):
+ yield self
+ for child in self.children:
+ yield from child._fast_findall(cls)
+
+ def _superfast_findall(self):
+ """Return iterator that doesn't check for a condition."""
+ # This is different from ``iter(self)`` implemented via
+ # __getitem__() and __len__() in the Element subclass,
+ # which yields only the direct children.
+ yield self
+ for child in self.children:
+ yield from child._superfast_findall()
+
+ def traverse(self, condition=None, include_self=True, descend=True,
+ siblings=False, ascend=False):
+ """Return list of nodes following `self`.
+
+ For looping, Node.findall() is faster and more memory efficient.
+ """
+ # traverse() may be eventually removed:
+ warnings.warn('nodes.Node.traverse() is obsoleted by Node.findall().',
+ PendingDeprecationWarning, stacklevel=2)
+ return list(self.findall(condition, include_self, descend,
+ siblings, ascend))
+
+ def findall(self, condition=None, include_self=True, descend=True,
+ siblings=False, ascend=False):
+ """
+ Return an iterator yielding nodes following `self`:
+
+ * self (if `include_self` is true)
+ * all descendants in tree traversal order (if `descend` is true)
+ * the following siblings (if `siblings` is true) and their
+ descendants (if also `descend` is true)
+ * the following siblings of the parent (if `ascend` is true) and
+ their descendants (if also `descend` is true), and so on.
+
+ If `condition` is not None, the iterator yields only nodes
+ for which ``condition(node)`` is true. If `condition` is a
+ node class ``cls``, it is equivalent to a function consisting
+ of ``return isinstance(node, cls)``.
+
+ If `ascend` is true, assume `siblings` to be true as well.
+
+ If the tree structure is modified during iteration, the result
+ is undefined.
+
+ For example, given the following tree::
+
+ <paragraph>
+ <emphasis> <--- emphasis.traverse() and
+ <strong> <--- strong.traverse() are called.
+ Foo
+ Bar
+ <reference name="Baz" refid="baz">
+ Baz
+
+ Then tuple(emphasis.traverse()) equals ::
+
+ (<emphasis>, <strong>, <#text: Foo>, <#text: Bar>)
+
+ and list(strong.traverse(ascend=True) equals ::
+
+ [<strong>, <#text: Foo>, <#text: Bar>, <reference>, <#text: Baz>]
+ """
+ if ascend:
+ siblings = True
+ # Check for special argument combinations that allow using an
+ # optimized version of traverse()
+ if include_self and descend and not siblings:
+ if condition is None:
+ yield from self._superfast_findall()
+ return
+ elif isinstance(condition, type):
+ yield from self._fast_findall(condition)
+ return
+ # Check if `condition` is a class (check for TypeType for Python
+ # implementations that use only new-style classes, like PyPy).
+ if isinstance(condition, type):
+ node_class = condition
+
+ def condition(node, node_class=node_class):
+ return isinstance(node, node_class)
+
+ if include_self and (condition is None or condition(self)):
+ yield self
+ if descend and len(self.children):
+ for child in self:
+ yield from child.findall(condition=condition,
+ include_self=True, descend=True,
+ siblings=False, ascend=False)
+ if siblings or ascend:
+ node = self
+ while node.parent:
+ index = node.parent.index(node)
+ # extra check since Text nodes have value-equality
+ while node.parent[index] is not node:
+ index = node.parent.index(node, index + 1)
+ for sibling in node.parent[index+1:]:
+ yield from sibling.findall(
+ condition=condition,
+ include_self=True, descend=descend,
+ siblings=False, ascend=False)
+ if not ascend:
+ break
+ else:
+ node = node.parent
+
+ def next_node(self, condition=None, include_self=False, descend=True,
+ siblings=False, ascend=False):
+ """
+ Return the first node in the iterator returned by findall(),
+ or None if the iterable is empty.
+
+ Parameter list is the same as of `findall()`. Note that `include_self`
+ defaults to False, though.
+ """
+ try:
+ return next(self.findall(condition, include_self,
+ descend, siblings, ascend))
+ except StopIteration:
+ return None
+
+
+# definition moved here from `utils` to avoid circular import dependency
+def unescape(text, restore_backslashes=False, respect_whitespace=False):
+ """
+ Return a string with nulls removed or restored to backslashes.
+ Backslash-escaped spaces are also removed.
+ """
+ # `respect_whitespace` is ignored (since introduction 2016-12-16)
+ if restore_backslashes:
+ return text.replace('\x00', '\\')
+ else:
+ for sep in ['\x00 ', '\x00\n', '\x00']:
+ text = ''.join(text.split(sep))
+ return text
+
+
+class Text(Node, str):
+
+ """
+ Instances are terminal nodes (leaves) containing text only; no child
+ nodes or attributes. Initialize by passing a string to the constructor.
+
+ Access the raw (null-escaped) text with ``str(<instance>)``
+ and unescaped text with ``<instance>.astext()``.
+ """
+
+ tagname = '#text'
+
+ children = ()
+ """Text nodes have no children, and cannot have children."""
+
+ def __new__(cls, data, rawsource=None):
+ """Assert that `data` is not an array of bytes
+ and warn if the deprecated `rawsource` argument is used.
+ """
+ if isinstance(data, bytes):
+ raise TypeError('expecting str data, not bytes')
+ if rawsource is not None:
+ warnings.warn('nodes.Text: initialization argument "rawsource" '
+ 'is ignored and will be removed in Docutils 2.0.',
+ DeprecationWarning, stacklevel=2)
+ return str.__new__(cls, data)
+
+ def shortrepr(self, maxlen=18):
+ data = self
+ if len(data) > maxlen:
+ data = data[:maxlen-4] + ' ...'
+ return '<%s: %r>' % (self.tagname, str(data))
+
+ def __repr__(self):
+ return self.shortrepr(maxlen=68)
+
+ def _dom_node(self, domroot):
+ return domroot.createTextNode(str(self))
+
+ def astext(self):
+ return str(unescape(self))
+
+ def copy(self):
+ return self.__class__(str(self))
+
+ def deepcopy(self):
+ return self.copy()
+
+ def pformat(self, indent=' ', level=0):
+ try:
+ if self.document.settings.detailed:
+ tag = '%s%s' % (indent*level, '<#text>')
+ lines = (indent*(level+1) + repr(line)
+ for line in self.splitlines(True))
+ return '\n'.join((tag, *lines)) + '\n'
+ except AttributeError:
+ pass
+ indent = indent * level
+ lines = [indent+line for line in self.astext().splitlines()]
+ if not lines:
+ return ''
+ return '\n'.join(lines) + '\n'
+
+ # rstrip and lstrip are used by substitution definitions where
+ # they are expected to return a Text instance, this was formerly
+ # taken care of by UserString.
+
+ def rstrip(self, chars=None):
+ return self.__class__(str.rstrip(self, chars))
+
+ def lstrip(self, chars=None):
+ return self.__class__(str.lstrip(self, chars))
+
+
+class Element(Node):
+
+ """
+ `Element` is the superclass to all specific elements.
+
+ Elements contain attributes and child nodes.
+ They can be described as a cross between a list and a dictionary.
+
+ Elements emulate dictionaries for external [#]_ attributes, indexing by
+ attribute name (a string). To set the attribute 'att' to 'value', do::
+
+ element['att'] = 'value'
+
+ .. [#] External attributes correspond to the XML element attributes.
+ From its `Node` superclass, Element also inherits "internal"
+ class attributes that are accessed using the standard syntax, e.g.
+ ``element.parent``.
+
+ There are two special attributes: 'ids' and 'names'. Both are
+ lists of unique identifiers: 'ids' conform to the regular expression
+ ``[a-z](-?[a-z0-9]+)*`` (see the make_id() function for rationale and
+ details). 'names' serve as user-friendly interfaces to IDs; they are
+ case- and whitespace-normalized (see the fully_normalize_name() function).
+
+ Elements emulate lists for child nodes (element nodes and/or text
+ nodes), indexing by integer. To get the first child node, use::
+
+ element[0]
+
+ to iterate over the child nodes (without descending), use::
+
+ for child in element:
+ ...
+
+ Elements may be constructed using the ``+=`` operator. To add one new
+ child node to element, do::
+
+ element += node
+
+ This is equivalent to ``element.append(node)``.
+
+ To add a list of multiple child nodes at once, use the same ``+=``
+ operator::
+
+ element += [node1, node2]
+
+ This is equivalent to ``element.extend([node1, node2])``.
+ """
+
+ basic_attributes = ('ids', 'classes', 'names', 'dupnames')
+ """Tuple of attributes which are defined for every Element-derived class
+ instance and can be safely transferred to a different node."""
+
+ local_attributes = ('backrefs',)
+ """Tuple of class-specific attributes that should not be copied with the
+ standard attributes when replacing a node.
+
+ NOTE: Derived classes should override this value to prevent any of its
+ attributes being copied by adding to the value in its parent class."""
+
+ list_attributes = basic_attributes + local_attributes
+ """Tuple of attributes that are automatically initialized to empty lists
+ for all nodes."""
+
+ known_attributes = list_attributes + ('source',)
+ """Tuple of attributes that are known to the Element base class."""
+
+ tagname = None
+ """The element generic identifier. If None, it is set as an instance
+ attribute to the name of the class."""
+
+ child_text_separator = '\n\n'
+ """Separator for child nodes, used by `astext()` method."""
+
+ def __init__(self, rawsource='', *children, **attributes):
+ self.rawsource = rawsource
+ """The raw text from which this element was constructed.
+
+ NOTE: some elements do not set this value (default '').
+ """
+
+ self.children = []
+ """List of child nodes (elements and/or `Text`)."""
+
+ self.extend(children) # maintain parent info
+
+ self.attributes = {}
+ """Dictionary of attribute {name: value}."""
+
+ # Initialize list attributes.
+ for att in self.list_attributes:
+ self.attributes[att] = []
+
+ for att, value in attributes.items():
+ att = att.lower()
+ if att in self.list_attributes:
+ # mutable list; make a copy for this node
+ self.attributes[att] = value[:]
+ else:
+ self.attributes[att] = value
+
+ if self.tagname is None:
+ self.tagname = self.__class__.__name__
+
+ def _dom_node(self, domroot):
+ element = domroot.createElement(self.tagname)
+ for attribute, value in self.attlist():
+ if isinstance(value, list):
+ value = ' '.join(serial_escape('%s' % (v,)) for v in value)
+ element.setAttribute(attribute, '%s' % value)
+ for child in self.children:
+ element.appendChild(child._dom_node(domroot))
+ return element
+
+ def __repr__(self):
+ data = ''
+ for c in self.children:
+ data += c.shortrepr()
+ if len(data) > 60:
+ data = data[:56] + ' ...'
+ break
+ if self['names']:
+ return '<%s "%s": %s>' % (self.__class__.__name__,
+ '; '.join(self['names']), data)
+ else:
+ return '<%s: %s>' % (self.__class__.__name__, data)
+
+ def shortrepr(self):
+ if self['names']:
+ return '<%s "%s"...>' % (self.__class__.__name__,
+ '; '.join(self['names']))
+ else:
+ return '<%s...>' % self.tagname
+
+ def __str__(self):
+ if self.children:
+ return '%s%s%s' % (self.starttag(),
+ ''.join(str(c) for c in self.children),
+ self.endtag())
+ else:
+ return self.emptytag()
+
+ def starttag(self, quoteattr=None):
+ # the optional arg is used by the docutils_xml writer
+ if quoteattr is None:
+ quoteattr = pseudo_quoteattr
+ parts = [self.tagname]
+ for name, value in self.attlist():
+ if value is None: # boolean attribute
+ parts.append('%s="True"' % name)
+ continue
+ if isinstance(value, list):
+ values = [serial_escape('%s' % (v,)) for v in value]
+ value = ' '.join(values)
+ else:
+ value = str(value)
+ value = quoteattr(value)
+ parts.append('%s=%s' % (name, value))
+ return '<%s>' % ' '.join(parts)
+
+ def endtag(self):
+ return '</%s>' % self.tagname
+
+ def emptytag(self):
+ attributes = ('%s="%s"' % (n, v) for n, v in self.attlist())
+ return '<%s/>' % ' '.join((self.tagname, *attributes))
+
+ def __len__(self):
+ return len(self.children)
+
+ def __contains__(self, key):
+ # Test for both, children and attributes with operator ``in``.
+ if isinstance(key, str):
+ return key in self.attributes
+ return key in self.children
+
+ def __getitem__(self, key):
+ if isinstance(key, str):
+ return self.attributes[key]
+ elif isinstance(key, int):
+ return self.children[key]
+ elif isinstance(key, slice):
+ assert key.step in (None, 1), 'cannot handle slice with stride'
+ return self.children[key.start:key.stop]
+ else:
+ raise TypeError('element index must be an integer, a slice, or '
+ 'an attribute name string')
+
+ def __setitem__(self, key, item):
+ if isinstance(key, str):
+ self.attributes[str(key)] = item
+ elif isinstance(key, int):
+ self.setup_child(item)
+ self.children[key] = item
+ elif isinstance(key, slice):
+ assert key.step in (None, 1), 'cannot handle slice with stride'
+ for node in item:
+ self.setup_child(node)
+ self.children[key.start:key.stop] = item
+ else:
+ raise TypeError('element index must be an integer, a slice, or '
+ 'an attribute name string')
+
+ def __delitem__(self, key):
+ if isinstance(key, str):
+ del self.attributes[key]
+ elif isinstance(key, int):
+ del self.children[key]
+ elif isinstance(key, slice):
+ assert key.step in (None, 1), 'cannot handle slice with stride'
+ del self.children[key.start:key.stop]
+ else:
+ raise TypeError('element index must be an integer, a simple '
+ 'slice, or an attribute name string')
+
+ def __add__(self, other):
+ return self.children + other
+
+ def __radd__(self, other):
+ return other + self.children
+
+ def __iadd__(self, other):
+ """Append a node or a list of nodes to `self.children`."""
+ if isinstance(other, Node):
+ self.append(other)
+ elif other is not None:
+ self.extend(other)
+ return self
+
+ def astext(self):
+ return self.child_text_separator.join(
+ [child.astext() for child in self.children])
+
+ def non_default_attributes(self):
+ atts = {}
+ for key, value in self.attributes.items():
+ if self.is_not_default(key):
+ atts[key] = value
+ return atts
+
+ def attlist(self):
+ return sorted(self.non_default_attributes().items())
+
+ def get(self, key, failobj=None):
+ return self.attributes.get(key, failobj)
+
+ def hasattr(self, attr):
+ return attr in self.attributes
+
+ def delattr(self, attr):
+ if attr in self.attributes:
+ del self.attributes[attr]
+
+ def setdefault(self, key, failobj=None):
+ return self.attributes.setdefault(key, failobj)
+
+ has_key = hasattr
+
+ def get_language_code(self, fallback=''):
+ """Return node's language tag.
+
+ Look iteratively in self and parents for a class argument
+ starting with ``language-`` and return the remainder of it
+ (which should be a `BCP49` language tag) or the `fallback`.
+ """
+ for cls in self.get('classes', []):
+ if cls.startswith('language-'):
+ return cls[9:]
+ try:
+ return self.parent.get_language(fallback)
+ except AttributeError:
+ return fallback
+
+ def append(self, item):
+ self.setup_child(item)
+ self.children.append(item)
+
+ def extend(self, item):
+ for node in item:
+ self.append(node)
+
+ def insert(self, index, item):
+ if isinstance(item, Node):
+ self.setup_child(item)
+ self.children.insert(index, item)
+ elif item is not None:
+ self[index:index] = item
+
+ def pop(self, i=-1):
+ return self.children.pop(i)
+
+ def remove(self, item):
+ self.children.remove(item)
+
+ def index(self, item, start=0, stop=sys.maxsize):
+ return self.children.index(item, start, stop)
+
+ def previous_sibling(self):
+ """Return preceding sibling node or ``None``."""
+ try:
+ i = self.parent.index(self)
+ except (AttributeError):
+ return None
+ return self.parent[i-1] if i > 0 else None
+
+ def is_not_default(self, key):
+ if self[key] == [] and key in self.list_attributes:
+ return 0
+ else:
+ return 1
+
+ def update_basic_atts(self, dict_):
+ """
+ Update basic attributes ('ids', 'names', 'classes',
+ 'dupnames', but not 'source') from node or dictionary `dict_`.
+ """
+ if isinstance(dict_, Node):
+ dict_ = dict_.attributes
+ for att in self.basic_attributes:
+ self.append_attr_list(att, dict_.get(att, []))
+
+ def append_attr_list(self, attr, values):
+ """
+ For each element in values, if it does not exist in self[attr], append
+ it.
+
+ NOTE: Requires self[attr] and values to be sequence type and the
+ former should specifically be a list.
+ """
+ # List Concatenation
+ for value in values:
+ if value not in self[attr]:
+ self[attr].append(value)
+
+ def coerce_append_attr_list(self, attr, value):
+ """
+ First, convert both self[attr] and value to a non-string sequence
+ type; if either is not already a sequence, convert it to a list of one
+ element. Then call append_attr_list.
+
+ NOTE: self[attr] and value both must not be None.
+ """
+ # List Concatenation
+ if not isinstance(self.get(attr), list):
+ self[attr] = [self[attr]]
+ if not isinstance(value, list):
+ value = [value]
+ self.append_attr_list(attr, value)
+
+ def replace_attr(self, attr, value, force=True):
+ """
+ If self[attr] does not exist or force is True or omitted, set
+ self[attr] to value, otherwise do nothing.
+ """
+ # One or the other
+ if force or self.get(attr) is None:
+ self[attr] = value
+
+ def copy_attr_convert(self, attr, value, replace=True):
+ """
+ If attr is an attribute of self, set self[attr] to
+ [self[attr], value], otherwise set self[attr] to value.
+
+ NOTE: replace is not used by this function and is kept only for
+ compatibility with the other copy functions.
+ """
+ if self.get(attr) is not value:
+ self.coerce_append_attr_list(attr, value)
+
+ def copy_attr_coerce(self, attr, value, replace):
+ """
+ If attr is an attribute of self and either self[attr] or value is a
+ list, convert all non-sequence values to a sequence of 1 element and
+ then concatenate the two sequence, setting the result to self[attr].
+ If both self[attr] and value are non-sequences and replace is True or
+ self[attr] is None, replace self[attr] with value. Otherwise, do
+ nothing.
+ """
+ if self.get(attr) is not value:
+ if isinstance(self.get(attr), list) or \
+ isinstance(value, list):
+ self.coerce_append_attr_list(attr, value)
+ else:
+ self.replace_attr(attr, value, replace)
+
+ def copy_attr_concatenate(self, attr, value, replace):
+ """
+ If attr is an attribute of self and both self[attr] and value are
+ lists, concatenate the two sequences, setting the result to
+ self[attr]. If either self[attr] or value are non-sequences and
+ replace is True or self[attr] is None, replace self[attr] with value.
+ Otherwise, do nothing.
+ """
+ if self.get(attr) is not value:
+ if isinstance(self.get(attr), list) and \
+ isinstance(value, list):
+ self.append_attr_list(attr, value)
+ else:
+ self.replace_attr(attr, value, replace)
+
+ def copy_attr_consistent(self, attr, value, replace):
+ """
+ If replace is True or self[attr] is None, replace self[attr] with
+ value. Otherwise, do nothing.
+ """
+ if self.get(attr) is not value:
+ self.replace_attr(attr, value, replace)
+
+ def update_all_atts(self, dict_, update_fun=copy_attr_consistent,
+ replace=True, and_source=False):
+ """
+ Updates all attributes from node or dictionary `dict_`.
+
+ Appends the basic attributes ('ids', 'names', 'classes',
+ 'dupnames', but not 'source') and then, for all other attributes in
+ dict_, updates the same attribute in self. When attributes with the
+ same identifier appear in both self and dict_, the two values are
+ merged based on the value of update_fun. Generally, when replace is
+ True, the values in self are replaced or merged with the values in
+ dict_; otherwise, the values in self may be preserved or merged. When
+ and_source is True, the 'source' attribute is included in the copy.
+
+ NOTE: When replace is False, and self contains a 'source' attribute,
+ 'source' is not replaced even when dict_ has a 'source'
+ attribute, though it may still be merged into a list depending
+ on the value of update_fun.
+ NOTE: It is easier to call the update-specific methods then to pass
+ the update_fun method to this function.
+ """
+ if isinstance(dict_, Node):
+ dict_ = dict_.attributes
+
+ # Include the source attribute when copying?
+ if and_source:
+ filter_fun = self.is_not_list_attribute
+ else:
+ filter_fun = self.is_not_known_attribute
+
+ # Copy the basic attributes
+ self.update_basic_atts(dict_)
+
+ # Grab other attributes in dict_ not in self except the
+ # (All basic attributes should be copied already)
+ for att in filter(filter_fun, dict_):
+ update_fun(self, att, dict_[att], replace)
+
+ def update_all_atts_consistantly(self, dict_, replace=True,
+ and_source=False):
+ """
+ Updates all attributes from node or dictionary `dict_`.
+
+ Appends the basic attributes ('ids', 'names', 'classes',
+ 'dupnames', but not 'source') and then, for all other attributes in
+ dict_, updates the same attribute in self. When attributes with the
+ same identifier appear in both self and dict_ and replace is True, the
+ values in self are replaced with the values in dict_; otherwise, the
+ values in self are preserved. When and_source is True, the 'source'
+ attribute is included in the copy.
+
+ NOTE: When replace is False, and self contains a 'source' attribute,
+ 'source' is not replaced even when dict_ has a 'source'
+ attribute, though it may still be merged into a list depending
+ on the value of update_fun.
+ """
+ self.update_all_atts(dict_, Element.copy_attr_consistent, replace,
+ and_source)
+
+ def update_all_atts_concatenating(self, dict_, replace=True,
+ and_source=False):
+ """
+ Updates all attributes from node or dictionary `dict_`.
+
+ Appends the basic attributes ('ids', 'names', 'classes',
+ 'dupnames', but not 'source') and then, for all other attributes in
+ dict_, updates the same attribute in self. When attributes with the
+ same identifier appear in both self and dict_ whose values aren't each
+ lists and replace is True, the values in self are replaced with the
+ values in dict_; if the values from self and dict_ for the given
+ identifier are both of list type, then the two lists are concatenated
+ and the result stored in self; otherwise, the values in self are
+ preserved. When and_source is True, the 'source' attribute is
+ included in the copy.
+
+ NOTE: When replace is False, and self contains a 'source' attribute,
+ 'source' is not replaced even when dict_ has a 'source'
+ attribute, though it may still be merged into a list depending
+ on the value of update_fun.
+ """
+ self.update_all_atts(dict_, Element.copy_attr_concatenate, replace,
+ and_source)
+
+ def update_all_atts_coercion(self, dict_, replace=True,
+ and_source=False):
+ """
+ Updates all attributes from node or dictionary `dict_`.
+
+ Appends the basic attributes ('ids', 'names', 'classes',
+ 'dupnames', but not 'source') and then, for all other attributes in
+ dict_, updates the same attribute in self. When attributes with the
+ same identifier appear in both self and dict_ whose values are both
+ not lists and replace is True, the values in self are replaced with
+ the values in dict_; if either of the values from self and dict_ for
+ the given identifier are of list type, then first any non-lists are
+ converted to 1-element lists and then the two lists are concatenated
+ and the result stored in self; otherwise, the values in self are
+ preserved. When and_source is True, the 'source' attribute is
+ included in the copy.
+
+ NOTE: When replace is False, and self contains a 'source' attribute,
+ 'source' is not replaced even when dict_ has a 'source'
+ attribute, though it may still be merged into a list depending
+ on the value of update_fun.
+ """
+ self.update_all_atts(dict_, Element.copy_attr_coerce, replace,
+ and_source)
+
+ def update_all_atts_convert(self, dict_, and_source=False):
+ """
+ Updates all attributes from node or dictionary `dict_`.
+
+ Appends the basic attributes ('ids', 'names', 'classes',
+ 'dupnames', but not 'source') and then, for all other attributes in
+ dict_, updates the same attribute in self. When attributes with the
+ same identifier appear in both self and dict_ then first any non-lists
+ are converted to 1-element lists and then the two lists are
+ concatenated and the result stored in self; otherwise, the values in
+ self are preserved. When and_source is True, the 'source' attribute
+ is included in the copy.
+
+ NOTE: When replace is False, and self contains a 'source' attribute,
+ 'source' is not replaced even when dict_ has a 'source'
+ attribute, though it may still be merged into a list depending
+ on the value of update_fun.
+ """
+ self.update_all_atts(dict_, Element.copy_attr_convert,
+ and_source=and_source)
+
+ def clear(self):
+ self.children = []
+
+ def replace(self, old, new):
+ """Replace one child `Node` with another child or children."""
+ index = self.index(old)
+ if isinstance(new, Node):
+ self.setup_child(new)
+ self[index] = new
+ elif new is not None:
+ self[index:index+1] = new
+
+ def replace_self(self, new):
+ """
+ Replace `self` node with `new`, where `new` is a node or a
+ list of nodes.
+ """
+ update = new
+ if not isinstance(new, Node):
+ # `new` is a list; update first child.
+ try:
+ update = new[0]
+ except IndexError:
+ update = None
+ if isinstance(update, Element):
+ update.update_basic_atts(self)
+ else:
+ # `update` is a Text node or `new` is an empty list.
+ # Assert that we aren't losing any attributes.
+ for att in self.basic_attributes:
+ assert not self[att], \
+ 'Losing "%s" attribute: %s' % (att, self[att])
+ self.parent.replace(self, new)
+
+ def first_child_matching_class(self, childclass, start=0, end=sys.maxsize):
+ """
+ Return the index of the first child whose class exactly matches.
+
+ Parameters:
+
+ - `childclass`: A `Node` subclass to search for, or a tuple of `Node`
+ classes. If a tuple, any of the classes may match.
+ - `start`: Initial index to check.
+ - `end`: Initial index to *not* check.
+ """
+ if not isinstance(childclass, tuple):
+ childclass = (childclass,)
+ for index in range(start, min(len(self), end)):
+ for c in childclass:
+ if isinstance(self[index], c):
+ return index
+ return None
+
+ def first_child_not_matching_class(self, childclass, start=0,
+ end=sys.maxsize):
+ """
+ Return the index of the first child whose class does *not* match.
+
+ Parameters:
+
+ - `childclass`: A `Node` subclass to skip, or a tuple of `Node`
+ classes. If a tuple, none of the classes may match.
+ - `start`: Initial index to check.
+ - `end`: Initial index to *not* check.
+ """
+ if not isinstance(childclass, tuple):
+ childclass = (childclass,)
+ for index in range(start, min(len(self), end)):
+ for c in childclass:
+ if isinstance(self.children[index], c):
+ break
+ else:
+ return index
+ return None
+
+ def pformat(self, indent=' ', level=0):
+ tagline = '%s%s\n' % (indent*level, self.starttag())
+ childreps = (c.pformat(indent, level+1) for c in self.children)
+ return ''.join((tagline, *childreps))
+
+ def copy(self):
+ obj = self.__class__(rawsource=self.rawsource, **self.attributes)
+ obj._document = self._document
+ obj.source = self.source
+ obj.line = self.line
+ return obj
+
+ def deepcopy(self):
+ copy = self.copy()
+ copy.extend([child.deepcopy() for child in self.children])
+ return copy
+
+ def set_class(self, name):
+ """Add a new class to the "classes" attribute."""
+ warnings.warn('docutils.nodes.Element.set_class() is deprecated; '
+ ' and will be removed in Docutils 0.21 or later.'
+ "Append to Element['classes'] list attribute directly",
+ DeprecationWarning, stacklevel=2)
+ assert ' ' not in name
+ self['classes'].append(name.lower())
+
+ def note_referenced_by(self, name=None, id=None):
+ """Note that this Element has been referenced by its name
+ `name` or id `id`."""
+ self.referenced = True
+ # Element.expect_referenced_by_* dictionaries map names or ids
+ # to nodes whose ``referenced`` attribute is set to true as
+ # soon as this node is referenced by the given name or id.
+ # Needed for target propagation.
+ by_name = getattr(self, 'expect_referenced_by_name', {}).get(name)
+ by_id = getattr(self, 'expect_referenced_by_id', {}).get(id)
+ if by_name:
+ assert name is not None
+ by_name.referenced = True
+ if by_id:
+ assert id is not None
+ by_id.referenced = True
+
+ @classmethod
+ def is_not_list_attribute(cls, attr):
+ """
+ Returns True if and only if the given attribute is NOT one of the
+ basic list attributes defined for all Elements.
+ """
+ return attr not in cls.list_attributes
+
+ @classmethod
+ def is_not_known_attribute(cls, attr):
+ """
+ Returns True if and only if the given attribute is NOT recognized by
+ this class.
+ """
+ return attr not in cls.known_attributes
+
+
+class TextElement(Element):
+
+ """
+ An element which directly contains text.
+
+ Its children are all `Text` or `Inline` subclass nodes. You can
+ check whether an element's context is inline simply by checking whether
+ its immediate parent is a `TextElement` instance (including subclasses).
+ This is handy for nodes like `image` that can appear both inline and as
+ standalone body elements.
+
+ If passing children to `__init__()`, make sure to set `text` to
+ ``''`` or some other suitable value.
+ """
+
+ child_text_separator = ''
+ """Separator for child nodes, used by `astext()` method."""
+
+ def __init__(self, rawsource='', text='', *children, **attributes):
+ if text != '':
+ textnode = Text(text)
+ Element.__init__(self, rawsource, textnode, *children,
+ **attributes)
+ else:
+ Element.__init__(self, rawsource, *children, **attributes)
+
+
+class FixedTextElement(TextElement):
+
+ """An element which directly contains preformatted text."""
+
+ def __init__(self, rawsource='', text='', *children, **attributes):
+ super().__init__(rawsource, text, *children, **attributes)
+ self.attributes['xml:space'] = 'preserve'
+
+
+# TODO: PureTextElement(TextElement):
+# """An element which only contains text, no children."""
+# For elements in the DTD that directly employ #PCDATA in their definition:
+# citation_reference, comment, footnote_reference, label, math, math_block,
+# option_argument, option_string, raw,
+
+
+# ========
+# Mixins
+# ========
+
+class Resolvable:
+
+ resolved = 0
+
+
+class BackLinkable:
+
+ def add_backref(self, refid):
+ self['backrefs'].append(refid)
+
+
+# ====================
+# Element Categories
+# ====================
+
+class Root:
+ pass
+
+
+class Titular:
+ pass
+
+
+class PreBibliographic:
+ """Category of Node which may occur before Bibliographic Nodes."""
+
+
+class Bibliographic:
+ pass
+
+
+class Decorative(PreBibliographic):
+ pass
+
+
+class Structural:
+ pass
+
+
+class Body:
+ pass
+
+
+class General(Body):
+ pass
+
+
+class Sequential(Body):
+ """List-like elements."""
+
+
+class Admonition(Body): pass
+
+
+class Special(Body):
+ """Special internal body elements."""
+
+
+class Invisible(PreBibliographic):
+ """Internal elements that don't appear in output."""
+
+
+class Part:
+ pass
+
+
+class Inline:
+ pass
+
+
+class Referential(Resolvable):
+ pass
+
+
+class Targetable(Resolvable):
+
+ referenced = 0
+
+ indirect_reference_name = None
+ """Holds the whitespace_normalized_name (contains mixed case) of a target.
+ Required for MoinMoin/reST compatibility."""
+
+
+class Labeled:
+ """Contains a `label` as its first element."""
+
+
+# ==============
+# Root Element
+# ==============
+
+class document(Root, Structural, Element):
+
+ """
+ The document root element.
+
+ Do not instantiate this class directly; use
+ `docutils.utils.new_document()` instead.
+ """
+
+ def __init__(self, settings, reporter, *args, **kwargs):
+ Element.__init__(self, *args, **kwargs)
+
+ self.current_source = None
+ """Path to or description of the input source being processed."""
+
+ self.current_line = None
+ """Line number (1-based) of `current_source`."""
+
+ self.settings = settings
+ """Runtime settings data record."""
+
+ self.reporter = reporter
+ """System message generator."""
+
+ self.indirect_targets = []
+ """List of indirect target nodes."""
+
+ self.substitution_defs = {}
+ """Mapping of substitution names to substitution_definition nodes."""
+
+ self.substitution_names = {}
+ """Mapping of case-normalized substitution names to case-sensitive
+ names."""
+
+ self.refnames = {}
+ """Mapping of names to lists of referencing nodes."""
+
+ self.refids = {}
+ """Mapping of ids to lists of referencing nodes."""
+
+ self.nameids = {}
+ """Mapping of names to unique id's."""
+
+ self.nametypes = {}
+ """Mapping of names to hyperlink type (boolean: True => explicit,
+ False => implicit."""
+
+ self.ids = {}
+ """Mapping of ids to nodes."""
+
+ self.footnote_refs = {}
+ """Mapping of footnote labels to lists of footnote_reference nodes."""
+
+ self.citation_refs = {}
+ """Mapping of citation labels to lists of citation_reference nodes."""
+
+ self.autofootnotes = []
+ """List of auto-numbered footnote nodes."""
+
+ self.autofootnote_refs = []
+ """List of auto-numbered footnote_reference nodes."""
+
+ self.symbol_footnotes = []
+ """List of symbol footnote nodes."""
+
+ self.symbol_footnote_refs = []
+ """List of symbol footnote_reference nodes."""
+
+ self.footnotes = []
+ """List of manually-numbered footnote nodes."""
+
+ self.citations = []
+ """List of citation nodes."""
+
+ self.autofootnote_start = 1
+ """Initial auto-numbered footnote number."""
+
+ self.symbol_footnote_start = 0
+ """Initial symbol footnote symbol index."""
+
+ self.id_counter = Counter()
+ """Numbers added to otherwise identical IDs."""
+
+ self.parse_messages = []
+ """System messages generated while parsing."""
+
+ self.transform_messages = []
+ """System messages generated while applying transforms."""
+
+ import docutils.transforms
+ self.transformer = docutils.transforms.Transformer(self)
+ """Storage for transforms to be applied to this document."""
+
+ self.include_log = []
+ """The current source's parents (to detect inclusion loops)."""
+
+ self.decoration = None
+ """Document's `decoration` node."""
+
+ self._document = self
+
+ def __getstate__(self):
+ """
+ Return dict with unpicklable references removed.
+ """
+ state = self.__dict__.copy()
+ state['reporter'] = None
+ state['transformer'] = None
+ return state
+
+ def asdom(self, dom=None):
+ """Return a DOM representation of this document."""
+ if dom is None:
+ import xml.dom.minidom as dom
+ domroot = dom.Document()
+ domroot.appendChild(self._dom_node(domroot))
+ return domroot
+
+ def set_id(self, node, msgnode=None, suggested_prefix=''):
+ if node['ids']:
+ # register and check for duplicates
+ for id in node['ids']:
+ self.ids.setdefault(id, node)
+ if self.ids[id] is not node:
+ msg = self.reporter.severe('Duplicate ID: "%s".' % id)
+ if msgnode is not None:
+ msgnode += msg
+ return id
+ # generate and set id
+ id_prefix = self.settings.id_prefix
+ auto_id_prefix = self.settings.auto_id_prefix
+ base_id = ''
+ id = ''
+ for name in node['names']:
+ if id_prefix:
+ # allow names starting with numbers if `id_prefix`
+ base_id = make_id('x'+name)[1:]
+ else:
+ base_id = make_id(name)
+ # TODO: normalize id-prefix? (would make code simpler)
+ id = id_prefix + base_id
+ if base_id and id not in self.ids:
+ break
+ else:
+ if base_id and auto_id_prefix.endswith('%'):
+ # disambiguate name-derived ID
+ # TODO: remove second condition after announcing change
+ prefix = id + '-'
+ else:
+ prefix = id_prefix + auto_id_prefix
+ if prefix.endswith('%'):
+ prefix = '%s%s-' % (prefix[:-1],
+ suggested_prefix
+ or make_id(node.tagname))
+ while True:
+ self.id_counter[prefix] += 1
+ id = '%s%d' % (prefix, self.id_counter[prefix])
+ if id not in self.ids:
+ break
+ node['ids'].append(id)
+ self.ids[id] = node
+ return id
+
+ def set_name_id_map(self, node, id, msgnode=None, explicit=None):
+ """
+ `self.nameids` maps names to IDs, while `self.nametypes` maps names to
+ booleans representing hyperlink type (True==explicit,
+ False==implicit). This method updates the mappings.
+
+ The following state transition table shows how `self.nameids` items
+ ("id") and `self.nametypes` items ("type") change with new input
+ (a call to this method), and what actions are performed
+ ("implicit"-type system messages are INFO/1, and
+ "explicit"-type system messages are ERROR/3):
+
+ ==== ===== ======== ======== ======= ==== ===== =====
+ Old State Input Action New State Notes
+ ----------- -------- ----------------- ----------- -----
+ id type new type sys.msg. dupname id type
+ ==== ===== ======== ======== ======= ==== ===== =====
+ - - explicit - - new True
+ - - implicit - - new False
+ - False explicit - - new True
+ old False explicit implicit old new True
+ - True explicit explicit new - True
+ old True explicit explicit new,old - True [#]_
+ - False implicit implicit new - False
+ old False implicit implicit new,old - False
+ - True implicit implicit new - True
+ old True implicit implicit new old True
+ ==== ===== ======== ======== ======= ==== ===== =====
+
+ .. [#] Do not clear the name-to-id map or invalidate the old target if
+ both old and new targets are external and refer to identical URIs.
+ The new target is invalidated regardless.
+ """
+ for name in tuple(node['names']):
+ if name in self.nameids:
+ self.set_duplicate_name_id(node, id, name, msgnode, explicit)
+ # attention: modifies node['names']
+ else:
+ self.nameids[name] = id
+ self.nametypes[name] = explicit
+
+ def set_duplicate_name_id(self, node, id, name, msgnode, explicit):
+ old_id = self.nameids[name]
+ old_explicit = self.nametypes[name]
+ self.nametypes[name] = old_explicit or explicit
+ if explicit:
+ if old_explicit:
+ level = 2
+ if old_id is not None:
+ old_node = self.ids[old_id]
+ if 'refuri' in node:
+ refuri = node['refuri']
+ if (old_node['names']
+ and 'refuri' in old_node
+ and old_node['refuri'] == refuri):
+ level = 1 # just inform if refuri's identical
+ if level > 1:
+ dupname(old_node, name)
+ self.nameids[name] = None
+ msg = self.reporter.system_message(
+ level, 'Duplicate explicit target name: "%s".' % name,
+ backrefs=[id], base_node=node)
+ if msgnode is not None:
+ msgnode += msg
+ dupname(node, name)
+ else:
+ self.nameids[name] = id
+ if old_id is not None:
+ old_node = self.ids[old_id]
+ dupname(old_node, name)
+ else:
+ if old_id is not None and not old_explicit:
+ self.nameids[name] = None
+ old_node = self.ids[old_id]
+ dupname(old_node, name)
+ dupname(node, name)
+ if not explicit or (not old_explicit and old_id is not None):
+ msg = self.reporter.info(
+ 'Duplicate implicit target name: "%s".' % name,
+ backrefs=[id], base_node=node)
+ if msgnode is not None:
+ msgnode += msg
+
+ def has_name(self, name):
+ return name in self.nameids
+
+ # "note" here is an imperative verb: "take note of".
+ def note_implicit_target(self, target, msgnode=None):
+ id = self.set_id(target, msgnode)
+ self.set_name_id_map(target, id, msgnode, explicit=False)
+
+ def note_explicit_target(self, target, msgnode=None):
+ id = self.set_id(target, msgnode)
+ self.set_name_id_map(target, id, msgnode, explicit=True)
+
+ def note_refname(self, node):
+ self.refnames.setdefault(node['refname'], []).append(node)
+
+ def note_refid(self, node):
+ self.refids.setdefault(node['refid'], []).append(node)
+
+ def note_indirect_target(self, target):
+ self.indirect_targets.append(target)
+ if target['names']:
+ self.note_refname(target)
+
+ def note_anonymous_target(self, target):
+ self.set_id(target)
+
+ def note_autofootnote(self, footnote):
+ self.set_id(footnote)
+ self.autofootnotes.append(footnote)
+
+ def note_autofootnote_ref(self, ref):
+ self.set_id(ref)
+ self.autofootnote_refs.append(ref)
+
+ def note_symbol_footnote(self, footnote):
+ self.set_id(footnote)
+ self.symbol_footnotes.append(footnote)
+
+ def note_symbol_footnote_ref(self, ref):
+ self.set_id(ref)
+ self.symbol_footnote_refs.append(ref)
+
+ def note_footnote(self, footnote):
+ self.set_id(footnote)
+ self.footnotes.append(footnote)
+
+ def note_footnote_ref(self, ref):
+ self.set_id(ref)
+ self.footnote_refs.setdefault(ref['refname'], []).append(ref)
+ self.note_refname(ref)
+
+ def note_citation(self, citation):
+ self.citations.append(citation)
+
+ def note_citation_ref(self, ref):
+ self.set_id(ref)
+ self.citation_refs.setdefault(ref['refname'], []).append(ref)
+ self.note_refname(ref)
+
+ def note_substitution_def(self, subdef, def_name, msgnode=None):
+ name = whitespace_normalize_name(def_name)
+ if name in self.substitution_defs:
+ msg = self.reporter.error(
+ 'Duplicate substitution definition name: "%s".' % name,
+ base_node=subdef)
+ if msgnode is not None:
+ msgnode += msg
+ oldnode = self.substitution_defs[name]
+ dupname(oldnode, name)
+ # keep only the last definition:
+ self.substitution_defs[name] = subdef
+ # case-insensitive mapping:
+ self.substitution_names[fully_normalize_name(name)] = name
+
+ def note_substitution_ref(self, subref, refname):
+ subref['refname'] = whitespace_normalize_name(refname)
+
+ def note_pending(self, pending, priority=None):
+ self.transformer.add_pending(pending, priority)
+
+ def note_parse_message(self, message):
+ self.parse_messages.append(message)
+
+ def note_transform_message(self, message):
+ self.transform_messages.append(message)
+
+ def note_source(self, source, offset):
+ self.current_source = source
+ if offset is None:
+ self.current_line = offset
+ else:
+ self.current_line = offset + 1
+
+ def copy(self):
+ obj = self.__class__(self.settings, self.reporter,
+ **self.attributes)
+ obj.source = self.source
+ obj.line = self.line
+ return obj
+
+ def get_decoration(self):
+ if not self.decoration:
+ self.decoration = decoration()
+ index = self.first_child_not_matching_class((Titular, meta))
+ if index is None:
+ self.append(self.decoration)
+ else:
+ self.insert(index, self.decoration)
+ return self.decoration
+
+
+# ================
+# Title Elements
+# ================
+
+class title(Titular, PreBibliographic, TextElement): pass
+class subtitle(Titular, PreBibliographic, TextElement): pass
+class rubric(Titular, TextElement): pass
+
+
+# ==================
+# Meta-Data Element
+# ==================
+
+class meta(PreBibliographic, Element):
+ """Container for "invisible" bibliographic data, or meta-data."""
+
+
+# ========================
+# Bibliographic Elements
+# ========================
+
+class docinfo(Bibliographic, Element): pass
+class author(Bibliographic, TextElement): pass
+class authors(Bibliographic, Element): pass
+class organization(Bibliographic, TextElement): pass
+class address(Bibliographic, FixedTextElement): pass
+class contact(Bibliographic, TextElement): pass
+class version(Bibliographic, TextElement): pass
+class revision(Bibliographic, TextElement): pass
+class status(Bibliographic, TextElement): pass
+class date(Bibliographic, TextElement): pass
+class copyright(Bibliographic, TextElement): pass
+
+
+# =====================
+# Decorative Elements
+# =====================
+
+class decoration(Decorative, Element):
+
+ def get_header(self):
+ if not len(self.children) or not isinstance(self.children[0], header):
+ self.insert(0, header())
+ return self.children[0]
+
+ def get_footer(self):
+ if not len(self.children) or not isinstance(self.children[-1], footer):
+ self.append(footer())
+ return self.children[-1]
+
+
+class header(Decorative, Element): pass
+class footer(Decorative, Element): pass
+
+
+# =====================
+# Structural Elements
+# =====================
+
+class section(Structural, Element): pass
+
+
+class topic(Structural, Element):
+
+ """
+ Topics are terminal, "leaf" mini-sections, like block quotes with titles,
+ or textual figures. A topic is just like a section, except that it has no
+ subsections, and it doesn't have to conform to section placement rules.
+
+ Topics are allowed wherever body elements (list, table, etc.) are allowed,
+ but only at the top level of a section or document. Topics cannot nest
+ inside topics, sidebars, or body elements; you can't have a topic inside a
+ table, list, block quote, etc.
+ """
+
+
+class sidebar(Structural, Element):
+
+ """
+ Sidebars are like miniature, parallel documents that occur inside other
+ documents, providing related or reference material. A sidebar is
+ typically offset by a border and "floats" to the side of the page; the
+ document's main text may flow around it. Sidebars can also be likened to
+ super-footnotes; their content is outside of the flow of the document's
+ main text.
+
+ Sidebars are allowed wherever body elements (list, table, etc.) are
+ allowed, but only at the top level of a section or document. Sidebars
+ cannot nest inside sidebars, topics, or body elements; you can't have a
+ sidebar inside a table, list, block quote, etc.
+ """
+
+
+class transition(Structural, Element): pass
+
+
+# ===============
+# Body Elements
+# ===============
+
+class paragraph(General, TextElement): pass
+class compound(General, Element): pass
+class container(General, Element): pass
+class bullet_list(Sequential, Element): pass
+class enumerated_list(Sequential, Element): pass
+class list_item(Part, Element): pass
+class definition_list(Sequential, Element): pass
+class definition_list_item(Part, Element): pass
+class term(Part, TextElement): pass
+class classifier(Part, TextElement): pass
+class definition(Part, Element): pass
+class field_list(Sequential, Element): pass
+class field(Part, Element): pass
+class field_name(Part, TextElement): pass
+class field_body(Part, Element): pass
+
+
+class option(Part, Element):
+
+ child_text_separator = ''
+
+
+class option_argument(Part, TextElement):
+
+ def astext(self):
+ return self.get('delimiter', ' ') + TextElement.astext(self)
+
+
+class option_group(Part, Element):
+
+ child_text_separator = ', '
+
+
+class option_list(Sequential, Element): pass
+
+
+class option_list_item(Part, Element):
+
+ child_text_separator = ' '
+
+
+class option_string(Part, TextElement): pass
+class description(Part, Element): pass
+class literal_block(General, FixedTextElement): pass
+class doctest_block(General, FixedTextElement): pass
+class math_block(General, FixedTextElement): pass
+class line_block(General, Element): pass
+
+
+class line(Part, TextElement):
+
+ indent = None
+
+
+class block_quote(General, Element): pass
+class attribution(Part, TextElement): pass
+class attention(Admonition, Element): pass
+class caution(Admonition, Element): pass
+class danger(Admonition, Element): pass
+class error(Admonition, Element): pass
+class important(Admonition, Element): pass
+class note(Admonition, Element): pass
+class tip(Admonition, Element): pass
+class hint(Admonition, Element): pass
+class warning(Admonition, Element): pass
+class admonition(Admonition, Element): pass
+class comment(Special, Invisible, FixedTextElement): pass
+class substitution_definition(Special, Invisible, TextElement): pass
+class target(Special, Invisible, Inline, TextElement, Targetable): pass
+class footnote(General, BackLinkable, Element, Labeled, Targetable): pass
+class citation(General, BackLinkable, Element, Labeled, Targetable): pass
+class label(Part, TextElement): pass
+class figure(General, Element): pass
+class caption(Part, TextElement): pass
+class legend(Part, Element): pass
+class table(General, Element): pass
+class tgroup(Part, Element): pass
+class colspec(Part, Element): pass
+class thead(Part, Element): pass
+class tbody(Part, Element): pass
+class row(Part, Element): pass
+class entry(Part, Element): pass
+
+
+class system_message(Special, BackLinkable, PreBibliographic, Element):
+
+ """
+ System message element.
+
+ Do not instantiate this class directly; use
+ ``document.reporter.info/warning/error/severe()`` instead.
+ """
+
+ def __init__(self, message=None, *children, **attributes):
+ rawsource = attributes.pop('rawsource', '')
+ if message:
+ p = paragraph('', message)
+ children = (p,) + children
+ try:
+ Element.__init__(self, rawsource, *children, **attributes)
+ except: # noqa catchall
+ print('system_message: children=%r' % (children,))
+ raise
+
+ def astext(self):
+ line = self.get('line', '')
+ return '%s:%s: (%s/%s) %s' % (self['source'], line, self['type'],
+ self['level'], Element.astext(self))
+
+
+class pending(Special, Invisible, Element):
+
+ """
+ The "pending" element is used to encapsulate a pending operation: the
+ operation (transform), the point at which to apply it, and any data it
+ requires. Only the pending operation's location within the document is
+ stored in the public document tree (by the "pending" object itself); the
+ operation and its data are stored in the "pending" object's internal
+ instance attributes.
+
+ For example, say you want a table of contents in your reStructuredText
+ document. The easiest way to specify where to put it is from within the
+ document, with a directive::
+
+ .. contents::
+
+ But the "contents" directive can't do its work until the entire document
+ has been parsed and possibly transformed to some extent. So the directive
+ code leaves a placeholder behind that will trigger the second phase of its
+ processing, something like this::
+
+ <pending ...public attributes...> + internal attributes
+
+ Use `document.note_pending()` so that the
+ `docutils.transforms.Transformer` stage of processing can run all pending
+ transforms.
+ """
+
+ def __init__(self, transform, details=None,
+ rawsource='', *children, **attributes):
+ Element.__init__(self, rawsource, *children, **attributes)
+
+ self.transform = transform
+ """The `docutils.transforms.Transform` class implementing the pending
+ operation."""
+
+ self.details = details or {}
+ """Detail data (dictionary) required by the pending operation."""
+
+ def pformat(self, indent=' ', level=0):
+ internals = ['.. internal attributes:',
+ ' .transform: %s.%s' % (self.transform.__module__,
+ self.transform.__name__),
+ ' .details:']
+ details = sorted(self.details.items())
+ for key, value in details:
+ if isinstance(value, Node):
+ internals.append('%7s%s:' % ('', key))
+ internals.extend(['%9s%s' % ('', line)
+ for line in value.pformat().splitlines()])
+ elif (value
+ and isinstance(value, list)
+ and isinstance(value[0], Node)):
+ internals.append('%7s%s:' % ('', key))
+ for v in value:
+ internals.extend(['%9s%s' % ('', line)
+ for line in v.pformat().splitlines()])
+ else:
+ internals.append('%7s%s: %r' % ('', key, value))
+ return (Element.pformat(self, indent, level)
+ + ''.join((' %s%s\n' % (indent * level, line))
+ for line in internals))
+
+ def copy(self):
+ obj = self.__class__(self.transform, self.details, self.rawsource,
+ **self.attributes)
+ obj._document = self._document
+ obj.source = self.source
+ obj.line = self.line
+ return obj
+
+
+class raw(Special, Inline, PreBibliographic, FixedTextElement):
+
+ """
+ Raw data that is to be passed untouched to the Writer.
+ """
+
+
+# =================
+# Inline Elements
+# =================
+
+class emphasis(Inline, TextElement): pass
+class strong(Inline, TextElement): pass
+class literal(Inline, TextElement): pass
+class reference(General, Inline, Referential, TextElement): pass
+class footnote_reference(Inline, Referential, TextElement): pass
+class citation_reference(Inline, Referential, TextElement): pass
+class substitution_reference(Inline, TextElement): pass
+class title_reference(Inline, TextElement): pass
+class abbreviation(Inline, TextElement): pass
+class acronym(Inline, TextElement): pass
+class superscript(Inline, TextElement): pass
+class subscript(Inline, TextElement): pass
+class math(Inline, TextElement): pass
+
+
+class image(General, Inline, Element):
+
+ def astext(self):
+ return self.get('alt', '')
+
+
+class inline(Inline, TextElement): pass
+class problematic(Inline, TextElement): pass
+class generated(Inline, TextElement): pass
+
+
+# ========================================
+# Auxiliary Classes, Functions, and Data
+# ========================================
+
+node_class_names = """
+ Text
+ abbreviation acronym address admonition attention attribution author
+ authors
+ block_quote bullet_list
+ caption caution citation citation_reference classifier colspec comment
+ compound contact container copyright
+ danger date decoration definition definition_list definition_list_item
+ description docinfo doctest_block document
+ emphasis entry enumerated_list error
+ field field_body field_list field_name figure footer
+ footnote footnote_reference
+ generated
+ header hint
+ image important inline
+ label legend line line_block list_item literal literal_block
+ math math_block meta
+ note
+ option option_argument option_group option_list option_list_item
+ option_string organization
+ paragraph pending problematic
+ raw reference revision row rubric
+ section sidebar status strong subscript substitution_definition
+ substitution_reference subtitle superscript system_message
+ table target tbody term tgroup thead tip title title_reference topic
+ transition
+ version
+ warning""".split()
+"""A list of names of all concrete Node subclasses."""
+
+
+class NodeVisitor:
+
+ """
+ "Visitor" pattern [GoF95]_ abstract superclass implementation for
+ document tree traversals.
+
+ Each node class has corresponding methods, doing nothing by
+ default; override individual methods for specific and useful
+ behaviour. The `dispatch_visit()` method is called by
+ `Node.walk()` upon entering a node. `Node.walkabout()` also calls
+ the `dispatch_departure()` method before exiting a node.
+
+ The dispatch methods call "``visit_`` + node class name" or
+ "``depart_`` + node class name", resp.
+
+ This is a base class for visitors whose ``visit_...`` & ``depart_...``
+ methods must be implemented for *all* compulsory node types encountered
+ (such as for `docutils.writers.Writer` subclasses).
+ Unimplemented methods will raise exceptions (except for optional nodes).
+
+ For sparse traversals, where only certain node types are of interest, use
+ subclass `SparseNodeVisitor` instead. When (mostly or entirely) uniform
+ processing is desired, subclass `GenericNodeVisitor`.
+
+ .. [GoF95] Gamma, Helm, Johnson, Vlissides. *Design Patterns: Elements of
+ Reusable Object-Oriented Software*. Addison-Wesley, Reading, MA, USA,
+ 1995.
+ """
+
+ optional = ('meta',)
+ """
+ Tuple containing node class names (as strings).
+
+ No exception will be raised if writers do not implement visit
+ or departure functions for these node classes.
+
+ Used to ensure transitional compatibility with existing 3rd-party writers.
+ """
+
+ def __init__(self, document):
+ self.document = document
+
+ def dispatch_visit(self, node):
+ """
+ Call self."``visit_`` + node class name" with `node` as
+ parameter. If the ``visit_...`` method does not exist, call
+ self.unknown_visit.
+ """
+ node_name = node.__class__.__name__
+ method = getattr(self, 'visit_' + node_name, self.unknown_visit)
+ self.document.reporter.debug(
+ 'docutils.nodes.NodeVisitor.dispatch_visit calling %s for %s'
+ % (method.__name__, node_name))
+ return method(node)
+
+ def dispatch_departure(self, node):
+ """
+ Call self."``depart_`` + node class name" with `node` as
+ parameter. If the ``depart_...`` method does not exist, call
+ self.unknown_departure.
+ """
+ node_name = node.__class__.__name__
+ method = getattr(self, 'depart_' + node_name, self.unknown_departure)
+ self.document.reporter.debug(
+ 'docutils.nodes.NodeVisitor.dispatch_departure calling %s for %s'
+ % (method.__name__, node_name))
+ return method(node)
+
+ def unknown_visit(self, node):
+ """
+ Called when entering unknown `Node` types.
+
+ Raise an exception unless overridden.
+ """
+ if (self.document.settings.strict_visitor
+ or node.__class__.__name__ not in self.optional):
+ raise NotImplementedError(
+ '%s visiting unknown node type: %s'
+ % (self.__class__, node.__class__.__name__))
+
+ def unknown_departure(self, node):
+ """
+ Called before exiting unknown `Node` types.
+
+ Raise exception unless overridden.
+ """
+ if (self.document.settings.strict_visitor
+ or node.__class__.__name__ not in self.optional):
+ raise NotImplementedError(
+ '%s departing unknown node type: %s'
+ % (self.__class__, node.__class__.__name__))
+
+
+class SparseNodeVisitor(NodeVisitor):
+
+ """
+ Base class for sparse traversals, where only certain node types are of
+ interest. When ``visit_...`` & ``depart_...`` methods should be
+ implemented for *all* node types (such as for `docutils.writers.Writer`
+ subclasses), subclass `NodeVisitor` instead.
+ """
+
+
+class GenericNodeVisitor(NodeVisitor):
+
+ """
+ Generic "Visitor" abstract superclass, for simple traversals.
+
+ Unless overridden, each ``visit_...`` method calls `default_visit()`, and
+ each ``depart_...`` method (when using `Node.walkabout()`) calls
+ `default_departure()`. `default_visit()` (and `default_departure()`) must
+ be overridden in subclasses.
+
+ Define fully generic visitors by overriding `default_visit()` (and
+ `default_departure()`) only. Define semi-generic visitors by overriding
+ individual ``visit_...()`` (and ``depart_...()``) methods also.
+
+ `NodeVisitor.unknown_visit()` (`NodeVisitor.unknown_departure()`) should
+ be overridden for default behavior.
+ """
+
+ def default_visit(self, node):
+ """Override for generic, uniform traversals."""
+ raise NotImplementedError
+
+ def default_departure(self, node):
+ """Override for generic, uniform traversals."""
+ raise NotImplementedError
+
+
+def _call_default_visit(self, node):
+ self.default_visit(node)
+
+
+def _call_default_departure(self, node):
+ self.default_departure(node)
+
+
+def _nop(self, node):
+ pass
+
+
+def _add_node_class_names(names):
+ """Save typing with dynamic assignments:"""
+ for _name in names:
+ setattr(GenericNodeVisitor, "visit_" + _name, _call_default_visit)
+ setattr(GenericNodeVisitor, "depart_" + _name, _call_default_departure)
+ setattr(SparseNodeVisitor, 'visit_' + _name, _nop)
+ setattr(SparseNodeVisitor, 'depart_' + _name, _nop)
+
+
+_add_node_class_names(node_class_names)
+
+
+class TreeCopyVisitor(GenericNodeVisitor):
+
+ """
+ Make a complete copy of a tree or branch, including element attributes.
+ """
+
+ def __init__(self, document):
+ GenericNodeVisitor.__init__(self, document)
+ self.parent_stack = []
+ self.parent = []
+
+ def get_tree_copy(self):
+ return self.parent[0]
+
+ def default_visit(self, node):
+ """Copy the current node, and make it the new acting parent."""
+ newnode = node.copy()
+ self.parent.append(newnode)
+ self.parent_stack.append(self.parent)
+ self.parent = newnode
+
+ def default_departure(self, node):
+ """Restore the previous acting parent."""
+ self.parent = self.parent_stack.pop()
+
+
+class TreePruningException(Exception):
+
+ """
+ Base class for `NodeVisitor`-related tree pruning exceptions.
+
+ Raise subclasses from within ``visit_...`` or ``depart_...`` methods
+ called from `Node.walk()` and `Node.walkabout()` tree traversals to prune
+ the tree traversed.
+ """
+
+
+class SkipChildren(TreePruningException):
+
+ """
+ Do not visit any children of the current node. The current node's
+ siblings and ``depart_...`` method are not affected.
+ """
+
+
+class SkipSiblings(TreePruningException):
+
+ """
+ Do not visit any more siblings (to the right) of the current node. The
+ current node's children and its ``depart_...`` method are not affected.
+ """
+
+
+class SkipNode(TreePruningException):
+
+ """
+ Do not visit the current node's children, and do not call the current
+ node's ``depart_...`` method.
+ """
+
+
+class SkipDeparture(TreePruningException):
+
+ """
+ Do not call the current node's ``depart_...`` method. The current node's
+ children and siblings are not affected.
+ """
+
+
+class NodeFound(TreePruningException):
+
+ """
+ Raise to indicate that the target of a search has been found. This
+ exception must be caught by the client; it is not caught by the traversal
+ code.
+ """
+
+
+class StopTraversal(TreePruningException):
+
+ """
+ Stop the traversal altogether. The current node's ``depart_...`` method
+ is not affected. The parent nodes ``depart_...`` methods are also called
+ as usual. No other nodes are visited. This is an alternative to
+ NodeFound that does not cause exception handling to trickle up to the
+ caller.
+ """
+
+
+def make_id(string):
+ """
+ Convert `string` into an identifier and return it.
+
+ Docutils identifiers will conform to the regular expression
+ ``[a-z](-?[a-z0-9]+)*``. For CSS compatibility, identifiers (the "class"
+ and "id" attributes) should have no underscores, colons, or periods.
+ Hyphens may be used.
+
+ - The `HTML 4.01 spec`_ defines identifiers based on SGML tokens:
+
+ ID and NAME tokens must begin with a letter ([A-Za-z]) and may be
+ followed by any number of letters, digits ([0-9]), hyphens ("-"),
+ underscores ("_"), colons (":"), and periods (".").
+
+ - However the `CSS1 spec`_ defines identifiers based on the "name" token,
+ a tighter interpretation ("flex" tokenizer notation; "latin1" and
+ "escape" 8-bit characters have been replaced with entities)::
+
+ unicode \\[0-9a-f]{1,4}
+ latin1 [&iexcl;-&yuml;]
+ escape {unicode}|\\[ -~&iexcl;-&yuml;]
+ nmchar [-a-z0-9]|{latin1}|{escape}
+ name {nmchar}+
+
+ The CSS1 "nmchar" rule does not include underscores ("_"), colons (":"),
+ or periods ("."), therefore "class" and "id" attributes should not contain
+ these characters. They should be replaced with hyphens ("-"). Combined
+ with HTML's requirements (the first character must be a letter; no
+ "unicode", "latin1", or "escape" characters), this results in the
+ ``[a-z](-?[a-z0-9]+)*`` pattern.
+
+ .. _HTML 4.01 spec: https://www.w3.org/TR/html401
+ .. _CSS1 spec: https://www.w3.org/TR/REC-CSS1
+ """
+ id = string.lower()
+ id = id.translate(_non_id_translate_digraphs)
+ id = id.translate(_non_id_translate)
+ # get rid of non-ascii characters.
+ # 'ascii' lowercase to prevent problems with turkish locale.
+ id = unicodedata.normalize(
+ 'NFKD', id).encode('ascii', 'ignore').decode('ascii')
+ # shrink runs of whitespace and replace by hyphen
+ id = _non_id_chars.sub('-', ' '.join(id.split()))
+ id = _non_id_at_ends.sub('', id)
+ return str(id)
+
+
+_non_id_chars = re.compile('[^a-z0-9]+')
+_non_id_at_ends = re.compile('^[-0-9]+|-+$')
+_non_id_translate = {
+ 0x00f8: 'o', # o with stroke
+ 0x0111: 'd', # d with stroke
+ 0x0127: 'h', # h with stroke
+ 0x0131: 'i', # dotless i
+ 0x0142: 'l', # l with stroke
+ 0x0167: 't', # t with stroke
+ 0x0180: 'b', # b with stroke
+ 0x0183: 'b', # b with topbar
+ 0x0188: 'c', # c with hook
+ 0x018c: 'd', # d with topbar
+ 0x0192: 'f', # f with hook
+ 0x0199: 'k', # k with hook
+ 0x019a: 'l', # l with bar
+ 0x019e: 'n', # n with long right leg
+ 0x01a5: 'p', # p with hook
+ 0x01ab: 't', # t with palatal hook
+ 0x01ad: 't', # t with hook
+ 0x01b4: 'y', # y with hook
+ 0x01b6: 'z', # z with stroke
+ 0x01e5: 'g', # g with stroke
+ 0x0225: 'z', # z with hook
+ 0x0234: 'l', # l with curl
+ 0x0235: 'n', # n with curl
+ 0x0236: 't', # t with curl
+ 0x0237: 'j', # dotless j
+ 0x023c: 'c', # c with stroke
+ 0x023f: 's', # s with swash tail
+ 0x0240: 'z', # z with swash tail
+ 0x0247: 'e', # e with stroke
+ 0x0249: 'j', # j with stroke
+ 0x024b: 'q', # q with hook tail
+ 0x024d: 'r', # r with stroke
+ 0x024f: 'y', # y with stroke
+}
+_non_id_translate_digraphs = {
+ 0x00df: 'sz', # ligature sz
+ 0x00e6: 'ae', # ae
+ 0x0153: 'oe', # ligature oe
+ 0x0238: 'db', # db digraph
+ 0x0239: 'qp', # qp digraph
+}
+
+
+def dupname(node, name):
+ node['dupnames'].append(name)
+ node['names'].remove(name)
+ # Assume that `node` is referenced, even though it isn't;
+ # we don't want to throw unnecessary system_messages.
+ node.referenced = True
+
+
+def fully_normalize_name(name):
+ """Return a case- and whitespace-normalized name."""
+ return ' '.join(name.lower().split())
+
+
+def whitespace_normalize_name(name):
+ """Return a whitespace-normalized name."""
+ return ' '.join(name.split())
+
+
+def serial_escape(value):
+ """Escape string values that are elements of a list, for serialization."""
+ return value.replace('\\', r'\\').replace(' ', r'\ ')
+
+
+def pseudo_quoteattr(value):
+ """Quote attributes for pseudo-xml"""
+ return '"%s"' % value
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/__init__.py b/.venv/lib/python3.12/site-packages/docutils/parsers/__init__.py
new file mode 100644
index 00000000..f1bb268e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/__init__.py
@@ -0,0 +1,92 @@
+# $Id: __init__.py 9048 2022-03-29 21:50:15Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+This package contains Docutils parser modules.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from importlib import import_module
+
+from docutils import Component, frontend
+
+
+class Parser(Component):
+ settings_spec = (
+ 'Generic Parser Options',
+ None,
+ (('Disable directives that insert the contents of an external file; '
+ 'replaced with a "warning" system message.',
+ ['--no-file-insertion'],
+ {'action': 'store_false', 'default': 1,
+ 'dest': 'file_insertion_enabled',
+ 'validator': frontend.validate_boolean}),
+ ('Enable directives that insert the contents '
+ 'of an external file. (default)',
+ ['--file-insertion-enabled'],
+ {'action': 'store_true'}),
+ ('Disable the "raw" directive; '
+ 'replaced with a "warning" system message.',
+ ['--no-raw'],
+ {'action': 'store_false', 'default': 1, 'dest': 'raw_enabled',
+ 'validator': frontend.validate_boolean}),
+ ('Enable the "raw" directive. (default)',
+ ['--raw-enabled'],
+ {'action': 'store_true'}),
+ ('Maximal number of characters in an input line. Default 10 000.',
+ ['--line-length-limit'],
+ {'metavar': '<length>', 'type': 'int', 'default': 10000,
+ 'validator': frontend.validate_nonnegative_int}),
+ )
+ )
+ component_type = 'parser'
+ config_section = 'parsers'
+
+ def parse(self, inputstring, document):
+ """Override to parse `inputstring` into document tree `document`."""
+ raise NotImplementedError('subclass must override this method')
+
+ def setup_parse(self, inputstring, document):
+ """Initial parse setup. Call at start of `self.parse()`."""
+ self.inputstring = inputstring
+ # provide fallbacks in case the document has only generic settings
+ document.settings.setdefault('file_insertion_enabled', False)
+ document.settings.setdefault('raw_enabled', False)
+ document.settings.setdefault('line_length_limit', 10000)
+ self.document = document
+ document.reporter.attach_observer(document.note_parse_message)
+
+ def finish_parse(self):
+ """Finalize parse details. Call at end of `self.parse()`."""
+ self.document.reporter.detach_observer(
+ self.document.note_parse_message)
+
+
+_parser_aliases = { # short names for known parsers
+ 'null': 'docutils.parsers.null',
+ # reStructuredText
+ 'rst': 'docutils.parsers.rst',
+ 'restructuredtext': 'docutils.parsers.rst',
+ 'rest': 'docutils.parsers.rst',
+ 'restx': 'docutils.parsers.rst',
+ 'rtxt': 'docutils.parsers.rst',
+ # 3rd-party Markdown parsers
+ 'recommonmark': 'docutils.parsers.recommonmark_wrapper',
+ 'myst': 'myst_parser.docutils_',
+ # 'pycmark': works out of the box
+ # dispatcher for 3rd-party Markdown parsers
+ 'commonmark': 'docutils.parsers.commonmark_wrapper',
+ 'markdown': 'docutils.parsers.commonmark_wrapper',
+ }
+
+
+def get_parser_class(parser_name):
+ """Return the Parser class from the `parser_name` module."""
+ name = parser_name.lower()
+ try:
+ module = import_module(_parser_aliases.get(name, name))
+ except ImportError as err:
+ raise ImportError(f'Parser "{parser_name}" not found. {err}')
+ return module.Parser
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/commonmark_wrapper.py b/.venv/lib/python3.12/site-packages/docutils/parsers/commonmark_wrapper.py
new file mode 100644
index 00000000..ea538a6b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/commonmark_wrapper.py
@@ -0,0 +1,56 @@
+#! /usr/bin/env python3
+# :Copyright: © 2022 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
+#
+# Revision: $Revision: 9561 $
+# Date: $Date: 2024-03-14 17:34:48 +0100 (Do, 14. Mär 2024) $
+"""
+An interface for parsing CommonMark input.
+
+Select a locally installed parser from the following 3rd-party
+parser packages:
+
+:pycmark: https://pypi.org/project/pycmark/
+:myst: https://pypi.org/project/myst-docutils/
+:recommonmark: https://pypi.org/project/recommonmark/ (deprecated)
+
+The first parser class that can be successfully imported is mapped to
+`commonmark_wrapper.Parser`.
+
+This module is provisional:
+the API is not settled and may change with any minor Docutils version.
+"""
+
+import docutils.parsers
+
+
+commonmark_parser_names = ('pycmark', 'myst', 'recommonmark')
+"""Names of compatible drop-in CommonMark parsers"""
+
+Parser = None
+parser_name = ''
+
+for name in commonmark_parser_names:
+ try:
+ Parser = docutils.parsers.get_parser_class(name)
+ except ImportError:
+ continue
+ parser_name = name
+ break
+
+if Parser is None:
+ raise ImportError(
+ 'Parsing "CommonMark" requires one of the packages\n'
+ f'{commonmark_parser_names} available at https://pypi.org')
+
+if parser_name == 'myst':
+ if not Parser.settings_defaults:
+ Parser.settings_defaults = {}
+ Parser.settings_defaults['myst_commonmark_only'] = True
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/null.py b/.venv/lib/python3.12/site-packages/docutils/parsers/null.py
new file mode 100644
index 00000000..238c4502
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/null.py
@@ -0,0 +1,20 @@
+# $Id: null.py 4564 2006-05-21 20:44:42Z wiemann $
+# Author: Martin Blais <blais@furius.ca>
+# Copyright: This module has been placed in the public domain.
+
+"""A do-nothing parser."""
+
+from docutils import parsers
+
+
+class Parser(parsers.Parser):
+
+ """A do-nothing parser."""
+
+ supported = ('null',)
+
+ config_section = 'null parser'
+ config_section_dependencies = ('parsers',)
+
+ def parse(self, inputstring, document):
+ pass
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/recommonmark_wrapper.py b/.venv/lib/python3.12/site-packages/docutils/parsers/recommonmark_wrapper.py
new file mode 100644
index 00000000..151a8bde
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/recommonmark_wrapper.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+# :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: https://opensource.org/licenses/BSD-2-Clause
+#
+# Revision: $Revision: 9302 $
+# Date: $Date: 2022-12-02 18:14:05 +0100 (Fr, 02. Dez 2022) $
+"""
+A parser for CommonMark Markdown text using `recommonmark`__.
+
+__ https://pypi.org/project/recommonmark/
+
+.. important:: This module is provisional
+
+ * The "recommonmark" package is unmaintained and deprecated.
+ This wrapper module will be removed in a future Docutils version.
+
+ * The API is not settled and may change with any minor Docutils version.
+"""
+
+from docutils import Component
+from docutils import nodes
+
+try:
+ # If possible, import Sphinx's 'addnodes'
+ from sphinx import addnodes
+except ImportError:
+ # stub to prevent errors if Sphinx isn't installed
+ import sys
+ import types
+
+ class pending_xref(nodes.Inline, nodes.Element): ... # NoQA
+
+ sys.modules['sphinx'] = sphinx = types.ModuleType('sphinx')
+ sphinx.addnodes = addnodes = types.SimpleNamespace()
+ addnodes.pending_xref = pending_xref
+try:
+ import recommonmark
+ from recommonmark.parser import CommonMarkParser
+except ImportError as err:
+ raise ImportError(
+ 'Parsing "recommonmark" Markdown flavour requires the\n'
+ ' package https://pypi.org/project/recommonmark.'
+ ) from err
+else:
+ if recommonmark.__version__ < '0.6.0':
+ raise ImportError('The installed version of "recommonmark" is too old.'
+ ' Update with "pip install -U recommonmark".')
+
+
+# auxiliary function for `document.findall()`
+def is_literal(node):
+ return isinstance(node, (nodes.literal, nodes.literal_block))
+
+
+class Parser(CommonMarkParser):
+ """MarkDown parser based on recommonmark.
+
+ This parser is provisional:
+ the API is not settled and may change with any minor Docutils version.
+ """
+ supported = ('recommonmark', 'commonmark', 'markdown', 'md')
+ """Formats this parser supports."""
+
+ config_section = 'recommonmark parser'
+ config_section_dependencies = ('parsers',)
+
+ def get_transforms(self):
+ return Component.get_transforms(self) # + [AutoStructify]
+
+ def parse(self, inputstring, document):
+ """Use the upstream parser and clean up afterwards.
+ """
+ # check for exorbitantly long lines
+ for i, line in enumerate(inputstring.split('\n')):
+ if len(line) > document.settings.line_length_limit:
+ error = document.reporter.error(
+ 'Line %d exceeds the line-length-limit.'%(i+1))
+ document.append(error)
+ return
+
+ # pass to upstream parser
+ try:
+ CommonMarkParser.parse(self, inputstring, document)
+ except Exception as err:
+ if document.settings.traceback:
+ raise err
+ error = document.reporter.error('Parsing with "recommonmark" '
+ 'returned the error:\n%s'%err)
+ document.append(error)
+
+ # Post-Processing
+ # ---------------
+
+ # merge adjoining Text nodes:
+ for node in document.findall(nodes.TextElement):
+ children = node.children
+ i = 0
+ while i+1 < len(children):
+ if (isinstance(children[i], nodes.Text)
+ and isinstance(children[i+1], nodes.Text)):
+ children[i] = nodes.Text(children[i]+children.pop(i+1))
+ children[i].parent = node
+ else:
+ i += 1
+
+ # add "code" class argument to literal elements (inline and block)
+ for node in document.findall(is_literal):
+ if 'code' not in node['classes']:
+ node['classes'].append('code')
+ # move "language" argument to classes
+ for node in document.findall(nodes.literal_block):
+ if 'language' in node.attributes:
+ node['classes'].append(node['language'])
+ del node['language']
+
+ # replace raw nodes if raw is not allowed
+ if not document.settings.raw_enabled:
+ for node in document.findall(nodes.raw):
+ warning = document.reporter.warning('Raw content disabled.')
+ node.parent.replace(node, warning)
+
+ # drop pending_xref (Sphinx cross reference extension)
+ for node in document.findall(addnodes.pending_xref):
+ reference = node.children[0]
+ if 'name' not in reference:
+ reference['name'] = nodes.fully_normalize_name(
+ reference.astext())
+ node.parent.replace(node, reference)
+
+ def visit_document(self, node):
+ """Dummy function to prevent spurious warnings.
+
+ cf. https://github.com/readthedocs/recommonmark/issues/177
+ """
+ pass
+
+ # Overwrite parent method with version that
+ # doesn't pass deprecated `rawsource` argument to nodes.Text:
+ def visit_text(self, mdnode):
+ self.current_node.append(nodes.Text(mdnode.literal))
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/__init__.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/__init__.py
new file mode 100644
index 00000000..460a2e95
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/__init__.py
@@ -0,0 +1,413 @@
+# $Id: __init__.py 9258 2022-11-21 14:51:43Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+This is ``docutils.parsers.rst`` package. It exports a single class, `Parser`,
+the reStructuredText parser.
+
+
+Usage
+=====
+
+1. Create a parser::
+
+ parser = docutils.parsers.rst.Parser()
+
+ Several optional arguments may be passed to modify the parser's behavior.
+ Please see `Customizing the Parser`_ below for details.
+
+2. Gather input (a multi-line string), by reading a file or the standard
+ input::
+
+ input = sys.stdin.read()
+
+3. Create a new empty `docutils.nodes.document` tree::
+
+ document = docutils.utils.new_document(source, settings)
+
+ See `docutils.utils.new_document()` for parameter details.
+
+4. Run the parser, populating the document tree::
+
+ parser.parse(input, document)
+
+
+Parser Overview
+===============
+
+The reStructuredText parser is implemented as a state machine, examining its
+input one line at a time. To understand how the parser works, please first
+become familiar with the `docutils.statemachine` module, then see the
+`states` module.
+
+
+Customizing the Parser
+----------------------
+
+Anything that isn't already customizable is that way simply because that type
+of customizability hasn't been implemented yet. Patches welcome!
+
+When instantiating an object of the `Parser` class, two parameters may be
+passed: ``rfc2822`` and ``inliner``. Pass ``rfc2822=True`` to enable an
+initial RFC-2822 style header block, parsed as a "field_list" element (with
+"class" attribute set to "rfc2822"). Currently this is the only body-level
+element which is customizable without subclassing. (Tip: subclass `Parser`
+and change its "state_classes" and "initial_state" attributes to refer to new
+classes. Contact the author if you need more details.)
+
+The ``inliner`` parameter takes an instance of `states.Inliner` or a subclass.
+It handles inline markup recognition. A common extension is the addition of
+further implicit hyperlinks, like "RFC 2822". This can be done by subclassing
+`states.Inliner`, adding a new method for the implicit markup, and adding a
+``(pattern, method)`` pair to the "implicit_dispatch" attribute of the
+subclass. See `states.Inliner.implicit_inline()` for details. Explicit
+inline markup can be customized in a `states.Inliner` subclass via the
+``patterns.initial`` and ``dispatch`` attributes (and new methods as
+appropriate).
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import docutils.parsers
+import docutils.statemachine
+from docutils.parsers.rst import roles, states
+from docutils import frontend, nodes
+from docutils.transforms import universal
+
+
+class Parser(docutils.parsers.Parser):
+
+ """The reStructuredText parser."""
+
+ supported = ('rst', 'restructuredtext', 'rest', 'restx', 'rtxt', 'rstx')
+ """Aliases this parser supports."""
+
+ settings_spec = docutils.parsers.Parser.settings_spec + (
+ 'reStructuredText Parser Options',
+ None,
+ (('Recognize and link to standalone PEP references (like "PEP 258").',
+ ['--pep-references'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ ('Base URL for PEP references '
+ '(default "https://peps.python.org/").',
+ ['--pep-base-url'],
+ {'metavar': '<URL>', 'default': 'https://peps.python.org/',
+ 'validator': frontend.validate_url_trailing_slash}),
+ ('Template for PEP file part of URL. (default "pep-%04d")',
+ ['--pep-file-url-template'],
+ {'metavar': '<URL>', 'default': 'pep-%04d'}),
+ ('Recognize and link to standalone RFC references (like "RFC 822").',
+ ['--rfc-references'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ ('Base URL for RFC references '
+ '(default "https://tools.ietf.org/html/").',
+ ['--rfc-base-url'],
+ {'metavar': '<URL>', 'default': 'https://tools.ietf.org/html/',
+ 'validator': frontend.validate_url_trailing_slash}),
+ ('Set number of spaces for tab expansion (default 8).',
+ ['--tab-width'],
+ {'metavar': '<width>', 'type': 'int', 'default': 8,
+ 'validator': frontend.validate_nonnegative_int}),
+ ('Remove spaces before footnote references.',
+ ['--trim-footnote-reference-space'],
+ {'action': 'store_true', 'validator': frontend.validate_boolean}),
+ ('Leave spaces before footnote references.',
+ ['--leave-footnote-reference-space'],
+ {'action': 'store_false', 'dest': 'trim_footnote_reference_space'}),
+ ('Token name set for parsing code with Pygments: one of '
+ '"long", "short", or "none" (no parsing). Default is "long".',
+ ['--syntax-highlight'],
+ {'choices': ['long', 'short', 'none'],
+ 'default': 'long', 'metavar': '<format>'}),
+ ('Change straight quotation marks to typographic form: '
+ 'one of "yes", "no", "alt[ernative]" (default "no").',
+ ['--smart-quotes'],
+ {'default': False, 'metavar': '<yes/no/alt>',
+ 'validator': frontend.validate_ternary}),
+ ('Characters to use as "smart quotes" for <language>. ',
+ ['--smartquotes-locales'],
+ {'metavar': '<language:quotes[,language:quotes,...]>',
+ 'action': 'append',
+ 'validator': frontend.validate_smartquotes_locales}),
+ ('Inline markup recognized at word boundaries only '
+ '(adjacent to punctuation or whitespace). '
+ 'Force character-level inline markup recognition with '
+ '"\\ " (backslash + space). Default.',
+ ['--word-level-inline-markup'],
+ {'action': 'store_false', 'dest': 'character_level_inline_markup'}),
+ ('Inline markup recognized anywhere, regardless of surrounding '
+ 'characters. Backslash-escapes must be used to avoid unwanted '
+ 'markup recognition. Useful for East Asian languages. '
+ 'Experimental.',
+ ['--character-level-inline-markup'],
+ {'action': 'store_true', 'default': False,
+ 'dest': 'character_level_inline_markup'}),
+ )
+ )
+
+ config_section = 'restructuredtext parser'
+ config_section_dependencies = ('parsers',)
+
+ def __init__(self, rfc2822=False, inliner=None):
+ if rfc2822:
+ self.initial_state = 'RFC2822Body'
+ else:
+ self.initial_state = 'Body'
+ self.state_classes = states.state_classes
+ self.inliner = inliner
+
+ def get_transforms(self):
+ return super().get_transforms() + [universal.SmartQuotes]
+
+ def parse(self, inputstring, document):
+ """Parse `inputstring` and populate `document`, a document tree."""
+ self.setup_parse(inputstring, document)
+ # provide fallbacks in case the document has only generic settings
+ self.document.settings.setdefault('tab_width', 8)
+ self.document.settings.setdefault('syntax_highlight', 'long')
+ self.statemachine = states.RSTStateMachine(
+ state_classes=self.state_classes,
+ initial_state=self.initial_state,
+ debug=document.reporter.debug_flag)
+ inputlines = docutils.statemachine.string2lines(
+ inputstring, tab_width=document.settings.tab_width,
+ convert_whitespace=True)
+ for i, line in enumerate(inputlines):
+ if len(line) > self.document.settings.line_length_limit:
+ error = self.document.reporter.error(
+ 'Line %d exceeds the line-length-limit.'%(i+1))
+ self.document.append(error)
+ break
+ else:
+ self.statemachine.run(inputlines, document, inliner=self.inliner)
+ # restore the "default" default role after parsing a document
+ if '' in roles._roles:
+ del roles._roles['']
+ self.finish_parse()
+
+
+class DirectiveError(Exception):
+
+ """
+ Store a message and a system message level.
+
+ To be thrown from inside directive code.
+
+ Do not instantiate directly -- use `Directive.directive_error()`
+ instead!
+ """
+
+ def __init__(self, level, message):
+ """Set error `message` and `level`"""
+ Exception.__init__(self)
+ self.level = level
+ self.msg = message
+
+
+class Directive:
+
+ """
+ Base class for reStructuredText directives.
+
+ The following attributes may be set by subclasses. They are
+ interpreted by the directive parser (which runs the directive
+ class):
+
+ - `required_arguments`: The number of required arguments (default:
+ 0).
+
+ - `optional_arguments`: The number of optional arguments (default:
+ 0).
+
+ - `final_argument_whitespace`: A boolean, indicating if the final
+ argument may contain whitespace (default: False).
+
+ - `option_spec`: A dictionary, mapping known option names to
+ conversion functions such as `int` or `float` (default: {}, no
+ options). Several conversion functions are defined in the
+ directives/__init__.py module.
+
+ Option conversion functions take a single parameter, the option
+ argument (a string or ``None``), validate it and/or convert it
+ to the appropriate form. Conversion functions may raise
+ `ValueError` and `TypeError` exceptions.
+
+ - `has_content`: A boolean; True if content is allowed. Client
+ code must handle the case where content is required but not
+ supplied (an empty content list will be supplied).
+
+ Arguments are normally single whitespace-separated words. The
+ final argument may contain whitespace and/or newlines if
+ `final_argument_whitespace` is True.
+
+ If the form of the arguments is more complex, specify only one
+ argument (either required or optional) and set
+ `final_argument_whitespace` to True; the client code must do any
+ context-sensitive parsing.
+
+ When a directive implementation is being run, the directive class
+ is instantiated, and the `run()` method is executed. During
+ instantiation, the following instance variables are set:
+
+ - ``name`` is the directive type or name (string).
+
+ - ``arguments`` is the list of positional arguments (strings).
+
+ - ``options`` is a dictionary mapping option names (strings) to
+ values (type depends on option conversion functions; see
+ `option_spec` above).
+
+ - ``content`` is a list of strings, the directive content line by line.
+
+ - ``lineno`` is the absolute line number of the first line
+ of the directive.
+
+ - ``content_offset`` is the line offset of the first line
+ of the content from the beginning of the current input.
+ Used when initiating a nested parse.
+
+ - ``block_text`` is a string containing the entire directive.
+
+ - ``state`` is the state which called the directive function.
+
+ - ``state_machine`` is the state machine which controls the state
+ which called the directive function.
+
+ - ``reporter`` is the state machine's `reporter` instance.
+
+ Directive functions return a list of nodes which will be inserted
+ into the document tree at the point where the directive was
+ encountered. This can be an empty list if there is nothing to
+ insert.
+
+ For ordinary directives, the list must contain body elements or
+ structural elements. Some directives are intended specifically
+ for substitution definitions, and must return a list of `Text`
+ nodes and/or inline elements (suitable for inline insertion, in
+ place of the substitution reference). Such directives must verify
+ substitution definition context, typically using code like this::
+
+ if not isinstance(state, states.SubstitutionDef):
+ error = self.reporter.error(
+ 'Invalid context: the "%s" directive can only be used '
+ 'within a substitution definition.' % (name),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error]
+ """
+
+ # There is a "Creating reStructuredText Directives" how-to at
+ # <https://docutils.sourceforge.io/docs/howto/rst-directives.html>. If you
+ # update this docstring, please update the how-to as well.
+
+ required_arguments = 0
+ """Number of required directive arguments."""
+
+ optional_arguments = 0
+ """Number of optional arguments after the required arguments."""
+
+ final_argument_whitespace = False
+ """May the final argument contain whitespace?"""
+
+ option_spec = None
+ """Mapping of option names to validator functions."""
+
+ has_content = False
+ """May the directive have content?"""
+
+ def __init__(self, name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ self.name = name
+ self.arguments = arguments
+ self.options = options
+ self.content = content
+ self.lineno = lineno
+ self.content_offset = content_offset
+ self.block_text = block_text
+ self.state = state
+ self.state_machine = state_machine
+ self.reporter = state_machine.reporter
+
+ def run(self):
+ raise NotImplementedError('Must override run() in subclass.')
+
+ # Directive errors:
+
+ def directive_error(self, level, message):
+ """
+ Return a DirectiveError suitable for being thrown as an exception.
+
+ Call "raise self.directive_error(level, message)" from within
+ a directive implementation to return one single system message
+ at level `level`, which automatically gets the directive block
+ and the line number added.
+
+ Preferably use the `debug`, `info`, `warning`, `error`, or `severe`
+ wrapper methods, e.g. ``self.error(message)`` to generate an
+ ERROR-level directive error.
+ """
+ return DirectiveError(level, message)
+
+ def debug(self, message):
+ return self.directive_error(0, message)
+
+ def info(self, message):
+ return self.directive_error(1, message)
+
+ def warning(self, message):
+ return self.directive_error(2, message)
+
+ def error(self, message):
+ return self.directive_error(3, message)
+
+ def severe(self, message):
+ return self.directive_error(4, message)
+
+ # Convenience methods:
+
+ def assert_has_content(self):
+ """
+ Throw an ERROR-level DirectiveError if the directive doesn't
+ have contents.
+ """
+ if not self.content:
+ raise self.error('Content block expected for the "%s" directive; '
+ 'none found.' % self.name)
+
+ def add_name(self, node):
+ """Append self.options['name'] to node['names'] if it exists.
+
+ Also normalize the name string and register it as explicit target.
+ """
+ if 'name' in self.options:
+ name = nodes.fully_normalize_name(self.options.pop('name'))
+ if 'name' in node:
+ del node['name']
+ node['names'].append(name)
+ self.state.document.note_explicit_target(node, node)
+
+
+def convert_directive_function(directive_fn):
+ """
+ Define & return a directive class generated from `directive_fn`.
+
+ `directive_fn` uses the old-style, functional interface.
+ """
+
+ class FunctionalDirective(Directive):
+
+ option_spec = getattr(directive_fn, 'options', None)
+ has_content = getattr(directive_fn, 'content', False)
+ _argument_spec = getattr(directive_fn, 'arguments', (0, 0, False))
+ required_arguments, optional_arguments, final_argument_whitespace \
+ = _argument_spec
+
+ def run(self):
+ return directive_fn(
+ self.name, self.arguments, self.options, self.content,
+ self.lineno, self.content_offset, self.block_text,
+ self.state, self.state_machine)
+
+ # Return new-style directive.
+ return FunctionalDirective
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/__init__.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/__init__.py
new file mode 100644
index 00000000..ebbdfe3b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/__init__.py
@@ -0,0 +1,466 @@
+# $Id: __init__.py 9426 2023-07-03 12:38:54Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+This package contains directive implementation modules.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import re
+import codecs
+from importlib import import_module
+
+from docutils import nodes, parsers
+from docutils.utils import split_escaped_whitespace, escape2null
+from docutils.parsers.rst.languages import en as _fallback_language_module
+
+
+_directive_registry = {
+ 'attention': ('admonitions', 'Attention'),
+ 'caution': ('admonitions', 'Caution'),
+ 'code': ('body', 'CodeBlock'),
+ 'danger': ('admonitions', 'Danger'),
+ 'error': ('admonitions', 'Error'),
+ 'important': ('admonitions', 'Important'),
+ 'note': ('admonitions', 'Note'),
+ 'tip': ('admonitions', 'Tip'),
+ 'hint': ('admonitions', 'Hint'),
+ 'warning': ('admonitions', 'Warning'),
+ 'admonition': ('admonitions', 'Admonition'),
+ 'sidebar': ('body', 'Sidebar'),
+ 'topic': ('body', 'Topic'),
+ 'line-block': ('body', 'LineBlock'),
+ 'parsed-literal': ('body', 'ParsedLiteral'),
+ 'math': ('body', 'MathBlock'),
+ 'rubric': ('body', 'Rubric'),
+ 'epigraph': ('body', 'Epigraph'),
+ 'highlights': ('body', 'Highlights'),
+ 'pull-quote': ('body', 'PullQuote'),
+ 'compound': ('body', 'Compound'),
+ 'container': ('body', 'Container'),
+ # 'questions': ('body', 'question_list'),
+ 'table': ('tables', 'RSTTable'),
+ 'csv-table': ('tables', 'CSVTable'),
+ 'list-table': ('tables', 'ListTable'),
+ 'image': ('images', 'Image'),
+ 'figure': ('images', 'Figure'),
+ 'contents': ('parts', 'Contents'),
+ 'sectnum': ('parts', 'Sectnum'),
+ 'header': ('parts', 'Header'),
+ 'footer': ('parts', 'Footer'),
+ # 'footnotes': ('parts', 'footnotes'),
+ # 'citations': ('parts', 'citations'),
+ 'target-notes': ('references', 'TargetNotes'),
+ 'meta': ('misc', 'Meta'),
+ # 'imagemap': ('html', 'imagemap'),
+ 'raw': ('misc', 'Raw'),
+ 'include': ('misc', 'Include'),
+ 'replace': ('misc', 'Replace'),
+ 'unicode': ('misc', 'Unicode'),
+ 'class': ('misc', 'Class'),
+ 'role': ('misc', 'Role'),
+ 'default-role': ('misc', 'DefaultRole'),
+ 'title': ('misc', 'Title'),
+ 'date': ('misc', 'Date'),
+ 'restructuredtext-test-directive': ('misc', 'TestDirective'),
+ }
+"""Mapping of directive name to (module name, class name). The
+directive name is canonical & must be lowercase. Language-dependent
+names are defined in the ``language`` subpackage."""
+
+_directives = {}
+"""Cache of imported directives."""
+
+
+def directive(directive_name, language_module, document):
+ """
+ Locate and return a directive function from its language-dependent name.
+ If not found in the current language, check English. Return None if the
+ named directive cannot be found.
+ """
+ normname = directive_name.lower()
+ messages = []
+ msg_text = []
+ if normname in _directives:
+ return _directives[normname], messages
+ canonicalname = None
+ try:
+ canonicalname = language_module.directives[normname]
+ except AttributeError as error:
+ msg_text.append('Problem retrieving directive entry from language '
+ 'module %r: %s.' % (language_module, error))
+ except KeyError:
+ msg_text.append('No directive entry for "%s" in module "%s".'
+ % (directive_name, language_module.__name__))
+ if not canonicalname:
+ try:
+ canonicalname = _fallback_language_module.directives[normname]
+ msg_text.append('Using English fallback for directive "%s".'
+ % directive_name)
+ except KeyError:
+ msg_text.append('Trying "%s" as canonical directive name.'
+ % directive_name)
+ # The canonical name should be an English name, but just in case:
+ canonicalname = normname
+ if msg_text:
+ message = document.reporter.info(
+ '\n'.join(msg_text), line=document.current_line)
+ messages.append(message)
+ try:
+ modulename, classname = _directive_registry[canonicalname]
+ except KeyError:
+ # Error handling done by caller.
+ return None, messages
+ try:
+ module = import_module('docutils.parsers.rst.directives.'+modulename)
+ except ImportError as detail:
+ messages.append(document.reporter.error(
+ 'Error importing directive module "%s" (directive "%s"):\n%s'
+ % (modulename, directive_name, detail),
+ line=document.current_line))
+ return None, messages
+ try:
+ directive = getattr(module, classname)
+ _directives[normname] = directive
+ except AttributeError:
+ messages.append(document.reporter.error(
+ 'No directive class "%s" in module "%s" (directive "%s").'
+ % (classname, modulename, directive_name),
+ line=document.current_line))
+ return None, messages
+ return directive, messages
+
+
+def register_directive(name, directive):
+ """
+ Register a nonstandard application-defined directive function.
+ Language lookups are not needed for such functions.
+ """
+ _directives[name] = directive
+
+
+# conversion functions for `Directive.option_spec`
+# ------------------------------------------------
+#
+# see also `parsers.rst.Directive` in ../__init__.py.
+
+
+def flag(argument):
+ """
+ Check for a valid flag option (no argument) and return ``None``.
+ (Directive option conversion function.)
+
+ Raise ``ValueError`` if an argument is found.
+ """
+ if argument and argument.strip():
+ raise ValueError('no argument is allowed; "%s" supplied' % argument)
+ else:
+ return None
+
+
+def unchanged_required(argument):
+ """
+ Return the argument text, unchanged.
+ (Directive option conversion function.)
+
+ Raise ``ValueError`` if no argument is found.
+ """
+ if argument is None:
+ raise ValueError('argument required but none supplied')
+ else:
+ return argument # unchanged!
+
+
+def unchanged(argument):
+ """
+ Return the argument text, unchanged.
+ (Directive option conversion function.)
+
+ No argument implies empty string ("").
+ """
+ if argument is None:
+ return ''
+ else:
+ return argument # unchanged!
+
+
+def path(argument):
+ """
+ Return the path argument unwrapped (with newlines removed).
+ (Directive option conversion function.)
+
+ Raise ``ValueError`` if no argument is found.
+ """
+ if argument is None:
+ raise ValueError('argument required but none supplied')
+ else:
+ return ''.join(s.strip() for s in argument.splitlines())
+
+
+def uri(argument):
+ """
+ Return the URI argument with unescaped whitespace removed.
+ (Directive option conversion function.)
+
+ Raise ``ValueError`` if no argument is found.
+ """
+ if argument is None:
+ raise ValueError('argument required but none supplied')
+ else:
+ parts = split_escaped_whitespace(escape2null(argument))
+ return ' '.join(''.join(nodes.unescape(part).split())
+ for part in parts)
+
+
+def nonnegative_int(argument):
+ """
+ Check for a nonnegative integer argument; raise ``ValueError`` if not.
+ (Directive option conversion function.)
+ """
+ value = int(argument)
+ if value < 0:
+ raise ValueError('negative value; must be positive or zero')
+ return value
+
+
+def percentage(argument):
+ """
+ Check for an integer percentage value with optional percent sign.
+ (Directive option conversion function.)
+ """
+ try:
+ argument = argument.rstrip(' %')
+ except AttributeError:
+ pass
+ return nonnegative_int(argument)
+
+
+length_units = ['em', 'ex', 'px', 'in', 'cm', 'mm', 'pt', 'pc']
+
+
+def get_measure(argument, units):
+ """
+ Check for a positive argument of one of the units and return a
+ normalized string of the form "<value><unit>" (without space in
+ between).
+ (Directive option conversion function.)
+
+ To be called from directive option conversion functions.
+ """
+ match = re.match(r'^([0-9.]+) *(%s)$' % '|'.join(units), argument)
+ try:
+ float(match.group(1))
+ except (AttributeError, ValueError):
+ raise ValueError(
+ 'not a positive measure of one of the following units:\n%s'
+ % ' '.join('"%s"' % i for i in units))
+ return match.group(1) + match.group(2)
+
+
+def length_or_unitless(argument):
+ return get_measure(argument, length_units + [''])
+
+
+def length_or_percentage_or_unitless(argument, default=''):
+ """
+ Return normalized string of a length or percentage unit.
+ (Directive option conversion function.)
+
+ Add <default> if there is no unit. Raise ValueError if the argument is not
+ a positive measure of one of the valid CSS units (or without unit).
+
+ >>> length_or_percentage_or_unitless('3 pt')
+ '3pt'
+ >>> length_or_percentage_or_unitless('3%', 'em')
+ '3%'
+ >>> length_or_percentage_or_unitless('3')
+ '3'
+ >>> length_or_percentage_or_unitless('3', 'px')
+ '3px'
+ """
+ try:
+ return get_measure(argument, length_units + ['%'])
+ except ValueError:
+ try:
+ return get_measure(argument, ['']) + default
+ except ValueError:
+ # raise ValueError with list of valid units:
+ return get_measure(argument, length_units + ['%'])
+
+
+def class_option(argument):
+ """
+ Convert the argument into a list of ID-compatible strings and return it.
+ (Directive option conversion function.)
+
+ Raise ``ValueError`` if no argument is found.
+ """
+ if argument is None:
+ raise ValueError('argument required but none supplied')
+ names = argument.split()
+ class_names = []
+ for name in names:
+ class_name = nodes.make_id(name)
+ if not class_name:
+ raise ValueError('cannot make "%s" into a class name' % name)
+ class_names.append(class_name)
+ return class_names
+
+
+unicode_pattern = re.compile(
+ r'(?:0x|x|\\x|U\+?|\\u)([0-9a-f]+)$|&#x([0-9a-f]+);$', re.IGNORECASE)
+
+
+def unicode_code(code):
+ r"""
+ Convert a Unicode character code to a Unicode character.
+ (Directive option conversion function.)
+
+ Codes may be decimal numbers, hexadecimal numbers (prefixed by ``0x``,
+ ``x``, ``\x``, ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style
+ numeric character entities (e.g. ``&#x262E;``). Other text remains as-is.
+
+ Raise ValueError for illegal Unicode code values.
+ """
+ try:
+ if code.isdigit(): # decimal number
+ return chr(int(code))
+ else:
+ match = unicode_pattern.match(code)
+ if match: # hex number
+ value = match.group(1) or match.group(2)
+ return chr(int(value, 16))
+ else: # other text
+ return code
+ except OverflowError as detail:
+ raise ValueError('code too large (%s)' % detail)
+
+
+def single_char_or_unicode(argument):
+ """
+ A single character is returned as-is. Unicode character codes are
+ converted as in `unicode_code`. (Directive option conversion function.)
+ """
+ char = unicode_code(argument)
+ if len(char) > 1:
+ raise ValueError('%r invalid; must be a single character or '
+ 'a Unicode code' % char)
+ return char
+
+
+def single_char_or_whitespace_or_unicode(argument):
+ """
+ As with `single_char_or_unicode`, but "tab" and "space" are also supported.
+ (Directive option conversion function.)
+ """
+ if argument == 'tab':
+ char = '\t'
+ elif argument == 'space':
+ char = ' '
+ else:
+ char = single_char_or_unicode(argument)
+ return char
+
+
+def positive_int(argument):
+ """
+ Converts the argument into an integer. Raises ValueError for negative,
+ zero, or non-integer values. (Directive option conversion function.)
+ """
+ value = int(argument)
+ if value < 1:
+ raise ValueError('negative or zero value; must be positive')
+ return value
+
+
+def positive_int_list(argument):
+ """
+ Converts a space- or comma-separated list of values into a Python list
+ of integers.
+ (Directive option conversion function.)
+
+ Raises ValueError for non-positive-integer values.
+ """
+ if ',' in argument:
+ entries = argument.split(',')
+ else:
+ entries = argument.split()
+ return [positive_int(entry) for entry in entries]
+
+
+def encoding(argument):
+ """
+ Verifies the encoding argument by lookup.
+ (Directive option conversion function.)
+
+ Raises ValueError for unknown encodings.
+ """
+ try:
+ codecs.lookup(argument)
+ except LookupError:
+ raise ValueError('unknown encoding: "%s"' % argument)
+ return argument
+
+
+def choice(argument, values):
+ """
+ Directive option utility function, supplied to enable options whose
+ argument must be a member of a finite set of possible values (must be
+ lower case). A custom conversion function must be written to use it. For
+ example::
+
+ from docutils.parsers.rst import directives
+
+ def yesno(argument):
+ return directives.choice(argument, ('yes', 'no'))
+
+ Raise ``ValueError`` if no argument is found or if the argument's value is
+ not valid (not an entry in the supplied list).
+ """
+ try:
+ value = argument.lower().strip()
+ except AttributeError:
+ raise ValueError('must supply an argument; choose from %s'
+ % format_values(values))
+ if value in values:
+ return value
+ else:
+ raise ValueError('"%s" unknown; choose from %s'
+ % (argument, format_values(values)))
+
+
+def format_values(values):
+ return '%s, or "%s"' % (', '.join('"%s"' % s for s in values[:-1]),
+ values[-1])
+
+
+def value_or(values, other):
+ """
+ Directive option conversion function.
+
+ The argument can be any of `values` or `argument_type`.
+ """
+ def auto_or_other(argument):
+ if argument in values:
+ return argument
+ else:
+ return other(argument)
+ return auto_or_other
+
+
+def parser_name(argument):
+ """
+ Return a docutils parser whose name matches the argument.
+ (Directive option conversion function.)
+
+ Return `None`, if the argument evaluates to `False`.
+ Raise `ValueError` if importing the parser module fails.
+ """
+ if not argument:
+ return None
+ try:
+ return parsers.get_parser_class(argument)
+ except ImportError as err:
+ raise ValueError(str(err))
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/admonitions.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/admonitions.py
new file mode 100644
index 00000000..1990099e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/admonitions.py
@@ -0,0 +1,101 @@
+# $Id: admonitions.py 9475 2023-11-13 22:30:00Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Admonition directives.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from docutils.parsers.rst import Directive
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.roles import set_classes
+from docutils import nodes
+
+
+class BaseAdmonition(Directive):
+
+ final_argument_whitespace = True
+ option_spec = {'class': directives.class_option,
+ 'name': directives.unchanged}
+ has_content = True
+
+ node_class = None
+ """Subclasses must set this to the appropriate admonition node class."""
+
+ def run(self):
+ set_classes(self.options)
+ self.assert_has_content()
+ text = '\n'.join(self.content)
+ admonition_node = self.node_class(text, **self.options)
+ self.add_name(admonition_node)
+ admonition_node.source, admonition_node.line = \
+ self.state_machine.get_source_and_line(self.lineno)
+ if self.node_class is nodes.admonition:
+ title_text = self.arguments[0]
+ textnodes, messages = self.state.inline_text(title_text,
+ self.lineno)
+ title = nodes.title(title_text, '', *textnodes)
+ title.source, title.line = (
+ self.state_machine.get_source_and_line(self.lineno))
+ admonition_node += title
+ admonition_node += messages
+ if 'classes' not in self.options:
+ admonition_node['classes'] += ['admonition-'
+ + nodes.make_id(title_text)]
+ self.state.nested_parse(self.content, self.content_offset,
+ admonition_node)
+ return [admonition_node]
+
+
+class Admonition(BaseAdmonition):
+
+ required_arguments = 1
+ node_class = nodes.admonition
+
+
+class Attention(BaseAdmonition):
+
+ node_class = nodes.attention
+
+
+class Caution(BaseAdmonition):
+
+ node_class = nodes.caution
+
+
+class Danger(BaseAdmonition):
+
+ node_class = nodes.danger
+
+
+class Error(BaseAdmonition):
+
+ node_class = nodes.error
+
+
+class Hint(BaseAdmonition):
+
+ node_class = nodes.hint
+
+
+class Important(BaseAdmonition):
+
+ node_class = nodes.important
+
+
+class Note(BaseAdmonition):
+
+ node_class = nodes.note
+
+
+class Tip(BaseAdmonition):
+
+ node_class = nodes.tip
+
+
+class Warning(BaseAdmonition):
+
+ node_class = nodes.warning
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/body.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/body.py
new file mode 100644
index 00000000..5cb90416
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/body.py
@@ -0,0 +1,305 @@
+# $Id: body.py 9500 2023-12-14 22:38:49Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Directives for additional body elements.
+
+See `docutils.parsers.rst.directives` for API details.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from docutils import nodes
+from docutils.parsers.rst import Directive
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.roles import set_classes
+from docutils.utils.code_analyzer import Lexer, LexerError, NumberLines
+
+
+class BasePseudoSection(Directive):
+
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec = {'class': directives.class_option,
+ 'name': directives.unchanged}
+ has_content = True
+
+ node_class = None
+ """Node class to be used (must be set in subclasses)."""
+
+ def run(self):
+ if not (self.state_machine.match_titles
+ or isinstance(self.state_machine.node, nodes.sidebar)):
+ raise self.error('The "%s" directive may not be used within '
+ 'topics or body elements.' % self.name)
+ self.assert_has_content()
+ if self.arguments: # title (in sidebars optional)
+ title_text = self.arguments[0]
+ textnodes, messages = self.state.inline_text(
+ title_text, self.lineno)
+ titles = [nodes.title(title_text, '', *textnodes)]
+ # Sidebar uses this code.
+ if 'subtitle' in self.options:
+ textnodes, more_messages = self.state.inline_text(
+ self.options['subtitle'], self.lineno)
+ titles.append(nodes.subtitle(self.options['subtitle'], '',
+ *textnodes))
+ messages.extend(more_messages)
+ else:
+ titles = []
+ messages = []
+ text = '\n'.join(self.content)
+ node = self.node_class(text, *(titles + messages))
+ node['classes'] += self.options.get('class', [])
+ self.add_name(node)
+ if text:
+ self.state.nested_parse(self.content, self.content_offset, node)
+ return [node]
+
+
+class Topic(BasePseudoSection):
+
+ node_class = nodes.topic
+
+
+class Sidebar(BasePseudoSection):
+
+ node_class = nodes.sidebar
+
+ required_arguments = 0
+ optional_arguments = 1
+ option_spec = BasePseudoSection.option_spec.copy()
+ option_spec['subtitle'] = directives.unchanged_required
+
+ def run(self):
+ if isinstance(self.state_machine.node, nodes.sidebar):
+ raise self.error('The "%s" directive may not be used within a '
+ 'sidebar element.' % self.name)
+ if 'subtitle' in self.options and not self.arguments:
+ raise self.error('The "subtitle" option may not be used '
+ 'without a title.')
+
+ return BasePseudoSection.run(self)
+
+
+class LineBlock(Directive):
+
+ option_spec = {'class': directives.class_option,
+ 'name': directives.unchanged}
+ has_content = True
+
+ def run(self):
+ self.assert_has_content()
+ block = nodes.line_block(classes=self.options.get('class', []))
+ self.add_name(block)
+ node_list = [block]
+ for line_text in self.content:
+ text_nodes, messages = self.state.inline_text(
+ line_text.strip(), self.lineno + self.content_offset)
+ line = nodes.line(line_text, '', *text_nodes)
+ if line_text.strip():
+ line.indent = len(line_text) - len(line_text.lstrip())
+ block += line
+ node_list.extend(messages)
+ self.content_offset += 1
+ self.state.nest_line_block_lines(block)
+ return node_list
+
+
+class ParsedLiteral(Directive):
+
+ option_spec = {'class': directives.class_option,
+ 'name': directives.unchanged}
+ has_content = True
+
+ def run(self):
+ set_classes(self.options)
+ self.assert_has_content()
+ text = '\n'.join(self.content)
+ text_nodes, messages = self.state.inline_text(text, self.lineno)
+ node = nodes.literal_block(text, '', *text_nodes, **self.options)
+ node.line = self.content_offset + 1
+ self.add_name(node)
+ return [node] + messages
+
+
+class CodeBlock(Directive):
+ """Parse and mark up content of a code block.
+
+ Configuration setting: syntax_highlight
+ Highlight Code content with Pygments?
+ Possible values: ('long', 'short', 'none')
+
+ """
+ optional_arguments = 1
+ option_spec = {'class': directives.class_option,
+ 'name': directives.unchanged,
+ 'number-lines': directives.unchanged # integer or None
+ }
+ has_content = True
+
+ def run(self):
+ self.assert_has_content()
+ if self.arguments:
+ language = self.arguments[0]
+ else:
+ language = ''
+ set_classes(self.options)
+ classes = ['code']
+ if language:
+ classes.append(language)
+ if 'classes' in self.options:
+ classes.extend(self.options['classes'])
+
+ # set up lexical analyzer
+ try:
+ tokens = Lexer('\n'.join(self.content), language,
+ self.state.document.settings.syntax_highlight)
+ except LexerError as error:
+ if self.state.document.settings.report_level > 2:
+ # don't report warnings -> insert without syntax highlight
+ tokens = Lexer('\n'.join(self.content), language, 'none')
+ else:
+ raise self.warning(error)
+
+ if 'number-lines' in self.options:
+ # optional argument `startline`, defaults to 1
+ try:
+ startline = int(self.options['number-lines'] or 1)
+ except ValueError:
+ raise self.error(':number-lines: with non-integer start value')
+ endline = startline + len(self.content)
+ # add linenumber filter:
+ tokens = NumberLines(tokens, startline, endline)
+
+ node = nodes.literal_block('\n'.join(self.content), classes=classes)
+ self.add_name(node)
+ # if called from "include", set the source
+ if 'source' in self.options:
+ node.attributes['source'] = self.options['source']
+ # analyze content and add nodes for every token
+ for classes, value in tokens:
+ if classes:
+ node += nodes.inline(value, value, classes=classes)
+ else:
+ # insert as Text to decrease the verbosity of the output
+ node += nodes.Text(value)
+
+ return [node]
+
+
+class MathBlock(Directive):
+
+ option_spec = {'class': directives.class_option,
+ 'name': directives.unchanged,
+ # TODO: Add Sphinx' ``mathbase.py`` option 'nowrap'?
+ # 'nowrap': directives.flag,
+ }
+ has_content = True
+
+ def run(self):
+ set_classes(self.options)
+ self.assert_has_content()
+ # join lines, separate blocks
+ content = '\n'.join(self.content).split('\n\n')
+ _nodes = []
+ for block in content:
+ if not block:
+ continue
+ node = nodes.math_block(self.block_text, block, **self.options)
+ (node.source,
+ node.line) = self.state_machine.get_source_and_line(self.lineno)
+ self.add_name(node)
+ _nodes.append(node)
+ return _nodes
+
+
+class Rubric(Directive):
+
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec = {'class': directives.class_option,
+ 'name': directives.unchanged}
+
+ def run(self):
+ set_classes(self.options)
+ rubric_text = self.arguments[0]
+ textnodes, messages = self.state.inline_text(rubric_text, self.lineno)
+ rubric = nodes.rubric(rubric_text, '', *textnodes, **self.options)
+ self.add_name(rubric)
+ return [rubric] + messages
+
+
+class BlockQuote(Directive):
+
+ has_content = True
+ classes = []
+
+ def run(self):
+ self.assert_has_content()
+ elements = self.state.block_quote(self.content, self.content_offset)
+ for element in elements:
+ if isinstance(element, nodes.block_quote):
+ element['classes'] += self.classes
+ return elements
+
+
+class Epigraph(BlockQuote):
+
+ classes = ['epigraph']
+
+
+class Highlights(BlockQuote):
+
+ classes = ['highlights']
+
+
+class PullQuote(BlockQuote):
+
+ classes = ['pull-quote']
+
+
+class Compound(Directive):
+
+ option_spec = {'class': directives.class_option,
+ 'name': directives.unchanged}
+ has_content = True
+
+ def run(self):
+ self.assert_has_content()
+ text = '\n'.join(self.content)
+ node = nodes.compound(text)
+ node['classes'] += self.options.get('class', [])
+ self.add_name(node)
+ self.state.nested_parse(self.content, self.content_offset, node)
+ return [node]
+
+
+class Container(Directive):
+
+ optional_arguments = 1
+ final_argument_whitespace = True
+ option_spec = {'name': directives.unchanged}
+ has_content = True
+
+ def run(self):
+ self.assert_has_content()
+ text = '\n'.join(self.content)
+ try:
+ if self.arguments:
+ classes = directives.class_option(self.arguments[0])
+ else:
+ classes = []
+ except ValueError:
+ raise self.error(
+ 'Invalid class attribute value for "%s" directive: "%s".'
+ % (self.name, self.arguments[0]))
+ node = nodes.container(text)
+ node['classes'].extend(classes)
+ self.add_name(node)
+ self.state.nested_parse(self.content, self.content_offset, node)
+ return [node]
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/html.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/html.py
new file mode 100644
index 00000000..c22a26f2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/html.py
@@ -0,0 +1,21 @@
+# $Id: html.py 9062 2022-05-30 21:09:09Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Dummy module for backwards compatibility.
+
+This module is provisional: it will be removed in Docutils 2.0.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import warnings
+
+from docutils.parsers.rst.directives.misc import MetaBody, Meta # noqa: F401
+
+warnings.warn('The `docutils.parsers.rst.directive.html` module'
+ ' will be removed in Docutils 2.0.'
+ ' Since Docutils 0.18, the "Meta" node is defined in'
+ ' `docutils.parsers.rst.directives.misc`.',
+ DeprecationWarning, stacklevel=2)
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/images.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/images.py
new file mode 100644
index 00000000..bcde3a39
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/images.py
@@ -0,0 +1,173 @@
+# $Id: images.py 9500 2023-12-14 22:38:49Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Directives for figures and simple images.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from urllib.request import url2pathname
+
+try: # check for the Python Imaging Library
+ import PIL.Image
+except ImportError:
+ try: # sometimes PIL modules are put in PYTHONPATH's root
+ import Image
+ class PIL: pass # noqa:E701 dummy wrapper
+ PIL.Image = Image
+ except ImportError:
+ PIL = None
+
+from docutils import nodes
+from docutils.nodes import fully_normalize_name, whitespace_normalize_name
+from docutils.parsers.rst import Directive
+from docutils.parsers.rst import directives, states
+from docutils.parsers.rst.roles import set_classes
+
+
+class Image(Directive):
+
+ align_h_values = ('left', 'center', 'right')
+ align_v_values = ('top', 'middle', 'bottom')
+ align_values = align_v_values + align_h_values
+ loading_values = ('embed', 'link', 'lazy')
+
+ def align(argument):
+ # This is not callable as `self.align()`. We cannot make it a
+ # staticmethod because we're saving an unbound method in
+ # option_spec below.
+ return directives.choice(argument, Image.align_values)
+
+ def loading(argument):
+ # This is not callable as `self.loading()` (see above).
+ return directives.choice(argument, Image.loading_values)
+
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec = {'alt': directives.unchanged,
+ 'height': directives.length_or_unitless,
+ 'width': directives.length_or_percentage_or_unitless,
+ 'scale': directives.percentage,
+ 'align': align,
+ 'target': directives.unchanged_required,
+ 'loading': loading,
+ 'class': directives.class_option,
+ 'name': directives.unchanged}
+
+ def run(self):
+ if 'align' in self.options:
+ if isinstance(self.state, states.SubstitutionDef):
+ # Check for align_v_values.
+ if self.options['align'] not in self.align_v_values:
+ raise self.error(
+ 'Error in "%s" directive: "%s" is not a valid value '
+ 'for the "align" option within a substitution '
+ 'definition. Valid values for "align" are: "%s".'
+ % (self.name, self.options['align'],
+ '", "'.join(self.align_v_values)))
+ elif self.options['align'] not in self.align_h_values:
+ raise self.error(
+ 'Error in "%s" directive: "%s" is not a valid value for '
+ 'the "align" option. Valid values for "align" are: "%s".'
+ % (self.name, self.options['align'],
+ '", "'.join(self.align_h_values)))
+ messages = []
+ reference = directives.uri(self.arguments[0])
+ self.options['uri'] = reference
+ reference_node = None
+ if 'target' in self.options:
+ block = states.escape2null(
+ self.options['target']).splitlines()
+ block = [line for line in block]
+ target_type, data = self.state.parse_target(
+ block, self.block_text, self.lineno)
+ if target_type == 'refuri':
+ reference_node = nodes.reference(refuri=data)
+ elif target_type == 'refname':
+ reference_node = nodes.reference(
+ refname=fully_normalize_name(data),
+ name=whitespace_normalize_name(data))
+ reference_node.indirect_reference_name = data
+ self.state.document.note_refname(reference_node)
+ else: # malformed target
+ messages.append(data) # data is a system message
+ del self.options['target']
+ set_classes(self.options)
+ image_node = nodes.image(self.block_text, **self.options)
+ (image_node.source,
+ image_node.line) = self.state_machine.get_source_and_line(self.lineno)
+ self.add_name(image_node)
+ if reference_node:
+ reference_node += image_node
+ return messages + [reference_node]
+ else:
+ return messages + [image_node]
+
+
+class Figure(Image):
+
+ def align(argument):
+ return directives.choice(argument, Figure.align_h_values)
+
+ def figwidth_value(argument):
+ if argument.lower() == 'image':
+ return 'image'
+ else:
+ return directives.length_or_percentage_or_unitless(argument, 'px')
+
+ option_spec = Image.option_spec.copy()
+ option_spec['figwidth'] = figwidth_value
+ option_spec['figclass'] = directives.class_option
+ option_spec['align'] = align
+ has_content = True
+
+ def run(self):
+ figwidth = self.options.pop('figwidth', None)
+ figclasses = self.options.pop('figclass', None)
+ align = self.options.pop('align', None)
+ (image_node,) = Image.run(self)
+ if isinstance(image_node, nodes.system_message):
+ return [image_node]
+ figure_node = nodes.figure('', image_node)
+ (figure_node.source, figure_node.line
+ ) = self.state_machine.get_source_and_line(self.lineno)
+ if figwidth == 'image':
+ if PIL and self.state.document.settings.file_insertion_enabled:
+ imagepath = url2pathname(image_node['uri'])
+ try:
+ with PIL.Image.open(imagepath) as img:
+ figure_node['width'] = '%dpx' % img.size[0]
+ except (OSError, UnicodeEncodeError):
+ pass # TODO: warn/info?
+ else:
+ self.state.document.settings.record_dependencies.add(
+ imagepath.replace('\\', '/'))
+ elif figwidth is not None:
+ figure_node['width'] = figwidth
+ if figclasses:
+ figure_node['classes'] += figclasses
+ if align:
+ figure_node['align'] = align
+ if self.content:
+ node = nodes.Element() # anonymous container for parsing
+ self.state.nested_parse(self.content, self.content_offset, node)
+ first_node = node[0]
+ if isinstance(first_node, nodes.paragraph):
+ caption = nodes.caption(first_node.rawsource, '',
+ *first_node.children)
+ caption.source = first_node.source
+ caption.line = first_node.line
+ figure_node += caption
+ elif not (isinstance(first_node, nodes.comment)
+ and len(first_node) == 0):
+ error = self.reporter.error(
+ 'Figure caption must be a paragraph or empty comment.',
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ return [figure_node, error]
+ if len(node) > 1:
+ figure_node += nodes.legend('', *node[1:])
+ return [figure_node]
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/misc.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/misc.py
new file mode 100644
index 00000000..c16e9430
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/misc.py
@@ -0,0 +1,642 @@
+# $Id: misc.py 9492 2023-11-29 16:58:13Z milde $
+# Authors: David Goodger <goodger@python.org>; Dethe Elza
+# Copyright: This module has been placed in the public domain.
+
+"""Miscellaneous directives."""
+
+__docformat__ = 'reStructuredText'
+
+from pathlib import Path
+import re
+import time
+from urllib.request import urlopen
+from urllib.error import URLError
+
+from docutils import io, nodes, statemachine, utils
+from docutils.parsers.rst import Directive, convert_directive_function
+from docutils.parsers.rst import directives, roles, states
+from docutils.parsers.rst.directives.body import CodeBlock, NumberLines
+from docutils.transforms import misc
+
+
+def adapt_path(path, source='', root_prefix='/'):
+ # Adapt path to files to include or embed.
+ # `root_prefix` is prepended to absolute paths (cf. root_prefix setting),
+ # `source` is the `current_source` of the including directive (which may
+ # be a file included by the main document).
+ if path.startswith('/'):
+ base = Path(root_prefix)
+ path = path[1:]
+ else:
+ base = Path(source).parent
+ # pepend "base" and convert to relative path for shorter system messages
+ return utils.relative_path(None, base/path)
+
+
+class Include(Directive):
+
+ """
+ Include content read from a separate source file.
+
+ Content may be parsed by the parser, or included as a literal
+ block. The encoding of the included file can be specified. Only
+ a part of the given file argument may be included by specifying
+ start and end line or text to match before and/or after the text
+ to be used.
+
+ https://docutils.sourceforge.io/docs/ref/rst/directives.html#including-an-external-document-fragment
+ """
+
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec = {'literal': directives.flag,
+ 'code': directives.unchanged,
+ 'encoding': directives.encoding,
+ 'parser': directives.parser_name,
+ 'tab-width': int,
+ 'start-line': int,
+ 'end-line': int,
+ 'start-after': directives.unchanged_required,
+ 'end-before': directives.unchanged_required,
+ # ignored except for 'literal' or 'code':
+ 'number-lines': directives.unchanged, # integer or None
+ 'class': directives.class_option,
+ 'name': directives.unchanged}
+
+ standard_include_path = Path(states.__file__).parent / 'include'
+
+ def run(self):
+ """Include a file as part of the content of this reST file.
+
+ Depending on the options, the file (or a clipping) is
+ converted to nodes and returned or inserted into the input stream.
+ """
+ settings = self.state.document.settings
+ if not settings.file_insertion_enabled:
+ raise self.warning('"%s" directive disabled.' % self.name)
+ tab_width = self.options.get('tab-width', settings.tab_width)
+ current_source = self.state.document.current_source
+ path = directives.path(self.arguments[0])
+ if path.startswith('<') and path.endswith('>'):
+ path = '/' + path[1:-1]
+ root_prefix = self.standard_include_path
+ else:
+ root_prefix = settings.root_prefix
+ path = adapt_path(path, current_source, root_prefix)
+ encoding = self.options.get('encoding', settings.input_encoding)
+ error_handler = settings.input_encoding_error_handler
+ try:
+ include_file = io.FileInput(source_path=path,
+ encoding=encoding,
+ error_handler=error_handler)
+ except UnicodeEncodeError:
+ raise self.severe(f'Problems with "{self.name}" directive path:\n'
+ f'Cannot encode input file path "{path}" '
+ '(wrong locale?).')
+ except OSError as error:
+ raise self.severe(f'Problems with "{self.name}" directive '
+ f'path:\n{io.error_string(error)}.')
+ else:
+ settings.record_dependencies.add(path)
+
+ # Get to-be-included content
+ startline = self.options.get('start-line', None)
+ endline = self.options.get('end-line', None)
+ try:
+ if startline or (endline is not None):
+ lines = include_file.readlines()
+ rawtext = ''.join(lines[startline:endline])
+ else:
+ rawtext = include_file.read()
+ except UnicodeError as error:
+ raise self.severe(f'Problem with "{self.name}" directive:\n'
+ + io.error_string(error))
+ # start-after/end-before: no restrictions on newlines in match-text,
+ # and no restrictions on matching inside lines vs. line boundaries
+ after_text = self.options.get('start-after', None)
+ if after_text:
+ # skip content in rawtext before *and incl.* a matching text
+ after_index = rawtext.find(after_text)
+ if after_index < 0:
+ raise self.severe('Problem with "start-after" option of "%s" '
+ 'directive:\nText not found.' % self.name)
+ rawtext = rawtext[after_index + len(after_text):]
+ before_text = self.options.get('end-before', None)
+ if before_text:
+ # skip content in rawtext after *and incl.* a matching text
+ before_index = rawtext.find(before_text)
+ if before_index < 0:
+ raise self.severe('Problem with "end-before" option of "%s" '
+ 'directive:\nText not found.' % self.name)
+ rawtext = rawtext[:before_index]
+
+ include_lines = statemachine.string2lines(rawtext, tab_width,
+ convert_whitespace=True)
+ for i, line in enumerate(include_lines):
+ if len(line) > settings.line_length_limit:
+ raise self.warning('"%s": line %d exceeds the'
+ ' line-length-limit.' % (path, i+1))
+
+ if 'literal' in self.options:
+ # Don't convert tabs to spaces, if `tab_width` is negative.
+ if tab_width >= 0:
+ text = rawtext.expandtabs(tab_width)
+ else:
+ text = rawtext
+ literal_block = nodes.literal_block(
+ rawtext, source=path,
+ classes=self.options.get('class', []))
+ literal_block.line = 1
+ self.add_name(literal_block)
+ if 'number-lines' in self.options:
+ try:
+ startline = int(self.options['number-lines'] or 1)
+ except ValueError:
+ raise self.error(':number-lines: with non-integer '
+ 'start value')
+ endline = startline + len(include_lines)
+ if text.endswith('\n'):
+ text = text[:-1]
+ tokens = NumberLines([([], text)], startline, endline)
+ for classes, value in tokens:
+ if classes:
+ literal_block += nodes.inline(value, value,
+ classes=classes)
+ else:
+ literal_block += nodes.Text(value)
+ else:
+ literal_block += nodes.Text(text)
+ return [literal_block]
+
+ if 'code' in self.options:
+ self.options['source'] = path
+ # Don't convert tabs to spaces, if `tab_width` is negative:
+ if tab_width < 0:
+ include_lines = rawtext.splitlines()
+ codeblock = CodeBlock(self.name,
+ [self.options.pop('code')], # arguments
+ self.options,
+ include_lines, # content
+ self.lineno,
+ self.content_offset,
+ self.block_text,
+ self.state,
+ self.state_machine)
+ return codeblock.run()
+
+ # Prevent circular inclusion:
+ clip_options = (startline, endline, before_text, after_text)
+ include_log = self.state.document.include_log
+ # log entries are tuples (<source>, <clip-options>)
+ if not include_log: # new document, initialize with document source
+ include_log.append((utils.relative_path(None, current_source),
+ (None, None, None, None)))
+ if (path, clip_options) in include_log:
+ master_paths = (pth for (pth, opt) in reversed(include_log))
+ inclusion_chain = '\n> '.join((path, *master_paths))
+ raise self.warning('circular inclusion in "%s" directive:\n%s'
+ % (self.name, inclusion_chain))
+
+ if 'parser' in self.options:
+ # parse into a dummy document and return created nodes
+ document = utils.new_document(path, settings)
+ document.include_log = include_log + [(path, clip_options)]
+ parser = self.options['parser']()
+ parser.parse('\n'.join(include_lines), document)
+ # clean up doctree and complete parsing
+ document.transformer.populate_from_components((parser,))
+ document.transformer.apply_transforms()
+ return document.children
+
+ # Include as rST source:
+ #
+ # mark end (cf. parsers.rst.states.Body.comment())
+ include_lines += ['', '.. end of inclusion from "%s"' % path]
+ self.state_machine.insert_input(include_lines, path)
+ # update include-log
+ include_log.append((path, clip_options))
+ return []
+
+
+class Raw(Directive):
+
+ """
+ Pass through content unchanged
+
+ Content is included in output based on type argument
+
+ Content may be included inline (content section of directive) or
+ imported from a file or url.
+ """
+
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec = {'file': directives.path,
+ 'url': directives.uri,
+ 'encoding': directives.encoding,
+ 'class': directives.class_option}
+ has_content = True
+
+ def run(self):
+ settings = self.state.document.settings
+ if (not settings.raw_enabled
+ or (not settings.file_insertion_enabled
+ and ('file' in self.options or 'url' in self.options))):
+ raise self.warning('"%s" directive disabled.' % self.name)
+ attributes = {'format': ' '.join(self.arguments[0].lower().split())}
+ encoding = self.options.get('encoding', settings.input_encoding)
+ error_handler = settings.input_encoding_error_handler
+ if self.content:
+ if 'file' in self.options or 'url' in self.options:
+ raise self.error(
+ '"%s" directive may not both specify an external file '
+ 'and have content.' % self.name)
+ text = '\n'.join(self.content)
+ elif 'file' in self.options:
+ if 'url' in self.options:
+ raise self.error(
+ 'The "file" and "url" options may not be simultaneously '
+ 'specified for the "%s" directive.' % self.name)
+ path = adapt_path(self.options['file'],
+ self.state.document.current_source,
+ settings.root_prefix)
+ try:
+ raw_file = io.FileInput(source_path=path,
+ encoding=encoding,
+ error_handler=error_handler)
+ except OSError as error:
+ raise self.severe(f'Problems with "{self.name}" directive '
+ f'path:\n{io.error_string(error)}.')
+ else:
+ # TODO: currently, raw input files are recorded as
+ # dependencies even if not used for the chosen output format.
+ settings.record_dependencies.add(path)
+ try:
+ text = raw_file.read()
+ except UnicodeError as error:
+ raise self.severe(f'Problem with "{self.name}" directive:\n'
+ + io.error_string(error))
+ attributes['source'] = path
+ elif 'url' in self.options:
+ source = self.options['url']
+ try:
+ raw_text = urlopen(source).read()
+ except (URLError, OSError) as error:
+ raise self.severe(f'Problems with "{self.name}" directive URL '
+ f'"{self.options["url"]}":\n'
+ f'{io.error_string(error)}.')
+ raw_file = io.StringInput(source=raw_text, source_path=source,
+ encoding=encoding,
+ error_handler=error_handler)
+ try:
+ text = raw_file.read()
+ except UnicodeError as error:
+ raise self.severe(f'Problem with "{self.name}" directive:\n'
+ + io.error_string(error))
+ attributes['source'] = source
+ else:
+ # This will always fail because there is no content.
+ self.assert_has_content()
+ raw_node = nodes.raw('', text, classes=self.options.get('class', []),
+ **attributes)
+ (raw_node.source,
+ raw_node.line) = self.state_machine.get_source_and_line(self.lineno)
+ return [raw_node]
+
+
+class Replace(Directive):
+
+ has_content = True
+
+ def run(self):
+ if not isinstance(self.state, states.SubstitutionDef):
+ raise self.error(
+ 'Invalid context: the "%s" directive can only be used within '
+ 'a substitution definition.' % self.name)
+ self.assert_has_content()
+ text = '\n'.join(self.content)
+ element = nodes.Element(text)
+ self.state.nested_parse(self.content, self.content_offset,
+ element)
+ # element might contain [paragraph] + system_message(s)
+ node = None
+ messages = []
+ for elem in element:
+ if not node and isinstance(elem, nodes.paragraph):
+ node = elem
+ elif isinstance(elem, nodes.system_message):
+ elem['backrefs'] = []
+ messages.append(elem)
+ else:
+ return [
+ self.reporter.error(
+ f'Error in "{self.name}" directive: may contain '
+ 'a single paragraph only.', line=self.lineno)]
+ if node:
+ return messages + node.children
+ return messages
+
+
+class Unicode(Directive):
+
+ r"""
+ Convert Unicode character codes (numbers) to characters. Codes may be
+ decimal numbers, hexadecimal numbers (prefixed by ``0x``, ``x``, ``\x``,
+ ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style numeric character
+ entities (e.g. ``&#x262E;``). Text following ".." is a comment and is
+ ignored. Spaces are ignored, and any other text remains as-is.
+ """
+
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec = {'trim': directives.flag,
+ 'ltrim': directives.flag,
+ 'rtrim': directives.flag}
+
+ comment_pattern = re.compile(r'( |\n|^)\.\. ')
+
+ def run(self):
+ if not isinstance(self.state, states.SubstitutionDef):
+ raise self.error(
+ 'Invalid context: the "%s" directive can only be used within '
+ 'a substitution definition.' % self.name)
+ substitution_definition = self.state_machine.node
+ if 'trim' in self.options:
+ substitution_definition.attributes['ltrim'] = 1
+ substitution_definition.attributes['rtrim'] = 1
+ if 'ltrim' in self.options:
+ substitution_definition.attributes['ltrim'] = 1
+ if 'rtrim' in self.options:
+ substitution_definition.attributes['rtrim'] = 1
+ codes = self.comment_pattern.split(self.arguments[0])[0].split()
+ element = nodes.Element()
+ for code in codes:
+ try:
+ decoded = directives.unicode_code(code)
+ except ValueError as error:
+ raise self.error('Invalid character code: %s\n%s'
+ % (code, io.error_string(error)))
+ element += nodes.Text(decoded)
+ return element.children
+
+
+class Class(Directive):
+
+ """
+ Set a "class" attribute on the directive content or the next element.
+ When applied to the next element, a "pending" element is inserted, and a
+ transform does the work later.
+ """
+
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ has_content = True
+
+ def run(self):
+ try:
+ class_value = directives.class_option(self.arguments[0])
+ except ValueError:
+ raise self.error(
+ 'Invalid class attribute value for "%s" directive: "%s".'
+ % (self.name, self.arguments[0]))
+ node_list = []
+ if self.content:
+ container = nodes.Element()
+ self.state.nested_parse(self.content, self.content_offset,
+ container)
+ for node in container:
+ node['classes'].extend(class_value)
+ node_list.extend(container.children)
+ else:
+ pending = nodes.pending(
+ misc.ClassAttribute,
+ {'class': class_value, 'directive': self.name},
+ self.block_text)
+ self.state_machine.document.note_pending(pending)
+ node_list.append(pending)
+ return node_list
+
+
+class Role(Directive):
+
+ has_content = True
+
+ argument_pattern = re.compile(r'(%s)\s*(\(\s*(%s)\s*\)\s*)?$'
+ % ((states.Inliner.simplename,) * 2))
+
+ def run(self):
+ """Dynamically create and register a custom interpreted text role."""
+ if self.content_offset > self.lineno or not self.content:
+ raise self.error('"%s" directive requires arguments on the first '
+ 'line.' % self.name)
+ args = self.content[0]
+ match = self.argument_pattern.match(args)
+ if not match:
+ raise self.error('"%s" directive arguments not valid role names: '
+ '"%s".' % (self.name, args))
+ new_role_name = match.group(1)
+ base_role_name = match.group(3)
+ messages = []
+ if base_role_name:
+ base_role, messages = roles.role(
+ base_role_name, self.state_machine.language, self.lineno,
+ self.state.reporter)
+ if base_role is None:
+ error = self.state.reporter.error(
+ 'Unknown interpreted text role "%s".' % base_role_name,
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ return messages + [error]
+ else:
+ base_role = roles.generic_custom_role
+ assert not hasattr(base_role, 'arguments'), (
+ 'Supplemental directive arguments for "%s" directive not '
+ 'supported (specified by "%r" role).' % (self.name, base_role))
+ try:
+ converted_role = convert_directive_function(base_role)
+ (arguments, options, content, content_offset
+ ) = self.state.parse_directive_block(
+ self.content[1:], self.content_offset,
+ converted_role, option_presets={})
+ except states.MarkupError as detail:
+ error = self.reporter.error(
+ 'Error in "%s" directive:\n%s.' % (self.name, detail),
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ return messages + [error]
+ if 'class' not in options:
+ try:
+ options['class'] = directives.class_option(new_role_name)
+ except ValueError as detail:
+ error = self.reporter.error(
+ 'Invalid argument for "%s" directive:\n%s.'
+ % (self.name, detail),
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ return messages + [error]
+ role = roles.CustomRole(new_role_name, base_role, options, content)
+ roles.register_local_role(new_role_name, role)
+ return messages
+
+
+class DefaultRole(Directive):
+
+ """Set the default interpreted text role."""
+
+ optional_arguments = 1
+ final_argument_whitespace = False
+
+ def run(self):
+ if not self.arguments:
+ if '' in roles._roles:
+ # restore the "default" default role
+ del roles._roles['']
+ return []
+ role_name = self.arguments[0]
+ role, messages = roles.role(role_name, self.state_machine.language,
+ self.lineno, self.state.reporter)
+ if role is None:
+ error = self.state.reporter.error(
+ 'Unknown interpreted text role "%s".' % role_name,
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ return messages + [error]
+ roles._roles[''] = role
+ return messages
+
+
+class Title(Directive):
+
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+
+ def run(self):
+ self.state_machine.document['title'] = self.arguments[0]
+ return []
+
+
+class MetaBody(states.SpecializedBody):
+
+ def field_marker(self, match, context, next_state):
+ """Meta element."""
+ node, blank_finish = self.parsemeta(match)
+ self.parent += node
+ return [], next_state, []
+
+ def parsemeta(self, match):
+ name = self.parse_field_marker(match)
+ name = nodes.unescape(utils.escape2null(name))
+ (indented, indent, line_offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end())
+ node = nodes.meta()
+ node['content'] = nodes.unescape(utils.escape2null(
+ ' '.join(indented)))
+ if not indented:
+ line = self.state_machine.line
+ msg = self.reporter.info(
+ 'No content for meta tag "%s".' % name,
+ nodes.literal_block(line, line))
+ return msg, blank_finish
+ tokens = name.split()
+ try:
+ attname, val = utils.extract_name_value(tokens[0])[0]
+ node[attname.lower()] = val
+ except utils.NameValueError:
+ node['name'] = tokens[0]
+ for token in tokens[1:]:
+ try:
+ attname, val = utils.extract_name_value(token)[0]
+ node[attname.lower()] = val
+ except utils.NameValueError as detail:
+ line = self.state_machine.line
+ msg = self.reporter.error(
+ 'Error parsing meta tag attribute "%s": %s.'
+ % (token, detail), nodes.literal_block(line, line))
+ return msg, blank_finish
+ return node, blank_finish
+
+
+class Meta(Directive):
+
+ has_content = True
+
+ SMkwargs = {'state_classes': (MetaBody,)}
+
+ def run(self):
+ self.assert_has_content()
+ node = nodes.Element()
+ new_line_offset, blank_finish = self.state.nested_list_parse(
+ self.content, self.content_offset, node,
+ initial_state='MetaBody', blank_finish=True,
+ state_machine_kwargs=self.SMkwargs)
+ if (new_line_offset - self.content_offset) != len(self.content):
+ # incomplete parse of block?
+ error = self.reporter.error(
+ 'Invalid meta directive.',
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ node += error
+ # insert at begin of document
+ index = self.state.document.first_child_not_matching_class(
+ (nodes.Titular, nodes.meta)) or 0
+ self.state.document[index:index] = node.children
+ return []
+
+
+class Date(Directive):
+
+ has_content = True
+
+ def run(self):
+ if not isinstance(self.state, states.SubstitutionDef):
+ raise self.error(
+ 'Invalid context: the "%s" directive can only be used within '
+ 'a substitution definition.' % self.name)
+ format_str = '\n'.join(self.content) or '%Y-%m-%d'
+ # @@@
+ # Use timestamp from the `SOURCE_DATE_EPOCH`_ environment variable?
+ # Pro: Docutils-generated documentation
+ # can easily be part of `reproducible software builds`__
+ #
+ # __ https://reproducible-builds.org/
+ #
+ # Con: Changes the specs, hard to predict behaviour,
+ #
+ # See also the discussion about \date \time \year in TeX
+ # http://tug.org/pipermail/tex-k/2016-May/002704.html
+ # source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH')
+ # if (source_date_epoch):
+ # text = time.strftime(format_str,
+ # time.gmtime(int(source_date_epoch)))
+ # else:
+ text = time.strftime(format_str)
+ return [nodes.Text(text)]
+
+
+class TestDirective(Directive):
+
+ """This directive is useful only for testing purposes."""
+
+ optional_arguments = 1
+ final_argument_whitespace = True
+ option_spec = {'option': directives.unchanged_required}
+ has_content = True
+
+ def run(self):
+ if self.content:
+ text = '\n'.join(self.content)
+ info = self.reporter.info(
+ 'Directive processed. Type="%s", arguments=%r, options=%r, '
+ 'content:' % (self.name, self.arguments, self.options),
+ nodes.literal_block(text, text), line=self.lineno)
+ else:
+ info = self.reporter.info(
+ 'Directive processed. Type="%s", arguments=%r, options=%r, '
+ 'content: None' % (self.name, self.arguments, self.options),
+ line=self.lineno)
+ return [info]
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/parts.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/parts.py
new file mode 100644
index 00000000..adb01d03
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/parts.py
@@ -0,0 +1,126 @@
+# $Id: parts.py 8993 2022-01-29 13:20:04Z milde $
+# Authors: David Goodger <goodger@python.org>; Dmitry Jemerov
+# Copyright: This module has been placed in the public domain.
+
+"""
+Directives for document parts.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from docutils import nodes, languages
+from docutils.transforms import parts
+from docutils.parsers.rst import Directive
+from docutils.parsers.rst import directives
+
+
+class Contents(Directive):
+
+ """
+ Table of contents.
+
+ The table of contents is generated in two passes: initial parse and
+ transform. During the initial parse, a 'pending' element is generated
+ which acts as a placeholder, storing the TOC title and any options
+ internally. At a later stage in the processing, the 'pending' element is
+ replaced by a 'topic' element, a title and the table of contents proper.
+ """
+
+ backlinks_values = ('top', 'entry', 'none')
+
+ def backlinks(arg):
+ value = directives.choice(arg, Contents.backlinks_values)
+ if value == 'none':
+ return None
+ else:
+ return value
+
+ optional_arguments = 1
+ final_argument_whitespace = True
+ option_spec = {'depth': directives.nonnegative_int,
+ 'local': directives.flag,
+ 'backlinks': backlinks,
+ 'class': directives.class_option}
+
+ def run(self):
+ if not (self.state_machine.match_titles
+ or isinstance(self.state_machine.node, nodes.sidebar)):
+ raise self.error('The "%s" directive may not be used within '
+ 'topics or body elements.' % self.name)
+ document = self.state_machine.document
+ language = languages.get_language(document.settings.language_code,
+ document.reporter)
+ if self.arguments:
+ title_text = self.arguments[0]
+ text_nodes, messages = self.state.inline_text(title_text,
+ self.lineno)
+ title = nodes.title(title_text, '', *text_nodes)
+ else:
+ messages = []
+ if 'local' in self.options:
+ title = None
+ else:
+ title = nodes.title('', language.labels['contents'])
+ topic = nodes.topic(classes=['contents'])
+ topic['classes'] += self.options.get('class', [])
+ # the latex2e writer needs source and line for a warning:
+ topic.source, topic.line = self.state_machine.get_source_and_line()
+ topic.line -= 1
+ if 'local' in self.options:
+ topic['classes'].append('local')
+ if title:
+ name = title.astext()
+ topic += title
+ else:
+ name = language.labels['contents']
+ name = nodes.fully_normalize_name(name)
+ if not document.has_name(name):
+ topic['names'].append(name)
+ document.note_implicit_target(topic)
+ pending = nodes.pending(parts.Contents, rawsource=self.block_text)
+ pending.details.update(self.options)
+ document.note_pending(pending)
+ topic += pending
+ return [topic] + messages
+
+
+class Sectnum(Directive):
+
+ """Automatic section numbering."""
+
+ option_spec = {'depth': int,
+ 'start': int,
+ 'prefix': directives.unchanged_required,
+ 'suffix': directives.unchanged_required}
+
+ def run(self):
+ pending = nodes.pending(parts.SectNum)
+ pending.details.update(self.options)
+ self.state_machine.document.note_pending(pending)
+ return [pending]
+
+
+class Header(Directive):
+
+ """Contents of document header."""
+
+ has_content = True
+
+ def run(self):
+ self.assert_has_content()
+ header = self.state_machine.document.get_decoration().get_header()
+ self.state.nested_parse(self.content, self.content_offset, header)
+ return []
+
+
+class Footer(Directive):
+
+ """Contents of document footer."""
+
+ has_content = True
+
+ def run(self):
+ self.assert_has_content()
+ footer = self.state_machine.document.get_decoration().get_footer()
+ self.state.nested_parse(self.content, self.content_offset, footer)
+ return []
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/references.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/references.py
new file mode 100644
index 00000000..96921f9d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/references.py
@@ -0,0 +1,29 @@
+# $Id: references.py 7062 2011-06-30 22:14:29Z milde $
+# Authors: David Goodger <goodger@python.org>; Dmitry Jemerov
+# Copyright: This module has been placed in the public domain.
+
+"""
+Directives for references and targets.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from docutils import nodes
+from docutils.transforms import references
+from docutils.parsers.rst import Directive
+from docutils.parsers.rst import directives
+
+
+class TargetNotes(Directive):
+
+ """Target footnote generation."""
+
+ option_spec = {'class': directives.class_option,
+ 'name': directives.unchanged}
+
+ def run(self):
+ pending = nodes.pending(references.TargetNotes)
+ self.add_name(pending)
+ pending.details.update(self.options)
+ self.state_machine.document.note_pending(pending)
+ return [pending]
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/tables.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/tables.py
new file mode 100644
index 00000000..2cc266ff
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/directives/tables.py
@@ -0,0 +1,538 @@
+# $Id: tables.py 9492 2023-11-29 16:58:13Z milde $
+# Authors: David Goodger <goodger@python.org>; David Priest
+# Copyright: This module has been placed in the public domain.
+
+"""
+Directives for table elements.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import csv
+from urllib.request import urlopen
+from urllib.error import URLError
+import warnings
+
+from docutils import nodes, statemachine
+from docutils.io import FileInput, StringInput
+from docutils.parsers.rst import Directive
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.directives.misc import adapt_path
+from docutils.utils import SystemMessagePropagation
+
+
+def align(argument):
+ return directives.choice(argument, ('left', 'center', 'right'))
+
+
+class Table(Directive):
+
+ """
+ Generic table base class.
+ """
+
+ optional_arguments = 1
+ final_argument_whitespace = True
+ option_spec = {'class': directives.class_option,
+ 'name': directives.unchanged,
+ 'align': align,
+ 'width': directives.length_or_percentage_or_unitless,
+ 'widths': directives.value_or(('auto', 'grid'),
+ directives.positive_int_list)}
+ has_content = True
+
+ def make_title(self):
+ if self.arguments:
+ title_text = self.arguments[0]
+ text_nodes, messages = self.state.inline_text(title_text,
+ self.lineno)
+ title = nodes.title(title_text, '', *text_nodes)
+ (title.source,
+ title.line) = self.state_machine.get_source_and_line(self.lineno)
+ else:
+ title = None
+ messages = []
+ return title, messages
+
+ def check_table_dimensions(self, rows, header_rows, stub_columns):
+ if len(rows) < header_rows:
+ error = self.reporter.error('%s header row(s) specified but '
+ 'only %s row(s) of data supplied ("%s" directive).'
+ % (header_rows, len(rows), self.name),
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+ if len(rows) == header_rows > 0:
+ error = self.reporter.error(
+ f'Insufficient data supplied ({len(rows)} row(s)); '
+ 'no data remaining for table body, '
+ f'required by "{self.name}" directive.',
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+ for row in rows:
+ if len(row) < stub_columns:
+ error = self.reporter.error(
+ f'{stub_columns} stub column(s) specified '
+ f'but only {len(row)} columns(s) of data supplied '
+ f'("{self.name}" directive).',
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+ if len(row) == stub_columns > 0:
+ error = self.reporter.error(
+ 'Insufficient data supplied (%s columns(s)); '
+ 'no data remaining for table body, required '
+ 'by "%s" directive.' % (len(row), self.name),
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+
+ def set_table_width(self, table_node):
+ if 'width' in self.options:
+ table_node['width'] = self.options.get('width')
+
+ @property
+ def widths(self):
+ return self.options.get('widths', '')
+
+ def get_column_widths(self, n_cols):
+ if isinstance(self.widths, list):
+ if len(self.widths) != n_cols:
+ # TODO: use last value for missing columns?
+ error = self.reporter.error('"%s" widths do not match the '
+ 'number of columns in table (%s).' % (self.name, n_cols),
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+ col_widths = self.widths
+ elif n_cols:
+ col_widths = [100 // n_cols] * n_cols
+ else:
+ error = self.reporter.error('No table data detected in CSV file.',
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+ return col_widths
+
+ def extend_short_rows_with_empty_cells(self, columns, parts):
+ for part in parts:
+ for row in part:
+ if len(row) < columns:
+ row.extend([(0, 0, 0, [])] * (columns - len(row)))
+
+
+class RSTTable(Table):
+ """
+ Class for the `"table" directive`__ for formal tables using rST syntax.
+
+ __ https://docutils.sourceforge.io/docs/ref/rst/directives.html
+ """
+
+ def run(self):
+ if not self.content:
+ warning = self.reporter.warning('Content block expected '
+ 'for the "%s" directive; none found.' % self.name,
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ return [warning]
+ title, messages = self.make_title()
+ node = nodes.Element() # anonymous container for parsing
+ self.state.nested_parse(self.content, self.content_offset, node)
+ if len(node) != 1 or not isinstance(node[0], nodes.table):
+ error = self.reporter.error('Error parsing content block for the '
+ '"%s" directive: exactly one table expected.' % self.name,
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ return [error]
+ table_node = node[0]
+ table_node['classes'] += self.options.get('class', [])
+ self.set_table_width(table_node)
+ if 'align' in self.options:
+ table_node['align'] = self.options.get('align')
+ if isinstance(self.widths, list):
+ tgroup = table_node[0]
+ try:
+ col_widths = self.get_column_widths(tgroup["cols"])
+ except SystemMessagePropagation as detail:
+ return [detail.args[0]]
+ colspecs = [child for child in tgroup.children
+ if child.tagname == 'colspec']
+ for colspec, col_width in zip(colspecs, col_widths):
+ colspec['colwidth'] = col_width
+ if self.widths == 'auto':
+ table_node['classes'] += ['colwidths-auto']
+ elif self.widths: # "grid" or list of integers
+ table_node['classes'] += ['colwidths-given']
+ self.add_name(table_node)
+ if title:
+ table_node.insert(0, title)
+ return [table_node] + messages
+
+
+class CSVTable(Table):
+
+ option_spec = {'header-rows': directives.nonnegative_int,
+ 'stub-columns': directives.nonnegative_int,
+ 'header': directives.unchanged,
+ 'width': directives.length_or_percentage_or_unitless,
+ 'widths': directives.value_or(('auto', ),
+ directives.positive_int_list),
+ 'file': directives.path,
+ 'url': directives.uri,
+ 'encoding': directives.encoding,
+ 'class': directives.class_option,
+ 'name': directives.unchanged,
+ 'align': align,
+ # field delimiter char
+ 'delim': directives.single_char_or_whitespace_or_unicode,
+ # treat whitespace after delimiter as significant
+ 'keepspace': directives.flag,
+ # text field quote/unquote char:
+ 'quote': directives.single_char_or_unicode,
+ # char used to escape delim & quote as-needed:
+ 'escape': directives.single_char_or_unicode}
+
+ class DocutilsDialect(csv.Dialect):
+
+ """CSV dialect for `csv_table` directive."""
+
+ delimiter = ','
+ quotechar = '"'
+ doublequote = True
+ skipinitialspace = True
+ strict = True
+ lineterminator = '\n'
+ quoting = csv.QUOTE_MINIMAL
+
+ def __init__(self, options):
+ if 'delim' in options:
+ self.delimiter = options['delim']
+ if 'keepspace' in options:
+ self.skipinitialspace = False
+ if 'quote' in options:
+ self.quotechar = options['quote']
+ if 'escape' in options:
+ self.doublequote = False
+ self.escapechar = options['escape']
+ super().__init__()
+
+ class HeaderDialect(csv.Dialect):
+ """
+ CSV dialect used for the "header" option data.
+
+ Deprecated. Will be removed in Docutils 0.22.
+ """
+ # The separate HeaderDialect was introduced in revision 2294
+ # (2004-06-17) in the sandbox before the "csv-table" directive moved
+ # to the trunk in r2309. Discussion in docutils-devel around this time
+ # did not mention a rationale (part of the discussion was in private
+ # mail).
+ # This is in conflict with the documentation, which always said:
+ # "Must use the same CSV format as the main CSV data."
+ # and did not change in this aspect.
+ #
+ # Maybe it was intended to have similar escape rules for rST and CSV,
+ # however with the current implementation this means we need
+ # `\\` for rST markup and ``\\\\`` for a literal backslash
+ # in the "option" header but ``\`` and ``\\`` in the header-lines and
+ # table cells of the main CSV data.
+ delimiter = ','
+ quotechar = '"'
+ escapechar = '\\'
+ doublequote = False
+ skipinitialspace = True
+ strict = True
+ lineterminator = '\n'
+ quoting = csv.QUOTE_MINIMAL
+
+ def __init__(self):
+ warnings.warn('CSVTable.HeaderDialect will be removed '
+ 'in Docutils 0.22.',
+ PendingDeprecationWarning, stacklevel=2)
+ super().__init__()
+
+ @staticmethod
+ def check_requirements():
+ warnings.warn('CSVTable.check_requirements()'
+ ' is not required with Python 3'
+ ' and will be removed in Docutils 0.22.',
+ DeprecationWarning, stacklevel=2)
+
+ def process_header_option(self):
+ source = self.state_machine.get_source(self.lineno - 1)
+ table_head = []
+ max_header_cols = 0
+ if 'header' in self.options: # separate table header in option
+ rows, max_header_cols = self.parse_csv_data_into_rows(
+ self.options['header'].split('\n'),
+ self.DocutilsDialect(self.options),
+ source)
+ table_head.extend(rows)
+ return table_head, max_header_cols
+
+ def run(self):
+ try:
+ if (not self.state.document.settings.file_insertion_enabled
+ and ('file' in self.options
+ or 'url' in self.options)):
+ warning = self.reporter.warning('File and URL access '
+ 'deactivated; ignoring "%s" directive.' % self.name,
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ return [warning]
+ title, messages = self.make_title()
+ csv_data, source = self.get_csv_data()
+ table_head, max_header_cols = self.process_header_option()
+ rows, max_cols = self.parse_csv_data_into_rows(
+ csv_data, self.DocutilsDialect(self.options), source)
+ max_cols = max(max_cols, max_header_cols)
+ header_rows = self.options.get('header-rows', 0)
+ stub_columns = self.options.get('stub-columns', 0)
+ self.check_table_dimensions(rows, header_rows, stub_columns)
+ table_head.extend(rows[:header_rows])
+ table_body = rows[header_rows:]
+ col_widths = self.get_column_widths(max_cols)
+ self.extend_short_rows_with_empty_cells(max_cols,
+ (table_head, table_body))
+ except SystemMessagePropagation as detail:
+ return [detail.args[0]]
+ except csv.Error as detail:
+ message = str(detail)
+ error = self.reporter.error('Error with CSV data'
+ ' in "%s" directive:\n%s' % (self.name, message),
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ return [error]
+ table = (col_widths, table_head, table_body)
+ table_node = self.state.build_table(table, self.content_offset,
+ stub_columns, widths=self.widths)
+ table_node['classes'] += self.options.get('class', [])
+ if 'align' in self.options:
+ table_node['align'] = self.options.get('align')
+ self.set_table_width(table_node)
+ self.add_name(table_node)
+ if title:
+ table_node.insert(0, title)
+ return [table_node] + messages
+
+ def get_csv_data(self):
+ """
+ Get CSV data from the directive content, from an external
+ file, or from a URL reference.
+ """
+ settings = self.state.document.settings
+ encoding = self.options.get('encoding', settings.input_encoding)
+ error_handler = settings.input_encoding_error_handler
+ if self.content:
+ # CSV data is from directive content.
+ if 'file' in self.options or 'url' in self.options:
+ error = self.reporter.error('"%s" directive may not both '
+ 'specify an external file and have content.' % self.name,
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+ source = self.content.source(0)
+ csv_data = self.content
+ elif 'file' in self.options:
+ # CSV data is from an external file.
+ if 'url' in self.options:
+ error = self.reporter.error('The "file" and "url" options '
+ 'may not be simultaneously specified '
+ 'for the "%s" directive.' % self.name,
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+ source = adapt_path(self.options['file'],
+ self.state.document.current_source,
+ settings.root_prefix)
+ try:
+ csv_file = FileInput(source_path=source,
+ encoding=encoding,
+ error_handler=error_handler)
+ csv_data = csv_file.read().splitlines()
+ except OSError as error:
+ severe = self.reporter.severe(
+ 'Problems with "%s" directive path:\n%s.'
+ % (self.name, error),
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(severe)
+ else:
+ settings.record_dependencies.add(source)
+ elif 'url' in self.options:
+ source = self.options['url']
+ try:
+ with urlopen(source) as response:
+ csv_text = response.read()
+ except (URLError, OSError, ValueError) as error:
+ severe = self.reporter.severe(
+ 'Problems with "%s" directive URL "%s":\n%s.'
+ % (self.name, self.options['url'], error),
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(severe)
+ csv_file = StringInput(source=csv_text, source_path=source,
+ encoding=encoding,
+ error_handler=error_handler)
+ csv_data = csv_file.read().splitlines()
+ else:
+ error = self.reporter.warning(
+ 'The "%s" directive requires content; none supplied.'
+ % self.name,
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+ return csv_data, source
+
+ @staticmethod
+ def decode_from_csv(s):
+ warnings.warn('CSVTable.decode_from_csv()'
+ ' is not required with Python 3'
+ ' and will be removed in Docutils 0.21 or later.',
+ DeprecationWarning, stacklevel=2)
+ return s
+
+ @staticmethod
+ def encode_for_csv(s):
+ warnings.warn('CSVTable.encode_from_csv()'
+ ' is not required with Python 3'
+ ' and will be removed in Docutils 0.21 or later.',
+ DeprecationWarning, stacklevel=2)
+ return s
+
+ def parse_csv_data_into_rows(self, csv_data, dialect, source):
+ csv_reader = csv.reader((line + '\n' for line in csv_data),
+ dialect=dialect)
+ rows = []
+ max_cols = 0
+ for row in csv_reader:
+ row_data = []
+ for cell in row:
+ cell_data = (0, 0, 0, statemachine.StringList(
+ cell.splitlines(), source=source))
+ row_data.append(cell_data)
+ rows.append(row_data)
+ max_cols = max(max_cols, len(row))
+ return rows, max_cols
+
+
+class ListTable(Table):
+
+ """
+ Implement tables whose data is encoded as a uniform two-level bullet list.
+ For further ideas, see
+ https://docutils.sourceforge.io/docs/dev/rst/alternatives.html#list-driven-tables
+ """
+
+ option_spec = {'header-rows': directives.nonnegative_int,
+ 'stub-columns': directives.nonnegative_int,
+ 'width': directives.length_or_percentage_or_unitless,
+ 'widths': directives.value_or(('auto', ),
+ directives.positive_int_list),
+ 'class': directives.class_option,
+ 'name': directives.unchanged,
+ 'align': align}
+
+ def run(self):
+ if not self.content:
+ error = self.reporter.error('The "%s" directive is empty; '
+ 'content required.' % self.name,
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ return [error]
+ title, messages = self.make_title()
+ node = nodes.Element() # anonymous container for parsing
+ self.state.nested_parse(self.content, self.content_offset, node)
+ try:
+ num_cols, col_widths = self.check_list_content(node)
+ table_data = [[item.children for item in row_list[0]]
+ for row_list in node[0]]
+ header_rows = self.options.get('header-rows', 0)
+ stub_columns = self.options.get('stub-columns', 0)
+ self.check_table_dimensions(table_data, header_rows, stub_columns)
+ except SystemMessagePropagation as detail:
+ return [detail.args[0]]
+ table_node = self.build_table_from_list(table_data, col_widths,
+ header_rows, stub_columns)
+ if 'align' in self.options:
+ table_node['align'] = self.options.get('align')
+ table_node['classes'] += self.options.get('class', [])
+ self.set_table_width(table_node)
+ self.add_name(table_node)
+ if title:
+ table_node.insert(0, title)
+ return [table_node] + messages
+
+ def check_list_content(self, node):
+ if len(node) != 1 or not isinstance(node[0], nodes.bullet_list):
+ error = self.reporter.error(
+ 'Error parsing content block for the "%s" directive: '
+ 'exactly one bullet list expected.' % self.name,
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+ list_node = node[0]
+ num_cols = 0
+ # Check for a uniform two-level bullet list:
+ for item_index in range(len(list_node)):
+ item = list_node[item_index]
+ if len(item) != 1 or not isinstance(item[0], nodes.bullet_list):
+ error = self.reporter.error(
+ 'Error parsing content block for the "%s" directive: '
+ 'two-level bullet list expected, but row %s does not '
+ 'contain a second-level bullet list.'
+ % (self.name, item_index + 1),
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+ elif item_index:
+ if len(item[0]) != num_cols:
+ error = self.reporter.error(
+ 'Error parsing content block for the "%s" directive: '
+ 'uniform two-level bullet list expected, but row %s '
+ 'does not contain the same number of items as row 1 '
+ '(%s vs %s).'
+ % (self.name, item_index + 1, len(item[0]), num_cols),
+ nodes.literal_block(self.block_text, self.block_text),
+ line=self.lineno)
+ raise SystemMessagePropagation(error)
+ else:
+ num_cols = len(item[0])
+ col_widths = self.get_column_widths(num_cols)
+ return num_cols, col_widths
+
+ def build_table_from_list(self, table_data,
+ col_widths, header_rows, stub_columns):
+ table = nodes.table()
+ if self.widths == 'auto':
+ table['classes'] += ['colwidths-auto']
+ elif self.widths: # explicitly set column widths
+ table['classes'] += ['colwidths-given']
+ tgroup = nodes.tgroup(cols=len(col_widths))
+ table += tgroup
+ for col_width in col_widths:
+ colspec = nodes.colspec()
+ if col_width is not None:
+ colspec.attributes['colwidth'] = col_width
+ if stub_columns:
+ colspec.attributes['stub'] = 1
+ stub_columns -= 1
+ tgroup += colspec
+ rows = []
+ for row in table_data:
+ row_node = nodes.row()
+ for cell in row:
+ entry = nodes.entry()
+ entry += cell
+ row_node += entry
+ rows.append(row_node)
+ if header_rows:
+ thead = nodes.thead()
+ thead.extend(rows[:header_rows])
+ tgroup += thead
+ tbody = nodes.tbody()
+ tbody.extend(rows[header_rows:])
+ tgroup += tbody
+ return table
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/README.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/README.txt
new file mode 100644
index 00000000..a51328fb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/README.txt
@@ -0,0 +1,17 @@
+============================================
+ ``docutils/parsers/rst/include`` Directory
+============================================
+
+This directory contains standard data files intended for inclusion in
+reStructuredText documents. To access these files, use the "include"
+directive with the special syntax for standard "include" data files,
+angle brackets around the file name::
+
+ .. include:: <isonum.txt>
+
+See the documentation for the `"include" directive`__ and
+`reStructuredText Standard Definition Files`__ for
+details.
+
+__ https://docutils.sourceforge.io/docs/ref/rst/directives.html#include
+__ https://docutils.sourceforge.io/docs/ref/rst/definitions.html
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsa.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsa.txt
new file mode 100644
index 00000000..a13b1d66
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsa.txt
@@ -0,0 +1,162 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |angzarr| unicode:: U+0237C .. RIGHT ANGLE WITH DOWNWARDS ZIGZAG ARROW
+.. |cirmid| unicode:: U+02AEF .. VERTICAL LINE WITH CIRCLE ABOVE
+.. |cudarrl| unicode:: U+02938 .. RIGHT-SIDE ARC CLOCKWISE ARROW
+.. |cudarrr| unicode:: U+02935 .. ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS
+.. |cularr| unicode:: U+021B6 .. ANTICLOCKWISE TOP SEMICIRCLE ARROW
+.. |cularrp| unicode:: U+0293D .. TOP ARC ANTICLOCKWISE ARROW WITH PLUS
+.. |curarr| unicode:: U+021B7 .. CLOCKWISE TOP SEMICIRCLE ARROW
+.. |curarrm| unicode:: U+0293C .. TOP ARC CLOCKWISE ARROW WITH MINUS
+.. |Darr| unicode:: U+021A1 .. DOWNWARDS TWO HEADED ARROW
+.. |dArr| unicode:: U+021D3 .. DOWNWARDS DOUBLE ARROW
+.. |darr2| unicode:: U+021CA .. DOWNWARDS PAIRED ARROWS
+.. |ddarr| unicode:: U+021CA .. DOWNWARDS PAIRED ARROWS
+.. |DDotrahd| unicode:: U+02911 .. RIGHTWARDS ARROW WITH DOTTED STEM
+.. |dfisht| unicode:: U+0297F .. DOWN FISH TAIL
+.. |dHar| unicode:: U+02965 .. DOWNWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT
+.. |dharl| unicode:: U+021C3 .. DOWNWARDS HARPOON WITH BARB LEFTWARDS
+.. |dharr| unicode:: U+021C2 .. DOWNWARDS HARPOON WITH BARB RIGHTWARDS
+.. |dlarr| unicode:: U+02199 .. SOUTH WEST ARROW
+.. |drarr| unicode:: U+02198 .. SOUTH EAST ARROW
+.. |duarr| unicode:: U+021F5 .. DOWNWARDS ARROW LEFTWARDS OF UPWARDS ARROW
+.. |duhar| unicode:: U+0296F .. DOWNWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT
+.. |dzigrarr| unicode:: U+027FF .. LONG RIGHTWARDS SQUIGGLE ARROW
+.. |erarr| unicode:: U+02971 .. EQUALS SIGN ABOVE RIGHTWARDS ARROW
+.. |hArr| unicode:: U+021D4 .. LEFT RIGHT DOUBLE ARROW
+.. |harr| unicode:: U+02194 .. LEFT RIGHT ARROW
+.. |harrcir| unicode:: U+02948 .. LEFT RIGHT ARROW THROUGH SMALL CIRCLE
+.. |harrw| unicode:: U+021AD .. LEFT RIGHT WAVE ARROW
+.. |hoarr| unicode:: U+021FF .. LEFT RIGHT OPEN-HEADED ARROW
+.. |imof| unicode:: U+022B7 .. IMAGE OF
+.. |lAarr| unicode:: U+021DA .. LEFTWARDS TRIPLE ARROW
+.. |Larr| unicode:: U+0219E .. LEFTWARDS TWO HEADED ARROW
+.. |larr2| unicode:: U+021C7 .. LEFTWARDS PAIRED ARROWS
+.. |larrbfs| unicode:: U+0291F .. LEFTWARDS ARROW FROM BAR TO BLACK DIAMOND
+.. |larrfs| unicode:: U+0291D .. LEFTWARDS ARROW TO BLACK DIAMOND
+.. |larrhk| unicode:: U+021A9 .. LEFTWARDS ARROW WITH HOOK
+.. |larrlp| unicode:: U+021AB .. LEFTWARDS ARROW WITH LOOP
+.. |larrpl| unicode:: U+02939 .. LEFT-SIDE ARC ANTICLOCKWISE ARROW
+.. |larrsim| unicode:: U+02973 .. LEFTWARDS ARROW ABOVE TILDE OPERATOR
+.. |larrtl| unicode:: U+021A2 .. LEFTWARDS ARROW WITH TAIL
+.. |lAtail| unicode:: U+0291B .. LEFTWARDS DOUBLE ARROW-TAIL
+.. |latail| unicode:: U+02919 .. LEFTWARDS ARROW-TAIL
+.. |lBarr| unicode:: U+0290E .. LEFTWARDS TRIPLE DASH ARROW
+.. |lbarr| unicode:: U+0290C .. LEFTWARDS DOUBLE DASH ARROW
+.. |ldca| unicode:: U+02936 .. ARROW POINTING DOWNWARDS THEN CURVING LEFTWARDS
+.. |ldrdhar| unicode:: U+02967 .. LEFTWARDS HARPOON WITH BARB DOWN ABOVE RIGHTWARDS HARPOON WITH BARB DOWN
+.. |ldrushar| unicode:: U+0294B .. LEFT BARB DOWN RIGHT BARB UP HARPOON
+.. |ldsh| unicode:: U+021B2 .. DOWNWARDS ARROW WITH TIP LEFTWARDS
+.. |lfisht| unicode:: U+0297C .. LEFT FISH TAIL
+.. |lHar| unicode:: U+02962 .. LEFTWARDS HARPOON WITH BARB UP ABOVE LEFTWARDS HARPOON WITH BARB DOWN
+.. |lhard| unicode:: U+021BD .. LEFTWARDS HARPOON WITH BARB DOWNWARDS
+.. |lharu| unicode:: U+021BC .. LEFTWARDS HARPOON WITH BARB UPWARDS
+.. |lharul| unicode:: U+0296A .. LEFTWARDS HARPOON WITH BARB UP ABOVE LONG DASH
+.. |llarr| unicode:: U+021C7 .. LEFTWARDS PAIRED ARROWS
+.. |llhard| unicode:: U+0296B .. LEFTWARDS HARPOON WITH BARB DOWN BELOW LONG DASH
+.. |loarr| unicode:: U+021FD .. LEFTWARDS OPEN-HEADED ARROW
+.. |lrarr| unicode:: U+021C6 .. LEFTWARDS ARROW OVER RIGHTWARDS ARROW
+.. |lrarr2| unicode:: U+021C6 .. LEFTWARDS ARROW OVER RIGHTWARDS ARROW
+.. |lrhar| unicode:: U+021CB .. LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON
+.. |lrhar2| unicode:: U+021CB .. LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON
+.. |lrhard| unicode:: U+0296D .. RIGHTWARDS HARPOON WITH BARB DOWN BELOW LONG DASH
+.. |lsh| unicode:: U+021B0 .. UPWARDS ARROW WITH TIP LEFTWARDS
+.. |lurdshar| unicode:: U+0294A .. LEFT BARB UP RIGHT BARB DOWN HARPOON
+.. |luruhar| unicode:: U+02966 .. LEFTWARDS HARPOON WITH BARB UP ABOVE RIGHTWARDS HARPOON WITH BARB UP
+.. |Map| unicode:: U+02905 .. RIGHTWARDS TWO-HEADED ARROW FROM BAR
+.. |map| unicode:: U+021A6 .. RIGHTWARDS ARROW FROM BAR
+.. |midcir| unicode:: U+02AF0 .. VERTICAL LINE WITH CIRCLE BELOW
+.. |mumap| unicode:: U+022B8 .. MULTIMAP
+.. |nearhk| unicode:: U+02924 .. NORTH EAST ARROW WITH HOOK
+.. |neArr| unicode:: U+021D7 .. NORTH EAST DOUBLE ARROW
+.. |nearr| unicode:: U+02197 .. NORTH EAST ARROW
+.. |nesear| unicode:: U+02928 .. NORTH EAST ARROW AND SOUTH EAST ARROW
+.. |nhArr| unicode:: U+021CE .. LEFT RIGHT DOUBLE ARROW WITH STROKE
+.. |nharr| unicode:: U+021AE .. LEFT RIGHT ARROW WITH STROKE
+.. |nlArr| unicode:: U+021CD .. LEFTWARDS DOUBLE ARROW WITH STROKE
+.. |nlarr| unicode:: U+0219A .. LEFTWARDS ARROW WITH STROKE
+.. |nrArr| unicode:: U+021CF .. RIGHTWARDS DOUBLE ARROW WITH STROKE
+.. |nrarr| unicode:: U+0219B .. RIGHTWARDS ARROW WITH STROKE
+.. |nrarrc| unicode:: U+02933 U+00338 .. WAVE ARROW POINTING DIRECTLY RIGHT with slash
+.. |nrarrw| unicode:: U+0219D U+00338 .. RIGHTWARDS WAVE ARROW with slash
+.. |nvHarr| unicode:: U+02904 .. LEFT RIGHT DOUBLE ARROW WITH VERTICAL STROKE
+.. |nvlArr| unicode:: U+02902 .. LEFTWARDS DOUBLE ARROW WITH VERTICAL STROKE
+.. |nvrArr| unicode:: U+02903 .. RIGHTWARDS DOUBLE ARROW WITH VERTICAL STROKE
+.. |nwarhk| unicode:: U+02923 .. NORTH WEST ARROW WITH HOOK
+.. |nwArr| unicode:: U+021D6 .. NORTH WEST DOUBLE ARROW
+.. |nwarr| unicode:: U+02196 .. NORTH WEST ARROW
+.. |nwnear| unicode:: U+02927 .. NORTH WEST ARROW AND NORTH EAST ARROW
+.. |olarr| unicode:: U+021BA .. ANTICLOCKWISE OPEN CIRCLE ARROW
+.. |orarr| unicode:: U+021BB .. CLOCKWISE OPEN CIRCLE ARROW
+.. |origof| unicode:: U+022B6 .. ORIGINAL OF
+.. |rAarr| unicode:: U+021DB .. RIGHTWARDS TRIPLE ARROW
+.. |Rarr| unicode:: U+021A0 .. RIGHTWARDS TWO HEADED ARROW
+.. |rarr2| unicode:: U+021C9 .. RIGHTWARDS PAIRED ARROWS
+.. |rarrap| unicode:: U+02975 .. RIGHTWARDS ARROW ABOVE ALMOST EQUAL TO
+.. |rarrbfs| unicode:: U+02920 .. RIGHTWARDS ARROW FROM BAR TO BLACK DIAMOND
+.. |rarrc| unicode:: U+02933 .. WAVE ARROW POINTING DIRECTLY RIGHT
+.. |rarrfs| unicode:: U+0291E .. RIGHTWARDS ARROW TO BLACK DIAMOND
+.. |rarrhk| unicode:: U+021AA .. RIGHTWARDS ARROW WITH HOOK
+.. |rarrlp| unicode:: U+021AC .. RIGHTWARDS ARROW WITH LOOP
+.. |rarrpl| unicode:: U+02945 .. RIGHTWARDS ARROW WITH PLUS BELOW
+.. |rarrsim| unicode:: U+02974 .. RIGHTWARDS ARROW ABOVE TILDE OPERATOR
+.. |Rarrtl| unicode:: U+02916 .. RIGHTWARDS TWO-HEADED ARROW WITH TAIL
+.. |rarrtl| unicode:: U+021A3 .. RIGHTWARDS ARROW WITH TAIL
+.. |rarrw| unicode:: U+0219D .. RIGHTWARDS WAVE ARROW
+.. |rAtail| unicode:: U+0291C .. RIGHTWARDS DOUBLE ARROW-TAIL
+.. |ratail| unicode:: U+0291A .. RIGHTWARDS ARROW-TAIL
+.. |RBarr| unicode:: U+02910 .. RIGHTWARDS TWO-HEADED TRIPLE DASH ARROW
+.. |rBarr| unicode:: U+0290F .. RIGHTWARDS TRIPLE DASH ARROW
+.. |rbarr| unicode:: U+0290D .. RIGHTWARDS DOUBLE DASH ARROW
+.. |rdca| unicode:: U+02937 .. ARROW POINTING DOWNWARDS THEN CURVING RIGHTWARDS
+.. |rdldhar| unicode:: U+02969 .. RIGHTWARDS HARPOON WITH BARB DOWN ABOVE LEFTWARDS HARPOON WITH BARB DOWN
+.. |rdsh| unicode:: U+021B3 .. DOWNWARDS ARROW WITH TIP RIGHTWARDS
+.. |rfisht| unicode:: U+0297D .. RIGHT FISH TAIL
+.. |rHar| unicode:: U+02964 .. RIGHTWARDS HARPOON WITH BARB UP ABOVE RIGHTWARDS HARPOON WITH BARB DOWN
+.. |rhard| unicode:: U+021C1 .. RIGHTWARDS HARPOON WITH BARB DOWNWARDS
+.. |rharu| unicode:: U+021C0 .. RIGHTWARDS HARPOON WITH BARB UPWARDS
+.. |rharul| unicode:: U+0296C .. RIGHTWARDS HARPOON WITH BARB UP ABOVE LONG DASH
+.. |rlarr| unicode:: U+021C4 .. RIGHTWARDS ARROW OVER LEFTWARDS ARROW
+.. |rlarr2| unicode:: U+021C4 .. RIGHTWARDS ARROW OVER LEFTWARDS ARROW
+.. |rlhar| unicode:: U+021CC .. RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON
+.. |rlhar2| unicode:: U+021CC .. RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON
+.. |roarr| unicode:: U+021FE .. RIGHTWARDS OPEN-HEADED ARROW
+.. |rrarr| unicode:: U+021C9 .. RIGHTWARDS PAIRED ARROWS
+.. |rsh| unicode:: U+021B1 .. UPWARDS ARROW WITH TIP RIGHTWARDS
+.. |ruluhar| unicode:: U+02968 .. RIGHTWARDS HARPOON WITH BARB UP ABOVE LEFTWARDS HARPOON WITH BARB UP
+.. |searhk| unicode:: U+02925 .. SOUTH EAST ARROW WITH HOOK
+.. |seArr| unicode:: U+021D8 .. SOUTH EAST DOUBLE ARROW
+.. |searr| unicode:: U+02198 .. SOUTH EAST ARROW
+.. |seswar| unicode:: U+02929 .. SOUTH EAST ARROW AND SOUTH WEST ARROW
+.. |simrarr| unicode:: U+02972 .. TILDE OPERATOR ABOVE RIGHTWARDS ARROW
+.. |slarr| unicode:: U+02190 .. LEFTWARDS ARROW
+.. |srarr| unicode:: U+02192 .. RIGHTWARDS ARROW
+.. |swarhk| unicode:: U+02926 .. SOUTH WEST ARROW WITH HOOK
+.. |swArr| unicode:: U+021D9 .. SOUTH WEST DOUBLE ARROW
+.. |swarr| unicode:: U+02199 .. SOUTH WEST ARROW
+.. |swnwar| unicode:: U+0292A .. SOUTH WEST ARROW AND NORTH WEST ARROW
+.. |Uarr| unicode:: U+0219F .. UPWARDS TWO HEADED ARROW
+.. |uArr| unicode:: U+021D1 .. UPWARDS DOUBLE ARROW
+.. |uarr2| unicode:: U+021C8 .. UPWARDS PAIRED ARROWS
+.. |Uarrocir| unicode:: U+02949 .. UPWARDS TWO-HEADED ARROW FROM SMALL CIRCLE
+.. |udarr| unicode:: U+021C5 .. UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW
+.. |udhar| unicode:: U+0296E .. UPWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT
+.. |ufisht| unicode:: U+0297E .. UP FISH TAIL
+.. |uHar| unicode:: U+02963 .. UPWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT
+.. |uharl| unicode:: U+021BF .. UPWARDS HARPOON WITH BARB LEFTWARDS
+.. |uharr| unicode:: U+021BE .. UPWARDS HARPOON WITH BARB RIGHTWARDS
+.. |uuarr| unicode:: U+021C8 .. UPWARDS PAIRED ARROWS
+.. |vArr| unicode:: U+021D5 .. UP DOWN DOUBLE ARROW
+.. |varr| unicode:: U+02195 .. UP DOWN ARROW
+.. |xhArr| unicode:: U+027FA .. LONG LEFT RIGHT DOUBLE ARROW
+.. |xharr| unicode:: U+027F7 .. LONG LEFT RIGHT ARROW
+.. |xlArr| unicode:: U+027F8 .. LONG LEFTWARDS DOUBLE ARROW
+.. |xlarr| unicode:: U+027F5 .. LONG LEFTWARDS ARROW
+.. |xmap| unicode:: U+027FC .. LONG RIGHTWARDS ARROW FROM BAR
+.. |xrArr| unicode:: U+027F9 .. LONG RIGHTWARDS DOUBLE ARROW
+.. |xrarr| unicode:: U+027F6 .. LONG RIGHTWARDS ARROW
+.. |zigrarr| unicode:: U+021DD .. RIGHTWARDS SQUIGGLE ARROW
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsb.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsb.txt
new file mode 100644
index 00000000..d66fd4dd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsb.txt
@@ -0,0 +1,126 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |ac| unicode:: U+0223E .. INVERTED LAZY S
+.. |acE| unicode:: U+0223E U+00333 .. INVERTED LAZY S with double underline
+.. |amalg| unicode:: U+02A3F .. AMALGAMATION OR COPRODUCT
+.. |barvee| unicode:: U+022BD .. NOR
+.. |Barwed| unicode:: U+02306 .. PERSPECTIVE
+.. |barwed| unicode:: U+02305 .. PROJECTIVE
+.. |bsolb| unicode:: U+029C5 .. SQUARED FALLING DIAGONAL SLASH
+.. |Cap| unicode:: U+022D2 .. DOUBLE INTERSECTION
+.. |capand| unicode:: U+02A44 .. INTERSECTION WITH LOGICAL AND
+.. |capbrcup| unicode:: U+02A49 .. INTERSECTION ABOVE BAR ABOVE UNION
+.. |capcap| unicode:: U+02A4B .. INTERSECTION BESIDE AND JOINED WITH INTERSECTION
+.. |capcup| unicode:: U+02A47 .. INTERSECTION ABOVE UNION
+.. |capdot| unicode:: U+02A40 .. INTERSECTION WITH DOT
+.. |caps| unicode:: U+02229 U+0FE00 .. INTERSECTION with serifs
+.. |ccaps| unicode:: U+02A4D .. CLOSED INTERSECTION WITH SERIFS
+.. |ccups| unicode:: U+02A4C .. CLOSED UNION WITH SERIFS
+.. |ccupssm| unicode:: U+02A50 .. CLOSED UNION WITH SERIFS AND SMASH PRODUCT
+.. |coprod| unicode:: U+02210 .. N-ARY COPRODUCT
+.. |Cup| unicode:: U+022D3 .. DOUBLE UNION
+.. |cupbrcap| unicode:: U+02A48 .. UNION ABOVE BAR ABOVE INTERSECTION
+.. |cupcap| unicode:: U+02A46 .. UNION ABOVE INTERSECTION
+.. |cupcup| unicode:: U+02A4A .. UNION BESIDE AND JOINED WITH UNION
+.. |cupdot| unicode:: U+0228D .. MULTISET MULTIPLICATION
+.. |cupor| unicode:: U+02A45 .. UNION WITH LOGICAL OR
+.. |cups| unicode:: U+0222A U+0FE00 .. UNION with serifs
+.. |cuvee| unicode:: U+022CE .. CURLY LOGICAL OR
+.. |cuwed| unicode:: U+022CF .. CURLY LOGICAL AND
+.. |Dagger| unicode:: U+02021 .. DOUBLE DAGGER
+.. |dagger| unicode:: U+02020 .. DAGGER
+.. |diam| unicode:: U+022C4 .. DIAMOND OPERATOR
+.. |divonx| unicode:: U+022C7 .. DIVISION TIMES
+.. |eplus| unicode:: U+02A71 .. EQUALS SIGN ABOVE PLUS SIGN
+.. |hercon| unicode:: U+022B9 .. HERMITIAN CONJUGATE MATRIX
+.. |intcal| unicode:: U+022BA .. INTERCALATE
+.. |iprod| unicode:: U+02A3C .. INTERIOR PRODUCT
+.. |loplus| unicode:: U+02A2D .. PLUS SIGN IN LEFT HALF CIRCLE
+.. |lotimes| unicode:: U+02A34 .. MULTIPLICATION SIGN IN LEFT HALF CIRCLE
+.. |lthree| unicode:: U+022CB .. LEFT SEMIDIRECT PRODUCT
+.. |ltimes| unicode:: U+022C9 .. LEFT NORMAL FACTOR SEMIDIRECT PRODUCT
+.. |midast| unicode:: U+0002A .. ASTERISK
+.. |minusb| unicode:: U+0229F .. SQUARED MINUS
+.. |minusd| unicode:: U+02238 .. DOT MINUS
+.. |minusdu| unicode:: U+02A2A .. MINUS SIGN WITH DOT BELOW
+.. |ncap| unicode:: U+02A43 .. INTERSECTION WITH OVERBAR
+.. |ncup| unicode:: U+02A42 .. UNION WITH OVERBAR
+.. |oast| unicode:: U+0229B .. CIRCLED ASTERISK OPERATOR
+.. |ocir| unicode:: U+0229A .. CIRCLED RING OPERATOR
+.. |odash| unicode:: U+0229D .. CIRCLED DASH
+.. |odiv| unicode:: U+02A38 .. CIRCLED DIVISION SIGN
+.. |odot| unicode:: U+02299 .. CIRCLED DOT OPERATOR
+.. |odsold| unicode:: U+029BC .. CIRCLED ANTICLOCKWISE-ROTATED DIVISION SIGN
+.. |ofcir| unicode:: U+029BF .. CIRCLED BULLET
+.. |ogt| unicode:: U+029C1 .. CIRCLED GREATER-THAN
+.. |ohbar| unicode:: U+029B5 .. CIRCLE WITH HORIZONTAL BAR
+.. |olcir| unicode:: U+029BE .. CIRCLED WHITE BULLET
+.. |olt| unicode:: U+029C0 .. CIRCLED LESS-THAN
+.. |omid| unicode:: U+029B6 .. CIRCLED VERTICAL BAR
+.. |ominus| unicode:: U+02296 .. CIRCLED MINUS
+.. |opar| unicode:: U+029B7 .. CIRCLED PARALLEL
+.. |operp| unicode:: U+029B9 .. CIRCLED PERPENDICULAR
+.. |oplus| unicode:: U+02295 .. CIRCLED PLUS
+.. |osol| unicode:: U+02298 .. CIRCLED DIVISION SLASH
+.. |Otimes| unicode:: U+02A37 .. MULTIPLICATION SIGN IN DOUBLE CIRCLE
+.. |otimes| unicode:: U+02297 .. CIRCLED TIMES
+.. |otimesas| unicode:: U+02A36 .. CIRCLED MULTIPLICATION SIGN WITH CIRCUMFLEX ACCENT
+.. |ovbar| unicode:: U+0233D .. APL FUNCTIONAL SYMBOL CIRCLE STILE
+.. |plusacir| unicode:: U+02A23 .. PLUS SIGN WITH CIRCUMFLEX ACCENT ABOVE
+.. |plusb| unicode:: U+0229E .. SQUARED PLUS
+.. |pluscir| unicode:: U+02A22 .. PLUS SIGN WITH SMALL CIRCLE ABOVE
+.. |plusdo| unicode:: U+02214 .. DOT PLUS
+.. |plusdu| unicode:: U+02A25 .. PLUS SIGN WITH DOT BELOW
+.. |pluse| unicode:: U+02A72 .. PLUS SIGN ABOVE EQUALS SIGN
+.. |plussim| unicode:: U+02A26 .. PLUS SIGN WITH TILDE BELOW
+.. |plustwo| unicode:: U+02A27 .. PLUS SIGN WITH SUBSCRIPT TWO
+.. |prod| unicode:: U+0220F .. N-ARY PRODUCT
+.. |race| unicode:: U+029DA .. LEFT DOUBLE WIGGLY FENCE
+.. |roplus| unicode:: U+02A2E .. PLUS SIGN IN RIGHT HALF CIRCLE
+.. |rotimes| unicode:: U+02A35 .. MULTIPLICATION SIGN IN RIGHT HALF CIRCLE
+.. |rthree| unicode:: U+022CC .. RIGHT SEMIDIRECT PRODUCT
+.. |rtimes| unicode:: U+022CA .. RIGHT NORMAL FACTOR SEMIDIRECT PRODUCT
+.. |sdot| unicode:: U+022C5 .. DOT OPERATOR
+.. |sdotb| unicode:: U+022A1 .. SQUARED DOT OPERATOR
+.. |setmn| unicode:: U+02216 .. SET MINUS
+.. |simplus| unicode:: U+02A24 .. PLUS SIGN WITH TILDE ABOVE
+.. |smashp| unicode:: U+02A33 .. SMASH PRODUCT
+.. |solb| unicode:: U+029C4 .. SQUARED RISING DIAGONAL SLASH
+.. |sqcap| unicode:: U+02293 .. SQUARE CAP
+.. |sqcaps| unicode:: U+02293 U+0FE00 .. SQUARE CAP with serifs
+.. |sqcup| unicode:: U+02294 .. SQUARE CUP
+.. |sqcups| unicode:: U+02294 U+0FE00 .. SQUARE CUP with serifs
+.. |ssetmn| unicode:: U+02216 .. SET MINUS
+.. |sstarf| unicode:: U+022C6 .. STAR OPERATOR
+.. |subdot| unicode:: U+02ABD .. SUBSET WITH DOT
+.. |sum| unicode:: U+02211 .. N-ARY SUMMATION
+.. |supdot| unicode:: U+02ABE .. SUPERSET WITH DOT
+.. |timesb| unicode:: U+022A0 .. SQUARED TIMES
+.. |timesbar| unicode:: U+02A31 .. MULTIPLICATION SIGN WITH UNDERBAR
+.. |timesd| unicode:: U+02A30 .. MULTIPLICATION SIGN WITH DOT ABOVE
+.. |top| unicode:: U+022A4 .. DOWN TACK
+.. |tridot| unicode:: U+025EC .. WHITE UP-POINTING TRIANGLE WITH DOT
+.. |triminus| unicode:: U+02A3A .. MINUS SIGN IN TRIANGLE
+.. |triplus| unicode:: U+02A39 .. PLUS SIGN IN TRIANGLE
+.. |trisb| unicode:: U+029CD .. TRIANGLE WITH SERIFS AT BOTTOM
+.. |tritime| unicode:: U+02A3B .. MULTIPLICATION SIGN IN TRIANGLE
+.. |uplus| unicode:: U+0228E .. MULTISET UNION
+.. |veebar| unicode:: U+022BB .. XOR
+.. |wedbar| unicode:: U+02A5F .. LOGICAL AND WITH UNDERBAR
+.. |wreath| unicode:: U+02240 .. WREATH PRODUCT
+.. |xcap| unicode:: U+022C2 .. N-ARY INTERSECTION
+.. |xcirc| unicode:: U+025EF .. LARGE CIRCLE
+.. |xcup| unicode:: U+022C3 .. N-ARY UNION
+.. |xdtri| unicode:: U+025BD .. WHITE DOWN-POINTING TRIANGLE
+.. |xodot| unicode:: U+02A00 .. N-ARY CIRCLED DOT OPERATOR
+.. |xoplus| unicode:: U+02A01 .. N-ARY CIRCLED PLUS OPERATOR
+.. |xotime| unicode:: U+02A02 .. N-ARY CIRCLED TIMES OPERATOR
+.. |xsqcup| unicode:: U+02A06 .. N-ARY SQUARE UNION OPERATOR
+.. |xuplus| unicode:: U+02A04 .. N-ARY UNION OPERATOR WITH PLUS
+.. |xutri| unicode:: U+025B3 .. WHITE UP-POINTING TRIANGLE
+.. |xvee| unicode:: U+022C1 .. N-ARY LOGICAL OR
+.. |xwedge| unicode:: U+022C0 .. N-ARY LOGICAL AND
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsc.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsc.txt
new file mode 100644
index 00000000..bef4c3e7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsc.txt
@@ -0,0 +1,29 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |dlcorn| unicode:: U+0231E .. BOTTOM LEFT CORNER
+.. |drcorn| unicode:: U+0231F .. BOTTOM RIGHT CORNER
+.. |gtlPar| unicode:: U+02995 .. DOUBLE LEFT ARC GREATER-THAN BRACKET
+.. |langd| unicode:: U+02991 .. LEFT ANGLE BRACKET WITH DOT
+.. |lbrke| unicode:: U+0298B .. LEFT SQUARE BRACKET WITH UNDERBAR
+.. |lbrksld| unicode:: U+0298F .. LEFT SQUARE BRACKET WITH TICK IN BOTTOM CORNER
+.. |lbrkslu| unicode:: U+0298D .. LEFT SQUARE BRACKET WITH TICK IN TOP CORNER
+.. |lceil| unicode:: U+02308 .. LEFT CEILING
+.. |lfloor| unicode:: U+0230A .. LEFT FLOOR
+.. |lmoust| unicode:: U+023B0 .. UPPER LEFT OR LOWER RIGHT CURLY BRACKET SECTION
+.. |lpargt| unicode:: U+029A0 .. SPHERICAL ANGLE OPENING LEFT
+.. |lparlt| unicode:: U+02993 .. LEFT ARC LESS-THAN BRACKET
+.. |ltrPar| unicode:: U+02996 .. DOUBLE RIGHT ARC LESS-THAN BRACKET
+.. |rangd| unicode:: U+02992 .. RIGHT ANGLE BRACKET WITH DOT
+.. |rbrke| unicode:: U+0298C .. RIGHT SQUARE BRACKET WITH UNDERBAR
+.. |rbrksld| unicode:: U+0298E .. RIGHT SQUARE BRACKET WITH TICK IN BOTTOM CORNER
+.. |rbrkslu| unicode:: U+02990 .. RIGHT SQUARE BRACKET WITH TICK IN TOP CORNER
+.. |rceil| unicode:: U+02309 .. RIGHT CEILING
+.. |rfloor| unicode:: U+0230B .. RIGHT FLOOR
+.. |rmoust| unicode:: U+023B1 .. UPPER RIGHT OR LOWER LEFT CURLY BRACKET SECTION
+.. |rpargt| unicode:: U+02994 .. RIGHT ARC GREATER-THAN BRACKET
+.. |ulcorn| unicode:: U+0231C .. TOP LEFT CORNER
+.. |urcorn| unicode:: U+0231D .. TOP RIGHT CORNER
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsn.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsn.txt
new file mode 100644
index 00000000..65389e8d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsn.txt
@@ -0,0 +1,96 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |gnap| unicode:: U+02A8A .. GREATER-THAN AND NOT APPROXIMATE
+.. |gnE| unicode:: U+02269 .. GREATER-THAN BUT NOT EQUAL TO
+.. |gne| unicode:: U+02A88 .. GREATER-THAN AND SINGLE-LINE NOT EQUAL TO
+.. |gnsim| unicode:: U+022E7 .. GREATER-THAN BUT NOT EQUIVALENT TO
+.. |gvnE| unicode:: U+02269 U+0FE00 .. GREATER-THAN BUT NOT EQUAL TO - with vertical stroke
+.. |lnap| unicode:: U+02A89 .. LESS-THAN AND NOT APPROXIMATE
+.. |lnE| unicode:: U+02268 .. LESS-THAN BUT NOT EQUAL TO
+.. |lne| unicode:: U+02A87 .. LESS-THAN AND SINGLE-LINE NOT EQUAL TO
+.. |lnsim| unicode:: U+022E6 .. LESS-THAN BUT NOT EQUIVALENT TO
+.. |lvnE| unicode:: U+02268 U+0FE00 .. LESS-THAN BUT NOT EQUAL TO - with vertical stroke
+.. |nap| unicode:: U+02249 .. NOT ALMOST EQUAL TO
+.. |napE| unicode:: U+02A70 U+00338 .. APPROXIMATELY EQUAL OR EQUAL TO with slash
+.. |napid| unicode:: U+0224B U+00338 .. TRIPLE TILDE with slash
+.. |ncong| unicode:: U+02247 .. NEITHER APPROXIMATELY NOR ACTUALLY EQUAL TO
+.. |ncongdot| unicode:: U+02A6D U+00338 .. CONGRUENT WITH DOT ABOVE with slash
+.. |nequiv| unicode:: U+02262 .. NOT IDENTICAL TO
+.. |ngE| unicode:: U+02267 U+00338 .. GREATER-THAN OVER EQUAL TO with slash
+.. |nge| unicode:: U+02271 .. NEITHER GREATER-THAN NOR EQUAL TO
+.. |nges| unicode:: U+02A7E U+00338 .. GREATER-THAN OR SLANTED EQUAL TO with slash
+.. |nGg| unicode:: U+022D9 U+00338 .. VERY MUCH GREATER-THAN with slash
+.. |ngsim| unicode:: U+02275 .. NEITHER GREATER-THAN NOR EQUIVALENT TO
+.. |nGt| unicode:: U+0226B U+020D2 .. MUCH GREATER THAN with vertical line
+.. |ngt| unicode:: U+0226F .. NOT GREATER-THAN
+.. |nGtv| unicode:: U+0226B U+00338 .. MUCH GREATER THAN with slash
+.. |nlE| unicode:: U+02266 U+00338 .. LESS-THAN OVER EQUAL TO with slash
+.. |nle| unicode:: U+02270 .. NEITHER LESS-THAN NOR EQUAL TO
+.. |nles| unicode:: U+02A7D U+00338 .. LESS-THAN OR SLANTED EQUAL TO with slash
+.. |nLl| unicode:: U+022D8 U+00338 .. VERY MUCH LESS-THAN with slash
+.. |nlsim| unicode:: U+02274 .. NEITHER LESS-THAN NOR EQUIVALENT TO
+.. |nLt| unicode:: U+0226A U+020D2 .. MUCH LESS THAN with vertical line
+.. |nlt| unicode:: U+0226E .. NOT LESS-THAN
+.. |nltri| unicode:: U+022EA .. NOT NORMAL SUBGROUP OF
+.. |nltrie| unicode:: U+022EC .. NOT NORMAL SUBGROUP OF OR EQUAL TO
+.. |nLtv| unicode:: U+0226A U+00338 .. MUCH LESS THAN with slash
+.. |nmid| unicode:: U+02224 .. DOES NOT DIVIDE
+.. |npar| unicode:: U+02226 .. NOT PARALLEL TO
+.. |npr| unicode:: U+02280 .. DOES NOT PRECEDE
+.. |nprcue| unicode:: U+022E0 .. DOES NOT PRECEDE OR EQUAL
+.. |npre| unicode:: U+02AAF U+00338 .. PRECEDES ABOVE SINGLE-LINE EQUALS SIGN with slash
+.. |nrtri| unicode:: U+022EB .. DOES NOT CONTAIN AS NORMAL SUBGROUP
+.. |nrtrie| unicode:: U+022ED .. DOES NOT CONTAIN AS NORMAL SUBGROUP OR EQUAL
+.. |nsc| unicode:: U+02281 .. DOES NOT SUCCEED
+.. |nsccue| unicode:: U+022E1 .. DOES NOT SUCCEED OR EQUAL
+.. |nsce| unicode:: U+02AB0 U+00338 .. SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN with slash
+.. |nsim| unicode:: U+02241 .. NOT TILDE
+.. |nsime| unicode:: U+02244 .. NOT ASYMPTOTICALLY EQUAL TO
+.. |nsmid| unicode:: U+02224 .. DOES NOT DIVIDE
+.. |nspar| unicode:: U+02226 .. NOT PARALLEL TO
+.. |nsqsube| unicode:: U+022E2 .. NOT SQUARE IMAGE OF OR EQUAL TO
+.. |nsqsupe| unicode:: U+022E3 .. NOT SQUARE ORIGINAL OF OR EQUAL TO
+.. |nsub| unicode:: U+02284 .. NOT A SUBSET OF
+.. |nsubE| unicode:: U+02AC5 U+00338 .. SUBSET OF ABOVE EQUALS SIGN with slash
+.. |nsube| unicode:: U+02288 .. NEITHER A SUBSET OF NOR EQUAL TO
+.. |nsup| unicode:: U+02285 .. NOT A SUPERSET OF
+.. |nsupE| unicode:: U+02AC6 U+00338 .. SUPERSET OF ABOVE EQUALS SIGN with slash
+.. |nsupe| unicode:: U+02289 .. NEITHER A SUPERSET OF NOR EQUAL TO
+.. |ntgl| unicode:: U+02279 .. NEITHER GREATER-THAN NOR LESS-THAN
+.. |ntlg| unicode:: U+02278 .. NEITHER LESS-THAN NOR GREATER-THAN
+.. |nvap| unicode:: U+0224D U+020D2 .. EQUIVALENT TO with vertical line
+.. |nVDash| unicode:: U+022AF .. NEGATED DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE
+.. |nVdash| unicode:: U+022AE .. DOES NOT FORCE
+.. |nvDash| unicode:: U+022AD .. NOT TRUE
+.. |nvdash| unicode:: U+022AC .. DOES NOT PROVE
+.. |nvge| unicode:: U+02265 U+020D2 .. GREATER-THAN OR EQUAL TO with vertical line
+.. |nvgt| unicode:: U+0003E U+020D2 .. GREATER-THAN SIGN with vertical line
+.. |nvle| unicode:: U+02264 U+020D2 .. LESS-THAN OR EQUAL TO with vertical line
+.. |nvlt| unicode:: U+0003C U+020D2 .. LESS-THAN SIGN with vertical line
+.. |nvltrie| unicode:: U+022B4 U+020D2 .. NORMAL SUBGROUP OF OR EQUAL TO with vertical line
+.. |nvrtrie| unicode:: U+022B5 U+020D2 .. CONTAINS AS NORMAL SUBGROUP OR EQUAL TO with vertical line
+.. |nvsim| unicode:: U+0223C U+020D2 .. TILDE OPERATOR with vertical line
+.. |parsim| unicode:: U+02AF3 .. PARALLEL WITH TILDE OPERATOR
+.. |prnap| unicode:: U+02AB9 .. PRECEDES ABOVE NOT ALMOST EQUAL TO
+.. |prnE| unicode:: U+02AB5 .. PRECEDES ABOVE NOT EQUAL TO
+.. |prnsim| unicode:: U+022E8 .. PRECEDES BUT NOT EQUIVALENT TO
+.. |rnmid| unicode:: U+02AEE .. DOES NOT DIVIDE WITH REVERSED NEGATION SLASH
+.. |scnap| unicode:: U+02ABA .. SUCCEEDS ABOVE NOT ALMOST EQUAL TO
+.. |scnE| unicode:: U+02AB6 .. SUCCEEDS ABOVE NOT EQUAL TO
+.. |scnsim| unicode:: U+022E9 .. SUCCEEDS BUT NOT EQUIVALENT TO
+.. |simne| unicode:: U+02246 .. APPROXIMATELY BUT NOT ACTUALLY EQUAL TO
+.. |solbar| unicode:: U+0233F .. APL FUNCTIONAL SYMBOL SLASH BAR
+.. |subnE| unicode:: U+02ACB .. SUBSET OF ABOVE NOT EQUAL TO
+.. |subne| unicode:: U+0228A .. SUBSET OF WITH NOT EQUAL TO
+.. |supnE| unicode:: U+02ACC .. SUPERSET OF ABOVE NOT EQUAL TO
+.. |supne| unicode:: U+0228B .. SUPERSET OF WITH NOT EQUAL TO
+.. |vnsub| unicode:: U+02282 U+020D2 .. SUBSET OF with vertical line
+.. |vnsup| unicode:: U+02283 U+020D2 .. SUPERSET OF with vertical line
+.. |vsubnE| unicode:: U+02ACB U+0FE00 .. SUBSET OF ABOVE NOT EQUAL TO - variant with stroke through bottom members
+.. |vsubne| unicode:: U+0228A U+0FE00 .. SUBSET OF WITH NOT EQUAL TO - variant with stroke through bottom members
+.. |vsupnE| unicode:: U+02ACC U+0FE00 .. SUPERSET OF ABOVE NOT EQUAL TO - variant with stroke through bottom members
+.. |vsupne| unicode:: U+0228B U+0FE00 .. SUPERSET OF WITH NOT EQUAL TO - variant with stroke through bottom members
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamso.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamso.txt
new file mode 100644
index 00000000..f17e16bc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamso.txt
@@ -0,0 +1,62 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |ang| unicode:: U+02220 .. ANGLE
+.. |ange| unicode:: U+029A4 .. ANGLE WITH UNDERBAR
+.. |angmsd| unicode:: U+02221 .. MEASURED ANGLE
+.. |angmsdaa| unicode:: U+029A8 .. MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING UP AND RIGHT
+.. |angmsdab| unicode:: U+029A9 .. MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING UP AND LEFT
+.. |angmsdac| unicode:: U+029AA .. MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING DOWN AND RIGHT
+.. |angmsdad| unicode:: U+029AB .. MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING DOWN AND LEFT
+.. |angmsdae| unicode:: U+029AC .. MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING RIGHT AND UP
+.. |angmsdaf| unicode:: U+029AD .. MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING LEFT AND UP
+.. |angmsdag| unicode:: U+029AE .. MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING RIGHT AND DOWN
+.. |angmsdah| unicode:: U+029AF .. MEASURED ANGLE WITH OPEN ARM ENDING IN ARROW POINTING LEFT AND DOWN
+.. |angrtvb| unicode:: U+022BE .. RIGHT ANGLE WITH ARC
+.. |angrtvbd| unicode:: U+0299D .. MEASURED RIGHT ANGLE WITH DOT
+.. |bbrk| unicode:: U+023B5 .. BOTTOM SQUARE BRACKET
+.. |bbrktbrk| unicode:: U+023B6 .. BOTTOM SQUARE BRACKET OVER TOP SQUARE BRACKET
+.. |bemptyv| unicode:: U+029B0 .. REVERSED EMPTY SET
+.. |beth| unicode:: U+02136 .. BET SYMBOL
+.. |boxbox| unicode:: U+029C9 .. TWO JOINED SQUARES
+.. |bprime| unicode:: U+02035 .. REVERSED PRIME
+.. |bsemi| unicode:: U+0204F .. REVERSED SEMICOLON
+.. |cemptyv| unicode:: U+029B2 .. EMPTY SET WITH SMALL CIRCLE ABOVE
+.. |cirE| unicode:: U+029C3 .. CIRCLE WITH TWO HORIZONTAL STROKES TO THE RIGHT
+.. |cirscir| unicode:: U+029C2 .. CIRCLE WITH SMALL CIRCLE TO THE RIGHT
+.. |comp| unicode:: U+02201 .. COMPLEMENT
+.. |daleth| unicode:: U+02138 .. DALET SYMBOL
+.. |demptyv| unicode:: U+029B1 .. EMPTY SET WITH OVERBAR
+.. |ell| unicode:: U+02113 .. SCRIPT SMALL L
+.. |empty| unicode:: U+02205 .. EMPTY SET
+.. |emptyv| unicode:: U+02205 .. EMPTY SET
+.. |gimel| unicode:: U+02137 .. GIMEL SYMBOL
+.. |iiota| unicode:: U+02129 .. TURNED GREEK SMALL LETTER IOTA
+.. |image| unicode:: U+02111 .. BLACK-LETTER CAPITAL I
+.. |imath| unicode:: U+00131 .. LATIN SMALL LETTER DOTLESS I
+.. |inodot| unicode:: U+00131 .. LATIN SMALL LETTER DOTLESS I
+.. |jmath| unicode:: U+0006A .. LATIN SMALL LETTER J
+.. |jnodot| unicode:: U+0006A .. LATIN SMALL LETTER J
+.. |laemptyv| unicode:: U+029B4 .. EMPTY SET WITH LEFT ARROW ABOVE
+.. |lltri| unicode:: U+025FA .. LOWER LEFT TRIANGLE
+.. |lrtri| unicode:: U+022BF .. RIGHT TRIANGLE
+.. |mho| unicode:: U+02127 .. INVERTED OHM SIGN
+.. |nang| unicode:: U+02220 U+020D2 .. ANGLE with vertical line
+.. |nexist| unicode:: U+02204 .. THERE DOES NOT EXIST
+.. |oS| unicode:: U+024C8 .. CIRCLED LATIN CAPITAL LETTER S
+.. |planck| unicode:: U+0210F .. PLANCK CONSTANT OVER TWO PI
+.. |plankv| unicode:: U+0210F .. PLANCK CONSTANT OVER TWO PI
+.. |raemptyv| unicode:: U+029B3 .. EMPTY SET WITH RIGHT ARROW ABOVE
+.. |range| unicode:: U+029A5 .. REVERSED ANGLE WITH UNDERBAR
+.. |real| unicode:: U+0211C .. BLACK-LETTER CAPITAL R
+.. |sbsol| unicode:: U+0FE68 .. SMALL REVERSE SOLIDUS
+.. |tbrk| unicode:: U+023B4 .. TOP SQUARE BRACKET
+.. |trpezium| unicode:: U+0FFFD .. REPLACEMENT CHARACTER
+.. |ultri| unicode:: U+025F8 .. UPPER LEFT TRIANGLE
+.. |urtri| unicode:: U+025F9 .. UPPER RIGHT TRIANGLE
+.. |vprime| unicode:: U+02032 .. PRIME
+.. |vzigzag| unicode:: U+0299A .. VERTICAL ZIGZAG LINE
+.. |weierp| unicode:: U+02118 .. SCRIPT CAPITAL P
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsr.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsr.txt
new file mode 100644
index 00000000..7d3c1aac
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isoamsr.txt
@@ -0,0 +1,191 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |apE| unicode:: U+02A70 .. APPROXIMATELY EQUAL OR EQUAL TO
+.. |ape| unicode:: U+0224A .. ALMOST EQUAL OR EQUAL TO
+.. |apid| unicode:: U+0224B .. TRIPLE TILDE
+.. |asymp| unicode:: U+02248 .. ALMOST EQUAL TO
+.. |Barv| unicode:: U+02AE7 .. SHORT DOWN TACK WITH OVERBAR
+.. |bcong| unicode:: U+0224C .. ALL EQUAL TO
+.. |bepsi| unicode:: U+003F6 .. GREEK REVERSED LUNATE EPSILON SYMBOL
+.. |bowtie| unicode:: U+022C8 .. BOWTIE
+.. |bsim| unicode:: U+0223D .. REVERSED TILDE
+.. |bsime| unicode:: U+022CD .. REVERSED TILDE EQUALS
+.. |bsolhsub| unicode:: U+0005C U+02282 .. REVERSE SOLIDUS, SUBSET OF
+.. |bump| unicode:: U+0224E .. GEOMETRICALLY EQUIVALENT TO
+.. |bumpE| unicode:: U+02AAE .. EQUALS SIGN WITH BUMPY ABOVE
+.. |bumpe| unicode:: U+0224F .. DIFFERENCE BETWEEN
+.. |cire| unicode:: U+02257 .. RING EQUAL TO
+.. |Colon| unicode:: U+02237 .. PROPORTION
+.. |Colone| unicode:: U+02A74 .. DOUBLE COLON EQUAL
+.. |colone| unicode:: U+02254 .. COLON EQUALS
+.. |congdot| unicode:: U+02A6D .. CONGRUENT WITH DOT ABOVE
+.. |csub| unicode:: U+02ACF .. CLOSED SUBSET
+.. |csube| unicode:: U+02AD1 .. CLOSED SUBSET OR EQUAL TO
+.. |csup| unicode:: U+02AD0 .. CLOSED SUPERSET
+.. |csupe| unicode:: U+02AD2 .. CLOSED SUPERSET OR EQUAL TO
+.. |cuepr| unicode:: U+022DE .. EQUAL TO OR PRECEDES
+.. |cuesc| unicode:: U+022DF .. EQUAL TO OR SUCCEEDS
+.. |cupre| unicode:: U+0227C .. PRECEDES OR EQUAL TO
+.. |Dashv| unicode:: U+02AE4 .. VERTICAL BAR DOUBLE LEFT TURNSTILE
+.. |dashv| unicode:: U+022A3 .. LEFT TACK
+.. |easter| unicode:: U+02A6E .. EQUALS WITH ASTERISK
+.. |ecir| unicode:: U+02256 .. RING IN EQUAL TO
+.. |ecolon| unicode:: U+02255 .. EQUALS COLON
+.. |eDDot| unicode:: U+02A77 .. EQUALS SIGN WITH TWO DOTS ABOVE AND TWO DOTS BELOW
+.. |eDot| unicode:: U+02251 .. GEOMETRICALLY EQUAL TO
+.. |efDot| unicode:: U+02252 .. APPROXIMATELY EQUAL TO OR THE IMAGE OF
+.. |eg| unicode:: U+02A9A .. DOUBLE-LINE EQUAL TO OR GREATER-THAN
+.. |egs| unicode:: U+02A96 .. SLANTED EQUAL TO OR GREATER-THAN
+.. |egsdot| unicode:: U+02A98 .. SLANTED EQUAL TO OR GREATER-THAN WITH DOT INSIDE
+.. |el| unicode:: U+02A99 .. DOUBLE-LINE EQUAL TO OR LESS-THAN
+.. |els| unicode:: U+02A95 .. SLANTED EQUAL TO OR LESS-THAN
+.. |elsdot| unicode:: U+02A97 .. SLANTED EQUAL TO OR LESS-THAN WITH DOT INSIDE
+.. |equest| unicode:: U+0225F .. QUESTIONED EQUAL TO
+.. |equivDD| unicode:: U+02A78 .. EQUIVALENT WITH FOUR DOTS ABOVE
+.. |erDot| unicode:: U+02253 .. IMAGE OF OR APPROXIMATELY EQUAL TO
+.. |esdot| unicode:: U+02250 .. APPROACHES THE LIMIT
+.. |Esim| unicode:: U+02A73 .. EQUALS SIGN ABOVE TILDE OPERATOR
+.. |esim| unicode:: U+02242 .. MINUS TILDE
+.. |fork| unicode:: U+022D4 .. PITCHFORK
+.. |forkv| unicode:: U+02AD9 .. ELEMENT OF OPENING DOWNWARDS
+.. |frown| unicode:: U+02322 .. FROWN
+.. |gap| unicode:: U+02A86 .. GREATER-THAN OR APPROXIMATE
+.. |gE| unicode:: U+02267 .. GREATER-THAN OVER EQUAL TO
+.. |gEl| unicode:: U+02A8C .. GREATER-THAN ABOVE DOUBLE-LINE EQUAL ABOVE LESS-THAN
+.. |gel| unicode:: U+022DB .. GREATER-THAN EQUAL TO OR LESS-THAN
+.. |ges| unicode:: U+02A7E .. GREATER-THAN OR SLANTED EQUAL TO
+.. |gescc| unicode:: U+02AA9 .. GREATER-THAN CLOSED BY CURVE ABOVE SLANTED EQUAL
+.. |gesdot| unicode:: U+02A80 .. GREATER-THAN OR SLANTED EQUAL TO WITH DOT INSIDE
+.. |gesdoto| unicode:: U+02A82 .. GREATER-THAN OR SLANTED EQUAL TO WITH DOT ABOVE
+.. |gesdotol| unicode:: U+02A84 .. GREATER-THAN OR SLANTED EQUAL TO WITH DOT ABOVE LEFT
+.. |gesl| unicode:: U+022DB U+0FE00 .. GREATER-THAN slanted EQUAL TO OR LESS-THAN
+.. |gesles| unicode:: U+02A94 .. GREATER-THAN ABOVE SLANTED EQUAL ABOVE LESS-THAN ABOVE SLANTED EQUAL
+.. |Gg| unicode:: U+022D9 .. VERY MUCH GREATER-THAN
+.. |gl| unicode:: U+02277 .. GREATER-THAN OR LESS-THAN
+.. |gla| unicode:: U+02AA5 .. GREATER-THAN BESIDE LESS-THAN
+.. |glE| unicode:: U+02A92 .. GREATER-THAN ABOVE LESS-THAN ABOVE DOUBLE-LINE EQUAL
+.. |glj| unicode:: U+02AA4 .. GREATER-THAN OVERLAPPING LESS-THAN
+.. |gsdot| unicode:: U+022D7 .. GREATER-THAN WITH DOT
+.. |gsim| unicode:: U+02273 .. GREATER-THAN OR EQUIVALENT TO
+.. |gsime| unicode:: U+02A8E .. GREATER-THAN ABOVE SIMILAR OR EQUAL
+.. |gsiml| unicode:: U+02A90 .. GREATER-THAN ABOVE SIMILAR ABOVE LESS-THAN
+.. |Gt| unicode:: U+0226B .. MUCH GREATER-THAN
+.. |gtcc| unicode:: U+02AA7 .. GREATER-THAN CLOSED BY CURVE
+.. |gtcir| unicode:: U+02A7A .. GREATER-THAN WITH CIRCLE INSIDE
+.. |gtdot| unicode:: U+022D7 .. GREATER-THAN WITH DOT
+.. |gtquest| unicode:: U+02A7C .. GREATER-THAN WITH QUESTION MARK ABOVE
+.. |gtrarr| unicode:: U+02978 .. GREATER-THAN ABOVE RIGHTWARDS ARROW
+.. |homtht| unicode:: U+0223B .. HOMOTHETIC
+.. |lap| unicode:: U+02A85 .. LESS-THAN OR APPROXIMATE
+.. |lat| unicode:: U+02AAB .. LARGER THAN
+.. |late| unicode:: U+02AAD .. LARGER THAN OR EQUAL TO
+.. |lates| unicode:: U+02AAD U+0FE00 .. LARGER THAN OR slanted EQUAL
+.. |ldot| unicode:: U+022D6 .. LESS-THAN WITH DOT
+.. |lE| unicode:: U+02266 .. LESS-THAN OVER EQUAL TO
+.. |lEg| unicode:: U+02A8B .. LESS-THAN ABOVE DOUBLE-LINE EQUAL ABOVE GREATER-THAN
+.. |leg| unicode:: U+022DA .. LESS-THAN EQUAL TO OR GREATER-THAN
+.. |les| unicode:: U+02A7D .. LESS-THAN OR SLANTED EQUAL TO
+.. |lescc| unicode:: U+02AA8 .. LESS-THAN CLOSED BY CURVE ABOVE SLANTED EQUAL
+.. |lesdot| unicode:: U+02A7F .. LESS-THAN OR SLANTED EQUAL TO WITH DOT INSIDE
+.. |lesdoto| unicode:: U+02A81 .. LESS-THAN OR SLANTED EQUAL TO WITH DOT ABOVE
+.. |lesdotor| unicode:: U+02A83 .. LESS-THAN OR SLANTED EQUAL TO WITH DOT ABOVE RIGHT
+.. |lesg| unicode:: U+022DA U+0FE00 .. LESS-THAN slanted EQUAL TO OR GREATER-THAN
+.. |lesges| unicode:: U+02A93 .. LESS-THAN ABOVE SLANTED EQUAL ABOVE GREATER-THAN ABOVE SLANTED EQUAL
+.. |lg| unicode:: U+02276 .. LESS-THAN OR GREATER-THAN
+.. |lgE| unicode:: U+02A91 .. LESS-THAN ABOVE GREATER-THAN ABOVE DOUBLE-LINE EQUAL
+.. |Ll| unicode:: U+022D8 .. VERY MUCH LESS-THAN
+.. |lsim| unicode:: U+02272 .. LESS-THAN OR EQUIVALENT TO
+.. |lsime| unicode:: U+02A8D .. LESS-THAN ABOVE SIMILAR OR EQUAL
+.. |lsimg| unicode:: U+02A8F .. LESS-THAN ABOVE SIMILAR ABOVE GREATER-THAN
+.. |Lt| unicode:: U+0226A .. MUCH LESS-THAN
+.. |ltcc| unicode:: U+02AA6 .. LESS-THAN CLOSED BY CURVE
+.. |ltcir| unicode:: U+02A79 .. LESS-THAN WITH CIRCLE INSIDE
+.. |ltdot| unicode:: U+022D6 .. LESS-THAN WITH DOT
+.. |ltlarr| unicode:: U+02976 .. LESS-THAN ABOVE LEFTWARDS ARROW
+.. |ltquest| unicode:: U+02A7B .. LESS-THAN WITH QUESTION MARK ABOVE
+.. |ltrie| unicode:: U+022B4 .. NORMAL SUBGROUP OF OR EQUAL TO
+.. |mcomma| unicode:: U+02A29 .. MINUS SIGN WITH COMMA ABOVE
+.. |mDDot| unicode:: U+0223A .. GEOMETRIC PROPORTION
+.. |mid| unicode:: U+02223 .. DIVIDES
+.. |mlcp| unicode:: U+02ADB .. TRANSVERSAL INTERSECTION
+.. |models| unicode:: U+022A7 .. MODELS
+.. |mstpos| unicode:: U+0223E .. INVERTED LAZY S
+.. |Pr| unicode:: U+02ABB .. DOUBLE PRECEDES
+.. |pr| unicode:: U+0227A .. PRECEDES
+.. |prap| unicode:: U+02AB7 .. PRECEDES ABOVE ALMOST EQUAL TO
+.. |prcue| unicode:: U+0227C .. PRECEDES OR EQUAL TO
+.. |prE| unicode:: U+02AB3 .. PRECEDES ABOVE EQUALS SIGN
+.. |pre| unicode:: U+02AAF .. PRECEDES ABOVE SINGLE-LINE EQUALS SIGN
+.. |prsim| unicode:: U+0227E .. PRECEDES OR EQUIVALENT TO
+.. |prurel| unicode:: U+022B0 .. PRECEDES UNDER RELATION
+.. |ratio| unicode:: U+02236 .. RATIO
+.. |rtrie| unicode:: U+022B5 .. CONTAINS AS NORMAL SUBGROUP OR EQUAL TO
+.. |rtriltri| unicode:: U+029CE .. RIGHT TRIANGLE ABOVE LEFT TRIANGLE
+.. |samalg| unicode:: U+02210 .. N-ARY COPRODUCT
+.. |Sc| unicode:: U+02ABC .. DOUBLE SUCCEEDS
+.. |sc| unicode:: U+0227B .. SUCCEEDS
+.. |scap| unicode:: U+02AB8 .. SUCCEEDS ABOVE ALMOST EQUAL TO
+.. |sccue| unicode:: U+0227D .. SUCCEEDS OR EQUAL TO
+.. |scE| unicode:: U+02AB4 .. SUCCEEDS ABOVE EQUALS SIGN
+.. |sce| unicode:: U+02AB0 .. SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN
+.. |scsim| unicode:: U+0227F .. SUCCEEDS OR EQUIVALENT TO
+.. |sdote| unicode:: U+02A66 .. EQUALS SIGN WITH DOT BELOW
+.. |sfrown| unicode:: U+02322 .. FROWN
+.. |simg| unicode:: U+02A9E .. SIMILAR OR GREATER-THAN
+.. |simgE| unicode:: U+02AA0 .. SIMILAR ABOVE GREATER-THAN ABOVE EQUALS SIGN
+.. |siml| unicode:: U+02A9D .. SIMILAR OR LESS-THAN
+.. |simlE| unicode:: U+02A9F .. SIMILAR ABOVE LESS-THAN ABOVE EQUALS SIGN
+.. |smid| unicode:: U+02223 .. DIVIDES
+.. |smile| unicode:: U+02323 .. SMILE
+.. |smt| unicode:: U+02AAA .. SMALLER THAN
+.. |smte| unicode:: U+02AAC .. SMALLER THAN OR EQUAL TO
+.. |smtes| unicode:: U+02AAC U+0FE00 .. SMALLER THAN OR slanted EQUAL
+.. |spar| unicode:: U+02225 .. PARALLEL TO
+.. |sqsub| unicode:: U+0228F .. SQUARE IMAGE OF
+.. |sqsube| unicode:: U+02291 .. SQUARE IMAGE OF OR EQUAL TO
+.. |sqsup| unicode:: U+02290 .. SQUARE ORIGINAL OF
+.. |sqsupe| unicode:: U+02292 .. SQUARE ORIGINAL OF OR EQUAL TO
+.. |ssmile| unicode:: U+02323 .. SMILE
+.. |Sub| unicode:: U+022D0 .. DOUBLE SUBSET
+.. |subE| unicode:: U+02AC5 .. SUBSET OF ABOVE EQUALS SIGN
+.. |subedot| unicode:: U+02AC3 .. SUBSET OF OR EQUAL TO WITH DOT ABOVE
+.. |submult| unicode:: U+02AC1 .. SUBSET WITH MULTIPLICATION SIGN BELOW
+.. |subplus| unicode:: U+02ABF .. SUBSET WITH PLUS SIGN BELOW
+.. |subrarr| unicode:: U+02979 .. SUBSET ABOVE RIGHTWARDS ARROW
+.. |subsim| unicode:: U+02AC7 .. SUBSET OF ABOVE TILDE OPERATOR
+.. |subsub| unicode:: U+02AD5 .. SUBSET ABOVE SUBSET
+.. |subsup| unicode:: U+02AD3 .. SUBSET ABOVE SUPERSET
+.. |Sup| unicode:: U+022D1 .. DOUBLE SUPERSET
+.. |supdsub| unicode:: U+02AD8 .. SUPERSET BESIDE AND JOINED BY DASH WITH SUBSET
+.. |supE| unicode:: U+02AC6 .. SUPERSET OF ABOVE EQUALS SIGN
+.. |supedot| unicode:: U+02AC4 .. SUPERSET OF OR EQUAL TO WITH DOT ABOVE
+.. |suphsol| unicode:: U+02283 U+0002F .. SUPERSET OF, SOLIDUS
+.. |suphsub| unicode:: U+02AD7 .. SUPERSET BESIDE SUBSET
+.. |suplarr| unicode:: U+0297B .. SUPERSET ABOVE LEFTWARDS ARROW
+.. |supmult| unicode:: U+02AC2 .. SUPERSET WITH MULTIPLICATION SIGN BELOW
+.. |supplus| unicode:: U+02AC0 .. SUPERSET WITH PLUS SIGN BELOW
+.. |supsim| unicode:: U+02AC8 .. SUPERSET OF ABOVE TILDE OPERATOR
+.. |supsub| unicode:: U+02AD4 .. SUPERSET ABOVE SUBSET
+.. |supsup| unicode:: U+02AD6 .. SUPERSET ABOVE SUPERSET
+.. |thkap| unicode:: U+02248 .. ALMOST EQUAL TO
+.. |thksim| unicode:: U+0223C .. TILDE OPERATOR
+.. |topfork| unicode:: U+02ADA .. PITCHFORK WITH TEE TOP
+.. |trie| unicode:: U+0225C .. DELTA EQUAL TO
+.. |twixt| unicode:: U+0226C .. BETWEEN
+.. |Vbar| unicode:: U+02AEB .. DOUBLE UP TACK
+.. |vBar| unicode:: U+02AE8 .. SHORT UP TACK WITH UNDERBAR
+.. |vBarv| unicode:: U+02AE9 .. SHORT UP TACK ABOVE SHORT DOWN TACK
+.. |VDash| unicode:: U+022AB .. DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE
+.. |Vdash| unicode:: U+022A9 .. FORCES
+.. |vDash| unicode:: U+022A8 .. TRUE
+.. |vdash| unicode:: U+022A2 .. RIGHT TACK
+.. |Vdashl| unicode:: U+02AE6 .. LONG DASH FROM LEFT MEMBER OF DOUBLE VERTICAL
+.. |veebar| unicode:: U+022BB .. XOR
+.. |vltri| unicode:: U+022B2 .. NORMAL SUBGROUP OF
+.. |vprop| unicode:: U+0221D .. PROPORTIONAL TO
+.. |vrtri| unicode:: U+022B3 .. CONTAINS AS NORMAL SUBGROUP
+.. |Vvdash| unicode:: U+022AA .. TRIPLE VERTICAL BAR RIGHT TURNSTILE
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isobox.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isobox.txt
new file mode 100644
index 00000000..17d45bc6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isobox.txt
@@ -0,0 +1,46 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |boxDL| unicode:: U+02557 .. BOX DRAWINGS DOUBLE DOWN AND LEFT
+.. |boxDl| unicode:: U+02556 .. BOX DRAWINGS DOWN DOUBLE AND LEFT SINGLE
+.. |boxdL| unicode:: U+02555 .. BOX DRAWINGS DOWN SINGLE AND LEFT DOUBLE
+.. |boxdl| unicode:: U+02510 .. BOX DRAWINGS LIGHT DOWN AND LEFT
+.. |boxDR| unicode:: U+02554 .. BOX DRAWINGS DOUBLE DOWN AND RIGHT
+.. |boxDr| unicode:: U+02553 .. BOX DRAWINGS DOWN DOUBLE AND RIGHT SINGLE
+.. |boxdR| unicode:: U+02552 .. BOX DRAWINGS DOWN SINGLE AND RIGHT DOUBLE
+.. |boxdr| unicode:: U+0250C .. BOX DRAWINGS LIGHT DOWN AND RIGHT
+.. |boxH| unicode:: U+02550 .. BOX DRAWINGS DOUBLE HORIZONTAL
+.. |boxh| unicode:: U+02500 .. BOX DRAWINGS LIGHT HORIZONTAL
+.. |boxHD| unicode:: U+02566 .. BOX DRAWINGS DOUBLE DOWN AND HORIZONTAL
+.. |boxHd| unicode:: U+02564 .. BOX DRAWINGS DOWN SINGLE AND HORIZONTAL DOUBLE
+.. |boxhD| unicode:: U+02565 .. BOX DRAWINGS DOWN DOUBLE AND HORIZONTAL SINGLE
+.. |boxhd| unicode:: U+0252C .. BOX DRAWINGS LIGHT DOWN AND HORIZONTAL
+.. |boxHU| unicode:: U+02569 .. BOX DRAWINGS DOUBLE UP AND HORIZONTAL
+.. |boxHu| unicode:: U+02567 .. BOX DRAWINGS UP SINGLE AND HORIZONTAL DOUBLE
+.. |boxhU| unicode:: U+02568 .. BOX DRAWINGS UP DOUBLE AND HORIZONTAL SINGLE
+.. |boxhu| unicode:: U+02534 .. BOX DRAWINGS LIGHT UP AND HORIZONTAL
+.. |boxUL| unicode:: U+0255D .. BOX DRAWINGS DOUBLE UP AND LEFT
+.. |boxUl| unicode:: U+0255C .. BOX DRAWINGS UP DOUBLE AND LEFT SINGLE
+.. |boxuL| unicode:: U+0255B .. BOX DRAWINGS UP SINGLE AND LEFT DOUBLE
+.. |boxul| unicode:: U+02518 .. BOX DRAWINGS LIGHT UP AND LEFT
+.. |boxUR| unicode:: U+0255A .. BOX DRAWINGS DOUBLE UP AND RIGHT
+.. |boxUr| unicode:: U+02559 .. BOX DRAWINGS UP DOUBLE AND RIGHT SINGLE
+.. |boxuR| unicode:: U+02558 .. BOX DRAWINGS UP SINGLE AND RIGHT DOUBLE
+.. |boxur| unicode:: U+02514 .. BOX DRAWINGS LIGHT UP AND RIGHT
+.. |boxV| unicode:: U+02551 .. BOX DRAWINGS DOUBLE VERTICAL
+.. |boxv| unicode:: U+02502 .. BOX DRAWINGS LIGHT VERTICAL
+.. |boxVH| unicode:: U+0256C .. BOX DRAWINGS DOUBLE VERTICAL AND HORIZONTAL
+.. |boxVh| unicode:: U+0256B .. BOX DRAWINGS VERTICAL DOUBLE AND HORIZONTAL SINGLE
+.. |boxvH| unicode:: U+0256A .. BOX DRAWINGS VERTICAL SINGLE AND HORIZONTAL DOUBLE
+.. |boxvh| unicode:: U+0253C .. BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL
+.. |boxVL| unicode:: U+02563 .. BOX DRAWINGS DOUBLE VERTICAL AND LEFT
+.. |boxVl| unicode:: U+02562 .. BOX DRAWINGS VERTICAL DOUBLE AND LEFT SINGLE
+.. |boxvL| unicode:: U+02561 .. BOX DRAWINGS VERTICAL SINGLE AND LEFT DOUBLE
+.. |boxvl| unicode:: U+02524 .. BOX DRAWINGS LIGHT VERTICAL AND LEFT
+.. |boxVR| unicode:: U+02560 .. BOX DRAWINGS DOUBLE VERTICAL AND RIGHT
+.. |boxVr| unicode:: U+0255F .. BOX DRAWINGS VERTICAL DOUBLE AND RIGHT SINGLE
+.. |boxvR| unicode:: U+0255E .. BOX DRAWINGS VERTICAL SINGLE AND RIGHT DOUBLE
+.. |boxvr| unicode:: U+0251C .. BOX DRAWINGS LIGHT VERTICAL AND RIGHT
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isocyr1.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isocyr1.txt
new file mode 100644
index 00000000..5e0a18f5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isocyr1.txt
@@ -0,0 +1,73 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Acy| unicode:: U+00410 .. CYRILLIC CAPITAL LETTER A
+.. |acy| unicode:: U+00430 .. CYRILLIC SMALL LETTER A
+.. |Bcy| unicode:: U+00411 .. CYRILLIC CAPITAL LETTER BE
+.. |bcy| unicode:: U+00431 .. CYRILLIC SMALL LETTER BE
+.. |CHcy| unicode:: U+00427 .. CYRILLIC CAPITAL LETTER CHE
+.. |chcy| unicode:: U+00447 .. CYRILLIC SMALL LETTER CHE
+.. |Dcy| unicode:: U+00414 .. CYRILLIC CAPITAL LETTER DE
+.. |dcy| unicode:: U+00434 .. CYRILLIC SMALL LETTER DE
+.. |Ecy| unicode:: U+0042D .. CYRILLIC CAPITAL LETTER E
+.. |ecy| unicode:: U+0044D .. CYRILLIC SMALL LETTER E
+.. |Fcy| unicode:: U+00424 .. CYRILLIC CAPITAL LETTER EF
+.. |fcy| unicode:: U+00444 .. CYRILLIC SMALL LETTER EF
+.. |Gcy| unicode:: U+00413 .. CYRILLIC CAPITAL LETTER GHE
+.. |gcy| unicode:: U+00433 .. CYRILLIC SMALL LETTER GHE
+.. |HARDcy| unicode:: U+0042A .. CYRILLIC CAPITAL LETTER HARD SIGN
+.. |hardcy| unicode:: U+0044A .. CYRILLIC SMALL LETTER HARD SIGN
+.. |Icy| unicode:: U+00418 .. CYRILLIC CAPITAL LETTER I
+.. |icy| unicode:: U+00438 .. CYRILLIC SMALL LETTER I
+.. |IEcy| unicode:: U+00415 .. CYRILLIC CAPITAL LETTER IE
+.. |iecy| unicode:: U+00435 .. CYRILLIC SMALL LETTER IE
+.. |IOcy| unicode:: U+00401 .. CYRILLIC CAPITAL LETTER IO
+.. |iocy| unicode:: U+00451 .. CYRILLIC SMALL LETTER IO
+.. |Jcy| unicode:: U+00419 .. CYRILLIC CAPITAL LETTER SHORT I
+.. |jcy| unicode:: U+00439 .. CYRILLIC SMALL LETTER SHORT I
+.. |Kcy| unicode:: U+0041A .. CYRILLIC CAPITAL LETTER KA
+.. |kcy| unicode:: U+0043A .. CYRILLIC SMALL LETTER KA
+.. |KHcy| unicode:: U+00425 .. CYRILLIC CAPITAL LETTER HA
+.. |khcy| unicode:: U+00445 .. CYRILLIC SMALL LETTER HA
+.. |Lcy| unicode:: U+0041B .. CYRILLIC CAPITAL LETTER EL
+.. |lcy| unicode:: U+0043B .. CYRILLIC SMALL LETTER EL
+.. |Mcy| unicode:: U+0041C .. CYRILLIC CAPITAL LETTER EM
+.. |mcy| unicode:: U+0043C .. CYRILLIC SMALL LETTER EM
+.. |Ncy| unicode:: U+0041D .. CYRILLIC CAPITAL LETTER EN
+.. |ncy| unicode:: U+0043D .. CYRILLIC SMALL LETTER EN
+.. |numero| unicode:: U+02116 .. NUMERO SIGN
+.. |Ocy| unicode:: U+0041E .. CYRILLIC CAPITAL LETTER O
+.. |ocy| unicode:: U+0043E .. CYRILLIC SMALL LETTER O
+.. |Pcy| unicode:: U+0041F .. CYRILLIC CAPITAL LETTER PE
+.. |pcy| unicode:: U+0043F .. CYRILLIC SMALL LETTER PE
+.. |Rcy| unicode:: U+00420 .. CYRILLIC CAPITAL LETTER ER
+.. |rcy| unicode:: U+00440 .. CYRILLIC SMALL LETTER ER
+.. |Scy| unicode:: U+00421 .. CYRILLIC CAPITAL LETTER ES
+.. |scy| unicode:: U+00441 .. CYRILLIC SMALL LETTER ES
+.. |SHCHcy| unicode:: U+00429 .. CYRILLIC CAPITAL LETTER SHCHA
+.. |shchcy| unicode:: U+00449 .. CYRILLIC SMALL LETTER SHCHA
+.. |SHcy| unicode:: U+00428 .. CYRILLIC CAPITAL LETTER SHA
+.. |shcy| unicode:: U+00448 .. CYRILLIC SMALL LETTER SHA
+.. |SOFTcy| unicode:: U+0042C .. CYRILLIC CAPITAL LETTER SOFT SIGN
+.. |softcy| unicode:: U+0044C .. CYRILLIC SMALL LETTER SOFT SIGN
+.. |Tcy| unicode:: U+00422 .. CYRILLIC CAPITAL LETTER TE
+.. |tcy| unicode:: U+00442 .. CYRILLIC SMALL LETTER TE
+.. |TScy| unicode:: U+00426 .. CYRILLIC CAPITAL LETTER TSE
+.. |tscy| unicode:: U+00446 .. CYRILLIC SMALL LETTER TSE
+.. |Ucy| unicode:: U+00423 .. CYRILLIC CAPITAL LETTER U
+.. |ucy| unicode:: U+00443 .. CYRILLIC SMALL LETTER U
+.. |Vcy| unicode:: U+00412 .. CYRILLIC CAPITAL LETTER VE
+.. |vcy| unicode:: U+00432 .. CYRILLIC SMALL LETTER VE
+.. |YAcy| unicode:: U+0042F .. CYRILLIC CAPITAL LETTER YA
+.. |yacy| unicode:: U+0044F .. CYRILLIC SMALL LETTER YA
+.. |Ycy| unicode:: U+0042B .. CYRILLIC CAPITAL LETTER YERU
+.. |ycy| unicode:: U+0044B .. CYRILLIC SMALL LETTER YERU
+.. |YUcy| unicode:: U+0042E .. CYRILLIC CAPITAL LETTER YU
+.. |yucy| unicode:: U+0044E .. CYRILLIC SMALL LETTER YU
+.. |Zcy| unicode:: U+00417 .. CYRILLIC CAPITAL LETTER ZE
+.. |zcy| unicode:: U+00437 .. CYRILLIC SMALL LETTER ZE
+.. |ZHcy| unicode:: U+00416 .. CYRILLIC CAPITAL LETTER ZHE
+.. |zhcy| unicode:: U+00436 .. CYRILLIC SMALL LETTER ZHE
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isocyr2.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isocyr2.txt
new file mode 100644
index 00000000..a78190c0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isocyr2.txt
@@ -0,0 +1,32 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |DJcy| unicode:: U+00402 .. CYRILLIC CAPITAL LETTER DJE
+.. |djcy| unicode:: U+00452 .. CYRILLIC SMALL LETTER DJE
+.. |DScy| unicode:: U+00405 .. CYRILLIC CAPITAL LETTER DZE
+.. |dscy| unicode:: U+00455 .. CYRILLIC SMALL LETTER DZE
+.. |DZcy| unicode:: U+0040F .. CYRILLIC CAPITAL LETTER DZHE
+.. |dzcy| unicode:: U+0045F .. CYRILLIC SMALL LETTER DZHE
+.. |GJcy| unicode:: U+00403 .. CYRILLIC CAPITAL LETTER GJE
+.. |gjcy| unicode:: U+00453 .. CYRILLIC SMALL LETTER GJE
+.. |Iukcy| unicode:: U+00406 .. CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I
+.. |iukcy| unicode:: U+00456 .. CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I
+.. |Jsercy| unicode:: U+00408 .. CYRILLIC CAPITAL LETTER JE
+.. |jsercy| unicode:: U+00458 .. CYRILLIC SMALL LETTER JE
+.. |Jukcy| unicode:: U+00404 .. CYRILLIC CAPITAL LETTER UKRAINIAN IE
+.. |jukcy| unicode:: U+00454 .. CYRILLIC SMALL LETTER UKRAINIAN IE
+.. |KJcy| unicode:: U+0040C .. CYRILLIC CAPITAL LETTER KJE
+.. |kjcy| unicode:: U+0045C .. CYRILLIC SMALL LETTER KJE
+.. |LJcy| unicode:: U+00409 .. CYRILLIC CAPITAL LETTER LJE
+.. |ljcy| unicode:: U+00459 .. CYRILLIC SMALL LETTER LJE
+.. |NJcy| unicode:: U+0040A .. CYRILLIC CAPITAL LETTER NJE
+.. |njcy| unicode:: U+0045A .. CYRILLIC SMALL LETTER NJE
+.. |TSHcy| unicode:: U+0040B .. CYRILLIC CAPITAL LETTER TSHE
+.. |tshcy| unicode:: U+0045B .. CYRILLIC SMALL LETTER TSHE
+.. |Ubrcy| unicode:: U+0040E .. CYRILLIC CAPITAL LETTER SHORT U
+.. |ubrcy| unicode:: U+0045E .. CYRILLIC SMALL LETTER SHORT U
+.. |YIcy| unicode:: U+00407 .. CYRILLIC CAPITAL LETTER YI
+.. |yicy| unicode:: U+00457 .. CYRILLIC SMALL LETTER YI
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isodia.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isodia.txt
new file mode 100644
index 00000000..cfe403ab
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isodia.txt
@@ -0,0 +1,20 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |acute| unicode:: U+000B4 .. ACUTE ACCENT
+.. |breve| unicode:: U+002D8 .. BREVE
+.. |caron| unicode:: U+002C7 .. CARON
+.. |cedil| unicode:: U+000B8 .. CEDILLA
+.. |circ| unicode:: U+002C6 .. MODIFIER LETTER CIRCUMFLEX ACCENT
+.. |dblac| unicode:: U+002DD .. DOUBLE ACUTE ACCENT
+.. |die| unicode:: U+000A8 .. DIAERESIS
+.. |dot| unicode:: U+002D9 .. DOT ABOVE
+.. |grave| unicode:: U+00060 .. GRAVE ACCENT
+.. |macr| unicode:: U+000AF .. MACRON
+.. |ogon| unicode:: U+002DB .. OGONEK
+.. |ring| unicode:: U+002DA .. RING ABOVE
+.. |tilde| unicode:: U+002DC .. SMALL TILDE
+.. |uml| unicode:: U+000A8 .. DIAERESIS
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk1.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk1.txt
new file mode 100644
index 00000000..22a414bb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk1.txt
@@ -0,0 +1,55 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Agr| unicode:: U+00391 .. GREEK CAPITAL LETTER ALPHA
+.. |agr| unicode:: U+003B1 .. GREEK SMALL LETTER ALPHA
+.. |Bgr| unicode:: U+00392 .. GREEK CAPITAL LETTER BETA
+.. |bgr| unicode:: U+003B2 .. GREEK SMALL LETTER BETA
+.. |Dgr| unicode:: U+00394 .. GREEK CAPITAL LETTER DELTA
+.. |dgr| unicode:: U+003B4 .. GREEK SMALL LETTER DELTA
+.. |EEgr| unicode:: U+00397 .. GREEK CAPITAL LETTER ETA
+.. |eegr| unicode:: U+003B7 .. GREEK SMALL LETTER ETA
+.. |Egr| unicode:: U+00395 .. GREEK CAPITAL LETTER EPSILON
+.. |egr| unicode:: U+003B5 .. GREEK SMALL LETTER EPSILON
+.. |Ggr| unicode:: U+00393 .. GREEK CAPITAL LETTER GAMMA
+.. |ggr| unicode:: U+003B3 .. GREEK SMALL LETTER GAMMA
+.. |Igr| unicode:: U+00399 .. GREEK CAPITAL LETTER IOTA
+.. |igr| unicode:: U+003B9 .. GREEK SMALL LETTER IOTA
+.. |Kgr| unicode:: U+0039A .. GREEK CAPITAL LETTER KAPPA
+.. |kgr| unicode:: U+003BA .. GREEK SMALL LETTER KAPPA
+.. |KHgr| unicode:: U+003A7 .. GREEK CAPITAL LETTER CHI
+.. |khgr| unicode:: U+003C7 .. GREEK SMALL LETTER CHI
+.. |Lgr| unicode:: U+0039B .. GREEK CAPITAL LETTER LAMDA
+.. |lgr| unicode:: U+003BB .. GREEK SMALL LETTER LAMDA
+.. |Mgr| unicode:: U+0039C .. GREEK CAPITAL LETTER MU
+.. |mgr| unicode:: U+003BC .. GREEK SMALL LETTER MU
+.. |Ngr| unicode:: U+0039D .. GREEK CAPITAL LETTER NU
+.. |ngr| unicode:: U+003BD .. GREEK SMALL LETTER NU
+.. |Ogr| unicode:: U+0039F .. GREEK CAPITAL LETTER OMICRON
+.. |ogr| unicode:: U+003BF .. GREEK SMALL LETTER OMICRON
+.. |OHgr| unicode:: U+003A9 .. GREEK CAPITAL LETTER OMEGA
+.. |ohgr| unicode:: U+003C9 .. GREEK SMALL LETTER OMEGA
+.. |Pgr| unicode:: U+003A0 .. GREEK CAPITAL LETTER PI
+.. |pgr| unicode:: U+003C0 .. GREEK SMALL LETTER PI
+.. |PHgr| unicode:: U+003A6 .. GREEK CAPITAL LETTER PHI
+.. |phgr| unicode:: U+003C6 .. GREEK SMALL LETTER PHI
+.. |PSgr| unicode:: U+003A8 .. GREEK CAPITAL LETTER PSI
+.. |psgr| unicode:: U+003C8 .. GREEK SMALL LETTER PSI
+.. |Rgr| unicode:: U+003A1 .. GREEK CAPITAL LETTER RHO
+.. |rgr| unicode:: U+003C1 .. GREEK SMALL LETTER RHO
+.. |sfgr| unicode:: U+003C2 .. GREEK SMALL LETTER FINAL SIGMA
+.. |Sgr| unicode:: U+003A3 .. GREEK CAPITAL LETTER SIGMA
+.. |sgr| unicode:: U+003C3 .. GREEK SMALL LETTER SIGMA
+.. |Tgr| unicode:: U+003A4 .. GREEK CAPITAL LETTER TAU
+.. |tgr| unicode:: U+003C4 .. GREEK SMALL LETTER TAU
+.. |THgr| unicode:: U+00398 .. GREEK CAPITAL LETTER THETA
+.. |thgr| unicode:: U+003B8 .. GREEK SMALL LETTER THETA
+.. |Ugr| unicode:: U+003A5 .. GREEK CAPITAL LETTER UPSILON
+.. |ugr| unicode:: U+003C5 .. GREEK SMALL LETTER UPSILON
+.. |Xgr| unicode:: U+0039E .. GREEK CAPITAL LETTER XI
+.. |xgr| unicode:: U+003BE .. GREEK SMALL LETTER XI
+.. |Zgr| unicode:: U+00396 .. GREEK CAPITAL LETTER ZETA
+.. |zgr| unicode:: U+003B6 .. GREEK SMALL LETTER ZETA
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk2.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk2.txt
new file mode 100644
index 00000000..4b4090ec
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk2.txt
@@ -0,0 +1,26 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Aacgr| unicode:: U+00386 .. GREEK CAPITAL LETTER ALPHA WITH TONOS
+.. |aacgr| unicode:: U+003AC .. GREEK SMALL LETTER ALPHA WITH TONOS
+.. |Eacgr| unicode:: U+00388 .. GREEK CAPITAL LETTER EPSILON WITH TONOS
+.. |eacgr| unicode:: U+003AD .. GREEK SMALL LETTER EPSILON WITH TONOS
+.. |EEacgr| unicode:: U+00389 .. GREEK CAPITAL LETTER ETA WITH TONOS
+.. |eeacgr| unicode:: U+003AE .. GREEK SMALL LETTER ETA WITH TONOS
+.. |Iacgr| unicode:: U+0038A .. GREEK CAPITAL LETTER IOTA WITH TONOS
+.. |iacgr| unicode:: U+003AF .. GREEK SMALL LETTER IOTA WITH TONOS
+.. |idiagr| unicode:: U+00390 .. GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS
+.. |Idigr| unicode:: U+003AA .. GREEK CAPITAL LETTER IOTA WITH DIALYTIKA
+.. |idigr| unicode:: U+003CA .. GREEK SMALL LETTER IOTA WITH DIALYTIKA
+.. |Oacgr| unicode:: U+0038C .. GREEK CAPITAL LETTER OMICRON WITH TONOS
+.. |oacgr| unicode:: U+003CC .. GREEK SMALL LETTER OMICRON WITH TONOS
+.. |OHacgr| unicode:: U+0038F .. GREEK CAPITAL LETTER OMEGA WITH TONOS
+.. |ohacgr| unicode:: U+003CE .. GREEK SMALL LETTER OMEGA WITH TONOS
+.. |Uacgr| unicode:: U+0038E .. GREEK CAPITAL LETTER UPSILON WITH TONOS
+.. |uacgr| unicode:: U+003CD .. GREEK SMALL LETTER UPSILON WITH TONOS
+.. |udiagr| unicode:: U+003B0 .. GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS
+.. |Udigr| unicode:: U+003AB .. GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA
+.. |udigr| unicode:: U+003CB .. GREEK SMALL LETTER UPSILON WITH DIALYTIKA
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk3.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk3.txt
new file mode 100644
index 00000000..54d212f2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk3.txt
@@ -0,0 +1,52 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |alpha| unicode:: U+003B1 .. GREEK SMALL LETTER ALPHA
+.. |beta| unicode:: U+003B2 .. GREEK SMALL LETTER BETA
+.. |chi| unicode:: U+003C7 .. GREEK SMALL LETTER CHI
+.. |Delta| unicode:: U+00394 .. GREEK CAPITAL LETTER DELTA
+.. |delta| unicode:: U+003B4 .. GREEK SMALL LETTER DELTA
+.. |epsi| unicode:: U+003F5 .. GREEK LUNATE EPSILON SYMBOL
+.. |epsis| unicode:: U+003F5 .. GREEK LUNATE EPSILON SYMBOL
+.. |epsiv| unicode:: U+003B5 .. GREEK SMALL LETTER EPSILON
+.. |eta| unicode:: U+003B7 .. GREEK SMALL LETTER ETA
+.. |Gamma| unicode:: U+00393 .. GREEK CAPITAL LETTER GAMMA
+.. |gamma| unicode:: U+003B3 .. GREEK SMALL LETTER GAMMA
+.. |Gammad| unicode:: U+003DC .. GREEK LETTER DIGAMMA
+.. |gammad| unicode:: U+003DD .. GREEK SMALL LETTER DIGAMMA
+.. |iota| unicode:: U+003B9 .. GREEK SMALL LETTER IOTA
+.. |kappa| unicode:: U+003BA .. GREEK SMALL LETTER KAPPA
+.. |kappav| unicode:: U+003F0 .. GREEK KAPPA SYMBOL
+.. |Lambda| unicode:: U+0039B .. GREEK CAPITAL LETTER LAMDA
+.. |lambda| unicode:: U+003BB .. GREEK SMALL LETTER LAMDA
+.. |mu| unicode:: U+003BC .. GREEK SMALL LETTER MU
+.. |nu| unicode:: U+003BD .. GREEK SMALL LETTER NU
+.. |Omega| unicode:: U+003A9 .. GREEK CAPITAL LETTER OMEGA
+.. |omega| unicode:: U+003C9 .. GREEK SMALL LETTER OMEGA
+.. |Phi| unicode:: U+003A6 .. GREEK CAPITAL LETTER PHI
+.. |phi| unicode:: U+003D5 .. GREEK PHI SYMBOL
+.. |phis| unicode:: U+003D5 .. GREEK PHI SYMBOL
+.. |phiv| unicode:: U+003C6 .. GREEK SMALL LETTER PHI
+.. |Pi| unicode:: U+003A0 .. GREEK CAPITAL LETTER PI
+.. |pi| unicode:: U+003C0 .. GREEK SMALL LETTER PI
+.. |piv| unicode:: U+003D6 .. GREEK PI SYMBOL
+.. |Psi| unicode:: U+003A8 .. GREEK CAPITAL LETTER PSI
+.. |psi| unicode:: U+003C8 .. GREEK SMALL LETTER PSI
+.. |rho| unicode:: U+003C1 .. GREEK SMALL LETTER RHO
+.. |rhov| unicode:: U+003F1 .. GREEK RHO SYMBOL
+.. |Sigma| unicode:: U+003A3 .. GREEK CAPITAL LETTER SIGMA
+.. |sigma| unicode:: U+003C3 .. GREEK SMALL LETTER SIGMA
+.. |sigmav| unicode:: U+003C2 .. GREEK SMALL LETTER FINAL SIGMA
+.. |tau| unicode:: U+003C4 .. GREEK SMALL LETTER TAU
+.. |Theta| unicode:: U+00398 .. GREEK CAPITAL LETTER THETA
+.. |theta| unicode:: U+003B8 .. GREEK SMALL LETTER THETA
+.. |thetas| unicode:: U+003B8 .. GREEK SMALL LETTER THETA
+.. |thetav| unicode:: U+003D1 .. GREEK THETA SYMBOL
+.. |Upsi| unicode:: U+003D2 .. GREEK UPSILON WITH HOOK SYMBOL
+.. |upsi| unicode:: U+003C5 .. GREEK SMALL LETTER UPSILON
+.. |Xi| unicode:: U+0039E .. GREEK CAPITAL LETTER XI
+.. |xi| unicode:: U+003BE .. GREEK SMALL LETTER XI
+.. |zeta| unicode:: U+003B6 .. GREEK SMALL LETTER ZETA
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk4-wide.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk4-wide.txt
new file mode 100644
index 00000000..c0e0238d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk4-wide.txt
@@ -0,0 +1,49 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |b.alpha| unicode:: U+1D6C2 .. MATHEMATICAL BOLD SMALL ALPHA
+.. |b.beta| unicode:: U+1D6C3 .. MATHEMATICAL BOLD SMALL BETA
+.. |b.chi| unicode:: U+1D6D8 .. MATHEMATICAL BOLD SMALL CHI
+.. |b.Delta| unicode:: U+1D6AB .. MATHEMATICAL BOLD CAPITAL DELTA
+.. |b.delta| unicode:: U+1D6C5 .. MATHEMATICAL BOLD SMALL DELTA
+.. |b.epsi| unicode:: U+1D6C6 .. MATHEMATICAL BOLD SMALL EPSILON
+.. |b.epsiv| unicode:: U+1D6DC .. MATHEMATICAL BOLD EPSILON SYMBOL
+.. |b.eta| unicode:: U+1D6C8 .. MATHEMATICAL BOLD SMALL ETA
+.. |b.Gamma| unicode:: U+1D6AA .. MATHEMATICAL BOLD CAPITAL GAMMA
+.. |b.gamma| unicode:: U+1D6C4 .. MATHEMATICAL BOLD SMALL GAMMA
+.. |b.Gammad| unicode:: U+003DC .. GREEK LETTER DIGAMMA
+.. |b.gammad| unicode:: U+003DD .. GREEK SMALL LETTER DIGAMMA
+.. |b.iota| unicode:: U+1D6CA .. MATHEMATICAL BOLD SMALL IOTA
+.. |b.kappa| unicode:: U+1D6CB .. MATHEMATICAL BOLD SMALL KAPPA
+.. |b.kappav| unicode:: U+1D6DE .. MATHEMATICAL BOLD KAPPA SYMBOL
+.. |b.Lambda| unicode:: U+1D6B2 .. MATHEMATICAL BOLD CAPITAL LAMDA
+.. |b.lambda| unicode:: U+1D6CC .. MATHEMATICAL BOLD SMALL LAMDA
+.. |b.mu| unicode:: U+1D6CD .. MATHEMATICAL BOLD SMALL MU
+.. |b.nu| unicode:: U+1D6CE .. MATHEMATICAL BOLD SMALL NU
+.. |b.Omega| unicode:: U+1D6C0 .. MATHEMATICAL BOLD CAPITAL OMEGA
+.. |b.omega| unicode:: U+1D6DA .. MATHEMATICAL BOLD SMALL OMEGA
+.. |b.Phi| unicode:: U+1D6BD .. MATHEMATICAL BOLD CAPITAL PHI
+.. |b.phi| unicode:: U+1D6D7 .. MATHEMATICAL BOLD SMALL PHI
+.. |b.phiv| unicode:: U+1D6DF .. MATHEMATICAL BOLD PHI SYMBOL
+.. |b.Pi| unicode:: U+1D6B7 .. MATHEMATICAL BOLD CAPITAL PI
+.. |b.pi| unicode:: U+1D6D1 .. MATHEMATICAL BOLD SMALL PI
+.. |b.piv| unicode:: U+1D6E1 .. MATHEMATICAL BOLD PI SYMBOL
+.. |b.Psi| unicode:: U+1D6BF .. MATHEMATICAL BOLD CAPITAL PSI
+.. |b.psi| unicode:: U+1D6D9 .. MATHEMATICAL BOLD SMALL PSI
+.. |b.rho| unicode:: U+1D6D2 .. MATHEMATICAL BOLD SMALL RHO
+.. |b.rhov| unicode:: U+1D6E0 .. MATHEMATICAL BOLD RHO SYMBOL
+.. |b.Sigma| unicode:: U+1D6BA .. MATHEMATICAL BOLD CAPITAL SIGMA
+.. |b.sigma| unicode:: U+1D6D4 .. MATHEMATICAL BOLD SMALL SIGMA
+.. |b.sigmav| unicode:: U+1D6D3 .. MATHEMATICAL BOLD SMALL FINAL SIGMA
+.. |b.tau| unicode:: U+1D6D5 .. MATHEMATICAL BOLD SMALL TAU
+.. |b.Theta| unicode:: U+1D6AF .. MATHEMATICAL BOLD CAPITAL THETA
+.. |b.thetas| unicode:: U+1D6C9 .. MATHEMATICAL BOLD SMALL THETA
+.. |b.thetav| unicode:: U+1D6DD .. MATHEMATICAL BOLD THETA SYMBOL
+.. |b.Upsi| unicode:: U+1D6BC .. MATHEMATICAL BOLD CAPITAL UPSILON
+.. |b.upsi| unicode:: U+1D6D6 .. MATHEMATICAL BOLD SMALL UPSILON
+.. |b.Xi| unicode:: U+1D6B5 .. MATHEMATICAL BOLD CAPITAL XI
+.. |b.xi| unicode:: U+1D6CF .. MATHEMATICAL BOLD SMALL XI
+.. |b.zeta| unicode:: U+1D6C7 .. MATHEMATICAL BOLD SMALL ZETA
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk4.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk4.txt
new file mode 100644
index 00000000..836b6bd7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isogrk4.txt
@@ -0,0 +1,8 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |b.Gammad| unicode:: U+003DC .. GREEK LETTER DIGAMMA
+.. |b.gammad| unicode:: U+003DD .. GREEK SMALL LETTER DIGAMMA
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isolat1.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isolat1.txt
new file mode 100644
index 00000000..4e4202b7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isolat1.txt
@@ -0,0 +1,68 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Aacute| unicode:: U+000C1 .. LATIN CAPITAL LETTER A WITH ACUTE
+.. |aacute| unicode:: U+000E1 .. LATIN SMALL LETTER A WITH ACUTE
+.. |Acirc| unicode:: U+000C2 .. LATIN CAPITAL LETTER A WITH CIRCUMFLEX
+.. |acirc| unicode:: U+000E2 .. LATIN SMALL LETTER A WITH CIRCUMFLEX
+.. |AElig| unicode:: U+000C6 .. LATIN CAPITAL LETTER AE
+.. |aelig| unicode:: U+000E6 .. LATIN SMALL LETTER AE
+.. |Agrave| unicode:: U+000C0 .. LATIN CAPITAL LETTER A WITH GRAVE
+.. |agrave| unicode:: U+000E0 .. LATIN SMALL LETTER A WITH GRAVE
+.. |Aring| unicode:: U+000C5 .. LATIN CAPITAL LETTER A WITH RING ABOVE
+.. |aring| unicode:: U+000E5 .. LATIN SMALL LETTER A WITH RING ABOVE
+.. |Atilde| unicode:: U+000C3 .. LATIN CAPITAL LETTER A WITH TILDE
+.. |atilde| unicode:: U+000E3 .. LATIN SMALL LETTER A WITH TILDE
+.. |Auml| unicode:: U+000C4 .. LATIN CAPITAL LETTER A WITH DIAERESIS
+.. |auml| unicode:: U+000E4 .. LATIN SMALL LETTER A WITH DIAERESIS
+.. |Ccedil| unicode:: U+000C7 .. LATIN CAPITAL LETTER C WITH CEDILLA
+.. |ccedil| unicode:: U+000E7 .. LATIN SMALL LETTER C WITH CEDILLA
+.. |Eacute| unicode:: U+000C9 .. LATIN CAPITAL LETTER E WITH ACUTE
+.. |eacute| unicode:: U+000E9 .. LATIN SMALL LETTER E WITH ACUTE
+.. |Ecirc| unicode:: U+000CA .. LATIN CAPITAL LETTER E WITH CIRCUMFLEX
+.. |ecirc| unicode:: U+000EA .. LATIN SMALL LETTER E WITH CIRCUMFLEX
+.. |Egrave| unicode:: U+000C8 .. LATIN CAPITAL LETTER E WITH GRAVE
+.. |egrave| unicode:: U+000E8 .. LATIN SMALL LETTER E WITH GRAVE
+.. |ETH| unicode:: U+000D0 .. LATIN CAPITAL LETTER ETH
+.. |eth| unicode:: U+000F0 .. LATIN SMALL LETTER ETH
+.. |Euml| unicode:: U+000CB .. LATIN CAPITAL LETTER E WITH DIAERESIS
+.. |euml| unicode:: U+000EB .. LATIN SMALL LETTER E WITH DIAERESIS
+.. |Iacute| unicode:: U+000CD .. LATIN CAPITAL LETTER I WITH ACUTE
+.. |iacute| unicode:: U+000ED .. LATIN SMALL LETTER I WITH ACUTE
+.. |Icirc| unicode:: U+000CE .. LATIN CAPITAL LETTER I WITH CIRCUMFLEX
+.. |icirc| unicode:: U+000EE .. LATIN SMALL LETTER I WITH CIRCUMFLEX
+.. |Igrave| unicode:: U+000CC .. LATIN CAPITAL LETTER I WITH GRAVE
+.. |igrave| unicode:: U+000EC .. LATIN SMALL LETTER I WITH GRAVE
+.. |Iuml| unicode:: U+000CF .. LATIN CAPITAL LETTER I WITH DIAERESIS
+.. |iuml| unicode:: U+000EF .. LATIN SMALL LETTER I WITH DIAERESIS
+.. |Ntilde| unicode:: U+000D1 .. LATIN CAPITAL LETTER N WITH TILDE
+.. |ntilde| unicode:: U+000F1 .. LATIN SMALL LETTER N WITH TILDE
+.. |Oacute| unicode:: U+000D3 .. LATIN CAPITAL LETTER O WITH ACUTE
+.. |oacute| unicode:: U+000F3 .. LATIN SMALL LETTER O WITH ACUTE
+.. |Ocirc| unicode:: U+000D4 .. LATIN CAPITAL LETTER O WITH CIRCUMFLEX
+.. |ocirc| unicode:: U+000F4 .. LATIN SMALL LETTER O WITH CIRCUMFLEX
+.. |Ograve| unicode:: U+000D2 .. LATIN CAPITAL LETTER O WITH GRAVE
+.. |ograve| unicode:: U+000F2 .. LATIN SMALL LETTER O WITH GRAVE
+.. |Oslash| unicode:: U+000D8 .. LATIN CAPITAL LETTER O WITH STROKE
+.. |oslash| unicode:: U+000F8 .. LATIN SMALL LETTER O WITH STROKE
+.. |Otilde| unicode:: U+000D5 .. LATIN CAPITAL LETTER O WITH TILDE
+.. |otilde| unicode:: U+000F5 .. LATIN SMALL LETTER O WITH TILDE
+.. |Ouml| unicode:: U+000D6 .. LATIN CAPITAL LETTER O WITH DIAERESIS
+.. |ouml| unicode:: U+000F6 .. LATIN SMALL LETTER O WITH DIAERESIS
+.. |szlig| unicode:: U+000DF .. LATIN SMALL LETTER SHARP S
+.. |THORN| unicode:: U+000DE .. LATIN CAPITAL LETTER THORN
+.. |thorn| unicode:: U+000FE .. LATIN SMALL LETTER THORN
+.. |Uacute| unicode:: U+000DA .. LATIN CAPITAL LETTER U WITH ACUTE
+.. |uacute| unicode:: U+000FA .. LATIN SMALL LETTER U WITH ACUTE
+.. |Ucirc| unicode:: U+000DB .. LATIN CAPITAL LETTER U WITH CIRCUMFLEX
+.. |ucirc| unicode:: U+000FB .. LATIN SMALL LETTER U WITH CIRCUMFLEX
+.. |Ugrave| unicode:: U+000D9 .. LATIN CAPITAL LETTER U WITH GRAVE
+.. |ugrave| unicode:: U+000F9 .. LATIN SMALL LETTER U WITH GRAVE
+.. |Uuml| unicode:: U+000DC .. LATIN CAPITAL LETTER U WITH DIAERESIS
+.. |uuml| unicode:: U+000FC .. LATIN SMALL LETTER U WITH DIAERESIS
+.. |Yacute| unicode:: U+000DD .. LATIN CAPITAL LETTER Y WITH ACUTE
+.. |yacute| unicode:: U+000FD .. LATIN SMALL LETTER Y WITH ACUTE
+.. |yuml| unicode:: U+000FF .. LATIN SMALL LETTER Y WITH DIAERESIS
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isolat2.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isolat2.txt
new file mode 100644
index 00000000..808ef937
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isolat2.txt
@@ -0,0 +1,128 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Abreve| unicode:: U+00102 .. LATIN CAPITAL LETTER A WITH BREVE
+.. |abreve| unicode:: U+00103 .. LATIN SMALL LETTER A WITH BREVE
+.. |Amacr| unicode:: U+00100 .. LATIN CAPITAL LETTER A WITH MACRON
+.. |amacr| unicode:: U+00101 .. LATIN SMALL LETTER A WITH MACRON
+.. |Aogon| unicode:: U+00104 .. LATIN CAPITAL LETTER A WITH OGONEK
+.. |aogon| unicode:: U+00105 .. LATIN SMALL LETTER A WITH OGONEK
+.. |Cacute| unicode:: U+00106 .. LATIN CAPITAL LETTER C WITH ACUTE
+.. |cacute| unicode:: U+00107 .. LATIN SMALL LETTER C WITH ACUTE
+.. |Ccaron| unicode:: U+0010C .. LATIN CAPITAL LETTER C WITH CARON
+.. |ccaron| unicode:: U+0010D .. LATIN SMALL LETTER C WITH CARON
+.. |Ccirc| unicode:: U+00108 .. LATIN CAPITAL LETTER C WITH CIRCUMFLEX
+.. |ccirc| unicode:: U+00109 .. LATIN SMALL LETTER C WITH CIRCUMFLEX
+.. |Cdot| unicode:: U+0010A .. LATIN CAPITAL LETTER C WITH DOT ABOVE
+.. |cdot| unicode:: U+0010B .. LATIN SMALL LETTER C WITH DOT ABOVE
+.. |Dcaron| unicode:: U+0010E .. LATIN CAPITAL LETTER D WITH CARON
+.. |dcaron| unicode:: U+0010F .. LATIN SMALL LETTER D WITH CARON
+.. |Dstrok| unicode:: U+00110 .. LATIN CAPITAL LETTER D WITH STROKE
+.. |dstrok| unicode:: U+00111 .. LATIN SMALL LETTER D WITH STROKE
+.. |Ecaron| unicode:: U+0011A .. LATIN CAPITAL LETTER E WITH CARON
+.. |ecaron| unicode:: U+0011B .. LATIN SMALL LETTER E WITH CARON
+.. |Edot| unicode:: U+00116 .. LATIN CAPITAL LETTER E WITH DOT ABOVE
+.. |edot| unicode:: U+00117 .. LATIN SMALL LETTER E WITH DOT ABOVE
+.. |Emacr| unicode:: U+00112 .. LATIN CAPITAL LETTER E WITH MACRON
+.. |emacr| unicode:: U+00113 .. LATIN SMALL LETTER E WITH MACRON
+.. |ENG| unicode:: U+0014A .. LATIN CAPITAL LETTER ENG
+.. |eng| unicode:: U+0014B .. LATIN SMALL LETTER ENG
+.. |Eogon| unicode:: U+00118 .. LATIN CAPITAL LETTER E WITH OGONEK
+.. |eogon| unicode:: U+00119 .. LATIN SMALL LETTER E WITH OGONEK
+.. |gacute| unicode:: U+001F5 .. LATIN SMALL LETTER G WITH ACUTE
+.. |Gbreve| unicode:: U+0011E .. LATIN CAPITAL LETTER G WITH BREVE
+.. |gbreve| unicode:: U+0011F .. LATIN SMALL LETTER G WITH BREVE
+.. |Gcedil| unicode:: U+00122 .. LATIN CAPITAL LETTER G WITH CEDILLA
+.. |gcedil| unicode:: U+00123 .. LATIN SMALL LETTER G WITH CEDILLA
+.. |Gcirc| unicode:: U+0011C .. LATIN CAPITAL LETTER G WITH CIRCUMFLEX
+.. |gcirc| unicode:: U+0011D .. LATIN SMALL LETTER G WITH CIRCUMFLEX
+.. |Gdot| unicode:: U+00120 .. LATIN CAPITAL LETTER G WITH DOT ABOVE
+.. |gdot| unicode:: U+00121 .. LATIN SMALL LETTER G WITH DOT ABOVE
+.. |Hcirc| unicode:: U+00124 .. LATIN CAPITAL LETTER H WITH CIRCUMFLEX
+.. |hcirc| unicode:: U+00125 .. LATIN SMALL LETTER H WITH CIRCUMFLEX
+.. |Hstrok| unicode:: U+00126 .. LATIN CAPITAL LETTER H WITH STROKE
+.. |hstrok| unicode:: U+00127 .. LATIN SMALL LETTER H WITH STROKE
+.. |Idot| unicode:: U+00130 .. LATIN CAPITAL LETTER I WITH DOT ABOVE
+.. |IJlig| unicode:: U+00132 .. LATIN CAPITAL LIGATURE IJ
+.. |ijlig| unicode:: U+00133 .. LATIN SMALL LIGATURE IJ
+.. |Imacr| unicode:: U+0012A .. LATIN CAPITAL LETTER I WITH MACRON
+.. |imacr| unicode:: U+0012B .. LATIN SMALL LETTER I WITH MACRON
+.. |inodot| unicode:: U+00131 .. LATIN SMALL LETTER DOTLESS I
+.. |Iogon| unicode:: U+0012E .. LATIN CAPITAL LETTER I WITH OGONEK
+.. |iogon| unicode:: U+0012F .. LATIN SMALL LETTER I WITH OGONEK
+.. |Itilde| unicode:: U+00128 .. LATIN CAPITAL LETTER I WITH TILDE
+.. |itilde| unicode:: U+00129 .. LATIN SMALL LETTER I WITH TILDE
+.. |Jcirc| unicode:: U+00134 .. LATIN CAPITAL LETTER J WITH CIRCUMFLEX
+.. |jcirc| unicode:: U+00135 .. LATIN SMALL LETTER J WITH CIRCUMFLEX
+.. |Kcedil| unicode:: U+00136 .. LATIN CAPITAL LETTER K WITH CEDILLA
+.. |kcedil| unicode:: U+00137 .. LATIN SMALL LETTER K WITH CEDILLA
+.. |kgreen| unicode:: U+00138 .. LATIN SMALL LETTER KRA
+.. |Lacute| unicode:: U+00139 .. LATIN CAPITAL LETTER L WITH ACUTE
+.. |lacute| unicode:: U+0013A .. LATIN SMALL LETTER L WITH ACUTE
+.. |Lcaron| unicode:: U+0013D .. LATIN CAPITAL LETTER L WITH CARON
+.. |lcaron| unicode:: U+0013E .. LATIN SMALL LETTER L WITH CARON
+.. |Lcedil| unicode:: U+0013B .. LATIN CAPITAL LETTER L WITH CEDILLA
+.. |lcedil| unicode:: U+0013C .. LATIN SMALL LETTER L WITH CEDILLA
+.. |Lmidot| unicode:: U+0013F .. LATIN CAPITAL LETTER L WITH MIDDLE DOT
+.. |lmidot| unicode:: U+00140 .. LATIN SMALL LETTER L WITH MIDDLE DOT
+.. |Lstrok| unicode:: U+00141 .. LATIN CAPITAL LETTER L WITH STROKE
+.. |lstrok| unicode:: U+00142 .. LATIN SMALL LETTER L WITH STROKE
+.. |Nacute| unicode:: U+00143 .. LATIN CAPITAL LETTER N WITH ACUTE
+.. |nacute| unicode:: U+00144 .. LATIN SMALL LETTER N WITH ACUTE
+.. |napos| unicode:: U+00149 .. LATIN SMALL LETTER N PRECEDED BY APOSTROPHE
+.. |Ncaron| unicode:: U+00147 .. LATIN CAPITAL LETTER N WITH CARON
+.. |ncaron| unicode:: U+00148 .. LATIN SMALL LETTER N WITH CARON
+.. |Ncedil| unicode:: U+00145 .. LATIN CAPITAL LETTER N WITH CEDILLA
+.. |ncedil| unicode:: U+00146 .. LATIN SMALL LETTER N WITH CEDILLA
+.. |Odblac| unicode:: U+00150 .. LATIN CAPITAL LETTER O WITH DOUBLE ACUTE
+.. |odblac| unicode:: U+00151 .. LATIN SMALL LETTER O WITH DOUBLE ACUTE
+.. |OElig| unicode:: U+00152 .. LATIN CAPITAL LIGATURE OE
+.. |oelig| unicode:: U+00153 .. LATIN SMALL LIGATURE OE
+.. |Omacr| unicode:: U+0014C .. LATIN CAPITAL LETTER O WITH MACRON
+.. |omacr| unicode:: U+0014D .. LATIN SMALL LETTER O WITH MACRON
+.. |Racute| unicode:: U+00154 .. LATIN CAPITAL LETTER R WITH ACUTE
+.. |racute| unicode:: U+00155 .. LATIN SMALL LETTER R WITH ACUTE
+.. |Rcaron| unicode:: U+00158 .. LATIN CAPITAL LETTER R WITH CARON
+.. |rcaron| unicode:: U+00159 .. LATIN SMALL LETTER R WITH CARON
+.. |Rcedil| unicode:: U+00156 .. LATIN CAPITAL LETTER R WITH CEDILLA
+.. |rcedil| unicode:: U+00157 .. LATIN SMALL LETTER R WITH CEDILLA
+.. |Sacute| unicode:: U+0015A .. LATIN CAPITAL LETTER S WITH ACUTE
+.. |sacute| unicode:: U+0015B .. LATIN SMALL LETTER S WITH ACUTE
+.. |Scaron| unicode:: U+00160 .. LATIN CAPITAL LETTER S WITH CARON
+.. |scaron| unicode:: U+00161 .. LATIN SMALL LETTER S WITH CARON
+.. |Scedil| unicode:: U+0015E .. LATIN CAPITAL LETTER S WITH CEDILLA
+.. |scedil| unicode:: U+0015F .. LATIN SMALL LETTER S WITH CEDILLA
+.. |Scirc| unicode:: U+0015C .. LATIN CAPITAL LETTER S WITH CIRCUMFLEX
+.. |scirc| unicode:: U+0015D .. LATIN SMALL LETTER S WITH CIRCUMFLEX
+.. |Tcaron| unicode:: U+00164 .. LATIN CAPITAL LETTER T WITH CARON
+.. |tcaron| unicode:: U+00165 .. LATIN SMALL LETTER T WITH CARON
+.. |Tcedil| unicode:: U+00162 .. LATIN CAPITAL LETTER T WITH CEDILLA
+.. |tcedil| unicode:: U+00163 .. LATIN SMALL LETTER T WITH CEDILLA
+.. |Tstrok| unicode:: U+00166 .. LATIN CAPITAL LETTER T WITH STROKE
+.. |tstrok| unicode:: U+00167 .. LATIN SMALL LETTER T WITH STROKE
+.. |Ubreve| unicode:: U+0016C .. LATIN CAPITAL LETTER U WITH BREVE
+.. |ubreve| unicode:: U+0016D .. LATIN SMALL LETTER U WITH BREVE
+.. |Udblac| unicode:: U+00170 .. LATIN CAPITAL LETTER U WITH DOUBLE ACUTE
+.. |udblac| unicode:: U+00171 .. LATIN SMALL LETTER U WITH DOUBLE ACUTE
+.. |Umacr| unicode:: U+0016A .. LATIN CAPITAL LETTER U WITH MACRON
+.. |umacr| unicode:: U+0016B .. LATIN SMALL LETTER U WITH MACRON
+.. |Uogon| unicode:: U+00172 .. LATIN CAPITAL LETTER U WITH OGONEK
+.. |uogon| unicode:: U+00173 .. LATIN SMALL LETTER U WITH OGONEK
+.. |Uring| unicode:: U+0016E .. LATIN CAPITAL LETTER U WITH RING ABOVE
+.. |uring| unicode:: U+0016F .. LATIN SMALL LETTER U WITH RING ABOVE
+.. |Utilde| unicode:: U+00168 .. LATIN CAPITAL LETTER U WITH TILDE
+.. |utilde| unicode:: U+00169 .. LATIN SMALL LETTER U WITH TILDE
+.. |Wcirc| unicode:: U+00174 .. LATIN CAPITAL LETTER W WITH CIRCUMFLEX
+.. |wcirc| unicode:: U+00175 .. LATIN SMALL LETTER W WITH CIRCUMFLEX
+.. |Ycirc| unicode:: U+00176 .. LATIN CAPITAL LETTER Y WITH CIRCUMFLEX
+.. |ycirc| unicode:: U+00177 .. LATIN SMALL LETTER Y WITH CIRCUMFLEX
+.. |Yuml| unicode:: U+00178 .. LATIN CAPITAL LETTER Y WITH DIAERESIS
+.. |Zacute| unicode:: U+00179 .. LATIN CAPITAL LETTER Z WITH ACUTE
+.. |zacute| unicode:: U+0017A .. LATIN SMALL LETTER Z WITH ACUTE
+.. |Zcaron| unicode:: U+0017D .. LATIN CAPITAL LETTER Z WITH CARON
+.. |zcaron| unicode:: U+0017E .. LATIN SMALL LETTER Z WITH CARON
+.. |Zdot| unicode:: U+0017B .. LATIN CAPITAL LETTER Z WITH DOT ABOVE
+.. |zdot| unicode:: U+0017C .. LATIN SMALL LETTER Z WITH DOT ABOVE
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomfrk-wide.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomfrk-wide.txt
new file mode 100644
index 00000000..73768bcc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomfrk-wide.txt
@@ -0,0 +1,58 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Afr| unicode:: U+1D504 .. MATHEMATICAL FRAKTUR CAPITAL A
+.. |afr| unicode:: U+1D51E .. MATHEMATICAL FRAKTUR SMALL A
+.. |Bfr| unicode:: U+1D505 .. MATHEMATICAL FRAKTUR CAPITAL B
+.. |bfr| unicode:: U+1D51F .. MATHEMATICAL FRAKTUR SMALL B
+.. |Cfr| unicode:: U+0212D .. BLACK-LETTER CAPITAL C
+.. |cfr| unicode:: U+1D520 .. MATHEMATICAL FRAKTUR SMALL C
+.. |Dfr| unicode:: U+1D507 .. MATHEMATICAL FRAKTUR CAPITAL D
+.. |dfr| unicode:: U+1D521 .. MATHEMATICAL FRAKTUR SMALL D
+.. |Efr| unicode:: U+1D508 .. MATHEMATICAL FRAKTUR CAPITAL E
+.. |efr| unicode:: U+1D522 .. MATHEMATICAL FRAKTUR SMALL E
+.. |Ffr| unicode:: U+1D509 .. MATHEMATICAL FRAKTUR CAPITAL F
+.. |ffr| unicode:: U+1D523 .. MATHEMATICAL FRAKTUR SMALL F
+.. |Gfr| unicode:: U+1D50A .. MATHEMATICAL FRAKTUR CAPITAL G
+.. |gfr| unicode:: U+1D524 .. MATHEMATICAL FRAKTUR SMALL G
+.. |Hfr| unicode:: U+0210C .. BLACK-LETTER CAPITAL H
+.. |hfr| unicode:: U+1D525 .. MATHEMATICAL FRAKTUR SMALL H
+.. |Ifr| unicode:: U+02111 .. BLACK-LETTER CAPITAL I
+.. |ifr| unicode:: U+1D526 .. MATHEMATICAL FRAKTUR SMALL I
+.. |Jfr| unicode:: U+1D50D .. MATHEMATICAL FRAKTUR CAPITAL J
+.. |jfr| unicode:: U+1D527 .. MATHEMATICAL FRAKTUR SMALL J
+.. |Kfr| unicode:: U+1D50E .. MATHEMATICAL FRAKTUR CAPITAL K
+.. |kfr| unicode:: U+1D528 .. MATHEMATICAL FRAKTUR SMALL K
+.. |Lfr| unicode:: U+1D50F .. MATHEMATICAL FRAKTUR CAPITAL L
+.. |lfr| unicode:: U+1D529 .. MATHEMATICAL FRAKTUR SMALL L
+.. |Mfr| unicode:: U+1D510 .. MATHEMATICAL FRAKTUR CAPITAL M
+.. |mfr| unicode:: U+1D52A .. MATHEMATICAL FRAKTUR SMALL M
+.. |Nfr| unicode:: U+1D511 .. MATHEMATICAL FRAKTUR CAPITAL N
+.. |nfr| unicode:: U+1D52B .. MATHEMATICAL FRAKTUR SMALL N
+.. |Ofr| unicode:: U+1D512 .. MATHEMATICAL FRAKTUR CAPITAL O
+.. |ofr| unicode:: U+1D52C .. MATHEMATICAL FRAKTUR SMALL O
+.. |Pfr| unicode:: U+1D513 .. MATHEMATICAL FRAKTUR CAPITAL P
+.. |pfr| unicode:: U+1D52D .. MATHEMATICAL FRAKTUR SMALL P
+.. |Qfr| unicode:: U+1D514 .. MATHEMATICAL FRAKTUR CAPITAL Q
+.. |qfr| unicode:: U+1D52E .. MATHEMATICAL FRAKTUR SMALL Q
+.. |Rfr| unicode:: U+0211C .. BLACK-LETTER CAPITAL R
+.. |rfr| unicode:: U+1D52F .. MATHEMATICAL FRAKTUR SMALL R
+.. |Sfr| unicode:: U+1D516 .. MATHEMATICAL FRAKTUR CAPITAL S
+.. |sfr| unicode:: U+1D530 .. MATHEMATICAL FRAKTUR SMALL S
+.. |Tfr| unicode:: U+1D517 .. MATHEMATICAL FRAKTUR CAPITAL T
+.. |tfr| unicode:: U+1D531 .. MATHEMATICAL FRAKTUR SMALL T
+.. |Ufr| unicode:: U+1D518 .. MATHEMATICAL FRAKTUR CAPITAL U
+.. |ufr| unicode:: U+1D532 .. MATHEMATICAL FRAKTUR SMALL U
+.. |Vfr| unicode:: U+1D519 .. MATHEMATICAL FRAKTUR CAPITAL V
+.. |vfr| unicode:: U+1D533 .. MATHEMATICAL FRAKTUR SMALL V
+.. |Wfr| unicode:: U+1D51A .. MATHEMATICAL FRAKTUR CAPITAL W
+.. |wfr| unicode:: U+1D534 .. MATHEMATICAL FRAKTUR SMALL W
+.. |Xfr| unicode:: U+1D51B .. MATHEMATICAL FRAKTUR CAPITAL X
+.. |xfr| unicode:: U+1D535 .. MATHEMATICAL FRAKTUR SMALL X
+.. |Yfr| unicode:: U+1D51C .. MATHEMATICAL FRAKTUR CAPITAL Y
+.. |yfr| unicode:: U+1D536 .. MATHEMATICAL FRAKTUR SMALL Y
+.. |Zfr| unicode:: U+02128 .. BLACK-LETTER CAPITAL Z
+.. |zfr| unicode:: U+1D537 .. MATHEMATICAL FRAKTUR SMALL Z
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomfrk.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomfrk.txt
new file mode 100644
index 00000000..81dd4b6e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomfrk.txt
@@ -0,0 +1,11 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Cfr| unicode:: U+0212D .. BLACK-LETTER CAPITAL C
+.. |Hfr| unicode:: U+0210C .. BLACK-LETTER CAPITAL H
+.. |Ifr| unicode:: U+02111 .. BLACK-LETTER CAPITAL I
+.. |Rfr| unicode:: U+0211C .. BLACK-LETTER CAPITAL R
+.. |Zfr| unicode:: U+02128 .. BLACK-LETTER CAPITAL Z
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomopf-wide.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomopf-wide.txt
new file mode 100644
index 00000000..2c16866e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomopf-wide.txt
@@ -0,0 +1,32 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Aopf| unicode:: U+1D538 .. MATHEMATICAL DOUBLE-STRUCK CAPITAL A
+.. |Bopf| unicode:: U+1D539 .. MATHEMATICAL DOUBLE-STRUCK CAPITAL B
+.. |Copf| unicode:: U+02102 .. DOUBLE-STRUCK CAPITAL C
+.. |Dopf| unicode:: U+1D53B .. MATHEMATICAL DOUBLE-STRUCK CAPITAL D
+.. |Eopf| unicode:: U+1D53C .. MATHEMATICAL DOUBLE-STRUCK CAPITAL E
+.. |Fopf| unicode:: U+1D53D .. MATHEMATICAL DOUBLE-STRUCK CAPITAL F
+.. |Gopf| unicode:: U+1D53E .. MATHEMATICAL DOUBLE-STRUCK CAPITAL G
+.. |Hopf| unicode:: U+0210D .. DOUBLE-STRUCK CAPITAL H
+.. |Iopf| unicode:: U+1D540 .. MATHEMATICAL DOUBLE-STRUCK CAPITAL I
+.. |Jopf| unicode:: U+1D541 .. MATHEMATICAL DOUBLE-STRUCK CAPITAL J
+.. |Kopf| unicode:: U+1D542 .. MATHEMATICAL DOUBLE-STRUCK CAPITAL K
+.. |Lopf| unicode:: U+1D543 .. MATHEMATICAL DOUBLE-STRUCK CAPITAL L
+.. |Mopf| unicode:: U+1D544 .. MATHEMATICAL DOUBLE-STRUCK CAPITAL M
+.. |Nopf| unicode:: U+02115 .. DOUBLE-STRUCK CAPITAL N
+.. |Oopf| unicode:: U+1D546 .. MATHEMATICAL DOUBLE-STRUCK CAPITAL O
+.. |Popf| unicode:: U+02119 .. DOUBLE-STRUCK CAPITAL P
+.. |Qopf| unicode:: U+0211A .. DOUBLE-STRUCK CAPITAL Q
+.. |Ropf| unicode:: U+0211D .. DOUBLE-STRUCK CAPITAL R
+.. |Sopf| unicode:: U+1D54A .. MATHEMATICAL DOUBLE-STRUCK CAPITAL S
+.. |Topf| unicode:: U+1D54B .. MATHEMATICAL DOUBLE-STRUCK CAPITAL T
+.. |Uopf| unicode:: U+1D54C .. MATHEMATICAL DOUBLE-STRUCK CAPITAL U
+.. |Vopf| unicode:: U+1D54D .. MATHEMATICAL DOUBLE-STRUCK CAPITAL V
+.. |Wopf| unicode:: U+1D54E .. MATHEMATICAL DOUBLE-STRUCK CAPITAL W
+.. |Xopf| unicode:: U+1D54F .. MATHEMATICAL DOUBLE-STRUCK CAPITAL X
+.. |Yopf| unicode:: U+1D550 .. MATHEMATICAL DOUBLE-STRUCK CAPITAL Y
+.. |Zopf| unicode:: U+02124 .. DOUBLE-STRUCK CAPITAL Z
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomopf.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomopf.txt
new file mode 100644
index 00000000..03c6a0d2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomopf.txt
@@ -0,0 +1,13 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Copf| unicode:: U+02102 .. DOUBLE-STRUCK CAPITAL C
+.. |Hopf| unicode:: U+0210D .. DOUBLE-STRUCK CAPITAL H
+.. |Nopf| unicode:: U+02115 .. DOUBLE-STRUCK CAPITAL N
+.. |Popf| unicode:: U+02119 .. DOUBLE-STRUCK CAPITAL P
+.. |Qopf| unicode:: U+0211A .. DOUBLE-STRUCK CAPITAL Q
+.. |Ropf| unicode:: U+0211D .. DOUBLE-STRUCK CAPITAL R
+.. |Zopf| unicode:: U+02124 .. DOUBLE-STRUCK CAPITAL Z
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomscr-wide.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomscr-wide.txt
new file mode 100644
index 00000000..acfe7a00
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomscr-wide.txt
@@ -0,0 +1,58 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Ascr| unicode:: U+1D49C .. MATHEMATICAL SCRIPT CAPITAL A
+.. |ascr| unicode:: U+1D4B6 .. MATHEMATICAL SCRIPT SMALL A
+.. |Bscr| unicode:: U+0212C .. SCRIPT CAPITAL B
+.. |bscr| unicode:: U+1D4B7 .. MATHEMATICAL SCRIPT SMALL B
+.. |Cscr| unicode:: U+1D49E .. MATHEMATICAL SCRIPT CAPITAL C
+.. |cscr| unicode:: U+1D4B8 .. MATHEMATICAL SCRIPT SMALL C
+.. |Dscr| unicode:: U+1D49F .. MATHEMATICAL SCRIPT CAPITAL D
+.. |dscr| unicode:: U+1D4B9 .. MATHEMATICAL SCRIPT SMALL D
+.. |Escr| unicode:: U+02130 .. SCRIPT CAPITAL E
+.. |escr| unicode:: U+0212F .. SCRIPT SMALL E
+.. |Fscr| unicode:: U+02131 .. SCRIPT CAPITAL F
+.. |fscr| unicode:: U+1D4BB .. MATHEMATICAL SCRIPT SMALL F
+.. |Gscr| unicode:: U+1D4A2 .. MATHEMATICAL SCRIPT CAPITAL G
+.. |gscr| unicode:: U+0210A .. SCRIPT SMALL G
+.. |Hscr| unicode:: U+0210B .. SCRIPT CAPITAL H
+.. |hscr| unicode:: U+1D4BD .. MATHEMATICAL SCRIPT SMALL H
+.. |Iscr| unicode:: U+02110 .. SCRIPT CAPITAL I
+.. |iscr| unicode:: U+1D4BE .. MATHEMATICAL SCRIPT SMALL I
+.. |Jscr| unicode:: U+1D4A5 .. MATHEMATICAL SCRIPT CAPITAL J
+.. |jscr| unicode:: U+1D4BF .. MATHEMATICAL SCRIPT SMALL J
+.. |Kscr| unicode:: U+1D4A6 .. MATHEMATICAL SCRIPT CAPITAL K
+.. |kscr| unicode:: U+1D4C0 .. MATHEMATICAL SCRIPT SMALL K
+.. |Lscr| unicode:: U+02112 .. SCRIPT CAPITAL L
+.. |lscr| unicode:: U+1D4C1 .. MATHEMATICAL SCRIPT SMALL L
+.. |Mscr| unicode:: U+02133 .. SCRIPT CAPITAL M
+.. |mscr| unicode:: U+1D4C2 .. MATHEMATICAL SCRIPT SMALL M
+.. |Nscr| unicode:: U+1D4A9 .. MATHEMATICAL SCRIPT CAPITAL N
+.. |nscr| unicode:: U+1D4C3 .. MATHEMATICAL SCRIPT SMALL N
+.. |Oscr| unicode:: U+1D4AA .. MATHEMATICAL SCRIPT CAPITAL O
+.. |oscr| unicode:: U+02134 .. SCRIPT SMALL O
+.. |Pscr| unicode:: U+1D4AB .. MATHEMATICAL SCRIPT CAPITAL P
+.. |pscr| unicode:: U+1D4C5 .. MATHEMATICAL SCRIPT SMALL P
+.. |Qscr| unicode:: U+1D4AC .. MATHEMATICAL SCRIPT CAPITAL Q
+.. |qscr| unicode:: U+1D4C6 .. MATHEMATICAL SCRIPT SMALL Q
+.. |Rscr| unicode:: U+0211B .. SCRIPT CAPITAL R
+.. |rscr| unicode:: U+1D4C7 .. MATHEMATICAL SCRIPT SMALL R
+.. |Sscr| unicode:: U+1D4AE .. MATHEMATICAL SCRIPT CAPITAL S
+.. |sscr| unicode:: U+1D4C8 .. MATHEMATICAL SCRIPT SMALL S
+.. |Tscr| unicode:: U+1D4AF .. MATHEMATICAL SCRIPT CAPITAL T
+.. |tscr| unicode:: U+1D4C9 .. MATHEMATICAL SCRIPT SMALL T
+.. |Uscr| unicode:: U+1D4B0 .. MATHEMATICAL SCRIPT CAPITAL U
+.. |uscr| unicode:: U+1D4CA .. MATHEMATICAL SCRIPT SMALL U
+.. |Vscr| unicode:: U+1D4B1 .. MATHEMATICAL SCRIPT CAPITAL V
+.. |vscr| unicode:: U+1D4CB .. MATHEMATICAL SCRIPT SMALL V
+.. |Wscr| unicode:: U+1D4B2 .. MATHEMATICAL SCRIPT CAPITAL W
+.. |wscr| unicode:: U+1D4CC .. MATHEMATICAL SCRIPT SMALL W
+.. |Xscr| unicode:: U+1D4B3 .. MATHEMATICAL SCRIPT CAPITAL X
+.. |xscr| unicode:: U+1D4CD .. MATHEMATICAL SCRIPT SMALL X
+.. |Yscr| unicode:: U+1D4B4 .. MATHEMATICAL SCRIPT CAPITAL Y
+.. |yscr| unicode:: U+1D4CE .. MATHEMATICAL SCRIPT SMALL Y
+.. |Zscr| unicode:: U+1D4B5 .. MATHEMATICAL SCRIPT CAPITAL Z
+.. |zscr| unicode:: U+1D4CF .. MATHEMATICAL SCRIPT SMALL Z
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomscr.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomscr.txt
new file mode 100644
index 00000000..951577e8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isomscr.txt
@@ -0,0 +1,17 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Bscr| unicode:: U+0212C .. SCRIPT CAPITAL B
+.. |Escr| unicode:: U+02130 .. SCRIPT CAPITAL E
+.. |escr| unicode:: U+0212F .. SCRIPT SMALL E
+.. |Fscr| unicode:: U+02131 .. SCRIPT CAPITAL F
+.. |gscr| unicode:: U+0210A .. SCRIPT SMALL G
+.. |Hscr| unicode:: U+0210B .. SCRIPT CAPITAL H
+.. |Iscr| unicode:: U+02110 .. SCRIPT CAPITAL I
+.. |Lscr| unicode:: U+02112 .. SCRIPT CAPITAL L
+.. |Mscr| unicode:: U+02133 .. SCRIPT CAPITAL M
+.. |oscr| unicode:: U+02134 .. SCRIPT SMALL O
+.. |Rscr| unicode:: U+0211B .. SCRIPT CAPITAL R
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isonum.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isonum.txt
new file mode 100644
index 00000000..0d280c98
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isonum.txt
@@ -0,0 +1,82 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |amp| unicode:: U+00026 .. AMPERSAND
+.. |apos| unicode:: U+00027 .. APOSTROPHE
+.. |ast| unicode:: U+0002A .. ASTERISK
+.. |brvbar| unicode:: U+000A6 .. BROKEN BAR
+.. |bsol| unicode:: U+0005C .. REVERSE SOLIDUS
+.. |cent| unicode:: U+000A2 .. CENT SIGN
+.. |colon| unicode:: U+0003A .. COLON
+.. |comma| unicode:: U+0002C .. COMMA
+.. |commat| unicode:: U+00040 .. COMMERCIAL AT
+.. |copy| unicode:: U+000A9 .. COPYRIGHT SIGN
+.. |curren| unicode:: U+000A4 .. CURRENCY SIGN
+.. |darr| unicode:: U+02193 .. DOWNWARDS ARROW
+.. |deg| unicode:: U+000B0 .. DEGREE SIGN
+.. |divide| unicode:: U+000F7 .. DIVISION SIGN
+.. |dollar| unicode:: U+00024 .. DOLLAR SIGN
+.. |equals| unicode:: U+0003D .. EQUALS SIGN
+.. |excl| unicode:: U+00021 .. EXCLAMATION MARK
+.. |frac12| unicode:: U+000BD .. VULGAR FRACTION ONE HALF
+.. |frac14| unicode:: U+000BC .. VULGAR FRACTION ONE QUARTER
+.. |frac18| unicode:: U+0215B .. VULGAR FRACTION ONE EIGHTH
+.. |frac34| unicode:: U+000BE .. VULGAR FRACTION THREE QUARTERS
+.. |frac38| unicode:: U+0215C .. VULGAR FRACTION THREE EIGHTHS
+.. |frac58| unicode:: U+0215D .. VULGAR FRACTION FIVE EIGHTHS
+.. |frac78| unicode:: U+0215E .. VULGAR FRACTION SEVEN EIGHTHS
+.. |gt| unicode:: U+0003E .. GREATER-THAN SIGN
+.. |half| unicode:: U+000BD .. VULGAR FRACTION ONE HALF
+.. |horbar| unicode:: U+02015 .. HORIZONTAL BAR
+.. |hyphen| unicode:: U+02010 .. HYPHEN
+.. |iexcl| unicode:: U+000A1 .. INVERTED EXCLAMATION MARK
+.. |iquest| unicode:: U+000BF .. INVERTED QUESTION MARK
+.. |laquo| unicode:: U+000AB .. LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+.. |larr| unicode:: U+02190 .. LEFTWARDS ARROW
+.. |lcub| unicode:: U+0007B .. LEFT CURLY BRACKET
+.. |ldquo| unicode:: U+0201C .. LEFT DOUBLE QUOTATION MARK
+.. |lowbar| unicode:: U+0005F .. LOW LINE
+.. |lpar| unicode:: U+00028 .. LEFT PARENTHESIS
+.. |lsqb| unicode:: U+0005B .. LEFT SQUARE BRACKET
+.. |lsquo| unicode:: U+02018 .. LEFT SINGLE QUOTATION MARK
+.. |lt| unicode:: U+0003C .. LESS-THAN SIGN
+.. |micro| unicode:: U+000B5 .. MICRO SIGN
+.. |middot| unicode:: U+000B7 .. MIDDLE DOT
+.. |nbsp| unicode:: U+000A0 .. NO-BREAK SPACE
+.. |not| unicode:: U+000AC .. NOT SIGN
+.. |num| unicode:: U+00023 .. NUMBER SIGN
+.. |ohm| unicode:: U+02126 .. OHM SIGN
+.. |ordf| unicode:: U+000AA .. FEMININE ORDINAL INDICATOR
+.. |ordm| unicode:: U+000BA .. MASCULINE ORDINAL INDICATOR
+.. |para| unicode:: U+000B6 .. PILCROW SIGN
+.. |percnt| unicode:: U+00025 .. PERCENT SIGN
+.. |period| unicode:: U+0002E .. FULL STOP
+.. |plus| unicode:: U+0002B .. PLUS SIGN
+.. |plusmn| unicode:: U+000B1 .. PLUS-MINUS SIGN
+.. |pound| unicode:: U+000A3 .. POUND SIGN
+.. |quest| unicode:: U+0003F .. QUESTION MARK
+.. |quot| unicode:: U+00022 .. QUOTATION MARK
+.. |raquo| unicode:: U+000BB .. RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+.. |rarr| unicode:: U+02192 .. RIGHTWARDS ARROW
+.. |rcub| unicode:: U+0007D .. RIGHT CURLY BRACKET
+.. |rdquo| unicode:: U+0201D .. RIGHT DOUBLE QUOTATION MARK
+.. |reg| unicode:: U+000AE .. REGISTERED SIGN
+.. |rpar| unicode:: U+00029 .. RIGHT PARENTHESIS
+.. |rsqb| unicode:: U+0005D .. RIGHT SQUARE BRACKET
+.. |rsquo| unicode:: U+02019 .. RIGHT SINGLE QUOTATION MARK
+.. |sect| unicode:: U+000A7 .. SECTION SIGN
+.. |semi| unicode:: U+0003B .. SEMICOLON
+.. |shy| unicode:: U+000AD .. SOFT HYPHEN
+.. |sol| unicode:: U+0002F .. SOLIDUS
+.. |sung| unicode:: U+0266A .. EIGHTH NOTE
+.. |sup1| unicode:: U+000B9 .. SUPERSCRIPT ONE
+.. |sup2| unicode:: U+000B2 .. SUPERSCRIPT TWO
+.. |sup3| unicode:: U+000B3 .. SUPERSCRIPT THREE
+.. |times| unicode:: U+000D7 .. MULTIPLICATION SIGN
+.. |trade| unicode:: U+02122 .. TRADE MARK SIGN
+.. |uarr| unicode:: U+02191 .. UPWARDS ARROW
+.. |verbar| unicode:: U+0007C .. VERTICAL LINE
+.. |yen| unicode:: U+000A5 .. YEN SIGN
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isopub.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isopub.txt
new file mode 100644
index 00000000..78e12513
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isopub.txt
@@ -0,0 +1,90 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |blank| unicode:: U+02423 .. OPEN BOX
+.. |blk12| unicode:: U+02592 .. MEDIUM SHADE
+.. |blk14| unicode:: U+02591 .. LIGHT SHADE
+.. |blk34| unicode:: U+02593 .. DARK SHADE
+.. |block| unicode:: U+02588 .. FULL BLOCK
+.. |bull| unicode:: U+02022 .. BULLET
+.. |caret| unicode:: U+02041 .. CARET INSERTION POINT
+.. |check| unicode:: U+02713 .. CHECK MARK
+.. |cir| unicode:: U+025CB .. WHITE CIRCLE
+.. |clubs| unicode:: U+02663 .. BLACK CLUB SUIT
+.. |copysr| unicode:: U+02117 .. SOUND RECORDING COPYRIGHT
+.. |cross| unicode:: U+02717 .. BALLOT X
+.. |Dagger| unicode:: U+02021 .. DOUBLE DAGGER
+.. |dagger| unicode:: U+02020 .. DAGGER
+.. |dash| unicode:: U+02010 .. HYPHEN
+.. |diams| unicode:: U+02666 .. BLACK DIAMOND SUIT
+.. |dlcrop| unicode:: U+0230D .. BOTTOM LEFT CROP
+.. |drcrop| unicode:: U+0230C .. BOTTOM RIGHT CROP
+.. |dtri| unicode:: U+025BF .. WHITE DOWN-POINTING SMALL TRIANGLE
+.. |dtrif| unicode:: U+025BE .. BLACK DOWN-POINTING SMALL TRIANGLE
+.. |emsp| unicode:: U+02003 .. EM SPACE
+.. |emsp13| unicode:: U+02004 .. THREE-PER-EM SPACE
+.. |emsp14| unicode:: U+02005 .. FOUR-PER-EM SPACE
+.. |ensp| unicode:: U+02002 .. EN SPACE
+.. |female| unicode:: U+02640 .. FEMALE SIGN
+.. |ffilig| unicode:: U+0FB03 .. LATIN SMALL LIGATURE FFI
+.. |fflig| unicode:: U+0FB00 .. LATIN SMALL LIGATURE FF
+.. |ffllig| unicode:: U+0FB04 .. LATIN SMALL LIGATURE FFL
+.. |filig| unicode:: U+0FB01 .. LATIN SMALL LIGATURE FI
+.. |flat| unicode:: U+0266D .. MUSIC FLAT SIGN
+.. |fllig| unicode:: U+0FB02 .. LATIN SMALL LIGATURE FL
+.. |frac13| unicode:: U+02153 .. VULGAR FRACTION ONE THIRD
+.. |frac15| unicode:: U+02155 .. VULGAR FRACTION ONE FIFTH
+.. |frac16| unicode:: U+02159 .. VULGAR FRACTION ONE SIXTH
+.. |frac23| unicode:: U+02154 .. VULGAR FRACTION TWO THIRDS
+.. |frac25| unicode:: U+02156 .. VULGAR FRACTION TWO FIFTHS
+.. |frac35| unicode:: U+02157 .. VULGAR FRACTION THREE FIFTHS
+.. |frac45| unicode:: U+02158 .. VULGAR FRACTION FOUR FIFTHS
+.. |frac56| unicode:: U+0215A .. VULGAR FRACTION FIVE SIXTHS
+.. |hairsp| unicode:: U+0200A .. HAIR SPACE
+.. |hearts| unicode:: U+02665 .. BLACK HEART SUIT
+.. |hellip| unicode:: U+02026 .. HORIZONTAL ELLIPSIS
+.. |hybull| unicode:: U+02043 .. HYPHEN BULLET
+.. |incare| unicode:: U+02105 .. CARE OF
+.. |ldquor| unicode:: U+0201E .. DOUBLE LOW-9 QUOTATION MARK
+.. |lhblk| unicode:: U+02584 .. LOWER HALF BLOCK
+.. |loz| unicode:: U+025CA .. LOZENGE
+.. |lozf| unicode:: U+029EB .. BLACK LOZENGE
+.. |lsquor| unicode:: U+0201A .. SINGLE LOW-9 QUOTATION MARK
+.. |ltri| unicode:: U+025C3 .. WHITE LEFT-POINTING SMALL TRIANGLE
+.. |ltrif| unicode:: U+025C2 .. BLACK LEFT-POINTING SMALL TRIANGLE
+.. |male| unicode:: U+02642 .. MALE SIGN
+.. |malt| unicode:: U+02720 .. MALTESE CROSS
+.. |marker| unicode:: U+025AE .. BLACK VERTICAL RECTANGLE
+.. |mdash| unicode:: U+02014 .. EM DASH
+.. |mldr| unicode:: U+02026 .. HORIZONTAL ELLIPSIS
+.. |natur| unicode:: U+0266E .. MUSIC NATURAL SIGN
+.. |ndash| unicode:: U+02013 .. EN DASH
+.. |nldr| unicode:: U+02025 .. TWO DOT LEADER
+.. |numsp| unicode:: U+02007 .. FIGURE SPACE
+.. |phone| unicode:: U+0260E .. BLACK TELEPHONE
+.. |puncsp| unicode:: U+02008 .. PUNCTUATION SPACE
+.. |rdquor| unicode:: U+0201D .. RIGHT DOUBLE QUOTATION MARK
+.. |rect| unicode:: U+025AD .. WHITE RECTANGLE
+.. |rsquor| unicode:: U+02019 .. RIGHT SINGLE QUOTATION MARK
+.. |rtri| unicode:: U+025B9 .. WHITE RIGHT-POINTING SMALL TRIANGLE
+.. |rtrif| unicode:: U+025B8 .. BLACK RIGHT-POINTING SMALL TRIANGLE
+.. |rx| unicode:: U+0211E .. PRESCRIPTION TAKE
+.. |sext| unicode:: U+02736 .. SIX POINTED BLACK STAR
+.. |sharp| unicode:: U+0266F .. MUSIC SHARP SIGN
+.. |spades| unicode:: U+02660 .. BLACK SPADE SUIT
+.. |squ| unicode:: U+025A1 .. WHITE SQUARE
+.. |squf| unicode:: U+025AA .. BLACK SMALL SQUARE
+.. |star| unicode:: U+02606 .. WHITE STAR
+.. |starf| unicode:: U+02605 .. BLACK STAR
+.. |target| unicode:: U+02316 .. POSITION INDICATOR
+.. |telrec| unicode:: U+02315 .. TELEPHONE RECORDER
+.. |thinsp| unicode:: U+02009 .. THIN SPACE
+.. |uhblk| unicode:: U+02580 .. UPPER HALF BLOCK
+.. |ulcrop| unicode:: U+0230F .. TOP LEFT CROP
+.. |urcrop| unicode:: U+0230E .. TOP RIGHT CROP
+.. |utri| unicode:: U+025B5 .. WHITE UP-POINTING SMALL TRIANGLE
+.. |utrif| unicode:: U+025B4 .. BLACK UP-POINTING SMALL TRIANGLE
+.. |vellip| unicode:: U+022EE .. VERTICAL ELLIPSIS
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isotech.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isotech.txt
new file mode 100644
index 00000000..9b01eaad
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/isotech.txt
@@ -0,0 +1,168 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |acd| unicode:: U+0223F .. SINE WAVE
+.. |aleph| unicode:: U+02135 .. ALEF SYMBOL
+.. |And| unicode:: U+02A53 .. DOUBLE LOGICAL AND
+.. |and| unicode:: U+02227 .. LOGICAL AND
+.. |andand| unicode:: U+02A55 .. TWO INTERSECTING LOGICAL AND
+.. |andd| unicode:: U+02A5C .. LOGICAL AND WITH HORIZONTAL DASH
+.. |andslope| unicode:: U+02A58 .. SLOPING LARGE AND
+.. |andv| unicode:: U+02A5A .. LOGICAL AND WITH MIDDLE STEM
+.. |ang90| unicode:: U+0221F .. RIGHT ANGLE
+.. |angrt| unicode:: U+0221F .. RIGHT ANGLE
+.. |angsph| unicode:: U+02222 .. SPHERICAL ANGLE
+.. |angst| unicode:: U+0212B .. ANGSTROM SIGN
+.. |ap| unicode:: U+02248 .. ALMOST EQUAL TO
+.. |apacir| unicode:: U+02A6F .. ALMOST EQUAL TO WITH CIRCUMFLEX ACCENT
+.. |awconint| unicode:: U+02233 .. ANTICLOCKWISE CONTOUR INTEGRAL
+.. |awint| unicode:: U+02A11 .. ANTICLOCKWISE INTEGRATION
+.. |becaus| unicode:: U+02235 .. BECAUSE
+.. |bernou| unicode:: U+0212C .. SCRIPT CAPITAL B
+.. |bne| unicode:: U+0003D U+020E5 .. EQUALS SIGN with reverse slash
+.. |bnequiv| unicode:: U+02261 U+020E5 .. IDENTICAL TO with reverse slash
+.. |bNot| unicode:: U+02AED .. REVERSED DOUBLE STROKE NOT SIGN
+.. |bnot| unicode:: U+02310 .. REVERSED NOT SIGN
+.. |bottom| unicode:: U+022A5 .. UP TACK
+.. |cap| unicode:: U+02229 .. INTERSECTION
+.. |Cconint| unicode:: U+02230 .. VOLUME INTEGRAL
+.. |cirfnint| unicode:: U+02A10 .. CIRCULATION FUNCTION
+.. |compfn| unicode:: U+02218 .. RING OPERATOR
+.. |cong| unicode:: U+02245 .. APPROXIMATELY EQUAL TO
+.. |Conint| unicode:: U+0222F .. SURFACE INTEGRAL
+.. |conint| unicode:: U+0222E .. CONTOUR INTEGRAL
+.. |ctdot| unicode:: U+022EF .. MIDLINE HORIZONTAL ELLIPSIS
+.. |cup| unicode:: U+0222A .. UNION
+.. |cwconint| unicode:: U+02232 .. CLOCKWISE CONTOUR INTEGRAL
+.. |cwint| unicode:: U+02231 .. CLOCKWISE INTEGRAL
+.. |cylcty| unicode:: U+0232D .. CYLINDRICITY
+.. |disin| unicode:: U+022F2 .. ELEMENT OF WITH LONG HORIZONTAL STROKE
+.. |Dot| unicode:: U+000A8 .. DIAERESIS
+.. |DotDot| unicode:: U+020DC .. COMBINING FOUR DOTS ABOVE
+.. |dsol| unicode:: U+029F6 .. SOLIDUS WITH OVERBAR
+.. |dtdot| unicode:: U+022F1 .. DOWN RIGHT DIAGONAL ELLIPSIS
+.. |dwangle| unicode:: U+029A6 .. OBLIQUE ANGLE OPENING UP
+.. |elinters| unicode:: U+0FFFD .. REPLACEMENT CHARACTER
+.. |epar| unicode:: U+022D5 .. EQUAL AND PARALLEL TO
+.. |eparsl| unicode:: U+029E3 .. EQUALS SIGN AND SLANTED PARALLEL
+.. |equiv| unicode:: U+02261 .. IDENTICAL TO
+.. |eqvparsl| unicode:: U+029E5 .. IDENTICAL TO AND SLANTED PARALLEL
+.. |exist| unicode:: U+02203 .. THERE EXISTS
+.. |fltns| unicode:: U+025B1 .. WHITE PARALLELOGRAM
+.. |fnof| unicode:: U+00192 .. LATIN SMALL LETTER F WITH HOOK
+.. |forall| unicode:: U+02200 .. FOR ALL
+.. |fpartint| unicode:: U+02A0D .. FINITE PART INTEGRAL
+.. |ge| unicode:: U+02265 .. GREATER-THAN OR EQUAL TO
+.. |hamilt| unicode:: U+0210B .. SCRIPT CAPITAL H
+.. |iff| unicode:: U+021D4 .. LEFT RIGHT DOUBLE ARROW
+.. |iinfin| unicode:: U+029DC .. INCOMPLETE INFINITY
+.. |imped| unicode:: U+001B5 .. LATIN CAPITAL LETTER Z WITH STROKE
+.. |infin| unicode:: U+0221E .. INFINITY
+.. |infintie| unicode:: U+029DD .. TIE OVER INFINITY
+.. |Int| unicode:: U+0222C .. DOUBLE INTEGRAL
+.. |int| unicode:: U+0222B .. INTEGRAL
+.. |intlarhk| unicode:: U+02A17 .. INTEGRAL WITH LEFTWARDS ARROW WITH HOOK
+.. |isin| unicode:: U+02208 .. ELEMENT OF
+.. |isindot| unicode:: U+022F5 .. ELEMENT OF WITH DOT ABOVE
+.. |isinE| unicode:: U+022F9 .. ELEMENT OF WITH TWO HORIZONTAL STROKES
+.. |isins| unicode:: U+022F4 .. SMALL ELEMENT OF WITH VERTICAL BAR AT END OF HORIZONTAL STROKE
+.. |isinsv| unicode:: U+022F3 .. ELEMENT OF WITH VERTICAL BAR AT END OF HORIZONTAL STROKE
+.. |isinv| unicode:: U+02208 .. ELEMENT OF
+.. |lagran| unicode:: U+02112 .. SCRIPT CAPITAL L
+.. |Lang| unicode:: U+0300A .. LEFT DOUBLE ANGLE BRACKET
+.. |lang| unicode:: U+02329 .. LEFT-POINTING ANGLE BRACKET
+.. |lArr| unicode:: U+021D0 .. LEFTWARDS DOUBLE ARROW
+.. |lbbrk| unicode:: U+03014 .. LEFT TORTOISE SHELL BRACKET
+.. |le| unicode:: U+02264 .. LESS-THAN OR EQUAL TO
+.. |loang| unicode:: U+03018 .. LEFT WHITE TORTOISE SHELL BRACKET
+.. |lobrk| unicode:: U+0301A .. LEFT WHITE SQUARE BRACKET
+.. |lopar| unicode:: U+02985 .. LEFT WHITE PARENTHESIS
+.. |lowast| unicode:: U+02217 .. ASTERISK OPERATOR
+.. |minus| unicode:: U+02212 .. MINUS SIGN
+.. |mnplus| unicode:: U+02213 .. MINUS-OR-PLUS SIGN
+.. |nabla| unicode:: U+02207 .. NABLA
+.. |ne| unicode:: U+02260 .. NOT EQUAL TO
+.. |nedot| unicode:: U+02250 U+00338 .. APPROACHES THE LIMIT with slash
+.. |nhpar| unicode:: U+02AF2 .. PARALLEL WITH HORIZONTAL STROKE
+.. |ni| unicode:: U+0220B .. CONTAINS AS MEMBER
+.. |nis| unicode:: U+022FC .. SMALL CONTAINS WITH VERTICAL BAR AT END OF HORIZONTAL STROKE
+.. |nisd| unicode:: U+022FA .. CONTAINS WITH LONG HORIZONTAL STROKE
+.. |niv| unicode:: U+0220B .. CONTAINS AS MEMBER
+.. |Not| unicode:: U+02AEC .. DOUBLE STROKE NOT SIGN
+.. |notin| unicode:: U+02209 .. NOT AN ELEMENT OF
+.. |notindot| unicode:: U+022F5 U+00338 .. ELEMENT OF WITH DOT ABOVE with slash
+.. |notinE| unicode:: U+022F9 U+00338 .. ELEMENT OF WITH TWO HORIZONTAL STROKES with slash
+.. |notinva| unicode:: U+02209 .. NOT AN ELEMENT OF
+.. |notinvb| unicode:: U+022F7 .. SMALL ELEMENT OF WITH OVERBAR
+.. |notinvc| unicode:: U+022F6 .. ELEMENT OF WITH OVERBAR
+.. |notni| unicode:: U+0220C .. DOES NOT CONTAIN AS MEMBER
+.. |notniva| unicode:: U+0220C .. DOES NOT CONTAIN AS MEMBER
+.. |notnivb| unicode:: U+022FE .. SMALL CONTAINS WITH OVERBAR
+.. |notnivc| unicode:: U+022FD .. CONTAINS WITH OVERBAR
+.. |nparsl| unicode:: U+02AFD U+020E5 .. DOUBLE SOLIDUS OPERATOR with reverse slash
+.. |npart| unicode:: U+02202 U+00338 .. PARTIAL DIFFERENTIAL with slash
+.. |npolint| unicode:: U+02A14 .. LINE INTEGRATION NOT INCLUDING THE POLE
+.. |nvinfin| unicode:: U+029DE .. INFINITY NEGATED WITH VERTICAL BAR
+.. |olcross| unicode:: U+029BB .. CIRCLE WITH SUPERIMPOSED X
+.. |Or| unicode:: U+02A54 .. DOUBLE LOGICAL OR
+.. |or| unicode:: U+02228 .. LOGICAL OR
+.. |ord| unicode:: U+02A5D .. LOGICAL OR WITH HORIZONTAL DASH
+.. |order| unicode:: U+02134 .. SCRIPT SMALL O
+.. |oror| unicode:: U+02A56 .. TWO INTERSECTING LOGICAL OR
+.. |orslope| unicode:: U+02A57 .. SLOPING LARGE OR
+.. |orv| unicode:: U+02A5B .. LOGICAL OR WITH MIDDLE STEM
+.. |par| unicode:: U+02225 .. PARALLEL TO
+.. |parsl| unicode:: U+02AFD .. DOUBLE SOLIDUS OPERATOR
+.. |part| unicode:: U+02202 .. PARTIAL DIFFERENTIAL
+.. |permil| unicode:: U+02030 .. PER MILLE SIGN
+.. |perp| unicode:: U+022A5 .. UP TACK
+.. |pertenk| unicode:: U+02031 .. PER TEN THOUSAND SIGN
+.. |phmmat| unicode:: U+02133 .. SCRIPT CAPITAL M
+.. |pointint| unicode:: U+02A15 .. INTEGRAL AROUND A POINT OPERATOR
+.. |Prime| unicode:: U+02033 .. DOUBLE PRIME
+.. |prime| unicode:: U+02032 .. PRIME
+.. |profalar| unicode:: U+0232E .. ALL AROUND-PROFILE
+.. |profline| unicode:: U+02312 .. ARC
+.. |profsurf| unicode:: U+02313 .. SEGMENT
+.. |prop| unicode:: U+0221D .. PROPORTIONAL TO
+.. |qint| unicode:: U+02A0C .. QUADRUPLE INTEGRAL OPERATOR
+.. |qprime| unicode:: U+02057 .. QUADRUPLE PRIME
+.. |quatint| unicode:: U+02A16 .. QUATERNION INTEGRAL OPERATOR
+.. |radic| unicode:: U+0221A .. SQUARE ROOT
+.. |Rang| unicode:: U+0300B .. RIGHT DOUBLE ANGLE BRACKET
+.. |rang| unicode:: U+0232A .. RIGHT-POINTING ANGLE BRACKET
+.. |rArr| unicode:: U+021D2 .. RIGHTWARDS DOUBLE ARROW
+.. |rbbrk| unicode:: U+03015 .. RIGHT TORTOISE SHELL BRACKET
+.. |roang| unicode:: U+03019 .. RIGHT WHITE TORTOISE SHELL BRACKET
+.. |robrk| unicode:: U+0301B .. RIGHT WHITE SQUARE BRACKET
+.. |ropar| unicode:: U+02986 .. RIGHT WHITE PARENTHESIS
+.. |rppolint| unicode:: U+02A12 .. LINE INTEGRATION WITH RECTANGULAR PATH AROUND POLE
+.. |scpolint| unicode:: U+02A13 .. LINE INTEGRATION WITH SEMICIRCULAR PATH AROUND POLE
+.. |sim| unicode:: U+0223C .. TILDE OPERATOR
+.. |simdot| unicode:: U+02A6A .. TILDE OPERATOR WITH DOT ABOVE
+.. |sime| unicode:: U+02243 .. ASYMPTOTICALLY EQUAL TO
+.. |smeparsl| unicode:: U+029E4 .. EQUALS SIGN AND SLANTED PARALLEL WITH TILDE ABOVE
+.. |square| unicode:: U+025A1 .. WHITE SQUARE
+.. |squarf| unicode:: U+025AA .. BLACK SMALL SQUARE
+.. |strns| unicode:: U+000AF .. MACRON
+.. |sub| unicode:: U+02282 .. SUBSET OF
+.. |sube| unicode:: U+02286 .. SUBSET OF OR EQUAL TO
+.. |sup| unicode:: U+02283 .. SUPERSET OF
+.. |supe| unicode:: U+02287 .. SUPERSET OF OR EQUAL TO
+.. |tdot| unicode:: U+020DB .. COMBINING THREE DOTS ABOVE
+.. |there4| unicode:: U+02234 .. THEREFORE
+.. |tint| unicode:: U+0222D .. TRIPLE INTEGRAL
+.. |top| unicode:: U+022A4 .. DOWN TACK
+.. |topbot| unicode:: U+02336 .. APL FUNCTIONAL SYMBOL I-BEAM
+.. |topcir| unicode:: U+02AF1 .. DOWN TACK WITH CIRCLE BELOW
+.. |tprime| unicode:: U+02034 .. TRIPLE PRIME
+.. |utdot| unicode:: U+022F0 .. UP RIGHT DIAGONAL ELLIPSIS
+.. |uwangle| unicode:: U+029A7 .. OBLIQUE ANGLE OPENING DOWN
+.. |vangrt| unicode:: U+0299C .. RIGHT ANGLE VARIANT WITH SQUARE
+.. |veeeq| unicode:: U+0225A .. EQUIANGULAR TO
+.. |Verbar| unicode:: U+02016 .. DOUBLE VERTICAL LINE
+.. |wedgeq| unicode:: U+02259 .. ESTIMATES
+.. |xnis| unicode:: U+022FB .. CONTAINS WITH VERTICAL BAR AT END OF HORIZONTAL STROKE
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlalias.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlalias.txt
new file mode 100644
index 00000000..49c23ca7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlalias.txt
@@ -0,0 +1,554 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |angle| unicode:: U+02220 .. ANGLE
+.. |ApplyFunction| unicode:: U+02061 .. FUNCTION APPLICATION
+.. |approx| unicode:: U+02248 .. ALMOST EQUAL TO
+.. |approxeq| unicode:: U+0224A .. ALMOST EQUAL OR EQUAL TO
+.. |Assign| unicode:: U+02254 .. COLON EQUALS
+.. |backcong| unicode:: U+0224C .. ALL EQUAL TO
+.. |backepsilon| unicode:: U+003F6 .. GREEK REVERSED LUNATE EPSILON SYMBOL
+.. |backprime| unicode:: U+02035 .. REVERSED PRIME
+.. |backsim| unicode:: U+0223D .. REVERSED TILDE
+.. |backsimeq| unicode:: U+022CD .. REVERSED TILDE EQUALS
+.. |Backslash| unicode:: U+02216 .. SET MINUS
+.. |barwedge| unicode:: U+02305 .. PROJECTIVE
+.. |Because| unicode:: U+02235 .. BECAUSE
+.. |because| unicode:: U+02235 .. BECAUSE
+.. |Bernoullis| unicode:: U+0212C .. SCRIPT CAPITAL B
+.. |between| unicode:: U+0226C .. BETWEEN
+.. |bigcap| unicode:: U+022C2 .. N-ARY INTERSECTION
+.. |bigcirc| unicode:: U+025EF .. LARGE CIRCLE
+.. |bigcup| unicode:: U+022C3 .. N-ARY UNION
+.. |bigodot| unicode:: U+02A00 .. N-ARY CIRCLED DOT OPERATOR
+.. |bigoplus| unicode:: U+02A01 .. N-ARY CIRCLED PLUS OPERATOR
+.. |bigotimes| unicode:: U+02A02 .. N-ARY CIRCLED TIMES OPERATOR
+.. |bigsqcup| unicode:: U+02A06 .. N-ARY SQUARE UNION OPERATOR
+.. |bigstar| unicode:: U+02605 .. BLACK STAR
+.. |bigtriangledown| unicode:: U+025BD .. WHITE DOWN-POINTING TRIANGLE
+.. |bigtriangleup| unicode:: U+025B3 .. WHITE UP-POINTING TRIANGLE
+.. |biguplus| unicode:: U+02A04 .. N-ARY UNION OPERATOR WITH PLUS
+.. |bigvee| unicode:: U+022C1 .. N-ARY LOGICAL OR
+.. |bigwedge| unicode:: U+022C0 .. N-ARY LOGICAL AND
+.. |bkarow| unicode:: U+0290D .. RIGHTWARDS DOUBLE DASH ARROW
+.. |blacklozenge| unicode:: U+029EB .. BLACK LOZENGE
+.. |blacksquare| unicode:: U+025AA .. BLACK SMALL SQUARE
+.. |blacktriangle| unicode:: U+025B4 .. BLACK UP-POINTING SMALL TRIANGLE
+.. |blacktriangledown| unicode:: U+025BE .. BLACK DOWN-POINTING SMALL TRIANGLE
+.. |blacktriangleleft| unicode:: U+025C2 .. BLACK LEFT-POINTING SMALL TRIANGLE
+.. |blacktriangleright| unicode:: U+025B8 .. BLACK RIGHT-POINTING SMALL TRIANGLE
+.. |bot| unicode:: U+022A5 .. UP TACK
+.. |boxminus| unicode:: U+0229F .. SQUARED MINUS
+.. |boxplus| unicode:: U+0229E .. SQUARED PLUS
+.. |boxtimes| unicode:: U+022A0 .. SQUARED TIMES
+.. |Breve| unicode:: U+002D8 .. BREVE
+.. |bullet| unicode:: U+02022 .. BULLET
+.. |Bumpeq| unicode:: U+0224E .. GEOMETRICALLY EQUIVALENT TO
+.. |bumpeq| unicode:: U+0224F .. DIFFERENCE BETWEEN
+.. |CapitalDifferentialD| unicode:: U+02145 .. DOUBLE-STRUCK ITALIC CAPITAL D
+.. |Cayleys| unicode:: U+0212D .. BLACK-LETTER CAPITAL C
+.. |Cedilla| unicode:: U+000B8 .. CEDILLA
+.. |CenterDot| unicode:: U+000B7 .. MIDDLE DOT
+.. |centerdot| unicode:: U+000B7 .. MIDDLE DOT
+.. |checkmark| unicode:: U+02713 .. CHECK MARK
+.. |circeq| unicode:: U+02257 .. RING EQUAL TO
+.. |circlearrowleft| unicode:: U+021BA .. ANTICLOCKWISE OPEN CIRCLE ARROW
+.. |circlearrowright| unicode:: U+021BB .. CLOCKWISE OPEN CIRCLE ARROW
+.. |circledast| unicode:: U+0229B .. CIRCLED ASTERISK OPERATOR
+.. |circledcirc| unicode:: U+0229A .. CIRCLED RING OPERATOR
+.. |circleddash| unicode:: U+0229D .. CIRCLED DASH
+.. |CircleDot| unicode:: U+02299 .. CIRCLED DOT OPERATOR
+.. |circledR| unicode:: U+000AE .. REGISTERED SIGN
+.. |circledS| unicode:: U+024C8 .. CIRCLED LATIN CAPITAL LETTER S
+.. |CircleMinus| unicode:: U+02296 .. CIRCLED MINUS
+.. |CirclePlus| unicode:: U+02295 .. CIRCLED PLUS
+.. |CircleTimes| unicode:: U+02297 .. CIRCLED TIMES
+.. |ClockwiseContourIntegral| unicode:: U+02232 .. CLOCKWISE CONTOUR INTEGRAL
+.. |CloseCurlyDoubleQuote| unicode:: U+0201D .. RIGHT DOUBLE QUOTATION MARK
+.. |CloseCurlyQuote| unicode:: U+02019 .. RIGHT SINGLE QUOTATION MARK
+.. |clubsuit| unicode:: U+02663 .. BLACK CLUB SUIT
+.. |coloneq| unicode:: U+02254 .. COLON EQUALS
+.. |complement| unicode:: U+02201 .. COMPLEMENT
+.. |complexes| unicode:: U+02102 .. DOUBLE-STRUCK CAPITAL C
+.. |Congruent| unicode:: U+02261 .. IDENTICAL TO
+.. |ContourIntegral| unicode:: U+0222E .. CONTOUR INTEGRAL
+.. |Coproduct| unicode:: U+02210 .. N-ARY COPRODUCT
+.. |CounterClockwiseContourIntegral| unicode:: U+02233 .. ANTICLOCKWISE CONTOUR INTEGRAL
+.. |CupCap| unicode:: U+0224D .. EQUIVALENT TO
+.. |curlyeqprec| unicode:: U+022DE .. EQUAL TO OR PRECEDES
+.. |curlyeqsucc| unicode:: U+022DF .. EQUAL TO OR SUCCEEDS
+.. |curlyvee| unicode:: U+022CE .. CURLY LOGICAL OR
+.. |curlywedge| unicode:: U+022CF .. CURLY LOGICAL AND
+.. |curvearrowleft| unicode:: U+021B6 .. ANTICLOCKWISE TOP SEMICIRCLE ARROW
+.. |curvearrowright| unicode:: U+021B7 .. CLOCKWISE TOP SEMICIRCLE ARROW
+.. |dbkarow| unicode:: U+0290F .. RIGHTWARDS TRIPLE DASH ARROW
+.. |ddagger| unicode:: U+02021 .. DOUBLE DAGGER
+.. |ddotseq| unicode:: U+02A77 .. EQUALS SIGN WITH TWO DOTS ABOVE AND TWO DOTS BELOW
+.. |Del| unicode:: U+02207 .. NABLA
+.. |DiacriticalAcute| unicode:: U+000B4 .. ACUTE ACCENT
+.. |DiacriticalDot| unicode:: U+002D9 .. DOT ABOVE
+.. |DiacriticalDoubleAcute| unicode:: U+002DD .. DOUBLE ACUTE ACCENT
+.. |DiacriticalGrave| unicode:: U+00060 .. GRAVE ACCENT
+.. |DiacriticalTilde| unicode:: U+002DC .. SMALL TILDE
+.. |Diamond| unicode:: U+022C4 .. DIAMOND OPERATOR
+.. |diamond| unicode:: U+022C4 .. DIAMOND OPERATOR
+.. |diamondsuit| unicode:: U+02666 .. BLACK DIAMOND SUIT
+.. |DifferentialD| unicode:: U+02146 .. DOUBLE-STRUCK ITALIC SMALL D
+.. |digamma| unicode:: U+003DD .. GREEK SMALL LETTER DIGAMMA
+.. |div| unicode:: U+000F7 .. DIVISION SIGN
+.. |divideontimes| unicode:: U+022C7 .. DIVISION TIMES
+.. |doteq| unicode:: U+02250 .. APPROACHES THE LIMIT
+.. |doteqdot| unicode:: U+02251 .. GEOMETRICALLY EQUAL TO
+.. |DotEqual| unicode:: U+02250 .. APPROACHES THE LIMIT
+.. |dotminus| unicode:: U+02238 .. DOT MINUS
+.. |dotplus| unicode:: U+02214 .. DOT PLUS
+.. |dotsquare| unicode:: U+022A1 .. SQUARED DOT OPERATOR
+.. |doublebarwedge| unicode:: U+02306 .. PERSPECTIVE
+.. |DoubleContourIntegral| unicode:: U+0222F .. SURFACE INTEGRAL
+.. |DoubleDot| unicode:: U+000A8 .. DIAERESIS
+.. |DoubleDownArrow| unicode:: U+021D3 .. DOWNWARDS DOUBLE ARROW
+.. |DoubleLeftArrow| unicode:: U+021D0 .. LEFTWARDS DOUBLE ARROW
+.. |DoubleLeftRightArrow| unicode:: U+021D4 .. LEFT RIGHT DOUBLE ARROW
+.. |DoubleLeftTee| unicode:: U+02AE4 .. VERTICAL BAR DOUBLE LEFT TURNSTILE
+.. |DoubleLongLeftArrow| unicode:: U+027F8 .. LONG LEFTWARDS DOUBLE ARROW
+.. |DoubleLongLeftRightArrow| unicode:: U+027FA .. LONG LEFT RIGHT DOUBLE ARROW
+.. |DoubleLongRightArrow| unicode:: U+027F9 .. LONG RIGHTWARDS DOUBLE ARROW
+.. |DoubleRightArrow| unicode:: U+021D2 .. RIGHTWARDS DOUBLE ARROW
+.. |DoubleRightTee| unicode:: U+022A8 .. TRUE
+.. |DoubleUpArrow| unicode:: U+021D1 .. UPWARDS DOUBLE ARROW
+.. |DoubleUpDownArrow| unicode:: U+021D5 .. UP DOWN DOUBLE ARROW
+.. |DoubleVerticalBar| unicode:: U+02225 .. PARALLEL TO
+.. |DownArrow| unicode:: U+02193 .. DOWNWARDS ARROW
+.. |Downarrow| unicode:: U+021D3 .. DOWNWARDS DOUBLE ARROW
+.. |downarrow| unicode:: U+02193 .. DOWNWARDS ARROW
+.. |DownArrowUpArrow| unicode:: U+021F5 .. DOWNWARDS ARROW LEFTWARDS OF UPWARDS ARROW
+.. |downdownarrows| unicode:: U+021CA .. DOWNWARDS PAIRED ARROWS
+.. |downharpoonleft| unicode:: U+021C3 .. DOWNWARDS HARPOON WITH BARB LEFTWARDS
+.. |downharpoonright| unicode:: U+021C2 .. DOWNWARDS HARPOON WITH BARB RIGHTWARDS
+.. |DownLeftVector| unicode:: U+021BD .. LEFTWARDS HARPOON WITH BARB DOWNWARDS
+.. |DownRightVector| unicode:: U+021C1 .. RIGHTWARDS HARPOON WITH BARB DOWNWARDS
+.. |DownTee| unicode:: U+022A4 .. DOWN TACK
+.. |DownTeeArrow| unicode:: U+021A7 .. DOWNWARDS ARROW FROM BAR
+.. |drbkarow| unicode:: U+02910 .. RIGHTWARDS TWO-HEADED TRIPLE DASH ARROW
+.. |Element| unicode:: U+02208 .. ELEMENT OF
+.. |emptyset| unicode:: U+02205 .. EMPTY SET
+.. |eqcirc| unicode:: U+02256 .. RING IN EQUAL TO
+.. |eqcolon| unicode:: U+02255 .. EQUALS COLON
+.. |eqsim| unicode:: U+02242 .. MINUS TILDE
+.. |eqslantgtr| unicode:: U+02A96 .. SLANTED EQUAL TO OR GREATER-THAN
+.. |eqslantless| unicode:: U+02A95 .. SLANTED EQUAL TO OR LESS-THAN
+.. |EqualTilde| unicode:: U+02242 .. MINUS TILDE
+.. |Equilibrium| unicode:: U+021CC .. RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON
+.. |Exists| unicode:: U+02203 .. THERE EXISTS
+.. |expectation| unicode:: U+02130 .. SCRIPT CAPITAL E
+.. |ExponentialE| unicode:: U+02147 .. DOUBLE-STRUCK ITALIC SMALL E
+.. |exponentiale| unicode:: U+02147 .. DOUBLE-STRUCK ITALIC SMALL E
+.. |fallingdotseq| unicode:: U+02252 .. APPROXIMATELY EQUAL TO OR THE IMAGE OF
+.. |ForAll| unicode:: U+02200 .. FOR ALL
+.. |Fouriertrf| unicode:: U+02131 .. SCRIPT CAPITAL F
+.. |geq| unicode:: U+02265 .. GREATER-THAN OR EQUAL TO
+.. |geqq| unicode:: U+02267 .. GREATER-THAN OVER EQUAL TO
+.. |geqslant| unicode:: U+02A7E .. GREATER-THAN OR SLANTED EQUAL TO
+.. |gg| unicode:: U+0226B .. MUCH GREATER-THAN
+.. |ggg| unicode:: U+022D9 .. VERY MUCH GREATER-THAN
+.. |gnapprox| unicode:: U+02A8A .. GREATER-THAN AND NOT APPROXIMATE
+.. |gneq| unicode:: U+02A88 .. GREATER-THAN AND SINGLE-LINE NOT EQUAL TO
+.. |gneqq| unicode:: U+02269 .. GREATER-THAN BUT NOT EQUAL TO
+.. |GreaterEqual| unicode:: U+02265 .. GREATER-THAN OR EQUAL TO
+.. |GreaterEqualLess| unicode:: U+022DB .. GREATER-THAN EQUAL TO OR LESS-THAN
+.. |GreaterFullEqual| unicode:: U+02267 .. GREATER-THAN OVER EQUAL TO
+.. |GreaterLess| unicode:: U+02277 .. GREATER-THAN OR LESS-THAN
+.. |GreaterSlantEqual| unicode:: U+02A7E .. GREATER-THAN OR SLANTED EQUAL TO
+.. |GreaterTilde| unicode:: U+02273 .. GREATER-THAN OR EQUIVALENT TO
+.. |gtrapprox| unicode:: U+02A86 .. GREATER-THAN OR APPROXIMATE
+.. |gtrdot| unicode:: U+022D7 .. GREATER-THAN WITH DOT
+.. |gtreqless| unicode:: U+022DB .. GREATER-THAN EQUAL TO OR LESS-THAN
+.. |gtreqqless| unicode:: U+02A8C .. GREATER-THAN ABOVE DOUBLE-LINE EQUAL ABOVE LESS-THAN
+.. |gtrless| unicode:: U+02277 .. GREATER-THAN OR LESS-THAN
+.. |gtrsim| unicode:: U+02273 .. GREATER-THAN OR EQUIVALENT TO
+.. |gvertneqq| unicode:: U+02269 U+0FE00 .. GREATER-THAN BUT NOT EQUAL TO - with vertical stroke
+.. |Hacek| unicode:: U+002C7 .. CARON
+.. |hbar| unicode:: U+0210F .. PLANCK CONSTANT OVER TWO PI
+.. |heartsuit| unicode:: U+02665 .. BLACK HEART SUIT
+.. |HilbertSpace| unicode:: U+0210B .. SCRIPT CAPITAL H
+.. |hksearow| unicode:: U+02925 .. SOUTH EAST ARROW WITH HOOK
+.. |hkswarow| unicode:: U+02926 .. SOUTH WEST ARROW WITH HOOK
+.. |hookleftarrow| unicode:: U+021A9 .. LEFTWARDS ARROW WITH HOOK
+.. |hookrightarrow| unicode:: U+021AA .. RIGHTWARDS ARROW WITH HOOK
+.. |hslash| unicode:: U+0210F .. PLANCK CONSTANT OVER TWO PI
+.. |HumpDownHump| unicode:: U+0224E .. GEOMETRICALLY EQUIVALENT TO
+.. |HumpEqual| unicode:: U+0224F .. DIFFERENCE BETWEEN
+.. |iiiint| unicode:: U+02A0C .. QUADRUPLE INTEGRAL OPERATOR
+.. |iiint| unicode:: U+0222D .. TRIPLE INTEGRAL
+.. |Im| unicode:: U+02111 .. BLACK-LETTER CAPITAL I
+.. |ImaginaryI| unicode:: U+02148 .. DOUBLE-STRUCK ITALIC SMALL I
+.. |imagline| unicode:: U+02110 .. SCRIPT CAPITAL I
+.. |imagpart| unicode:: U+02111 .. BLACK-LETTER CAPITAL I
+.. |Implies| unicode:: U+021D2 .. RIGHTWARDS DOUBLE ARROW
+.. |in| unicode:: U+02208 .. ELEMENT OF
+.. |integers| unicode:: U+02124 .. DOUBLE-STRUCK CAPITAL Z
+.. |Integral| unicode:: U+0222B .. INTEGRAL
+.. |intercal| unicode:: U+022BA .. INTERCALATE
+.. |Intersection| unicode:: U+022C2 .. N-ARY INTERSECTION
+.. |intprod| unicode:: U+02A3C .. INTERIOR PRODUCT
+.. |InvisibleComma| unicode:: U+02063 .. INVISIBLE SEPARATOR
+.. |InvisibleTimes| unicode:: U+02062 .. INVISIBLE TIMES
+.. |langle| unicode:: U+02329 .. LEFT-POINTING ANGLE BRACKET
+.. |Laplacetrf| unicode:: U+02112 .. SCRIPT CAPITAL L
+.. |lbrace| unicode:: U+0007B .. LEFT CURLY BRACKET
+.. |lbrack| unicode:: U+0005B .. LEFT SQUARE BRACKET
+.. |LeftAngleBracket| unicode:: U+02329 .. LEFT-POINTING ANGLE BRACKET
+.. |LeftArrow| unicode:: U+02190 .. LEFTWARDS ARROW
+.. |Leftarrow| unicode:: U+021D0 .. LEFTWARDS DOUBLE ARROW
+.. |leftarrow| unicode:: U+02190 .. LEFTWARDS ARROW
+.. |LeftArrowBar| unicode:: U+021E4 .. LEFTWARDS ARROW TO BAR
+.. |LeftArrowRightArrow| unicode:: U+021C6 .. LEFTWARDS ARROW OVER RIGHTWARDS ARROW
+.. |leftarrowtail| unicode:: U+021A2 .. LEFTWARDS ARROW WITH TAIL
+.. |LeftCeiling| unicode:: U+02308 .. LEFT CEILING
+.. |LeftDoubleBracket| unicode:: U+0301A .. LEFT WHITE SQUARE BRACKET
+.. |LeftDownVector| unicode:: U+021C3 .. DOWNWARDS HARPOON WITH BARB LEFTWARDS
+.. |LeftFloor| unicode:: U+0230A .. LEFT FLOOR
+.. |leftharpoondown| unicode:: U+021BD .. LEFTWARDS HARPOON WITH BARB DOWNWARDS
+.. |leftharpoonup| unicode:: U+021BC .. LEFTWARDS HARPOON WITH BARB UPWARDS
+.. |leftleftarrows| unicode:: U+021C7 .. LEFTWARDS PAIRED ARROWS
+.. |LeftRightArrow| unicode:: U+02194 .. LEFT RIGHT ARROW
+.. |Leftrightarrow| unicode:: U+021D4 .. LEFT RIGHT DOUBLE ARROW
+.. |leftrightarrow| unicode:: U+02194 .. LEFT RIGHT ARROW
+.. |leftrightarrows| unicode:: U+021C6 .. LEFTWARDS ARROW OVER RIGHTWARDS ARROW
+.. |leftrightharpoons| unicode:: U+021CB .. LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON
+.. |leftrightsquigarrow| unicode:: U+021AD .. LEFT RIGHT WAVE ARROW
+.. |LeftTee| unicode:: U+022A3 .. LEFT TACK
+.. |LeftTeeArrow| unicode:: U+021A4 .. LEFTWARDS ARROW FROM BAR
+.. |leftthreetimes| unicode:: U+022CB .. LEFT SEMIDIRECT PRODUCT
+.. |LeftTriangle| unicode:: U+022B2 .. NORMAL SUBGROUP OF
+.. |LeftTriangleEqual| unicode:: U+022B4 .. NORMAL SUBGROUP OF OR EQUAL TO
+.. |LeftUpVector| unicode:: U+021BF .. UPWARDS HARPOON WITH BARB LEFTWARDS
+.. |LeftVector| unicode:: U+021BC .. LEFTWARDS HARPOON WITH BARB UPWARDS
+.. |leq| unicode:: U+02264 .. LESS-THAN OR EQUAL TO
+.. |leqq| unicode:: U+02266 .. LESS-THAN OVER EQUAL TO
+.. |leqslant| unicode:: U+02A7D .. LESS-THAN OR SLANTED EQUAL TO
+.. |lessapprox| unicode:: U+02A85 .. LESS-THAN OR APPROXIMATE
+.. |lessdot| unicode:: U+022D6 .. LESS-THAN WITH DOT
+.. |lesseqgtr| unicode:: U+022DA .. LESS-THAN EQUAL TO OR GREATER-THAN
+.. |lesseqqgtr| unicode:: U+02A8B .. LESS-THAN ABOVE DOUBLE-LINE EQUAL ABOVE GREATER-THAN
+.. |LessEqualGreater| unicode:: U+022DA .. LESS-THAN EQUAL TO OR GREATER-THAN
+.. |LessFullEqual| unicode:: U+02266 .. LESS-THAN OVER EQUAL TO
+.. |LessGreater| unicode:: U+02276 .. LESS-THAN OR GREATER-THAN
+.. |lessgtr| unicode:: U+02276 .. LESS-THAN OR GREATER-THAN
+.. |lesssim| unicode:: U+02272 .. LESS-THAN OR EQUIVALENT TO
+.. |LessSlantEqual| unicode:: U+02A7D .. LESS-THAN OR SLANTED EQUAL TO
+.. |LessTilde| unicode:: U+02272 .. LESS-THAN OR EQUIVALENT TO
+.. |ll| unicode:: U+0226A .. MUCH LESS-THAN
+.. |llcorner| unicode:: U+0231E .. BOTTOM LEFT CORNER
+.. |Lleftarrow| unicode:: U+021DA .. LEFTWARDS TRIPLE ARROW
+.. |lmoustache| unicode:: U+023B0 .. UPPER LEFT OR LOWER RIGHT CURLY BRACKET SECTION
+.. |lnapprox| unicode:: U+02A89 .. LESS-THAN AND NOT APPROXIMATE
+.. |lneq| unicode:: U+02A87 .. LESS-THAN AND SINGLE-LINE NOT EQUAL TO
+.. |lneqq| unicode:: U+02268 .. LESS-THAN BUT NOT EQUAL TO
+.. |LongLeftArrow| unicode:: U+027F5 .. LONG LEFTWARDS ARROW
+.. |Longleftarrow| unicode:: U+027F8 .. LONG LEFTWARDS DOUBLE ARROW
+.. |longleftarrow| unicode:: U+027F5 .. LONG LEFTWARDS ARROW
+.. |LongLeftRightArrow| unicode:: U+027F7 .. LONG LEFT RIGHT ARROW
+.. |Longleftrightarrow| unicode:: U+027FA .. LONG LEFT RIGHT DOUBLE ARROW
+.. |longleftrightarrow| unicode:: U+027F7 .. LONG LEFT RIGHT ARROW
+.. |longmapsto| unicode:: U+027FC .. LONG RIGHTWARDS ARROW FROM BAR
+.. |LongRightArrow| unicode:: U+027F6 .. LONG RIGHTWARDS ARROW
+.. |Longrightarrow| unicode:: U+027F9 .. LONG RIGHTWARDS DOUBLE ARROW
+.. |longrightarrow| unicode:: U+027F6 .. LONG RIGHTWARDS ARROW
+.. |looparrowleft| unicode:: U+021AB .. LEFTWARDS ARROW WITH LOOP
+.. |looparrowright| unicode:: U+021AC .. RIGHTWARDS ARROW WITH LOOP
+.. |LowerLeftArrow| unicode:: U+02199 .. SOUTH WEST ARROW
+.. |LowerRightArrow| unicode:: U+02198 .. SOUTH EAST ARROW
+.. |lozenge| unicode:: U+025CA .. LOZENGE
+.. |lrcorner| unicode:: U+0231F .. BOTTOM RIGHT CORNER
+.. |Lsh| unicode:: U+021B0 .. UPWARDS ARROW WITH TIP LEFTWARDS
+.. |lvertneqq| unicode:: U+02268 U+0FE00 .. LESS-THAN BUT NOT EQUAL TO - with vertical stroke
+.. |maltese| unicode:: U+02720 .. MALTESE CROSS
+.. |mapsto| unicode:: U+021A6 .. RIGHTWARDS ARROW FROM BAR
+.. |measuredangle| unicode:: U+02221 .. MEASURED ANGLE
+.. |Mellintrf| unicode:: U+02133 .. SCRIPT CAPITAL M
+.. |MinusPlus| unicode:: U+02213 .. MINUS-OR-PLUS SIGN
+.. |mp| unicode:: U+02213 .. MINUS-OR-PLUS SIGN
+.. |multimap| unicode:: U+022B8 .. MULTIMAP
+.. |napprox| unicode:: U+02249 .. NOT ALMOST EQUAL TO
+.. |natural| unicode:: U+0266E .. MUSIC NATURAL SIGN
+.. |naturals| unicode:: U+02115 .. DOUBLE-STRUCK CAPITAL N
+.. |nearrow| unicode:: U+02197 .. NORTH EAST ARROW
+.. |NegativeMediumSpace| unicode:: U+0200B .. ZERO WIDTH SPACE
+.. |NegativeThickSpace| unicode:: U+0200B .. ZERO WIDTH SPACE
+.. |NegativeThinSpace| unicode:: U+0200B .. ZERO WIDTH SPACE
+.. |NegativeVeryThinSpace| unicode:: U+0200B .. ZERO WIDTH SPACE
+.. |NestedGreaterGreater| unicode:: U+0226B .. MUCH GREATER-THAN
+.. |NestedLessLess| unicode:: U+0226A .. MUCH LESS-THAN
+.. |nexists| unicode:: U+02204 .. THERE DOES NOT EXIST
+.. |ngeq| unicode:: U+02271 .. NEITHER GREATER-THAN NOR EQUAL TO
+.. |ngeqq| unicode:: U+02267 U+00338 .. GREATER-THAN OVER EQUAL TO with slash
+.. |ngeqslant| unicode:: U+02A7E U+00338 .. GREATER-THAN OR SLANTED EQUAL TO with slash
+.. |ngtr| unicode:: U+0226F .. NOT GREATER-THAN
+.. |nLeftarrow| unicode:: U+021CD .. LEFTWARDS DOUBLE ARROW WITH STROKE
+.. |nleftarrow| unicode:: U+0219A .. LEFTWARDS ARROW WITH STROKE
+.. |nLeftrightarrow| unicode:: U+021CE .. LEFT RIGHT DOUBLE ARROW WITH STROKE
+.. |nleftrightarrow| unicode:: U+021AE .. LEFT RIGHT ARROW WITH STROKE
+.. |nleq| unicode:: U+02270 .. NEITHER LESS-THAN NOR EQUAL TO
+.. |nleqq| unicode:: U+02266 U+00338 .. LESS-THAN OVER EQUAL TO with slash
+.. |nleqslant| unicode:: U+02A7D U+00338 .. LESS-THAN OR SLANTED EQUAL TO with slash
+.. |nless| unicode:: U+0226E .. NOT LESS-THAN
+.. |NonBreakingSpace| unicode:: U+000A0 .. NO-BREAK SPACE
+.. |NotCongruent| unicode:: U+02262 .. NOT IDENTICAL TO
+.. |NotDoubleVerticalBar| unicode:: U+02226 .. NOT PARALLEL TO
+.. |NotElement| unicode:: U+02209 .. NOT AN ELEMENT OF
+.. |NotEqual| unicode:: U+02260 .. NOT EQUAL TO
+.. |NotEqualTilde| unicode:: U+02242 U+00338 .. MINUS TILDE with slash
+.. |NotExists| unicode:: U+02204 .. THERE DOES NOT EXIST
+.. |NotGreater| unicode:: U+0226F .. NOT GREATER-THAN
+.. |NotGreaterEqual| unicode:: U+02271 .. NEITHER GREATER-THAN NOR EQUAL TO
+.. |NotGreaterFullEqual| unicode:: U+02266 U+00338 .. LESS-THAN OVER EQUAL TO with slash
+.. |NotGreaterGreater| unicode:: U+0226B U+00338 .. MUCH GREATER THAN with slash
+.. |NotGreaterLess| unicode:: U+02279 .. NEITHER GREATER-THAN NOR LESS-THAN
+.. |NotGreaterSlantEqual| unicode:: U+02A7E U+00338 .. GREATER-THAN OR SLANTED EQUAL TO with slash
+.. |NotGreaterTilde| unicode:: U+02275 .. NEITHER GREATER-THAN NOR EQUIVALENT TO
+.. |NotHumpDownHump| unicode:: U+0224E U+00338 .. GEOMETRICALLY EQUIVALENT TO with slash
+.. |NotLeftTriangle| unicode:: U+022EA .. NOT NORMAL SUBGROUP OF
+.. |NotLeftTriangleEqual| unicode:: U+022EC .. NOT NORMAL SUBGROUP OF OR EQUAL TO
+.. |NotLess| unicode:: U+0226E .. NOT LESS-THAN
+.. |NotLessEqual| unicode:: U+02270 .. NEITHER LESS-THAN NOR EQUAL TO
+.. |NotLessGreater| unicode:: U+02278 .. NEITHER LESS-THAN NOR GREATER-THAN
+.. |NotLessLess| unicode:: U+0226A U+00338 .. MUCH LESS THAN with slash
+.. |NotLessSlantEqual| unicode:: U+02A7D U+00338 .. LESS-THAN OR SLANTED EQUAL TO with slash
+.. |NotLessTilde| unicode:: U+02274 .. NEITHER LESS-THAN NOR EQUIVALENT TO
+.. |NotPrecedes| unicode:: U+02280 .. DOES NOT PRECEDE
+.. |NotPrecedesEqual| unicode:: U+02AAF U+00338 .. PRECEDES ABOVE SINGLE-LINE EQUALS SIGN with slash
+.. |NotPrecedesSlantEqual| unicode:: U+022E0 .. DOES NOT PRECEDE OR EQUAL
+.. |NotReverseElement| unicode:: U+0220C .. DOES NOT CONTAIN AS MEMBER
+.. |NotRightTriangle| unicode:: U+022EB .. DOES NOT CONTAIN AS NORMAL SUBGROUP
+.. |NotRightTriangleEqual| unicode:: U+022ED .. DOES NOT CONTAIN AS NORMAL SUBGROUP OR EQUAL
+.. |NotSquareSubsetEqual| unicode:: U+022E2 .. NOT SQUARE IMAGE OF OR EQUAL TO
+.. |NotSquareSupersetEqual| unicode:: U+022E3 .. NOT SQUARE ORIGINAL OF OR EQUAL TO
+.. |NotSubset| unicode:: U+02282 U+020D2 .. SUBSET OF with vertical line
+.. |NotSubsetEqual| unicode:: U+02288 .. NEITHER A SUBSET OF NOR EQUAL TO
+.. |NotSucceeds| unicode:: U+02281 .. DOES NOT SUCCEED
+.. |NotSucceedsEqual| unicode:: U+02AB0 U+00338 .. SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN with slash
+.. |NotSucceedsSlantEqual| unicode:: U+022E1 .. DOES NOT SUCCEED OR EQUAL
+.. |NotSuperset| unicode:: U+02283 U+020D2 .. SUPERSET OF with vertical line
+.. |NotSupersetEqual| unicode:: U+02289 .. NEITHER A SUPERSET OF NOR EQUAL TO
+.. |NotTilde| unicode:: U+02241 .. NOT TILDE
+.. |NotTildeEqual| unicode:: U+02244 .. NOT ASYMPTOTICALLY EQUAL TO
+.. |NotTildeFullEqual| unicode:: U+02247 .. NEITHER APPROXIMATELY NOR ACTUALLY EQUAL TO
+.. |NotTildeTilde| unicode:: U+02249 .. NOT ALMOST EQUAL TO
+.. |NotVerticalBar| unicode:: U+02224 .. DOES NOT DIVIDE
+.. |nparallel| unicode:: U+02226 .. NOT PARALLEL TO
+.. |nprec| unicode:: U+02280 .. DOES NOT PRECEDE
+.. |npreceq| unicode:: U+02AAF U+00338 .. PRECEDES ABOVE SINGLE-LINE EQUALS SIGN with slash
+.. |nRightarrow| unicode:: U+021CF .. RIGHTWARDS DOUBLE ARROW WITH STROKE
+.. |nrightarrow| unicode:: U+0219B .. RIGHTWARDS ARROW WITH STROKE
+.. |nshortmid| unicode:: U+02224 .. DOES NOT DIVIDE
+.. |nshortparallel| unicode:: U+02226 .. NOT PARALLEL TO
+.. |nsimeq| unicode:: U+02244 .. NOT ASYMPTOTICALLY EQUAL TO
+.. |nsubset| unicode:: U+02282 U+020D2 .. SUBSET OF with vertical line
+.. |nsubseteq| unicode:: U+02288 .. NEITHER A SUBSET OF NOR EQUAL TO
+.. |nsubseteqq| unicode:: U+02AC5 U+00338 .. SUBSET OF ABOVE EQUALS SIGN with slash
+.. |nsucc| unicode:: U+02281 .. DOES NOT SUCCEED
+.. |nsucceq| unicode:: U+02AB0 U+00338 .. SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN with slash
+.. |nsupset| unicode:: U+02283 U+020D2 .. SUPERSET OF with vertical line
+.. |nsupseteq| unicode:: U+02289 .. NEITHER A SUPERSET OF NOR EQUAL TO
+.. |nsupseteqq| unicode:: U+02AC6 U+00338 .. SUPERSET OF ABOVE EQUALS SIGN with slash
+.. |ntriangleleft| unicode:: U+022EA .. NOT NORMAL SUBGROUP OF
+.. |ntrianglelefteq| unicode:: U+022EC .. NOT NORMAL SUBGROUP OF OR EQUAL TO
+.. |ntriangleright| unicode:: U+022EB .. DOES NOT CONTAIN AS NORMAL SUBGROUP
+.. |ntrianglerighteq| unicode:: U+022ED .. DOES NOT CONTAIN AS NORMAL SUBGROUP OR EQUAL
+.. |nwarrow| unicode:: U+02196 .. NORTH WEST ARROW
+.. |oint| unicode:: U+0222E .. CONTOUR INTEGRAL
+.. |OpenCurlyDoubleQuote| unicode:: U+0201C .. LEFT DOUBLE QUOTATION MARK
+.. |OpenCurlyQuote| unicode:: U+02018 .. LEFT SINGLE QUOTATION MARK
+.. |orderof| unicode:: U+02134 .. SCRIPT SMALL O
+.. |parallel| unicode:: U+02225 .. PARALLEL TO
+.. |PartialD| unicode:: U+02202 .. PARTIAL DIFFERENTIAL
+.. |pitchfork| unicode:: U+022D4 .. PITCHFORK
+.. |PlusMinus| unicode:: U+000B1 .. PLUS-MINUS SIGN
+.. |pm| unicode:: U+000B1 .. PLUS-MINUS SIGN
+.. |Poincareplane| unicode:: U+0210C .. BLACK-LETTER CAPITAL H
+.. |prec| unicode:: U+0227A .. PRECEDES
+.. |precapprox| unicode:: U+02AB7 .. PRECEDES ABOVE ALMOST EQUAL TO
+.. |preccurlyeq| unicode:: U+0227C .. PRECEDES OR EQUAL TO
+.. |Precedes| unicode:: U+0227A .. PRECEDES
+.. |PrecedesEqual| unicode:: U+02AAF .. PRECEDES ABOVE SINGLE-LINE EQUALS SIGN
+.. |PrecedesSlantEqual| unicode:: U+0227C .. PRECEDES OR EQUAL TO
+.. |PrecedesTilde| unicode:: U+0227E .. PRECEDES OR EQUIVALENT TO
+.. |preceq| unicode:: U+02AAF .. PRECEDES ABOVE SINGLE-LINE EQUALS SIGN
+.. |precnapprox| unicode:: U+02AB9 .. PRECEDES ABOVE NOT ALMOST EQUAL TO
+.. |precneqq| unicode:: U+02AB5 .. PRECEDES ABOVE NOT EQUAL TO
+.. |precnsim| unicode:: U+022E8 .. PRECEDES BUT NOT EQUIVALENT TO
+.. |precsim| unicode:: U+0227E .. PRECEDES OR EQUIVALENT TO
+.. |primes| unicode:: U+02119 .. DOUBLE-STRUCK CAPITAL P
+.. |Proportion| unicode:: U+02237 .. PROPORTION
+.. |Proportional| unicode:: U+0221D .. PROPORTIONAL TO
+.. |propto| unicode:: U+0221D .. PROPORTIONAL TO
+.. |quaternions| unicode:: U+0210D .. DOUBLE-STRUCK CAPITAL H
+.. |questeq| unicode:: U+0225F .. QUESTIONED EQUAL TO
+.. |rangle| unicode:: U+0232A .. RIGHT-POINTING ANGLE BRACKET
+.. |rationals| unicode:: U+0211A .. DOUBLE-STRUCK CAPITAL Q
+.. |rbrace| unicode:: U+0007D .. RIGHT CURLY BRACKET
+.. |rbrack| unicode:: U+0005D .. RIGHT SQUARE BRACKET
+.. |Re| unicode:: U+0211C .. BLACK-LETTER CAPITAL R
+.. |realine| unicode:: U+0211B .. SCRIPT CAPITAL R
+.. |realpart| unicode:: U+0211C .. BLACK-LETTER CAPITAL R
+.. |reals| unicode:: U+0211D .. DOUBLE-STRUCK CAPITAL R
+.. |ReverseElement| unicode:: U+0220B .. CONTAINS AS MEMBER
+.. |ReverseEquilibrium| unicode:: U+021CB .. LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON
+.. |ReverseUpEquilibrium| unicode:: U+0296F .. DOWNWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT
+.. |RightAngleBracket| unicode:: U+0232A .. RIGHT-POINTING ANGLE BRACKET
+.. |RightArrow| unicode:: U+02192 .. RIGHTWARDS ARROW
+.. |Rightarrow| unicode:: U+021D2 .. RIGHTWARDS DOUBLE ARROW
+.. |rightarrow| unicode:: U+02192 .. RIGHTWARDS ARROW
+.. |RightArrowBar| unicode:: U+021E5 .. RIGHTWARDS ARROW TO BAR
+.. |RightArrowLeftArrow| unicode:: U+021C4 .. RIGHTWARDS ARROW OVER LEFTWARDS ARROW
+.. |rightarrowtail| unicode:: U+021A3 .. RIGHTWARDS ARROW WITH TAIL
+.. |RightCeiling| unicode:: U+02309 .. RIGHT CEILING
+.. |RightDoubleBracket| unicode:: U+0301B .. RIGHT WHITE SQUARE BRACKET
+.. |RightDownVector| unicode:: U+021C2 .. DOWNWARDS HARPOON WITH BARB RIGHTWARDS
+.. |RightFloor| unicode:: U+0230B .. RIGHT FLOOR
+.. |rightharpoondown| unicode:: U+021C1 .. RIGHTWARDS HARPOON WITH BARB DOWNWARDS
+.. |rightharpoonup| unicode:: U+021C0 .. RIGHTWARDS HARPOON WITH BARB UPWARDS
+.. |rightleftarrows| unicode:: U+021C4 .. RIGHTWARDS ARROW OVER LEFTWARDS ARROW
+.. |rightleftharpoons| unicode:: U+021CC .. RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON
+.. |rightrightarrows| unicode:: U+021C9 .. RIGHTWARDS PAIRED ARROWS
+.. |rightsquigarrow| unicode:: U+0219D .. RIGHTWARDS WAVE ARROW
+.. |RightTee| unicode:: U+022A2 .. RIGHT TACK
+.. |RightTeeArrow| unicode:: U+021A6 .. RIGHTWARDS ARROW FROM BAR
+.. |rightthreetimes| unicode:: U+022CC .. RIGHT SEMIDIRECT PRODUCT
+.. |RightTriangle| unicode:: U+022B3 .. CONTAINS AS NORMAL SUBGROUP
+.. |RightTriangleEqual| unicode:: U+022B5 .. CONTAINS AS NORMAL SUBGROUP OR EQUAL TO
+.. |RightUpVector| unicode:: U+021BE .. UPWARDS HARPOON WITH BARB RIGHTWARDS
+.. |RightVector| unicode:: U+021C0 .. RIGHTWARDS HARPOON WITH BARB UPWARDS
+.. |risingdotseq| unicode:: U+02253 .. IMAGE OF OR APPROXIMATELY EQUAL TO
+.. |rmoustache| unicode:: U+023B1 .. UPPER RIGHT OR LOWER LEFT CURLY BRACKET SECTION
+.. |Rrightarrow| unicode:: U+021DB .. RIGHTWARDS TRIPLE ARROW
+.. |Rsh| unicode:: U+021B1 .. UPWARDS ARROW WITH TIP RIGHTWARDS
+.. |searrow| unicode:: U+02198 .. SOUTH EAST ARROW
+.. |setminus| unicode:: U+02216 .. SET MINUS
+.. |ShortDownArrow| unicode:: U+02193 .. DOWNWARDS ARROW
+.. |ShortLeftArrow| unicode:: U+02190 .. LEFTWARDS ARROW
+.. |shortmid| unicode:: U+02223 .. DIVIDES
+.. |shortparallel| unicode:: U+02225 .. PARALLEL TO
+.. |ShortRightArrow| unicode:: U+02192 .. RIGHTWARDS ARROW
+.. |ShortUpArrow| unicode:: U+02191 .. UPWARDS ARROW
+.. |simeq| unicode:: U+02243 .. ASYMPTOTICALLY EQUAL TO
+.. |SmallCircle| unicode:: U+02218 .. RING OPERATOR
+.. |smallsetminus| unicode:: U+02216 .. SET MINUS
+.. |spadesuit| unicode:: U+02660 .. BLACK SPADE SUIT
+.. |Sqrt| unicode:: U+0221A .. SQUARE ROOT
+.. |sqsubset| unicode:: U+0228F .. SQUARE IMAGE OF
+.. |sqsubseteq| unicode:: U+02291 .. SQUARE IMAGE OF OR EQUAL TO
+.. |sqsupset| unicode:: U+02290 .. SQUARE ORIGINAL OF
+.. |sqsupseteq| unicode:: U+02292 .. SQUARE ORIGINAL OF OR EQUAL TO
+.. |Square| unicode:: U+025A1 .. WHITE SQUARE
+.. |SquareIntersection| unicode:: U+02293 .. SQUARE CAP
+.. |SquareSubset| unicode:: U+0228F .. SQUARE IMAGE OF
+.. |SquareSubsetEqual| unicode:: U+02291 .. SQUARE IMAGE OF OR EQUAL TO
+.. |SquareSuperset| unicode:: U+02290 .. SQUARE ORIGINAL OF
+.. |SquareSupersetEqual| unicode:: U+02292 .. SQUARE ORIGINAL OF OR EQUAL TO
+.. |SquareUnion| unicode:: U+02294 .. SQUARE CUP
+.. |Star| unicode:: U+022C6 .. STAR OPERATOR
+.. |straightepsilon| unicode:: U+003F5 .. GREEK LUNATE EPSILON SYMBOL
+.. |straightphi| unicode:: U+003D5 .. GREEK PHI SYMBOL
+.. |Subset| unicode:: U+022D0 .. DOUBLE SUBSET
+.. |subset| unicode:: U+02282 .. SUBSET OF
+.. |subseteq| unicode:: U+02286 .. SUBSET OF OR EQUAL TO
+.. |subseteqq| unicode:: U+02AC5 .. SUBSET OF ABOVE EQUALS SIGN
+.. |SubsetEqual| unicode:: U+02286 .. SUBSET OF OR EQUAL TO
+.. |subsetneq| unicode:: U+0228A .. SUBSET OF WITH NOT EQUAL TO
+.. |subsetneqq| unicode:: U+02ACB .. SUBSET OF ABOVE NOT EQUAL TO
+.. |succ| unicode:: U+0227B .. SUCCEEDS
+.. |succapprox| unicode:: U+02AB8 .. SUCCEEDS ABOVE ALMOST EQUAL TO
+.. |succcurlyeq| unicode:: U+0227D .. SUCCEEDS OR EQUAL TO
+.. |Succeeds| unicode:: U+0227B .. SUCCEEDS
+.. |SucceedsEqual| unicode:: U+02AB0 .. SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN
+.. |SucceedsSlantEqual| unicode:: U+0227D .. SUCCEEDS OR EQUAL TO
+.. |SucceedsTilde| unicode:: U+0227F .. SUCCEEDS OR EQUIVALENT TO
+.. |succeq| unicode:: U+02AB0 .. SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN
+.. |succnapprox| unicode:: U+02ABA .. SUCCEEDS ABOVE NOT ALMOST EQUAL TO
+.. |succneqq| unicode:: U+02AB6 .. SUCCEEDS ABOVE NOT EQUAL TO
+.. |succnsim| unicode:: U+022E9 .. SUCCEEDS BUT NOT EQUIVALENT TO
+.. |succsim| unicode:: U+0227F .. SUCCEEDS OR EQUIVALENT TO
+.. |SuchThat| unicode:: U+0220B .. CONTAINS AS MEMBER
+.. |Sum| unicode:: U+02211 .. N-ARY SUMMATION
+.. |Superset| unicode:: U+02283 .. SUPERSET OF
+.. |SupersetEqual| unicode:: U+02287 .. SUPERSET OF OR EQUAL TO
+.. |Supset| unicode:: U+022D1 .. DOUBLE SUPERSET
+.. |supset| unicode:: U+02283 .. SUPERSET OF
+.. |supseteq| unicode:: U+02287 .. SUPERSET OF OR EQUAL TO
+.. |supseteqq| unicode:: U+02AC6 .. SUPERSET OF ABOVE EQUALS SIGN
+.. |supsetneq| unicode:: U+0228B .. SUPERSET OF WITH NOT EQUAL TO
+.. |supsetneqq| unicode:: U+02ACC .. SUPERSET OF ABOVE NOT EQUAL TO
+.. |swarrow| unicode:: U+02199 .. SOUTH WEST ARROW
+.. |Therefore| unicode:: U+02234 .. THEREFORE
+.. |therefore| unicode:: U+02234 .. THEREFORE
+.. |thickapprox| unicode:: U+02248 .. ALMOST EQUAL TO
+.. |thicksim| unicode:: U+0223C .. TILDE OPERATOR
+.. |ThinSpace| unicode:: U+02009 .. THIN SPACE
+.. |Tilde| unicode:: U+0223C .. TILDE OPERATOR
+.. |TildeEqual| unicode:: U+02243 .. ASYMPTOTICALLY EQUAL TO
+.. |TildeFullEqual| unicode:: U+02245 .. APPROXIMATELY EQUAL TO
+.. |TildeTilde| unicode:: U+02248 .. ALMOST EQUAL TO
+.. |toea| unicode:: U+02928 .. NORTH EAST ARROW AND SOUTH EAST ARROW
+.. |tosa| unicode:: U+02929 .. SOUTH EAST ARROW AND SOUTH WEST ARROW
+.. |triangle| unicode:: U+025B5 .. WHITE UP-POINTING SMALL TRIANGLE
+.. |triangledown| unicode:: U+025BF .. WHITE DOWN-POINTING SMALL TRIANGLE
+.. |triangleleft| unicode:: U+025C3 .. WHITE LEFT-POINTING SMALL TRIANGLE
+.. |trianglelefteq| unicode:: U+022B4 .. NORMAL SUBGROUP OF OR EQUAL TO
+.. |triangleq| unicode:: U+0225C .. DELTA EQUAL TO
+.. |triangleright| unicode:: U+025B9 .. WHITE RIGHT-POINTING SMALL TRIANGLE
+.. |trianglerighteq| unicode:: U+022B5 .. CONTAINS AS NORMAL SUBGROUP OR EQUAL TO
+.. |TripleDot| unicode:: U+020DB .. COMBINING THREE DOTS ABOVE
+.. |twoheadleftarrow| unicode:: U+0219E .. LEFTWARDS TWO HEADED ARROW
+.. |twoheadrightarrow| unicode:: U+021A0 .. RIGHTWARDS TWO HEADED ARROW
+.. |ulcorner| unicode:: U+0231C .. TOP LEFT CORNER
+.. |Union| unicode:: U+022C3 .. N-ARY UNION
+.. |UnionPlus| unicode:: U+0228E .. MULTISET UNION
+.. |UpArrow| unicode:: U+02191 .. UPWARDS ARROW
+.. |Uparrow| unicode:: U+021D1 .. UPWARDS DOUBLE ARROW
+.. |uparrow| unicode:: U+02191 .. UPWARDS ARROW
+.. |UpArrowDownArrow| unicode:: U+021C5 .. UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW
+.. |UpDownArrow| unicode:: U+02195 .. UP DOWN ARROW
+.. |Updownarrow| unicode:: U+021D5 .. UP DOWN DOUBLE ARROW
+.. |updownarrow| unicode:: U+02195 .. UP DOWN ARROW
+.. |UpEquilibrium| unicode:: U+0296E .. UPWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT
+.. |upharpoonleft| unicode:: U+021BF .. UPWARDS HARPOON WITH BARB LEFTWARDS
+.. |upharpoonright| unicode:: U+021BE .. UPWARDS HARPOON WITH BARB RIGHTWARDS
+.. |UpperLeftArrow| unicode:: U+02196 .. NORTH WEST ARROW
+.. |UpperRightArrow| unicode:: U+02197 .. NORTH EAST ARROW
+.. |upsilon| unicode:: U+003C5 .. GREEK SMALL LETTER UPSILON
+.. |UpTee| unicode:: U+022A5 .. UP TACK
+.. |UpTeeArrow| unicode:: U+021A5 .. UPWARDS ARROW FROM BAR
+.. |upuparrows| unicode:: U+021C8 .. UPWARDS PAIRED ARROWS
+.. |urcorner| unicode:: U+0231D .. TOP RIGHT CORNER
+.. |varepsilon| unicode:: U+003B5 .. GREEK SMALL LETTER EPSILON
+.. |varkappa| unicode:: U+003F0 .. GREEK KAPPA SYMBOL
+.. |varnothing| unicode:: U+02205 .. EMPTY SET
+.. |varphi| unicode:: U+003C6 .. GREEK SMALL LETTER PHI
+.. |varpi| unicode:: U+003D6 .. GREEK PI SYMBOL
+.. |varpropto| unicode:: U+0221D .. PROPORTIONAL TO
+.. |varrho| unicode:: U+003F1 .. GREEK RHO SYMBOL
+.. |varsigma| unicode:: U+003C2 .. GREEK SMALL LETTER FINAL SIGMA
+.. |varsubsetneq| unicode:: U+0228A U+0FE00 .. SUBSET OF WITH NOT EQUAL TO - variant with stroke through bottom members
+.. |varsubsetneqq| unicode:: U+02ACB U+0FE00 .. SUBSET OF ABOVE NOT EQUAL TO - variant with stroke through bottom members
+.. |varsupsetneq| unicode:: U+0228B U+0FE00 .. SUPERSET OF WITH NOT EQUAL TO - variant with stroke through bottom members
+.. |varsupsetneqq| unicode:: U+02ACC U+0FE00 .. SUPERSET OF ABOVE NOT EQUAL TO - variant with stroke through bottom members
+.. |vartheta| unicode:: U+003D1 .. GREEK THETA SYMBOL
+.. |vartriangleleft| unicode:: U+022B2 .. NORMAL SUBGROUP OF
+.. |vartriangleright| unicode:: U+022B3 .. CONTAINS AS NORMAL SUBGROUP
+.. |Vee| unicode:: U+022C1 .. N-ARY LOGICAL OR
+.. |vee| unicode:: U+02228 .. LOGICAL OR
+.. |Vert| unicode:: U+02016 .. DOUBLE VERTICAL LINE
+.. |vert| unicode:: U+0007C .. VERTICAL LINE
+.. |VerticalBar| unicode:: U+02223 .. DIVIDES
+.. |VerticalTilde| unicode:: U+02240 .. WREATH PRODUCT
+.. |VeryThinSpace| unicode:: U+0200A .. HAIR SPACE
+.. |Wedge| unicode:: U+022C0 .. N-ARY LOGICAL AND
+.. |wedge| unicode:: U+02227 .. LOGICAL AND
+.. |wp| unicode:: U+02118 .. SCRIPT CAPITAL P
+.. |wr| unicode:: U+02240 .. WREATH PRODUCT
+.. |zeetrf| unicode:: U+02128 .. BLACK-LETTER CAPITAL Z
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlextra-wide.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlextra-wide.txt
new file mode 100644
index 00000000..f45fc8cb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlextra-wide.txt
@@ -0,0 +1,113 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |af| unicode:: U+02061 .. FUNCTION APPLICATION
+.. |aopf| unicode:: U+1D552 .. MATHEMATICAL DOUBLE-STRUCK SMALL A
+.. |asympeq| unicode:: U+0224D .. EQUIVALENT TO
+.. |bopf| unicode:: U+1D553 .. MATHEMATICAL DOUBLE-STRUCK SMALL B
+.. |copf| unicode:: U+1D554 .. MATHEMATICAL DOUBLE-STRUCK SMALL C
+.. |Cross| unicode:: U+02A2F .. VECTOR OR CROSS PRODUCT
+.. |DD| unicode:: U+02145 .. DOUBLE-STRUCK ITALIC CAPITAL D
+.. |dd| unicode:: U+02146 .. DOUBLE-STRUCK ITALIC SMALL D
+.. |dopf| unicode:: U+1D555 .. MATHEMATICAL DOUBLE-STRUCK SMALL D
+.. |DownArrowBar| unicode:: U+02913 .. DOWNWARDS ARROW TO BAR
+.. |DownBreve| unicode:: U+00311 .. COMBINING INVERTED BREVE
+.. |DownLeftRightVector| unicode:: U+02950 .. LEFT BARB DOWN RIGHT BARB DOWN HARPOON
+.. |DownLeftTeeVector| unicode:: U+0295E .. LEFTWARDS HARPOON WITH BARB DOWN FROM BAR
+.. |DownLeftVectorBar| unicode:: U+02956 .. LEFTWARDS HARPOON WITH BARB DOWN TO BAR
+.. |DownRightTeeVector| unicode:: U+0295F .. RIGHTWARDS HARPOON WITH BARB DOWN FROM BAR
+.. |DownRightVectorBar| unicode:: U+02957 .. RIGHTWARDS HARPOON WITH BARB DOWN TO BAR
+.. |ee| unicode:: U+02147 .. DOUBLE-STRUCK ITALIC SMALL E
+.. |EmptySmallSquare| unicode:: U+025FB .. WHITE MEDIUM SQUARE
+.. |EmptyVerySmallSquare| unicode:: U+025AB .. WHITE SMALL SQUARE
+.. |eopf| unicode:: U+1D556 .. MATHEMATICAL DOUBLE-STRUCK SMALL E
+.. |Equal| unicode:: U+02A75 .. TWO CONSECUTIVE EQUALS SIGNS
+.. |FilledSmallSquare| unicode:: U+025FC .. BLACK MEDIUM SQUARE
+.. |FilledVerySmallSquare| unicode:: U+025AA .. BLACK SMALL SQUARE
+.. |fopf| unicode:: U+1D557 .. MATHEMATICAL DOUBLE-STRUCK SMALL F
+.. |gopf| unicode:: U+1D558 .. MATHEMATICAL DOUBLE-STRUCK SMALL G
+.. |GreaterGreater| unicode:: U+02AA2 .. DOUBLE NESTED GREATER-THAN
+.. |Hat| unicode:: U+0005E .. CIRCUMFLEX ACCENT
+.. |hopf| unicode:: U+1D559 .. MATHEMATICAL DOUBLE-STRUCK SMALL H
+.. |HorizontalLine| unicode:: U+02500 .. BOX DRAWINGS LIGHT HORIZONTAL
+.. |ic| unicode:: U+02063 .. INVISIBLE SEPARATOR
+.. |ii| unicode:: U+02148 .. DOUBLE-STRUCK ITALIC SMALL I
+.. |iopf| unicode:: U+1D55A .. MATHEMATICAL DOUBLE-STRUCK SMALL I
+.. |it| unicode:: U+02062 .. INVISIBLE TIMES
+.. |jopf| unicode:: U+1D55B .. MATHEMATICAL DOUBLE-STRUCK SMALL J
+.. |kopf| unicode:: U+1D55C .. MATHEMATICAL DOUBLE-STRUCK SMALL K
+.. |larrb| unicode:: U+021E4 .. LEFTWARDS ARROW TO BAR
+.. |LeftDownTeeVector| unicode:: U+02961 .. DOWNWARDS HARPOON WITH BARB LEFT FROM BAR
+.. |LeftDownVectorBar| unicode:: U+02959 .. DOWNWARDS HARPOON WITH BARB LEFT TO BAR
+.. |LeftRightVector| unicode:: U+0294E .. LEFT BARB UP RIGHT BARB UP HARPOON
+.. |LeftTeeVector| unicode:: U+0295A .. LEFTWARDS HARPOON WITH BARB UP FROM BAR
+.. |LeftTriangleBar| unicode:: U+029CF .. LEFT TRIANGLE BESIDE VERTICAL BAR
+.. |LeftUpDownVector| unicode:: U+02951 .. UP BARB LEFT DOWN BARB LEFT HARPOON
+.. |LeftUpTeeVector| unicode:: U+02960 .. UPWARDS HARPOON WITH BARB LEFT FROM BAR
+.. |LeftUpVectorBar| unicode:: U+02958 .. UPWARDS HARPOON WITH BARB LEFT TO BAR
+.. |LeftVectorBar| unicode:: U+02952 .. LEFTWARDS HARPOON WITH BARB UP TO BAR
+.. |LessLess| unicode:: U+02AA1 .. DOUBLE NESTED LESS-THAN
+.. |lopf| unicode:: U+1D55D .. MATHEMATICAL DOUBLE-STRUCK SMALL L
+.. |mapstodown| unicode:: U+021A7 .. DOWNWARDS ARROW FROM BAR
+.. |mapstoleft| unicode:: U+021A4 .. LEFTWARDS ARROW FROM BAR
+.. |mapstoup| unicode:: U+021A5 .. UPWARDS ARROW FROM BAR
+.. |MediumSpace| unicode:: U+0205F .. MEDIUM MATHEMATICAL SPACE
+.. |mopf| unicode:: U+1D55E .. MATHEMATICAL DOUBLE-STRUCK SMALL M
+.. |nbump| unicode:: U+0224E U+00338 .. GEOMETRICALLY EQUIVALENT TO with slash
+.. |nbumpe| unicode:: U+0224F U+00338 .. DIFFERENCE BETWEEN with slash
+.. |nesim| unicode:: U+02242 U+00338 .. MINUS TILDE with slash
+.. |NewLine| unicode:: U+0000A .. LINE FEED (LF)
+.. |NoBreak| unicode:: U+02060 .. WORD JOINER
+.. |nopf| unicode:: U+1D55F .. MATHEMATICAL DOUBLE-STRUCK SMALL N
+.. |NotCupCap| unicode:: U+0226D .. NOT EQUIVALENT TO
+.. |NotHumpEqual| unicode:: U+0224F U+00338 .. DIFFERENCE BETWEEN with slash
+.. |NotLeftTriangleBar| unicode:: U+029CF U+00338 .. LEFT TRIANGLE BESIDE VERTICAL BAR with slash
+.. |NotNestedGreaterGreater| unicode:: U+02AA2 U+00338 .. DOUBLE NESTED GREATER-THAN with slash
+.. |NotNestedLessLess| unicode:: U+02AA1 U+00338 .. DOUBLE NESTED LESS-THAN with slash
+.. |NotRightTriangleBar| unicode:: U+029D0 U+00338 .. VERTICAL BAR BESIDE RIGHT TRIANGLE with slash
+.. |NotSquareSubset| unicode:: U+0228F U+00338 .. SQUARE IMAGE OF with slash
+.. |NotSquareSuperset| unicode:: U+02290 U+00338 .. SQUARE ORIGINAL OF with slash
+.. |NotSucceedsTilde| unicode:: U+0227F U+00338 .. SUCCEEDS OR EQUIVALENT TO with slash
+.. |oopf| unicode:: U+1D560 .. MATHEMATICAL DOUBLE-STRUCK SMALL O
+.. |OverBar| unicode:: U+000AF .. MACRON
+.. |OverBrace| unicode:: U+0FE37 .. PRESENTATION FORM FOR VERTICAL LEFT CURLY BRACKET
+.. |OverBracket| unicode:: U+023B4 .. TOP SQUARE BRACKET
+.. |OverParenthesis| unicode:: U+0FE35 .. PRESENTATION FORM FOR VERTICAL LEFT PARENTHESIS
+.. |planckh| unicode:: U+0210E .. PLANCK CONSTANT
+.. |popf| unicode:: U+1D561 .. MATHEMATICAL DOUBLE-STRUCK SMALL P
+.. |Product| unicode:: U+0220F .. N-ARY PRODUCT
+.. |qopf| unicode:: U+1D562 .. MATHEMATICAL DOUBLE-STRUCK SMALL Q
+.. |rarrb| unicode:: U+021E5 .. RIGHTWARDS ARROW TO BAR
+.. |RightDownTeeVector| unicode:: U+0295D .. DOWNWARDS HARPOON WITH BARB RIGHT FROM BAR
+.. |RightDownVectorBar| unicode:: U+02955 .. DOWNWARDS HARPOON WITH BARB RIGHT TO BAR
+.. |RightTeeVector| unicode:: U+0295B .. RIGHTWARDS HARPOON WITH BARB UP FROM BAR
+.. |RightTriangleBar| unicode:: U+029D0 .. VERTICAL BAR BESIDE RIGHT TRIANGLE
+.. |RightUpDownVector| unicode:: U+0294F .. UP BARB RIGHT DOWN BARB RIGHT HARPOON
+.. |RightUpTeeVector| unicode:: U+0295C .. UPWARDS HARPOON WITH BARB RIGHT FROM BAR
+.. |RightUpVectorBar| unicode:: U+02954 .. UPWARDS HARPOON WITH BARB RIGHT TO BAR
+.. |RightVectorBar| unicode:: U+02953 .. RIGHTWARDS HARPOON WITH BARB UP TO BAR
+.. |ropf| unicode:: U+1D563 .. MATHEMATICAL DOUBLE-STRUCK SMALL R
+.. |RoundImplies| unicode:: U+02970 .. RIGHT DOUBLE ARROW WITH ROUNDED HEAD
+.. |RuleDelayed| unicode:: U+029F4 .. RULE-DELAYED
+.. |sopf| unicode:: U+1D564 .. MATHEMATICAL DOUBLE-STRUCK SMALL S
+.. |Tab| unicode:: U+00009 .. CHARACTER TABULATION
+.. |ThickSpace| unicode:: U+02009 U+0200A U+0200A .. space of width 5/18 em
+.. |topf| unicode:: U+1D565 .. MATHEMATICAL DOUBLE-STRUCK SMALL T
+.. |UnderBar| unicode:: U+00332 .. COMBINING LOW LINE
+.. |UnderBrace| unicode:: U+0FE38 .. PRESENTATION FORM FOR VERTICAL RIGHT CURLY BRACKET
+.. |UnderBracket| unicode:: U+023B5 .. BOTTOM SQUARE BRACKET
+.. |UnderParenthesis| unicode:: U+0FE36 .. PRESENTATION FORM FOR VERTICAL RIGHT PARENTHESIS
+.. |uopf| unicode:: U+1D566 .. MATHEMATICAL DOUBLE-STRUCK SMALL U
+.. |UpArrowBar| unicode:: U+02912 .. UPWARDS ARROW TO BAR
+.. |Upsilon| unicode:: U+003A5 .. GREEK CAPITAL LETTER UPSILON
+.. |VerticalLine| unicode:: U+0007C .. VERTICAL LINE
+.. |VerticalSeparator| unicode:: U+02758 .. LIGHT VERTICAL BAR
+.. |vopf| unicode:: U+1D567 .. MATHEMATICAL DOUBLE-STRUCK SMALL V
+.. |wopf| unicode:: U+1D568 .. MATHEMATICAL DOUBLE-STRUCK SMALL W
+.. |xopf| unicode:: U+1D569 .. MATHEMATICAL DOUBLE-STRUCK SMALL X
+.. |yopf| unicode:: U+1D56A .. MATHEMATICAL DOUBLE-STRUCK SMALL Y
+.. |ZeroWidthSpace| unicode:: U+0200B .. ZERO WIDTH SPACE
+.. |zopf| unicode:: U+1D56B .. MATHEMATICAL DOUBLE-STRUCK SMALL Z
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlextra.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlextra.txt
new file mode 100644
index 00000000..27262473
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/mmlextra.txt
@@ -0,0 +1,87 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |af| unicode:: U+02061 .. FUNCTION APPLICATION
+.. |asympeq| unicode:: U+0224D .. EQUIVALENT TO
+.. |Cross| unicode:: U+02A2F .. VECTOR OR CROSS PRODUCT
+.. |DD| unicode:: U+02145 .. DOUBLE-STRUCK ITALIC CAPITAL D
+.. |dd| unicode:: U+02146 .. DOUBLE-STRUCK ITALIC SMALL D
+.. |DownArrowBar| unicode:: U+02913 .. DOWNWARDS ARROW TO BAR
+.. |DownBreve| unicode:: U+00311 .. COMBINING INVERTED BREVE
+.. |DownLeftRightVector| unicode:: U+02950 .. LEFT BARB DOWN RIGHT BARB DOWN HARPOON
+.. |DownLeftTeeVector| unicode:: U+0295E .. LEFTWARDS HARPOON WITH BARB DOWN FROM BAR
+.. |DownLeftVectorBar| unicode:: U+02956 .. LEFTWARDS HARPOON WITH BARB DOWN TO BAR
+.. |DownRightTeeVector| unicode:: U+0295F .. RIGHTWARDS HARPOON WITH BARB DOWN FROM BAR
+.. |DownRightVectorBar| unicode:: U+02957 .. RIGHTWARDS HARPOON WITH BARB DOWN TO BAR
+.. |ee| unicode:: U+02147 .. DOUBLE-STRUCK ITALIC SMALL E
+.. |EmptySmallSquare| unicode:: U+025FB .. WHITE MEDIUM SQUARE
+.. |EmptyVerySmallSquare| unicode:: U+025AB .. WHITE SMALL SQUARE
+.. |Equal| unicode:: U+02A75 .. TWO CONSECUTIVE EQUALS SIGNS
+.. |FilledSmallSquare| unicode:: U+025FC .. BLACK MEDIUM SQUARE
+.. |FilledVerySmallSquare| unicode:: U+025AA .. BLACK SMALL SQUARE
+.. |GreaterGreater| unicode:: U+02AA2 .. DOUBLE NESTED GREATER-THAN
+.. |Hat| unicode:: U+0005E .. CIRCUMFLEX ACCENT
+.. |HorizontalLine| unicode:: U+02500 .. BOX DRAWINGS LIGHT HORIZONTAL
+.. |ic| unicode:: U+02063 .. INVISIBLE SEPARATOR
+.. |ii| unicode:: U+02148 .. DOUBLE-STRUCK ITALIC SMALL I
+.. |it| unicode:: U+02062 .. INVISIBLE TIMES
+.. |larrb| unicode:: U+021E4 .. LEFTWARDS ARROW TO BAR
+.. |LeftDownTeeVector| unicode:: U+02961 .. DOWNWARDS HARPOON WITH BARB LEFT FROM BAR
+.. |LeftDownVectorBar| unicode:: U+02959 .. DOWNWARDS HARPOON WITH BARB LEFT TO BAR
+.. |LeftRightVector| unicode:: U+0294E .. LEFT BARB UP RIGHT BARB UP HARPOON
+.. |LeftTeeVector| unicode:: U+0295A .. LEFTWARDS HARPOON WITH BARB UP FROM BAR
+.. |LeftTriangleBar| unicode:: U+029CF .. LEFT TRIANGLE BESIDE VERTICAL BAR
+.. |LeftUpDownVector| unicode:: U+02951 .. UP BARB LEFT DOWN BARB LEFT HARPOON
+.. |LeftUpTeeVector| unicode:: U+02960 .. UPWARDS HARPOON WITH BARB LEFT FROM BAR
+.. |LeftUpVectorBar| unicode:: U+02958 .. UPWARDS HARPOON WITH BARB LEFT TO BAR
+.. |LeftVectorBar| unicode:: U+02952 .. LEFTWARDS HARPOON WITH BARB UP TO BAR
+.. |LessLess| unicode:: U+02AA1 .. DOUBLE NESTED LESS-THAN
+.. |mapstodown| unicode:: U+021A7 .. DOWNWARDS ARROW FROM BAR
+.. |mapstoleft| unicode:: U+021A4 .. LEFTWARDS ARROW FROM BAR
+.. |mapstoup| unicode:: U+021A5 .. UPWARDS ARROW FROM BAR
+.. |MediumSpace| unicode:: U+0205F .. MEDIUM MATHEMATICAL SPACE
+.. |nbump| unicode:: U+0224E U+00338 .. GEOMETRICALLY EQUIVALENT TO with slash
+.. |nbumpe| unicode:: U+0224F U+00338 .. DIFFERENCE BETWEEN with slash
+.. |nesim| unicode:: U+02242 U+00338 .. MINUS TILDE with slash
+.. |NewLine| unicode:: U+0000A .. LINE FEED (LF)
+.. |NoBreak| unicode:: U+02060 .. WORD JOINER
+.. |NotCupCap| unicode:: U+0226D .. NOT EQUIVALENT TO
+.. |NotHumpEqual| unicode:: U+0224F U+00338 .. DIFFERENCE BETWEEN with slash
+.. |NotLeftTriangleBar| unicode:: U+029CF U+00338 .. LEFT TRIANGLE BESIDE VERTICAL BAR with slash
+.. |NotNestedGreaterGreater| unicode:: U+02AA2 U+00338 .. DOUBLE NESTED GREATER-THAN with slash
+.. |NotNestedLessLess| unicode:: U+02AA1 U+00338 .. DOUBLE NESTED LESS-THAN with slash
+.. |NotRightTriangleBar| unicode:: U+029D0 U+00338 .. VERTICAL BAR BESIDE RIGHT TRIANGLE with slash
+.. |NotSquareSubset| unicode:: U+0228F U+00338 .. SQUARE IMAGE OF with slash
+.. |NotSquareSuperset| unicode:: U+02290 U+00338 .. SQUARE ORIGINAL OF with slash
+.. |NotSucceedsTilde| unicode:: U+0227F U+00338 .. SUCCEEDS OR EQUIVALENT TO with slash
+.. |OverBar| unicode:: U+000AF .. MACRON
+.. |OverBrace| unicode:: U+0FE37 .. PRESENTATION FORM FOR VERTICAL LEFT CURLY BRACKET
+.. |OverBracket| unicode:: U+023B4 .. TOP SQUARE BRACKET
+.. |OverParenthesis| unicode:: U+0FE35 .. PRESENTATION FORM FOR VERTICAL LEFT PARENTHESIS
+.. |planckh| unicode:: U+0210E .. PLANCK CONSTANT
+.. |Product| unicode:: U+0220F .. N-ARY PRODUCT
+.. |rarrb| unicode:: U+021E5 .. RIGHTWARDS ARROW TO BAR
+.. |RightDownTeeVector| unicode:: U+0295D .. DOWNWARDS HARPOON WITH BARB RIGHT FROM BAR
+.. |RightDownVectorBar| unicode:: U+02955 .. DOWNWARDS HARPOON WITH BARB RIGHT TO BAR
+.. |RightTeeVector| unicode:: U+0295B .. RIGHTWARDS HARPOON WITH BARB UP FROM BAR
+.. |RightTriangleBar| unicode:: U+029D0 .. VERTICAL BAR BESIDE RIGHT TRIANGLE
+.. |RightUpDownVector| unicode:: U+0294F .. UP BARB RIGHT DOWN BARB RIGHT HARPOON
+.. |RightUpTeeVector| unicode:: U+0295C .. UPWARDS HARPOON WITH BARB RIGHT FROM BAR
+.. |RightUpVectorBar| unicode:: U+02954 .. UPWARDS HARPOON WITH BARB RIGHT TO BAR
+.. |RightVectorBar| unicode:: U+02953 .. RIGHTWARDS HARPOON WITH BARB UP TO BAR
+.. |RoundImplies| unicode:: U+02970 .. RIGHT DOUBLE ARROW WITH ROUNDED HEAD
+.. |RuleDelayed| unicode:: U+029F4 .. RULE-DELAYED
+.. |Tab| unicode:: U+00009 .. CHARACTER TABULATION
+.. |ThickSpace| unicode:: U+02009 U+0200A U+0200A .. space of width 5/18 em
+.. |UnderBar| unicode:: U+00332 .. COMBINING LOW LINE
+.. |UnderBrace| unicode:: U+0FE38 .. PRESENTATION FORM FOR VERTICAL RIGHT CURLY BRACKET
+.. |UnderBracket| unicode:: U+023B5 .. BOTTOM SQUARE BRACKET
+.. |UnderParenthesis| unicode:: U+0FE36 .. PRESENTATION FORM FOR VERTICAL RIGHT PARENTHESIS
+.. |UpArrowBar| unicode:: U+02912 .. UPWARDS ARROW TO BAR
+.. |Upsilon| unicode:: U+003A5 .. GREEK CAPITAL LETTER UPSILON
+.. |VerticalLine| unicode:: U+0007C .. VERTICAL LINE
+.. |VerticalSeparator| unicode:: U+02758 .. LIGHT VERTICAL BAR
+.. |ZeroWidthSpace| unicode:: U+0200B .. ZERO WIDTH SPACE
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/s5defs.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/s5defs.txt
new file mode 100644
index 00000000..8aceeac0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/s5defs.txt
@@ -0,0 +1,68 @@
+.. Definitions of interpreted text roles (classes) for S5/HTML data.
+.. This data file has been placed in the public domain.
+
+.. Colours
+ =======
+
+.. role:: black
+.. role:: gray
+.. role:: silver
+.. role:: white
+
+.. role:: maroon
+.. role:: red
+.. role:: magenta
+.. role:: fuchsia
+.. role:: pink
+.. role:: orange
+.. role:: yellow
+.. role:: lime
+.. role:: green
+.. role:: olive
+.. role:: teal
+.. role:: cyan
+.. role:: aqua
+.. role:: blue
+.. role:: navy
+.. role:: purple
+
+
+.. Text Sizes
+ ==========
+
+.. role:: huge
+.. role:: big
+.. role:: small
+.. role:: tiny
+
+
+.. Display in Slides (Presentation Mode) Only
+ ==========================================
+
+.. role:: slide
+ :class: slide-display
+
+
+.. Display in Outline Mode Only
+ ============================
+
+.. role:: outline
+
+
+.. Display in Print Only
+ =====================
+
+.. role:: print
+
+
+.. Display in Handout Mode Only
+ ============================
+
+.. role:: handout
+
+
+.. Incremental Display
+ ===================
+
+.. role:: incremental
+.. default-role:: incremental
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-lat1.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-lat1.txt
new file mode 100644
index 00000000..1cae194e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-lat1.txt
@@ -0,0 +1,102 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |Aacute| unicode:: U+000C1 .. LATIN CAPITAL LETTER A WITH ACUTE
+.. |aacute| unicode:: U+000E1 .. LATIN SMALL LETTER A WITH ACUTE
+.. |Acirc| unicode:: U+000C2 .. LATIN CAPITAL LETTER A WITH CIRCUMFLEX
+.. |acirc| unicode:: U+000E2 .. LATIN SMALL LETTER A WITH CIRCUMFLEX
+.. |acute| unicode:: U+000B4 .. ACUTE ACCENT
+.. |AElig| unicode:: U+000C6 .. LATIN CAPITAL LETTER AE
+.. |aelig| unicode:: U+000E6 .. LATIN SMALL LETTER AE
+.. |Agrave| unicode:: U+000C0 .. LATIN CAPITAL LETTER A WITH GRAVE
+.. |agrave| unicode:: U+000E0 .. LATIN SMALL LETTER A WITH GRAVE
+.. |Aring| unicode:: U+000C5 .. LATIN CAPITAL LETTER A WITH RING ABOVE
+.. |aring| unicode:: U+000E5 .. LATIN SMALL LETTER A WITH RING ABOVE
+.. |Atilde| unicode:: U+000C3 .. LATIN CAPITAL LETTER A WITH TILDE
+.. |atilde| unicode:: U+000E3 .. LATIN SMALL LETTER A WITH TILDE
+.. |Auml| unicode:: U+000C4 .. LATIN CAPITAL LETTER A WITH DIAERESIS
+.. |auml| unicode:: U+000E4 .. LATIN SMALL LETTER A WITH DIAERESIS
+.. |brvbar| unicode:: U+000A6 .. BROKEN BAR
+.. |Ccedil| unicode:: U+000C7 .. LATIN CAPITAL LETTER C WITH CEDILLA
+.. |ccedil| unicode:: U+000E7 .. LATIN SMALL LETTER C WITH CEDILLA
+.. |cedil| unicode:: U+000B8 .. CEDILLA
+.. |cent| unicode:: U+000A2 .. CENT SIGN
+.. |copy| unicode:: U+000A9 .. COPYRIGHT SIGN
+.. |curren| unicode:: U+000A4 .. CURRENCY SIGN
+.. |deg| unicode:: U+000B0 .. DEGREE SIGN
+.. |divide| unicode:: U+000F7 .. DIVISION SIGN
+.. |Eacute| unicode:: U+000C9 .. LATIN CAPITAL LETTER E WITH ACUTE
+.. |eacute| unicode:: U+000E9 .. LATIN SMALL LETTER E WITH ACUTE
+.. |Ecirc| unicode:: U+000CA .. LATIN CAPITAL LETTER E WITH CIRCUMFLEX
+.. |ecirc| unicode:: U+000EA .. LATIN SMALL LETTER E WITH CIRCUMFLEX
+.. |Egrave| unicode:: U+000C8 .. LATIN CAPITAL LETTER E WITH GRAVE
+.. |egrave| unicode:: U+000E8 .. LATIN SMALL LETTER E WITH GRAVE
+.. |ETH| unicode:: U+000D0 .. LATIN CAPITAL LETTER ETH
+.. |eth| unicode:: U+000F0 .. LATIN SMALL LETTER ETH
+.. |Euml| unicode:: U+000CB .. LATIN CAPITAL LETTER E WITH DIAERESIS
+.. |euml| unicode:: U+000EB .. LATIN SMALL LETTER E WITH DIAERESIS
+.. |frac12| unicode:: U+000BD .. VULGAR FRACTION ONE HALF
+.. |frac14| unicode:: U+000BC .. VULGAR FRACTION ONE QUARTER
+.. |frac34| unicode:: U+000BE .. VULGAR FRACTION THREE QUARTERS
+.. |Iacute| unicode:: U+000CD .. LATIN CAPITAL LETTER I WITH ACUTE
+.. |iacute| unicode:: U+000ED .. LATIN SMALL LETTER I WITH ACUTE
+.. |Icirc| unicode:: U+000CE .. LATIN CAPITAL LETTER I WITH CIRCUMFLEX
+.. |icirc| unicode:: U+000EE .. LATIN SMALL LETTER I WITH CIRCUMFLEX
+.. |iexcl| unicode:: U+000A1 .. INVERTED EXCLAMATION MARK
+.. |Igrave| unicode:: U+000CC .. LATIN CAPITAL LETTER I WITH GRAVE
+.. |igrave| unicode:: U+000EC .. LATIN SMALL LETTER I WITH GRAVE
+.. |iquest| unicode:: U+000BF .. INVERTED QUESTION MARK
+.. |Iuml| unicode:: U+000CF .. LATIN CAPITAL LETTER I WITH DIAERESIS
+.. |iuml| unicode:: U+000EF .. LATIN SMALL LETTER I WITH DIAERESIS
+.. |laquo| unicode:: U+000AB .. LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+.. |macr| unicode:: U+000AF .. MACRON
+.. |micro| unicode:: U+000B5 .. MICRO SIGN
+.. |middot| unicode:: U+000B7 .. MIDDLE DOT
+.. |nbsp| unicode:: U+000A0 .. NO-BREAK SPACE
+.. |not| unicode:: U+000AC .. NOT SIGN
+.. |Ntilde| unicode:: U+000D1 .. LATIN CAPITAL LETTER N WITH TILDE
+.. |ntilde| unicode:: U+000F1 .. LATIN SMALL LETTER N WITH TILDE
+.. |Oacute| unicode:: U+000D3 .. LATIN CAPITAL LETTER O WITH ACUTE
+.. |oacute| unicode:: U+000F3 .. LATIN SMALL LETTER O WITH ACUTE
+.. |Ocirc| unicode:: U+000D4 .. LATIN CAPITAL LETTER O WITH CIRCUMFLEX
+.. |ocirc| unicode:: U+000F4 .. LATIN SMALL LETTER O WITH CIRCUMFLEX
+.. |Ograve| unicode:: U+000D2 .. LATIN CAPITAL LETTER O WITH GRAVE
+.. |ograve| unicode:: U+000F2 .. LATIN SMALL LETTER O WITH GRAVE
+.. |ordf| unicode:: U+000AA .. FEMININE ORDINAL INDICATOR
+.. |ordm| unicode:: U+000BA .. MASCULINE ORDINAL INDICATOR
+.. |Oslash| unicode:: U+000D8 .. LATIN CAPITAL LETTER O WITH STROKE
+.. |oslash| unicode:: U+000F8 .. LATIN SMALL LETTER O WITH STROKE
+.. |Otilde| unicode:: U+000D5 .. LATIN CAPITAL LETTER O WITH TILDE
+.. |otilde| unicode:: U+000F5 .. LATIN SMALL LETTER O WITH TILDE
+.. |Ouml| unicode:: U+000D6 .. LATIN CAPITAL LETTER O WITH DIAERESIS
+.. |ouml| unicode:: U+000F6 .. LATIN SMALL LETTER O WITH DIAERESIS
+.. |para| unicode:: U+000B6 .. PILCROW SIGN
+.. |plusmn| unicode:: U+000B1 .. PLUS-MINUS SIGN
+.. |pound| unicode:: U+000A3 .. POUND SIGN
+.. |raquo| unicode:: U+000BB .. RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+.. |reg| unicode:: U+000AE .. REGISTERED SIGN
+.. |sect| unicode:: U+000A7 .. SECTION SIGN
+.. |shy| unicode:: U+000AD .. SOFT HYPHEN
+.. |sup1| unicode:: U+000B9 .. SUPERSCRIPT ONE
+.. |sup2| unicode:: U+000B2 .. SUPERSCRIPT TWO
+.. |sup3| unicode:: U+000B3 .. SUPERSCRIPT THREE
+.. |szlig| unicode:: U+000DF .. LATIN SMALL LETTER SHARP S
+.. |THORN| unicode:: U+000DE .. LATIN CAPITAL LETTER THORN
+.. |thorn| unicode:: U+000FE .. LATIN SMALL LETTER THORN
+.. |times| unicode:: U+000D7 .. MULTIPLICATION SIGN
+.. |Uacute| unicode:: U+000DA .. LATIN CAPITAL LETTER U WITH ACUTE
+.. |uacute| unicode:: U+000FA .. LATIN SMALL LETTER U WITH ACUTE
+.. |Ucirc| unicode:: U+000DB .. LATIN CAPITAL LETTER U WITH CIRCUMFLEX
+.. |ucirc| unicode:: U+000FB .. LATIN SMALL LETTER U WITH CIRCUMFLEX
+.. |Ugrave| unicode:: U+000D9 .. LATIN CAPITAL LETTER U WITH GRAVE
+.. |ugrave| unicode:: U+000F9 .. LATIN SMALL LETTER U WITH GRAVE
+.. |uml| unicode:: U+000A8 .. DIAERESIS
+.. |Uuml| unicode:: U+000DC .. LATIN CAPITAL LETTER U WITH DIAERESIS
+.. |uuml| unicode:: U+000FC .. LATIN SMALL LETTER U WITH DIAERESIS
+.. |Yacute| unicode:: U+000DD .. LATIN CAPITAL LETTER Y WITH ACUTE
+.. |yacute| unicode:: U+000FD .. LATIN SMALL LETTER Y WITH ACUTE
+.. |yen| unicode:: U+000A5 .. YEN SIGN
+.. |yuml| unicode:: U+000FF .. LATIN SMALL LETTER Y WITH DIAERESIS
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-special.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-special.txt
new file mode 100644
index 00000000..b19c0b51
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-special.txt
@@ -0,0 +1,37 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |bdquo| unicode:: U+0201E .. DOUBLE LOW-9 QUOTATION MARK
+.. |circ| unicode:: U+002C6 .. MODIFIER LETTER CIRCUMFLEX ACCENT
+.. |Dagger| unicode:: U+02021 .. DOUBLE DAGGER
+.. |dagger| unicode:: U+02020 .. DAGGER
+.. |emsp| unicode:: U+02003 .. EM SPACE
+.. |ensp| unicode:: U+02002 .. EN SPACE
+.. |euro| unicode:: U+020AC .. EURO SIGN
+.. |gt| unicode:: U+0003E .. GREATER-THAN SIGN
+.. |ldquo| unicode:: U+0201C .. LEFT DOUBLE QUOTATION MARK
+.. |lrm| unicode:: U+0200E .. LEFT-TO-RIGHT MARK
+.. |lsaquo| unicode:: U+02039 .. SINGLE LEFT-POINTING ANGLE QUOTATION MARK
+.. |lsquo| unicode:: U+02018 .. LEFT SINGLE QUOTATION MARK
+.. |lt| unicode:: U+0003C .. LESS-THAN SIGN
+.. |mdash| unicode:: U+02014 .. EM DASH
+.. |ndash| unicode:: U+02013 .. EN DASH
+.. |OElig| unicode:: U+00152 .. LATIN CAPITAL LIGATURE OE
+.. |oelig| unicode:: U+00153 .. LATIN SMALL LIGATURE OE
+.. |permil| unicode:: U+02030 .. PER MILLE SIGN
+.. |quot| unicode:: U+00022 .. QUOTATION MARK
+.. |rdquo| unicode:: U+0201D .. RIGHT DOUBLE QUOTATION MARK
+.. |rlm| unicode:: U+0200F .. RIGHT-TO-LEFT MARK
+.. |rsaquo| unicode:: U+0203A .. SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
+.. |rsquo| unicode:: U+02019 .. RIGHT SINGLE QUOTATION MARK
+.. |sbquo| unicode:: U+0201A .. SINGLE LOW-9 QUOTATION MARK
+.. |Scaron| unicode:: U+00160 .. LATIN CAPITAL LETTER S WITH CARON
+.. |scaron| unicode:: U+00161 .. LATIN SMALL LETTER S WITH CARON
+.. |thinsp| unicode:: U+02009 .. THIN SPACE
+.. |tilde| unicode:: U+002DC .. SMALL TILDE
+.. |Yuml| unicode:: U+00178 .. LATIN CAPITAL LETTER Y WITH DIAERESIS
+.. |zwj| unicode:: U+0200D .. ZERO WIDTH JOINER
+.. |zwnj| unicode:: U+0200C .. ZERO WIDTH NON-JOINER
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-symbol.txt b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-symbol.txt
new file mode 100644
index 00000000..1f935905
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/include/xhtml1-symbol.txt
@@ -0,0 +1,130 @@
+.. This data file has been placed in the public domain.
+.. Derived from the Unicode character mappings available from
+ <http://www.w3.org/2003/entities/xml/>.
+ Processed by unicode2rstsubs.py, part of Docutils:
+ <https://docutils.sourceforge.io>.
+
+.. |alefsym| unicode:: U+02135 .. ALEF SYMBOL
+.. |Alpha| unicode:: U+00391 .. GREEK CAPITAL LETTER ALPHA
+.. |alpha| unicode:: U+003B1 .. GREEK SMALL LETTER ALPHA
+.. |and| unicode:: U+02227 .. LOGICAL AND
+.. |ang| unicode:: U+02220 .. ANGLE
+.. |asymp| unicode:: U+02248 .. ALMOST EQUAL TO
+.. |Beta| unicode:: U+00392 .. GREEK CAPITAL LETTER BETA
+.. |beta| unicode:: U+003B2 .. GREEK SMALL LETTER BETA
+.. |bull| unicode:: U+02022 .. BULLET
+.. |cap| unicode:: U+02229 .. INTERSECTION
+.. |Chi| unicode:: U+003A7 .. GREEK CAPITAL LETTER CHI
+.. |chi| unicode:: U+003C7 .. GREEK SMALL LETTER CHI
+.. |clubs| unicode:: U+02663 .. BLACK CLUB SUIT
+.. |cong| unicode:: U+02245 .. APPROXIMATELY EQUAL TO
+.. |crarr| unicode:: U+021B5 .. DOWNWARDS ARROW WITH CORNER LEFTWARDS
+.. |cup| unicode:: U+0222A .. UNION
+.. |dArr| unicode:: U+021D3 .. DOWNWARDS DOUBLE ARROW
+.. |darr| unicode:: U+02193 .. DOWNWARDS ARROW
+.. |Delta| unicode:: U+00394 .. GREEK CAPITAL LETTER DELTA
+.. |delta| unicode:: U+003B4 .. GREEK SMALL LETTER DELTA
+.. |diams| unicode:: U+02666 .. BLACK DIAMOND SUIT
+.. |empty| unicode:: U+02205 .. EMPTY SET
+.. |Epsilon| unicode:: U+00395 .. GREEK CAPITAL LETTER EPSILON
+.. |epsilon| unicode:: U+003B5 .. GREEK SMALL LETTER EPSILON
+.. |equiv| unicode:: U+02261 .. IDENTICAL TO
+.. |Eta| unicode:: U+00397 .. GREEK CAPITAL LETTER ETA
+.. |eta| unicode:: U+003B7 .. GREEK SMALL LETTER ETA
+.. |exist| unicode:: U+02203 .. THERE EXISTS
+.. |fnof| unicode:: U+00192 .. LATIN SMALL LETTER F WITH HOOK
+.. |forall| unicode:: U+02200 .. FOR ALL
+.. |frasl| unicode:: U+02044 .. FRACTION SLASH
+.. |Gamma| unicode:: U+00393 .. GREEK CAPITAL LETTER GAMMA
+.. |gamma| unicode:: U+003B3 .. GREEK SMALL LETTER GAMMA
+.. |ge| unicode:: U+02265 .. GREATER-THAN OR EQUAL TO
+.. |hArr| unicode:: U+021D4 .. LEFT RIGHT DOUBLE ARROW
+.. |harr| unicode:: U+02194 .. LEFT RIGHT ARROW
+.. |hearts| unicode:: U+02665 .. BLACK HEART SUIT
+.. |hellip| unicode:: U+02026 .. HORIZONTAL ELLIPSIS
+.. |image| unicode:: U+02111 .. BLACK-LETTER CAPITAL I
+.. |infin| unicode:: U+0221E .. INFINITY
+.. |int| unicode:: U+0222B .. INTEGRAL
+.. |Iota| unicode:: U+00399 .. GREEK CAPITAL LETTER IOTA
+.. |iota| unicode:: U+003B9 .. GREEK SMALL LETTER IOTA
+.. |isin| unicode:: U+02208 .. ELEMENT OF
+.. |Kappa| unicode:: U+0039A .. GREEK CAPITAL LETTER KAPPA
+.. |kappa| unicode:: U+003BA .. GREEK SMALL LETTER KAPPA
+.. |Lambda| unicode:: U+0039B .. GREEK CAPITAL LETTER LAMDA
+.. |lambda| unicode:: U+003BB .. GREEK SMALL LETTER LAMDA
+.. |lang| unicode:: U+02329 .. LEFT-POINTING ANGLE BRACKET
+.. |lArr| unicode:: U+021D0 .. LEFTWARDS DOUBLE ARROW
+.. |larr| unicode:: U+02190 .. LEFTWARDS ARROW
+.. |lceil| unicode:: U+02308 .. LEFT CEILING
+.. |le| unicode:: U+02264 .. LESS-THAN OR EQUAL TO
+.. |lfloor| unicode:: U+0230A .. LEFT FLOOR
+.. |lowast| unicode:: U+02217 .. ASTERISK OPERATOR
+.. |loz| unicode:: U+025CA .. LOZENGE
+.. |minus| unicode:: U+02212 .. MINUS SIGN
+.. |Mu| unicode:: U+0039C .. GREEK CAPITAL LETTER MU
+.. |mu| unicode:: U+003BC .. GREEK SMALL LETTER MU
+.. |nabla| unicode:: U+02207 .. NABLA
+.. |ne| unicode:: U+02260 .. NOT EQUAL TO
+.. |ni| unicode:: U+0220B .. CONTAINS AS MEMBER
+.. |notin| unicode:: U+02209 .. NOT AN ELEMENT OF
+.. |nsub| unicode:: U+02284 .. NOT A SUBSET OF
+.. |Nu| unicode:: U+0039D .. GREEK CAPITAL LETTER NU
+.. |nu| unicode:: U+003BD .. GREEK SMALL LETTER NU
+.. |oline| unicode:: U+0203E .. OVERLINE
+.. |Omega| unicode:: U+003A9 .. GREEK CAPITAL LETTER OMEGA
+.. |omega| unicode:: U+003C9 .. GREEK SMALL LETTER OMEGA
+.. |Omicron| unicode:: U+0039F .. GREEK CAPITAL LETTER OMICRON
+.. |omicron| unicode:: U+003BF .. GREEK SMALL LETTER OMICRON
+.. |oplus| unicode:: U+02295 .. CIRCLED PLUS
+.. |or| unicode:: U+02228 .. LOGICAL OR
+.. |otimes| unicode:: U+02297 .. CIRCLED TIMES
+.. |part| unicode:: U+02202 .. PARTIAL DIFFERENTIAL
+.. |perp| unicode:: U+022A5 .. UP TACK
+.. |Phi| unicode:: U+003A6 .. GREEK CAPITAL LETTER PHI
+.. |phi| unicode:: U+003D5 .. GREEK PHI SYMBOL
+.. |Pi| unicode:: U+003A0 .. GREEK CAPITAL LETTER PI
+.. |pi| unicode:: U+003C0 .. GREEK SMALL LETTER PI
+.. |piv| unicode:: U+003D6 .. GREEK PI SYMBOL
+.. |Prime| unicode:: U+02033 .. DOUBLE PRIME
+.. |prime| unicode:: U+02032 .. PRIME
+.. |prod| unicode:: U+0220F .. N-ARY PRODUCT
+.. |prop| unicode:: U+0221D .. PROPORTIONAL TO
+.. |Psi| unicode:: U+003A8 .. GREEK CAPITAL LETTER PSI
+.. |psi| unicode:: U+003C8 .. GREEK SMALL LETTER PSI
+.. |radic| unicode:: U+0221A .. SQUARE ROOT
+.. |rang| unicode:: U+0232A .. RIGHT-POINTING ANGLE BRACKET
+.. |rArr| unicode:: U+021D2 .. RIGHTWARDS DOUBLE ARROW
+.. |rarr| unicode:: U+02192 .. RIGHTWARDS ARROW
+.. |rceil| unicode:: U+02309 .. RIGHT CEILING
+.. |real| unicode:: U+0211C .. BLACK-LETTER CAPITAL R
+.. |rfloor| unicode:: U+0230B .. RIGHT FLOOR
+.. |Rho| unicode:: U+003A1 .. GREEK CAPITAL LETTER RHO
+.. |rho| unicode:: U+003C1 .. GREEK SMALL LETTER RHO
+.. |sdot| unicode:: U+022C5 .. DOT OPERATOR
+.. |Sigma| unicode:: U+003A3 .. GREEK CAPITAL LETTER SIGMA
+.. |sigma| unicode:: U+003C3 .. GREEK SMALL LETTER SIGMA
+.. |sigmaf| unicode:: U+003C2 .. GREEK SMALL LETTER FINAL SIGMA
+.. |sim| unicode:: U+0223C .. TILDE OPERATOR
+.. |spades| unicode:: U+02660 .. BLACK SPADE SUIT
+.. |sub| unicode:: U+02282 .. SUBSET OF
+.. |sube| unicode:: U+02286 .. SUBSET OF OR EQUAL TO
+.. |sum| unicode:: U+02211 .. N-ARY SUMMATION
+.. |sup| unicode:: U+02283 .. SUPERSET OF
+.. |supe| unicode:: U+02287 .. SUPERSET OF OR EQUAL TO
+.. |Tau| unicode:: U+003A4 .. GREEK CAPITAL LETTER TAU
+.. |tau| unicode:: U+003C4 .. GREEK SMALL LETTER TAU
+.. |there4| unicode:: U+02234 .. THEREFORE
+.. |Theta| unicode:: U+00398 .. GREEK CAPITAL LETTER THETA
+.. |theta| unicode:: U+003B8 .. GREEK SMALL LETTER THETA
+.. |thetasym| unicode:: U+003D1 .. GREEK THETA SYMBOL
+.. |trade| unicode:: U+02122 .. TRADE MARK SIGN
+.. |uArr| unicode:: U+021D1 .. UPWARDS DOUBLE ARROW
+.. |uarr| unicode:: U+02191 .. UPWARDS ARROW
+.. |upsih| unicode:: U+003D2 .. GREEK UPSILON WITH HOOK SYMBOL
+.. |Upsilon| unicode:: U+003A5 .. GREEK CAPITAL LETTER UPSILON
+.. |upsilon| unicode:: U+003C5 .. GREEK SMALL LETTER UPSILON
+.. |weierp| unicode:: U+02118 .. SCRIPT CAPITAL P
+.. |Xi| unicode:: U+0039E .. GREEK CAPITAL LETTER XI
+.. |xi| unicode:: U+003BE .. GREEK SMALL LETTER XI
+.. |Zeta| unicode:: U+00396 .. GREEK CAPITAL LETTER ZETA
+.. |zeta| unicode:: U+003B6 .. GREEK SMALL LETTER ZETA
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/__init__.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/__init__.py
new file mode 100644
index 00000000..a8bfd231
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/__init__.py
@@ -0,0 +1,40 @@
+# $Id: __init__.py 9026 2022-03-04 15:57:13Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+# Internationalization details are documented in
+# <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+
+"""
+This package contains modules for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from docutils.languages import LanguageImporter
+
+
+class RstLanguageImporter(LanguageImporter):
+ """Import language modules.
+
+ When called with a BCP 47 language tag, instances return a module
+ with localisations for "directive" and "role" names for from
+ `docutils.parsers.rst.languages` or the PYTHONPATH.
+
+ If there is no matching module, warn (if a `reporter` is passed)
+ and return None.
+ """
+ packages = ('docutils.parsers.rst.languages.', '')
+ warn_msg = 'rST localisation for language "%s" not found.'
+ fallback = None
+
+ def check_content(self, module):
+ """Check if we got an rST language module."""
+ if not (isinstance(module.directives, dict)
+ and isinstance(module.roles, dict)):
+ raise ImportError
+
+
+get_language = RstLanguageImporter()
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/af.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/af.py
new file mode 100644
index 00000000..31cb4ebf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/af.py
@@ -0,0 +1,108 @@
+# $Id: af.py 9417 2023-06-27 20:04:54Z milde $
+# Author: Jannie Hofmeyr <jhsh@sun.ac.za>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Afrikaans-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ 'aandag': 'attention',
+ 'versigtig': 'caution',
+ 'code (translation required)': 'code',
+ 'gevaar': 'danger',
+ 'fout': 'error',
+ 'wenk': 'hint',
+ 'belangrik': 'important',
+ 'nota': 'note',
+ 'tip': 'tip', # hint and tip both have the same translation: wenk
+ 'waarskuwing': 'warning',
+ 'advies': 'admonition',
+ 'vermaning': 'admonition', # sic! kept for backwards compatibiltity
+ 'kantstreep': 'sidebar',
+ 'onderwerp': 'topic',
+ 'lynblok': 'line-block',
+ 'math (translation required)': 'math',
+ 'parsed-literal (translation required)': 'parsed-literal',
+ 'rubriek': 'rubric',
+ 'epigraaf': 'epigraph',
+ 'hoogtepunte': 'highlights',
+ 'pull-quote (translation required)': 'pull-quote',
+ 'compound (translation required)': 'compound',
+ 'container (translation required)': 'container',
+ # 'vrae': 'questions',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'table (translation required)': 'table',
+ 'csv-table (translation required)': 'csv-table',
+ 'list-table (translation required)': 'list-table',
+ 'meta': 'meta',
+ # 'beeldkaart': 'imagemap',
+ 'beeld': 'image',
+ 'figuur': 'figure',
+ 'insluiting': 'include',
+ 'rou': 'raw',
+ 'vervang': 'replace',
+ 'unicode': 'unicode', # should this be translated? unikode
+ 'datum': 'date',
+ 'klas': 'class',
+ 'role (translation required)': 'role',
+ 'default-role (translation required)': 'default-role',
+ 'title (translation required)': 'title',
+ 'inhoud': 'contents',
+ 'sectnum': 'sectnum',
+ 'section-numbering': 'sectnum',
+ 'header (translation required)': 'header',
+ 'footer (translation required)': 'footer',
+ # 'voetnote': 'footnotes',
+ # 'aanhalings': 'citations',
+ 'teikennotas': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Afrikaans name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ 'afkorting': 'abbreviation',
+ 'ab': 'abbreviation',
+ 'akroniem': 'acronym',
+ 'ac': 'acronym',
+ 'code (translation required)': 'code',
+ 'indeks': 'index',
+ 'i': 'index',
+ 'voetskrif': 'subscript',
+ 'sub': 'subscript',
+ 'boskrif': 'superscript',
+ 'sup': 'superscript',
+ 'titelverwysing': 'title-reference',
+ 'titel': 'title-reference',
+ 't': 'title-reference',
+ 'pep-verwysing': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'rfc-verwysing': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'nadruk': 'emphasis',
+ 'sterk': 'strong',
+ 'literal (translation required)': 'literal',
+ 'math (translation required)': 'math',
+ 'benoemde verwysing': 'named-reference',
+ 'anonieme verwysing': 'anonymous-reference',
+ 'voetnootverwysing': 'footnote-reference',
+ 'aanhalingverwysing': 'citation-reference',
+ 'vervangingsverwysing': 'substitution-reference',
+ 'teiken': 'target',
+ 'uri-verwysing': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'rou': 'raw',
+ }
+"""Mapping of Afrikaans role names to canonical names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ar.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ar.py
new file mode 100644
index 00000000..71be92a5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ar.py
@@ -0,0 +1,99 @@
+# $Id: fa.py 4564 2016-08-10 11:48:42Z
+# Author: Shahin <me@5hah.in>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Arabic-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+directives = {
+ # language-dependent: fixed
+ 'تنبيه': 'attention',
+ 'احتیاط': 'caution',
+ 'كود': 'code',
+ 'خطر': 'danger',
+ 'خطأ': 'error',
+ 'تلميح': 'hint',
+ 'مهم': 'important',
+ 'ملاحظة': 'note',
+ 'نصيحة': 'tip',
+ 'تحذير': 'warning',
+ 'تذكير': 'admonition',
+ 'شريط-جانبي': 'sidebar',
+ 'موضوع': 'topic',
+ 'قالب-سطري': 'line-block',
+ 'لفظ-حرفي': 'parsed-literal',
+ 'معيار': 'rubric',
+ 'فكرة-الكتاب': 'epigraph',
+ 'تمييز': 'highlights',
+ 'نقل-قول': 'pull-quote',
+ 'ترکیب': 'compound',
+ 'وعاء': 'container',
+ # 'questions': 'questions',
+ 'جدول': 'table',
+ 'جدول-csv': 'csv-table',
+ 'جدول-قوائم': 'list-table',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'ميتا': 'meta',
+ 'رياضيات': 'math',
+ # 'imagemap': 'imagemap',
+ 'صورة': 'image',
+ 'رسم-توضيحي': 'figure',
+ 'تضمين': 'include',
+ 'خام': 'raw',
+ 'تبديل': 'replace',
+ 'یونیکد': 'unicode',
+ 'تاریخ': 'date',
+ 'كائن': 'class',
+ 'قانون': 'role',
+ 'قانون-افتراضي': 'default-role',
+ 'عنوان': 'title',
+ 'المحتوى': 'contents',
+ 'رقم-الفصل': 'sectnum',
+ 'رقم-القسم': 'sectnum',
+ 'رأس-الصفحة': 'header',
+ 'هامش': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ '': 'target-notes',
+}
+"""Arabic name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'اختصار': 'abbreviation',
+ 'اختزال': 'acronym',
+ 'كود': 'code',
+ 'فهرس': 'index',
+ 'خفض': 'subscript',
+ 'رفع': 'superscript',
+ 'عنوان-مرجع': 'title-reference',
+ 'مرجع-pep': 'pep-reference',
+ 'rfc-مرجع': 'rfc-reference',
+ 'تأكيد': 'emphasis',
+ 'عريض': 'strong',
+ 'لفظی': 'literal',
+ 'رياضيات': 'math',
+ 'مرجع-مسمى': 'named-reference',
+ 'مرجع-مجهول': 'anonymous-reference',
+ 'مرجع-هامشي': 'footnote-reference',
+ 'مرجع-منقول': 'citation-reference',
+ 'مرجع-معوض': 'substitution-reference',
+ 'هدف': 'target',
+ 'منبع-uri': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'خام': 'raw',
+}
+"""Mapping of Arabic role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ca.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ca.py
new file mode 100644
index 00000000..8c8eae40
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ca.py
@@ -0,0 +1,130 @@
+# $Id: ca.py 9457 2023-10-02 16:25:50Z milde $
+# Authors: Ivan Vilata i Balaguer <ivan@selidor.net>;
+# Antoni Bella Pérez <antonibella5@yahoo.com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation,
+# please read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+# These translations can be used without changes for
+# Valencian variant of Catalan (use language tag "ca-valencia").
+# Checked by a native speaker of Valentian.
+
+"""
+Catalan-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'atenció': 'attention',
+ 'compte': 'caution',
+ 'perill': 'danger',
+ 'error': 'error',
+ 'suggeriment': 'hint',
+ 'important': 'important',
+ 'nota': 'note',
+ 'consell': 'tip',
+ 'avís': 'warning',
+ 'advertiment': 'admonition',
+ 'nota-al-marge': 'sidebar',
+ 'nota-marge': 'sidebar',
+ 'tema': 'topic',
+ 'bloc-de-línies': 'line-block',
+ 'bloc-línies': 'line-block',
+ 'literal-analitzat': 'parsed-literal',
+ 'codi': 'code',
+ 'bloc-de-codi': 'code',
+ 'matemàtiques': 'math',
+ 'rúbrica': 'rubric',
+ 'epígraf': 'epigraph',
+ 'sumari': 'highlights',
+ 'cita-destacada': 'pull-quote',
+ 'compost': 'compound',
+ 'contenidor': 'container',
+ 'taula': 'table',
+ 'taula-csv': 'csv-table',
+ 'taula-llista': 'list-table',
+ 'meta': 'meta',
+ # 'imagemap': 'imagemap',
+ 'imatge': 'image',
+ 'figura': 'figure',
+ 'inclou': 'include',
+ 'incloure': 'include',
+ 'cru': 'raw',
+ 'reemplaça': 'replace',
+ 'reemplaçar': 'replace',
+ 'unicode': 'unicode',
+ 'data': 'date',
+ 'classe': 'class',
+ 'rol': 'role',
+ 'rol-predeterminat': 'default-role',
+ 'títol': 'title',
+ 'contingut': 'contents',
+ 'numsec': 'sectnum',
+ 'numeració-de-seccions': 'sectnum',
+ 'numeració-seccions': 'sectnum',
+ 'capçalera': 'header',
+ 'peu-de-pàgina': 'footer',
+ 'peu-pàgina': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ 'notes-amb-destinacions': 'target-notes',
+ 'notes-destinacions': 'target-notes',
+ 'directiva-de-prova-de-restructuredtext': 'restructuredtext-test-directive'} # noqa:E501
+"""Catalan name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'abreviatura': 'abbreviation',
+ 'abreviació': 'abbreviation',
+ 'abrev': 'abbreviation',
+ 'ab': 'abbreviation',
+ 'acrònim': 'acronym',
+ 'ac': 'acronym',
+ 'codi': 'code',
+ 'èmfasi': 'emphasis',
+ 'literal': 'literal',
+ 'matemàtiques': 'math',
+ 'referència-a-pep': 'pep-reference',
+ 'referència-pep': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'referència-a-rfc': 'rfc-reference',
+ 'referència-rfc': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'destacat': 'strong',
+ 'subíndex': 'subscript',
+ 'sub': 'subscript',
+ 'superíndex': 'superscript',
+ 'sup': 'superscript',
+ 'referència-a-títol': 'title-reference',
+ 'referència-títol': 'title-reference',
+ 'títol': 'title-reference',
+ 't': 'title-reference',
+ 'cru': 'raw',
+ # the following roles are not implemented in Docutils
+ 'índex': 'index',
+ 'i': 'index',
+ 'referència-anònima': 'anonymous-reference',
+ 'referència-a-cita': 'citation-reference',
+ 'referència-cita': 'citation-reference',
+ 'referència-a-nota-al-peu': 'footnote-reference',
+ 'referència-nota-al-peu': 'footnote-reference',
+ 'referència-amb-nom': 'named-reference',
+ 'referència-nom': 'named-reference',
+ 'referència-a-substitució': 'substitution-reference',
+ 'referència-substitució': 'substitution-reference',
+ 'referència-a-uri': 'uri-reference',
+ 'referència-uri': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'destinació': 'target',
+ }
+"""Mapping of Catalan role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/cs.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/cs.py
new file mode 100644
index 00000000..70274d28
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/cs.py
@@ -0,0 +1,110 @@
+# $Id: cs.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Marek Blaha <mb@dat.cz>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Czech-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'pozor': 'attention',
+ # jak rozlisit caution a warning?
+ 'caution (translation required)': 'caution',
+ 'code (translation required)': 'code',
+ 'nebezpečí': 'danger',
+ 'chyba': 'error',
+ 'rada': 'hint',
+ 'důležité': 'important',
+ 'poznámka': 'note',
+ 'tip (translation required)': 'tip',
+ 'varování': 'warning',
+ 'admonition (translation required)': 'admonition',
+ 'sidebar (translation required)': 'sidebar',
+ 'téma': 'topic',
+ 'line-block (translation required)': 'line-block',
+ 'parsed-literal (translation required)': 'parsed-literal',
+ 'oddíl': 'rubric',
+ 'moto': 'epigraph',
+ 'highlights (translation required)': 'highlights',
+ 'pull-quote (translation required)': 'pull-quote',
+ 'compound (translation required)': 'compound',
+ 'container (translation required)': 'container',
+ # 'questions': 'questions',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'table (translation required)': 'table',
+ 'csv-table (translation required)': 'csv-table',
+ 'list-table (translation required)': 'list-table',
+ 'math (translation required)': 'math',
+ 'meta (translation required)': 'meta',
+ # 'imagemap': 'imagemap',
+ 'image (translation required)': 'image', # obrazek
+ 'figure (translation required)': 'figure', # a tady?
+ 'include (translation required)': 'include',
+ 'raw (translation required)': 'raw',
+ 'replace (translation required)': 'replace',
+ 'unicode (translation required)': 'unicode',
+ 'datum': 'date',
+ 'třída': 'class',
+ 'role (translation required)': 'role',
+ 'default-role (translation required)': 'default-role',
+ 'title (translation required)': 'title',
+ 'obsah': 'contents',
+ 'sectnum (translation required)': 'sectnum',
+ 'section-numbering (translation required)': 'sectnum',
+ 'header (translation required)': 'header',
+ 'footer (translation required)': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ 'target-notes (translation required)': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Czech name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'abbreviation (translation required)': 'abbreviation',
+ 'ab (translation required)': 'abbreviation',
+ 'acronym (translation required)': 'acronym',
+ 'ac (translation required)': 'acronym',
+ 'code (translation required)': 'code',
+ 'index (translation required)': 'index',
+ 'i (translation required)': 'index',
+ 'subscript (translation required)': 'subscript',
+ 'sub (translation required)': 'subscript',
+ 'superscript (translation required)': 'superscript',
+ 'sup (translation required)': 'superscript',
+ 'title-reference (translation required)': 'title-reference',
+ 'title (translation required)': 'title-reference',
+ 't (translation required)': 'title-reference',
+ 'pep-reference (translation required)': 'pep-reference',
+ 'pep (translation required)': 'pep-reference',
+ 'rfc-reference (translation required)': 'rfc-reference',
+ 'rfc (translation required)': 'rfc-reference',
+ 'emphasis (translation required)': 'emphasis',
+ 'strong (translation required)': 'strong',
+ 'literal (translation required)': 'literal',
+ 'math (translation required)': 'math',
+ 'named-reference (translation required)': 'named-reference',
+ 'anonymous-reference (translation required)': 'anonymous-reference',
+ 'footnote-reference (translation required)': 'footnote-reference',
+ 'citation-reference (translation required)': 'citation-reference',
+ 'substitution-reference (translation required)': 'substitution-reference',
+ 'target (translation required)': 'target',
+ 'uri-reference (translation required)': 'uri-reference',
+ 'uri (translation required)': 'uri-reference',
+ 'url (translation required)': 'uri-reference',
+ 'raw (translation required)': 'raw',
+ }
+"""Mapping of Czech role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/da.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/da.py
new file mode 100644
index 00000000..31a250e1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/da.py
@@ -0,0 +1,114 @@
+# $Id: da.py 9417 2023-06-27 20:04:54Z milde $
+# Author: E D
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Danish-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'giv agt': 'attention',
+ 'pas på': 'caution',
+ 'kode': 'code',
+ 'kode-blok': 'code',
+ 'kildekode': 'code',
+ 'fare': 'danger',
+ 'fejl': 'error',
+ 'vink': 'hint',
+ 'vigtigt': 'important',
+ 'bemærk': 'note',
+ 'tips': 'tip',
+ 'advarsel': 'warning',
+ 'varsel': 'admonition',
+ 'formaning': 'admonition', # sic! kept for backwards compatibiltity
+ 'sidebjælke': 'sidebar',
+ 'emne': 'topic',
+ 'linje-blok': 'line-block',
+ 'linie-blok': 'line-block',
+ 'parset-literal': 'parsed-literal',
+ 'rubrik': 'rubric',
+ 'epigraf': 'epigraph',
+ 'fremhævninger': 'highlights',
+ 'pull-quote (translation required)': 'pull-quote',
+ 'compound (translation required)': 'compound',
+ 'container (translation required)': 'container',
+ # 'questions': 'questions',
+ 'tabel': 'table',
+ 'csv-tabel': 'csv-table',
+ 'liste-tabel': 'list-table',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'meta': 'meta',
+ 'math (translation required)': 'math',
+ # 'imagemap': 'imagemap',
+ 'billede': 'image',
+ 'figur': 'figure',
+ 'inkludér': 'include',
+ 'inkluder': 'include',
+ 'rå': 'raw',
+ 'erstat': 'replace',
+ 'unicode': 'unicode',
+ 'dato': 'date',
+ 'klasse': 'class',
+ 'rolle': 'role',
+ 'forvalgt-rolle': 'default-role',
+ 'titel': 'title',
+ 'indhold': 'contents',
+ 'sektnum': 'sectnum',
+ 'sektions-nummerering': 'sectnum',
+ 'sidehovede': 'header',
+ 'sidefod': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ 'target-notes (translation required)': 'target-notes',
+ 'restructuredtext-test-direktiv': 'restructuredtext-test-directive'}
+"""Danish name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'forkortelse': 'abbreviation',
+ 'fork': 'abbreviation',
+ 'akronym': 'acronym',
+ 'ac (translation required)': 'acronym',
+ 'kode': 'code',
+ 'indeks': 'index',
+ 'i': 'index',
+ 'subscript (translation required)': 'subscript',
+ 'sub (translation required)': 'subscript',
+ 'superscript (translation required)': 'superscript',
+ 'sup (translation required)': 'superscript',
+ 'titel-reference': 'title-reference',
+ 'titel': 'title-reference',
+ 't': 'title-reference',
+ 'pep-reference': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'rfc-reference': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'emfase': 'emphasis',
+ 'kraftig': 'strong',
+ 'literal': 'literal',
+ 'math (translation required)': 'math',
+ 'navngivet-reference': 'named-reference',
+ 'anonym-reference': 'anonymous-reference',
+ 'fodnote-reference': 'footnote-reference',
+ 'citation-reference (translation required)': 'citation-reference',
+ 'substitutions-reference': 'substitution-reference',
+ 'target (translation required)': 'target',
+ 'uri-reference': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'rå': 'raw',
+ }
+"""Mapping of Danish role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/de.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/de.py
new file mode 100644
index 00000000..4b7ef8e0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/de.py
@@ -0,0 +1,107 @@
+# $Id: de.py 9428 2023-07-07 06:50:26Z milde $
+# Authors: Engelbert Gruber <grubert@users.sourceforge.net>;
+# Lea Wiemann <LeWiemann@gmail.com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+German-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ 'warnhinweis': 'admonition', # or, more generally, 'anmerkung'?
+ 'ermahnung': 'admonition', # sic! kept for backwards compatibiltity
+ 'achtung': 'attention',
+ 'vorsicht': 'caution',
+ 'code': 'code',
+ 'gefahr': 'danger',
+ 'fehler': 'error',
+ 'hinweis': 'hint', # Wink
+ 'wichtig': 'important',
+ 'notiz': 'note',
+ 'tipp': 'tip',
+ 'warnung': 'warning',
+ 'kasten': 'sidebar',
+ 'seitenkasten': 'sidebar', # kept for backwards compatibiltity
+ 'seitenleiste': 'sidebar',
+ 'thema': 'topic',
+ 'zeilenblock': 'line-block',
+ 'parsed-literal (translation required)': 'parsed-literal',
+ 'rubrik': 'rubric',
+ 'epigraph': 'epigraph',
+ 'highlights': 'highlights',
+ 'pull-quote': 'pull-quote', # commonly used in German too
+ 'seitenansprache': 'pull-quote',
+ # cf. http://www.typografie.info/2/wiki.php?title=Seitenansprache
+ 'zusammengesetzt': 'compound',
+ 'verbund': 'compound',
+ 'container': 'container',
+ # 'fragen': 'questions',
+ 'tabelle': 'table',
+ 'csv-tabelle': 'csv-table',
+ 'listentabelle': 'list-table',
+ 'mathe': 'math',
+ 'formel': 'math',
+ 'meta': 'meta',
+ # 'imagemap': 'imagemap',
+ 'bild': 'image',
+ 'abbildung': 'figure',
+ 'unverändert': 'raw',
+ 'roh': 'raw',
+ 'einfügen': 'include',
+ 'ersetzung': 'replace',
+ 'ersetzen': 'replace',
+ 'ersetze': 'replace',
+ 'unicode': 'unicode',
+ 'datum': 'date',
+ 'klasse': 'class',
+ 'rolle': 'role',
+ 'standardrolle': 'default-role',
+ 'titel': 'title',
+ 'inhalt': 'contents',
+ 'kapitelnummerierung': 'sectnum',
+ 'abschnittsnummerierung': 'sectnum',
+ 'linkziel-fußnoten': 'target-notes',
+ 'kopfzeilen': 'header',
+ 'fußzeilen': 'footer',
+ # 'fußnoten': 'footnotes',
+ # 'zitate': 'citations',
+ }
+"""German name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ 'abkürzung': 'abbreviation',
+ 'akronym': 'acronym',
+ 'code': 'code',
+ 'index': 'index',
+ 'tiefgestellt': 'subscript',
+ 'hochgestellt': 'superscript',
+ 'titel-referenz': 'title-reference',
+ 'pep-referenz': 'pep-reference',
+ 'rfc-referenz': 'rfc-reference',
+ 'betonung': 'emphasis', # for backwards compatibility
+ 'betont': 'emphasis',
+ 'fett': 'strong',
+ 'wörtlich': 'literal',
+ 'mathe': 'math',
+ 'benannte-referenz': 'named-reference',
+ 'unbenannte-referenz': 'anonymous-reference',
+ 'fußnoten-referenz': 'footnote-reference',
+ 'zitat-referenz': 'citation-reference',
+ 'ersetzungs-referenz': 'substitution-reference',
+ 'ziel': 'target',
+ 'uri-referenz': 'uri-reference',
+ 'unverändert': 'raw',
+ 'roh': 'raw',
+ }
+"""Mapping of German role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/en.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/en.py
new file mode 100644
index 00000000..7b9319e6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/en.py
@@ -0,0 +1,114 @@
+# $Id: en.py 9417 2023-06-27 20:04:54Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+English-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'attention': 'attention',
+ 'caution': 'caution',
+ 'danger': 'danger',
+ 'error': 'error',
+ 'hint': 'hint',
+ 'important': 'important',
+ 'note': 'note',
+ 'tip': 'tip',
+ 'warning': 'warning',
+ 'admonition': 'admonition', # advice/advisory/remark, not reprimand
+ 'sidebar': 'sidebar',
+ 'topic': 'topic',
+ 'line-block': 'line-block',
+ 'parsed-literal': 'parsed-literal',
+ 'code': 'code',
+ 'code-block': 'code',
+ 'sourcecode': 'code',
+ 'math': 'math',
+ 'rubric': 'rubric',
+ 'epigraph': 'epigraph',
+ 'highlights': 'highlights',
+ 'pull-quote': 'pull-quote',
+ 'compound': 'compound',
+ 'container': 'container',
+ 'table': 'table',
+ 'csv-table': 'csv-table',
+ 'list-table': 'list-table',
+ 'meta': 'meta',
+ # 'imagemap': 'imagemap',
+ 'image': 'image',
+ 'figure': 'figure',
+ 'include': 'include',
+ 'raw': 'raw',
+ 'replace': 'replace',
+ 'unicode': 'unicode',
+ 'date': 'date',
+ 'class': 'class',
+ 'role': 'role',
+ 'default-role': 'default-role',
+ 'title': 'title',
+ 'contents': 'contents',
+ 'sectnum': 'sectnum',
+ 'section-numbering': 'sectnum',
+ 'header': 'header',
+ 'footer': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ 'target-notes': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Mapping of English directive name to registered directive names
+
+Cf. https://docutils.sourceforge.io/docs/ref/rst/directives.html
+and `_directive_registry` in ``directives/__init__.py``.
+"""
+
+roles = {
+ # language-dependent: fixed
+ 'abbreviation': 'abbreviation',
+ 'ab': 'abbreviation',
+ 'acronym': 'acronym',
+ 'ac': 'acronym',
+ 'code': 'code',
+ 'emphasis': 'emphasis',
+ 'literal': 'literal',
+ 'math': 'math',
+ 'pep-reference': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'rfc-reference': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'strong': 'strong',
+ 'subscript': 'subscript',
+ 'sub': 'subscript',
+ 'superscript': 'superscript',
+ 'sup': 'superscript',
+ 'title-reference': 'title-reference',
+ 'title': 'title-reference',
+ 't': 'title-reference',
+ 'raw': 'raw',
+ # the following roles are not implemented in Docutils
+ 'index': 'index',
+ 'i': 'index',
+ 'anonymous-reference': 'anonymous-reference',
+ 'citation-reference': 'citation-reference',
+ 'footnote-reference': 'footnote-reference',
+ 'named-reference': 'named-reference',
+ 'substitution-reference': 'substitution-reference',
+ 'uri-reference': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'target': 'target',
+ }
+"""Mapping of English role names to canonical role names for interpreted text.
+
+Cf. https://docutils.sourceforge.io/docs/ref/rst/roles.html
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/eo.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/eo.py
new file mode 100644
index 00000000..1db6694f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/eo.py
@@ -0,0 +1,119 @@
+# $Id: eo.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Marcelo Huerta San Martin <richieadler@users.sourceforge.net>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Esperanto-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'atentu': 'attention',
+ 'zorgu': 'caution',
+ 'code (translation required)': 'code',
+ 'dangxero': 'danger',
+ 'danĝero': 'danger',
+ 'eraro': 'error',
+ 'spuro': 'hint',
+ 'grava': 'important',
+ 'noto': 'note',
+ 'helpeto': 'tip',
+ 'averto': 'warning',
+ 'sciigo': 'admonition',
+ 'admono': 'admonition', # sic! kept for backwards compatibiltity
+ 'flankteksto': 'sidebar',
+ 'temo': 'topic',
+ 'linea-bloko': 'line-block',
+ 'analizota-literalo': 'parsed-literal',
+ 'rubriko': 'rubric',
+ 'epigrafo': 'epigraph',
+ 'elstarajxoj': 'highlights',
+ 'elstaraĵoj': 'highlights',
+ 'ekstera-citajxo': 'pull-quote',
+ 'ekstera-citaĵo': 'pull-quote',
+ 'kombinajxo': 'compound',
+ 'kombinaĵo': 'compound',
+ 'tekstingo': 'container',
+ 'enhavilo': 'container',
+ # 'questions': 'questions',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'tabelo': 'table',
+ 'tabelo-vdk': 'csv-table', # "valoroj disigitaj per komoj"
+ 'tabelo-csv': 'csv-table',
+ 'tabelo-lista': 'list-table',
+ 'meta': 'meta',
+ 'math (translation required)': 'math',
+ # 'imagemap': 'imagemap',
+ 'bildo': 'image',
+ 'figuro': 'figure',
+ 'inkludi': 'include',
+ 'senanaliza': 'raw',
+ 'anstatauxi': 'replace',
+ 'anstataŭi': 'replace',
+ 'unicode': 'unicode',
+ 'dato': 'date',
+ 'klaso': 'class',
+ 'rolo': 'role',
+ 'preterlasita-rolo': 'default-role',
+ 'titolo': 'title',
+ 'enhavo': 'contents',
+ 'seknum': 'sectnum',
+ 'sekcia-numerado': 'sectnum',
+ 'kapsekcio': 'header',
+ 'piedsekcio': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ 'celaj-notoj': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Esperanto name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'mallongigo': 'abbreviation',
+ 'mall': 'abbreviation',
+ 'komenclitero': 'acronym',
+ 'kl': 'acronym',
+ 'code (translation required)': 'code',
+ 'indekso': 'index',
+ 'i': 'index',
+ 'subskribo': 'subscript',
+ 'sub': 'subscript',
+ 'supraskribo': 'superscript',
+ 'sup': 'superscript',
+ 'titola-referenco': 'title-reference',
+ 'titolo': 'title-reference',
+ 't': 'title-reference',
+ 'pep-referenco': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'rfc-referenco': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'emfazo': 'emphasis',
+ 'forta': 'strong',
+ 'litera': 'literal',
+ 'math (translation required)': 'math',
+ 'nomita-referenco': 'named-reference',
+ 'nenomita-referenco': 'anonymous-reference',
+ 'piednota-referenco': 'footnote-reference',
+ 'citajxo-referenco': 'citation-reference',
+ 'citaĵo-referenco': 'citation-reference',
+ 'anstatauxa-referenco': 'substitution-reference',
+ 'anstataŭa-referenco': 'substitution-reference',
+ 'celo': 'target',
+ 'uri-referenco': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'senanaliza': 'raw',
+}
+"""Mapping of Esperanto role names to canonical names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/es.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/es.py
new file mode 100644
index 00000000..b7dda6c9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/es.py
@@ -0,0 +1,123 @@
+# $Id: es.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Marcelo Huerta San Martín <richieadler@users.sourceforge.net>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Spanish-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+directives = {
+ 'atención': 'attention',
+ 'atencion': 'attention',
+ 'precaución': 'caution',
+ 'code (translation required)': 'code',
+ 'precaucion': 'caution',
+ 'peligro': 'danger',
+ 'error': 'error',
+ 'sugerencia': 'hint',
+ 'importante': 'important',
+ 'nota': 'note',
+ 'consejo': 'tip',
+ 'advertencia': 'warning',
+ 'aviso': 'admonition',
+ 'exhortacion': 'admonition', # sic! kept for backwards compatibiltity
+ 'exhortación': 'admonition', # sic! kept for backwards compatibiltity
+ 'nota-al-margen': 'sidebar',
+ 'tema': 'topic',
+ 'bloque-de-lineas': 'line-block',
+ 'bloque-de-líneas': 'line-block',
+ 'literal-evaluado': 'parsed-literal',
+ 'firma': 'rubric',
+ 'epígrafe': 'epigraph',
+ 'epigrafe': 'epigraph',
+ 'destacado': 'highlights',
+ 'cita-destacada': 'pull-quote',
+ 'combinacion': 'compound',
+ 'combinación': 'compound',
+ 'contenedor': 'container',
+ # 'questions': 'questions',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'tabla': 'table',
+ 'tabla-vsc': 'csv-table',
+ 'tabla-csv': 'csv-table',
+ 'tabla-lista': 'list-table',
+ 'meta': 'meta',
+ 'math (translation required)': 'math',
+ # 'imagemap': 'imagemap',
+ 'imagen': 'image',
+ 'figura': 'figure',
+ 'incluir': 'include',
+ 'sin-analisis': 'raw',
+ 'sin-análisis': 'raw',
+ 'reemplazar': 'replace',
+ 'unicode': 'unicode',
+ 'fecha': 'date',
+ 'clase': 'class',
+ 'rol': 'role',
+ 'rol-por-omision': 'default-role',
+ 'rol-por-omisión': 'default-role',
+ 'titulo': 'title',
+ 'título': 'title',
+ 'contenido': 'contents',
+ 'numseccion': 'sectnum',
+ 'numsección': 'sectnum',
+ 'numeracion-seccion': 'sectnum',
+ 'numeración-sección': 'sectnum',
+ 'notas-destino': 'target-notes',
+ 'cabecera': 'header',
+ 'pie': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Spanish name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ 'abreviatura': 'abbreviation',
+ 'ab': 'abbreviation',
+ 'acronimo': 'acronym',
+ 'ac': 'acronym',
+ 'code (translation required)': 'code',
+ 'indice': 'index',
+ 'i': 'index',
+ 'subindice': 'subscript',
+ 'subíndice': 'subscript',
+ 'superindice': 'superscript',
+ 'superíndice': 'superscript',
+ 'referencia-titulo': 'title-reference',
+ 'titulo': 'title-reference',
+ 't': 'title-reference',
+ 'referencia-pep': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'referencia-rfc': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'enfasis': 'emphasis',
+ 'énfasis': 'emphasis',
+ 'destacado': 'strong',
+ 'literal': 'literal', # "literal" is also a word in Spanish :-)
+ 'math (translation required)': 'math',
+ 'referencia-con-nombre': 'named-reference',
+ 'referencia-anonima': 'anonymous-reference',
+ 'referencia-anónima': 'anonymous-reference',
+ 'referencia-nota-al-pie': 'footnote-reference',
+ 'referencia-cita': 'citation-reference',
+ 'referencia-sustitucion': 'substitution-reference',
+ 'referencia-sustitución': 'substitution-reference',
+ 'destino': 'target',
+ 'referencia-uri': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'sin-analisis': 'raw',
+ 'sin-análisis': 'raw',
+}
+"""Mapping of Spanish role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fa.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fa.py
new file mode 100644
index 00000000..420a315e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fa.py
@@ -0,0 +1,102 @@
+# $Id: fa.py 4564 2016-08-10 11:48:42Z
+# Author: Shahin <me@5hah.in>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Persian-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'توجه': 'attention',
+ 'احتیاط': 'caution',
+ 'کد': 'code',
+ 'بلوک-کد': 'code',
+ 'کد-منبع': 'code',
+ 'خطر': 'danger',
+ 'خطا': 'error',
+ 'راهنما': 'hint',
+ 'مهم': 'important',
+ 'یادداشت': 'note',
+ 'نکته': 'tip',
+ 'اخطار': 'warning',
+ 'تذکر': 'admonition',
+ 'نوار-کناری': 'sidebar',
+ 'موضوع': 'topic',
+ 'بلوک-خط': 'line-block',
+ 'تلفظ-پردازش-شده': 'parsed-literal',
+ 'سر-فصل': 'rubric',
+ 'کتیبه': 'epigraph',
+ 'نکات-برجسته': 'highlights',
+ 'نقل-قول': 'pull-quote',
+ 'ترکیب': 'compound',
+ 'ظرف': 'container',
+ # 'questions': 'questions',
+ 'جدول': 'table',
+ 'جدول-csv': 'csv-table',
+ 'جدول-لیست': 'list-table',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'متا': 'meta',
+ 'ریاضی': 'math',
+ # 'imagemap': 'imagemap',
+ 'تصویر': 'image',
+ 'شکل': 'figure',
+ 'شامل': 'include',
+ 'خام': 'raw',
+ 'جایگزین': 'replace',
+ 'یونیکد': 'unicode',
+ 'تاریخ': 'date',
+ 'کلاس': 'class',
+ 'قانون': 'role',
+ 'قانون-پیش‌فرض': 'default-role',
+ 'عنوان': 'title',
+ 'محتوا': 'contents',
+ 'شماره-فصل': 'sectnum',
+ 'شماره‌گذاری-فصل': 'sectnum',
+ 'سرآیند': 'header',
+ 'پاصفحه': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ 'یادداشت-هدف': 'target-notes',
+}
+"""Persian name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'مخفف': 'abbreviation',
+ 'سرنام': 'acronym',
+ 'کد': 'code',
+ 'شاخص': 'index',
+ 'زیرنویس': 'subscript',
+ 'بالانویس': 'superscript',
+ 'عنوان': 'title-reference',
+ 'نیرو': 'pep-reference',
+ 'rfc-reference (translation required)': 'rfc-reference',
+ 'تاکید': 'emphasis',
+ 'قوی': 'strong',
+ 'لفظی': 'literal',
+ 'ریاضی': 'math',
+ 'منبع-نام‌گذاری': 'named-reference',
+ 'منبع-ناشناس': 'anonymous-reference',
+ 'منبع-پانویس': 'footnote-reference',
+ 'منبع-نقل‌فول': 'citation-reference',
+ 'منبع-جایگزینی': 'substitution-reference',
+ 'هدف': 'target',
+ 'منبع-uri': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'خام': 'raw',
+}
+"""Mapping of Persian role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fi.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fi.py
new file mode 100644
index 00000000..88653ac5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fi.py
@@ -0,0 +1,98 @@
+# $Id: fi.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Asko Soukka <asko.soukka@iki.fi>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Finnish-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'huomio': 'attention',
+ 'varo': 'caution',
+ 'code (translation required)': 'code',
+ 'vaara': 'danger',
+ 'virhe': 'error',
+ 'vihje': 'hint',
+ 'tärkeää': 'important',
+ 'huomautus': 'note',
+ 'neuvo': 'tip',
+ 'varoitus': 'warning',
+ 'kehotus': 'admonition', # sic! advice/advisory/remark, not reprimand
+ 'sivupalkki': 'sidebar',
+ 'aihe': 'topic',
+ 'rivi': 'line-block',
+ 'tasalevyinen': 'parsed-literal',
+ 'ohje': 'rubric',
+ 'epigraafi': 'epigraph',
+ 'kohokohdat': 'highlights',
+ 'lainaus': 'pull-quote',
+ 'taulukko': 'table',
+ 'csv-taulukko': 'csv-table',
+ 'list-table (translation required)': 'list-table',
+ 'compound (translation required)': 'compound',
+ 'container (translation required)': 'container',
+ # 'kysymykset': 'questions',
+ 'meta': 'meta',
+ 'math (translation required)': 'math',
+ # 'kuvakartta': 'imagemap',
+ 'kuva': 'image',
+ 'kaavio': 'figure',
+ 'sisällytä': 'include',
+ 'raaka': 'raw',
+ 'korvaa': 'replace',
+ 'unicode': 'unicode',
+ 'päiväys': 'date',
+ 'luokka': 'class',
+ 'rooli': 'role',
+ 'default-role (translation required)': 'default-role',
+ 'title (translation required)': 'title',
+ 'sisällys': 'contents',
+ 'kappale': 'sectnum',
+ 'header (translation required)': 'header',
+ 'footer (translation required)': 'footer',
+ # 'alaviitteet': 'footnotes',
+ # 'viitaukset': 'citations',
+ 'target-notes (translation required)': 'target-notes'}
+"""Finnish name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'lyhennys': 'abbreviation',
+ 'akronyymi': 'acronym',
+ 'kirjainsana': 'acronym',
+ 'code (translation required)': 'code',
+ 'hakemisto': 'index',
+ 'luettelo': 'index',
+ 'alaindeksi': 'subscript',
+ 'indeksi': 'subscript',
+ 'yläindeksi': 'superscript',
+ 'title-reference (translation required)': 'title-reference',
+ 'title (translation required)': 'title-reference',
+ 'pep-reference (translation required)': 'pep-reference',
+ 'rfc-reference (translation required)': 'rfc-reference',
+ 'korostus': 'emphasis',
+ 'vahvistus': 'strong',
+ 'tasalevyinen': 'literal',
+ 'math (translation required)': 'math',
+ 'named-reference (translation required)': 'named-reference',
+ 'anonymous-reference (translation required)': 'anonymous-reference',
+ 'footnote-reference (translation required)': 'footnote-reference',
+ 'citation-reference (translation required)': 'citation-reference',
+ 'substitution-reference (translation required)': 'substitution-reference',
+ 'kohde': 'target',
+ 'uri-reference (translation required)': 'uri-reference',
+ 'raw (translation required)': 'raw',
+ }
+"""Mapping of Finnish role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fr.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fr.py
new file mode 100644
index 00000000..cd4ca9db
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/fr.py
@@ -0,0 +1,108 @@
+# $Id: fr.py 9417 2023-06-27 20:04:54Z milde $
+# Authors: David Goodger <goodger@python.org>; William Dode
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+French-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ 'attention': 'attention',
+ 'précaution': 'caution',
+ 'danger': 'danger',
+ 'erreur': 'error',
+ 'conseil': 'hint',
+ 'important': 'important',
+ 'note': 'note',
+ 'astuce': 'tip',
+ 'avertissement': 'warning',
+ 'annonce': 'admonition',
+ 'admonition': 'admonition', # sic! kept for backwards compatibiltity
+ # suggestions: annonce, avis, indication, remarque, renseignement
+ # see also https://sourceforge.net/p/docutils/bugs/453/
+ 'encadré': 'sidebar',
+ 'sujet': 'topic',
+ 'bloc-textuel': 'line-block',
+ 'bloc-interprété': 'parsed-literal',
+ 'code-interprété': 'parsed-literal',
+ 'code': 'code',
+ 'math (translation required)': 'math',
+ 'intertitre': 'rubric',
+ 'exergue': 'epigraph',
+ 'épigraphe': 'epigraph',
+ 'chapeau': 'highlights',
+ 'accroche': 'pull-quote',
+ 'compound (translation required)': 'compound',
+ 'container (translation required)': 'container',
+ 'tableau': 'table',
+ 'csv-table (translation required)': 'csv-table',
+ 'list-table (translation required)': 'list-table',
+ 'méta': 'meta',
+ # 'imagemap (translation required)': 'imagemap',
+ 'image': 'image',
+ 'figure': 'figure',
+ 'inclure': 'include',
+ 'brut': 'raw',
+ 'remplacer': 'replace',
+ 'remplace': 'replace',
+ 'unicode': 'unicode',
+ 'date': 'date',
+ 'classe': 'class',
+ 'role (translation required)': 'role',
+ 'default-role (translation required)': 'default-role',
+ 'titre (translation required)': 'title',
+ 'sommaire': 'contents',
+ 'table-des-matières': 'contents',
+ 'sectnum': 'sectnum',
+ 'section-numérotée': 'sectnum',
+ 'liens': 'target-notes',
+ 'header (translation required)': 'header',
+ 'footer (translation required)': 'footer',
+ # 'footnotes (translation required)': 'footnotes',
+ # 'citations (translation required)': 'citations',
+ }
+"""Mapping of French directive names to registered directive names
+
+Cf. https://docutils.sourceforge.io/docs/ref/rst/directives.html
+and `_directive_registry` in ``directives/__init__.py``.
+"""
+
+roles = {
+ 'abréviation': 'abbreviation',
+ 'acronyme': 'acronym',
+ 'sigle': 'acronym',
+ 'code': 'code',
+ 'emphase': 'emphasis',
+ 'littéral': 'literal',
+ 'math (translation required)': 'math',
+ 'pep-référence': 'pep-reference',
+ 'rfc-référence': 'rfc-reference',
+ 'fort': 'strong',
+ 'indice': 'subscript',
+ 'ind': 'subscript',
+ 'exposant': 'superscript',
+ 'exp': 'superscript',
+ 'titre-référence': 'title-reference',
+ 'titre': 'title-reference',
+ 'brut': 'raw',
+ # the following roles are not implemented in Docutils
+ 'index': 'index',
+ 'nommée-référence': 'named-reference',
+ 'anonyme-référence': 'anonymous-reference',
+ 'note-référence': 'footnote-reference',
+ 'citation-référence': 'citation-reference',
+ 'substitution-référence': 'substitution-reference',
+ 'lien': 'target',
+ 'uri-référence': 'uri-reference',
+ }
+"""Mapping of French role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/gl.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/gl.py
new file mode 100644
index 00000000..837c3f1f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/gl.py
@@ -0,0 +1,106 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision: 4229 $
+# Date: $Date: 2005-12-23 00:46:16 +0100 (Fri, 23 Dec 2005) $
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Galician-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'atención': 'attention',
+ 'advertencia': 'caution',
+ 'code (translation required)': 'code',
+ 'perigo': 'danger',
+ 'erro': 'error',
+ 'pista': 'hint',
+ 'importante': 'important',
+ 'nota': 'note',
+ 'consello': 'tip',
+ 'aviso': 'warning',
+ 'admonición': 'admonition', # sic! advice/advisory/remark, not reprimand
+ 'barra lateral': 'sidebar',
+ 'tópico': 'topic',
+ 'bloque-liña': 'line-block',
+ 'literal-analizado': 'parsed-literal',
+ 'rúbrica': 'rubric',
+ 'epígrafe': 'epigraph',
+ 'realzados': 'highlights',
+ 'coller-citación': 'pull-quote',
+ 'compor': 'compound',
+ 'recipiente': 'container',
+ 'táboa': 'table',
+ 'táboa-csv': 'csv-table',
+ 'táboa-listaxe': 'list-table',
+ 'meta': 'meta',
+ 'math (translation required)': 'math',
+ 'imaxe': 'image',
+ 'figura': 'figure',
+ 'incluír': 'include',
+ 'cru': 'raw',
+ 'substituír': 'replace',
+ 'unicode': 'unicode',
+ 'data': 'date',
+ 'clase': 'class',
+ 'regra': 'role',
+ 'regra-predeterminada': 'default-role',
+ 'título': 'title',
+ 'contido': 'contents',
+ 'seccnum': 'sectnum',
+ 'sección-numerar': 'sectnum',
+ 'cabeceira': 'header',
+ 'pé de páxina': 'footer',
+ 'notas-destino': 'target-notes',
+ 'texto restruturado-proba-directiva': 'restructuredtext-test-directive',
+ }
+"""Galician name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'abreviatura': 'abbreviation',
+ 'ab': 'abbreviation',
+ 'acrónimo': 'acronym',
+ 'ac': 'acronym',
+ 'code (translation required)': 'code',
+ 'índice': 'index',
+ 'i': 'index',
+ 'subíndice': 'subscript',
+ 'sub': 'subscript',
+ 'superíndice': 'superscript',
+ 'sup': 'superscript',
+ 'referencia título': 'title-reference',
+ 'título': 'title-reference',
+ 't': 'title-reference',
+ 'referencia-pep': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'referencia-rfc': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'énfase': 'emphasis',
+ 'forte': 'strong',
+ 'literal': 'literal',
+ 'math (translation required)': 'math',
+ 'referencia-nome': 'named-reference',
+ 'referencia-anónimo': 'anonymous-reference',
+ 'referencia-nota ao pé': 'footnote-reference',
+ 'referencia-citación': 'citation-reference',
+ 'referencia-substitución': 'substitution-reference',
+ 'destino': 'target',
+ 'referencia-uri': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'cru': 'raw',
+ }
+"""Mapping of Galician role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/he.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/he.py
new file mode 100644
index 00000000..7a5f3bae
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/he.py
@@ -0,0 +1,110 @@
+# Author: Meir Kriheli
+# Id: $Id: he.py 9452 2023-09-27 00:11:54Z milde $
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+English-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'תשומת לב': 'attention',
+ 'זהירות': 'caution',
+ 'code (translation required)': 'code',
+ 'סכנה': 'danger',
+ 'שגיאה': 'error',
+ 'רמז': 'hint',
+ 'חשוב': 'important',
+ 'הערה': 'note',
+ 'טיפ': 'tip',
+ 'אזהרה': 'warning',
+ 'admonition': 'admonition',
+ 'sidebar': 'sidebar',
+ 'topic': 'topic',
+ 'line-block': 'line-block',
+ 'parsed-literal': 'parsed-literal',
+ 'rubric': 'rubric',
+ 'epigraph': 'epigraph',
+ 'highlights': 'highlights',
+ 'pull-quote': 'pull-quote',
+ 'compound': 'compound',
+ 'container': 'container',
+ 'table': 'table',
+ 'csv-table': 'csv-table',
+ 'list-table': 'list-table',
+ 'meta': 'meta',
+ 'math (translation required)': 'math',
+ 'תמונה': 'image',
+ 'figure': 'figure',
+ 'include': 'include',
+ 'raw': 'raw',
+ 'replace': 'replace',
+ 'unicode': 'unicode',
+ 'date': 'date',
+ 'סגנון': 'class',
+ 'role': 'role',
+ 'default-role': 'default-role',
+ 'title': 'title',
+ 'תוכן': 'contents',
+ 'sectnum': 'sectnum',
+ 'section-numbering': 'sectnum',
+ 'header': 'header',
+ 'footer': 'footer',
+ 'target-notes': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive',
+ # 'questions': 'questions',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ # 'imagemap': 'imagemap',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ }
+"""English name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'abbreviation': 'abbreviation',
+ 'ab': 'abbreviation',
+ 'acronym': 'acronym',
+ 'ac': 'acronym',
+ 'code (translation required)': 'code',
+ 'index': 'index',
+ 'i': 'index',
+ 'תחתי': 'subscript',
+ 'sub': 'subscript',
+ 'עילי': 'superscript',
+ 'sup': 'superscript',
+ 'title-reference': 'title-reference',
+ 'title': 'title-reference',
+ 't': 'title-reference',
+ 'pep-reference': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'rfc-reference': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'emphasis': 'emphasis',
+ 'strong': 'strong',
+ 'literal': 'literal',
+ 'math (translation required)': 'math',
+ 'named-reference': 'named-reference',
+ 'anonymous-reference': 'anonymous-reference',
+ 'footnote-reference': 'footnote-reference',
+ 'citation-reference': 'citation-reference',
+ 'substitution-reference': 'substitution-reference',
+ 'target': 'target',
+ 'uri-reference': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'raw': 'raw',
+ }
+"""Mapping of English role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/it.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/it.py
new file mode 100644
index 00000000..ee5ba829
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/it.py
@@ -0,0 +1,99 @@
+# $Id: it.py 9417 2023-06-27 20:04:54Z milde $
+# Authors: Nicola Larosa <docutils@tekNico.net>;
+# Lele Gaifax <lele@seldati.it>
+# Copyright: This module has been placed in the public domain.
+
+# Beware: the italian translation of the reStructuredText documentation
+# at http://docit.bice.dyndns.org/static/ReST, in particular
+# http://docit.bice.dyndns.org/static/ReST/ref/rst/directives.html, needs
+# to be synced with the content of this file.
+
+"""
+Italian-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ 'attenzione': 'attention',
+ 'cautela': 'caution',
+ 'code (translation required)': 'code',
+ 'pericolo': 'danger',
+ 'errore': 'error',
+ 'suggerimento': 'hint',
+ 'importante': 'important',
+ 'nota': 'note',
+ 'consiglio': 'tip',
+ 'avvertenza': 'warning',
+ 'avviso': 'admonition',
+ 'ammonizione': 'admonition', # sic! kept for backards compatibility
+ 'riquadro': 'sidebar',
+ 'argomento': 'topic',
+ 'blocco-di-righe': 'line-block',
+ 'blocco-interpretato': 'parsed-literal',
+ 'rubrica': 'rubric',
+ 'epigrafe': 'epigraph',
+ 'punti-salienti': 'highlights',
+ 'estratto-evidenziato': 'pull-quote',
+ 'composito': 'compound',
+ 'container (translation required)': 'container',
+ # 'questions': 'questions',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'tabella': 'table',
+ 'tabella-csv': 'csv-table',
+ 'tabella-elenco': 'list-table',
+ 'meta': 'meta',
+ 'math (translation required)': 'math',
+ # 'imagemap': 'imagemap',
+ 'immagine': 'image',
+ 'figura': 'figure',
+ 'includi': 'include',
+ 'grezzo': 'raw',
+ 'sostituisci': 'replace',
+ 'unicode': 'unicode',
+ 'data': 'date',
+ 'classe': 'class',
+ 'ruolo': 'role',
+ 'ruolo-predefinito': 'default-role',
+ 'titolo': 'title',
+ 'indice': 'contents',
+ 'contenuti': 'contents',
+ 'seznum': 'sectnum',
+ 'sezioni-autonumerate': 'sectnum',
+ 'annota-riferimenti-esterni': 'target-notes',
+ 'intestazione': 'header',
+ 'piede-pagina': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Italian name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ 'abbreviazione': 'abbreviation',
+ 'acronimo': 'acronym',
+ 'code (translation required)': 'code',
+ 'indice': 'index',
+ 'deponente': 'subscript',
+ 'esponente': 'superscript',
+ 'riferimento-titolo': 'title-reference',
+ 'riferimento-pep': 'pep-reference',
+ 'riferimento-rfc': 'rfc-reference',
+ 'enfasi': 'emphasis',
+ 'forte': 'strong',
+ 'letterale': 'literal',
+ 'math (translation required)': 'math',
+ 'riferimento-con-nome': 'named-reference',
+ 'riferimento-anonimo': 'anonymous-reference',
+ 'riferimento-nota': 'footnote-reference',
+ 'riferimento-citazione': 'citation-reference',
+ 'riferimento-sostituzione': 'substitution-reference',
+ 'destinazione': 'target',
+ 'riferimento-uri': 'uri-reference',
+ 'grezzo': 'raw',
+ }
+"""Mapping of Italian role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ja.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ja.py
new file mode 100644
index 00000000..eef1549d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ja.py
@@ -0,0 +1,119 @@
+# $Id: ja.py 9030 2022-03-05 23:28:32Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Japanese-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+# Corrections to these translations are welcome!
+# 間違いがあれば、どうぞ正しい翻訳を教えて下さい。
+
+directives = {
+ # language-dependent: fixed
+ '注目': 'attention',
+ '注意': 'caution',
+ 'code (translation required)': 'code',
+ '危険': 'danger',
+ 'エラー': 'error',
+ 'ヒント': 'hint',
+ '重要': 'important',
+ '備考': 'note',
+ '通報': 'tip',
+ '警告': 'warning',
+ '戒告': 'admonition',
+ 'サイドバー': 'sidebar',
+ 'トピック': 'topic',
+ 'ラインブロック': 'line-block',
+ 'パーズドリテラル': 'parsed-literal',
+ 'ルブリック': 'rubric',
+ 'エピグラフ': 'epigraph',
+ '題言': 'epigraph',
+ 'ハイライト': 'highlights',
+ '見所': 'highlights',
+ 'プルクオート': 'pull-quote',
+ '合成': 'compound',
+ 'コンテナー': 'container',
+ '容器': 'container',
+ '表': 'table',
+ 'csv表': 'csv-table',
+ 'リスト表': 'list-table',
+ # '質問': 'questions',
+ # '問答': 'questions',
+ # 'faq': 'questions',
+ 'math (translation required)': 'math',
+ 'メタ': 'meta',
+ # 'イメージマプ': 'imagemap',
+ 'イメージ': 'image',
+ '画像': 'image',
+ 'フィグア': 'figure',
+ '図版': 'figure',
+ 'インクルード': 'include',
+ '含む': 'include',
+ '組み込み': 'include',
+ '生': 'raw',
+ '原': 'raw',
+ '換える': 'replace',
+ '取り換える': 'replace',
+ '掛け替える': 'replace',
+ 'ユニコード': 'unicode',
+ '日付': 'date',
+ 'クラス': 'class',
+ 'ロール': 'role',
+ '役': 'role',
+ 'ディフォルトロール': 'default-role',
+ '既定役': 'default-role',
+ 'タイトル': 'title',
+ '題': 'title', # 題名 件名
+ '目次': 'contents',
+ '節数': 'sectnum',
+ 'ヘッダ': 'header',
+ 'フッタ': 'footer',
+ # '脚注': 'footnotes', # 脚註?
+ # 'サイテーション': 'citations',   # 出典 引証 引用
+ 'ターゲットノート': 'target-notes', # 的注 的脚注
+ }
+"""Japanese name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ '略': 'abbreviation',
+ '頭字語': 'acronym',
+ 'code (translation required)': 'code',
+ 'インデックス': 'index',
+ '索引': 'index',
+ '添字': 'subscript',
+ '下付': 'subscript',
+ '下': 'subscript',
+ '上付': 'superscript',
+ '上': 'superscript',
+ '題参照': 'title-reference',
+ 'pep参照': 'pep-reference',
+ 'rfc参照': 'rfc-reference',
+ '強調': 'emphasis',
+ '強い': 'strong',
+ 'リテラル': 'literal',
+ '整形済み': 'literal',
+ 'math (translation required)': 'math',
+ '名付参照': 'named-reference',
+ '無名参照': 'anonymous-reference',
+ '脚注参照': 'footnote-reference',
+ '出典参照': 'citation-reference',
+ '代入参照': 'substitution-reference',
+ '的': 'target',
+ 'uri参照': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ '生': 'raw',
+ }
+"""Mapping of Japanese role names to canonical role names for interpreted
+text."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ka.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ka.py
new file mode 100644
index 00000000..e7f18ab7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ka.py
@@ -0,0 +1,90 @@
+# $Id: ka.py 9444 2023-08-23 12:02:41Z grubert $
+# Author: Temuri Doghonadze <temuri.doghonadze@gmail.com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Georgian-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+directives = {
+ 'ხაზების-ბლოკი': 'line-block',
+ 'მეტა': 'meta',
+ 'მათემატიკა': 'math',
+ 'დამუშავებული-ლიტერალი': 'parsed-literal',
+ 'გამოყოფილი-ციტატა': 'pull-quote',
+ 'კოდი': 'code',
+ 'შერეული': 'compound',
+ 'კონტეინერი': 'container',
+ 'ცხრილი': 'table',
+ 'csv-ცხრილი': 'csv-table',
+ 'ჩამონათვალი-ცხრილი': 'list-table',
+ 'დაუმუშავებელი': 'raw',
+ 'ჩანაცვლება': 'replace',
+ 'restructuredtext-ის-სატესტო-დირექტივა': 'restructuredtext-test-directive',
+ 'სამიზნე-შენიშვნები': 'target-notes',
+ 'უნიკოდი': 'unicode',
+ 'თარიღი': 'date',
+ 'გვერდითი-პანელი': 'sidebar',
+ 'მნიშვნელოვანი': 'important',
+ 'ჩასმა': 'include',
+ 'ყურადღება': 'attention',
+ 'გამოკვეთა': 'highlights',
+ 'შენიშვნა': 'admonition',
+ 'გამოსახულება': 'image',
+ 'კლასი': 'class',
+ 'როლი': 'role',
+ 'ნაგულისხმევი-როლი': 'default-role',
+ 'სათაური': 'title',
+ 'განყ-ნომერი': 'sectnum',
+ 'განყ-ნომერი': 'sectnum',
+ 'საფრთხე': 'danger',
+ 'ფრთხილად': 'caution',
+ 'შეცდომა': 'error',
+ 'მინიშნება': 'tip',
+ 'ყურადღებით': 'warning',
+ 'აღნიშვნა': 'note',
+ 'ფიგურა': 'figure',
+ 'რუბრიკა': 'rubric',
+ 'რჩევა': 'hint',
+ 'შემცველობა': 'contents',
+ 'თემა': 'topic',
+ 'ეპიგრაფი': 'epigraph',
+ 'თავსართი': 'header',
+ 'ქვედა კოლონტიტული': 'footer',
+ }
+"""Georgian name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ 'აკრონიმი': 'acronym',
+ 'კოდი': 'code',
+ 'ანონიმური-მიმართვა': 'anonymous-reference',
+ 'სიტყვასიტყვითი': 'literal',
+ 'მათემატიკა': 'math',
+ 'ზედა-ინდექსი': 'superscript',
+ 'მახვილი': 'emphasis',
+ 'სახელიანი-მიმართვა': 'named-reference',
+ 'ინდექსი': 'index',
+ 'ქვედა-ინდექსი': 'subscript',
+ 'სქელი-ფონტი': 'strong',
+ 'აბრევიატურა': 'abbreviation',
+ 'ჩანაცვლების-მიმართვა': 'substitution-reference',
+ 'pep-მიმართვა': 'pep-reference',
+ 'rfc-მიმართვა ': 'rfc-reference',
+ 'uri-მიმართვა': 'uri-reference',
+ 'title-მიმართვა': 'title-reference',
+ 'ქვედა-კოლონტიტულზე-მიმართვა': 'footnote-reference',
+ 'ციტატაზე-მიმართვა': 'citation-reference',
+ 'სამიზნე': 'target',
+ 'დაუმუშავებელი': 'raw',
+ }
+"""Mapping of Georgian role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ko.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ko.py
new file mode 100644
index 00000000..434fea12
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ko.py
@@ -0,0 +1,111 @@
+# $Id: ko.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Thomas SJ Kang <thomas.kangsj@ujuc.kr>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Korean-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ '집중': 'attention',
+ '주의': 'caution',
+ '코드': 'code',
+ '코드-블록': 'code',
+ '소스코드': 'code',
+ '위험': 'danger',
+ '오류': 'error',
+ '실마리': 'hint',
+ '중요한': 'important',
+ '비고': 'note',
+ '팁': 'tip',
+ '경고': 'warning',
+ '권고': 'admonition',
+ '사이드바': 'sidebar',
+ '주제': 'topic',
+ '라인-블록': 'line-block',
+ '파싱된-리터럴': 'parsed-literal',
+ '지시문': 'rubric',
+ '제명': 'epigraph',
+ '하이라이트': 'highlights',
+ '발췌문': 'pull-quote',
+ '합성어': 'compound',
+ '컨테이너': 'container',
+ # '질문': 'questions',
+ '표': 'table',
+ 'csv-표': 'csv-table',
+ 'list-표': 'list-table',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ '메타': 'meta',
+ '수학': 'math',
+ # '이미지맵': 'imagemap',
+ '이미지': 'image',
+ '도표': 'figure',
+ '포함': 'include',
+ 'raw': 'raw',
+ '대신하다': 'replace',
+ '유니코드': 'unicode',
+ '날짜': 'date',
+ '클래스': 'class',
+ '역할': 'role',
+ '기본-역할': 'default-role',
+ '제목': 'title',
+ '내용': 'contents',
+ 'sectnum': 'sectnum',
+ '섹션-번호-매기기': 'sectnum',
+ '머리말': 'header',
+ '꼬리말': 'footer',
+ # '긱주': 'footnotes',
+ # '인용구': 'citations',
+ '목표-노트': 'target-notes',
+ 'restructuredtext 테스트 지시어': 'restructuredtext-test-directive'}
+"""Korean name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ '약어': 'abbreviation',
+ 'ab': 'abbreviation',
+ '두음문자': 'acronym',
+ 'ac': 'acronym',
+ '코드': 'code',
+ '색인': 'index',
+ 'i': 'index',
+ '다리-글자': 'subscript',
+ 'sub': 'subscript',
+ '어깨-글자': 'superscript',
+ 'sup': 'superscript',
+ '제목-참조': 'title-reference',
+ '제목': 'title-reference',
+ 't': 'title-reference',
+ 'pep-참조': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'rfc-참조': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ '강조': 'emphasis',
+ '굵게': 'strong',
+ '기울기': 'literal',
+ '수학': 'math',
+ '명명된-참조': 'named-reference',
+ '익명-참조': 'anonymous-reference',
+ '각주-참조': 'footnote-reference',
+ '인용-참조': 'citation-reference',
+ '대리-참조': 'substitution-reference',
+ '대상': 'target',
+ 'uri-참조': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'raw': 'raw',
+ }
+"""Mapping of Korean role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/lt.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/lt.py
new file mode 100644
index 00000000..7d324d67
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/lt.py
@@ -0,0 +1,109 @@
+# $Id: lt.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Dalius Dobravolskas <dalius.do...@gmail.com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Lithuanian-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'dėmesio': 'attention',
+ 'atsargiai': 'caution',
+ 'code (translation required)': 'code',
+ 'pavojinga': 'danger',
+ 'klaida': 'error',
+ 'užuomina': 'hint',
+ 'svarbu': 'important',
+ 'pastaba': 'note',
+ 'patarimas': 'tip',
+ 'įspėjimas': 'warning',
+ 'perspėjimas': 'admonition',
+ 'šoninė-juosta': 'sidebar',
+ 'tema': 'topic',
+ 'linijinis-blokas': 'line-block',
+ 'išanalizuotas-literalas': 'parsed-literal',
+ 'rubrika': 'rubric',
+ 'epigrafas': 'epigraph',
+ 'pagridiniai-momentai': 'highlights',
+ 'atitraukta-citata': 'pull-quote',
+ 'sudėtinis-darinys': 'compound',
+ 'konteineris': 'container',
+ # 'questions': 'questions',
+ 'lentelė': 'table',
+ 'csv-lentelė': 'csv-table',
+ 'sąrašo-lentelė': 'list-table',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'meta': 'meta',
+ 'matematika': 'math',
+ # 'imagemap': 'imagemap',
+ 'paveiksliukas': 'image',
+ 'iliustracija': 'figure',
+ 'pridėti': 'include',
+ 'žalia': 'raw',
+ 'pakeisti': 'replace',
+ 'unikodas': 'unicode',
+ 'data': 'date',
+ 'klasė': 'class',
+ 'rolė': 'role',
+ 'numatytoji-rolė': 'default-role',
+ 'titulas': 'title',
+ 'turinys': 'contents',
+ 'seknum': 'sectnum',
+ 'sekcijos-numeravimas': 'sectnum',
+ 'antraštė': 'header',
+ 'poraštė': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ 'nutaikytos-pastaba': 'target-notes',
+ 'restructuredtext-testinė-direktyva': 'restructuredtext-test-directive'}
+"""Lithuanian name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'santrumpa': 'abbreviation',
+ 'sa': 'abbreviation',
+ 'akronimas': 'acronym',
+ 'ak': 'acronym',
+ 'code (translation required)': 'code',
+ 'indeksas': 'index',
+ 'i': 'index',
+ 'apatinis-indeksas': 'subscript',
+ 'sub': 'subscript',
+ 'viršutinis-indeksas': 'superscript',
+ 'sup': 'superscript',
+ 'antrašės-nuoroda': 'title-reference',
+ 'antraštė': 'title-reference',
+ 'a': 'title-reference',
+ 'pep-nuoroda': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'rfc-nuoroda': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'paryškinimas': 'emphasis',
+ 'sustiprintas': 'strong',
+ 'literalas': 'literal',
+ 'matematika': 'math',
+ 'vardinė-nuoroda': 'named-reference',
+ 'anoniminė-nuoroda': 'anonymous-reference',
+ 'išnašos-nuoroda': 'footnote-reference',
+ 'citatos-nuoroda': 'citation-reference',
+ 'pakeitimo-nuoroda': 'substitution-reference',
+ 'taikinys': 'target',
+ 'uri-nuoroda': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'žalia': 'raw',
+ }
+"""Mapping of English role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/lv.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/lv.py
new file mode 100644
index 00000000..18e4dc49
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/lv.py
@@ -0,0 +1,108 @@
+# $Id: lv.py 9030 2022-03-05 23:28:32Z milde $
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Latvian-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'uzmanību': 'attention',
+ 'piesardzību': 'caution',
+ 'kods': 'code',
+ 'koda-bloks': 'code',
+ 'pirmkods': 'code',
+ 'bīstami': 'danger',
+ 'kļūda': 'error',
+ 'ieteikums': 'hint',
+ 'svarīgi': 'important',
+ 'piezīme': 'note',
+ 'padoms': 'tip',
+ 'brīdinājums': 'warning',
+ 'aizrādījums': 'admonition',
+ 'sānjosla': 'sidebar',
+ 'tēma': 'topic',
+ 'rindu-bloks': 'line-block',
+ 'parsēts-literālis': 'parsed-literal',
+ 'rubrika': 'rubric',
+ 'epigrāfs': 'epigraph',
+ 'apskats': 'highlights',
+ 'izvilkuma-citāts': 'pull-quote',
+ 'savienojums': 'compound',
+ 'konteiners': 'container',
+ # 'questions': 'questions',
+ 'tabula': 'table',
+ 'csv-tabula': 'csv-table',
+ 'sarakstveida-tabula': 'list-table',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'meta': 'meta',
+ 'matemātika': 'math',
+ # 'imagemap': 'imagemap',
+ 'attēls': 'image',
+ 'figūra': 'figure',
+ 'ietvert': 'include',
+ 'burtiski': 'raw',
+ 'aizvieto': 'replace',
+ 'unicode': 'unicode',
+ 'datums': 'date',
+ 'klase': 'class',
+ 'role': 'role',
+ 'noklusējuma-role': 'default-role',
+ 'virsraksts': 'title',
+ 'saturs': 'contents',
+ 'numurēt-sekcijas': 'sectnum',
+ 'galvene': 'header',
+ 'kājene': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ 'atsauces-apakšā': 'target-notes',
+ 'restructuredtext-testa-direktīva': 'restructuredtext-test-directive'}
+"""English name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'saīsinājums': 'abbreviation',
+ 'īsi': 'abbreviation',
+ 'akronīms': 'acronym',
+ 'kods': 'code',
+ 'indekss': 'index',
+ 'i': 'index',
+ 'apakšraksts': 'subscript',
+ 'apakšā': 'subscript',
+ 'augšraksts': 'superscript',
+ 'augšā': 'superscript',
+ 'virsraksta-atsauce': 'title-reference',
+ 'virsraksts': 'title-reference',
+ 'v': 'title-reference',
+ 'atsauce-uz-pep': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'atsauce-uz-rfc': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'izcēlums': 'emphasis',
+ 'blīvs': 'strong',
+ 'literālis': 'literal',
+ 'matemātika': 'math',
+ 'nosaukta-atsauce': 'named-reference',
+ 'nenosaukta-atsauce': 'anonymous-reference',
+ 'kājenes-atsauce': 'footnote-reference',
+ 'citātā-atsauce': 'citation-reference',
+ 'aizvietojuma-atsauce': 'substitution-reference',
+ 'mēr''kis': 'target',
+ 'atsauce-uz-uri': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'burtiski': 'raw',
+ }
+"""Mapping of English role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/nl.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/nl.py
new file mode 100644
index 00000000..762ddcf2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/nl.py
@@ -0,0 +1,114 @@
+# $Id: nl.py 9417 2023-06-27 20:04:54Z milde $
+# Author: Martijn Pieters <mjpieters@users.sourceforge.net>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Dutch-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'attentie': 'attention',
+ 'let-op': 'caution',
+ 'code (translation required)': 'code',
+ 'gevaar': 'danger',
+ 'fout': 'error',
+ 'hint': 'hint',
+ 'belangrijk': 'important',
+ 'opmerking': 'note',
+ 'tip': 'tip',
+ 'waarschuwing': 'warning',
+ 'advies': 'admonition',
+ 'aanmaning': 'admonition', # sic! kept for backwards compatibiltity
+ 'katern': 'sidebar',
+ 'onderwerp': 'topic',
+ 'lijn-blok': 'line-block',
+ 'letterlijk-ontleed': 'parsed-literal',
+ 'rubriek': 'rubric',
+ 'opschrift': 'epigraph',
+ 'hoogtepunten': 'highlights',
+ 'pull-quote': 'pull-quote', # Dutch printers use the english term
+ 'samenstelling': 'compound',
+ 'verbinding': 'compound',
+ 'container (translation required)': 'container',
+ # 'vragen': 'questions',
+ 'tabel': 'table',
+ 'csv-tabel': 'csv-table',
+ 'lijst-tabel': 'list-table',
+ # 'veelgestelde-vragen': 'questions',
+ 'meta': 'meta',
+ 'math (translation required)': 'math',
+ # 'imagemap': 'imagemap',
+ 'beeld': 'image',
+ 'figuur': 'figure',
+ 'opnemen': 'include',
+ 'onbewerkt': 'raw',
+ 'vervang': 'replace',
+ 'vervanging': 'replace',
+ 'unicode': 'unicode',
+ 'datum': 'date',
+ 'klasse': 'class',
+ 'rol': 'role',
+ 'default-role (translation required)': 'default-role',
+ 'title (translation required)': 'title',
+ 'inhoud': 'contents',
+ 'sectnum': 'sectnum',
+ 'sectie-nummering': 'sectnum',
+ 'hoofdstuk-nummering': 'sectnum',
+ 'header (translation required)': 'header',
+ 'footer (translation required)': 'footer',
+ # 'voetnoten': 'footnotes',
+ # 'citaten': 'citations',
+ 'verwijzing-voetnoten': 'target-notes',
+ 'restructuredtext-test-instructie': 'restructuredtext-test-directive'}
+"""Dutch name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'afkorting': 'abbreviation',
+ # 'ab': 'abbreviation',
+ 'acroniem': 'acronym',
+ 'ac': 'acronym',
+ 'code (translation required)': 'code',
+ 'index': 'index',
+ 'i': 'index',
+ 'inferieur': 'subscript',
+ 'inf': 'subscript',
+ 'superieur': 'superscript',
+ 'sup': 'superscript',
+ 'titel-referentie': 'title-reference',
+ 'titel': 'title-reference',
+ 't': 'title-reference',
+ 'pep-referentie': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'rfc-referentie': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'nadruk': 'emphasis',
+ 'extra': 'strong',
+ 'extra-nadruk': 'strong',
+ 'vet': 'strong',
+ 'letterlijk': 'literal',
+ 'math (translation required)': 'math',
+ 'benoemde-referentie': 'named-reference',
+ 'anonieme-referentie': 'anonymous-reference',
+ 'voetnoot-referentie': 'footnote-reference',
+ 'citaat-referentie': 'citation-reference',
+ 'substitie-reference': 'substitution-reference',
+ 'verwijzing': 'target',
+ 'uri-referentie': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'onbewerkt': 'raw',
+ }
+"""Mapping of Dutch role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/pl.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/pl.py
new file mode 100644
index 00000000..9aac2d42
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/pl.py
@@ -0,0 +1,101 @@
+# $Id$
+# Author: Robert Wojciechowicz <rw@smsnet.pl>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Polish-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'uwaga': 'attention',
+ 'ostrożnie': 'caution',
+ 'code (translation required)': 'code',
+ 'niebezpieczeństwo': 'danger',
+ 'błąd': 'error',
+ 'wskazówka': 'hint',
+ 'ważne': 'important',
+ 'przypis': 'note',
+ 'rada': 'tip',
+ 'ostrzeżenie': 'warning',
+ 'zauważenie': 'admonition', # remark
+ 'upomnienie': 'admonition', # sic! kept for backwards compatibiltity
+ 'ramka': 'sidebar',
+ 'temat': 'topic',
+ 'blok-linii': 'line-block',
+ 'sparsowany-literał': 'parsed-literal',
+ 'rubryka': 'rubric',
+ 'epigraf': 'epigraph',
+ 'highlights': 'highlights', # FIXME no polish equivalent?
+ 'pull-quote': 'pull-quote', # FIXME no polish equivalent?
+ 'złożony': 'compound',
+ 'kontener': 'container',
+ # 'questions': 'questions',
+ 'tabela': 'table',
+ 'tabela-csv': 'csv-table',
+ 'tabela-listowa': 'list-table',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'meta': 'meta',
+ 'math (translation required)': 'math',
+ # 'imagemap': 'imagemap',
+ 'obraz': 'image',
+ 'rycina': 'figure',
+ 'dołącz': 'include',
+ 'surowe': 'raw',
+ 'zastąp': 'replace',
+ 'unikod': 'unicode',
+ 'data': 'date',
+ 'klasa': 'class',
+ 'rola': 'role',
+ 'rola-domyślna': 'default-role',
+ 'tytuł': 'title',
+ 'treść': 'contents',
+ 'sectnum': 'sectnum',
+ 'numeracja-sekcji': 'sectnum',
+ 'nagłówek': 'header',
+ 'stopka': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ 'target-notes': 'target-notes', # FIXME no polish equivalent?
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Polish name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'skrót': 'abbreviation',
+ 'akronim': 'acronym',
+ 'code (translation required)': 'code',
+ 'indeks': 'index',
+ 'indeks-dolny': 'subscript',
+ 'indeks-górny': 'superscript',
+ 'referencja-tytuł': 'title-reference',
+ 'referencja-pep': 'pep-reference',
+ 'referencja-rfc': 'rfc-reference',
+ 'podkreślenie': 'emphasis',
+ 'wytłuszczenie': 'strong',
+ 'dosłownie': 'literal',
+ 'math (translation required)': 'math',
+ 'referencja-nazwana': 'named-reference',
+ 'referencja-anonimowa': 'anonymous-reference',
+ 'referencja-przypis': 'footnote-reference',
+ 'referencja-cytat': 'citation-reference',
+ 'referencja-podstawienie': 'substitution-reference',
+ 'cel': 'target',
+ 'referencja-uri': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'surowe': 'raw',
+ }
+"""Mapping of Polish role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/pt_br.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/pt_br.py
new file mode 100644
index 00000000..45a670db
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/pt_br.py
@@ -0,0 +1,110 @@
+# $Id: pt_br.py 9452 2023-09-27 00:11:54Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Brazilian Portuguese-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'atenção': 'attention',
+ 'cuidado': 'caution',
+ 'code (translation required)': 'code',
+ 'perigo': 'danger',
+ 'erro': 'error',
+ 'sugestão': 'hint',
+ 'importante': 'important',
+ 'nota': 'note',
+ 'dica': 'tip',
+ 'aviso': 'warning',
+ 'advertência': 'admonition',
+ 'exortação': 'admonition', # sic! advice/advisory/remark, not reprimand
+ 'barra-lateral': 'sidebar',
+ 'tópico': 'topic',
+ 'bloco-de-linhas': 'line-block',
+ 'literal-interpretado': 'parsed-literal',
+ 'rubrica': 'rubric',
+ 'epígrafo': 'epigraph',
+ 'destaques': 'highlights',
+ 'citação-destacada': 'pull-quote',
+ 'compound (translation required)': 'compound',
+ 'container (translation required)': 'container',
+ # 'perguntas': 'questions',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'table (translation required)': 'table',
+ 'csv-table (translation required)': 'csv-table',
+ 'list-table (translation required)': 'list-table',
+ 'meta': 'meta',
+ 'math (translation required)': 'math',
+ # 'imagemap': 'imagemap',
+ 'imagem': 'image',
+ 'figura': 'figure',
+ 'inclusão': 'include',
+ 'cru': 'raw',
+ 'substituição': 'replace',
+ 'unicode': 'unicode',
+ 'data': 'date',
+ 'classe': 'class',
+ 'role (translation required)': 'role',
+ 'default-role (translation required)': 'default-role',
+ 'title (translation required)': 'title',
+ 'índice': 'contents',
+ 'numsec': 'sectnum',
+ 'numeração-de-seções': 'sectnum',
+ 'header (translation required)': 'header',
+ 'footer (translation required)': 'footer',
+ # 'notas-de-rorapé': 'footnotes',
+ # 'citações': 'citations',
+ 'links-no-rodapé': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Brazilian Portuguese name to registered (in directives/__init__.py)
+directive name mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'abbreviação': 'abbreviation',
+ 'ab': 'abbreviation',
+ 'acrônimo': 'acronym',
+ 'ac': 'acronym',
+ 'code (translation required)': 'code',
+ 'índice-remissivo': 'index',
+ 'i': 'index',
+ 'subscrito': 'subscript',
+ 'sub': 'subscript',
+ 'sobrescrito': 'superscript',
+ 'sob': 'superscript',
+ 'referência-a-título': 'title-reference',
+ 'título': 'title-reference',
+ 't': 'title-reference',
+ 'referência-a-pep': 'pep-reference',
+ 'pep': 'pep-reference',
+ 'referência-a-rfc': 'rfc-reference',
+ 'rfc': 'rfc-reference',
+ 'ênfase': 'emphasis',
+ 'forte': 'strong',
+ 'literal': 'literal',
+ 'math (translation required)': 'math', # translation required?
+ 'referência-por-nome': 'named-reference',
+ 'referência-anônima': 'anonymous-reference',
+ 'referência-a-nota-de-rodapé': 'footnote-reference',
+ 'referência-a-citação': 'citation-reference',
+ 'referência-a-substituição': 'substitution-reference',
+ 'alvo': 'target',
+ 'referência-a-uri': 'uri-reference',
+ 'uri': 'uri-reference',
+ 'url': 'uri-reference',
+ 'cru': 'raw',
+ }
+"""Mapping of Brazilian Portuguese role names to canonical role names
+for interpreted text."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ru.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ru.py
new file mode 100644
index 00000000..7c84cd02
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/ru.py
@@ -0,0 +1,90 @@
+# $Id: ru.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Roman Suzi <rnd@onego.ru>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Russian-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+directives = {
+ 'блок-строк': 'line-block',
+ 'meta': 'meta',
+ 'математика': 'math',
+ 'обработанный-литерал': 'parsed-literal',
+ 'выделенная-цитата': 'pull-quote',
+ 'код': 'code',
+ 'compound (translation required)': 'compound',
+ 'контейнер': 'container',
+ 'таблица': 'table',
+ 'csv-table (translation required)': 'csv-table',
+ 'list-table (translation required)': 'list-table',
+ 'сырой': 'raw',
+ 'замена': 'replace',
+ 'тестовая-директива-restructuredtext': 'restructuredtext-test-directive',
+ 'целевые-сноски': 'target-notes',
+ 'unicode': 'unicode',
+ 'дата': 'date',
+ 'боковая-полоса': 'sidebar',
+ 'важно': 'important',
+ 'включать': 'include',
+ 'внимание': 'attention',
+ 'выделение': 'highlights',
+ 'замечание': 'admonition',
+ 'изображение': 'image',
+ 'класс': 'class',
+ 'роль': 'role',
+ 'default-role (translation required)': 'default-role',
+ 'титул': 'title',
+ 'номер-раздела': 'sectnum',
+ 'нумерация-разделов': 'sectnum',
+ 'опасно': 'danger',
+ 'осторожно': 'caution',
+ 'ошибка': 'error',
+ 'подсказка': 'tip',
+ 'предупреждение': 'warning',
+ 'примечание': 'note',
+ 'рисунок': 'figure',
+ 'рубрика': 'rubric',
+ 'совет': 'hint',
+ 'содержание': 'contents',
+ 'тема': 'topic',
+ 'эпиграф': 'epigraph',
+ 'header (translation required)': 'header',
+ 'footer (translation required)': 'footer',
+ }
+"""Russian name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ 'акроним': 'acronym',
+ 'код': 'code',
+ 'анонимная-ссылка': 'anonymous-reference',
+ 'буквально': 'literal',
+ 'математика': 'math',
+ 'верхний-индекс': 'superscript',
+ 'выделение': 'emphasis',
+ 'именованная-ссылка': 'named-reference',
+ 'индекс': 'index',
+ 'нижний-индекс': 'subscript',
+ 'сильное-выделение': 'strong',
+ 'сокращение': 'abbreviation',
+ 'ссылка-замена': 'substitution-reference',
+ 'ссылка-на-pep': 'pep-reference',
+ 'ссылка-на-rfc': 'rfc-reference',
+ 'ссылка-на-uri': 'uri-reference',
+ 'ссылка-на-заглавие': 'title-reference',
+ 'ссылка-на-сноску': 'footnote-reference',
+ 'цитатная-ссылка': 'citation-reference',
+ 'цель': 'target',
+ 'сырой': 'raw',
+ }
+"""Mapping of Russian role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/sk.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/sk.py
new file mode 100644
index 00000000..7b1cef82
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/sk.py
@@ -0,0 +1,96 @@
+# $Id: sk.py 9452 2023-09-27 00:11:54Z milde $
+# Author: Miroslav Vasko <zemiak@zoznam.sk>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Slovak-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ 'pozor': 'attention',
+ 'opatrne': 'caution',
+ 'code (translation required)': 'code',
+ 'nebezpe\xe8enstvo': 'danger',
+ 'chyba': 'error',
+ 'rada': 'hint',
+ 'd\xf4le\x9eit\xe9': 'important',
+ 'pozn\xe1mka': 'note',
+ 'tip (translation required)': 'tip',
+ 'varovanie': 'warning',
+ 'admonition (translation required)': 'admonition',
+ 'sidebar (translation required)': 'sidebar',
+ 't\xe9ma': 'topic',
+ 'blok-riadkov': 'line-block',
+ 'parsed-literal': 'parsed-literal',
+ 'rubric (translation required)': 'rubric',
+ 'epigraph (translation required)': 'epigraph',
+ 'highlights (translation required)': 'highlights',
+ 'pull-quote (translation required)': 'pull-quote',
+ 'compound (translation required)': 'compound',
+ 'container (translation required)': 'container',
+ # 'questions': 'questions',
+ # 'qa': 'questions',
+ # 'faq': 'questions',
+ 'table (translation required)': 'table',
+ 'csv-table (translation required)': 'csv-table',
+ 'list-table (translation required)': 'list-table',
+ 'meta': 'meta',
+ 'math (translation required)': 'math',
+ # 'imagemap': 'imagemap',
+ 'obr\xe1zok': 'image',
+ 'tvar': 'figure',
+ 'vlo\x9ei\x9d': 'include',
+ 'raw (translation required)': 'raw',
+ 'nahradi\x9d': 'replace',
+ 'unicode': 'unicode',
+ 'dátum': 'date',
+ 'class (translation required)': 'class',
+ 'role (translation required)': 'role',
+ 'default-role (translation required)': 'default-role',
+ 'title (translation required)': 'title',
+ 'obsah': 'contents',
+ '\xe8as\x9d': 'sectnum',
+ '\xe8as\x9d-\xe8\xedslovanie': 'sectnum',
+ 'cie\xbeov\xe9-pozn\xe1mky': 'target-notes',
+ 'header (translation required)': 'header',
+ 'footer (translation required)': 'footer',
+ # 'footnotes': 'footnotes',
+ # 'citations': 'citations',
+ }
+"""Slovak name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ 'abbreviation (translation required)': 'abbreviation',
+ 'acronym (translation required)': 'acronym',
+ 'code (translation required)': 'code',
+ 'index (translation required)': 'index',
+ 'subscript (translation required)': 'subscript',
+ 'superscript (translation required)': 'superscript',
+ 'title-reference (translation required)': 'title-reference',
+ 'pep-reference (translation required)': 'pep-reference',
+ 'rfc-reference (translation required)': 'rfc-reference',
+ 'emphasis (translation required)': 'emphasis',
+ 'strong (translation required)': 'strong',
+ 'literal (translation required)': 'literal',
+ 'math (translation required)': 'math',
+ 'named-reference (translation required)': 'named-reference',
+ 'anonymous-reference (translation required)': 'anonymous-reference',
+ 'footnote-reference (translation required)': 'footnote-reference',
+ 'citation-reference (translation required)': 'citation-reference',
+ 'substitution-reference (translation required)': 'substitution-reference', # noqa:E501
+ 'target (translation required)': 'target',
+ 'uri-reference (translation required)': 'uri-reference',
+ 'raw (translation required)': 'raw',
+ }
+"""Mapping of Slovak role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/sv.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/sv.py
new file mode 100644
index 00000000..e6b11aea
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/sv.py
@@ -0,0 +1,96 @@
+# $Id: sv.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Adam Chodorowski <chodorowski@users.sourceforge.net>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Swedish language mappings for language-dependent features of reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+directives = {
+ 'observera': 'attention',
+ 'akta': 'caution', # also 'försiktigt'
+ 'kod': 'code',
+ 'fara': 'danger',
+ 'fel': 'error',
+ 'vink': 'hint', # also 'hint'
+ 'viktigt': 'important',
+ 'notera': 'note',
+ 'tips': 'tip',
+ 'varning': 'warning',
+ 'anmärkning': 'admonition', # literal 'tillrättavisning', 'förmaning'
+ 'sidorad': 'sidebar',
+ 'ämne': 'topic',
+ 'tema': 'topic',
+ 'rad-block': 'line-block',
+ # 'tolkad-bokstavlig'?
+ 'parsed-literal (translation required)': 'parsed-literal',
+ 'rubrik': 'rubric',
+ 'epigraf': 'epigraph',
+ 'höjdpunkter': 'highlights',
+ 'pull-quote (translation required)': 'pull-quote',
+ 'sammansatt': 'compound',
+ 'container': 'container',
+ # 'frågor': 'questions',
+ # NOTE: A bit long, but recommended by http://www.nada.kth.se/dataterm/:
+ # 'frågor-och-svar': 'questions',
+ # 'vanliga-frågor': 'questions',
+ 'tabell': 'table',
+ 'csv-tabell': 'csv-table',
+ 'list-tabell': 'list-table',
+ 'meta': 'meta',
+ 'matematik': 'math',
+ # 'bildkarta': 'imagemap', # FIXME: Translation might be too literal.
+ 'bild': 'image',
+ 'figur': 'figure',
+ 'inkludera': 'include',
+ 'rå': 'raw',
+ 'ersätta': 'replace',
+ 'unicode': 'unicode',
+ 'datum': 'date',
+ 'klass': 'class',
+ 'roll': 'role',
+ 'standardroll': 'default-role',
+ 'titel': 'title',
+ 'innehåll': 'contents',
+ 'sektionsnumrering': 'sectnum',
+ 'target-notes (translation required)': 'target-notes',
+ 'sidhuvud': 'header',
+ 'sidfot': 'footer',
+ # 'fotnoter': 'footnotes',
+ # 'citeringar': 'citations',
+ }
+"""Swedish name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ 'förkortning': 'abbreviation',
+ 'akronym': 'acronym',
+ 'kod': 'code',
+ 'index': 'index',
+ 'nedsänkt': 'subscript',
+ 'upphöjd': 'superscript',
+ 'titel-referens': 'title-reference',
+ 'pep-referens': 'pep-reference',
+ 'rfc-referens': 'rfc-reference',
+ 'betoning': 'emphasis',
+ 'stark': 'strong',
+ 'bokstavlig': 'literal', # also 'ordagranna'
+ 'matematik': 'math',
+ 'namngiven-referens': 'named-reference',
+ 'anonym-referens': 'anonymous-reference',
+ 'fotnot-referens': 'footnote-reference',
+ 'citat-referens': 'citation-reference',
+ 'ersättnings-referens': 'substitution-reference',
+ 'mål': 'target',
+ 'uri-referens': 'uri-reference',
+ 'rå': 'raw',
+ }
+"""Mapping of Swedish role names to canonical role names for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/uk.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/uk.py
new file mode 100644
index 00000000..9e74dc32
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/uk.py
@@ -0,0 +1,91 @@
+# $Id: uk.py 9114 2022-07-28 17:06:10Z milde $
+# Author: Dmytro Kazanzhy <dkazanzhy@gmail.com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <http://docutils.sf.net/docs/howto/i18n.html>. Two files must be
+# translated for each language: one in docutils/languages, the other in
+# docutils/parsers/rst/languages.
+
+"""
+Ukrainian-language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+directives = {
+ 'блок-строк': 'line-block',
+ 'мета': 'meta',
+ 'математика': 'math',
+ 'оброблений-літерал': 'parsed-literal',
+ 'виділена-цитата': 'pull-quote',
+ 'код': 'code',
+ 'складений абзац': 'compound',
+ 'контейнер': 'container',
+ 'таблиця': 'table',
+ 'таблиця-csv': 'csv-table',
+ 'таблиця-списків': 'list-table',
+ 'сирий': 'raw',
+ 'заміна': 'replace',
+ 'тестова-директива-restructuredtext': 'restructuredtext-test-directive',
+ 'цільові-виноски': 'target-notes',
+ 'юнікод': 'unicode',
+ 'дата': 'date',
+ 'бічна-панель': 'sidebar',
+ 'важливо': 'important',
+ 'включати': 'include',
+ 'увага': 'attention',
+ 'виділення': 'highlights',
+ 'зауваження': 'admonition',
+ 'зображення': 'image',
+ 'клас': 'class',
+ 'роль': 'role',
+ 'роль-за-замовчуванням': 'default-role',
+ 'заголовок': 'title',
+ 'номер-розділу': 'sectnum',
+ 'нумерація-розділів': 'sectnum',
+ 'небезпечно': 'danger',
+ 'обережно': 'caution',
+ 'помилка': 'error',
+ 'підказка': 'tip',
+ 'попередження': 'warning',
+ 'примітка': 'note',
+ 'малюнок': 'figure',
+ 'рубрика': 'rubric',
+ 'порада': 'hint',
+ 'зміст': 'contents',
+ 'тема': 'topic',
+ 'епіграф': 'epigraph',
+ 'верхній колонтитул': 'header',
+ 'нижній колонтитул': 'footer',
+ }
+"""Ukrainian name to registered (in directives/__init__.py) directive name
+mapping."""
+
+roles = {
+ 'акронім': 'acronym',
+ 'код': 'code',
+ 'анонімне-посилання': 'anonymous-reference',
+ 'буквально': 'literal',
+ 'математика': 'math',
+ 'верхній-індекс': 'superscript',
+ 'наголос': 'emphasis',
+ 'іменоване-посилання': 'named-reference',
+ 'індекс': 'index',
+ 'нижній-індекс': 'subscript',
+ 'жирне-накреслення': 'strong',
+ 'скорочення': 'abbreviation',
+ 'посилання-заміна': 'substitution-reference',
+ 'посилання-на-pep': 'pep-reference',
+ 'посилання-на-rfc': 'rfc-reference',
+ 'посилання-на-uri': 'uri-reference',
+ 'посилання-на-заголовок': 'title-reference',
+ 'посилання-на-зноску': 'footnote-reference',
+ 'посилання-на-цитату': 'citation-reference',
+ 'ціль': 'target',
+ 'сирий': 'raw',
+ }
+"""Mapping of Ukrainian role names to canonical role names
+for interpreted text.
+"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/zh_cn.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/zh_cn.py
new file mode 100644
index 00000000..fbb6fd8c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/zh_cn.py
@@ -0,0 +1,104 @@
+# $Id: zh_cn.py 9030 2022-03-05 23:28:32Z milde $
+# Author: Panjunyong <panjy@zopechina.com>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Simplified Chinese language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ '注意': 'attention',
+ '小心': 'caution',
+ 'code (translation required)': 'code',
+ '危险': 'danger',
+ '错误': 'error',
+ '提示': 'hint',
+ '重要': 'important',
+ '注解': 'note',
+ '技巧': 'tip',
+ '警告': 'warning',
+ '忠告': 'admonition',
+ '侧框': 'sidebar',
+ '主题': 'topic',
+ 'line-block (translation required)': 'line-block',
+ 'parsed-literal (translation required)': 'parsed-literal',
+ '醒目': 'rubric',
+ '铭文': 'epigraph',
+ '要点': 'highlights',
+ 'pull-quote (translation required)': 'pull-quote',
+ '复合': 'compound',
+ '容器': 'container',
+ # 'questions (translation required)': 'questions',
+ '表格': 'table',
+ 'csv表格': 'csv-table',
+ '列表表格': 'list-table',
+ # 'qa (translation required)': 'questions',
+ # 'faq (translation required)': 'questions',
+ '元数据': 'meta',
+ 'math (translation required)': 'math',
+ # 'imagemap (translation required)': 'imagemap',
+ '图片': 'image',
+ '图例': 'figure',
+ '包含': 'include',
+ '原文': 'raw',
+ '代替': 'replace',
+ '统一码': 'unicode',
+ '日期': 'date',
+ '类型': 'class',
+ '角色': 'role',
+ '默认角色': 'default-role',
+ '标题': 'title',
+ '目录': 'contents',
+ '章节序号': 'sectnum',
+ '题头': 'header',
+ '页脚': 'footer',
+ # 'footnotes (translation required)': 'footnotes',
+ # 'citations (translation required)': 'citations',
+ 'target-notes (translation required)': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Simplified Chinese name to registered (in directives/__init__.py)
+directive name mapping."""
+
+roles = {
+ # language-dependent: fixed
+ '缩写': 'abbreviation',
+ '简称': 'acronym',
+ 'code (translation required)': 'code',
+ 'index (translation required)': 'index',
+ 'i (translation required)': 'index',
+ '下标': 'subscript',
+ '上标': 'superscript',
+ 'title-reference (translation required)': 'title-reference',
+ 'title (translation required)': 'title-reference',
+ 't (translation required)': 'title-reference',
+ 'pep-reference (translation required)': 'pep-reference',
+ 'pep (translation required)': 'pep-reference',
+ 'rfc-reference (translation required)': 'rfc-reference',
+ 'rfc (translation required)': 'rfc-reference',
+ '强调': 'emphasis',
+ '加粗': 'strong',
+ '字面': 'literal',
+ 'math (translation required)': 'math',
+ 'named-reference (translation required)': 'named-reference',
+ 'anonymous-reference (translation required)': 'anonymous-reference',
+ 'footnote-reference (translation required)': 'footnote-reference',
+ 'citation-reference (translation required)': 'citation-reference',
+ 'substitution-reference (translation required)': 'substitution-reference',
+ 'target (translation required)': 'target',
+ 'uri-reference (translation required)': 'uri-reference',
+ 'uri (translation required)': 'uri-reference',
+ 'url (translation required)': 'uri-reference',
+ 'raw (translation required)': 'raw',
+ }
+"""Mapping of Simplified Chinese role names to canonical role names
+for interpreted text."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/zh_tw.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/zh_tw.py
new file mode 100644
index 00000000..126255d0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/languages/zh_tw.py
@@ -0,0 +1,109 @@
+# $Id: zh_tw.py 9030 2022-03-05 23:28:32Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+# New language mappings are welcome. Before doing a new translation, please
+# read <https://docutils.sourceforge.io/docs/howto/i18n.html>.
+# Two files must be translated for each language: one in docutils/languages,
+# the other in docutils/parsers/rst/languages.
+
+"""
+Traditional Chinese language mappings for language-dependent features of
+reStructuredText.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+directives = {
+ # language-dependent: fixed
+ 'attention (translation required)': 'attention',
+ 'caution (translation required)': 'caution',
+ 'code (translation required)': 'code',
+ 'danger (translation required)': 'danger',
+ 'error (translation required)': 'error',
+ 'hint (translation required)': 'hint',
+ 'important (translation required)': 'important',
+ 'note (translation required)': 'note',
+ 'tip (translation required)': 'tip',
+ 'warning (translation required)': 'warning',
+ 'admonition (translation required)': 'admonition',
+ 'sidebar (translation required)': 'sidebar',
+ 'topic (translation required)': 'topic',
+ 'line-block (translation required)': 'line-block',
+ 'parsed-literal (translation required)': 'parsed-literal',
+ 'rubric (translation required)': 'rubric',
+ 'epigraph (translation required)': 'epigraph',
+ 'highlights (translation required)': 'highlights',
+ 'pull-quote (translation required)': 'pull-quote',
+ 'compound (translation required)': 'compound',
+ 'container (translation required)': 'container',
+ # 'questions (translation required)': 'questions',
+ 'table (translation required)': 'table',
+ 'csv-table (translation required)': 'csv-table',
+ 'list-table (translation required)': 'list-table',
+ # 'qa (translation required)': 'questions',
+ # 'faq (translation required)': 'questions',
+ 'meta (translation required)': 'meta',
+ 'math (translation required)': 'math',
+ # 'imagemap (translation required)': 'imagemap',
+ 'image (translation required)': 'image',
+ 'figure (translation required)': 'figure',
+ 'include (translation required)': 'include',
+ 'raw (translation required)': 'raw',
+ 'replace (translation required)': 'replace',
+ 'unicode (translation required)': 'unicode',
+ '日期': 'date',
+ 'class (translation required)': 'class',
+ 'role (translation required)': 'role',
+ 'default-role (translation required)': 'default-role',
+ 'title (translation required)': 'title',
+ 'contents (translation required)': 'contents',
+ 'sectnum (translation required)': 'sectnum',
+ 'section-numbering (translation required)': 'sectnum',
+ 'header (translation required)': 'header',
+ 'footer (translation required)': 'footer',
+ # 'footnotes (translation required)': 'footnotes',
+ # 'citations (translation required)': 'citations',
+ 'target-notes (translation required)': 'target-notes',
+ 'restructuredtext-test-directive': 'restructuredtext-test-directive'}
+"""Traditional Chinese name to registered (in directives/__init__.py)
+directive name mapping."""
+
+roles = {
+ # language-dependent: fixed
+ 'abbreviation (translation required)': 'abbreviation',
+ 'ab (translation required)': 'abbreviation',
+ 'acronym (translation required)': 'acronym',
+ 'ac (translation required)': 'acronym',
+ 'code (translation required)': 'code',
+ 'index (translation required)': 'index',
+ 'i (translation required)': 'index',
+ 'subscript (translation required)': 'subscript',
+ 'sub (translation required)': 'subscript',
+ 'superscript (translation required)': 'superscript',
+ 'sup (translation required)': 'superscript',
+ 'title-reference (translation required)': 'title-reference',
+ 'title (translation required)': 'title-reference',
+ 't (translation required)': 'title-reference',
+ 'pep-reference (translation required)': 'pep-reference',
+ 'pep (translation required)': 'pep-reference',
+ 'rfc-reference (translation required)': 'rfc-reference',
+ 'rfc (translation required)': 'rfc-reference',
+ 'emphasis (translation required)': 'emphasis',
+ 'strong (translation required)': 'strong',
+ 'literal (translation required)': 'literal',
+ 'math (translation required)': 'math',
+ 'named-reference (translation required)': 'named-reference',
+ 'anonymous-reference (translation required)': 'anonymous-reference',
+ 'footnote-reference (translation required)': 'footnote-reference',
+ 'citation-reference (translation required)': 'citation-reference',
+ 'substitution-reference (translation required)': 'substitution-reference',
+ 'target (translation required)': 'target',
+ 'uri-reference (translation required)': 'uri-reference',
+ 'uri (translation required)': 'uri-reference',
+ 'url (translation required)': 'uri-reference',
+ 'raw (translation required)': 'raw',
+ }
+"""Mapping of Traditional Chinese role names to canonical role names for
+interpreted text."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/roles.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/roles.py
new file mode 100644
index 00000000..657a86ef
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/roles.py
@@ -0,0 +1,439 @@
+# $Id: roles.py 9037 2022-03-05 23:31:10Z milde $
+# Author: Edward Loper <edloper@gradient.cis.upenn.edu>
+# Copyright: This module has been placed in the public domain.
+
+"""
+This module defines standard interpreted text role functions, a registry for
+interpreted text roles, and an API for adding to and retrieving from the
+registry. See also `Creating reStructuredText Interpreted Text Roles`__.
+
+__ https://docutils.sourceforge.io/docs/ref/rst/roles.html
+
+
+The interface for interpreted role functions is as follows::
+
+ def role_fn(name, rawtext, text, lineno, inliner,
+ options=None, content=None):
+ code...
+
+ # Set function attributes for customization:
+ role_fn.options = ...
+ role_fn.content = ...
+
+Parameters:
+
+- ``name`` is the local name of the interpreted text role, the role name
+ actually used in the document.
+
+- ``rawtext`` is a string containing the entire interpreted text construct.
+ Return it as a ``problematic`` node linked to a system message if there is a
+ problem.
+
+- ``text`` is the interpreted text content, with backslash escapes converted
+ to nulls (``\x00``).
+
+- ``lineno`` is the line number where the text block containing the
+ interpreted text begins.
+
+- ``inliner`` is the Inliner object that called the role function.
+ It defines the following useful attributes: ``reporter``,
+ ``problematic``, ``memo``, ``parent``, ``document``.
+
+- ``options``: A dictionary of directive options for customization, to be
+ interpreted by the role function. Used for additional attributes for the
+ generated elements and other functionality.
+
+- ``content``: A list of strings, the directive content for customization
+ ("role" directive). To be interpreted by the role function.
+
+Function attributes for customization, interpreted by the "role" directive:
+
+- ``options``: A dictionary, mapping known option names to conversion
+ functions such as `int` or `float`. ``None`` or an empty dict implies no
+ options to parse. Several directive option conversion functions are defined
+ in the `directives` module.
+
+ All role functions implicitly support the "class" option, unless disabled
+ with an explicit ``{'class': None}``.
+
+- ``content``: A boolean; true if content is allowed. Client code must handle
+ the case where content is required but not supplied (an empty content list
+ will be supplied).
+
+Note that unlike directives, the "arguments" function attribute is not
+supported for role customization. Directive arguments are handled by the
+"role" directive itself.
+
+Interpreted role functions return a tuple of two values:
+
+- A list of nodes which will be inserted into the document tree at the
+ point where the interpreted role was encountered (can be an empty
+ list).
+
+- A list of system messages, which will be inserted into the document tree
+ immediately after the end of the current inline block (can also be empty).
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from docutils import nodes
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.languages import en as _fallback_language_module
+from docutils.utils.code_analyzer import Lexer, LexerError
+
+DEFAULT_INTERPRETED_ROLE = 'title-reference'
+"""The canonical name of the default interpreted role.
+
+This role is used when no role is specified for a piece of interpreted text.
+"""
+
+_role_registry = {}
+"""Mapping of canonical role names to role functions.
+
+Language-dependent role names are defined in the ``language`` subpackage.
+"""
+
+_roles = {}
+"""Mapping of local or language-dependent interpreted text role names to role
+functions."""
+
+
+def role(role_name, language_module, lineno, reporter):
+ """
+ Locate and return a role function from its language-dependent name, along
+ with a list of system messages.
+
+ If the role is not found in the current language, check English. Return a
+ 2-tuple: role function (``None`` if the named role cannot be found) and a
+ list of system messages.
+ """
+ normname = role_name.lower()
+ messages = []
+ msg_text = []
+
+ if normname in _roles:
+ return _roles[normname], messages
+
+ if role_name:
+ canonicalname = None
+ try:
+ canonicalname = language_module.roles[normname]
+ except AttributeError as error:
+ msg_text.append('Problem retrieving role entry from language '
+ 'module %r: %s.' % (language_module, error))
+ except KeyError:
+ msg_text.append('No role entry for "%s" in module "%s".'
+ % (role_name, language_module.__name__))
+ else:
+ canonicalname = DEFAULT_INTERPRETED_ROLE
+
+ # If we didn't find it, try English as a fallback.
+ if not canonicalname:
+ try:
+ canonicalname = _fallback_language_module.roles[normname]
+ msg_text.append('Using English fallback for role "%s".'
+ % role_name)
+ except KeyError:
+ msg_text.append('Trying "%s" as canonical role name.'
+ % role_name)
+ # The canonical name should be an English name, but just in case:
+ canonicalname = normname
+
+ # Collect any messages that we generated.
+ if msg_text:
+ message = reporter.info('\n'.join(msg_text), line=lineno)
+ messages.append(message)
+
+ # Look the role up in the registry, and return it.
+ if canonicalname in _role_registry:
+ role_fn = _role_registry[canonicalname]
+ register_local_role(normname, role_fn)
+ return role_fn, messages
+ return None, messages # Error message will be generated by caller.
+
+
+def register_canonical_role(name, role_fn):
+ """
+ Register an interpreted text role by its canonical name.
+
+ :Parameters:
+ - `name`: The canonical name of the interpreted role.
+ - `role_fn`: The role function. See the module docstring.
+ """
+ set_implicit_options(role_fn)
+ _role_registry[name.lower()] = role_fn
+
+
+def register_local_role(name, role_fn):
+ """
+ Register an interpreted text role by its local or language-dependent name.
+
+ :Parameters:
+ - `name`: The local or language-dependent name of the interpreted role.
+ - `role_fn`: The role function. See the module docstring.
+ """
+ set_implicit_options(role_fn)
+ _roles[name.lower()] = role_fn
+
+
+def set_implicit_options(role_fn):
+ """
+ Add customization options to role functions, unless explicitly set or
+ disabled.
+ """
+ if not hasattr(role_fn, 'options') or role_fn.options is None:
+ role_fn.options = {'class': directives.class_option}
+ elif 'class' not in role_fn.options:
+ role_fn.options['class'] = directives.class_option
+
+
+def register_generic_role(canonical_name, node_class):
+ """For roles which simply wrap a given `node_class` around the text."""
+ role = GenericRole(canonical_name, node_class)
+ register_canonical_role(canonical_name, role)
+
+
+class GenericRole:
+ """
+ Generic interpreted text role.
+
+ The interpreted text is simply wrapped with the provided node class.
+ """
+
+ def __init__(self, role_name, node_class):
+ self.name = role_name
+ self.node_class = node_class
+
+ def __call__(self, role, rawtext, text, lineno, inliner,
+ options=None, content=None):
+ options = normalized_role_options(options)
+ return [self.node_class(rawtext, text, **options)], []
+
+
+class CustomRole:
+ """Wrapper for custom interpreted text roles."""
+
+ def __init__(self, role_name, base_role, options=None, content=None):
+ self.name = role_name
+ self.base_role = base_role
+ self.options = getattr(base_role, 'options', None)
+ self.content = getattr(base_role, 'content', None)
+ self.supplied_options = options
+ self.supplied_content = content
+
+ def __call__(self, role, rawtext, text, lineno, inliner,
+ options=None, content=None):
+ opts = normalized_role_options(self.supplied_options)
+ try:
+ opts.update(options)
+ except TypeError: # options may be ``None``
+ pass
+ # pass concatenation of content from instance and call argument:
+ supplied_content = self.supplied_content or []
+ content = content or []
+ delimiter = ['\n'] if supplied_content and content else []
+ return self.base_role(role, rawtext, text, lineno, inliner,
+ options=opts,
+ content=supplied_content+delimiter+content)
+
+
+def generic_custom_role(role, rawtext, text, lineno, inliner,
+ options=None, content=None):
+ """Base for custom roles if no other base role is specified."""
+ # Once nested inline markup is implemented, this and other methods should
+ # recursively call inliner.nested_parse().
+ options = normalized_role_options(options)
+ return [nodes.inline(rawtext, text, **options)], []
+
+
+generic_custom_role.options = {'class': directives.class_option}
+
+
+######################################################################
+# Define and register the standard roles:
+######################################################################
+
+register_generic_role('abbreviation', nodes.abbreviation)
+register_generic_role('acronym', nodes.acronym)
+register_generic_role('emphasis', nodes.emphasis)
+register_generic_role('literal', nodes.literal)
+register_generic_role('strong', nodes.strong)
+register_generic_role('subscript', nodes.subscript)
+register_generic_role('superscript', nodes.superscript)
+register_generic_role('title-reference', nodes.title_reference)
+
+
+def pep_reference_role(role, rawtext, text, lineno, inliner,
+ options=None, content=None):
+ options = normalized_role_options(options)
+ try:
+ pepnum = int(nodes.unescape(text))
+ if pepnum < 0 or pepnum > 9999:
+ raise ValueError
+ except ValueError:
+ msg = inliner.reporter.error(
+ 'PEP number must be a number from 0 to 9999; "%s" is invalid.'
+ % text, line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+ # Base URL mainly used by inliner.pep_reference; so this is correct:
+ ref = (inliner.document.settings.pep_base_url
+ + inliner.document.settings.pep_file_url_template % pepnum)
+ return [nodes.reference(rawtext, 'PEP ' + text, refuri=ref, **options)], []
+
+
+register_canonical_role('pep-reference', pep_reference_role)
+
+
+def rfc_reference_role(role, rawtext, text, lineno, inliner,
+ options=None, content=None):
+ options = normalized_role_options(options)
+ if "#" in text:
+ rfcnum, section = nodes.unescape(text).split("#", 1)
+ else:
+ rfcnum, section = nodes.unescape(text), None
+ try:
+ rfcnum = int(rfcnum)
+ if rfcnum < 1:
+ raise ValueError
+ except ValueError:
+ msg = inliner.reporter.error(
+ 'RFC number must be a number greater than or equal to 1; '
+ '"%s" is invalid.' % text, line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+ # Base URL mainly used by inliner.rfc_reference, so this is correct:
+ ref = inliner.document.settings.rfc_base_url + inliner.rfc_url % rfcnum
+ if section is not None:
+ ref += "#" + section
+ node = nodes.reference(rawtext, 'RFC '+str(rfcnum), refuri=ref, **options)
+ return [node], []
+
+
+register_canonical_role('rfc-reference', rfc_reference_role)
+
+
+def raw_role(role, rawtext, text, lineno, inliner, options=None, content=None):
+ options = normalized_role_options(options)
+ if not inliner.document.settings.raw_enabled:
+ msg = inliner.reporter.warning('raw (and derived) roles disabled')
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+ if 'format' not in options:
+ msg = inliner.reporter.error(
+ 'No format (Writer name) is associated with this role: "%s".\n'
+ 'The "raw" role cannot be used directly.\n'
+ 'Instead, use the "role" directive to create a new role with '
+ 'an associated format.' % role, line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+ node = nodes.raw(rawtext, nodes.unescape(text, True), **options)
+ node.source, node.line = inliner.reporter.get_source_and_line(lineno)
+ return [node], []
+
+
+raw_role.options = {'format': directives.unchanged}
+
+register_canonical_role('raw', raw_role)
+
+
+def code_role(role, rawtext, text, lineno, inliner,
+ options=None, content=None):
+ options = normalized_role_options(options)
+ language = options.get('language', '')
+ classes = ['code']
+ if 'classes' in options:
+ classes.extend(options['classes'])
+ if language and language not in classes:
+ classes.append(language)
+ try:
+ tokens = Lexer(nodes.unescape(text, True), language,
+ inliner.document.settings.syntax_highlight)
+ except LexerError as error:
+ msg = inliner.reporter.warning(error)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+
+ node = nodes.literal(rawtext, '', classes=classes)
+
+ # analyse content and add nodes for every token
+ for classes, value in tokens:
+ if classes:
+ node += nodes.inline(value, value, classes=classes)
+ else:
+ # insert as Text to decrease the verbosity of the output
+ node += nodes.Text(value)
+
+ return [node], []
+
+
+code_role.options = {'language': directives.unchanged}
+
+register_canonical_role('code', code_role)
+
+
+def math_role(role, rawtext, text, lineno, inliner,
+ options=None, content=None):
+ options = normalized_role_options(options)
+ text = nodes.unescape(text, True) # raw text without inline role markup
+ node = nodes.math(rawtext, text, **options)
+ return [node], []
+
+
+register_canonical_role('math', math_role)
+
+
+######################################################################
+# Register roles that are currently unimplemented.
+######################################################################
+
+def unimplemented_role(role, rawtext, text, lineno, inliner,
+ options=None, content=None):
+ msg = inliner.reporter.error(
+ 'Interpreted text role "%s" not implemented.' % role, line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+
+
+register_canonical_role('index', unimplemented_role)
+register_canonical_role('named-reference', unimplemented_role)
+register_canonical_role('anonymous-reference', unimplemented_role)
+register_canonical_role('uri-reference', unimplemented_role)
+register_canonical_role('footnote-reference', unimplemented_role)
+register_canonical_role('citation-reference', unimplemented_role)
+register_canonical_role('substitution-reference', unimplemented_role)
+register_canonical_role('target', unimplemented_role)
+
+# This should remain unimplemented, for testing purposes:
+register_canonical_role('restructuredtext-unimplemented-role',
+ unimplemented_role)
+
+
+def set_classes(options):
+ """Deprecated. Obsoleted by ``normalized_role_options()``."""
+ # TODO: Change use in directives.py and uncomment.
+ # warnings.warn('The auxiliary function roles.set_classes() is obsoleted'
+ # ' by roles.normalized_role_options() and will be removed'
+ # ' in Docutils 0.21 or later', DeprecationWarning, stacklevel=2)
+ if options and 'class' in options:
+ assert 'classes' not in options
+ options['classes'] = options['class']
+ del options['class']
+
+
+def normalized_role_options(options):
+ """
+ Return normalized dictionary of role options.
+
+ * ``None`` is replaced by an empty dictionary.
+ * The key 'class' is renamed to 'classes'.
+ """
+ if options is None:
+ return {}
+ result = options.copy()
+ if 'class' in result:
+ assert 'classes' not in result
+ result['classes'] = result['class']
+ del result['class']
+ return result
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/states.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/states.py
new file mode 100644
index 00000000..e2b25c7c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/states.py
@@ -0,0 +1,3145 @@
+# $Id: states.py 9500 2023-12-14 22:38:49Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+This is the ``docutils.parsers.rst.states`` module, the core of
+the reStructuredText parser. It defines the following:
+
+:Classes:
+ - `RSTStateMachine`: reStructuredText parser's entry point.
+ - `NestedStateMachine`: recursive StateMachine.
+ - `RSTState`: reStructuredText State superclass.
+ - `Inliner`: For parsing inline markup.
+ - `Body`: Generic classifier of the first line of a block.
+ - `SpecializedBody`: Superclass for compound element members.
+ - `BulletList`: Second and subsequent bullet_list list_items
+ - `DefinitionList`: Second+ definition_list_items.
+ - `EnumeratedList`: Second+ enumerated_list list_items.
+ - `FieldList`: Second+ fields.
+ - `OptionList`: Second+ option_list_items.
+ - `RFC2822List`: Second+ RFC2822-style fields.
+ - `ExtensionOptions`: Parses directive option fields.
+ - `Explicit`: Second+ explicit markup constructs.
+ - `SubstitutionDef`: For embedded directives in substitution definitions.
+ - `Text`: Classifier of second line of a text block.
+ - `SpecializedText`: Superclass for continuation lines of Text-variants.
+ - `Definition`: Second line of potential definition_list_item.
+ - `Line`: Second line of overlined section title or transition marker.
+ - `Struct`: An auxiliary collection class.
+
+:Exception classes:
+ - `MarkupError`
+ - `ParserError`
+ - `MarkupMismatch`
+
+:Functions:
+ - `escape2null()`: Return a string, escape-backslashes converted to nulls.
+ - `unescape()`: Return a string, nulls removed or restored to backslashes.
+
+:Attributes:
+ - `state_classes`: set of State classes used with `RSTStateMachine`.
+
+Parser Overview
+===============
+
+The reStructuredText parser is implemented as a recursive state machine,
+examining its input one line at a time. To understand how the parser works,
+please first become familiar with the `docutils.statemachine` module. In the
+description below, references are made to classes defined in this module;
+please see the individual classes for details.
+
+Parsing proceeds as follows:
+
+1. The state machine examines each line of input, checking each of the
+ transition patterns of the state `Body`, in order, looking for a match.
+ The implicit transitions (blank lines and indentation) are checked before
+ any others. The 'text' transition is a catch-all (matches anything).
+
+2. The method associated with the matched transition pattern is called.
+
+ A. Some transition methods are self-contained, appending elements to the
+ document tree (`Body.doctest` parses a doctest block). The parser's
+ current line index is advanced to the end of the element, and parsing
+ continues with step 1.
+
+ B. Other transition methods trigger the creation of a nested state machine,
+ whose job is to parse a compound construct ('indent' does a block quote,
+ 'bullet' does a bullet list, 'overline' does a section [first checking
+ for a valid section header], etc.).
+
+ - In the case of lists and explicit markup, a one-off state machine is
+ created and run to parse contents of the first item.
+
+ - A new state machine is created and its initial state is set to the
+ appropriate specialized state (`BulletList` in the case of the
+ 'bullet' transition; see `SpecializedBody` for more detail). This
+ state machine is run to parse the compound element (or series of
+ explicit markup elements), and returns as soon as a non-member element
+ is encountered. For example, the `BulletList` state machine ends as
+ soon as it encounters an element which is not a list item of that
+ bullet list. The optional omission of inter-element blank lines is
+ enabled by this nested state machine.
+
+ - The current line index is advanced to the end of the elements parsed,
+ and parsing continues with step 1.
+
+ C. The result of the 'text' transition depends on the next line of text.
+ The current state is changed to `Text`, under which the second line is
+ examined. If the second line is:
+
+ - Indented: The element is a definition list item, and parsing proceeds
+ similarly to step 2.B, using the `DefinitionList` state.
+
+ - A line of uniform punctuation characters: The element is a section
+ header; again, parsing proceeds as in step 2.B, and `Body` is still
+ used.
+
+ - Anything else: The element is a paragraph, which is examined for
+ inline markup and appended to the parent element. Processing
+ continues with step 1.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import re
+from types import FunctionType, MethodType
+
+from docutils import nodes, statemachine, utils
+from docutils import ApplicationError, DataError
+from docutils.statemachine import StateMachineWS, StateWS
+from docutils.nodes import fully_normalize_name as normalize_name
+from docutils.nodes import unescape, whitespace_normalize_name
+import docutils.parsers.rst
+from docutils.parsers.rst import directives, languages, tableparser, roles
+from docutils.utils import escape2null, column_width
+from docutils.utils import punctuation_chars, roman, urischemes
+from docutils.utils import split_escaped_whitespace
+
+
+class MarkupError(DataError): pass
+class UnknownInterpretedRoleError(DataError): pass
+class InterpretedRoleNotImplementedError(DataError): pass
+class ParserError(ApplicationError): pass
+class MarkupMismatch(Exception): pass
+
+
+class Struct:
+
+ """Stores data attributes for dotted-attribute access."""
+
+ def __init__(self, **keywordargs):
+ self.__dict__.update(keywordargs)
+
+
+class RSTStateMachine(StateMachineWS):
+
+ """
+ reStructuredText's master StateMachine.
+
+ The entry point to reStructuredText parsing is the `run()` method.
+ """
+
+ def run(self, input_lines, document, input_offset=0, match_titles=True,
+ inliner=None):
+ """
+ Parse `input_lines` and modify the `document` node in place.
+
+ Extend `StateMachineWS.run()`: set up parse-global data and
+ run the StateMachine.
+ """
+ self.language = languages.get_language(
+ document.settings.language_code, document.reporter)
+ self.match_titles = match_titles
+ if inliner is None:
+ inliner = Inliner()
+ inliner.init_customizations(document.settings)
+ self.memo = Struct(document=document,
+ reporter=document.reporter,
+ language=self.language,
+ title_styles=[],
+ section_level=0,
+ section_bubble_up_kludge=False,
+ inliner=inliner)
+ self.document = document
+ self.attach_observer(document.note_source)
+ self.reporter = self.memo.reporter
+ self.node = document
+ results = StateMachineWS.run(self, input_lines, input_offset,
+ input_source=document['source'])
+ assert results == [], 'RSTStateMachine.run() results should be empty!'
+ self.node = self.memo = None # remove unneeded references
+
+
+class NestedStateMachine(StateMachineWS):
+
+ """
+ StateMachine run from within other StateMachine runs, to parse nested
+ document structures.
+ """
+
+ def run(self, input_lines, input_offset, memo, node, match_titles=True):
+ """
+ Parse `input_lines` and populate a `docutils.nodes.document` instance.
+
+ Extend `StateMachineWS.run()`: set up document-wide data.
+ """
+ self.match_titles = match_titles
+ self.memo = memo
+ self.document = memo.document
+ self.attach_observer(self.document.note_source)
+ self.reporter = memo.reporter
+ self.language = memo.language
+ self.node = node
+ results = StateMachineWS.run(self, input_lines, input_offset)
+ assert results == [], ('NestedStateMachine.run() results should be '
+ 'empty!')
+ return results
+
+
+class RSTState(StateWS):
+
+ """
+ reStructuredText State superclass.
+
+ Contains methods used by all State subclasses.
+ """
+
+ nested_sm = NestedStateMachine
+ nested_sm_cache = []
+
+ def __init__(self, state_machine, debug=False):
+ self.nested_sm_kwargs = {'state_classes': state_classes,
+ 'initial_state': 'Body'}
+ StateWS.__init__(self, state_machine, debug)
+
+ def runtime_init(self):
+ StateWS.runtime_init(self)
+ memo = self.state_machine.memo
+ self.memo = memo
+ self.reporter = memo.reporter
+ self.inliner = memo.inliner
+ self.document = memo.document
+ self.parent = self.state_machine.node
+ # enable the reporter to determine source and source-line
+ if not hasattr(self.reporter, 'get_source_and_line'):
+ self.reporter.get_source_and_line = self.state_machine.get_source_and_line # noqa:E501
+
+ def goto_line(self, abs_line_offset):
+ """
+ Jump to input line `abs_line_offset`, ignoring jumps past the end.
+ """
+ try:
+ self.state_machine.goto_line(abs_line_offset)
+ except EOFError:
+ pass
+
+ def no_match(self, context, transitions):
+ """
+ Override `StateWS.no_match` to generate a system message.
+
+ This code should never be run.
+ """
+ self.reporter.severe(
+ 'Internal error: no transition pattern match. State: "%s"; '
+ 'transitions: %s; context: %s; current line: %r.'
+ % (self.__class__.__name__, transitions, context,
+ self.state_machine.line))
+ return context, None, []
+
+ def bof(self, context):
+ """Called at beginning of file."""
+ return [], []
+
+ def nested_parse(self, block, input_offset, node, match_titles=False,
+ state_machine_class=None, state_machine_kwargs=None):
+ """
+ Create a new StateMachine rooted at `node` and run it over the input
+ `block`.
+ """
+ use_default = 0
+ if state_machine_class is None:
+ state_machine_class = self.nested_sm
+ use_default += 1
+ if state_machine_kwargs is None:
+ state_machine_kwargs = self.nested_sm_kwargs
+ use_default += 1
+ block_length = len(block)
+
+ state_machine = None
+ if use_default == 2:
+ try:
+ state_machine = self.nested_sm_cache.pop()
+ except IndexError:
+ pass
+ if not state_machine:
+ state_machine = state_machine_class(debug=self.debug,
+ **state_machine_kwargs)
+ state_machine.run(block, input_offset, memo=self.memo,
+ node=node, match_titles=match_titles)
+ if use_default == 2:
+ self.nested_sm_cache.append(state_machine)
+ else:
+ state_machine.unlink()
+ new_offset = state_machine.abs_line_offset()
+ # No `block.parent` implies disconnected -- lines aren't in sync:
+ if block.parent and (len(block) - block_length) != 0:
+ # Adjustment for block if modified in nested parse:
+ self.state_machine.next_line(len(block) - block_length)
+ return new_offset
+
+ def nested_list_parse(self, block, input_offset, node, initial_state,
+ blank_finish,
+ blank_finish_state=None,
+ extra_settings={},
+ match_titles=False,
+ state_machine_class=None,
+ state_machine_kwargs=None):
+ """
+ Create a new StateMachine rooted at `node` and run it over the input
+ `block`. Also keep track of optional intermediate blank lines and the
+ required final one.
+ """
+ if state_machine_class is None:
+ state_machine_class = self.nested_sm
+ if state_machine_kwargs is None:
+ state_machine_kwargs = self.nested_sm_kwargs.copy()
+ state_machine_kwargs['initial_state'] = initial_state
+ state_machine = state_machine_class(debug=self.debug,
+ **state_machine_kwargs)
+ if blank_finish_state is None:
+ blank_finish_state = initial_state
+ state_machine.states[blank_finish_state].blank_finish = blank_finish
+ for key, value in extra_settings.items():
+ setattr(state_machine.states[initial_state], key, value)
+ state_machine.run(block, input_offset, memo=self.memo,
+ node=node, match_titles=match_titles)
+ blank_finish = state_machine.states[blank_finish_state].blank_finish
+ state_machine.unlink()
+ return state_machine.abs_line_offset(), blank_finish
+
+ def section(self, title, source, style, lineno, messages):
+ """Check for a valid subsection and create one if it checks out."""
+ if self.check_subsection(source, style, lineno):
+ self.new_subsection(title, lineno, messages)
+
+ def check_subsection(self, source, style, lineno):
+ """
+ Check for a valid subsection header. Return True or False.
+
+ When a new section is reached that isn't a subsection of the current
+ section, back up the line count (use ``previous_line(-x)``), then
+ ``raise EOFError``. The current StateMachine will finish, then the
+ calling StateMachine can re-examine the title. This will work its way
+ back up the calling chain until the correct section level isreached.
+
+ @@@ Alternative: Evaluate the title, store the title info & level, and
+ back up the chain until that level is reached. Store in memo? Or
+ return in results?
+
+ :Exception: `EOFError` when a sibling or supersection encountered.
+ """
+ memo = self.memo
+ title_styles = memo.title_styles
+ mylevel = memo.section_level
+ try: # check for existing title style
+ level = title_styles.index(style) + 1
+ except ValueError: # new title style
+ if len(title_styles) == memo.section_level: # new subsection
+ title_styles.append(style)
+ return True
+ else: # not at lowest level
+ self.parent += self.title_inconsistent(source, lineno)
+ return False
+ if level <= mylevel: # sibling or supersection
+ memo.section_level = level # bubble up to parent section
+ if len(style) == 2:
+ memo.section_bubble_up_kludge = True
+ # back up 2 lines for underline title, 3 for overline title
+ self.state_machine.previous_line(len(style) + 1)
+ raise EOFError # let parent section re-evaluate
+ if level == mylevel + 1: # immediate subsection
+ return True
+ else: # invalid subsection
+ self.parent += self.title_inconsistent(source, lineno)
+ return False
+
+ def title_inconsistent(self, sourcetext, lineno):
+ error = self.reporter.severe(
+ 'Title level inconsistent:', nodes.literal_block('', sourcetext),
+ line=lineno)
+ return error
+
+ def new_subsection(self, title, lineno, messages):
+ """Append new subsection to document tree. On return, check level."""
+ memo = self.memo
+ mylevel = memo.section_level
+ memo.section_level += 1
+ section_node = nodes.section()
+ self.parent += section_node
+ textnodes, title_messages = self.inline_text(title, lineno)
+ titlenode = nodes.title(title, '', *textnodes)
+ name = normalize_name(titlenode.astext())
+ section_node['names'].append(name)
+ section_node += titlenode
+ section_node += messages
+ section_node += title_messages
+ self.document.note_implicit_target(section_node, section_node)
+ offset = self.state_machine.line_offset + 1
+ absoffset = self.state_machine.abs_line_offset() + 1
+ newabsoffset = self.nested_parse(
+ self.state_machine.input_lines[offset:], input_offset=absoffset,
+ node=section_node, match_titles=True)
+ self.goto_line(newabsoffset)
+ if memo.section_level <= mylevel: # can't handle next section?
+ raise EOFError # bubble up to supersection
+ # reset section_level; next pass will detect it properly
+ memo.section_level = mylevel
+
+ def paragraph(self, lines, lineno):
+ """
+ Return a list (paragraph & messages) & a boolean: literal_block next?
+ """
+ data = '\n'.join(lines).rstrip()
+ if re.search(r'(?<!\\)(\\\\)*::$', data):
+ if len(data) == 2:
+ return [], 1
+ elif data[-3] in ' \n':
+ text = data[:-3].rstrip()
+ else:
+ text = data[:-1]
+ literalnext = 1
+ else:
+ text = data
+ literalnext = 0
+ textnodes, messages = self.inline_text(text, lineno)
+ p = nodes.paragraph(data, '', *textnodes)
+ p.source, p.line = self.state_machine.get_source_and_line(lineno)
+ return [p] + messages, literalnext
+
+ def inline_text(self, text, lineno):
+ """
+ Return 2 lists: nodes (text and inline elements), and system_messages.
+ """
+ nodes, messages = self.inliner.parse(text, lineno,
+ self.memo, self.parent)
+ return nodes, messages
+
+ def unindent_warning(self, node_name):
+ # the actual problem is one line below the current line
+ lineno = self.state_machine.abs_line_number() + 1
+ return self.reporter.warning('%s ends without a blank line; '
+ 'unexpected unindent.' % node_name,
+ line=lineno)
+
+
+def build_regexp(definition, compile=True):
+ """
+ Build, compile and return a regular expression based on `definition`.
+
+ :Parameter: `definition`: a 4-tuple (group name, prefix, suffix, parts),
+ where "parts" is a list of regular expressions and/or regular
+ expression definitions to be joined into an or-group.
+ """
+ name, prefix, suffix, parts = definition
+ part_strings = []
+ for part in parts:
+ if isinstance(part, tuple):
+ part_strings.append(build_regexp(part, None))
+ else:
+ part_strings.append(part)
+ or_group = '|'.join(part_strings)
+ regexp = '%(prefix)s(?P<%(name)s>%(or_group)s)%(suffix)s' % locals()
+ if compile:
+ return re.compile(regexp)
+ else:
+ return regexp
+
+
+class Inliner:
+
+ """
+ Parse inline markup; call the `parse()` method.
+ """
+
+ def __init__(self):
+ self.implicit_dispatch = []
+ """List of (pattern, bound method) tuples, used by
+ `self.implicit_inline`."""
+
+ def init_customizations(self, settings):
+ # lookahead and look-behind expressions for inline markup rules
+ if getattr(settings, 'character_level_inline_markup', False):
+ start_string_prefix = '(^|(?<!\x00))'
+ end_string_suffix = ''
+ else:
+ start_string_prefix = ('(^|(?<=\\s|[%s%s]))' %
+ (punctuation_chars.openers,
+ punctuation_chars.delimiters))
+ end_string_suffix = ('($|(?=\\s|[\x00%s%s%s]))' %
+ (punctuation_chars.closing_delimiters,
+ punctuation_chars.delimiters,
+ punctuation_chars.closers))
+ args = locals().copy()
+ args.update(vars(self.__class__))
+
+ parts = ('initial_inline', start_string_prefix, '',
+ [
+ ('start', '', self.non_whitespace_after, # simple start-strings
+ [r'\*\*', # strong
+ r'\*(?!\*)', # emphasis but not strong
+ r'``', # literal
+ r'_`', # inline internal target
+ r'\|(?!\|)'] # substitution reference
+ ),
+ ('whole', '', end_string_suffix, # whole constructs
+ [ # reference name & end-string
+ r'(?P<refname>%s)(?P<refend>__?)' % self.simplename,
+ ('footnotelabel', r'\[', r'(?P<fnend>\]_)',
+ [r'[0-9]+', # manually numbered
+ r'\#(%s)?' % self.simplename, # auto-numbered (w/ label?)
+ r'\*', # auto-symbol
+ r'(?P<citationlabel>%s)' % self.simplename, # citation ref
+ ]
+ )
+ ]
+ ),
+ ('backquote', # interpreted text or phrase reference
+ '(?P<role>(:%s:)?)' % self.simplename, # optional role
+ self.non_whitespace_after,
+ ['`(?!`)'] # but not literal
+ )
+ ]
+ )
+ self.start_string_prefix = start_string_prefix
+ self.end_string_suffix = end_string_suffix
+ self.parts = parts
+
+ self.patterns = Struct(
+ initial=build_regexp(parts),
+ emphasis=re.compile(self.non_whitespace_escape_before
+ + r'(\*)' + end_string_suffix),
+ strong=re.compile(self.non_whitespace_escape_before
+ + r'(\*\*)' + end_string_suffix),
+ interpreted_or_phrase_ref=re.compile(
+ r"""
+ %(non_unescaped_whitespace_escape_before)s
+ (
+ `
+ (?P<suffix>
+ (?P<role>:%(simplename)s:)?
+ (?P<refend>__?)?
+ )
+ )
+ %(end_string_suffix)s
+ """ % args, re.VERBOSE),
+ embedded_link=re.compile(
+ r"""
+ (
+ (?:[ \n]+|^) # spaces or beginning of line/string
+ < # open bracket
+ %(non_whitespace_after)s
+ (([^<>]|\x00[<>])+) # anything but unescaped angle brackets
+ %(non_whitespace_escape_before)s
+ > # close bracket
+ )
+ $ # end of string
+ """ % args, re.VERBOSE),
+ literal=re.compile(self.non_whitespace_before + '(``)'
+ + end_string_suffix),
+ target=re.compile(self.non_whitespace_escape_before
+ + r'(`)' + end_string_suffix),
+ substitution_ref=re.compile(self.non_whitespace_escape_before
+ + r'(\|_{0,2})'
+ + end_string_suffix),
+ email=re.compile(self.email_pattern % args + '$',
+ re.VERBOSE),
+ uri=re.compile(
+ (r"""
+ %(start_string_prefix)s
+ (?P<whole>
+ (?P<absolute> # absolute URI
+ (?P<scheme> # scheme (http, ftp, mailto)
+ [a-zA-Z][a-zA-Z0-9.+-]*
+ )
+ :
+ (
+ ( # either:
+ (//?)? # hierarchical URI
+ %(uric)s* # URI characters
+ %(uri_end)s # final URI char
+ )
+ ( # optional query
+ \?%(uric)s*
+ %(uri_end)s
+ )?
+ ( # optional fragment
+ \#%(uric)s*
+ %(uri_end)s
+ )?
+ )
+ )
+ | # *OR*
+ (?P<email> # email address
+ """ + self.email_pattern + r"""
+ )
+ )
+ %(end_string_suffix)s
+ """) % args, re.VERBOSE),
+ pep=re.compile(
+ r"""
+ %(start_string_prefix)s
+ (
+ (pep-(?P<pepnum1>\d+)(.txt)?) # reference to source file
+ |
+ (PEP\s+(?P<pepnum2>\d+)) # reference by name
+ )
+ %(end_string_suffix)s""" % args, re.VERBOSE),
+ rfc=re.compile(
+ r"""
+ %(start_string_prefix)s
+ (RFC(-|\s+)?(?P<rfcnum>\d+))
+ %(end_string_suffix)s""" % args, re.VERBOSE))
+
+ self.implicit_dispatch.append((self.patterns.uri,
+ self.standalone_uri))
+ if settings.pep_references:
+ self.implicit_dispatch.append((self.patterns.pep,
+ self.pep_reference))
+ if settings.rfc_references:
+ self.implicit_dispatch.append((self.patterns.rfc,
+ self.rfc_reference))
+
+ def parse(self, text, lineno, memo, parent):
+ # Needs to be refactored for nested inline markup.
+ # Add nested_parse() method?
+ """
+ Return 2 lists: nodes (text and inline elements), and system_messages.
+
+ Using `self.patterns.initial`, a pattern which matches start-strings
+ (emphasis, strong, interpreted, phrase reference, literal,
+ substitution reference, and inline target) and complete constructs
+ (simple reference, footnote reference), search for a candidate. When
+ one is found, check for validity (e.g., not a quoted '*' character).
+ If valid, search for the corresponding end string if applicable, and
+ check it for validity. If not found or invalid, generate a warning
+ and ignore the start-string. Implicit inline markup (e.g. standalone
+ URIs) is found last.
+
+ :text: source string
+ :lineno: absolute line number (cf. statemachine.get_source_and_line())
+ """
+ self.reporter = memo.reporter
+ self.document = memo.document
+ self.language = memo.language
+ self.parent = parent
+ pattern_search = self.patterns.initial.search
+ dispatch = self.dispatch
+ remaining = escape2null(text)
+ processed = []
+ unprocessed = []
+ messages = []
+ while remaining:
+ match = pattern_search(remaining)
+ if match:
+ groups = match.groupdict()
+ method = dispatch[groups['start'] or groups['backquote']
+ or groups['refend'] or groups['fnend']]
+ before, inlines, remaining, sysmessages = method(self, match,
+ lineno)
+ unprocessed.append(before)
+ messages += sysmessages
+ if inlines:
+ processed += self.implicit_inline(''.join(unprocessed),
+ lineno)
+ processed += inlines
+ unprocessed = []
+ else:
+ break
+ remaining = ''.join(unprocessed) + remaining
+ if remaining:
+ processed += self.implicit_inline(remaining, lineno)
+ return processed, messages
+
+ # Inline object recognition
+ # -------------------------
+ # See also init_customizations().
+ non_whitespace_before = r'(?<!\s)'
+ non_whitespace_escape_before = r'(?<![\s\x00])'
+ non_unescaped_whitespace_escape_before = r'(?<!(?<!\x00)[\s\x00])'
+ non_whitespace_after = r'(?!\s)'
+ # Alphanumerics with isolated internal [-._+:] chars (i.e. not 2 together):
+ simplename = r'(?:(?!_)\w)+(?:[-._+:](?:(?!_)\w)+)*'
+ # Valid URI characters (see RFC 2396 & RFC 2732);
+ # final \x00 allows backslash escapes in URIs:
+ uric = r"""[-_.!~*'()[\];/:@&=+$,%a-zA-Z0-9\x00]"""
+ # Delimiter indicating the end of a URI (not part of the URI):
+ uri_end_delim = r"""[>]"""
+ # Last URI character; same as uric but no punctuation:
+ urilast = r"""[_~*/=+a-zA-Z0-9]"""
+ # End of a URI (either 'urilast' or 'uric followed by a
+ # uri_end_delim'):
+ uri_end = r"""(?:%(urilast)s|%(uric)s(?=%(uri_end_delim)s))""" % locals()
+ emailc = r"""[-_!~*'{|}/#?^`&=+$%a-zA-Z0-9\x00]"""
+ email_pattern = r"""
+ %(emailc)s+(?:\.%(emailc)s+)* # name
+ (?<!\x00)@ # at
+ %(emailc)s+(?:\.%(emailc)s*)* # host
+ %(uri_end)s # final URI char
+ """
+
+ def quoted_start(self, match):
+ """Test if inline markup start-string is 'quoted'.
+
+ 'Quoted' in this context means the start-string is enclosed in a pair
+ of matching opening/closing delimiters (not necessarily quotes)
+ or at the end of the match.
+ """
+ string = match.string
+ start = match.start()
+ if start == 0: # start-string at beginning of text
+ return False
+ prestart = string[start - 1]
+ try:
+ poststart = string[match.end()]
+ except IndexError: # start-string at end of text
+ return True # not "quoted" but no markup start-string either
+ return punctuation_chars.match_chars(prestart, poststart)
+
+ def inline_obj(self, match, lineno, end_pattern, nodeclass,
+ restore_backslashes=False):
+ string = match.string
+ matchstart = match.start('start')
+ matchend = match.end('start')
+ if self.quoted_start(match):
+ return string[:matchend], [], string[matchend:], [], ''
+ endmatch = end_pattern.search(string[matchend:])
+ if endmatch and endmatch.start(1): # 1 or more chars
+ text = endmatch.string[:endmatch.start(1)]
+ if restore_backslashes:
+ text = unescape(text, True)
+ textend = matchend + endmatch.end(1)
+ rawsource = unescape(string[matchstart:textend], True)
+ node = nodeclass(rawsource, text)
+ return (string[:matchstart], [node],
+ string[textend:], [], endmatch.group(1))
+ msg = self.reporter.warning(
+ 'Inline %s start-string without end-string.'
+ % nodeclass.__name__, line=lineno)
+ text = unescape(string[matchstart:matchend], True)
+ prb = self.problematic(text, text, msg)
+ return string[:matchstart], [prb], string[matchend:], [msg], ''
+
+ def problematic(self, text, rawsource, message):
+ msgid = self.document.set_id(message, self.parent)
+ problematic = nodes.problematic(rawsource, text, refid=msgid)
+ prbid = self.document.set_id(problematic)
+ message.add_backref(prbid)
+ return problematic
+
+ def emphasis(self, match, lineno):
+ before, inlines, remaining, sysmessages, endstring = self.inline_obj(
+ match, lineno, self.patterns.emphasis, nodes.emphasis)
+ return before, inlines, remaining, sysmessages
+
+ def strong(self, match, lineno):
+ before, inlines, remaining, sysmessages, endstring = self.inline_obj(
+ match, lineno, self.patterns.strong, nodes.strong)
+ return before, inlines, remaining, sysmessages
+
+ def interpreted_or_phrase_ref(self, match, lineno):
+ end_pattern = self.patterns.interpreted_or_phrase_ref
+ string = match.string
+ matchstart = match.start('backquote')
+ matchend = match.end('backquote')
+ rolestart = match.start('role')
+ role = match.group('role')
+ position = ''
+ if role:
+ role = role[1:-1]
+ position = 'prefix'
+ elif self.quoted_start(match):
+ return string[:matchend], [], string[matchend:], []
+ endmatch = end_pattern.search(string[matchend:])
+ if endmatch and endmatch.start(1): # 1 or more chars
+ textend = matchend + endmatch.end()
+ if endmatch.group('role'):
+ if role:
+ msg = self.reporter.warning(
+ 'Multiple roles in interpreted text (both '
+ 'prefix and suffix present; only one allowed).',
+ line=lineno)
+ text = unescape(string[rolestart:textend], True)
+ prb = self.problematic(text, text, msg)
+ return string[:rolestart], [prb], string[textend:], [msg]
+ role = endmatch.group('suffix')[1:-1]
+ position = 'suffix'
+ escaped = endmatch.string[:endmatch.start(1)]
+ rawsource = unescape(string[matchstart:textend], True)
+ if rawsource[-1:] == '_':
+ if role:
+ msg = self.reporter.warning(
+ 'Mismatch: both interpreted text role %s and '
+ 'reference suffix.' % position, line=lineno)
+ text = unescape(string[rolestart:textend], True)
+ prb = self.problematic(text, text, msg)
+ return string[:rolestart], [prb], string[textend:], [msg]
+ return self.phrase_ref(string[:matchstart], string[textend:],
+ rawsource, escaped)
+ else:
+ rawsource = unescape(string[rolestart:textend], True)
+ nodelist, messages = self.interpreted(rawsource, escaped, role,
+ lineno)
+ return (string[:rolestart], nodelist,
+ string[textend:], messages)
+ msg = self.reporter.warning(
+ 'Inline interpreted text or phrase reference start-string '
+ 'without end-string.', line=lineno)
+ text = unescape(string[matchstart:matchend], True)
+ prb = self.problematic(text, text, msg)
+ return string[:matchstart], [prb], string[matchend:], [msg]
+
+ def phrase_ref(self, before, after, rawsource, escaped, text=None):
+ # `text` is ignored (since 0.16)
+ match = self.patterns.embedded_link.search(escaped)
+ if match: # embedded <URI> or <alias_>
+ text = escaped[:match.start(0)]
+ unescaped = unescape(text)
+ rawtext = unescape(text, True)
+ aliastext = match.group(2)
+ rawaliastext = unescape(aliastext, True)
+ underscore_escaped = rawaliastext.endswith(r'\_')
+ if (aliastext.endswith('_')
+ and not (underscore_escaped
+ or self.patterns.uri.match(aliastext))):
+ aliastype = 'name'
+ alias = normalize_name(unescape(aliastext[:-1]))
+ target = nodes.target(match.group(1), refname=alias)
+ target.indirect_reference_name = whitespace_normalize_name(
+ unescape(aliastext[:-1]))
+ else:
+ aliastype = 'uri'
+ # remove unescaped whitespace
+ alias_parts = split_escaped_whitespace(match.group(2))
+ alias = ' '.join(''.join(part.split())
+ for part in alias_parts)
+ alias = self.adjust_uri(unescape(alias))
+ if alias.endswith(r'\_'):
+ alias = alias[:-2] + '_'
+ target = nodes.target(match.group(1), refuri=alias)
+ target.referenced = 1
+ if not aliastext:
+ raise ApplicationError('problem with embedded link: %r'
+ % aliastext)
+ if not text:
+ text = alias
+ unescaped = unescape(text)
+ rawtext = rawaliastext
+ else:
+ text = escaped
+ unescaped = unescape(text)
+ target = None
+ rawtext = unescape(escaped, True)
+
+ refname = normalize_name(unescaped)
+ reference = nodes.reference(rawsource, text,
+ name=whitespace_normalize_name(unescaped))
+ reference[0].rawsource = rawtext
+
+ node_list = [reference]
+
+ if rawsource[-2:] == '__':
+ if target and (aliastype == 'name'):
+ reference['refname'] = alias
+ self.document.note_refname(reference)
+ # self.document.note_indirect_target(target) # required?
+ elif target and (aliastype == 'uri'):
+ reference['refuri'] = alias
+ else:
+ reference['anonymous'] = 1
+ else:
+ if target:
+ target['names'].append(refname)
+ if aliastype == 'name':
+ reference['refname'] = alias
+ self.document.note_indirect_target(target)
+ self.document.note_refname(reference)
+ else:
+ reference['refuri'] = alias
+ self.document.note_explicit_target(target, self.parent)
+ # target.note_referenced_by(name=refname)
+ node_list.append(target)
+ else:
+ reference['refname'] = refname
+ self.document.note_refname(reference)
+ return before, node_list, after, []
+
+ def adjust_uri(self, uri):
+ match = self.patterns.email.match(uri)
+ if match:
+ return 'mailto:' + uri
+ else:
+ return uri
+
+ def interpreted(self, rawsource, text, role, lineno):
+ role_fn, messages = roles.role(role, self.language, lineno,
+ self.reporter)
+ if role_fn:
+ nodes, messages2 = role_fn(role, rawsource, text, lineno, self)
+ return nodes, messages + messages2
+ else:
+ msg = self.reporter.error(
+ 'Unknown interpreted text role "%s".' % role,
+ line=lineno)
+ return ([self.problematic(rawsource, rawsource, msg)],
+ messages + [msg])
+
+ def literal(self, match, lineno):
+ before, inlines, remaining, sysmessages, endstring = self.inline_obj(
+ match, lineno, self.patterns.literal, nodes.literal,
+ restore_backslashes=True)
+ return before, inlines, remaining, sysmessages
+
+ def inline_internal_target(self, match, lineno):
+ before, inlines, remaining, sysmessages, endstring = self.inline_obj(
+ match, lineno, self.patterns.target, nodes.target)
+ if inlines and isinstance(inlines[0], nodes.target):
+ assert len(inlines) == 1
+ target = inlines[0]
+ name = normalize_name(target.astext())
+ target['names'].append(name)
+ self.document.note_explicit_target(target, self.parent)
+ return before, inlines, remaining, sysmessages
+
+ def substitution_reference(self, match, lineno):
+ before, inlines, remaining, sysmessages, endstring = self.inline_obj(
+ match, lineno, self.patterns.substitution_ref,
+ nodes.substitution_reference)
+ if len(inlines) == 1:
+ subref_node = inlines[0]
+ if isinstance(subref_node, nodes.substitution_reference):
+ subref_text = subref_node.astext()
+ self.document.note_substitution_ref(subref_node, subref_text)
+ if endstring[-1:] == '_':
+ reference_node = nodes.reference(
+ '|%s%s' % (subref_text, endstring), '')
+ if endstring[-2:] == '__':
+ reference_node['anonymous'] = 1
+ else:
+ reference_node['refname'] = normalize_name(subref_text)
+ self.document.note_refname(reference_node)
+ reference_node += subref_node
+ inlines = [reference_node]
+ return before, inlines, remaining, sysmessages
+
+ def footnote_reference(self, match, lineno):
+ """
+ Handles `nodes.footnote_reference` and `nodes.citation_reference`
+ elements.
+ """
+ label = match.group('footnotelabel')
+ refname = normalize_name(label)
+ string = match.string
+ before = string[:match.start('whole')]
+ remaining = string[match.end('whole'):]
+ if match.group('citationlabel'):
+ refnode = nodes.citation_reference('[%s]_' % label,
+ refname=refname)
+ refnode += nodes.Text(label)
+ self.document.note_citation_ref(refnode)
+ else:
+ refnode = nodes.footnote_reference('[%s]_' % label)
+ if refname[0] == '#':
+ refname = refname[1:]
+ refnode['auto'] = 1
+ self.document.note_autofootnote_ref(refnode)
+ elif refname == '*':
+ refname = ''
+ refnode['auto'] = '*'
+ self.document.note_symbol_footnote_ref(
+ refnode)
+ else:
+ refnode += nodes.Text(label)
+ if refname:
+ refnode['refname'] = refname
+ self.document.note_footnote_ref(refnode)
+ if utils.get_trim_footnote_ref_space(self.document.settings):
+ before = before.rstrip()
+ return before, [refnode], remaining, []
+
+ def reference(self, match, lineno, anonymous=False):
+ referencename = match.group('refname')
+ refname = normalize_name(referencename)
+ referencenode = nodes.reference(
+ referencename + match.group('refend'), referencename,
+ name=whitespace_normalize_name(referencename))
+ referencenode[0].rawsource = referencename
+ if anonymous:
+ referencenode['anonymous'] = 1
+ else:
+ referencenode['refname'] = refname
+ self.document.note_refname(referencenode)
+ string = match.string
+ matchstart = match.start('whole')
+ matchend = match.end('whole')
+ return string[:matchstart], [referencenode], string[matchend:], []
+
+ def anonymous_reference(self, match, lineno):
+ return self.reference(match, lineno, anonymous=True)
+
+ def standalone_uri(self, match, lineno):
+ if (not match.group('scheme')
+ or match.group('scheme').lower() in urischemes.schemes):
+ if match.group('email'):
+ addscheme = 'mailto:'
+ else:
+ addscheme = ''
+ text = match.group('whole')
+ refuri = addscheme + unescape(text)
+ reference = nodes.reference(unescape(text, True), text,
+ refuri=refuri)
+ return [reference]
+ else: # not a valid scheme
+ raise MarkupMismatch
+
+ def pep_reference(self, match, lineno):
+ text = match.group(0)
+ if text.startswith('pep-'):
+ pepnum = int(unescape(match.group('pepnum1')))
+ elif text.startswith('PEP'):
+ pepnum = int(unescape(match.group('pepnum2')))
+ else:
+ raise MarkupMismatch
+ ref = (self.document.settings.pep_base_url
+ + self.document.settings.pep_file_url_template % pepnum)
+ return [nodes.reference(unescape(text, True), text, refuri=ref)]
+
+ rfc_url = 'rfc%d.html'
+
+ def rfc_reference(self, match, lineno):
+ text = match.group(0)
+ if text.startswith('RFC'):
+ rfcnum = int(unescape(match.group('rfcnum')))
+ ref = self.document.settings.rfc_base_url + self.rfc_url % rfcnum
+ else:
+ raise MarkupMismatch
+ return [nodes.reference(unescape(text, True), text, refuri=ref)]
+
+ def implicit_inline(self, text, lineno):
+ """
+ Check each of the patterns in `self.implicit_dispatch` for a match,
+ and dispatch to the stored method for the pattern. Recursively check
+ the text before and after the match. Return a list of `nodes.Text`
+ and inline element nodes.
+ """
+ if not text:
+ return []
+ for pattern, method in self.implicit_dispatch:
+ match = pattern.search(text)
+ if match:
+ try:
+ # Must recurse on strings before *and* after the match;
+ # there may be multiple patterns.
+ return (self.implicit_inline(text[:match.start()], lineno)
+ + method(match, lineno)
+ + self.implicit_inline(text[match.end():], lineno))
+ except MarkupMismatch:
+ pass
+ return [nodes.Text(text)]
+
+ dispatch = {'*': emphasis,
+ '**': strong,
+ '`': interpreted_or_phrase_ref,
+ '``': literal,
+ '_`': inline_internal_target,
+ ']_': footnote_reference,
+ '|': substitution_reference,
+ '_': reference,
+ '__': anonymous_reference}
+
+
+def _loweralpha_to_int(s, _zero=(ord('a')-1)):
+ return ord(s) - _zero
+
+
+def _upperalpha_to_int(s, _zero=(ord('A')-1)):
+ return ord(s) - _zero
+
+
+def _lowerroman_to_int(s):
+ return roman.fromRoman(s.upper())
+
+
+class Body(RSTState):
+
+ """
+ Generic classifier of the first line of a block.
+ """
+
+ double_width_pad_char = tableparser.TableParser.double_width_pad_char
+ """Padding character for East Asian double-width text."""
+
+ enum = Struct()
+ """Enumerated list parsing information."""
+
+ enum.formatinfo = {
+ 'parens': Struct(prefix='(', suffix=')', start=1, end=-1),
+ 'rparen': Struct(prefix='', suffix=')', start=0, end=-1),
+ 'period': Struct(prefix='', suffix='.', start=0, end=-1)}
+ enum.formats = enum.formatinfo.keys()
+ enum.sequences = ['arabic', 'loweralpha', 'upperalpha',
+ 'lowerroman', 'upperroman'] # ORDERED!
+ enum.sequencepats = {'arabic': '[0-9]+',
+ 'loweralpha': '[a-z]',
+ 'upperalpha': '[A-Z]',
+ 'lowerroman': '[ivxlcdm]+',
+ 'upperroman': '[IVXLCDM]+'}
+ enum.converters = {'arabic': int,
+ 'loweralpha': _loweralpha_to_int,
+ 'upperalpha': _upperalpha_to_int,
+ 'lowerroman': _lowerroman_to_int,
+ 'upperroman': roman.fromRoman}
+
+ enum.sequenceregexps = {}
+ for sequence in enum.sequences:
+ enum.sequenceregexps[sequence] = re.compile(
+ enum.sequencepats[sequence] + '$')
+
+ grid_table_top_pat = re.compile(r'\+-[-+]+-\+ *$')
+ """Matches the top (& bottom) of a full table)."""
+
+ simple_table_top_pat = re.compile('=+( +=+)+ *$')
+ """Matches the top of a simple table."""
+
+ simple_table_border_pat = re.compile('=+[ =]*$')
+ """Matches the bottom & header bottom of a simple table."""
+
+ pats = {}
+ """Fragments of patterns used by transitions."""
+
+ pats['nonalphanum7bit'] = '[!-/:-@[-`{-~]'
+ pats['alpha'] = '[a-zA-Z]'
+ pats['alphanum'] = '[a-zA-Z0-9]'
+ pats['alphanumplus'] = '[a-zA-Z0-9_-]'
+ pats['enum'] = ('(%(arabic)s|%(loweralpha)s|%(upperalpha)s|%(lowerroman)s'
+ '|%(upperroman)s|#)' % enum.sequencepats)
+ pats['optname'] = '%(alphanum)s%(alphanumplus)s*' % pats
+ # @@@ Loosen up the pattern? Allow Unicode?
+ pats['optarg'] = '(%(alpha)s%(alphanumplus)s*|<[^<>]+>)' % pats
+ pats['shortopt'] = r'(-|\+)%(alphanum)s( ?%(optarg)s)?' % pats
+ pats['longopt'] = r'(--|/)%(optname)s([ =]%(optarg)s)?' % pats
+ pats['option'] = r'(%(shortopt)s|%(longopt)s)' % pats
+
+ for format in enum.formats:
+ pats[format] = '(?P<%s>%s%s%s)' % (
+ format, re.escape(enum.formatinfo[format].prefix),
+ pats['enum'], re.escape(enum.formatinfo[format].suffix))
+
+ patterns = {
+ 'bullet': '[-+*\u2022\u2023\u2043]( +|$)',
+ 'enumerator': r'(%(parens)s|%(rparen)s|%(period)s)( +|$)' % pats,
+ 'field_marker': r':(?![: ])([^:\\]|\\.|:(?!([ `]|$)))*(?<! ):( +|$)',
+ 'option_marker': r'%(option)s(, %(option)s)*( +| ?$)' % pats,
+ 'doctest': r'>>>( +|$)',
+ 'line_block': r'\|( +|$)',
+ 'grid_table_top': grid_table_top_pat,
+ 'simple_table_top': simple_table_top_pat,
+ 'explicit_markup': r'\.\.( +|$)',
+ 'anonymous': r'__( +|$)',
+ 'line': r'(%(nonalphanum7bit)s)\1* *$' % pats,
+ 'text': r''}
+ initial_transitions = (
+ 'bullet',
+ 'enumerator',
+ 'field_marker',
+ 'option_marker',
+ 'doctest',
+ 'line_block',
+ 'grid_table_top',
+ 'simple_table_top',
+ 'explicit_markup',
+ 'anonymous',
+ 'line',
+ 'text')
+
+ def indent(self, match, context, next_state):
+ """Block quote."""
+ (indented, indent, line_offset, blank_finish
+ ) = self.state_machine.get_indented()
+ elements = self.block_quote(indented, line_offset)
+ self.parent += elements
+ if not blank_finish:
+ self.parent += self.unindent_warning('Block quote')
+ return context, next_state, []
+
+ def block_quote(self, indented, line_offset):
+ elements = []
+ while indented:
+ blockquote = nodes.block_quote(rawsource='\n'.join(indented))
+ (blockquote.source, blockquote.line
+ ) = self.state_machine.get_source_and_line(line_offset+1)
+ (blockquote_lines,
+ attribution_lines,
+ attribution_offset,
+ indented,
+ new_line_offset) = self.split_attribution(indented, line_offset)
+ self.nested_parse(blockquote_lines, line_offset, blockquote)
+ elements.append(blockquote)
+ if attribution_lines:
+ attribution, messages = self.parse_attribution(
+ attribution_lines, line_offset+attribution_offset)
+ blockquote += attribution
+ elements += messages
+ line_offset = new_line_offset
+ while indented and not indented[0]:
+ indented = indented[1:]
+ line_offset += 1
+ return elements
+
+ # U+2014 is an em-dash:
+ attribution_pattern = re.compile('(---?(?!-)|\u2014) *(?=[^ \\n])')
+
+ def split_attribution(self, indented, line_offset):
+ """
+ Check for a block quote attribution and split it off:
+
+ * First line after a blank line must begin with a dash ("--", "---",
+ em-dash; matches `self.attribution_pattern`).
+ * Every line after that must have consistent indentation.
+ * Attributions must be preceded by block quote content.
+
+ Return a tuple of: (block quote content lines, attribution lines,
+ attribution offset, remaining indented lines, remaining lines offset).
+ """
+ blank = None
+ nonblank_seen = False
+ for i in range(len(indented)):
+ line = indented[i].rstrip()
+ if line:
+ if nonblank_seen and blank == i - 1: # last line blank
+ match = self.attribution_pattern.match(line)
+ if match:
+ attribution_end, indent = self.check_attribution(
+ indented, i)
+ if attribution_end:
+ a_lines = indented[i:attribution_end]
+ a_lines.trim_left(match.end(), end=1)
+ a_lines.trim_left(indent, start=1)
+ return (indented[:i], a_lines,
+ i, indented[attribution_end:],
+ line_offset + attribution_end)
+ nonblank_seen = True
+ else:
+ blank = i
+ else:
+ return indented, None, None, None, None
+
+ def check_attribution(self, indented, attribution_start):
+ """
+ Check attribution shape.
+ Return the index past the end of the attribution, and the indent.
+ """
+ indent = None
+ i = attribution_start + 1
+ for i in range(attribution_start + 1, len(indented)):
+ line = indented[i].rstrip()
+ if not line:
+ break
+ if indent is None:
+ indent = len(line) - len(line.lstrip())
+ elif len(line) - len(line.lstrip()) != indent:
+ return None, None # bad shape; not an attribution
+ else:
+ # return index of line after last attribution line:
+ i += 1
+ return i, (indent or 0)
+
+ def parse_attribution(self, indented, line_offset):
+ text = '\n'.join(indented).rstrip()
+ lineno = 1 + line_offset # line_offset is zero-based
+ textnodes, messages = self.inline_text(text, lineno)
+ node = nodes.attribution(text, '', *textnodes)
+ node.source, node.line = self.state_machine.get_source_and_line(lineno)
+ return node, messages
+
+ def bullet(self, match, context, next_state):
+ """Bullet list item."""
+ ul = nodes.bullet_list()
+ ul.source, ul.line = self.state_machine.get_source_and_line()
+ self.parent += ul
+ ul['bullet'] = match.string[0]
+ i, blank_finish = self.list_item(match.end())
+ ul += i
+ offset = self.state_machine.line_offset + 1 # next line
+ new_line_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=ul, initial_state='BulletList',
+ blank_finish=blank_finish)
+ self.goto_line(new_line_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Bullet list')
+ return [], next_state, []
+
+ def list_item(self, indent):
+ src, srcline = self.state_machine.get_source_and_line()
+ if self.state_machine.line[indent:]:
+ indented, line_offset, blank_finish = (
+ self.state_machine.get_known_indented(indent))
+ else:
+ indented, indent, line_offset, blank_finish = (
+ self.state_machine.get_first_known_indented(indent))
+ listitem = nodes.list_item('\n'.join(indented))
+ listitem.source, listitem.line = src, srcline
+ if indented:
+ self.nested_parse(indented, input_offset=line_offset,
+ node=listitem)
+ return listitem, blank_finish
+
+ def enumerator(self, match, context, next_state):
+ """Enumerated List Item"""
+ format, sequence, text, ordinal = self.parse_enumerator(match)
+ if not self.is_enumerated_list_item(ordinal, sequence, format):
+ raise statemachine.TransitionCorrection('text')
+ enumlist = nodes.enumerated_list()
+ self.parent += enumlist
+ if sequence == '#':
+ enumlist['enumtype'] = 'arabic'
+ else:
+ enumlist['enumtype'] = sequence
+ enumlist['prefix'] = self.enum.formatinfo[format].prefix
+ enumlist['suffix'] = self.enum.formatinfo[format].suffix
+ if ordinal != 1:
+ enumlist['start'] = ordinal
+ msg = self.reporter.info(
+ 'Enumerated list start value not ordinal-1: "%s" (ordinal %s)'
+ % (text, ordinal))
+ self.parent += msg
+ listitem, blank_finish = self.list_item(match.end())
+ enumlist += listitem
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=enumlist, initial_state='EnumeratedList',
+ blank_finish=blank_finish,
+ extra_settings={'lastordinal': ordinal,
+ 'format': format,
+ 'auto': sequence == '#'})
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Enumerated list')
+ return [], next_state, []
+
+ def parse_enumerator(self, match, expected_sequence=None):
+ """
+ Analyze an enumerator and return the results.
+
+ :Return:
+ - the enumerator format ('period', 'parens', or 'rparen'),
+ - the sequence used ('arabic', 'loweralpha', 'upperroman', etc.),
+ - the text of the enumerator, stripped of formatting, and
+ - the ordinal value of the enumerator ('a' -> 1, 'ii' -> 2, etc.;
+ ``None`` is returned for invalid enumerator text).
+
+ The enumerator format has already been determined by the regular
+ expression match. If `expected_sequence` is given, that sequence is
+ tried first. If not, we check for Roman numeral 1. This way,
+ single-character Roman numerals (which are also alphabetical) can be
+ matched. If no sequence has been matched, all sequences are checked in
+ order.
+ """
+ groupdict = match.groupdict()
+ sequence = ''
+ for format in self.enum.formats:
+ if groupdict[format]: # was this the format matched?
+ break # yes; keep `format`
+ else: # shouldn't happen
+ raise ParserError('enumerator format not matched')
+ text = groupdict[format][self.enum.formatinfo[format].start # noqa: E203,E501
+ : self.enum.formatinfo[format].end]
+ if text == '#':
+ sequence = '#'
+ elif expected_sequence:
+ try:
+ if self.enum.sequenceregexps[expected_sequence].match(text):
+ sequence = expected_sequence
+ except KeyError: # shouldn't happen
+ raise ParserError('unknown enumerator sequence: %s'
+ % sequence)
+ elif text == 'i':
+ sequence = 'lowerroman'
+ elif text == 'I':
+ sequence = 'upperroman'
+ if not sequence:
+ for sequence in self.enum.sequences:
+ if self.enum.sequenceregexps[sequence].match(text):
+ break
+ else: # shouldn't happen
+ raise ParserError('enumerator sequence not matched')
+ if sequence == '#':
+ ordinal = 1
+ else:
+ try:
+ ordinal = self.enum.converters[sequence](text)
+ except roman.InvalidRomanNumeralError:
+ ordinal = None
+ return format, sequence, text, ordinal
+
+ def is_enumerated_list_item(self, ordinal, sequence, format):
+ """
+ Check validity based on the ordinal value and the second line.
+
+ Return true if the ordinal is valid and the second line is blank,
+ indented, or starts with the next enumerator or an auto-enumerator.
+ """
+ if ordinal is None:
+ return None
+ try:
+ next_line = self.state_machine.next_line()
+ except EOFError: # end of input lines
+ self.state_machine.previous_line()
+ return 1
+ else:
+ self.state_machine.previous_line()
+ if not next_line[:1].strip(): # blank or indented
+ return 1
+ result = self.make_enumerator(ordinal + 1, sequence, format)
+ if result:
+ next_enumerator, auto_enumerator = result
+ try:
+ if (next_line.startswith(next_enumerator)
+ or next_line.startswith(auto_enumerator)):
+ return 1
+ except TypeError:
+ pass
+ return None
+
+ def make_enumerator(self, ordinal, sequence, format):
+ """
+ Construct and return the next enumerated list item marker, and an
+ auto-enumerator ("#" instead of the regular enumerator).
+
+ Return ``None`` for invalid (out of range) ordinals.
+ """
+ if sequence == '#':
+ enumerator = '#'
+ elif sequence == 'arabic':
+ enumerator = str(ordinal)
+ else:
+ if sequence.endswith('alpha'):
+ if ordinal > 26:
+ return None
+ enumerator = chr(ordinal + ord('a') - 1)
+ elif sequence.endswith('roman'):
+ try:
+ enumerator = roman.toRoman(ordinal)
+ except roman.RomanError:
+ return None
+ else: # shouldn't happen
+ raise ParserError('unknown enumerator sequence: "%s"'
+ % sequence)
+ if sequence.startswith('lower'):
+ enumerator = enumerator.lower()
+ elif sequence.startswith('upper'):
+ enumerator = enumerator.upper()
+ else: # shouldn't happen
+ raise ParserError('unknown enumerator sequence: "%s"'
+ % sequence)
+ formatinfo = self.enum.formatinfo[format]
+ next_enumerator = (formatinfo.prefix + enumerator + formatinfo.suffix
+ + ' ')
+ auto_enumerator = formatinfo.prefix + '#' + formatinfo.suffix + ' '
+ return next_enumerator, auto_enumerator
+
+ def field_marker(self, match, context, next_state):
+ """Field list item."""
+ field_list = nodes.field_list()
+ self.parent += field_list
+ field, blank_finish = self.field(match)
+ field_list += field
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=field_list, initial_state='FieldList',
+ blank_finish=blank_finish)
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Field list')
+ return [], next_state, []
+
+ def field(self, match):
+ name = self.parse_field_marker(match)
+ src, srcline = self.state_machine.get_source_and_line()
+ lineno = self.state_machine.abs_line_number()
+ (indented, indent, line_offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end())
+ field_node = nodes.field()
+ field_node.source = src
+ field_node.line = srcline
+ name_nodes, name_messages = self.inline_text(name, lineno)
+ field_node += nodes.field_name(name, '', *name_nodes)
+ field_body = nodes.field_body('\n'.join(indented), *name_messages)
+ field_node += field_body
+ if indented:
+ self.parse_field_body(indented, line_offset, field_body)
+ return field_node, blank_finish
+
+ def parse_field_marker(self, match):
+ """Extract & return field name from a field marker match."""
+ field = match.group()[1:] # strip off leading ':'
+ field = field[:field.rfind(':')] # strip off trailing ':' etc.
+ return field
+
+ def parse_field_body(self, indented, offset, node):
+ self.nested_parse(indented, input_offset=offset, node=node)
+
+ def option_marker(self, match, context, next_state):
+ """Option list item."""
+ optionlist = nodes.option_list()
+ (optionlist.source, optionlist.line
+ ) = self.state_machine.get_source_and_line()
+ try:
+ listitem, blank_finish = self.option_list_item(match)
+ except MarkupError as error:
+ # This shouldn't happen; pattern won't match.
+ msg = self.reporter.error('Invalid option list marker: %s'
+ % error)
+ self.parent += msg
+ (indented, indent, line_offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end())
+ elements = self.block_quote(indented, line_offset)
+ self.parent += elements
+ if not blank_finish:
+ self.parent += self.unindent_warning('Option list')
+ return [], next_state, []
+ self.parent += optionlist
+ optionlist += listitem
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=optionlist, initial_state='OptionList',
+ blank_finish=blank_finish)
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Option list')
+ return [], next_state, []
+
+ def option_list_item(self, match):
+ offset = self.state_machine.abs_line_offset()
+ options = self.parse_option_marker(match)
+ (indented, indent, line_offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end())
+ if not indented: # not an option list item
+ self.goto_line(offset)
+ raise statemachine.TransitionCorrection('text')
+ option_group = nodes.option_group('', *options)
+ description = nodes.description('\n'.join(indented))
+ option_list_item = nodes.option_list_item('', option_group,
+ description)
+ if indented:
+ self.nested_parse(indented, input_offset=line_offset,
+ node=description)
+ return option_list_item, blank_finish
+
+ def parse_option_marker(self, match):
+ """
+ Return a list of `node.option` and `node.option_argument` objects,
+ parsed from an option marker match.
+
+ :Exception: `MarkupError` for invalid option markers.
+ """
+ optlist = []
+ # split at ", ", except inside < > (complex arguments)
+ optionstrings = re.split(r', (?![^<]*>)', match.group().rstrip())
+ for optionstring in optionstrings:
+ tokens = optionstring.split()
+ delimiter = ' '
+ firstopt = tokens[0].split('=', 1)
+ if len(firstopt) > 1:
+ # "--opt=value" form
+ tokens[:1] = firstopt
+ delimiter = '='
+ elif (len(tokens[0]) > 2
+ and ((tokens[0].startswith('-')
+ and not tokens[0].startswith('--'))
+ or tokens[0].startswith('+'))):
+ # "-ovalue" form
+ tokens[:1] = [tokens[0][:2], tokens[0][2:]]
+ delimiter = ''
+ if len(tokens) > 1 and (tokens[1].startswith('<')
+ and tokens[-1].endswith('>')):
+ # "-o <value1 value2>" form; join all values into one token
+ tokens[1:] = [' '.join(tokens[1:])]
+ if 0 < len(tokens) <= 2:
+ option = nodes.option(optionstring)
+ option += nodes.option_string(tokens[0], tokens[0])
+ if len(tokens) > 1:
+ option += nodes.option_argument(tokens[1], tokens[1],
+ delimiter=delimiter)
+ optlist.append(option)
+ else:
+ raise MarkupError(
+ 'wrong number of option tokens (=%s), should be 1 or 2: '
+ '"%s"' % (len(tokens), optionstring))
+ return optlist
+
+ def doctest(self, match, context, next_state):
+ data = '\n'.join(self.state_machine.get_text_block())
+ # TODO: prepend class value ['pycon'] (Python Console)
+ # parse with `directives.body.CodeBlock` (returns literal-block
+ # with class "code" and syntax highlight markup).
+ self.parent += nodes.doctest_block(data, data)
+ return [], next_state, []
+
+ def line_block(self, match, context, next_state):
+ """First line of a line block."""
+ block = nodes.line_block()
+ self.parent += block
+ lineno = self.state_machine.abs_line_number()
+ line, messages, blank_finish = self.line_block_line(match, lineno)
+ block += line
+ self.parent += messages
+ if not blank_finish:
+ offset = self.state_machine.line_offset + 1 # next line
+ new_line_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=block, initial_state='LineBlock',
+ blank_finish=0)
+ self.goto_line(new_line_offset)
+ if not blank_finish:
+ self.parent += self.reporter.warning(
+ 'Line block ends without a blank line.',
+ line=lineno+1)
+ if len(block):
+ if block[0].indent is None:
+ block[0].indent = 0
+ self.nest_line_block_lines(block)
+ return [], next_state, []
+
+ def line_block_line(self, match, lineno):
+ """Return one line element of a line_block."""
+ (indented, indent, line_offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end(),
+ until_blank=True)
+ text = '\n'.join(indented)
+ text_nodes, messages = self.inline_text(text, lineno)
+ line = nodes.line(text, '', *text_nodes)
+ if match.string.rstrip() != '|': # not empty
+ line.indent = len(match.group(1)) - 1
+ return line, messages, blank_finish
+
+ def nest_line_block_lines(self, block):
+ for index in range(1, len(block)):
+ if getattr(block[index], 'indent', None) is None:
+ block[index].indent = block[index - 1].indent
+ self.nest_line_block_segment(block)
+
+ def nest_line_block_segment(self, block):
+ indents = [item.indent for item in block]
+ least = min(indents)
+ new_items = []
+ new_block = nodes.line_block()
+ for item in block:
+ if item.indent > least:
+ new_block.append(item)
+ else:
+ if len(new_block):
+ self.nest_line_block_segment(new_block)
+ new_items.append(new_block)
+ new_block = nodes.line_block()
+ new_items.append(item)
+ if len(new_block):
+ self.nest_line_block_segment(new_block)
+ new_items.append(new_block)
+ block[:] = new_items
+
+ def grid_table_top(self, match, context, next_state):
+ """Top border of a full table."""
+ return self.table_top(match, context, next_state,
+ self.isolate_grid_table,
+ tableparser.GridTableParser)
+
+ def simple_table_top(self, match, context, next_state):
+ """Top border of a simple table."""
+ return self.table_top(match, context, next_state,
+ self.isolate_simple_table,
+ tableparser.SimpleTableParser)
+
+ def table_top(self, match, context, next_state,
+ isolate_function, parser_class):
+ """Top border of a generic table."""
+ nodelist, blank_finish = self.table(isolate_function, parser_class)
+ self.parent += nodelist
+ if not blank_finish:
+ msg = self.reporter.warning(
+ 'Blank line required after table.',
+ line=self.state_machine.abs_line_number()+1)
+ self.parent += msg
+ return [], next_state, []
+
+ def table(self, isolate_function, parser_class):
+ """Parse a table."""
+ block, messages, blank_finish = isolate_function()
+ if block:
+ try:
+ parser = parser_class()
+ tabledata = parser.parse(block)
+ tableline = (self.state_machine.abs_line_number() - len(block)
+ + 1)
+ table = self.build_table(tabledata, tableline)
+ nodelist = [table] + messages
+ except tableparser.TableMarkupError as err:
+ nodelist = self.malformed_table(block, ' '.join(err.args),
+ offset=err.offset) + messages
+ else:
+ nodelist = messages
+ return nodelist, blank_finish
+
+ def isolate_grid_table(self):
+ messages = []
+ blank_finish = 1
+ try:
+ block = self.state_machine.get_text_block(flush_left=True)
+ except statemachine.UnexpectedIndentationError as err:
+ block, src, srcline = err.args
+ messages.append(self.reporter.error('Unexpected indentation.',
+ source=src, line=srcline))
+ blank_finish = 0
+ block.disconnect()
+ # for East Asian chars:
+ block.pad_double_width(self.double_width_pad_char)
+ width = len(block[0].strip())
+ for i in range(len(block)):
+ block[i] = block[i].strip()
+ if block[i][0] not in '+|': # check left edge
+ blank_finish = 0
+ self.state_machine.previous_line(len(block) - i)
+ del block[i:]
+ break
+ if not self.grid_table_top_pat.match(block[-1]): # find bottom
+ blank_finish = 0
+ # from second-last to third line of table:
+ for i in range(len(block) - 2, 1, -1):
+ if self.grid_table_top_pat.match(block[i]):
+ self.state_machine.previous_line(len(block) - i + 1)
+ del block[i+1:]
+ break
+ else:
+ messages.extend(self.malformed_table(block))
+ return [], messages, blank_finish
+ for i in range(len(block)): # check right edge
+ if len(block[i]) != width or block[i][-1] not in '+|':
+ messages.extend(self.malformed_table(block))
+ return [], messages, blank_finish
+ return block, messages, blank_finish
+
+ def isolate_simple_table(self):
+ start = self.state_machine.line_offset
+ lines = self.state_machine.input_lines
+ limit = len(lines) - 1
+ toplen = len(lines[start].strip())
+ pattern_match = self.simple_table_border_pat.match
+ found = 0
+ found_at = None
+ i = start + 1
+ while i <= limit:
+ line = lines[i]
+ match = pattern_match(line)
+ if match:
+ if len(line.strip()) != toplen:
+ self.state_machine.next_line(i - start)
+ messages = self.malformed_table(
+ lines[start:i+1], 'Bottom/header table border does '
+ 'not match top border.')
+ return [], messages, i == limit or not lines[i+1].strip()
+ found += 1
+ found_at = i
+ if found == 2 or i == limit or not lines[i+1].strip():
+ end = i
+ break
+ i += 1
+ else: # reached end of input_lines
+ if found:
+ extra = ' or no blank line after table bottom'
+ self.state_machine.next_line(found_at - start)
+ block = lines[start:found_at+1]
+ else:
+ extra = ''
+ self.state_machine.next_line(i - start - 1)
+ block = lines[start:]
+ messages = self.malformed_table(
+ block, 'No bottom table border found%s.' % extra)
+ return [], messages, not extra
+ self.state_machine.next_line(end - start)
+ block = lines[start:end+1]
+ # for East Asian chars:
+ block.pad_double_width(self.double_width_pad_char)
+ return block, [], end == limit or not lines[end+1].strip()
+
+ def malformed_table(self, block, detail='', offset=0):
+ block.replace(self.double_width_pad_char, '')
+ data = '\n'.join(block)
+ message = 'Malformed table.'
+ startline = self.state_machine.abs_line_number() - len(block) + 1
+ if detail:
+ message += '\n' + detail
+ error = self.reporter.error(message, nodes.literal_block(data, data),
+ line=startline+offset)
+ return [error]
+
+ def build_table(self, tabledata, tableline, stub_columns=0, widths=None):
+ colwidths, headrows, bodyrows = tabledata
+ table = nodes.table()
+ if widths == 'auto':
+ table['classes'] += ['colwidths-auto']
+ elif widths: # "grid" or list of integers
+ table['classes'] += ['colwidths-given']
+ tgroup = nodes.tgroup(cols=len(colwidths))
+ table += tgroup
+ for colwidth in colwidths:
+ colspec = nodes.colspec(colwidth=colwidth)
+ if stub_columns:
+ colspec.attributes['stub'] = 1
+ stub_columns -= 1
+ tgroup += colspec
+ if headrows:
+ thead = nodes.thead()
+ tgroup += thead
+ for row in headrows:
+ thead += self.build_table_row(row, tableline)
+ tbody = nodes.tbody()
+ tgroup += tbody
+ for row in bodyrows:
+ tbody += self.build_table_row(row, tableline)
+ return table
+
+ def build_table_row(self, rowdata, tableline):
+ row = nodes.row()
+ for cell in rowdata:
+ if cell is None:
+ continue
+ morerows, morecols, offset, cellblock = cell
+ attributes = {}
+ if morerows:
+ attributes['morerows'] = morerows
+ if morecols:
+ attributes['morecols'] = morecols
+ entry = nodes.entry(**attributes)
+ row += entry
+ if ''.join(cellblock):
+ self.nested_parse(cellblock, input_offset=tableline+offset,
+ node=entry)
+ return row
+
+ explicit = Struct()
+ """Patterns and constants used for explicit markup recognition."""
+
+ explicit.patterns = Struct(
+ target=re.compile(r"""
+ (
+ _ # anonymous target
+ | # *OR*
+ (?!_) # no underscore at the beginning
+ (?P<quote>`?) # optional open quote
+ (?![ `]) # first char. not space or
+ # backquote
+ (?P<name> # reference name
+ .+?
+ )
+ %(non_whitespace_escape_before)s
+ (?P=quote) # close quote if open quote used
+ )
+ (?<!(?<!\x00):) # no unescaped colon at end
+ %(non_whitespace_escape_before)s
+ [ ]? # optional space
+ : # end of reference name
+ ([ ]+|$) # followed by whitespace
+ """ % vars(Inliner), re.VERBOSE),
+ reference=re.compile(r"""
+ (
+ (?P<simple>%(simplename)s)_
+ | # *OR*
+ ` # open backquote
+ (?![ ]) # not space
+ (?P<phrase>.+?) # hyperlink phrase
+ %(non_whitespace_escape_before)s
+ `_ # close backquote,
+ # reference mark
+ )
+ $ # end of string
+ """ % vars(Inliner), re.VERBOSE),
+ substitution=re.compile(r"""
+ (
+ (?![ ]) # first char. not space
+ (?P<name>.+?) # substitution text
+ %(non_whitespace_escape_before)s
+ \| # close delimiter
+ )
+ ([ ]+|$) # followed by whitespace
+ """ % vars(Inliner),
+ re.VERBOSE),)
+
+ def footnote(self, match):
+ src, srcline = self.state_machine.get_source_and_line()
+ (indented, indent, offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end())
+ label = match.group(1)
+ name = normalize_name(label)
+ footnote = nodes.footnote('\n'.join(indented))
+ footnote.source = src
+ footnote.line = srcline
+ if name[0] == '#': # auto-numbered
+ name = name[1:] # autonumber label
+ footnote['auto'] = 1
+ if name:
+ footnote['names'].append(name)
+ self.document.note_autofootnote(footnote)
+ elif name == '*': # auto-symbol
+ name = ''
+ footnote['auto'] = '*'
+ self.document.note_symbol_footnote(footnote)
+ else: # manually numbered
+ footnote += nodes.label('', label)
+ footnote['names'].append(name)
+ self.document.note_footnote(footnote)
+ if name:
+ self.document.note_explicit_target(footnote, footnote)
+ else:
+ self.document.set_id(footnote, footnote)
+ if indented:
+ self.nested_parse(indented, input_offset=offset, node=footnote)
+ return [footnote], blank_finish
+
+ def citation(self, match):
+ src, srcline = self.state_machine.get_source_and_line()
+ (indented, indent, offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end())
+ label = match.group(1)
+ name = normalize_name(label)
+ citation = nodes.citation('\n'.join(indented))
+ citation.source = src
+ citation.line = srcline
+ citation += nodes.label('', label)
+ citation['names'].append(name)
+ self.document.note_citation(citation)
+ self.document.note_explicit_target(citation, citation)
+ if indented:
+ self.nested_parse(indented, input_offset=offset, node=citation)
+ return [citation], blank_finish
+
+ def hyperlink_target(self, match):
+ pattern = self.explicit.patterns.target
+ lineno = self.state_machine.abs_line_number()
+ (block, indent, offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(
+ match.end(), until_blank=True, strip_indent=False)
+ blocktext = match.string[:match.end()] + '\n'.join(block)
+ block = [escape2null(line) for line in block]
+ escaped = block[0]
+ blockindex = 0
+ while True:
+ targetmatch = pattern.match(escaped)
+ if targetmatch:
+ break
+ blockindex += 1
+ try:
+ escaped += block[blockindex]
+ except IndexError:
+ raise MarkupError('malformed hyperlink target.')
+ del block[:blockindex]
+ block[0] = (block[0] + ' ')[targetmatch.end()-len(escaped)-1:].strip()
+ target = self.make_target(block, blocktext, lineno,
+ targetmatch.group('name'))
+ return [target], blank_finish
+
+ def make_target(self, block, block_text, lineno, target_name):
+ target_type, data = self.parse_target(block, block_text, lineno)
+ if target_type == 'refname':
+ target = nodes.target(block_text, '', refname=normalize_name(data))
+ target.indirect_reference_name = data
+ self.add_target(target_name, '', target, lineno)
+ self.document.note_indirect_target(target)
+ return target
+ elif target_type == 'refuri':
+ target = nodes.target(block_text, '')
+ self.add_target(target_name, data, target, lineno)
+ return target
+ else:
+ return data
+
+ def parse_target(self, block, block_text, lineno):
+ """
+ Determine the type of reference of a target.
+
+ :Return: A 2-tuple, one of:
+
+ - 'refname' and the indirect reference name
+ - 'refuri' and the URI
+ - 'malformed' and a system_message node
+ """
+ if block and block[-1].strip()[-1:] == '_': # possible indirect target
+ reference = ' '.join(line.strip() for line in block)
+ refname = self.is_reference(reference)
+ if refname:
+ return 'refname', refname
+ ref_parts = split_escaped_whitespace(' '.join(block))
+ reference = ' '.join(''.join(unescape(part).split())
+ for part in ref_parts)
+ return 'refuri', reference
+
+ def is_reference(self, reference):
+ match = self.explicit.patterns.reference.match(
+ whitespace_normalize_name(reference))
+ if not match:
+ return None
+ return unescape(match.group('simple') or match.group('phrase'))
+
+ def add_target(self, targetname, refuri, target, lineno):
+ target.line = lineno
+ if targetname:
+ name = normalize_name(unescape(targetname))
+ target['names'].append(name)
+ if refuri:
+ uri = self.inliner.adjust_uri(refuri)
+ if uri:
+ target['refuri'] = uri
+ else:
+ raise ApplicationError('problem with URI: %r' % refuri)
+ self.document.note_explicit_target(target, self.parent)
+ else: # anonymous target
+ if refuri:
+ target['refuri'] = refuri
+ target['anonymous'] = 1
+ self.document.note_anonymous_target(target)
+
+ def substitution_def(self, match):
+ pattern = self.explicit.patterns.substitution
+ src, srcline = self.state_machine.get_source_and_line()
+ (block, indent, offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end(),
+ strip_indent=False)
+ blocktext = (match.string[:match.end()] + '\n'.join(block))
+ block.disconnect()
+ escaped = escape2null(block[0].rstrip())
+ blockindex = 0
+ while True:
+ subdefmatch = pattern.match(escaped)
+ if subdefmatch:
+ break
+ blockindex += 1
+ try:
+ escaped = escaped + ' ' + escape2null(
+ block[blockindex].strip())
+ except IndexError:
+ raise MarkupError('malformed substitution definition.')
+ del block[:blockindex] # strip out the substitution marker
+ start = subdefmatch.end()-len(escaped)-1
+ block[0] = (block[0].strip() + ' ')[start:-1]
+ if not block[0]:
+ del block[0]
+ offset += 1
+ while block and not block[-1].strip():
+ block.pop()
+ subname = subdefmatch.group('name')
+ substitution_node = nodes.substitution_definition(blocktext)
+ substitution_node.source = src
+ substitution_node.line = srcline
+ if not block:
+ msg = self.reporter.warning(
+ 'Substitution definition "%s" missing contents.' % subname,
+ nodes.literal_block(blocktext, blocktext),
+ source=src, line=srcline)
+ return [msg], blank_finish
+ block[0] = block[0].strip()
+ substitution_node['names'].append(
+ nodes.whitespace_normalize_name(subname))
+ new_abs_offset, blank_finish = self.nested_list_parse(
+ block, input_offset=offset, node=substitution_node,
+ initial_state='SubstitutionDef', blank_finish=blank_finish)
+ i = 0
+ for node in substitution_node[:]:
+ if not (isinstance(node, nodes.Inline)
+ or isinstance(node, nodes.Text)):
+ self.parent += substitution_node[i]
+ del substitution_node[i]
+ else:
+ i += 1
+ for node in substitution_node.findall(nodes.Element):
+ if self.disallowed_inside_substitution_definitions(node):
+ pformat = nodes.literal_block('', node.pformat().rstrip())
+ msg = self.reporter.error(
+ 'Substitution definition contains illegal element <%s>:'
+ % node.tagname,
+ pformat, nodes.literal_block(blocktext, blocktext),
+ source=src, line=srcline)
+ return [msg], blank_finish
+ if len(substitution_node) == 0:
+ msg = self.reporter.warning(
+ 'Substitution definition "%s" empty or invalid.' % subname,
+ nodes.literal_block(blocktext, blocktext),
+ source=src, line=srcline)
+ return [msg], blank_finish
+ self.document.note_substitution_def(
+ substitution_node, subname, self.parent)
+ return [substitution_node], blank_finish
+
+ def disallowed_inside_substitution_definitions(self, node):
+ if (node['ids']
+ or isinstance(node, nodes.reference) and node.get('anonymous')
+ or isinstance(node, nodes.footnote_reference) and node.get('auto')): # noqa: E501
+ return True
+ else:
+ return False
+
+ def directive(self, match, **option_presets):
+ """Returns a 2-tuple: list of nodes, and a "blank finish" boolean."""
+ type_name = match.group(1)
+ directive_class, messages = directives.directive(
+ type_name, self.memo.language, self.document)
+ self.parent += messages
+ if directive_class:
+ return self.run_directive(
+ directive_class, match, type_name, option_presets)
+ else:
+ return self.unknown_directive(type_name)
+
+ def run_directive(self, directive, match, type_name, option_presets):
+ """
+ Parse a directive then run its directive function.
+
+ Parameters:
+
+ - `directive`: The class implementing the directive. Must be
+ a subclass of `rst.Directive`.
+
+ - `match`: A regular expression match object which matched the first
+ line of the directive.
+
+ - `type_name`: The directive name, as used in the source text.
+
+ - `option_presets`: A dictionary of preset options, defaults for the
+ directive options. Currently, only an "alt" option is passed by
+ substitution definitions (value: the substitution name), which may
+ be used by an embedded image directive.
+
+ Returns a 2-tuple: list of nodes, and a "blank finish" boolean.
+ """
+ if isinstance(directive, (FunctionType, MethodType)):
+ from docutils.parsers.rst import convert_directive_function
+ directive = convert_directive_function(directive)
+ lineno = self.state_machine.abs_line_number()
+ initial_line_offset = self.state_machine.line_offset
+ (indented, indent, line_offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end(),
+ strip_top=0)
+ block_text = '\n'.join(self.state_machine.input_lines[
+ initial_line_offset : self.state_machine.line_offset + 1]) # noqa: E203,E501
+ try:
+ arguments, options, content, content_offset = (
+ self.parse_directive_block(indented, line_offset,
+ directive, option_presets))
+ except MarkupError as detail:
+ error = self.reporter.error(
+ 'Error in "%s" directive:\n%s.' % (type_name,
+ ' '.join(detail.args)),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [error], blank_finish
+ directive_instance = directive(
+ type_name, arguments, options, content, lineno,
+ content_offset, block_text, self, self.state_machine)
+ try:
+ result = directive_instance.run()
+ except docutils.parsers.rst.DirectiveError as error:
+ msg_node = self.reporter.system_message(error.level, error.msg,
+ line=lineno)
+ msg_node += nodes.literal_block(block_text, block_text)
+ result = [msg_node]
+ assert isinstance(result, list), \
+ 'Directive "%s" must return a list of nodes.' % type_name
+ for i in range(len(result)):
+ assert isinstance(result[i], nodes.Node), \
+ ('Directive "%s" returned non-Node object (index %s): %r'
+ % (type_name, i, result[i]))
+ return (result,
+ blank_finish or self.state_machine.is_next_line_blank())
+
+ def parse_directive_block(self, indented, line_offset, directive,
+ option_presets):
+ option_spec = directive.option_spec
+ has_content = directive.has_content
+ if indented and not indented[0].strip():
+ indented.trim_start()
+ line_offset += 1
+ while indented and not indented[-1].strip():
+ indented.trim_end()
+ if indented and (directive.required_arguments
+ or directive.optional_arguments
+ or option_spec):
+ for i, line in enumerate(indented):
+ if not line.strip():
+ break
+ else:
+ i += 1
+ arg_block = indented[:i]
+ content = indented[i+1:]
+ content_offset = line_offset + i + 1
+ else:
+ content = indented
+ content_offset = line_offset
+ arg_block = []
+ if option_spec:
+ options, arg_block = self.parse_directive_options(
+ option_presets, option_spec, arg_block)
+ else:
+ options = {}
+ if arg_block and not (directive.required_arguments
+ or directive.optional_arguments):
+ content = arg_block + indented[i:]
+ content_offset = line_offset
+ arg_block = []
+ while content and not content[0].strip():
+ content.trim_start()
+ content_offset += 1
+ if directive.required_arguments or directive.optional_arguments:
+ arguments = self.parse_directive_arguments(
+ directive, arg_block)
+ else:
+ arguments = []
+ if content and not has_content:
+ raise MarkupError('no content permitted')
+ return arguments, options, content, content_offset
+
+ def parse_directive_options(self, option_presets, option_spec, arg_block):
+ options = option_presets.copy()
+ for i, line in enumerate(arg_block):
+ if re.match(Body.patterns['field_marker'], line):
+ opt_block = arg_block[i:]
+ arg_block = arg_block[:i]
+ break
+ else:
+ opt_block = []
+ if opt_block:
+ success, data = self.parse_extension_options(option_spec,
+ opt_block)
+ if success: # data is a dict of options
+ options.update(data)
+ else: # data is an error string
+ raise MarkupError(data)
+ return options, arg_block
+
+ def parse_directive_arguments(self, directive, arg_block):
+ required = directive.required_arguments
+ optional = directive.optional_arguments
+ arg_text = '\n'.join(arg_block)
+ arguments = arg_text.split()
+ if len(arguments) < required:
+ raise MarkupError('%s argument(s) required, %s supplied'
+ % (required, len(arguments)))
+ elif len(arguments) > required + optional:
+ if directive.final_argument_whitespace:
+ arguments = arg_text.split(None, required + optional - 1)
+ else:
+ raise MarkupError(
+ 'maximum %s argument(s) allowed, %s supplied'
+ % (required + optional, len(arguments)))
+ return arguments
+
+ def parse_extension_options(self, option_spec, datalines):
+ """
+ Parse `datalines` for a field list containing extension options
+ matching `option_spec`.
+
+ :Parameters:
+ - `option_spec`: a mapping of option name to conversion
+ function, which should raise an exception on bad input.
+ - `datalines`: a list of input strings.
+
+ :Return:
+ - Success value, 1 or 0.
+ - An option dictionary on success, an error string on failure.
+ """
+ node = nodes.field_list()
+ newline_offset, blank_finish = self.nested_list_parse(
+ datalines, 0, node, initial_state='ExtensionOptions',
+ blank_finish=True)
+ if newline_offset != len(datalines): # incomplete parse of block
+ return 0, 'invalid option block'
+ try:
+ options = utils.extract_extension_options(node, option_spec)
+ except KeyError as detail:
+ return 0, 'unknown option: "%s"' % detail.args[0]
+ except (ValueError, TypeError) as detail:
+ return 0, 'invalid option value: %s' % ' '.join(detail.args)
+ except utils.ExtensionOptionError as detail:
+ return 0, 'invalid option data: %s' % ' '.join(detail.args)
+ if blank_finish:
+ return 1, options
+ else:
+ return 0, 'option data incompletely parsed'
+
+ def unknown_directive(self, type_name):
+ lineno = self.state_machine.abs_line_number()
+ (indented, indent, offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(0, strip_indent=False)
+ text = '\n'.join(indented)
+ error = self.reporter.error('Unknown directive type "%s".' % type_name,
+ nodes.literal_block(text, text),
+ line=lineno)
+ return [error], blank_finish
+
+ def comment(self, match):
+ if self.state_machine.is_next_line_blank():
+ first_comment_line = match.string[match.end():]
+ if not first_comment_line.strip(): # empty comment
+ return [nodes.comment()], True # "A tiny but practical wart."
+ if first_comment_line.startswith('end of inclusion from "'):
+ # cf. parsers.rst.directives.misc.Include
+ self.document.include_log.pop()
+ return [], True
+ (indented, indent, offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end())
+ while indented and not indented[-1].strip():
+ indented.trim_end()
+ text = '\n'.join(indented)
+ return [nodes.comment(text, text)], blank_finish
+
+ explicit.constructs = [
+ (footnote,
+ re.compile(r"""
+ \.\.[ ]+ # explicit markup start
+ \[
+ ( # footnote label:
+ [0-9]+ # manually numbered footnote
+ | # *OR*
+ \# # anonymous auto-numbered footnote
+ | # *OR*
+ \#%s # auto-number ed?) footnote label
+ | # *OR*
+ \* # auto-symbol footnote
+ )
+ \]
+ ([ ]+|$) # whitespace or end of line
+ """ % Inliner.simplename, re.VERBOSE)),
+ (citation,
+ re.compile(r"""
+ \.\.[ ]+ # explicit markup start
+ \[(%s)\] # citation label
+ ([ ]+|$) # whitespace or end of line
+ """ % Inliner.simplename, re.VERBOSE)),
+ (hyperlink_target,
+ re.compile(r"""
+ \.\.[ ]+ # explicit markup start
+ _ # target indicator
+ (?![ ]|$) # first char. not space or EOL
+ """, re.VERBOSE)),
+ (substitution_def,
+ re.compile(r"""
+ \.\.[ ]+ # explicit markup start
+ \| # substitution indicator
+ (?![ ]|$) # first char. not space or EOL
+ """, re.VERBOSE)),
+ (directive,
+ re.compile(r"""
+ \.\.[ ]+ # explicit markup start
+ (%s) # directive name
+ [ ]? # optional space
+ :: # directive delimiter
+ ([ ]+|$) # whitespace or end of line
+ """ % Inliner.simplename, re.VERBOSE))]
+
+ def explicit_markup(self, match, context, next_state):
+ """Footnotes, hyperlink targets, directives, comments."""
+ nodelist, blank_finish = self.explicit_construct(match)
+ self.parent += nodelist
+ self.explicit_list(blank_finish)
+ return [], next_state, []
+
+ def explicit_construct(self, match):
+ """Determine which explicit construct this is, parse & return it."""
+ errors = []
+ for method, pattern in self.explicit.constructs:
+ expmatch = pattern.match(match.string)
+ if expmatch:
+ try:
+ return method(self, expmatch)
+ except MarkupError as error:
+ lineno = self.state_machine.abs_line_number()
+ message = ' '.join(error.args)
+ errors.append(self.reporter.warning(message, line=lineno))
+ break
+ nodelist, blank_finish = self.comment(match)
+ return nodelist + errors, blank_finish
+
+ def explicit_list(self, blank_finish):
+ """
+ Create a nested state machine for a series of explicit markup
+ constructs (including anonymous hyperlink targets).
+ """
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=self.parent, initial_state='Explicit',
+ blank_finish=blank_finish,
+ match_titles=self.state_machine.match_titles)
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Explicit markup')
+
+ def anonymous(self, match, context, next_state):
+ """Anonymous hyperlink targets."""
+ nodelist, blank_finish = self.anonymous_target(match)
+ self.parent += nodelist
+ self.explicit_list(blank_finish)
+ return [], next_state, []
+
+ def anonymous_target(self, match):
+ lineno = self.state_machine.abs_line_number()
+ (block, indent, offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end(),
+ until_blank=True)
+ blocktext = match.string[:match.end()] + '\n'.join(block)
+ block = [escape2null(line) for line in block]
+ target = self.make_target(block, blocktext, lineno, '')
+ return [target], blank_finish
+
+ def line(self, match, context, next_state):
+ """Section title overline or transition marker."""
+ if self.state_machine.match_titles:
+ return [match.string], 'Line', []
+ elif match.string.strip() == '::':
+ raise statemachine.TransitionCorrection('text')
+ elif len(match.string.strip()) < 4:
+ msg = self.reporter.info(
+ 'Unexpected possible title overline or transition.\n'
+ "Treating it as ordinary text because it's so short.",
+ line=self.state_machine.abs_line_number())
+ self.parent += msg
+ raise statemachine.TransitionCorrection('text')
+ else:
+ blocktext = self.state_machine.line
+ msg = self.reporter.severe(
+ 'Unexpected section title or transition.',
+ nodes.literal_block(blocktext, blocktext),
+ line=self.state_machine.abs_line_number())
+ self.parent += msg
+ return [], next_state, []
+
+ def text(self, match, context, next_state):
+ """Titles, definition lists, paragraphs."""
+ return [match.string], 'Text', []
+
+
+class RFC2822Body(Body):
+
+ """
+ RFC2822 headers are only valid as the first constructs in documents. As
+ soon as anything else appears, the `Body` state should take over.
+ """
+
+ patterns = Body.patterns.copy() # can't modify the original
+ patterns['rfc2822'] = r'[!-9;-~]+:( +|$)'
+ initial_transitions = [(name, 'Body')
+ for name in Body.initial_transitions]
+ initial_transitions.insert(-1, ('rfc2822', 'Body')) # just before 'text'
+
+ def rfc2822(self, match, context, next_state):
+ """RFC2822-style field list item."""
+ fieldlist = nodes.field_list(classes=['rfc2822'])
+ self.parent += fieldlist
+ field, blank_finish = self.rfc2822_field(match)
+ fieldlist += field
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=fieldlist, initial_state='RFC2822List',
+ blank_finish=blank_finish)
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning(
+ 'RFC2822-style field list')
+ return [], next_state, []
+
+ def rfc2822_field(self, match):
+ name = match.string[:match.string.find(':')]
+ (indented, indent, line_offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end(),
+ until_blank=True)
+ fieldnode = nodes.field()
+ fieldnode += nodes.field_name(name, name)
+ fieldbody = nodes.field_body('\n'.join(indented))
+ fieldnode += fieldbody
+ if indented:
+ self.nested_parse(indented, input_offset=line_offset,
+ node=fieldbody)
+ return fieldnode, blank_finish
+
+
+class SpecializedBody(Body):
+
+ """
+ Superclass for second and subsequent compound element members. Compound
+ elements are lists and list-like constructs.
+
+ All transition methods are disabled (redefined as `invalid_input`).
+ Override individual methods in subclasses to re-enable.
+
+ For example, once an initial bullet list item, say, is recognized, the
+ `BulletList` subclass takes over, with a "bullet_list" node as its
+ container. Upon encountering the initial bullet list item, `Body.bullet`
+ calls its ``self.nested_list_parse`` (`RSTState.nested_list_parse`), which
+ starts up a nested parsing session with `BulletList` as the initial state.
+ Only the ``bullet`` transition method is enabled in `BulletList`; as long
+ as only bullet list items are encountered, they are parsed and inserted
+ into the container. The first construct which is *not* a bullet list item
+ triggers the `invalid_input` method, which ends the nested parse and
+ closes the container. `BulletList` needs to recognize input that is
+ invalid in the context of a bullet list, which means everything *other
+ than* bullet list items, so it inherits the transition list created in
+ `Body`.
+ """
+
+ def invalid_input(self, match=None, context=None, next_state=None):
+ """Not a compound element member. Abort this state machine."""
+ self.state_machine.previous_line() # back up so parent SM can reassess
+ raise EOFError
+
+ indent = invalid_input
+ bullet = invalid_input
+ enumerator = invalid_input
+ field_marker = invalid_input
+ option_marker = invalid_input
+ doctest = invalid_input
+ line_block = invalid_input
+ grid_table_top = invalid_input
+ simple_table_top = invalid_input
+ explicit_markup = invalid_input
+ anonymous = invalid_input
+ line = invalid_input
+ text = invalid_input
+
+
+class BulletList(SpecializedBody):
+
+ """Second and subsequent bullet_list list_items."""
+
+ def bullet(self, match, context, next_state):
+ """Bullet list item."""
+ if match.string[0] != self.parent['bullet']:
+ # different bullet: new list
+ self.invalid_input()
+ listitem, blank_finish = self.list_item(match.end())
+ self.parent += listitem
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+
+class DefinitionList(SpecializedBody):
+
+ """Second and subsequent definition_list_items."""
+
+ def text(self, match, context, next_state):
+ """Definition lists."""
+ return [match.string], 'Definition', []
+
+
+class EnumeratedList(SpecializedBody):
+
+ """Second and subsequent enumerated_list list_items."""
+
+ def enumerator(self, match, context, next_state):
+ """Enumerated list item."""
+ format, sequence, text, ordinal = self.parse_enumerator(
+ match, self.parent['enumtype'])
+ if (format != self.format
+ or (sequence != '#' and (sequence != self.parent['enumtype']
+ or self.auto
+ or ordinal != (self.lastordinal + 1)))
+ or not self.is_enumerated_list_item(ordinal, sequence, format)):
+ # different enumeration: new list
+ self.invalid_input()
+ if sequence == '#':
+ self.auto = 1
+ listitem, blank_finish = self.list_item(match.end())
+ self.parent += listitem
+ self.blank_finish = blank_finish
+ self.lastordinal = ordinal
+ return [], next_state, []
+
+
+class FieldList(SpecializedBody):
+
+ """Second and subsequent field_list fields."""
+
+ def field_marker(self, match, context, next_state):
+ """Field list field."""
+ field, blank_finish = self.field(match)
+ self.parent += field
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+
+class OptionList(SpecializedBody):
+
+ """Second and subsequent option_list option_list_items."""
+
+ def option_marker(self, match, context, next_state):
+ """Option list item."""
+ try:
+ option_list_item, blank_finish = self.option_list_item(match)
+ except MarkupError:
+ self.invalid_input()
+ self.parent += option_list_item
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+
+class RFC2822List(SpecializedBody, RFC2822Body):
+
+ """Second and subsequent RFC2822-style field_list fields."""
+
+ patterns = RFC2822Body.patterns
+ initial_transitions = RFC2822Body.initial_transitions
+
+ def rfc2822(self, match, context, next_state):
+ """RFC2822-style field list item."""
+ field, blank_finish = self.rfc2822_field(match)
+ self.parent += field
+ self.blank_finish = blank_finish
+ return [], 'RFC2822List', []
+
+ blank = SpecializedBody.invalid_input
+
+
+class ExtensionOptions(FieldList):
+
+ """
+ Parse field_list fields for extension options.
+
+ No nested parsing is done (including inline markup parsing).
+ """
+
+ def parse_field_body(self, indented, offset, node):
+ """Override `Body.parse_field_body` for simpler parsing."""
+ lines = []
+ for line in list(indented) + ['']:
+ if line.strip():
+ lines.append(line)
+ elif lines:
+ text = '\n'.join(lines)
+ node += nodes.paragraph(text, text)
+ lines = []
+
+
+class LineBlock(SpecializedBody):
+
+ """Second and subsequent lines of a line_block."""
+
+ blank = SpecializedBody.invalid_input
+
+ def line_block(self, match, context, next_state):
+ """New line of line block."""
+ lineno = self.state_machine.abs_line_number()
+ line, messages, blank_finish = self.line_block_line(match, lineno)
+ self.parent += line
+ self.parent.parent += messages
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+
+class Explicit(SpecializedBody):
+
+ """Second and subsequent explicit markup construct."""
+
+ def explicit_markup(self, match, context, next_state):
+ """Footnotes, hyperlink targets, directives, comments."""
+ nodelist, blank_finish = self.explicit_construct(match)
+ self.parent += nodelist
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+ def anonymous(self, match, context, next_state):
+ """Anonymous hyperlink targets."""
+ nodelist, blank_finish = self.anonymous_target(match)
+ self.parent += nodelist
+ self.blank_finish = blank_finish
+ return [], next_state, []
+
+ blank = SpecializedBody.invalid_input
+
+
+class SubstitutionDef(Body):
+
+ """
+ Parser for the contents of a substitution_definition element.
+ """
+
+ patterns = {
+ 'embedded_directive': re.compile(r'(%s)::( +|$)'
+ % Inliner.simplename),
+ 'text': r''}
+ initial_transitions = ['embedded_directive', 'text']
+
+ def embedded_directive(self, match, context, next_state):
+ nodelist, blank_finish = self.directive(match,
+ alt=self.parent['names'][0])
+ self.parent += nodelist
+ if not self.state_machine.at_eof():
+ self.blank_finish = blank_finish
+ raise EOFError
+
+ def text(self, match, context, next_state):
+ if not self.state_machine.at_eof():
+ self.blank_finish = self.state_machine.is_next_line_blank()
+ raise EOFError
+
+
+class Text(RSTState):
+
+ """
+ Classifier of second line of a text block.
+
+ Could be a paragraph, a definition list item, or a title.
+ """
+
+ patterns = {'underline': Body.patterns['line'],
+ 'text': r''}
+ initial_transitions = [('underline', 'Body'), ('text', 'Body')]
+
+ def blank(self, match, context, next_state):
+ """End of paragraph."""
+ # NOTE: self.paragraph returns [node, system_message(s)], literalnext
+ paragraph, literalnext = self.paragraph(
+ context, self.state_machine.abs_line_number() - 1)
+ self.parent += paragraph
+ if literalnext:
+ self.parent += self.literal_block()
+ return [], 'Body', []
+
+ def eof(self, context):
+ if context:
+ self.blank(None, context, None)
+ return []
+
+ def indent(self, match, context, next_state):
+ """Definition list item."""
+ dl = nodes.definition_list()
+ # the definition list starts on the line before the indent:
+ lineno = self.state_machine.abs_line_number() - 1
+ dl.source, dl.line = self.state_machine.get_source_and_line(lineno)
+ dl_item, blank_finish = self.definition_list_item(context)
+ dl += dl_item
+ self.parent += dl
+ offset = self.state_machine.line_offset + 1 # next line
+ newline_offset, blank_finish = self.nested_list_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=self.state_machine.abs_line_offset() + 1,
+ node=dl, initial_state='DefinitionList',
+ blank_finish=blank_finish, blank_finish_state='Definition')
+ self.goto_line(newline_offset)
+ if not blank_finish:
+ self.parent += self.unindent_warning('Definition list')
+ return [], 'Body', []
+
+ def underline(self, match, context, next_state):
+ """Section title."""
+ lineno = self.state_machine.abs_line_number()
+ title = context[0].rstrip()
+ underline = match.string.rstrip()
+ source = title + '\n' + underline
+ messages = []
+ if column_width(title) > len(underline):
+ if len(underline) < 4:
+ if self.state_machine.match_titles:
+ msg = self.reporter.info(
+ 'Possible title underline, too short for the title.\n'
+ "Treating it as ordinary text because it's so short.",
+ line=lineno)
+ self.parent += msg
+ raise statemachine.TransitionCorrection('text')
+ else:
+ blocktext = context[0] + '\n' + self.state_machine.line
+ msg = self.reporter.warning(
+ 'Title underline too short.',
+ nodes.literal_block(blocktext, blocktext),
+ line=lineno)
+ messages.append(msg)
+ if not self.state_machine.match_titles:
+ blocktext = context[0] + '\n' + self.state_machine.line
+ # We need get_source_and_line() here to report correctly
+ src, srcline = self.state_machine.get_source_and_line()
+ # TODO: why is abs_line_number() == srcline+1
+ # if the error is in a table (try with test_tables.py)?
+ # print("get_source_and_line", srcline)
+ # print("abs_line_number", self.state_machine.abs_line_number())
+ msg = self.reporter.severe(
+ 'Unexpected section title.',
+ nodes.literal_block(blocktext, blocktext),
+ source=src, line=srcline)
+ self.parent += messages
+ self.parent += msg
+ return [], next_state, []
+ style = underline[0]
+ context[:] = []
+ self.section(title, source, style, lineno - 1, messages)
+ return [], next_state, []
+
+ def text(self, match, context, next_state):
+ """Paragraph."""
+ startline = self.state_machine.abs_line_number() - 1
+ msg = None
+ try:
+ block = self.state_machine.get_text_block(flush_left=True)
+ except statemachine.UnexpectedIndentationError as err:
+ block, src, srcline = err.args
+ msg = self.reporter.error('Unexpected indentation.',
+ source=src, line=srcline)
+ lines = context + list(block)
+ paragraph, literalnext = self.paragraph(lines, startline)
+ self.parent += paragraph
+ self.parent += msg
+ if literalnext:
+ try:
+ self.state_machine.next_line()
+ except EOFError:
+ pass
+ self.parent += self.literal_block()
+ return [], next_state, []
+
+ def literal_block(self):
+ """Return a list of nodes."""
+ (indented, indent, offset, blank_finish
+ ) = self.state_machine.get_indented()
+ while indented and not indented[-1].strip():
+ indented.trim_end()
+ if not indented:
+ return self.quoted_literal_block()
+ data = '\n'.join(indented)
+ literal_block = nodes.literal_block(data, data)
+ (literal_block.source,
+ literal_block.line) = self.state_machine.get_source_and_line(offset+1)
+ nodelist = [literal_block]
+ if not blank_finish:
+ nodelist.append(self.unindent_warning('Literal block'))
+ return nodelist
+
+ def quoted_literal_block(self):
+ abs_line_offset = self.state_machine.abs_line_offset()
+ offset = self.state_machine.line_offset
+ parent_node = nodes.Element()
+ new_abs_offset = self.nested_parse(
+ self.state_machine.input_lines[offset:],
+ input_offset=abs_line_offset, node=parent_node, match_titles=False,
+ state_machine_kwargs={'state_classes': (QuotedLiteralBlock,),
+ 'initial_state': 'QuotedLiteralBlock'})
+ self.goto_line(new_abs_offset)
+ return parent_node.children
+
+ def definition_list_item(self, termline):
+ # the parser is already on the second (indented) line:
+ dd_lineno = self.state_machine.abs_line_number()
+ dt_lineno = dd_lineno - 1
+ (indented, indent, line_offset, blank_finish
+ ) = self.state_machine.get_indented()
+ dl_item = nodes.definition_list_item(
+ '\n'.join(termline + list(indented)))
+ (dl_item.source,
+ dl_item.line) = self.state_machine.get_source_and_line(dt_lineno)
+ dt_nodes, messages = self.term(termline, dt_lineno)
+ dl_item += dt_nodes
+ dd = nodes.definition('', *messages)
+ dd.source, dd.line = self.state_machine.get_source_and_line(dd_lineno)
+ dl_item += dd
+ if termline[0][-2:] == '::':
+ dd += self.reporter.info(
+ 'Blank line missing before literal block (after the "::")? '
+ 'Interpreted as a definition list item.',
+ line=dd_lineno)
+ # TODO: drop a definition if it is an empty comment to allow
+ # definition list items with several terms?
+ # https://sourceforge.net/p/docutils/feature-requests/60/
+ self.nested_parse(indented, input_offset=line_offset, node=dd)
+ return dl_item, blank_finish
+
+ classifier_delimiter = re.compile(' +: +')
+
+ def term(self, lines, lineno):
+ """Return a definition_list's term and optional classifiers."""
+ assert len(lines) == 1
+ text_nodes, messages = self.inline_text(lines[0], lineno)
+ dt = nodes.term(lines[0])
+ dt.source, dt.line = self.state_machine.get_source_and_line(lineno)
+ node_list = [dt]
+ for i in range(len(text_nodes)):
+ node = text_nodes[i]
+ if isinstance(node, nodes.Text):
+ parts = self.classifier_delimiter.split(node)
+ if len(parts) == 1:
+ node_list[-1] += node
+ else:
+ text = parts[0].rstrip()
+ textnode = nodes.Text(text)
+ node_list[-1] += textnode
+ for part in parts[1:]:
+ node_list.append(
+ nodes.classifier(unescape(part, True), part))
+ else:
+ node_list[-1] += node
+ return node_list, messages
+
+
+class SpecializedText(Text):
+
+ """
+ Superclass for second and subsequent lines of Text-variants.
+
+ All transition methods are disabled. Override individual methods in
+ subclasses to re-enable.
+ """
+
+ def eof(self, context):
+ """Incomplete construct."""
+ return []
+
+ def invalid_input(self, match=None, context=None, next_state=None):
+ """Not a compound element member. Abort this state machine."""
+ raise EOFError
+
+ blank = invalid_input
+ indent = invalid_input
+ underline = invalid_input
+ text = invalid_input
+
+
+class Definition(SpecializedText):
+
+ """Second line of potential definition_list_item."""
+
+ def eof(self, context):
+ """Not a definition."""
+ self.state_machine.previous_line(2) # so parent SM can reassess
+ return []
+
+ def indent(self, match, context, next_state):
+ """Definition list item."""
+ dl_item, blank_finish = self.definition_list_item(context)
+ self.parent += dl_item
+ self.blank_finish = blank_finish
+ return [], 'DefinitionList', []
+
+
+class Line(SpecializedText):
+
+ """
+ Second line of over- & underlined section title or transition marker.
+ """
+
+ eofcheck = 1 # @@@ ???
+ """Set to 0 while parsing sections, so that we don't catch the EOF."""
+
+ def eof(self, context):
+ """Transition marker at end of section or document."""
+ marker = context[0].strip()
+ if self.memo.section_bubble_up_kludge:
+ self.memo.section_bubble_up_kludge = False
+ elif len(marker) < 4:
+ self.state_correction(context)
+ if self.eofcheck: # ignore EOFError with sections
+ src, srcline = self.state_machine.get_source_and_line()
+ # lineno = self.state_machine.abs_line_number() - 1
+ transition = nodes.transition(rawsource=context[0])
+ transition.source = src
+ transition.line = srcline - 1
+ # transition.line = lineno
+ self.parent += transition
+ self.eofcheck = 1
+ return []
+
+ def blank(self, match, context, next_state):
+ """Transition marker."""
+ src, srcline = self.state_machine.get_source_and_line()
+ marker = context[0].strip()
+ if len(marker) < 4:
+ self.state_correction(context)
+ transition = nodes.transition(rawsource=marker)
+ transition.source = src
+ transition.line = srcline - 1
+ self.parent += transition
+ return [], 'Body', []
+
+ def text(self, match, context, next_state):
+ """Potential over- & underlined title."""
+ lineno = self.state_machine.abs_line_number() - 1
+ overline = context[0]
+ title = match.string
+ underline = ''
+ try:
+ underline = self.state_machine.next_line()
+ except EOFError:
+ blocktext = overline + '\n' + title
+ if len(overline.rstrip()) < 4:
+ self.short_overline(context, blocktext, lineno, 2)
+ else:
+ msg = self.reporter.severe(
+ 'Incomplete section title.',
+ nodes.literal_block(blocktext, blocktext),
+ line=lineno)
+ self.parent += msg
+ return [], 'Body', []
+ source = '%s\n%s\n%s' % (overline, title, underline)
+ overline = overline.rstrip()
+ underline = underline.rstrip()
+ if not self.transitions['underline'][0].match(underline):
+ blocktext = overline + '\n' + title + '\n' + underline
+ if len(overline.rstrip()) < 4:
+ self.short_overline(context, blocktext, lineno, 2)
+ else:
+ msg = self.reporter.severe(
+ 'Missing matching underline for section title overline.',
+ nodes.literal_block(source, source),
+ line=lineno)
+ self.parent += msg
+ return [], 'Body', []
+ elif overline != underline:
+ blocktext = overline + '\n' + title + '\n' + underline
+ if len(overline.rstrip()) < 4:
+ self.short_overline(context, blocktext, lineno, 2)
+ else:
+ msg = self.reporter.severe(
+ 'Title overline & underline mismatch.',
+ nodes.literal_block(source, source),
+ line=lineno)
+ self.parent += msg
+ return [], 'Body', []
+ title = title.rstrip()
+ messages = []
+ if column_width(title) > len(overline):
+ blocktext = overline + '\n' + title + '\n' + underline
+ if len(overline.rstrip()) < 4:
+ self.short_overline(context, blocktext, lineno, 2)
+ else:
+ msg = self.reporter.warning(
+ 'Title overline too short.',
+ nodes.literal_block(source, source),
+ line=lineno)
+ messages.append(msg)
+ style = (overline[0], underline[0])
+ self.eofcheck = 0 # @@@ not sure this is correct
+ self.section(title.lstrip(), source, style, lineno + 1, messages)
+ self.eofcheck = 1
+ return [], 'Body', []
+
+ indent = text # indented title
+
+ def underline(self, match, context, next_state):
+ overline = context[0]
+ blocktext = overline + '\n' + self.state_machine.line
+ lineno = self.state_machine.abs_line_number() - 1
+ if len(overline.rstrip()) < 4:
+ self.short_overline(context, blocktext, lineno, 1)
+ msg = self.reporter.error(
+ 'Invalid section title or transition marker.',
+ nodes.literal_block(blocktext, blocktext),
+ line=lineno)
+ self.parent += msg
+ return [], 'Body', []
+
+ def short_overline(self, context, blocktext, lineno, lines=1):
+ msg = self.reporter.info(
+ 'Possible incomplete section title.\nTreating the overline as '
+ "ordinary text because it's so short.",
+ line=lineno)
+ self.parent += msg
+ self.state_correction(context, lines)
+
+ def state_correction(self, context, lines=1):
+ self.state_machine.previous_line(lines)
+ context[:] = []
+ raise statemachine.StateCorrection('Body', 'text')
+
+
+class QuotedLiteralBlock(RSTState):
+
+ """
+ Nested parse handler for quoted (unindented) literal blocks.
+
+ Special-purpose. Not for inclusion in `state_classes`.
+ """
+
+ patterns = {'initial_quoted': r'(%(nonalphanum7bit)s)' % Body.pats,
+ 'text': r''}
+ initial_transitions = ('initial_quoted', 'text')
+
+ def __init__(self, state_machine, debug=False):
+ RSTState.__init__(self, state_machine, debug)
+ self.messages = []
+ self.initial_lineno = None
+
+ def blank(self, match, context, next_state):
+ if context:
+ raise EOFError
+ else:
+ return context, next_state, []
+
+ def eof(self, context):
+ if context:
+ src, srcline = self.state_machine.get_source_and_line(
+ self.initial_lineno)
+ text = '\n'.join(context)
+ literal_block = nodes.literal_block(text, text)
+ literal_block.source = src
+ literal_block.line = srcline
+ self.parent += literal_block
+ else:
+ self.parent += self.reporter.warning(
+ 'Literal block expected; none found.',
+ line=self.state_machine.abs_line_number()
+ ) # src not available, statemachine.input_lines is empty
+ self.state_machine.previous_line()
+ self.parent += self.messages
+ return []
+
+ def indent(self, match, context, next_state):
+ assert context, ('QuotedLiteralBlock.indent: context should not '
+ 'be empty!')
+ self.messages.append(
+ self.reporter.error('Unexpected indentation.',
+ line=self.state_machine.abs_line_number()))
+ self.state_machine.previous_line()
+ raise EOFError
+
+ def initial_quoted(self, match, context, next_state):
+ """Match arbitrary quote character on the first line only."""
+ self.remove_transition('initial_quoted')
+ quote = match.string[0]
+ pattern = re.compile(re.escape(quote))
+ # New transition matches consistent quotes only:
+ self.add_transition('quoted',
+ (pattern, self.quoted, self.__class__.__name__))
+ self.initial_lineno = self.state_machine.abs_line_number()
+ return [match.string], next_state, []
+
+ def quoted(self, match, context, next_state):
+ """Match consistent quotes on subsequent lines."""
+ context.append(match.string)
+ return context, next_state, []
+
+ def text(self, match, context, next_state):
+ if context:
+ self.messages.append(
+ self.reporter.error('Inconsistent literal block quoting.',
+ line=self.state_machine.abs_line_number()))
+ self.state_machine.previous_line()
+ raise EOFError
+
+
+state_classes = (Body, BulletList, DefinitionList, EnumeratedList, FieldList,
+ OptionList, LineBlock, ExtensionOptions, Explicit, Text,
+ Definition, Line, SubstitutionDef, RFC2822Body, RFC2822List)
+"""Standard set of State classes used to start `RSTStateMachine`."""
diff --git a/.venv/lib/python3.12/site-packages/docutils/parsers/rst/tableparser.py b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/tableparser.py
new file mode 100644
index 00000000..1e0e2be2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/parsers/rst/tableparser.py
@@ -0,0 +1,539 @@
+# $Id: tableparser.py 9038 2022-03-05 23:31:46Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+This module defines table parser classes,which parse plaintext-graphic tables
+and produce a well-formed data structure suitable for building a CALS table.
+
+:Classes:
+ - `GridTableParser`: Parse fully-formed tables represented with a grid.
+ - `SimpleTableParser`: Parse simple tables, delimited by top & bottom
+ borders.
+
+:Exception class: `TableMarkupError`
+
+:Function:
+ `update_dict_of_lists()`: Merge two dictionaries containing list values.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import re
+import sys
+from docutils import DataError
+from docutils.utils import strip_combining_chars
+
+
+class TableMarkupError(DataError):
+
+ """
+ Raise if there is any problem with table markup.
+
+ The keyword argument `offset` denotes the offset of the problem
+ from the table's start line.
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.offset = kwargs.pop('offset', 0)
+ DataError.__init__(self, *args)
+
+
+class TableParser:
+
+ """
+ Abstract superclass for the common parts of the syntax-specific parsers.
+ """
+
+ head_body_separator_pat = None
+ """Matches the row separator between head rows and body rows."""
+
+ double_width_pad_char = '\x00'
+ """Padding character for East Asian double-width text."""
+
+ def parse(self, block):
+ """
+ Analyze the text `block` and return a table data structure.
+
+ Given a plaintext-graphic table in `block` (list of lines of text; no
+ whitespace padding), parse the table, construct and return the data
+ necessary to construct a CALS table or equivalent.
+
+ Raise `TableMarkupError` if there is any problem with the markup.
+ """
+ self.setup(block)
+ self.find_head_body_sep()
+ self.parse_table()
+ return self.structure_from_cells()
+
+ def find_head_body_sep(self):
+ """Look for a head/body row separator line; store the line index."""
+ for i in range(len(self.block)):
+ line = self.block[i]
+ if self.head_body_separator_pat.match(line):
+ if self.head_body_sep:
+ raise TableMarkupError(
+ 'Multiple head/body row separators '
+ '(table lines %s and %s); only one allowed.'
+ % (self.head_body_sep+1, i+1), offset=i)
+ else:
+ self.head_body_sep = i
+ self.block[i] = line.replace('=', '-')
+ if self.head_body_sep == 0 or self.head_body_sep == (len(self.block)
+ - 1):
+ raise TableMarkupError('The head/body row separator may not be '
+ 'the first or last line of the table.',
+ offset=i)
+
+
+class GridTableParser(TableParser):
+
+ """
+ Parse a grid table using `parse()`.
+
+ Here's an example of a grid table::
+
+ +------------------------+------------+----------+----------+
+ | Header row, column 1 | Header 2 | Header 3 | Header 4 |
+ +========================+============+==========+==========+
+ | body row 1, column 1 | column 2 | column 3 | column 4 |
+ +------------------------+------------+----------+----------+
+ | body row 2 | Cells may span columns. |
+ +------------------------+------------+---------------------+
+ | body row 3 | Cells may | - Table cells |
+ +------------------------+ span rows. | - contain |
+ | body row 4 | | - body elements. |
+ +------------------------+------------+---------------------+
+
+ Intersections use '+', row separators use '-' (except for one optional
+ head/body row separator, which uses '='), and column separators use '|'.
+
+ Passing the above table to the `parse()` method will result in the
+ following data structure::
+
+ ([24, 12, 10, 10],
+ [[(0, 0, 1, ['Header row, column 1']),
+ (0, 0, 1, ['Header 2']),
+ (0, 0, 1, ['Header 3']),
+ (0, 0, 1, ['Header 4'])]],
+ [[(0, 0, 3, ['body row 1, column 1']),
+ (0, 0, 3, ['column 2']),
+ (0, 0, 3, ['column 3']),
+ (0, 0, 3, ['column 4'])],
+ [(0, 0, 5, ['body row 2']),
+ (0, 2, 5, ['Cells may span columns.']),
+ None,
+ None],
+ [(0, 0, 7, ['body row 3']),
+ (1, 0, 7, ['Cells may', 'span rows.', '']),
+ (1, 1, 7, ['- Table cells', '- contain', '- body elements.']),
+ None],
+ [(0, 0, 9, ['body row 4']), None, None, None]])
+
+ The first item is a list containing column widths (colspecs). The second
+ item is a list of head rows, and the third is a list of body rows. Each
+ row contains a list of cells. Each cell is either None (for a cell unused
+ because of another cell's span), or a tuple. A cell tuple contains four
+ items: the number of extra rows used by the cell in a vertical span
+ (morerows); the number of extra columns used by the cell in a horizontal
+ span (morecols); the line offset of the first line of the cell contents;
+ and the cell contents, a list of lines of text.
+ """
+
+ head_body_separator_pat = re.compile(r'\+=[=+]+=\+ *$')
+
+ def setup(self, block):
+ self.block = block[:] # make a copy; it may be modified
+ self.block.disconnect() # don't propagate changes to parent
+ self.bottom = len(block) - 1
+ self.right = len(block[0]) - 1
+ self.head_body_sep = None
+ self.done = [-1] * len(block[0])
+ self.cells = []
+ self.rowseps = {0: [0]}
+ self.colseps = {0: [0]}
+
+ def parse_table(self):
+ """
+ Start with a queue of upper-left corners, containing the upper-left
+ corner of the table itself. Trace out one rectangular cell, remember
+ it, and add its upper-right and lower-left corners to the queue of
+ potential upper-left corners of further cells. Process the queue in
+ top-to-bottom order, keeping track of how much of each text column has
+ been seen.
+
+ We'll end up knowing all the row and column boundaries, cell positions
+ and their dimensions.
+ """
+ corners = [(0, 0)]
+ while corners:
+ top, left = corners.pop(0)
+ if (top == self.bottom
+ or left == self.right
+ or top <= self.done[left]):
+ continue
+ result = self.scan_cell(top, left)
+ if not result:
+ continue
+ bottom, right, rowseps, colseps = result
+ update_dict_of_lists(self.rowseps, rowseps)
+ update_dict_of_lists(self.colseps, colseps)
+ self.mark_done(top, left, bottom, right)
+ cellblock = self.block.get_2D_block(top + 1, left + 1,
+ bottom, right)
+ cellblock.disconnect() # lines in cell can't sync with parent
+ cellblock.replace(self.double_width_pad_char, '')
+ self.cells.append((top, left, bottom, right, cellblock))
+ corners.extend([(top, right), (bottom, left)])
+ corners.sort()
+ if not self.check_parse_complete():
+ raise TableMarkupError('Malformed table; parse incomplete.')
+
+ def mark_done(self, top, left, bottom, right):
+ """For keeping track of how much of each text column has been seen."""
+ before = top - 1
+ after = bottom - 1
+ for col in range(left, right):
+ assert self.done[col] == before
+ self.done[col] = after
+
+ def check_parse_complete(self):
+ """Each text column should have been completely seen."""
+ last = self.bottom - 1
+ for col in range(self.right):
+ if self.done[col] != last:
+ return False
+ return True
+
+ def scan_cell(self, top, left):
+ """Starting at the top-left corner, start tracing out a cell."""
+ assert self.block[top][left] == '+'
+ return self.scan_right(top, left)
+
+ def scan_right(self, top, left):
+ """
+ Look for the top-right corner of the cell, and make note of all column
+ boundaries ('+').
+ """
+ colseps = {}
+ line = self.block[top]
+ for i in range(left + 1, self.right + 1):
+ if line[i] == '+':
+ colseps[i] = [top]
+ result = self.scan_down(top, left, i)
+ if result:
+ bottom, rowseps, newcolseps = result
+ update_dict_of_lists(colseps, newcolseps)
+ return bottom, i, rowseps, colseps
+ elif line[i] != '-':
+ return None
+ return None
+
+ def scan_down(self, top, left, right):
+ """
+ Look for the bottom-right corner of the cell, making note of all row
+ boundaries.
+ """
+ rowseps = {}
+ for i in range(top + 1, self.bottom + 1):
+ if self.block[i][right] == '+':
+ rowseps[i] = [right]
+ result = self.scan_left(top, left, i, right)
+ if result:
+ newrowseps, colseps = result
+ update_dict_of_lists(rowseps, newrowseps)
+ return i, rowseps, colseps
+ elif self.block[i][right] != '|':
+ return None
+ return None
+
+ def scan_left(self, top, left, bottom, right):
+ """
+ Noting column boundaries, look for the bottom-left corner of the cell.
+ It must line up with the starting point.
+ """
+ colseps = {}
+ line = self.block[bottom]
+ for i in range(right - 1, left, -1):
+ if line[i] == '+':
+ colseps[i] = [bottom]
+ elif line[i] != '-':
+ return None
+ if line[left] != '+':
+ return None
+ result = self.scan_up(top, left, bottom, right)
+ if result is not None:
+ rowseps = result
+ return rowseps, colseps
+ return None
+
+ def scan_up(self, top, left, bottom, right):
+ """
+ Noting row boundaries, see if we can return to the starting point.
+ """
+ rowseps = {}
+ for i in range(bottom - 1, top, -1):
+ if self.block[i][left] == '+':
+ rowseps[i] = [left]
+ elif self.block[i][left] != '|':
+ return None
+ return rowseps
+
+ def structure_from_cells(self):
+ """
+ From the data collected by `scan_cell()`, convert to the final data
+ structure.
+ """
+ rowseps = sorted(self.rowseps.keys()) # list of row boundaries
+ rowindex = {}
+ for i in range(len(rowseps)):
+ rowindex[rowseps[i]] = i # row boundary -> row number mapping
+ colseps = sorted(self.colseps.keys()) # list of column boundaries
+ colindex = {}
+ for i in range(len(colseps)):
+ colindex[colseps[i]] = i # column boundary -> col number map
+ colspecs = [(colseps[i] - colseps[i - 1] - 1)
+ for i in range(1, len(colseps))] # list of column widths
+ # prepare an empty table with the correct number of rows & columns
+ onerow = [None for i in range(len(colseps) - 1)]
+ rows = [onerow[:] for i in range(len(rowseps) - 1)]
+ # keep track of # of cells remaining; should reduce to zero
+ remaining = (len(rowseps) - 1) * (len(colseps) - 1)
+ for top, left, bottom, right, block in self.cells:
+ rownum = rowindex[top]
+ colnum = colindex[left]
+ assert rows[rownum][colnum] is None, (
+ 'Cell (row %s, column %s) already used.'
+ % (rownum + 1, colnum + 1))
+ morerows = rowindex[bottom] - rownum - 1
+ morecols = colindex[right] - colnum - 1
+ remaining -= (morerows + 1) * (morecols + 1)
+ # write the cell into the table
+ rows[rownum][colnum] = (morerows, morecols, top + 1, block)
+ assert remaining == 0, 'Unused cells remaining.'
+ if self.head_body_sep: # separate head rows from body rows
+ numheadrows = rowindex[self.head_body_sep]
+ headrows = rows[:numheadrows]
+ bodyrows = rows[numheadrows:]
+ else:
+ headrows = []
+ bodyrows = rows
+ return colspecs, headrows, bodyrows
+
+
+class SimpleTableParser(TableParser):
+
+ """
+ Parse a simple table using `parse()`.
+
+ Here's an example of a simple table::
+
+ ===== =====
+ col 1 col 2
+ ===== =====
+ 1 Second column of row 1.
+ 2 Second column of row 2.
+ Second line of paragraph.
+ 3 - Second column of row 3.
+
+ - Second item in bullet
+ list (row 3, column 2).
+ 4 is a span
+ ------------
+ 5
+ ===== =====
+
+ Top and bottom borders use '=', column span underlines use '-', column
+ separation is indicated with spaces.
+
+ Passing the above table to the `parse()` method will result in the
+ following data structure, whose interpretation is the same as for
+ `GridTableParser`::
+
+ ([5, 25],
+ [[(0, 0, 1, ['col 1']),
+ (0, 0, 1, ['col 2'])]],
+ [[(0, 0, 3, ['1']),
+ (0, 0, 3, ['Second column of row 1.'])],
+ [(0, 0, 4, ['2']),
+ (0, 0, 4, ['Second column of row 2.',
+ 'Second line of paragraph.'])],
+ [(0, 0, 6, ['3']),
+ (0, 0, 6, ['- Second column of row 3.',
+ '',
+ '- Second item in bullet',
+ ' list (row 3, column 2).'])],
+ [(0, 1, 10, ['4 is a span'])],
+ [(0, 0, 12, ['5']),
+ (0, 0, 12, [''])]])
+ """
+
+ head_body_separator_pat = re.compile('=[ =]*$')
+ span_pat = re.compile('-[ -]*$')
+
+ def setup(self, block):
+ self.block = block[:] # make a copy; it will be modified
+ self.block.disconnect() # don't propagate changes to parent
+ # Convert top & bottom borders to column span underlines:
+ self.block[0] = self.block[0].replace('=', '-')
+ self.block[-1] = self.block[-1].replace('=', '-')
+ self.head_body_sep = None
+ self.columns = []
+ self.border_end = None
+ self.table = []
+ self.done = [-1] * len(block[0])
+ self.rowseps = {0: [0]}
+ self.colseps = {0: [0]}
+
+ def parse_table(self):
+ """
+ First determine the column boundaries from the top border, then
+ process rows. Each row may consist of multiple lines; accumulate
+ lines until a row is complete. Call `self.parse_row` to finish the
+ job.
+ """
+ # Top border must fully describe all table columns.
+ self.columns = self.parse_columns(self.block[0], 0)
+ self.border_end = self.columns[-1][1]
+ firststart, firstend = self.columns[0]
+ offset = 1 # skip top border
+ start = 1
+ text_found = None
+ while offset < len(self.block):
+ line = self.block[offset]
+ if self.span_pat.match(line):
+ # Column span underline or border; row is complete.
+ self.parse_row(self.block[start:offset], start,
+ (line.rstrip(), offset))
+ start = offset + 1
+ text_found = None
+ elif line[firststart:firstend].strip():
+ # First column not blank, therefore it's a new row.
+ if text_found and offset != start:
+ self.parse_row(self.block[start:offset], start)
+ start = offset
+ text_found = 1
+ elif not text_found:
+ start = offset + 1
+ offset += 1
+
+ def parse_columns(self, line, offset):
+ """
+ Given a column span underline, return a list of (begin, end) pairs.
+ """
+ cols = []
+ end = 0
+ while True:
+ begin = line.find('-', end)
+ end = line.find(' ', begin)
+ if begin < 0:
+ break
+ if end < 0:
+ end = len(line)
+ cols.append((begin, end))
+ if self.columns:
+ if cols[-1][1] != self.border_end:
+ raise TableMarkupError('Column span incomplete in table '
+ 'line %s.' % (offset+1),
+ offset=offset)
+ # Allow for an unbounded rightmost column:
+ cols[-1] = (cols[-1][0], self.columns[-1][1])
+ return cols
+
+ def init_row(self, colspec, offset):
+ i = 0
+ cells = []
+ for start, end in colspec:
+ morecols = 0
+ try:
+ assert start == self.columns[i][0]
+ while end != self.columns[i][1]:
+ i += 1
+ morecols += 1
+ except (AssertionError, IndexError):
+ raise TableMarkupError('Column span alignment problem '
+ 'in table line %s.' % (offset+2),
+ offset=offset+1)
+ cells.append([0, morecols, offset, []])
+ i += 1
+ return cells
+
+ def parse_row(self, lines, start, spanline=None):
+ """
+ Given the text `lines` of a row, parse it and append to `self.table`.
+
+ The row is parsed according to the current column spec (either
+ `spanline` if provided or `self.columns`). For each column, extract
+ text from each line, and check for text in column margins. Finally,
+ adjust for insignificant whitespace.
+ """
+ if not (lines or spanline):
+ # No new row, just blank lines.
+ return
+ if spanline:
+ columns = self.parse_columns(*spanline)
+ else:
+ columns = self.columns[:]
+ self.check_columns(lines, start, columns)
+ row = self.init_row(columns, start)
+ for i in range(len(columns)):
+ start, end = columns[i]
+ cellblock = lines.get_2D_block(0, start, len(lines), end)
+ cellblock.disconnect() # lines in cell can't sync with parent
+ cellblock.replace(self.double_width_pad_char, '')
+ row[i][3] = cellblock
+ self.table.append(row)
+
+ def check_columns(self, lines, first_line, columns):
+ """
+ Check for text in column margins and text overflow in the last column.
+ Raise TableMarkupError if anything but whitespace is in column margins.
+ Adjust the end value for the last column if there is text overflow.
+ """
+ # "Infinite" value for a dummy last column's beginning, used to
+ # check for text overflow:
+ columns.append((sys.maxsize, None))
+ lastcol = len(columns) - 2
+ # combining characters do not contribute to the column width
+ lines = [strip_combining_chars(line) for line in lines]
+
+ for i in range(len(columns) - 1):
+ start, end = columns[i]
+ nextstart = columns[i+1][0]
+ offset = 0
+ for line in lines:
+ if i == lastcol and line[end:].strip():
+ text = line[start:].rstrip()
+ new_end = start + len(text)
+ main_start, main_end = self.columns[-1]
+ columns[i] = (start, max(main_end, new_end))
+ if new_end > main_end:
+ self.columns[-1] = (main_start, new_end)
+ elif line[end:nextstart].strip():
+ raise TableMarkupError('Text in column margin in table '
+ 'line %s.' % (first_line+offset+1),
+ offset=first_line+offset)
+ offset += 1
+ columns.pop()
+
+ def structure_from_cells(self):
+ colspecs = [end - start for start, end in self.columns]
+ first_body_row = 0
+ if self.head_body_sep:
+ for i in range(len(self.table)):
+ if self.table[i][0][2] > self.head_body_sep:
+ first_body_row = i
+ break
+ return (colspecs, self.table[:first_body_row],
+ self.table[first_body_row:])
+
+
+def update_dict_of_lists(master, newdata):
+ """
+ Extend the list values of `master` with those from `newdata`.
+
+ Both parameters must be dictionaries containing list values.
+ """
+ for key, values in newdata.items():
+ master.setdefault(key, []).extend(values)
diff --git a/.venv/lib/python3.12/site-packages/docutils/readers/__init__.py b/.venv/lib/python3.12/site-packages/docutils/readers/__init__.py
new file mode 100644
index 00000000..9ab47840
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/readers/__init__.py
@@ -0,0 +1,113 @@
+# $Id: __init__.py 9258 2022-11-21 14:51:43Z milde $
+# Authors: David Goodger <goodger@python.org>; Ueli Schlaepfer
+# Copyright: This module has been placed in the public domain.
+
+"""
+This package contains Docutils Reader modules.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from importlib import import_module
+
+from docutils import utils, parsers, Component
+from docutils.transforms import universal
+
+
+class Reader(Component):
+
+ """
+ Abstract base class for docutils Readers.
+
+ Each reader module or package must export a subclass also called 'Reader'.
+
+ The two steps of a Reader's responsibility are to read data from the
+ source Input object and parse the data with the Parser object.
+ Call `read()` to process a document.
+ """
+
+ component_type = 'reader'
+ config_section = 'readers'
+
+ def get_transforms(self):
+ return super().get_transforms() + [universal.Decorations,
+ universal.ExposeInternals,
+ universal.StripComments]
+
+ def __init__(self, parser=None, parser_name=None):
+ """
+ Initialize the Reader instance.
+
+ Several instance attributes are defined with dummy initial values.
+ Subclasses may use these attributes as they wish.
+ """
+
+ self.parser = parser
+ """A `parsers.Parser` instance shared by all doctrees. May be left
+ unspecified if the document source determines the parser."""
+
+ if parser is None and parser_name:
+ self.set_parser(parser_name)
+
+ self.source = None
+ """`docutils.io` IO object, source of input data."""
+
+ self.input = None
+ """Raw text input; either a single string or, for more complex cases,
+ a collection of strings."""
+
+ def set_parser(self, parser_name):
+ """Set `self.parser` by name."""
+ parser_class = parsers.get_parser_class(parser_name)
+ self.parser = parser_class()
+
+ def read(self, source, parser, settings):
+ self.source = source
+ if not self.parser:
+ self.parser = parser
+ self.settings = settings
+ self.input = self.source.read()
+ self.parse()
+ return self.document
+
+ def parse(self):
+ """Parse `self.input` into a document tree."""
+ self.document = document = self.new_document()
+ self.parser.parse(self.input, document)
+ document.current_source = document.current_line = None
+
+ def new_document(self):
+ """Create and return a new empty document tree (root node)."""
+ return utils.new_document(self.source.source_path, self.settings)
+
+
+class ReReader(Reader):
+
+ """
+ A reader which rereads an existing document tree (e.g. a
+ deserializer).
+
+ Often used in conjunction with `writers.UnfilteredWriter`.
+ """
+
+ def get_transforms(self):
+ # Do not add any transforms. They have already been applied
+ # by the reader which originally created the document.
+ return Component.get_transforms(self)
+
+
+_reader_aliases = {}
+
+
+def get_reader_class(reader_name):
+ """Return the Reader class from the `reader_name` module."""
+ name = reader_name.lower()
+ name = _reader_aliases.get(name, name)
+ try:
+ module = import_module('docutils.readers.'+name)
+ except ImportError:
+ try:
+ module = import_module(name)
+ except ImportError as err:
+ raise ImportError(f'Reader "{reader_name}" not found. {err}')
+ return module.Reader
diff --git a/.venv/lib/python3.12/site-packages/docutils/readers/doctree.py b/.venv/lib/python3.12/site-packages/docutils/readers/doctree.py
new file mode 100644
index 00000000..f8d37269
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/readers/doctree.py
@@ -0,0 +1,46 @@
+# $Id: doctree.py 4564 2006-05-21 20:44:42Z wiemann $
+# Author: Martin Blais <blais@furius.ca>
+# Copyright: This module has been placed in the public domain.
+
+"""Reader for existing document trees."""
+
+from docutils import readers, utils, transforms
+
+
+class Reader(readers.ReReader):
+
+ """
+ Adapt the Reader API for an existing document tree.
+
+ The existing document tree must be passed as the ``source`` parameter to
+ the `docutils.core.Publisher` initializer, wrapped in a
+ `docutils.io.DocTreeInput` object::
+
+ pub = docutils.core.Publisher(
+ ..., source=docutils.io.DocTreeInput(document), ...)
+
+ The original document settings are overridden; if you want to use the
+ settings of the original document, pass ``settings=document.settings`` to
+ the Publisher call above.
+ """
+
+ supported = ('doctree',)
+
+ config_section = 'doctree reader'
+ config_section_dependencies = ('readers',)
+
+ def parse(self):
+ """
+ No parsing to do; refurbish the document tree instead.
+ Overrides the inherited method.
+ """
+ self.document = self.input
+ # Create fresh Transformer object, to be populated from Writer
+ # component.
+ self.document.transformer = transforms.Transformer(self.document)
+ # Replace existing settings object with new one.
+ self.document.settings = self.settings
+ # Create fresh Reporter object because it is dependent on
+ # (new) settings.
+ self.document.reporter = utils.new_reporter(
+ self.document.get('source', ''), self.document.settings)
diff --git a/.venv/lib/python3.12/site-packages/docutils/readers/pep.py b/.venv/lib/python3.12/site-packages/docutils/readers/pep.py
new file mode 100644
index 00000000..3540f83a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/readers/pep.py
@@ -0,0 +1,48 @@
+# $Id: pep.py 9258 2022-11-21 14:51:43Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Python Enhancement Proposal (PEP) Reader.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from docutils.readers import standalone
+from docutils.transforms import peps, frontmatter
+from docutils.parsers import rst
+
+
+class Reader(standalone.Reader):
+
+ supported = ('pep',)
+ """Contexts this reader supports."""
+
+ settings_spec = (
+ 'PEP Reader Option Defaults',
+ 'The --pep-references and --rfc-references options (for the '
+ 'reStructuredText parser) are on by default.',
+ ())
+
+ config_section = 'pep reader'
+ config_section_dependencies = ('readers', 'standalone reader')
+
+ def get_transforms(self):
+ transforms = super().get_transforms()
+ # We have PEP-specific frontmatter handling.
+ transforms.remove(frontmatter.DocTitle)
+ transforms.remove(frontmatter.SectionSubTitle)
+ transforms.remove(frontmatter.DocInfo)
+ transforms.extend([peps.Headers, peps.Contents, peps.TargetNotes])
+ return transforms
+
+ settings_default_overrides = {'pep_references': 1, 'rfc_references': 1}
+
+ inliner_class = rst.states.Inliner
+
+ def __init__(self, parser=None, parser_name=None):
+ """`parser` should be ``None``."""
+ if parser is None:
+ parser = rst.Parser(rfc2822=True, inliner=self.inliner_class())
+ standalone.Reader.__init__(self, parser, '')
diff --git a/.venv/lib/python3.12/site-packages/docutils/readers/standalone.py b/.venv/lib/python3.12/site-packages/docutils/readers/standalone.py
new file mode 100644
index 00000000..a6dfd6d7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/readers/standalone.py
@@ -0,0 +1,65 @@
+# $Id: standalone.py 9539 2024-02-17 10:36:51Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Standalone file Reader for the reStructuredText markup syntax.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from docutils import frontend, readers
+from docutils.transforms import frontmatter, references, misc
+
+
+class Reader(readers.Reader):
+
+ supported = ('standalone',)
+ """Contexts this reader supports."""
+
+ document = None
+ """A single document tree."""
+
+ settings_spec = (
+ 'Standalone Reader Options',
+ None,
+ (('Disable the promotion of a lone top-level section title to '
+ 'document title (and subsequent section title to document '
+ 'subtitle promotion; enabled by default).',
+ ['--no-doc-title'],
+ {'dest': 'doctitle_xform', 'action': 'store_false',
+ 'default': True, 'validator': frontend.validate_boolean}),
+ ('Disable the bibliographic field list transform (enabled by '
+ 'default).',
+ ['--no-doc-info'],
+ {'dest': 'docinfo_xform', 'action': 'store_false',
+ 'default': True, 'validator': frontend.validate_boolean}),
+ ('Activate the promotion of lone subsection titles to '
+ 'section subtitles (disabled by default).',
+ ['--section-subtitles'],
+ {'dest': 'sectsubtitle_xform', 'action': 'store_true',
+ 'default': False, 'validator': frontend.validate_boolean}),
+ ('Deactivate the promotion of lone subsection titles.',
+ ['--no-section-subtitles'],
+ {'dest': 'sectsubtitle_xform', 'action': 'store_false'}),
+ ))
+
+ config_section = 'standalone reader'
+ config_section_dependencies = ('readers',)
+
+ def get_transforms(self):
+ return super().get_transforms() + [
+ references.Substitutions,
+ references.PropagateTargets,
+ frontmatter.DocTitle,
+ frontmatter.SectionSubTitle,
+ frontmatter.DocInfo,
+ references.AnonymousHyperlinks,
+ references.IndirectHyperlinks,
+ references.Footnotes,
+ references.ExternalTargets,
+ references.InternalTargets,
+ references.DanglingReferences,
+ misc.Transitions,
+ ]
diff --git a/.venv/lib/python3.12/site-packages/docutils/statemachine.py b/.venv/lib/python3.12/site-packages/docutils/statemachine.py
new file mode 100644
index 00000000..abdecfa7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/statemachine.py
@@ -0,0 +1,1525 @@
+# $Id: statemachine.py 9072 2022-06-15 11:31:09Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+A finite state machine specialized for regular-expression-based text filters,
+this module defines the following classes:
+
+- `StateMachine`, a state machine
+- `State`, a state superclass
+- `StateMachineWS`, a whitespace-sensitive version of `StateMachine`
+- `StateWS`, a state superclass for use with `StateMachineWS`
+- `SearchStateMachine`, uses `re.search()` instead of `re.match()`
+- `SearchStateMachineWS`, uses `re.search()` instead of `re.match()`
+- `ViewList`, extends standard Python lists.
+- `StringList`, string-specific ViewList.
+
+Exception classes:
+
+- `StateMachineError`
+- `UnknownStateError`
+- `DuplicateStateError`
+- `UnknownTransitionError`
+- `DuplicateTransitionError`
+- `TransitionPatternNotFound`
+- `TransitionMethodNotFound`
+- `UnexpectedIndentationError`
+- `TransitionCorrection`: Raised to switch to another transition.
+- `StateCorrection`: Raised to switch to another state & transition.
+
+Functions:
+
+- `string2lines()`: split a multi-line string into a list of one-line strings
+
+
+How To Use This Module
+======================
+(See the individual classes, methods, and attributes for details.)
+
+1. Import it: ``import statemachine`` or ``from statemachine import ...``.
+ You will also need to ``import re``.
+
+2. Derive a subclass of `State` (or `StateWS`) for each state in your state
+ machine::
+
+ class MyState(statemachine.State):
+
+ Within the state's class definition:
+
+ a) Include a pattern for each transition, in `State.patterns`::
+
+ patterns = {'atransition': r'pattern', ...}
+
+ b) Include a list of initial transitions to be set up automatically, in
+ `State.initial_transitions`::
+
+ initial_transitions = ['atransition', ...]
+
+ c) Define a method for each transition, with the same name as the
+ transition pattern::
+
+ def atransition(self, match, context, next_state):
+ # do something
+ result = [...] # a list
+ return context, next_state, result
+ # context, next_state may be altered
+
+ Transition methods may raise an `EOFError` to cut processing short.
+
+ d) You may wish to override the `State.bof()` and/or `State.eof()` implicit
+ transition methods, which handle the beginning- and end-of-file.
+
+ e) In order to handle nested processing, you may wish to override the
+ attributes `State.nested_sm` and/or `State.nested_sm_kwargs`.
+
+ If you are using `StateWS` as a base class, in order to handle nested
+ indented blocks, you may wish to:
+
+ - override the attributes `StateWS.indent_sm`,
+ `StateWS.indent_sm_kwargs`, `StateWS.known_indent_sm`, and/or
+ `StateWS.known_indent_sm_kwargs`;
+ - override the `StateWS.blank()` method; and/or
+ - override or extend the `StateWS.indent()`, `StateWS.known_indent()`,
+ and/or `StateWS.firstknown_indent()` methods.
+
+3. Create a state machine object::
+
+ sm = StateMachine(state_classes=[MyState, ...],
+ initial_state='MyState')
+
+4. Obtain the input text, which needs to be converted into a tab-free list of
+ one-line strings. For example, to read text from a file called
+ 'inputfile'::
+
+ with open('inputfile', encoding='utf-8') as fp:
+ input_string = fp.read()
+ input_lines = statemachine.string2lines(input_string)
+
+5. Run the state machine on the input text and collect the results, a list::
+
+ results = sm.run(input_lines)
+
+6. Remove any lingering circular references::
+
+ sm.unlink()
+"""
+
+__docformat__ = 'restructuredtext'
+
+import sys
+import re
+from unicodedata import east_asian_width
+
+from docutils import utils
+
+
+class StateMachine:
+
+ """
+ A finite state machine for text filters using regular expressions.
+
+ The input is provided in the form of a list of one-line strings (no
+ newlines). States are subclasses of the `State` class. Transitions consist
+ of regular expression patterns and transition methods, and are defined in
+ each state.
+
+ The state machine is started with the `run()` method, which returns the
+ results of processing in a list.
+ """
+
+ def __init__(self, state_classes, initial_state, debug=False):
+ """
+ Initialize a `StateMachine` object; add state objects.
+
+ Parameters:
+
+ - `state_classes`: a list of `State` (sub)classes.
+ - `initial_state`: a string, the class name of the initial state.
+ - `debug`: a boolean; produce verbose output if true (nonzero).
+ """
+
+ self.input_lines = None
+ """`StringList` of input lines (without newlines).
+ Filled by `self.run()`."""
+
+ self.input_offset = 0
+ """Offset of `self.input_lines` from the beginning of the file."""
+
+ self.line = None
+ """Current input line."""
+
+ self.line_offset = -1
+ """Current input line offset from beginning of `self.input_lines`."""
+
+ self.debug = debug
+ """Debugging mode on/off."""
+
+ self.initial_state = initial_state
+ """The name of the initial state (key to `self.states`)."""
+
+ self.current_state = initial_state
+ """The name of the current state (key to `self.states`)."""
+
+ self.states = {}
+ """Mapping of {state_name: State_object}."""
+
+ self.add_states(state_classes)
+
+ self.observers = []
+ """List of bound methods or functions to call whenever the current
+ line changes. Observers are called with one argument, ``self``.
+ Cleared at the end of `run()`."""
+
+ def unlink(self):
+ """Remove circular references to objects no longer required."""
+ for state in self.states.values():
+ state.unlink()
+ self.states = None
+
+ def run(self, input_lines, input_offset=0, context=None,
+ input_source=None, initial_state=None):
+ """
+ Run the state machine on `input_lines`. Return results (a list).
+
+ Reset `self.line_offset` and `self.current_state`. Run the
+ beginning-of-file transition. Input one line at a time and check for a
+ matching transition. If a match is found, call the transition method
+ and possibly change the state. Store the context returned by the
+ transition method to be passed on to the next transition matched.
+ Accumulate the results returned by the transition methods in a list.
+ Run the end-of-file transition. Finally, return the accumulated
+ results.
+
+ Parameters:
+
+ - `input_lines`: a list of strings without newlines, or `StringList`.
+ - `input_offset`: the line offset of `input_lines` from the beginning
+ of the file.
+ - `context`: application-specific storage.
+ - `input_source`: name or path of source of `input_lines`.
+ - `initial_state`: name of initial state.
+ """
+ self.runtime_init()
+ if isinstance(input_lines, StringList):
+ self.input_lines = input_lines
+ else:
+ self.input_lines = StringList(input_lines, source=input_source)
+ self.input_offset = input_offset
+ self.line_offset = -1
+ self.current_state = initial_state or self.initial_state
+ if self.debug:
+ print('\nStateMachine.run: input_lines (line_offset=%s):\n| %s'
+ % (self.line_offset, '\n| '.join(self.input_lines)),
+ file=sys.stderr)
+ transitions = None
+ results = []
+ state = self.get_state()
+ try:
+ if self.debug:
+ print('\nStateMachine.run: bof transition', file=sys.stderr)
+ context, result = state.bof(context)
+ results.extend(result)
+ while True:
+ try:
+ try:
+ self.next_line()
+ if self.debug:
+ source, offset = self.input_lines.info(
+ self.line_offset)
+ print(f'\nStateMachine.run: line '
+ f'(source={source!r}, offset={offset!r}):\n'
+ f'| {self.line}', file=sys.stderr)
+ context, next_state, result = self.check_line(
+ context, state, transitions)
+ except EOFError:
+ if self.debug:
+ print('\nStateMachine.run: %s.eof transition'
+ % state.__class__.__name__, file=sys.stderr)
+ result = state.eof(context)
+ results.extend(result)
+ break
+ else:
+ results.extend(result)
+ except TransitionCorrection as exception:
+ self.previous_line() # back up for another try
+ transitions = (exception.args[0],)
+ if self.debug:
+ print('\nStateMachine.run: TransitionCorrection to '
+ f'state "{state.__class__.__name__}", '
+ f'transition {transitions[0]}.',
+ file=sys.stderr)
+ continue
+ except StateCorrection as exception:
+ self.previous_line() # back up for another try
+ next_state = exception.args[0]
+ if len(exception.args) == 1:
+ transitions = None
+ else:
+ transitions = (exception.args[1],)
+ if self.debug:
+ print('\nStateMachine.run: StateCorrection to state '
+ f'"{next_state}", transition {transitions[0]}.',
+ file=sys.stderr)
+ else:
+ transitions = None
+ state = self.get_state(next_state)
+ except: # noqa catchall
+ if self.debug:
+ self.error()
+ raise
+ self.observers = []
+ return results
+
+ def get_state(self, next_state=None):
+ """
+ Return current state object; set it first if `next_state` given.
+
+ Parameter `next_state`: a string, the name of the next state.
+
+ Exception: `UnknownStateError` raised if `next_state` unknown.
+ """
+ if next_state:
+ if self.debug and next_state != self.current_state:
+ print('\nStateMachine.get_state: Changing state from '
+ '"%s" to "%s" (input line %s).'
+ % (self.current_state, next_state,
+ self.abs_line_number()), file=sys.stderr)
+ self.current_state = next_state
+ try:
+ return self.states[self.current_state]
+ except KeyError:
+ raise UnknownStateError(self.current_state)
+
+ def next_line(self, n=1):
+ """Load `self.line` with the `n`'th next line and return it."""
+ try:
+ try:
+ self.line_offset += n
+ self.line = self.input_lines[self.line_offset]
+ except IndexError:
+ self.line = None
+ raise EOFError
+ return self.line
+ finally:
+ self.notify_observers()
+
+ def is_next_line_blank(self):
+ """Return True if the next line is blank or non-existent."""
+ try:
+ return not self.input_lines[self.line_offset + 1].strip()
+ except IndexError:
+ return 1
+
+ def at_eof(self):
+ """Return 1 if the input is at or past end-of-file."""
+ return self.line_offset >= len(self.input_lines) - 1
+
+ def at_bof(self):
+ """Return 1 if the input is at or before beginning-of-file."""
+ return self.line_offset <= 0
+
+ def previous_line(self, n=1):
+ """Load `self.line` with the `n`'th previous line and return it."""
+ self.line_offset -= n
+ if self.line_offset < 0:
+ self.line = None
+ else:
+ self.line = self.input_lines[self.line_offset]
+ self.notify_observers()
+ return self.line
+
+ def goto_line(self, line_offset):
+ """Jump to absolute line offset `line_offset`, load and return it."""
+ try:
+ try:
+ self.line_offset = line_offset - self.input_offset
+ self.line = self.input_lines[self.line_offset]
+ except IndexError:
+ self.line = None
+ raise EOFError
+ return self.line
+ finally:
+ self.notify_observers()
+
+ def get_source(self, line_offset):
+ """Return source of line at absolute line offset `line_offset`."""
+ return self.input_lines.source(line_offset - self.input_offset)
+
+ def abs_line_offset(self):
+ """Return line offset of current line, from beginning of file."""
+ return self.line_offset + self.input_offset
+
+ def abs_line_number(self):
+ """Return line number of current line (counting from 1)."""
+ return self.line_offset + self.input_offset + 1
+
+ def get_source_and_line(self, lineno=None):
+ """Return (source, line) tuple for current or given line number.
+
+ Looks up the source and line number in the `self.input_lines`
+ StringList instance to count for included source files.
+
+ If the optional argument `lineno` is given, convert it from an
+ absolute line number to the corresponding (source, line) pair.
+ """
+ if lineno is None:
+ offset = self.line_offset
+ else:
+ offset = lineno - self.input_offset - 1
+ try:
+ src, srcoffset = self.input_lines.info(offset)
+ srcline = srcoffset + 1
+ except TypeError:
+ # line is None if index is "Just past the end"
+ src, srcline = self.get_source_and_line(offset + self.input_offset)
+ return src, srcline + 1
+ except IndexError: # `offset` is off the list
+ src, srcline = None, None
+ # raise AssertionError('cannot find line %d in %s lines' %
+ # (offset, len(self.input_lines)))
+ # # list(self.input_lines.lines())))
+ return src, srcline
+
+ def insert_input(self, input_lines, source):
+ self.input_lines.insert(self.line_offset + 1, '',
+ source='internal padding after '+source,
+ offset=len(input_lines))
+ self.input_lines.insert(self.line_offset + 1, '',
+ source='internal padding before '+source,
+ offset=-1)
+ self.input_lines.insert(self.line_offset + 2,
+ StringList(input_lines, source))
+
+ def get_text_block(self, flush_left=False):
+ """
+ Return a contiguous block of text.
+
+ If `flush_left` is true, raise `UnexpectedIndentationError` if an
+ indented line is encountered before the text block ends (with a blank
+ line).
+ """
+ try:
+ block = self.input_lines.get_text_block(self.line_offset,
+ flush_left)
+ self.next_line(len(block) - 1)
+ return block
+ except UnexpectedIndentationError as err:
+ block = err.args[0]
+ self.next_line(len(block) - 1) # advance to last line of block
+ raise
+
+ def check_line(self, context, state, transitions=None):
+ """
+ Examine one line of input for a transition match & execute its method.
+
+ Parameters:
+
+ - `context`: application-dependent storage.
+ - `state`: a `State` object, the current state.
+ - `transitions`: an optional ordered list of transition names to try,
+ instead of ``state.transition_order``.
+
+ Return the values returned by the transition method:
+
+ - context: possibly modified from the parameter `context`;
+ - next state name (`State` subclass name);
+ - the result output of the transition, a list.
+
+ When there is no match, ``state.no_match()`` is called and its return
+ value is returned.
+ """
+ if transitions is None:
+ transitions = state.transition_order
+ if self.debug:
+ print('\nStateMachine.check_line: state="%s", transitions=%r.'
+ % (state.__class__.__name__, transitions), file=sys.stderr)
+ for name in transitions:
+ pattern, method, next_state = state.transitions[name]
+ match = pattern.match(self.line)
+ if match:
+ if self.debug:
+ print('\nStateMachine.check_line: Matched transition '
+ f'"{name}" in state "{state.__class__.__name__}".',
+ file=sys.stderr)
+ return method(match, context, next_state)
+ else:
+ if self.debug:
+ print('\nStateMachine.check_line: No match in state "%s".'
+ % state.__class__.__name__, file=sys.stderr)
+ return state.no_match(context, transitions)
+
+ def add_state(self, state_class):
+ """
+ Initialize & add a `state_class` (`State` subclass) object.
+
+ Exception: `DuplicateStateError` raised if `state_class` was already
+ added.
+ """
+ statename = state_class.__name__
+ if statename in self.states:
+ raise DuplicateStateError(statename)
+ self.states[statename] = state_class(self, self.debug)
+
+ def add_states(self, state_classes):
+ """
+ Add `state_classes` (a list of `State` subclasses).
+ """
+ for state_class in state_classes:
+ self.add_state(state_class)
+
+ def runtime_init(self):
+ """
+ Initialize `self.states`.
+ """
+ for state in self.states.values():
+ state.runtime_init()
+
+ def error(self):
+ """Report error details."""
+ type, value, module, line, function = _exception_data()
+ print('%s: %s' % (type, value), file=sys.stderr)
+ print('input line %s' % (self.abs_line_number()), file=sys.stderr)
+ print('module %s, line %s, function %s' % (module, line, function),
+ file=sys.stderr)
+
+ def attach_observer(self, observer):
+ """
+ The `observer` parameter is a function or bound method which takes two
+ arguments, the source and offset of the current line.
+ """
+ self.observers.append(observer)
+
+ def detach_observer(self, observer):
+ self.observers.remove(observer)
+
+ def notify_observers(self):
+ for observer in self.observers:
+ try:
+ info = self.input_lines.info(self.line_offset)
+ except IndexError:
+ info = (None, None)
+ observer(*info)
+
+
+class State:
+
+ """
+ State superclass. Contains a list of transitions, and transition methods.
+
+ Transition methods all have the same signature. They take 3 parameters:
+
+ - An `re` match object. ``match.string`` contains the matched input line,
+ ``match.start()`` gives the start index of the match, and
+ ``match.end()`` gives the end index.
+ - A context object, whose meaning is application-defined (initial value
+ ``None``). It can be used to store any information required by the state
+ machine, and the returned context is passed on to the next transition
+ method unchanged.
+ - The name of the next state, a string, taken from the transitions list;
+ normally it is returned unchanged, but it may be altered by the
+ transition method if necessary.
+
+ Transition methods all return a 3-tuple:
+
+ - A context object, as (potentially) modified by the transition method.
+ - The next state name (a return value of ``None`` means no state change).
+ - The processing result, a list, which is accumulated by the state
+ machine.
+
+ Transition methods may raise an `EOFError` to cut processing short.
+
+ There are two implicit transitions, and corresponding transition methods
+ are defined: `bof()` handles the beginning-of-file, and `eof()` handles
+ the end-of-file. These methods have non-standard signatures and return
+ values. `bof()` returns the initial context and results, and may be used
+ to return a header string, or do any other processing needed. `eof()`
+ should handle any remaining context and wrap things up; it returns the
+ final processing result.
+
+ Typical applications need only subclass `State` (or a subclass), set the
+ `patterns` and `initial_transitions` class attributes, and provide
+ corresponding transition methods. The default object initialization will
+ take care of constructing the list of transitions.
+ """
+
+ patterns = None
+ """
+ {Name: pattern} mapping, used by `make_transition()`. Each pattern may
+ be a string or a compiled `re` pattern. Override in subclasses.
+ """
+
+ initial_transitions = None
+ """
+ A list of transitions to initialize when a `State` is instantiated.
+ Each entry is either a transition name string, or a (transition name, next
+ state name) pair. See `make_transitions()`. Override in subclasses.
+ """
+
+ nested_sm = None
+ """
+ The `StateMachine` class for handling nested processing.
+
+ If left as ``None``, `nested_sm` defaults to the class of the state's
+ controlling state machine. Override it in subclasses to avoid the default.
+ """
+
+ nested_sm_kwargs = None
+ """
+ Keyword arguments dictionary, passed to the `nested_sm` constructor.
+
+ Two keys must have entries in the dictionary:
+
+ - Key 'state_classes' must be set to a list of `State` classes.
+ - Key 'initial_state' must be set to the name of the initial state class.
+
+ If `nested_sm_kwargs` is left as ``None``, 'state_classes' defaults to the
+ class of the current state, and 'initial_state' defaults to the name of
+ the class of the current state. Override in subclasses to avoid the
+ defaults.
+ """
+
+ def __init__(self, state_machine, debug=False):
+ """
+ Initialize a `State` object; make & add initial transitions.
+
+ Parameters:
+
+ - `statemachine`: the controlling `StateMachine` object.
+ - `debug`: a boolean; produce verbose output if true.
+ """
+
+ self.transition_order = []
+ """A list of transition names in search order."""
+
+ self.transitions = {}
+ """
+ A mapping of transition names to 3-tuples containing
+ (compiled_pattern, transition_method, next_state_name). Initialized as
+ an instance attribute dynamically (instead of as a class attribute)
+ because it may make forward references to patterns and methods in this
+ or other classes.
+ """
+
+ self.add_initial_transitions()
+
+ self.state_machine = state_machine
+ """A reference to the controlling `StateMachine` object."""
+
+ self.debug = debug
+ """Debugging mode on/off."""
+
+ if self.nested_sm is None:
+ self.nested_sm = self.state_machine.__class__
+ if self.nested_sm_kwargs is None:
+ self.nested_sm_kwargs = {'state_classes': [self.__class__],
+ 'initial_state': self.__class__.__name__}
+
+ def runtime_init(self):
+ """
+ Initialize this `State` before running the state machine; called from
+ `self.state_machine.run()`.
+ """
+ pass
+
+ def unlink(self):
+ """Remove circular references to objects no longer required."""
+ self.state_machine = None
+
+ def add_initial_transitions(self):
+ """Make and add transitions listed in `self.initial_transitions`."""
+ if self.initial_transitions:
+ names, transitions = self.make_transitions(
+ self.initial_transitions)
+ self.add_transitions(names, transitions)
+
+ def add_transitions(self, names, transitions):
+ """
+ Add a list of transitions to the start of the transition list.
+
+ Parameters:
+
+ - `names`: a list of transition names.
+ - `transitions`: a mapping of names to transition tuples.
+
+ Exceptions: `DuplicateTransitionError`, `UnknownTransitionError`.
+ """
+ for name in names:
+ if name in self.transitions:
+ raise DuplicateTransitionError(name)
+ if name not in transitions:
+ raise UnknownTransitionError(name)
+ self.transition_order[:0] = names
+ self.transitions.update(transitions)
+
+ def add_transition(self, name, transition):
+ """
+ Add a transition to the start of the transition list.
+
+ Parameter `transition`: a ready-made transition 3-tuple.
+
+ Exception: `DuplicateTransitionError`.
+ """
+ if name in self.transitions:
+ raise DuplicateTransitionError(name)
+ self.transition_order[:0] = [name]
+ self.transitions[name] = transition
+
+ def remove_transition(self, name):
+ """
+ Remove a transition by `name`.
+
+ Exception: `UnknownTransitionError`.
+ """
+ try:
+ del self.transitions[name]
+ self.transition_order.remove(name)
+ except: # noqa catchall
+ raise UnknownTransitionError(name)
+
+ def make_transition(self, name, next_state=None):
+ """
+ Make & return a transition tuple based on `name`.
+
+ This is a convenience function to simplify transition creation.
+
+ Parameters:
+
+ - `name`: a string, the name of the transition pattern & method. This
+ `State` object must have a method called '`name`', and a dictionary
+ `self.patterns` containing a key '`name`'.
+ - `next_state`: a string, the name of the next `State` object for this
+ transition. A value of ``None`` (or absent) implies no state change
+ (i.e., continue with the same state).
+
+ Exceptions: `TransitionPatternNotFound`, `TransitionMethodNotFound`.
+ """
+ if next_state is None:
+ next_state = self.__class__.__name__
+ try:
+ pattern = self.patterns[name]
+ if not hasattr(pattern, 'match'):
+ pattern = self.patterns[name] = re.compile(pattern)
+ except KeyError:
+ raise TransitionPatternNotFound(
+ '%s.patterns[%r]' % (self.__class__.__name__, name))
+ try:
+ method = getattr(self, name)
+ except AttributeError:
+ raise TransitionMethodNotFound(
+ '%s.%s' % (self.__class__.__name__, name))
+ return pattern, method, next_state
+
+ def make_transitions(self, name_list):
+ """
+ Return a list of transition names and a transition mapping.
+
+ Parameter `name_list`: a list, where each entry is either a transition
+ name string, or a 1- or 2-tuple (transition name, optional next state
+ name).
+ """
+ names = []
+ transitions = {}
+ for namestate in name_list:
+ if isinstance(namestate, str):
+ transitions[namestate] = self.make_transition(namestate)
+ names.append(namestate)
+ else:
+ transitions[namestate[0]] = self.make_transition(*namestate)
+ names.append(namestate[0])
+ return names, transitions
+
+ def no_match(self, context, transitions):
+ """
+ Called when there is no match from `StateMachine.check_line()`.
+
+ Return the same values returned by transition methods:
+
+ - context: unchanged;
+ - next state name: ``None``;
+ - empty result list.
+
+ Override in subclasses to catch this event.
+ """
+ return context, None, []
+
+ def bof(self, context):
+ """
+ Handle beginning-of-file. Return unchanged `context`, empty result.
+
+ Override in subclasses.
+
+ Parameter `context`: application-defined storage.
+ """
+ return context, []
+
+ def eof(self, context):
+ """
+ Handle end-of-file. Return empty result.
+
+ Override in subclasses.
+
+ Parameter `context`: application-defined storage.
+ """
+ return []
+
+ def nop(self, match, context, next_state):
+ """
+ A "do nothing" transition method.
+
+ Return unchanged `context` & `next_state`, empty result. Useful for
+ simple state changes (actionless transitions).
+ """
+ return context, next_state, []
+
+
+class StateMachineWS(StateMachine):
+
+ """
+ `StateMachine` subclass specialized for whitespace recognition.
+
+ There are three methods provided for extracting indented text blocks:
+
+ - `get_indented()`: use when the indent is unknown.
+ - `get_known_indented()`: use when the indent is known for all lines.
+ - `get_first_known_indented()`: use when only the first line's indent is
+ known.
+ """
+
+ def get_indented(self, until_blank=False, strip_indent=True):
+ """
+ Return a block of indented lines of text, and info.
+
+ Extract an indented block where the indent is unknown for all lines.
+
+ :Parameters:
+ - `until_blank`: Stop collecting at the first blank line if true.
+ - `strip_indent`: Strip common leading indent if true (default).
+
+ :Return:
+ - the indented block (a list of lines of text),
+ - its indent,
+ - its first line offset from BOF, and
+ - whether or not it finished with a blank line.
+ """
+ offset = self.abs_line_offset()
+ indented, indent, blank_finish = self.input_lines.get_indented(
+ self.line_offset, until_blank, strip_indent)
+ if indented:
+ self.next_line(len(indented) - 1) # advance to last indented line
+ while indented and not indented[0].strip():
+ indented.trim_start()
+ offset += 1
+ return indented, indent, offset, blank_finish
+
+ def get_known_indented(self, indent, until_blank=False, strip_indent=True):
+ """
+ Return an indented block and info.
+
+ Extract an indented block where the indent is known for all lines.
+ Starting with the current line, extract the entire text block with at
+ least `indent` indentation (which must be whitespace, except for the
+ first line).
+
+ :Parameters:
+ - `indent`: The number of indent columns/characters.
+ - `until_blank`: Stop collecting at the first blank line if true.
+ - `strip_indent`: Strip `indent` characters of indentation if true
+ (default).
+
+ :Return:
+ - the indented block,
+ - its first line offset from BOF, and
+ - whether or not it finished with a blank line.
+ """
+ offset = self.abs_line_offset()
+ indented, indent, blank_finish = self.input_lines.get_indented(
+ self.line_offset, until_blank, strip_indent,
+ block_indent=indent)
+ self.next_line(len(indented) - 1) # advance to last indented line
+ while indented and not indented[0].strip():
+ indented.trim_start()
+ offset += 1
+ return indented, offset, blank_finish
+
+ def get_first_known_indented(self, indent, until_blank=False,
+ strip_indent=True, strip_top=True):
+ """
+ Return an indented block and info.
+
+ Extract an indented block where the indent is known for the first line
+ and unknown for all other lines.
+
+ :Parameters:
+ - `indent`: The first line's indent (# of columns/characters).
+ - `until_blank`: Stop collecting at the first blank line if true
+ (1).
+ - `strip_indent`: Strip `indent` characters of indentation if true
+ (1, default).
+ - `strip_top`: Strip blank lines from the beginning of the block.
+
+ :Return:
+ - the indented block,
+ - its indent,
+ - its first line offset from BOF, and
+ - whether or not it finished with a blank line.
+ """
+ offset = self.abs_line_offset()
+ indented, indent, blank_finish = self.input_lines.get_indented(
+ self.line_offset, until_blank, strip_indent,
+ first_indent=indent)
+ self.next_line(len(indented) - 1) # advance to last indented line
+ if strip_top:
+ while indented and not indented[0].strip():
+ indented.trim_start()
+ offset += 1
+ return indented, indent, offset, blank_finish
+
+
+class StateWS(State):
+
+ """
+ State superclass specialized for whitespace (blank lines & indents).
+
+ Use this class with `StateMachineWS`. The transitions 'blank' (for blank
+ lines) and 'indent' (for indented text blocks) are added automatically,
+ before any other transitions. The transition method `blank()` handles
+ blank lines and `indent()` handles nested indented blocks. Indented
+ blocks trigger a new state machine to be created by `indent()` and run.
+ The class of the state machine to be created is in `indent_sm`, and the
+ constructor keyword arguments are in the dictionary `indent_sm_kwargs`.
+
+ The methods `known_indent()` and `firstknown_indent()` are provided for
+ indented blocks where the indent (all lines' and first line's only,
+ respectively) is known to the transition method, along with the attributes
+ `known_indent_sm` and `known_indent_sm_kwargs`. Neither transition method
+ is triggered automatically.
+ """
+
+ indent_sm = None
+ """
+ The `StateMachine` class handling indented text blocks.
+
+ If left as ``None``, `indent_sm` defaults to the value of
+ `State.nested_sm`. Override it in subclasses to avoid the default.
+ """
+
+ indent_sm_kwargs = None
+ """
+ Keyword arguments dictionary, passed to the `indent_sm` constructor.
+
+ If left as ``None``, `indent_sm_kwargs` defaults to the value of
+ `State.nested_sm_kwargs`. Override it in subclasses to avoid the default.
+ """
+
+ known_indent_sm = None
+ """
+ The `StateMachine` class handling known-indented text blocks.
+
+ If left as ``None``, `known_indent_sm` defaults to the value of
+ `indent_sm`. Override it in subclasses to avoid the default.
+ """
+
+ known_indent_sm_kwargs = None
+ """
+ Keyword arguments dictionary, passed to the `known_indent_sm` constructor.
+
+ If left as ``None``, `known_indent_sm_kwargs` defaults to the value of
+ `indent_sm_kwargs`. Override it in subclasses to avoid the default.
+ """
+
+ ws_patterns = {'blank': re.compile(' *$'),
+ 'indent': re.compile(' +')}
+ """Patterns for default whitespace transitions. May be overridden in
+ subclasses."""
+
+ ws_initial_transitions = ('blank', 'indent')
+ """Default initial whitespace transitions, added before those listed in
+ `State.initial_transitions`. May be overridden in subclasses."""
+
+ def __init__(self, state_machine, debug=False):
+ """
+ Initialize a `StateSM` object; extends `State.__init__()`.
+
+ Check for indent state machine attributes, set defaults if not set.
+ """
+ State.__init__(self, state_machine, debug)
+ if self.indent_sm is None:
+ self.indent_sm = self.nested_sm
+ if self.indent_sm_kwargs is None:
+ self.indent_sm_kwargs = self.nested_sm_kwargs
+ if self.known_indent_sm is None:
+ self.known_indent_sm = self.indent_sm
+ if self.known_indent_sm_kwargs is None:
+ self.known_indent_sm_kwargs = self.indent_sm_kwargs
+
+ def add_initial_transitions(self):
+ """
+ Add whitespace-specific transitions before those defined in subclass.
+
+ Extends `State.add_initial_transitions()`.
+ """
+ State.add_initial_transitions(self)
+ if self.patterns is None:
+ self.patterns = {}
+ self.patterns.update(self.ws_patterns)
+ names, transitions = self.make_transitions(
+ self.ws_initial_transitions)
+ self.add_transitions(names, transitions)
+
+ def blank(self, match, context, next_state):
+ """Handle blank lines. Does nothing. Override in subclasses."""
+ return self.nop(match, context, next_state)
+
+ def indent(self, match, context, next_state):
+ """
+ Handle an indented text block. Extend or override in subclasses.
+
+ Recursively run the registered state machine for indented blocks
+ (`self.indent_sm`).
+ """
+ (indented, indent, line_offset, blank_finish
+ ) = self.state_machine.get_indented()
+ sm = self.indent_sm(debug=self.debug, **self.indent_sm_kwargs)
+ results = sm.run(indented, input_offset=line_offset)
+ return context, next_state, results
+
+ def known_indent(self, match, context, next_state):
+ """
+ Handle a known-indent text block. Extend or override in subclasses.
+
+ Recursively run the registered state machine for known-indent indented
+ blocks (`self.known_indent_sm`). The indent is the length of the
+ match, ``match.end()``.
+ """
+ (indented, line_offset, blank_finish
+ ) = self.state_machine.get_known_indented(match.end())
+ sm = self.known_indent_sm(debug=self.debug,
+ **self.known_indent_sm_kwargs)
+ results = sm.run(indented, input_offset=line_offset)
+ return context, next_state, results
+
+ def first_known_indent(self, match, context, next_state):
+ """
+ Handle an indented text block (first line's indent known).
+
+ Extend or override in subclasses.
+
+ Recursively run the registered state machine for known-indent indented
+ blocks (`self.known_indent_sm`). The indent is the length of the
+ match, ``match.end()``.
+ """
+ (indented, line_offset, blank_finish
+ ) = self.state_machine.get_first_known_indented(match.end())
+ sm = self.known_indent_sm(debug=self.debug,
+ **self.known_indent_sm_kwargs)
+ results = sm.run(indented, input_offset=line_offset)
+ return context, next_state, results
+
+
+class _SearchOverride:
+
+ """
+ Mix-in class to override `StateMachine` regular expression behavior.
+
+ Changes regular expression matching, from the default `re.match()`
+ (succeeds only if the pattern matches at the start of `self.line`) to
+ `re.search()` (succeeds if the pattern matches anywhere in `self.line`).
+ When subclassing a `StateMachine`, list this class **first** in the
+ inheritance list of the class definition.
+ """
+
+ def match(self, pattern):
+ """
+ Return the result of a regular expression search.
+
+ Overrides `StateMachine.match()`.
+
+ Parameter `pattern`: `re` compiled regular expression.
+ """
+ return pattern.search(self.line)
+
+
+class SearchStateMachine(_SearchOverride, StateMachine):
+ """`StateMachine` which uses `re.search()` instead of `re.match()`."""
+ pass
+
+
+class SearchStateMachineWS(_SearchOverride, StateMachineWS):
+ """`StateMachineWS` which uses `re.search()` instead of `re.match()`."""
+ pass
+
+
+class ViewList:
+
+ """
+ List with extended functionality: slices of ViewList objects are child
+ lists, linked to their parents. Changes made to a child list also affect
+ the parent list. A child list is effectively a "view" (in the SQL sense)
+ of the parent list. Changes to parent lists, however, do *not* affect
+ active child lists. If a parent list is changed, any active child lists
+ should be recreated.
+
+ The start and end of the slice can be trimmed using the `trim_start()` and
+ `trim_end()` methods, without affecting the parent list. The link between
+ child and parent lists can be broken by calling `disconnect()` on the
+ child list.
+
+ Also, ViewList objects keep track of the source & offset of each item.
+ This information is accessible via the `source()`, `offset()`, and
+ `info()` methods.
+ """
+
+ def __init__(self, initlist=None, source=None, items=None,
+ parent=None, parent_offset=None):
+ self.data = []
+ """The actual list of data, flattened from various sources."""
+
+ self.items = []
+ """A list of (source, offset) pairs, same length as `self.data`: the
+ source of each line and the offset of each line from the beginning of
+ its source."""
+
+ self.parent = parent
+ """The parent list."""
+
+ self.parent_offset = parent_offset
+ """Offset of this list from the beginning of the parent list."""
+
+ if isinstance(initlist, ViewList):
+ self.data = initlist.data[:]
+ self.items = initlist.items[:]
+ elif initlist is not None:
+ self.data = list(initlist)
+ if items:
+ self.items = items
+ else:
+ self.items = [(source, i) for i in range(len(initlist))]
+ assert len(self.data) == len(self.items), 'data mismatch'
+
+ def __str__(self):
+ return str(self.data)
+
+ def __repr__(self):
+ return f'{self.__class__.__name__}({self.data}, items={self.items})'
+
+ def __lt__(self, other): return self.data < self.__cast(other) # noqa
+ def __le__(self, other): return self.data <= self.__cast(other) # noqa
+ def __eq__(self, other): return self.data == self.__cast(other) # noqa
+ def __ne__(self, other): return self.data != self.__cast(other) # noqa
+ def __gt__(self, other): return self.data > self.__cast(other) # noqa
+ def __ge__(self, other): return self.data >= self.__cast(other) # noqa
+
+ def __cast(self, other):
+ if isinstance(other, ViewList):
+ return other.data
+ else:
+ return other
+
+ def __contains__(self, item):
+ return item in self.data
+
+ def __len__(self):
+ return len(self.data)
+
+ # The __getitem__()/__setitem__() methods check whether the index
+ # is a slice first, since indexing a native list with a slice object
+ # just works.
+
+ def __getitem__(self, i):
+ if isinstance(i, slice):
+ assert i.step in (None, 1), 'cannot handle slice with stride'
+ return self.__class__(self.data[i.start:i.stop],
+ items=self.items[i.start:i.stop],
+ parent=self, parent_offset=i.start or 0)
+ else:
+ return self.data[i]
+
+ def __setitem__(self, i, item):
+ if isinstance(i, slice):
+ assert i.step in (None, 1), 'cannot handle slice with stride'
+ if not isinstance(item, ViewList):
+ raise TypeError('assigning non-ViewList to ViewList slice')
+ self.data[i.start:i.stop] = item.data
+ self.items[i.start:i.stop] = item.items
+ assert len(self.data) == len(self.items), 'data mismatch'
+ if self.parent:
+ k = (i.start or 0) + self.parent_offset
+ n = (i.stop or len(self)) + self.parent_offset
+ self.parent[k:n] = item
+ else:
+ self.data[i] = item
+ if self.parent:
+ self.parent[i + self.parent_offset] = item
+
+ def __delitem__(self, i):
+ try:
+ del self.data[i]
+ del self.items[i]
+ if self.parent:
+ del self.parent[i + self.parent_offset]
+ except TypeError:
+ assert i.step is None, 'cannot handle slice with stride'
+ del self.data[i.start:i.stop]
+ del self.items[i.start:i.stop]
+ if self.parent:
+ k = (i.start or 0) + self.parent_offset
+ n = (i.stop or len(self)) + self.parent_offset
+ del self.parent[k:n]
+
+ def __add__(self, other):
+ if isinstance(other, ViewList):
+ return self.__class__(self.data + other.data,
+ items=(self.items + other.items))
+ else:
+ raise TypeError('adding non-ViewList to a ViewList')
+
+ def __radd__(self, other):
+ if isinstance(other, ViewList):
+ return self.__class__(other.data + self.data,
+ items=(other.items + self.items))
+ else:
+ raise TypeError('adding ViewList to a non-ViewList')
+
+ def __iadd__(self, other):
+ if isinstance(other, ViewList):
+ self.data += other.data
+ else:
+ raise TypeError('argument to += must be a ViewList')
+ return self
+
+ def __mul__(self, n):
+ return self.__class__(self.data * n, items=(self.items * n))
+
+ __rmul__ = __mul__
+
+ def __imul__(self, n):
+ self.data *= n
+ self.items *= n
+ return self
+
+ def extend(self, other):
+ if not isinstance(other, ViewList):
+ raise TypeError('extending a ViewList with a non-ViewList')
+ if self.parent:
+ self.parent.insert(len(self.data) + self.parent_offset, other)
+ self.data.extend(other.data)
+ self.items.extend(other.items)
+
+ def append(self, item, source=None, offset=0):
+ if source is None:
+ self.extend(item)
+ else:
+ if self.parent:
+ self.parent.insert(len(self.data) + self.parent_offset, item,
+ source, offset)
+ self.data.append(item)
+ self.items.append((source, offset))
+
+ def insert(self, i, item, source=None, offset=0):
+ if source is None:
+ if not isinstance(item, ViewList):
+ raise TypeError('inserting non-ViewList with no source given')
+ self.data[i:i] = item.data
+ self.items[i:i] = item.items
+ if self.parent:
+ index = (len(self.data) + i) % len(self.data)
+ self.parent.insert(index + self.parent_offset, item)
+ else:
+ self.data.insert(i, item)
+ self.items.insert(i, (source, offset))
+ if self.parent:
+ index = (len(self.data) + i) % len(self.data)
+ self.parent.insert(index + self.parent_offset, item,
+ source, offset)
+
+ def pop(self, i=-1):
+ if self.parent:
+ index = (len(self.data) + i) % len(self.data)
+ self.parent.pop(index + self.parent_offset)
+ self.items.pop(i)
+ return self.data.pop(i)
+
+ def trim_start(self, n=1):
+ """
+ Remove items from the start of the list, without touching the parent.
+ """
+ if n > len(self.data):
+ raise IndexError("Size of trim too large; can't trim %s items "
+ "from a list of size %s." % (n, len(self.data)))
+ elif n < 0:
+ raise IndexError('Trim size must be >= 0.')
+ del self.data[:n]
+ del self.items[:n]
+ if self.parent:
+ self.parent_offset += n
+
+ def trim_end(self, n=1):
+ """
+ Remove items from the end of the list, without touching the parent.
+ """
+ if n > len(self.data):
+ raise IndexError("Size of trim too large; can't trim %s items "
+ "from a list of size %s." % (n, len(self.data)))
+ elif n < 0:
+ raise IndexError('Trim size must be >= 0.')
+ del self.data[-n:]
+ del self.items[-n:]
+
+ def remove(self, item):
+ index = self.index(item)
+ del self[index]
+
+ def count(self, item):
+ return self.data.count(item)
+
+ def index(self, item):
+ return self.data.index(item)
+
+ def reverse(self):
+ self.data.reverse()
+ self.items.reverse()
+ self.parent = None
+
+ def sort(self, *args):
+ tmp = sorted(zip(self.data, self.items), *args)
+ self.data = [entry[0] for entry in tmp]
+ self.items = [entry[1] for entry in tmp]
+ self.parent = None
+
+ def info(self, i):
+ """Return source & offset for index `i`."""
+ try:
+ return self.items[i]
+ except IndexError:
+ if i == len(self.data): # Just past the end
+ return self.items[i - 1][0], None
+ else:
+ raise
+
+ def source(self, i):
+ """Return source for index `i`."""
+ return self.info(i)[0]
+
+ def offset(self, i):
+ """Return offset for index `i`."""
+ return self.info(i)[1]
+
+ def disconnect(self):
+ """Break link between this list and parent list."""
+ self.parent = None
+
+ def xitems(self):
+ """Return iterator yielding (source, offset, value) tuples."""
+ for (value, (source, offset)) in zip(self.data, self.items):
+ yield source, offset, value
+
+ def pprint(self):
+ """Print the list in `grep` format (`source:offset:value` lines)"""
+ for line in self.xitems():
+ print("%s:%d:%s" % line)
+
+
+class StringList(ViewList):
+
+ """A `ViewList` with string-specific methods."""
+
+ def trim_left(self, length, start=0, end=sys.maxsize):
+ """
+ Trim `length` characters off the beginning of each item, in-place,
+ from index `start` to `end`. No whitespace-checking is done on the
+ trimmed text. Does not affect slice parent.
+ """
+ self.data[start:end] = [line[length:]
+ for line in self.data[start:end]]
+
+ def get_text_block(self, start, flush_left=False):
+ """
+ Return a contiguous block of text.
+
+ If `flush_left` is true, raise `UnexpectedIndentationError` if an
+ indented line is encountered before the text block ends (with a blank
+ line).
+ """
+ end = start
+ last = len(self.data)
+ while end < last:
+ line = self.data[end]
+ if not line.strip():
+ break
+ if flush_left and (line[0] == ' '):
+ source, offset = self.info(end)
+ raise UnexpectedIndentationError(self[start:end], source,
+ offset + 1)
+ end += 1
+ return self[start:end]
+
+ def get_indented(self, start=0, until_blank=False, strip_indent=True,
+ block_indent=None, first_indent=None):
+ """
+ Extract and return a StringList of indented lines of text.
+
+ Collect all lines with indentation, determine the minimum indentation,
+ remove the minimum indentation from all indented lines (unless
+ `strip_indent` is false), and return them. All lines up to but not
+ including the first unindented line will be returned.
+
+ :Parameters:
+ - `start`: The index of the first line to examine.
+ - `until_blank`: Stop collecting at the first blank line if true.
+ - `strip_indent`: Strip common leading indent if true (default).
+ - `block_indent`: The indent of the entire block, if known.
+ - `first_indent`: The indent of the first line, if known.
+
+ :Return:
+ - a StringList of indented lines with minimum indent removed;
+ - the amount of the indent;
+ - a boolean: did the indented block finish with a blank line or EOF?
+ """
+ indent = block_indent # start with None if unknown
+ end = start
+ if block_indent is not None and first_indent is None:
+ first_indent = block_indent
+ if first_indent is not None:
+ end += 1
+ last = len(self.data)
+ while end < last:
+ line = self.data[end]
+ if line and (line[0] != ' '
+ or (block_indent is not None
+ and line[:block_indent].strip())):
+ # Line not indented or insufficiently indented.
+ # Block finished properly iff the last indented line blank:
+ blank_finish = ((end > start)
+ and not self.data[end - 1].strip())
+ break
+ stripped = line.lstrip()
+ if not stripped: # blank line
+ if until_blank:
+ blank_finish = 1
+ break
+ elif block_indent is None:
+ line_indent = len(line) - len(stripped)
+ if indent is None:
+ indent = line_indent
+ else:
+ indent = min(indent, line_indent)
+ end += 1
+ else:
+ blank_finish = 1 # block ends at end of lines
+ block = self[start:end]
+ if first_indent is not None and block:
+ block.data[0] = block.data[0][first_indent:]
+ if indent and strip_indent:
+ block.trim_left(indent, start=(first_indent is not None))
+ return block, indent or 0, blank_finish
+
+ def get_2D_block(self, top, left, bottom, right, strip_indent=True):
+ block = self[top:bottom]
+ indent = right
+ for i in range(len(block.data)):
+ # get slice from line, care for combining characters
+ ci = utils.column_indices(block.data[i])
+ try:
+ left = ci[left]
+ except IndexError:
+ left += len(block.data[i]) - len(ci)
+ try:
+ right = ci[right]
+ except IndexError:
+ right += len(block.data[i]) - len(ci)
+ block.data[i] = line = block.data[i][left:right].rstrip()
+ if line:
+ indent = min(indent, len(line) - len(line.lstrip()))
+ if strip_indent and 0 < indent < right:
+ block.data = [line[indent:] for line in block.data]
+ return block
+
+ def pad_double_width(self, pad_char):
+ """Pad all double-width characters in `self` appending `pad_char`.
+
+ For East Asian language support.
+ """
+ for i in range(len(self.data)):
+ line = self.data[i]
+ if isinstance(line, str):
+ new = []
+ for char in line:
+ new.append(char)
+ if east_asian_width(char) in 'WF': # Wide & Full-width
+ new.append(pad_char)
+ self.data[i] = ''.join(new)
+
+ def replace(self, old, new):
+ """Replace all occurrences of substring `old` with `new`."""
+ for i in range(len(self.data)):
+ self.data[i] = self.data[i].replace(old, new)
+
+
+class StateMachineError(Exception): pass
+class UnknownStateError(StateMachineError): pass
+class DuplicateStateError(StateMachineError): pass
+class UnknownTransitionError(StateMachineError): pass
+class DuplicateTransitionError(StateMachineError): pass
+class TransitionPatternNotFound(StateMachineError): pass
+class TransitionMethodNotFound(StateMachineError): pass
+class UnexpectedIndentationError(StateMachineError): pass
+
+
+class TransitionCorrection(Exception):
+
+ """
+ Raise from within a transition method to switch to another transition.
+
+ Raise with one argument, the new transition name.
+ """
+
+
+class StateCorrection(Exception):
+
+ """
+ Raise from within a transition method to switch to another state.
+
+ Raise with one or two arguments: new state name, and an optional new
+ transition name.
+ """
+
+
+def string2lines(astring, tab_width=8, convert_whitespace=False,
+ whitespace=re.compile('[\v\f]')):
+ """
+ Return a list of one-line strings with tabs expanded, no newlines, and
+ trailing whitespace stripped.
+
+ Each tab is expanded with between 1 and `tab_width` spaces, so that the
+ next character's index becomes a multiple of `tab_width` (8 by default).
+
+ Parameters:
+
+ - `astring`: a multi-line string.
+ - `tab_width`: the number of columns between tab stops.
+ - `convert_whitespace`: convert form feeds and vertical tabs to spaces?
+ - `whitespace`: pattern object with the to-be-converted
+ whitespace characters (default [\\v\\f]).
+ """
+ if convert_whitespace:
+ astring = whitespace.sub(' ', astring)
+ return [s.expandtabs(tab_width).rstrip() for s in astring.splitlines()]
+
+
+def _exception_data():
+ """
+ Return exception information:
+
+ - the exception's class name;
+ - the exception object;
+ - the name of the file containing the offending code;
+ - the line number of the offending code;
+ - the function name of the offending code.
+ """
+ type, value, traceback = sys.exc_info()
+ while traceback.tb_next:
+ traceback = traceback.tb_next
+ code = traceback.tb_frame.f_code
+ return (type.__name__, value, code.co_filename, traceback.tb_lineno,
+ code.co_name)
diff --git a/.venv/lib/python3.12/site-packages/docutils/transforms/__init__.py b/.venv/lib/python3.12/site-packages/docutils/transforms/__init__.py
new file mode 100644
index 00000000..20536b3d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/transforms/__init__.py
@@ -0,0 +1,185 @@
+# $Id: __init__.py 9502 2023-12-14 22:39:08Z milde $
+# Authors: David Goodger <goodger@python.org>; Ueli Schlaepfer
+# Copyright: This module has been placed in the public domain.
+
+"""
+This package contains modules for standard tree transforms available
+to Docutils components. Tree transforms serve a variety of purposes:
+
+- To tie up certain syntax-specific "loose ends" that remain after the
+ initial parsing of the input plaintext. These transforms are used to
+ supplement a limited syntax.
+
+- To automate the internal linking of the document tree (hyperlink
+ references, footnote references, etc.).
+
+- To extract useful information from the document tree. These
+ transforms may be used to construct (for example) indexes and tables
+ of contents.
+
+Each transform is an optional step that a Docutils component may
+choose to perform on the parsed document.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+from docutils import languages, ApplicationError, TransformSpec
+
+
+class TransformError(ApplicationError):
+ pass
+
+
+class Transform:
+ """Docutils transform component abstract base class."""
+
+ default_priority = None
+ """Numerical priority of this transform, 0 through 999 (override)."""
+
+ def __init__(self, document, startnode=None):
+ """
+ Initial setup for in-place document transforms.
+ """
+
+ self.document = document
+ """The document tree to transform."""
+
+ self.startnode = startnode
+ """Node from which to begin the transform. For many transforms which
+ apply to the document as a whole, `startnode` is not set (i.e. its
+ value is `None`)."""
+
+ self.language = languages.get_language(
+ document.settings.language_code, document.reporter)
+ """Language module local to this document."""
+
+ def apply(self, **kwargs):
+ """Override to apply the transform to the document tree."""
+ raise NotImplementedError('subclass must override this method')
+
+
+class Transformer(TransformSpec):
+ """
+ Store "transforms" and apply them to the document tree.
+
+ Collect lists of `Transform` instances and "unknown_reference_resolvers"
+ from Docutils components (`TransformSpec` instances).
+ Apply collected "transforms" to the document tree.
+
+ Also keeps track of components by component type name.
+
+ https://docutils.sourceforge.io/docs/peps/pep-0258.html#transformer
+ """
+
+ def __init__(self, document):
+ self.transforms = []
+ """List of transforms to apply. Each item is a 4-tuple:
+ ``(priority string, transform class, pending node or None, kwargs)``.
+ """
+
+ self.unknown_reference_resolvers = []
+ """List of hook functions which assist in resolving references."""
+
+ self.document = document
+ """The `nodes.document` object this Transformer is attached to."""
+
+ self.applied = []
+ """Transforms already applied, in order."""
+
+ self.sorted = False
+ """Boolean: is `self.tranforms` sorted?"""
+
+ self.components = {}
+ """Mapping of component type name to component object.
+
+ Set by `self.populate_from_components()`.
+ """
+
+ self.serialno = 0
+ """Internal serial number to keep track of the add order of
+ transforms."""
+
+ def add_transform(self, transform_class, priority=None, **kwargs):
+ """
+ Store a single transform. Use `priority` to override the default.
+ `kwargs` is a dictionary whose contents are passed as keyword
+ arguments to the `apply` method of the transform. This can be used to
+ pass application-specific data to the transform instance.
+ """
+ if priority is None:
+ priority = transform_class.default_priority
+ priority_string = self.get_priority_string(priority)
+ self.transforms.append(
+ (priority_string, transform_class, None, kwargs))
+ self.sorted = False
+
+ def add_transforms(self, transform_list):
+ """Store multiple transforms, with default priorities."""
+ for transform_class in transform_list:
+ priority_string = self.get_priority_string(
+ transform_class.default_priority)
+ self.transforms.append(
+ (priority_string, transform_class, None, {}))
+ self.sorted = False
+
+ def add_pending(self, pending, priority=None):
+ """Store a transform with an associated `pending` node."""
+ transform_class = pending.transform
+ if priority is None:
+ priority = transform_class.default_priority
+ priority_string = self.get_priority_string(priority)
+ self.transforms.append(
+ (priority_string, transform_class, pending, {}))
+ self.sorted = False
+
+ def get_priority_string(self, priority):
+ """
+ Return a string, `priority` combined with `self.serialno`.
+
+ This ensures FIFO order on transforms with identical priority.
+ """
+ self.serialno += 1
+ return '%03d-%03d' % (priority, self.serialno)
+
+ def populate_from_components(self, components):
+ """
+ Store each component's default transforms and reference resolvers
+
+ Transforms are stored with default priorities for later sorting.
+ "Unknown reference resolvers" are sorted and stored.
+ Components that don't inherit from `TransformSpec` are ignored.
+
+ Also, store components by type name in a mapping for later lookup.
+ """
+ resolvers = []
+ for component in components:
+ if not isinstance(component, TransformSpec):
+ continue
+ self.add_transforms(component.get_transforms())
+ self.components[component.component_type] = component
+ resolvers.extend(component.unknown_reference_resolvers)
+ self.sorted = False # sort transform list in self.apply_transforms()
+
+ # Sort and add helper functions to help resolve unknown references.
+ def keyfun(f):
+ return f.priority
+ resolvers.sort(key=keyfun)
+ self.unknown_reference_resolvers += resolvers
+
+ def apply_transforms(self):
+ """Apply all of the stored transforms, in priority order."""
+ self.document.reporter.attach_observer(
+ self.document.note_transform_message)
+ while self.transforms:
+ if not self.sorted:
+ # Unsorted initially, and whenever a transform is added
+ # (transforms may add other transforms).
+ self.transforms.sort(reverse=True)
+ self.sorted = True
+ priority, transform_class, pending, kwargs = self.transforms.pop()
+ transform = transform_class(self.document, startnode=pending)
+ transform.apply(**kwargs)
+ self.applied.append((priority, transform_class, pending, kwargs))
+ self.document.reporter.detach_observer(
+ self.document.note_transform_message)
diff --git a/.venv/lib/python3.12/site-packages/docutils/transforms/components.py b/.venv/lib/python3.12/site-packages/docutils/transforms/components.py
new file mode 100644
index 00000000..9cbbb503
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/transforms/components.py
@@ -0,0 +1,54 @@
+# $Id: components.py 9037 2022-03-05 23:31:10Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Docutils component-related transforms.
+"""
+
+from docutils.transforms import Transform
+
+__docformat__ = 'reStructuredText'
+
+
+class Filter(Transform):
+
+ """
+ Include or exclude elements which depend on a specific Docutils component.
+
+ For use with `nodes.pending` elements. A "pending" element's dictionary
+ attribute ``details`` must contain the keys "component" and "format". The
+ value of ``details['component']`` must match the type name of the
+ component the elements depend on (e.g. "writer"). The value of
+ ``details['format']`` is the name of a specific format or context of that
+ component (e.g. "html"). If the matching Docutils component supports that
+ format or context, the "pending" element is replaced by the contents of
+ ``details['nodes']`` (a list of nodes); otherwise, the "pending" element
+ is removed.
+
+ For example, up to version 0.17, the reStructuredText "meta"
+ directive created a "pending" element containing a "meta" element
+ (in ``pending.details['nodes']``).
+ Only writers (``pending.details['component'] == 'writer'``)
+ supporting the "html", "latex", or "odf" formats
+ (``pending.details['format'] == 'html,latex,odf'``) included the
+ "meta" element; it was deleted from the output of all other writers.
+
+ This transform is no longer used by Docutils, it may be removed in future.
+ """
+ # TODO: clean up or keep this for 3rd party (or possible future) use?
+ # (GM 2021-05-18)
+
+ default_priority = 780
+
+ def apply(self):
+ pending = self.startnode
+ component_type = pending.details['component'] # 'reader' or 'writer'
+ formats = (pending.details['format']).split(',')
+ component = self.document.transformer.components[component_type]
+ for format in formats:
+ if component.supports(format):
+ pending.replace_self(pending.details['nodes'])
+ break
+ else:
+ pending.parent.remove(pending)
diff --git a/.venv/lib/python3.12/site-packages/docutils/transforms/frontmatter.py b/.venv/lib/python3.12/site-packages/docutils/transforms/frontmatter.py
new file mode 100644
index 00000000..9f534cce
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/transforms/frontmatter.py
@@ -0,0 +1,540 @@
+# $Id: frontmatter.py 9552 2024-03-08 23:41:31Z milde $
+# Author: David Goodger, Ueli Schlaepfer <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Transforms_ related to the front matter of a document or a section
+(information found before the main text):
+
+- `DocTitle`: Used to transform a lone top level section's title to
+ the document title, promote a remaining lone top-level section's
+ title to the document subtitle, and determine the document's title
+ metadata (document['title']) based on the document title and/or the
+ "title" setting.
+
+- `SectionSubTitle`: Used to transform a lone subsection into a
+ subtitle.
+
+- `DocInfo`: Used to transform a bibliographic field list into docinfo
+ elements.
+
+.. _transforms: https://docutils.sourceforge.io/docs/api/transforms.html
+"""
+
+__docformat__ = 'reStructuredText'
+
+import re
+
+from docutils import nodes, parsers, utils
+from docutils.transforms import TransformError, Transform
+
+
+class TitlePromoter(Transform):
+
+ """
+ Abstract base class for DocTitle and SectionSubTitle transforms.
+ """
+
+ def promote_title(self, node):
+ """
+ Transform the following tree::
+
+ <node>
+ <section>
+ <title>
+ ...
+
+ into ::
+
+ <node>
+ <title>
+ ...
+
+ `node` is normally a document.
+ """
+ # Type check
+ if not isinstance(node, nodes.Element):
+ raise TypeError('node must be of Element-derived type.')
+
+ # `node` must not have a title yet.
+ assert not (len(node) and isinstance(node[0], nodes.title))
+ section, index = self.candidate_index(node)
+ if index is None:
+ return False
+
+ # Transfer the section's attributes to the node:
+ # NOTE: Change `replace` to False to NOT replace attributes that
+ # already exist in node with those in section.
+ # NOTE: Remove `and_source` to NOT copy the 'source'
+ # attribute from section
+ node.update_all_atts_concatenating(section, replace=True,
+ and_source=True)
+
+ # setup_child is called automatically for all nodes.
+ node[:] = (section[:1] # section title
+ + node[:index] # everything that was in the
+ # node before the section
+ + section[1:]) # everything that was in the section
+ assert isinstance(node[0], nodes.title)
+ return True
+
+ def promote_subtitle(self, node):
+ """
+ Transform the following node tree::
+
+ <node>
+ <title>
+ <section>
+ <title>
+ ...
+
+ into ::
+
+ <node>
+ <title>
+ <subtitle>
+ ...
+ """
+ # Type check
+ if not isinstance(node, nodes.Element):
+ raise TypeError('node must be of Element-derived type.')
+
+ subsection, index = self.candidate_index(node)
+ if index is None:
+ return False
+ subtitle = nodes.subtitle()
+
+ # Transfer the subsection's attributes to the new subtitle
+ # NOTE: Change `replace` to False to NOT replace attributes
+ # that already exist in node with those in section.
+ # NOTE: Remove `and_source` to NOT copy the 'source'
+ # attribute from section.
+ subtitle.update_all_atts_concatenating(subsection, replace=True,
+ and_source=True)
+
+ # Transfer the contents of the subsection's title to the
+ # subtitle:
+ subtitle[:] = subsection[0][:]
+ node[:] = (node[:1] # title
+ + [subtitle]
+ # everything that was before the section:
+ + node[1:index]
+ # everything that was in the subsection:
+ + subsection[1:])
+ return True
+
+ def candidate_index(self, node):
+ """
+ Find and return the promotion candidate and its index.
+
+ Return (None, None) if no valid candidate was found.
+ """
+ index = node.first_child_not_matching_class(
+ nodes.PreBibliographic)
+ if (index is None or len(node) > (index + 1)
+ or not isinstance(node[index], nodes.section)):
+ return None, None
+ else:
+ return node[index], index
+
+
+class DocTitle(TitlePromoter):
+
+ """
+ In reStructuredText_, there is no way to specify a document title
+ and subtitle explicitly. Instead, we can supply the document title
+ (and possibly the subtitle as well) implicitly, and use this
+ two-step transform to "raise" or "promote" the title(s) (and their
+ corresponding section contents) to the document level.
+
+ 1. If the document contains a single top-level section as its first
+ element (instances of `nodes.PreBibliographic` are ignored),
+ the top-level section's title becomes the document's title, and
+ the top-level section's contents become the document's immediate
+ contents. The title is also used for the <document> element's
+ "title" attribute default value.
+
+ 2. If step 1 successfully determines the document title, we
+ continue by checking for a subtitle.
+
+ If the lone top-level section itself contains a single second-level
+ section as its first "non-PreBibliographic" element, that section's
+ title is promoted to the document's subtitle, and that section's
+ contents become the document's immediate contents.
+
+ Example:
+ Given this input text::
+
+ =================
+ Top-Level Title
+ =================
+
+ Second-Level Title
+ ~~~~~~~~~~~~~~~~~~
+
+ A paragraph.
+
+ After parsing and running the DocTitle transform, the result is::
+
+ <document names="top-level title">
+ <title>
+ Top-Level Title
+ <subtitle names="second-level title">
+ Second-Level Title
+ <paragraph>
+ A paragraph.
+
+ (Note that the implicit hyperlink target generated by the
+ "Second-Level Title" is preserved on the <subtitle> element
+ itself.)
+
+ Any `nodes.PreBibliographic` instances occurring before the
+ document title or subtitle are accumulated and inserted as
+ the first body elements after the title(s).
+
+ .. _reStructuredText: https://docutils.sourceforge.io/rst.html
+ """
+
+ default_priority = 320
+
+ def set_metadata(self):
+ """
+ Set document['title'] metadata title from the following
+ sources, listed in order of priority:
+
+ * Existing document['title'] attribute.
+ * "title" setting.
+ * Document title node (as promoted by promote_title).
+ """
+ if not self.document.hasattr('title'):
+ if self.document.settings.title is not None:
+ self.document['title'] = self.document.settings.title
+ elif len(self.document) and isinstance(self.document[0],
+ nodes.title):
+ self.document['title'] = self.document[0].astext()
+
+ def apply(self):
+ if self.document.settings.setdefault('doctitle_xform', True):
+ # promote_(sub)title defined in TitlePromoter base class.
+ if self.promote_title(self.document):
+ # If a title has been promoted, also try to promote a
+ # subtitle.
+ self.promote_subtitle(self.document)
+ # Set document['title'].
+ self.set_metadata()
+
+
+class SectionSubTitle(TitlePromoter):
+
+ """
+ This works like document subtitles, but for sections. For example, ::
+
+ <section>
+ <title>
+ Title
+ <section>
+ <title>
+ Subtitle
+ ...
+
+ is transformed into ::
+
+ <section>
+ <title>
+ Title
+ <subtitle>
+ Subtitle
+ ...
+
+ For details refer to the docstring of DocTitle.
+ """
+
+ default_priority = 350
+
+ def apply(self):
+ if not self.document.settings.setdefault('sectsubtitle_xform', True):
+ return
+ for section in self.document.findall(nodes.section):
+ # On our way through the node tree, we are modifying it
+ # but only the not-yet-visited part, so that the iterator
+ # returned by findall() is not corrupted.
+ self.promote_subtitle(section)
+
+
+class DocInfo(Transform):
+
+ """
+ This transform is specific to the reStructuredText_ markup syntax;
+ see "Bibliographic Fields" in the `reStructuredText Markup
+ Specification`_ for a high-level description. This transform
+ should be run *after* the `DocTitle` transform.
+
+ If the document contains a field list as the first element (instances
+ of `nodes.PreBibliographic` are ignored), registered bibliographic
+ field names are transformed to the corresponding DTD elements,
+ becoming child elements of the <docinfo> element (except for a
+ dedication and/or an abstract, which become <topic> elements after
+ <docinfo>).
+
+ For example, given this document fragment after parsing::
+
+ <document>
+ <title>
+ Document Title
+ <field_list>
+ <field>
+ <field_name>
+ Author
+ <field_body>
+ <paragraph>
+ A. Name
+ <field>
+ <field_name>
+ Status
+ <field_body>
+ <paragraph>
+ $RCSfile$
+ ...
+
+ After running the bibliographic field list transform, the
+ resulting document tree would look like this::
+
+ <document>
+ <title>
+ Document Title
+ <docinfo>
+ <author>
+ A. Name
+ <status>
+ frontmatter.py
+ ...
+
+ The "Status" field contained an expanded RCS keyword, which is
+ normally (but optionally) cleaned up by the transform. The sole
+ contents of the field body must be a paragraph containing an
+ expanded RCS keyword of the form "$keyword: expansion text $". Any
+ RCS keyword can be processed in any bibliographic field. The
+ dollar signs and leading RCS keyword name are removed. Extra
+ processing is done for the following RCS keywords:
+
+ - "RCSfile" expands to the name of the file in the RCS or CVS
+ repository, which is the name of the source file with a ",v"
+ suffix appended. The transform will remove the ",v" suffix.
+
+ - "Date" expands to the format "YYYY/MM/DD hh:mm:ss" (in the UTC
+ time zone). The RCS Keywords transform will extract just the
+ date itself and transform it to an ISO 8601 format date, as in
+ "2000-12-31".
+
+ (Since the source file for this text is itself stored under CVS,
+ we can't show an example of the "Date" RCS keyword because we
+ can't prevent any RCS keywords used in this explanation from
+ being expanded. Only the "RCSfile" keyword is stable; its
+ expansion text changes only if the file name changes.)
+
+ .. _reStructuredText: https://docutils.sourceforge.io/rst.html
+ .. _reStructuredText Markup Specification:
+ https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html
+ """
+
+ default_priority = 340
+
+ biblio_nodes = {
+ 'author': nodes.author,
+ 'authors': nodes.authors,
+ 'organization': nodes.organization,
+ 'address': nodes.address,
+ 'contact': nodes.contact,
+ 'version': nodes.version,
+ 'revision': nodes.revision,
+ 'status': nodes.status,
+ 'date': nodes.date,
+ 'copyright': nodes.copyright,
+ 'dedication': nodes.topic,
+ 'abstract': nodes.topic}
+ """Canonical field name (lowcased) to node class name mapping for
+ bibliographic fields (field_list)."""
+
+ def apply(self):
+ if not self.document.settings.setdefault('docinfo_xform', True):
+ return
+ document = self.document
+ index = document.first_child_not_matching_class(
+ nodes.PreBibliographic)
+ if index is None:
+ return
+ candidate = document[index]
+ if isinstance(candidate, nodes.field_list):
+ biblioindex = document.first_child_not_matching_class(
+ (nodes.Titular, nodes.Decorative, nodes.meta))
+ nodelist = self.extract_bibliographic(candidate)
+ del document[index] # untransformed field list (candidate)
+ document[biblioindex:biblioindex] = nodelist
+
+ def extract_bibliographic(self, field_list):
+ docinfo = nodes.docinfo()
+ bibliofields = self.language.bibliographic_fields
+ labels = self.language.labels
+ topics = {'dedication': None, 'abstract': None}
+ for field in field_list:
+ try:
+ name = field[0][0].astext()
+ normedname = nodes.fully_normalize_name(name)
+ if not (len(field) == 2 and normedname in bibliofields
+ and self.check_empty_biblio_field(field, name)):
+ raise TransformError
+ canonical = bibliofields[normedname]
+ biblioclass = self.biblio_nodes[canonical]
+ if issubclass(biblioclass, nodes.TextElement):
+ if not self.check_compound_biblio_field(field, name):
+ raise TransformError
+ utils.clean_rcs_keywords(
+ field[1][0], self.rcs_keyword_substitutions)
+ docinfo.append(biblioclass('', '', *field[1][0]))
+ elif issubclass(biblioclass, nodes.authors):
+ self.extract_authors(field, name, docinfo)
+ elif issubclass(biblioclass, nodes.topic):
+ if topics[canonical]:
+ field[-1] += self.document.reporter.warning(
+ 'There can only be one "%s" field.' % name,
+ base_node=field)
+ raise TransformError
+ title = nodes.title(name, labels[canonical])
+ title[0].rawsource = labels[canonical]
+ topics[canonical] = biblioclass(
+ '', title, classes=[canonical], *field[1].children)
+ else:
+ docinfo.append(biblioclass('', *field[1].children))
+ except TransformError:
+ if len(field[-1]) == 1 \
+ and isinstance(field[-1][0], nodes.paragraph):
+ utils.clean_rcs_keywords(
+ field[-1][0], self.rcs_keyword_substitutions)
+ # if normedname not in bibliofields:
+ classvalue = nodes.make_id(normedname)
+ if classvalue:
+ field['classes'].append(classvalue)
+ docinfo.append(field)
+ nodelist = []
+ if len(docinfo) != 0:
+ nodelist.append(docinfo)
+ for name in ('dedication', 'abstract'):
+ if topics[name]:
+ nodelist.append(topics[name])
+ return nodelist
+
+ def check_empty_biblio_field(self, field, name):
+ if len(field[-1]) < 1:
+ field[-1] += self.document.reporter.warning(
+ f'Cannot extract empty bibliographic field "{name}".',
+ base_node=field)
+ return False
+ return True
+
+ def check_compound_biblio_field(self, field, name):
+ # Check that the `field` body contains a single paragraph
+ # (i.e. it must *not* be a compound element).
+ f_body = field[-1]
+ if len(f_body) == 1 and isinstance(f_body[0], nodes.paragraph):
+ return True
+ # Restore single author name with initial (E. Xampl) parsed as
+ # enumerated list
+ # https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#enumerated-lists
+ if (isinstance(f_body[0], nodes.enumerated_list)
+ and '\n' not in f_body.rawsource.strip()):
+ # parse into a dummy document and use created nodes
+ _document = utils.new_document('*DocInfo transform*',
+ field.document.settings)
+ parser = parsers.rst.Parser()
+ parser.parse('\\'+f_body.rawsource, _document)
+ if (len(_document.children) == 1
+ and isinstance(_document.children[0], nodes.paragraph)):
+ f_body.children = _document.children
+ return True
+ # Check failed, add a warning
+ content = [f'<{e.tagname}>' for e in f_body.children]
+ if len(content) > 1:
+ content = '[' + ', '.join(content) + ']'
+ else:
+ content = 'a ' + content[0]
+ f_body += self.document.reporter.warning(
+ f'Bibliographic field "{name}"\nmust contain '
+ f'a single <paragraph>, not {content}.',
+ base_node=field)
+ return False
+
+ rcs_keyword_substitutions = [
+ (re.compile(r'\$' r'Date: (\d\d\d\d)[-/](\d\d)[-/](\d\d)[ T][\d:]+'
+ r'[^$]* \$', re.IGNORECASE), r'\1-\2-\3'),
+ (re.compile(r'\$' r'RCSfile: (.+),v \$', re.IGNORECASE), r'\1'),
+ (re.compile(r'\$[a-zA-Z]+: (.+) \$'), r'\1')]
+
+ def extract_authors(self, field, name, docinfo):
+ try:
+ if len(field[1]) == 1:
+ if isinstance(field[1][0], nodes.paragraph):
+ authors = self.authors_from_one_paragraph(field)
+ elif isinstance(field[1][0], nodes.bullet_list):
+ authors = self.authors_from_bullet_list(field)
+ else:
+ raise TransformError
+ else:
+ authors = self.authors_from_paragraphs(field)
+ authornodes = [nodes.author('', '', *author)
+ for author in authors if author]
+ if len(authornodes) >= 1:
+ docinfo.append(nodes.authors('', *authornodes))
+ else:
+ raise TransformError
+ except TransformError:
+ field[-1] += self.document.reporter.warning(
+ f'Cannot extract "{name}" from bibliographic field:\n'
+ f'Bibliographic field "{name}" must contain either\n'
+ ' a single paragraph (with author names separated by one of '
+ f'"{"".join(self.language.author_separators)}"),\n'
+ ' multiple paragraphs (one per author),\n'
+ ' or a bullet list with one author name per item.\n'
+ 'Note: Leading initials can cause (mis)recognizing names '
+ 'as enumerated list.',
+ base_node=field)
+ raise
+
+ def authors_from_one_paragraph(self, field):
+ """Return list of Text nodes with author names in `field`.
+
+ Author names must be separated by one of the "autor separators"
+ defined for the document language (default: ";" or ",").
+ """
+ # @@ keep original formatting? (e.g. ``:authors: A. Test, *et-al*``)
+ text = ''.join(str(node)
+ for node in field[1].findall(nodes.Text))
+ if not text:
+ raise TransformError
+ for authorsep in self.language.author_separators:
+ # don't split at escaped `authorsep`:
+ pattern = '(?<!\x00)%s' % authorsep
+ authornames = re.split(pattern, text)
+ if len(authornames) > 1:
+ break
+ authornames = (name.strip() for name in authornames)
+ return [[nodes.Text(name)] for name in authornames if name]
+
+ def authors_from_bullet_list(self, field):
+ authors = []
+ for item in field[1][0]:
+ if isinstance(item, nodes.comment):
+ continue
+ if len(item) != 1 or not isinstance(item[0], nodes.paragraph):
+ raise TransformError
+ authors.append(item[0].children)
+ if not authors:
+ raise TransformError
+ return authors
+
+ def authors_from_paragraphs(self, field):
+ for item in field[1]:
+ if not isinstance(item, (nodes.paragraph, nodes.comment)):
+ raise TransformError
+ authors = [item.children for item in field[1]
+ if not isinstance(item, nodes.comment)]
+ return authors
diff --git a/.venv/lib/python3.12/site-packages/docutils/transforms/misc.py b/.venv/lib/python3.12/site-packages/docutils/transforms/misc.py
new file mode 100644
index 00000000..e2cc796c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/transforms/misc.py
@@ -0,0 +1,144 @@
+# $Id: misc.py 9037 2022-03-05 23:31:10Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Miscellaneous transforms.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from docutils import nodes
+from docutils.transforms import Transform
+
+
+class CallBack(Transform):
+
+ """
+ Inserts a callback into a document. The callback is called when the
+ transform is applied, which is determined by its priority.
+
+ For use with `nodes.pending` elements. Requires a ``details['callback']``
+ entry, a bound method or function which takes one parameter: the pending
+ node. Other data can be stored in the ``details`` attribute or in the
+ object hosting the callback method.
+ """
+
+ default_priority = 990
+
+ def apply(self):
+ pending = self.startnode
+ pending.details['callback'](pending)
+ pending.parent.remove(pending)
+
+
+class ClassAttribute(Transform):
+
+ """
+ Move the "class" attribute specified in the "pending" node into the
+ immediately following non-comment element.
+ """
+
+ default_priority = 210
+
+ def apply(self):
+ pending = self.startnode
+ parent = pending.parent
+ child = pending
+ while parent:
+ # Check for appropriate following siblings:
+ for index in range(parent.index(child) + 1, len(parent)):
+ element = parent[index]
+ if (isinstance(element, nodes.Invisible)
+ or isinstance(element, nodes.system_message)):
+ continue
+ element['classes'] += pending.details['class']
+ pending.parent.remove(pending)
+ return
+ else:
+ # At end of section or container; apply to sibling
+ child = parent
+ parent = parent.parent
+ error = self.document.reporter.error(
+ 'No suitable element following "%s" directive'
+ % pending.details['directive'],
+ nodes.literal_block(pending.rawsource, pending.rawsource),
+ line=pending.line)
+ pending.replace_self(error)
+
+
+class Transitions(Transform):
+
+ """
+ Move transitions at the end of sections up the tree. Complain
+ on transitions after a title, at the beginning or end of the
+ document, and after another transition.
+
+ For example, transform this::
+
+ <section>
+ ...
+ <transition>
+ <section>
+ ...
+
+ into this::
+
+ <section>
+ ...
+ <transition>
+ <section>
+ ...
+ """
+
+ default_priority = 830
+
+ def apply(self):
+ for node in self.document.findall(nodes.transition):
+ self.visit_transition(node)
+
+ def visit_transition(self, node):
+ index = node.parent.index(node)
+ error = None
+ if (index == 0
+ or isinstance(node.parent[0], nodes.title)
+ and (index == 1
+ or isinstance(node.parent[1], nodes.subtitle)
+ and index == 2)):
+ assert (isinstance(node.parent, nodes.document)
+ or isinstance(node.parent, nodes.section))
+ error = self.document.reporter.error(
+ 'Document or section may not begin with a transition.',
+ source=node.source, line=node.line)
+ elif isinstance(node.parent[index - 1], nodes.transition):
+ error = self.document.reporter.error(
+ 'At least one body element must separate transitions; '
+ 'adjacent transitions are not allowed.',
+ source=node.source, line=node.line)
+ if error:
+ # Insert before node and update index.
+ node.parent.insert(index, error)
+ index += 1
+ assert index < len(node.parent)
+ if index != len(node.parent) - 1:
+ # No need to move the node.
+ return
+ # Node behind which the transition is to be moved.
+ sibling = node
+ # While sibling is the last node of its parent.
+ while index == len(sibling.parent) - 1:
+ sibling = sibling.parent
+ # If sibling is the whole document (i.e. it has no parent).
+ if sibling.parent is None:
+ # Transition at the end of document. Do not move the
+ # transition up, and place an error behind.
+ error = self.document.reporter.error(
+ 'Document may not end with a transition.',
+ line=node.line)
+ node.parent.insert(node.parent.index(node) + 1, error)
+ return
+ index = sibling.parent.index(sibling)
+ # Remove the original transition node.
+ node.parent.remove(node)
+ # Insert the transition after the sibling.
+ sibling.parent.insert(index + 1, node)
diff --git a/.venv/lib/python3.12/site-packages/docutils/transforms/parts.py b/.venv/lib/python3.12/site-packages/docutils/transforms/parts.py
new file mode 100644
index 00000000..fb3898fa
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/transforms/parts.py
@@ -0,0 +1,176 @@
+# $Id: parts.py 9038 2022-03-05 23:31:46Z milde $
+# Authors: David Goodger <goodger@python.org>; Ueli Schlaepfer; Dmitry Jemerov
+# Copyright: This module has been placed in the public domain.
+
+"""
+Transforms related to document parts.
+"""
+
+__docformat__ = 'reStructuredText'
+
+
+import sys
+from docutils import nodes
+from docutils.transforms import Transform
+
+
+class SectNum(Transform):
+
+ """
+ Automatically assigns numbers to the titles of document sections.
+
+ It is possible to limit the maximum section level for which the numbers
+ are added. For those sections that are auto-numbered, the "autonum"
+ attribute is set, informing the contents table generator that a different
+ form of the TOC should be used.
+ """
+
+ default_priority = 710
+ """Should be applied before `Contents`."""
+
+ def apply(self):
+ self.maxdepth = self.startnode.details.get('depth', None)
+ self.startvalue = self.startnode.details.get('start', 1)
+ self.prefix = self.startnode.details.get('prefix', '')
+ self.suffix = self.startnode.details.get('suffix', '')
+ self.startnode.parent.remove(self.startnode)
+ if self.document.settings.sectnum_xform:
+ if self.maxdepth is None:
+ self.maxdepth = sys.maxsize
+ self.update_section_numbers(self.document)
+ else: # store details for eventual section numbering by the writer
+ self.document.settings.sectnum_depth = self.maxdepth
+ self.document.settings.sectnum_start = self.startvalue
+ self.document.settings.sectnum_prefix = self.prefix
+ self.document.settings.sectnum_suffix = self.suffix
+
+ def update_section_numbers(self, node, prefix=(), depth=0):
+ depth += 1
+ if prefix:
+ sectnum = 1
+ else:
+ sectnum = self.startvalue
+ for child in node:
+ if isinstance(child, nodes.section):
+ numbers = prefix + (str(sectnum),)
+ title = child[0]
+ # Use &nbsp; for spacing:
+ generated = nodes.generated(
+ '', (self.prefix + '.'.join(numbers) + self.suffix
+ + '\u00a0' * 3),
+ classes=['sectnum'])
+ title.insert(0, generated)
+ title['auto'] = 1
+ if depth < self.maxdepth:
+ self.update_section_numbers(child, numbers, depth)
+ sectnum += 1
+
+
+class Contents(Transform):
+
+ """
+ This transform generates a table of contents from the entire document tree
+ or from a single branch. It locates "section" elements and builds them
+ into a nested bullet list, which is placed within a "topic" created by the
+ contents directive. A title is either explicitly specified, taken from
+ the appropriate language module, or omitted (local table of contents).
+ The depth may be specified. Two-way references between the table of
+ contents and section titles are generated (requires Writer support).
+
+ This transform requires a startnode, which contains generation
+ options and provides the location for the generated table of contents (the
+ startnode is replaced by the table of contents "topic").
+ """
+
+ default_priority = 720
+
+ def apply(self):
+ # let the writer (or output software) build the contents list?
+ toc_by_writer = getattr(self.document.settings, 'use_latex_toc', False)
+ details = self.startnode.details
+ if 'local' in details:
+ startnode = self.startnode.parent.parent
+ while not (isinstance(startnode, nodes.section)
+ or isinstance(startnode, nodes.document)):
+ # find the ToC root: a direct ancestor of startnode
+ startnode = startnode.parent
+ else:
+ startnode = self.document
+ self.toc_id = self.startnode.parent['ids'][0]
+ if 'backlinks' in details:
+ self.backlinks = details['backlinks']
+ else:
+ self.backlinks = self.document.settings.toc_backlinks
+ if toc_by_writer:
+ # move customization settings to the parent node
+ self.startnode.parent.attributes.update(details)
+ self.startnode.parent.remove(self.startnode)
+ else:
+ contents = self.build_contents(startnode)
+ if len(contents):
+ self.startnode.replace_self(contents)
+ else:
+ self.startnode.parent.parent.remove(self.startnode.parent)
+
+ def build_contents(self, node, level=0):
+ level += 1
+ sections = [sect for sect in node if isinstance(sect, nodes.section)]
+ entries = []
+ depth = self.startnode.details.get('depth', sys.maxsize)
+ for section in sections:
+ title = section[0]
+ auto = title.get('auto') # May be set by SectNum.
+ entrytext = self.copy_and_filter(title)
+ reference = nodes.reference('', '', refid=section['ids'][0],
+ *entrytext)
+ ref_id = self.document.set_id(reference,
+ suggested_prefix='toc-entry')
+ entry = nodes.paragraph('', '', reference)
+ item = nodes.list_item('', entry)
+ if (self.backlinks in ('entry', 'top')
+ and title.next_node(nodes.reference) is None):
+ if self.backlinks == 'entry':
+ title['refid'] = ref_id
+ elif self.backlinks == 'top':
+ title['refid'] = self.toc_id
+ if level < depth:
+ subsects = self.build_contents(section, level)
+ item += subsects
+ entries.append(item)
+ if entries:
+ contents = nodes.bullet_list('', *entries)
+ if auto: # auto-numbered sections
+ contents['classes'].append('auto-toc')
+ return contents
+ else:
+ return []
+
+ def copy_and_filter(self, node):
+ """Return a copy of a title, with references, images, etc. removed."""
+ visitor = ContentsFilter(self.document)
+ node.walkabout(visitor)
+ return visitor.get_entry_text()
+
+
+class ContentsFilter(nodes.TreeCopyVisitor):
+
+ def get_entry_text(self):
+ return self.get_tree_copy().children
+
+ def visit_citation_reference(self, node):
+ raise nodes.SkipNode
+
+ def visit_footnote_reference(self, node):
+ raise nodes.SkipNode
+
+ def visit_image(self, node):
+ if node.hasattr('alt'):
+ self.parent.append(nodes.Text(node['alt']))
+ raise nodes.SkipNode
+
+ def ignore_node_but_process_children(self, node):
+ raise nodes.SkipDeparture
+
+ visit_problematic = ignore_node_but_process_children
+ visit_reference = ignore_node_but_process_children
+ visit_target = ignore_node_but_process_children
diff --git a/.venv/lib/python3.12/site-packages/docutils/transforms/peps.py b/.venv/lib/python3.12/site-packages/docutils/transforms/peps.py
new file mode 100644
index 00000000..750dbf39
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/transforms/peps.py
@@ -0,0 +1,308 @@
+# $Id: peps.py 9037 2022-03-05 23:31:10Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Transforms for PEP processing.
+
+- `Headers`: Used to transform a PEP's initial RFC-2822 header. It remains a
+ field list, but some entries get processed.
+- `Contents`: Auto-inserts a table of contents.
+- `PEPZero`: Special processing for PEP 0.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import os
+import re
+import time
+from docutils import nodes, utils, languages
+from docutils import DataError
+from docutils.transforms import Transform
+from docutils.transforms import parts, references, misc
+
+
+class Headers(Transform):
+
+ """
+ Process fields in a PEP's initial RFC-2822 header.
+ """
+
+ default_priority = 360
+
+ pep_url = 'pep-%04d'
+ pep_cvs_url = ('http://hg.python.org'
+ '/peps/file/default/pep-%04d.txt')
+ rcs_keyword_substitutions = (
+ (re.compile(r'\$' r'RCSfile: (.+),v \$$', re.IGNORECASE), r'\1'),
+ (re.compile(r'\$[a-zA-Z]+: (.+) \$$'), r'\1'),)
+
+ def apply(self):
+ if not len(self.document):
+ # @@@ replace these DataErrors with proper system messages
+ raise DataError('Document tree is empty.')
+ header = self.document[0]
+ if (not isinstance(header, nodes.field_list)
+ or 'rfc2822' not in header['classes']):
+ raise DataError('Document does not begin with an RFC-2822 '
+ 'header; it is not a PEP.')
+ pep = None
+ for field in header:
+ if field[0].astext().lower() == 'pep': # should be the first field
+ value = field[1].astext()
+ try:
+ pep = int(value)
+ cvs_url = self.pep_cvs_url % pep
+ except ValueError:
+ pep = value
+ cvs_url = None
+ msg = self.document.reporter.warning(
+ '"PEP" header must contain an integer; "%s" is an '
+ 'invalid value.' % pep, base_node=field)
+ msgid = self.document.set_id(msg)
+ prb = nodes.problematic(value, value or '(none)',
+ refid=msgid)
+ prbid = self.document.set_id(prb)
+ msg.add_backref(prbid)
+ if len(field[1]):
+ field[1][0][:] = [prb]
+ else:
+ field[1] += nodes.paragraph('', '', prb)
+ break
+ if pep is None:
+ raise DataError('Document does not contain an RFC-2822 "PEP" '
+ 'header.')
+ if pep == 0:
+ # Special processing for PEP 0.
+ pending = nodes.pending(PEPZero)
+ self.document.insert(1, pending)
+ self.document.note_pending(pending)
+ if len(header) < 2 or header[1][0].astext().lower() != 'title':
+ raise DataError('No title!')
+ for field in header:
+ name = field[0].astext().lower()
+ body = field[1]
+ if len(body) > 1:
+ raise DataError('PEP header field body contains multiple '
+ 'elements:\n%s' % field.pformat(level=1))
+ elif len(body) == 1:
+ if not isinstance(body[0], nodes.paragraph):
+ raise DataError('PEP header field body may only contain '
+ 'a single paragraph:\n%s'
+ % field.pformat(level=1))
+ elif name == 'last-modified':
+ try:
+ date = time.strftime(
+ '%d-%b-%Y',
+ time.localtime(os.stat(self.document['source'])[8]))
+ except OSError:
+ date = 'unknown'
+ if cvs_url:
+ body += nodes.paragraph(
+ '', '', nodes.reference('', date, refuri=cvs_url))
+ else:
+ # empty
+ continue
+ para = body[0]
+ if name == 'author':
+ for node in para:
+ if isinstance(node, nodes.reference):
+ node.replace_self(mask_email(node))
+ elif name == 'discussions-to':
+ for node in para:
+ if isinstance(node, nodes.reference):
+ node.replace_self(mask_email(node, pep))
+ elif name in ('replaces', 'replaced-by', 'requires'):
+ newbody = []
+ space = nodes.Text(' ')
+ for refpep in re.split(r',?\s+', body.astext()):
+ pepno = int(refpep)
+ newbody.append(nodes.reference(
+ refpep, refpep,
+ refuri=(self.document.settings.pep_base_url
+ + self.pep_url % pepno)))
+ newbody.append(space)
+ para[:] = newbody[:-1] # drop trailing space
+ elif name == 'last-modified':
+ utils.clean_rcs_keywords(para, self.rcs_keyword_substitutions)
+ if cvs_url:
+ date = para.astext()
+ para[:] = [nodes.reference('', date, refuri=cvs_url)]
+ elif name == 'content-type':
+ pep_type = para.astext()
+ uri = self.document.settings.pep_base_url + self.pep_url % 12
+ para[:] = [nodes.reference('', pep_type, refuri=uri)]
+ elif name == 'version' and len(body):
+ utils.clean_rcs_keywords(para, self.rcs_keyword_substitutions)
+
+
+class Contents(Transform):
+
+ """
+ Insert an empty table of contents topic and a transform placeholder into
+ the document after the RFC 2822 header.
+ """
+
+ default_priority = 380
+
+ def apply(self):
+ language = languages.get_language(self.document.settings.language_code,
+ self.document.reporter)
+ name = language.labels['contents']
+ title = nodes.title('', name)
+ topic = nodes.topic('', title, classes=['contents'])
+ name = nodes.fully_normalize_name(name)
+ if not self.document.has_name(name):
+ topic['names'].append(name)
+ self.document.note_implicit_target(topic)
+ pending = nodes.pending(parts.Contents)
+ topic += pending
+ self.document.insert(1, topic)
+ self.document.note_pending(pending)
+
+
+class TargetNotes(Transform):
+
+ """
+ Locate the "References" section, insert a placeholder for an external
+ target footnote insertion transform at the end, and schedule the
+ transform to run immediately.
+ """
+
+ default_priority = 520
+
+ def apply(self):
+ doc = self.document
+ i = len(doc) - 1
+ refsect = copyright = None
+ while i >= 0 and isinstance(doc[i], nodes.section):
+ title_words = doc[i][0].astext().lower().split()
+ if 'references' in title_words:
+ refsect = doc[i]
+ break
+ elif 'copyright' in title_words:
+ copyright = i
+ i -= 1
+ if not refsect:
+ refsect = nodes.section()
+ refsect += nodes.title('', 'References')
+ doc.set_id(refsect)
+ if copyright:
+ # Put the new "References" section before "Copyright":
+ doc.insert(copyright, refsect)
+ else:
+ # Put the new "References" section at end of doc:
+ doc.append(refsect)
+ pending = nodes.pending(references.TargetNotes)
+ refsect.append(pending)
+ self.document.note_pending(pending, 0)
+ pending = nodes.pending(misc.CallBack,
+ details={'callback': self.cleanup_callback})
+ refsect.append(pending)
+ self.document.note_pending(pending, 1)
+
+ def cleanup_callback(self, pending):
+ """
+ Remove an empty "References" section.
+
+ Called after the `references.TargetNotes` transform is complete.
+ """
+ if len(pending.parent) == 2: # <title> and <pending>
+ pending.parent.parent.remove(pending.parent)
+
+
+class PEPZero(Transform):
+
+ """
+ Special processing for PEP 0.
+ """
+
+ default_priority = 760
+
+ def apply(self):
+ visitor = PEPZeroSpecial(self.document)
+ self.document.walk(visitor)
+ self.startnode.parent.remove(self.startnode)
+
+
+class PEPZeroSpecial(nodes.SparseNodeVisitor):
+
+ """
+ Perform the special processing needed by PEP 0:
+
+ - Mask email addresses.
+
+ - Link PEP numbers in the second column of 4-column tables to the PEPs
+ themselves.
+ """
+
+ pep_url = Headers.pep_url
+
+ def unknown_visit(self, node):
+ pass
+
+ def visit_reference(self, node):
+ node.replace_self(mask_email(node))
+
+ def visit_field_list(self, node):
+ if 'rfc2822' in node['classes']:
+ raise nodes.SkipNode
+
+ def visit_tgroup(self, node):
+ self.pep_table = node['cols'] == 4
+ self.entry = 0
+
+ def visit_colspec(self, node):
+ self.entry += 1
+ if self.pep_table and self.entry == 2:
+ node['classes'].append('num')
+
+ def visit_row(self, node):
+ self.entry = 0
+
+ def visit_entry(self, node):
+ self.entry += 1
+ if self.pep_table and self.entry == 2 and len(node) == 1:
+ node['classes'].append('num')
+ p = node[0]
+ if isinstance(p, nodes.paragraph) and len(p) == 1:
+ text = p.astext()
+ try:
+ pep = int(text)
+ ref = (self.document.settings.pep_base_url
+ + self.pep_url % pep)
+ p[0] = nodes.reference(text, text, refuri=ref)
+ except ValueError:
+ pass
+
+
+non_masked_addresses = ('peps@python.org',
+ 'python-list@python.org',
+ 'python-dev@python.org')
+
+
+def mask_email(ref, pepno=None):
+ """
+ Mask the email address in `ref` and return a replacement node.
+
+ `ref` is returned unchanged if it contains no email address.
+
+ For email addresses such as "user@host", mask the address as "user at
+ host" (text) to thwart simple email address harvesters (except for those
+ listed in `non_masked_addresses`). If a PEP number (`pepno`) is given,
+ return a reference including a default email subject.
+ """
+ if ref.hasattr('refuri') and ref['refuri'].startswith('mailto:'):
+ if ref['refuri'][8:] in non_masked_addresses:
+ replacement = ref[0]
+ else:
+ replacement_text = ref.astext().replace('@', '&#32;&#97;t&#32;')
+ replacement = nodes.raw('', replacement_text, format='html')
+ if pepno is None:
+ return replacement
+ else:
+ ref['refuri'] += '?subject=PEP%%20%s' % pepno
+ ref[:] = [replacement]
+ return ref
+ else:
+ return ref
diff --git a/.venv/lib/python3.12/site-packages/docutils/transforms/references.py b/.venv/lib/python3.12/site-packages/docutils/transforms/references.py
new file mode 100644
index 00000000..06b7697e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/transforms/references.py
@@ -0,0 +1,924 @@
+# $Id: references.py 9613 2024-04-06 13:27:52Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Transforms for resolving references.
+"""
+
+__docformat__ = 'reStructuredText'
+
+from docutils import nodes, utils
+from docutils.transforms import Transform
+
+
+class PropagateTargets(Transform):
+
+ """
+ Propagate empty internal targets to the next element.
+
+ Given the following nodes::
+
+ <target ids="internal1" names="internal1">
+ <target anonymous="1" ids="id1">
+ <target ids="internal2" names="internal2">
+ <paragraph>
+ This is a test.
+
+ `PropagateTargets` propagates the ids and names of the internal
+ targets preceding the paragraph to the paragraph itself::
+
+ <target refid="internal1">
+ <target anonymous="1" refid="id1">
+ <target refid="internal2">
+ <paragraph ids="internal2 id1 internal1" names="internal2 internal1">
+ This is a test.
+ """
+
+ default_priority = 260
+
+ def apply(self):
+ for target in self.document.findall(nodes.target):
+ # Only block-level targets without reference (like ".. _target:"):
+ if (isinstance(target.parent, nodes.TextElement)
+ or (target.hasattr('refid') or target.hasattr('refuri')
+ or target.hasattr('refname'))):
+ continue
+ assert len(target) == 0, 'error: block-level target has children'
+ next_node = target.next_node(ascend=True)
+ # skip system messages (may be removed by universal.FilterMessages)
+ while isinstance(next_node, nodes.system_message):
+ next_node = next_node.next_node(ascend=True, descend=False)
+ # Do not move names and ids into Invisibles (we'd lose the
+ # attributes) or different Targetables (e.g. footnotes).
+ if (next_node is None
+ or isinstance(next_node, (nodes.Invisible, nodes.Targetable))
+ and not isinstance(next_node, nodes.target)):
+ continue
+ next_node['ids'].extend(target['ids'])
+ next_node['names'].extend(target['names'])
+ # Set defaults for next_node.expect_referenced_by_name/id.
+ if not hasattr(next_node, 'expect_referenced_by_name'):
+ next_node.expect_referenced_by_name = {}
+ if not hasattr(next_node, 'expect_referenced_by_id'):
+ next_node.expect_referenced_by_id = {}
+ for id in target['ids']:
+ # Update IDs to node mapping.
+ self.document.ids[id] = next_node
+ # If next_node is referenced by id ``id``, this
+ # target shall be marked as referenced.
+ next_node.expect_referenced_by_id[id] = target
+ for name in target['names']:
+ next_node.expect_referenced_by_name[name] = target
+ # If there are any expect_referenced_by_... attributes
+ # in target set, copy them to next_node.
+ next_node.expect_referenced_by_name.update(
+ getattr(target, 'expect_referenced_by_name', {}))
+ next_node.expect_referenced_by_id.update(
+ getattr(target, 'expect_referenced_by_id', {}))
+ # Set refid to point to the first former ID of target
+ # which is now an ID of next_node.
+ target['refid'] = target['ids'][0]
+ # Clear ids and names; they have been moved to
+ # next_node.
+ target['ids'] = []
+ target['names'] = []
+ self.document.note_refid(target)
+
+
+class AnonymousHyperlinks(Transform):
+
+ """
+ Link anonymous references to targets. Given::
+
+ <paragraph>
+ <reference anonymous="1">
+ internal
+ <reference anonymous="1">
+ external
+ <target anonymous="1" ids="id1">
+ <target anonymous="1" ids="id2" refuri="http://external">
+
+ Corresponding references are linked via "refid" or resolved via "refuri"::
+
+ <paragraph>
+ <reference anonymous="1" refid="id1">
+ text
+ <reference anonymous="1" refuri="http://external">
+ external
+ <target anonymous="1" ids="id1">
+ <target anonymous="1" ids="id2" refuri="http://external">
+ """
+
+ default_priority = 440
+
+ def apply(self):
+ anonymous_refs = []
+ anonymous_targets = []
+ for node in self.document.findall(nodes.reference):
+ if node.get('anonymous'):
+ anonymous_refs.append(node)
+ for node in self.document.findall(nodes.target):
+ if node.get('anonymous'):
+ anonymous_targets.append(node)
+ if len(anonymous_refs) != len(anonymous_targets):
+ msg = self.document.reporter.error(
+ 'Anonymous hyperlink mismatch: %s references but %s '
+ 'targets.\nSee "backrefs" attribute for IDs.'
+ % (len(anonymous_refs), len(anonymous_targets)))
+ msgid = self.document.set_id(msg)
+ for ref in anonymous_refs:
+ prb = nodes.problematic(
+ ref.rawsource, ref.rawsource, refid=msgid)
+ prbid = self.document.set_id(prb)
+ msg.add_backref(prbid)
+ ref.replace_self(prb)
+ return
+ for ref, target in zip(anonymous_refs, anonymous_targets):
+ target.referenced = 1
+ while True:
+ if target.hasattr('refuri'):
+ ref['refuri'] = target['refuri']
+ ref.resolved = 1
+ break
+ else:
+ if not target['ids']:
+ # Propagated target.
+ target = self.document.ids[target['refid']]
+ continue
+ ref['refid'] = target['ids'][0]
+ self.document.note_refid(ref)
+ break
+
+
+class IndirectHyperlinks(Transform):
+
+ """
+ a) Indirect external references::
+
+ <paragraph>
+ <reference refname="indirect external">
+ indirect external
+ <target id="id1" name="direct external"
+ refuri="http://indirect">
+ <target id="id2" name="indirect external"
+ refname="direct external">
+
+ The "refuri" attribute is migrated back to all indirect targets
+ from the final direct target (i.e. a target not referring to
+ another indirect target)::
+
+ <paragraph>
+ <reference refname="indirect external">
+ indirect external
+ <target id="id1" name="direct external"
+ refuri="http://indirect">
+ <target id="id2" name="indirect external"
+ refuri="http://indirect">
+
+ Once the attribute is migrated, the preexisting "refname" attribute
+ is dropped.
+
+ b) Indirect internal references::
+
+ <target id="id1" name="final target">
+ <paragraph>
+ <reference refname="indirect internal">
+ indirect internal
+ <target id="id2" name="indirect internal 2"
+ refname="final target">
+ <target id="id3" name="indirect internal"
+ refname="indirect internal 2">
+
+ Targets which indirectly refer to an internal target become one-hop
+ indirect (their "refid" attributes are directly set to the internal
+ target's "id"). References which indirectly refer to an internal
+ target become direct internal references::
+
+ <target id="id1" name="final target">
+ <paragraph>
+ <reference refid="id1">
+ indirect internal
+ <target id="id2" name="indirect internal 2" refid="id1">
+ <target id="id3" name="indirect internal" refid="id1">
+ """
+
+ default_priority = 460
+
+ def apply(self):
+ for target in self.document.indirect_targets:
+ if not target.resolved:
+ self.resolve_indirect_target(target)
+ self.resolve_indirect_references(target)
+
+ def resolve_indirect_target(self, target):
+ refname = target.get('refname')
+ if refname is None:
+ reftarget_id = target['refid']
+ else:
+ reftarget_id = self.document.nameids.get(refname)
+ if not reftarget_id:
+ # Check the unknown_reference_resolvers
+ for resolver_function in \
+ self.document.transformer.unknown_reference_resolvers:
+ if resolver_function(target):
+ break
+ else:
+ self.nonexistent_indirect_target(target)
+ return
+ reftarget = self.document.ids[reftarget_id]
+ reftarget.note_referenced_by(id=reftarget_id)
+ if (isinstance(reftarget, nodes.target)
+ and not reftarget.resolved
+ and reftarget.hasattr('refname')):
+ if hasattr(target, 'multiply_indirect'):
+ self.circular_indirect_reference(target)
+ return
+ target.multiply_indirect = 1
+ self.resolve_indirect_target(reftarget) # multiply indirect
+ del target.multiply_indirect
+ if reftarget.hasattr('refuri'):
+ target['refuri'] = reftarget['refuri']
+ if 'refid' in target:
+ del target['refid']
+ elif reftarget.hasattr('refid'):
+ target['refid'] = reftarget['refid']
+ self.document.note_refid(target)
+ else:
+ if reftarget['ids']:
+ target['refid'] = reftarget_id
+ self.document.note_refid(target)
+ else:
+ self.nonexistent_indirect_target(target)
+ return
+ if refname is not None:
+ del target['refname']
+ target.resolved = 1
+
+ def nonexistent_indirect_target(self, target):
+ if target['refname'] in self.document.nameids:
+ self.indirect_target_error(target, 'which is a duplicate, and '
+ 'cannot be used as a unique reference')
+ else:
+ self.indirect_target_error(target, 'which does not exist')
+
+ def circular_indirect_reference(self, target):
+ self.indirect_target_error(target, 'forming a circular reference')
+
+ def indirect_target_error(self, target, explanation):
+ naming = ''
+ reflist = []
+ if target['names']:
+ naming = '"%s" ' % target['names'][0]
+ for name in target['names']:
+ reflist.extend(self.document.refnames.get(name, []))
+ for id in target['ids']:
+ reflist.extend(self.document.refids.get(id, []))
+ if target['ids']:
+ naming += '(id="%s")' % target['ids'][0]
+ msg = self.document.reporter.error(
+ 'Indirect hyperlink target %s refers to target "%s", %s.'
+ % (naming, target['refname'], explanation), base_node=target)
+ msgid = self.document.set_id(msg)
+ for ref in utils.uniq(reflist):
+ prb = nodes.problematic(
+ ref.rawsource, ref.rawsource, refid=msgid)
+ prbid = self.document.set_id(prb)
+ msg.add_backref(prbid)
+ ref.replace_self(prb)
+ target.resolved = 1
+
+ def resolve_indirect_references(self, target):
+ if target.hasattr('refid'):
+ attname = 'refid'
+ call_method = self.document.note_refid
+ elif target.hasattr('refuri'):
+ attname = 'refuri'
+ call_method = None
+ else:
+ return
+ attval = target[attname]
+ for name in target['names']:
+ reflist = self.document.refnames.get(name, [])
+ if reflist:
+ target.note_referenced_by(name=name)
+ for ref in reflist:
+ if ref.resolved:
+ continue
+ del ref['refname']
+ ref[attname] = attval
+ if call_method:
+ call_method(ref)
+ ref.resolved = 1
+ if isinstance(ref, nodes.target):
+ self.resolve_indirect_references(ref)
+ for id in target['ids']:
+ reflist = self.document.refids.get(id, [])
+ if reflist:
+ target.note_referenced_by(id=id)
+ for ref in reflist:
+ if ref.resolved:
+ continue
+ del ref['refid']
+ ref[attname] = attval
+ if call_method:
+ call_method(ref)
+ ref.resolved = 1
+ if isinstance(ref, nodes.target):
+ self.resolve_indirect_references(ref)
+
+
+class ExternalTargets(Transform):
+
+ """
+ Given::
+
+ <paragraph>
+ <reference refname="direct external">
+ direct external
+ <target id="id1" name="direct external" refuri="http://direct">
+
+ The "refname" attribute is replaced by the direct "refuri" attribute::
+
+ <paragraph>
+ <reference refuri="http://direct">
+ direct external
+ <target id="id1" name="direct external" refuri="http://direct">
+ """
+
+ default_priority = 640
+
+ def apply(self):
+ for target in self.document.findall(nodes.target):
+ if target.hasattr('refuri'):
+ refuri = target['refuri']
+ for name in target['names']:
+ reflist = self.document.refnames.get(name, [])
+ if reflist:
+ target.note_referenced_by(name=name)
+ for ref in reflist:
+ if ref.resolved:
+ continue
+ del ref['refname']
+ ref['refuri'] = refuri
+ ref.resolved = 1
+
+
+class InternalTargets(Transform):
+
+ default_priority = 660
+
+ def apply(self):
+ for target in self.document.findall(nodes.target):
+ if not target.hasattr('refuri') and not target.hasattr('refid'):
+ self.resolve_reference_ids(target)
+
+ def resolve_reference_ids(self, target):
+ """
+ Given::
+
+ <paragraph>
+ <reference refname="direct internal">
+ direct internal
+ <target id="id1" name="direct internal">
+
+ The "refname" attribute is replaced by "refid" linking to the target's
+ "id"::
+
+ <paragraph>
+ <reference refid="id1">
+ direct internal
+ <target id="id1" name="direct internal">
+ """
+ for name in target['names']:
+ refid = self.document.nameids.get(name)
+ reflist = self.document.refnames.get(name, [])
+ if reflist:
+ target.note_referenced_by(name=name)
+ for ref in reflist:
+ if ref.resolved:
+ continue
+ if refid:
+ del ref['refname']
+ ref['refid'] = refid
+ ref.resolved = 1
+
+
+class Footnotes(Transform):
+
+ """
+ Assign numbers to autonumbered footnotes, and resolve links to footnotes,
+ citations, and their references.
+
+ Given the following ``document`` as input::
+
+ <document>
+ <paragraph>
+ A labeled autonumbered footnote reference:
+ <footnote_reference auto="1" id="id1" refname="footnote">
+ <paragraph>
+ An unlabeled autonumbered footnote reference:
+ <footnote_reference auto="1" id="id2">
+ <footnote auto="1" id="id3">
+ <paragraph>
+ Unlabeled autonumbered footnote.
+ <footnote auto="1" id="footnote" name="footnote">
+ <paragraph>
+ Labeled autonumbered footnote.
+
+ Auto-numbered footnotes have attribute ``auto="1"`` and no label.
+ Auto-numbered footnote_references have no reference text (they're
+ empty elements). When resolving the numbering, a ``label`` element
+ is added to the beginning of the ``footnote``, and reference text
+ to the ``footnote_reference``.
+
+ The transformed result will be::
+
+ <document>
+ <paragraph>
+ A labeled autonumbered footnote reference:
+ <footnote_reference auto="1" id="id1" refid="footnote">
+ 2
+ <paragraph>
+ An unlabeled autonumbered footnote reference:
+ <footnote_reference auto="1" id="id2" refid="id3">
+ 1
+ <footnote auto="1" id="id3" backrefs="id2">
+ <label>
+ 1
+ <paragraph>
+ Unlabeled autonumbered footnote.
+ <footnote auto="1" id="footnote" name="footnote" backrefs="id1">
+ <label>
+ 2
+ <paragraph>
+ Labeled autonumbered footnote.
+
+ Note that the footnotes are not in the same order as the references.
+
+ The labels and reference text are added to the auto-numbered ``footnote``
+ and ``footnote_reference`` elements. Footnote elements are backlinked to
+ their references via "refids" attributes. References are assigned "id"
+ and "refid" attributes.
+
+ After adding labels and reference text, the "auto" attributes can be
+ ignored.
+ """
+
+ default_priority = 620
+
+ autofootnote_labels = None
+ """Keep track of unlabeled autonumbered footnotes."""
+
+ symbols = [
+ # Entries 1-4 and 6 below are from section 12.51 of
+ # The Chicago Manual of Style, 14th edition.
+ '*', # asterisk/star
+ '\u2020', # † &dagger; dagger
+ '\u2021', # ‡ &Dagger; double dagger
+ '\u00A7', # § &sect; section mark
+ '\u00B6', # ¶ &para; paragraph mark (pilcrow)
+ # (parallels ['||'] in CMoS)
+ '#', # number sign
+ # The entries below were chosen arbitrarily.
+ '\u2660', # ♠ &spades; spade suit
+ '\u2665', # ♡ &hearts; heart suit
+ '\u2666', # ♢ &diams; diamond suit
+ '\u2663', # ♣ &clubs; club suit
+ ]
+
+ def apply(self):
+ self.autofootnote_labels = []
+ startnum = self.document.autofootnote_start
+ self.document.autofootnote_start = self.number_footnotes(startnum)
+ self.number_footnote_references(startnum)
+ self.symbolize_footnotes()
+ self.resolve_footnotes_and_citations()
+
+ def number_footnotes(self, startnum):
+ """
+ Assign numbers to autonumbered footnotes.
+
+ For labeled autonumbered footnotes, copy the number over to
+ corresponding footnote references.
+ """
+ for footnote in self.document.autofootnotes:
+ while True:
+ label = str(startnum)
+ startnum += 1
+ if label not in self.document.nameids:
+ break
+ footnote.insert(0, nodes.label('', label))
+ for name in footnote['names']:
+ for ref in self.document.footnote_refs.get(name, []):
+ ref += nodes.Text(label)
+ ref.delattr('refname')
+ assert len(footnote['ids']) == len(ref['ids']) == 1
+ ref['refid'] = footnote['ids'][0]
+ footnote.add_backref(ref['ids'][0])
+ self.document.note_refid(ref)
+ ref.resolved = 1
+ if not footnote['names'] and not footnote['dupnames']:
+ footnote['names'].append(label)
+ self.document.note_explicit_target(footnote, footnote)
+ self.autofootnote_labels.append(label)
+ return startnum
+
+ def number_footnote_references(self, startnum):
+ """Assign numbers to autonumbered footnote references."""
+ i = 0
+ for ref in self.document.autofootnote_refs:
+ if ref.resolved or ref.hasattr('refid'):
+ continue
+ try:
+ label = self.autofootnote_labels[i]
+ except IndexError:
+ msg = self.document.reporter.error(
+ 'Too many autonumbered footnote references: only %s '
+ 'corresponding footnotes available.'
+ % len(self.autofootnote_labels), base_node=ref)
+ msgid = self.document.set_id(msg)
+ for ref in self.document.autofootnote_refs[i:]:
+ if ref.resolved or ref.hasattr('refname'):
+ continue
+ prb = nodes.problematic(
+ ref.rawsource, ref.rawsource, refid=msgid)
+ prbid = self.document.set_id(prb)
+ msg.add_backref(prbid)
+ ref.replace_self(prb)
+ break
+ ref += nodes.Text(label)
+ id = self.document.nameids[label]
+ footnote = self.document.ids[id]
+ ref['refid'] = id
+ self.document.note_refid(ref)
+ assert len(ref['ids']) == 1
+ footnote.add_backref(ref['ids'][0])
+ ref.resolved = 1
+ i += 1
+
+ def symbolize_footnotes(self):
+ """Add symbols indexes to "[*]"-style footnotes and references."""
+ labels = []
+ for footnote in self.document.symbol_footnotes:
+ reps, index = divmod(self.document.symbol_footnote_start,
+ len(self.symbols))
+ labeltext = self.symbols[index] * (reps + 1)
+ labels.append(labeltext)
+ footnote.insert(0, nodes.label('', labeltext))
+ self.document.symbol_footnote_start += 1
+ self.document.set_id(footnote)
+ i = 0
+ for ref in self.document.symbol_footnote_refs:
+ try:
+ ref += nodes.Text(labels[i])
+ except IndexError:
+ msg = self.document.reporter.error(
+ 'Too many symbol footnote references: only %s '
+ 'corresponding footnotes available.' % len(labels),
+ base_node=ref)
+ msgid = self.document.set_id(msg)
+ for ref in self.document.symbol_footnote_refs[i:]:
+ if ref.resolved or ref.hasattr('refid'):
+ continue
+ prb = nodes.problematic(
+ ref.rawsource, ref.rawsource, refid=msgid)
+ prbid = self.document.set_id(prb)
+ msg.add_backref(prbid)
+ ref.replace_self(prb)
+ break
+ footnote = self.document.symbol_footnotes[i]
+ assert len(footnote['ids']) == 1
+ ref['refid'] = footnote['ids'][0]
+ self.document.note_refid(ref)
+ footnote.add_backref(ref['ids'][0])
+ i += 1
+
+ def resolve_footnotes_and_citations(self):
+ """
+ Link manually-labeled footnotes and citations to/from their
+ references.
+ """
+ for footnote in self.document.footnotes:
+ for label in footnote['names']:
+ if label in self.document.footnote_refs:
+ reflist = self.document.footnote_refs[label]
+ self.resolve_references(footnote, reflist)
+ for citation in self.document.citations:
+ for label in citation['names']:
+ if label in self.document.citation_refs:
+ reflist = self.document.citation_refs[label]
+ self.resolve_references(citation, reflist)
+
+ def resolve_references(self, note, reflist):
+ assert len(note['ids']) == 1
+ id = note['ids'][0]
+ for ref in reflist:
+ if ref.resolved:
+ continue
+ ref.delattr('refname')
+ ref['refid'] = id
+ assert len(ref['ids']) == 1
+ note.add_backref(ref['ids'][0])
+ ref.resolved = 1
+ note.resolved = 1
+
+
+class CircularSubstitutionDefinitionError(Exception):
+ pass
+
+
+class Substitutions(Transform):
+
+ """
+ Given the following ``document`` as input::
+
+ <document>
+ <paragraph>
+ The
+ <substitution_reference refname="biohazard">
+ biohazard
+ symbol is deservedly scary-looking.
+ <substitution_definition name="biohazard">
+ <image alt="biohazard" uri="biohazard.png">
+
+ The ``substitution_reference`` will simply be replaced by the
+ contents of the corresponding ``substitution_definition``.
+
+ The transformed result will be::
+
+ <document>
+ <paragraph>
+ The
+ <image alt="biohazard" uri="biohazard.png">
+ symbol is deservedly scary-looking.
+ <substitution_definition name="biohazard">
+ <image alt="biohazard" uri="biohazard.png">
+ """
+
+ default_priority = 220
+ """The Substitutions transform has to be applied very early, before
+ `docutils.transforms.frontmatter.DocTitle` and others."""
+
+ def apply(self):
+ defs = self.document.substitution_defs
+ normed = self.document.substitution_names
+ nested = {}
+ line_length_limit = getattr(self.document.settings,
+ "line_length_limit", 10000)
+
+ subreflist = list(self.document.findall(nodes.substitution_reference))
+ for ref in subreflist:
+ msg = ''
+ refname = ref['refname']
+ if refname in defs:
+ key = refname
+ else:
+ normed_name = refname.lower()
+ key = normed.get(normed_name, None)
+ if key is None:
+ msg = self.document.reporter.error(
+ 'Undefined substitution referenced: "%s".'
+ % refname, base_node=ref)
+ else:
+ subdef = defs[key]
+ if len(subdef.astext()) > line_length_limit:
+ msg = self.document.reporter.error(
+ 'Substitution definition "%s" exceeds the'
+ ' line-length-limit.' % key)
+ if msg:
+ msgid = self.document.set_id(msg)
+ prb = nodes.problematic(
+ ref.rawsource, ref.rawsource, refid=msgid)
+ prbid = self.document.set_id(prb)
+ msg.add_backref(prbid)
+ ref.replace_self(prb)
+ continue
+
+ parent = ref.parent
+ index = parent.index(ref)
+ if ('ltrim' in subdef.attributes
+ or 'trim' in subdef.attributes):
+ if index > 0 and isinstance(parent[index - 1],
+ nodes.Text):
+ parent[index - 1] = parent[index - 1].rstrip()
+ if ('rtrim' in subdef.attributes
+ or 'trim' in subdef.attributes):
+ if (len(parent) > index + 1
+ and isinstance(parent[index + 1], nodes.Text)):
+ parent[index + 1] = parent[index + 1].lstrip()
+ subdef_copy = subdef.deepcopy()
+ try:
+ # Take care of nested substitution references:
+ for nested_ref in subdef_copy.findall(
+ nodes.substitution_reference):
+ nested_name = normed[nested_ref['refname'].lower()]
+ if nested_name in nested.setdefault(nested_name, []):
+ raise CircularSubstitutionDefinitionError
+ nested[nested_name].append(key)
+ nested_ref['ref-origin'] = ref
+ subreflist.append(nested_ref)
+ except CircularSubstitutionDefinitionError:
+ parent = ref.parent
+ if isinstance(parent, nodes.substitution_definition):
+ msg = self.document.reporter.error(
+ 'Circular substitution definition detected:',
+ nodes.literal_block(parent.rawsource,
+ parent.rawsource),
+ line=parent.line, base_node=parent)
+ parent.replace_self(msg)
+ else:
+ # find original ref substitution which caused this error
+ ref_origin = ref
+ while ref_origin.hasattr('ref-origin'):
+ ref_origin = ref_origin['ref-origin']
+ msg = self.document.reporter.error(
+ 'Circular substitution definition referenced: '
+ '"%s".' % refname, base_node=ref_origin)
+ msgid = self.document.set_id(msg)
+ prb = nodes.problematic(
+ ref.rawsource, ref.rawsource, refid=msgid)
+ prbid = self.document.set_id(prb)
+ msg.add_backref(prbid)
+ ref.replace_self(prb)
+ continue
+ ref.replace_self(subdef_copy.children)
+ # register refname of the replacement node(s)
+ # (needed for resolution of references)
+ for node in subdef_copy.children:
+ if isinstance(node, nodes.Referential):
+ # HACK: verify refname attribute exists.
+ # Test with docs/dev/todo.txt, see. |donate|
+ if 'refname' in node:
+ self.document.note_refname(node)
+
+
+class TargetNotes(Transform):
+
+ """
+ Creates a footnote for each external target in the text, and corresponding
+ footnote references after each reference.
+ """
+
+ default_priority = 540
+ """The TargetNotes transform has to be applied after `IndirectHyperlinks`
+ but before `Footnotes`."""
+
+ def __init__(self, document, startnode):
+ Transform.__init__(self, document, startnode=startnode)
+
+ self.classes = startnode.details.get('class', [])
+
+ def apply(self):
+ notes = {}
+ nodelist = []
+ for target in self.document.findall(nodes.target):
+ # Only external targets.
+ if not target.hasattr('refuri'):
+ continue
+ names = target['names']
+ refs = []
+ for name in names:
+ refs.extend(self.document.refnames.get(name, []))
+ if not refs:
+ continue
+ footnote = self.make_target_footnote(target['refuri'], refs,
+ notes)
+ if target['refuri'] not in notes:
+ notes[target['refuri']] = footnote
+ nodelist.append(footnote)
+ # Take care of anonymous references.
+ for ref in self.document.findall(nodes.reference):
+ if not ref.get('anonymous'):
+ continue
+ if ref.hasattr('refuri'):
+ footnote = self.make_target_footnote(ref['refuri'], [ref],
+ notes)
+ if ref['refuri'] not in notes:
+ notes[ref['refuri']] = footnote
+ nodelist.append(footnote)
+ self.startnode.replace_self(nodelist)
+
+ def make_target_footnote(self, refuri, refs, notes):
+ if refuri in notes: # duplicate?
+ footnote = notes[refuri]
+ assert len(footnote['names']) == 1
+ footnote_name = footnote['names'][0]
+ else: # original
+ footnote = nodes.footnote()
+ footnote_id = self.document.set_id(footnote)
+ # Use uppercase letters and a colon; they can't be
+ # produced inside names by the parser.
+ footnote_name = 'TARGET_NOTE: ' + footnote_id
+ footnote['auto'] = 1
+ footnote['names'] = [footnote_name]
+ footnote_paragraph = nodes.paragraph()
+ footnote_paragraph += nodes.reference('', refuri, refuri=refuri)
+ footnote += footnote_paragraph
+ self.document.note_autofootnote(footnote)
+ self.document.note_explicit_target(footnote, footnote)
+ for ref in refs:
+ if isinstance(ref, nodes.target):
+ continue
+ refnode = nodes.footnote_reference(refname=footnote_name, auto=1)
+ refnode['classes'] += self.classes
+ self.document.note_autofootnote_ref(refnode)
+ self.document.note_footnote_ref(refnode)
+ index = ref.parent.index(ref) + 1
+ reflist = [refnode]
+ if not utils.get_trim_footnote_ref_space(self.document.settings):
+ if self.classes:
+ reflist.insert(
+ 0, nodes.inline(text=' ', Classes=self.classes))
+ else:
+ reflist.insert(0, nodes.Text(' '))
+ ref.parent.insert(index, reflist)
+ return footnote
+
+
+class DanglingReferences(Transform):
+
+ """
+ Check for dangling references (incl. footnote & citation) and for
+ unreferenced targets.
+ """
+
+ default_priority = 850
+
+ def apply(self):
+ visitor = DanglingReferencesVisitor(
+ self.document,
+ self.document.transformer.unknown_reference_resolvers)
+ self.document.walk(visitor)
+ # *After* resolving all references, check for unreferenced
+ # targets:
+ for target in self.document.findall(nodes.target):
+ if not target.referenced:
+ if target.get('anonymous'):
+ # If we have unreferenced anonymous targets, there
+ # is already an error message about anonymous
+ # hyperlink mismatch; no need to generate another
+ # message.
+ continue
+ if target['names']:
+ naming = target['names'][0]
+ elif target['ids']:
+ naming = target['ids'][0]
+ else:
+ # Hack: Propagated targets always have their refid
+ # attribute set.
+ naming = target['refid']
+ self.document.reporter.info(
+ 'Hyperlink target "%s" is not referenced.'
+ % naming, base_node=target)
+
+
+class DanglingReferencesVisitor(nodes.SparseNodeVisitor):
+
+ def __init__(self, document, unknown_reference_resolvers):
+ nodes.SparseNodeVisitor.__init__(self, document)
+ self.document = document
+ self.unknown_reference_resolvers = unknown_reference_resolvers
+
+ def unknown_visit(self, node):
+ pass
+
+ def visit_reference(self, node):
+ if node.resolved or not node.hasattr('refname'):
+ return
+ refname = node['refname']
+ id = self.document.nameids.get(refname)
+ if id is None:
+ for resolver_function in self.unknown_reference_resolvers:
+ if resolver_function(node):
+ break
+ else:
+ if (getattr(self.document.settings, 'use_bibtex', False)
+ and isinstance(node, nodes.citation_reference)):
+ # targets added from BibTeX database by LaTeX
+ node.resolved = True
+ return
+ if refname in self.document.nameids:
+ msg = self.document.reporter.error(
+ 'Duplicate target name, cannot be used as a unique '
+ 'reference: "%s".' % (node['refname']), base_node=node)
+ else:
+ msg = self.document.reporter.error(
+ f'Unknown target name: "{node["refname"]}".',
+ base_node=node)
+ msgid = self.document.set_id(msg)
+ prb = nodes.problematic(
+ node.rawsource, node.rawsource, refid=msgid)
+ try:
+ prbid = node['ids'][0]
+ except IndexError:
+ prbid = self.document.set_id(prb)
+ msg.add_backref(prbid)
+ node.replace_self(prb)
+ else:
+ del node['refname']
+ node['refid'] = id
+ self.document.ids[id].note_referenced_by(id=id)
+ node.resolved = True
+
+ visit_footnote_reference = visit_citation_reference = visit_reference
diff --git a/.venv/lib/python3.12/site-packages/docutils/transforms/universal.py b/.venv/lib/python3.12/site-packages/docutils/transforms/universal.py
new file mode 100644
index 00000000..00d57b4f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/transforms/universal.py
@@ -0,0 +1,335 @@
+# $Id: universal.py 9502 2023-12-14 22:39:08Z milde $
+# Authors: David Goodger <goodger@python.org>; Ueli Schlaepfer; Günter Milde
+# Maintainer: docutils-develop@lists.sourceforge.net
+# Copyright: This module has been placed in the public domain.
+
+"""
+Transforms needed by most or all documents:
+
+- `Decorations`: Generate a document's header & footer.
+- `ExposeInternals`: Expose internal attributes.
+- `Messages`: Placement of system messages generated after parsing.
+- `FilterMessages`: Remove system messages below verbosity threshold.
+- `TestMessages`: Like `Messages`, used on test runs.
+- `StripComments`: Remove comment elements from the document tree.
+- `StripClassesAndElements`: Remove elements with classes
+ in `self.document.settings.strip_elements_with_classes`
+ and class values in `self.document.settings.strip_classes`.
+- `SmartQuotes`: Replace ASCII quotation marks with typographic form.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import re
+import time
+from docutils import nodes, utils
+from docutils.transforms import Transform
+from docutils.utils import smartquotes
+
+
+class Decorations(Transform):
+
+ """
+ Populate a document's decoration element (header, footer).
+ """
+
+ default_priority = 820
+
+ def apply(self):
+ header_nodes = self.generate_header()
+ if header_nodes:
+ decoration = self.document.get_decoration()
+ header = decoration.get_header()
+ header.extend(header_nodes)
+ footer_nodes = self.generate_footer()
+ if footer_nodes:
+ decoration = self.document.get_decoration()
+ footer = decoration.get_footer()
+ footer.extend(footer_nodes)
+
+ def generate_header(self):
+ return None
+
+ def generate_footer(self):
+ # @@@ Text is hard-coded for now.
+ # Should be made dynamic (language-dependent).
+ # @@@ Use timestamp from the `SOURCE_DATE_EPOCH`_ environment variable
+ # for the datestamp?
+ # See https://sourceforge.net/p/docutils/patches/132/
+ # and https://reproducible-builds.org/specs/source-date-epoch/
+ settings = self.document.settings
+ if (settings.generator or settings.datestamp
+ or settings.source_link or settings.source_url):
+ text = []
+ if (settings.source_link and settings._source
+ or settings.source_url):
+ if settings.source_url:
+ source = settings.source_url
+ else:
+ source = utils.relative_path(settings._destination,
+ settings._source)
+ text.extend([
+ nodes.reference('', 'View document source',
+ refuri=source),
+ nodes.Text('.\n')])
+ if settings.datestamp:
+ datestamp = time.strftime(settings.datestamp, time.gmtime())
+ text.append(nodes.Text('Generated on: ' + datestamp + '.\n'))
+ if settings.generator:
+ text.extend([
+ nodes.Text('Generated by '),
+ nodes.reference('', 'Docutils',
+ refuri='https://docutils.sourceforge.io/'),
+ nodes.Text(' from '),
+ nodes.reference('', 'reStructuredText',
+ refuri='https://docutils.sourceforge.io/'
+ 'rst.html'),
+ nodes.Text(' source.\n')])
+ return [nodes.paragraph('', '', *text)]
+ else:
+ return None
+
+
+class ExposeInternals(Transform):
+
+ """
+ Expose internal attributes if ``expose_internals`` setting is set.
+ """
+
+ default_priority = 840
+
+ def not_Text(self, node):
+ return not isinstance(node, nodes.Text)
+
+ def apply(self):
+ if self.document.settings.expose_internals:
+ for node in self.document.findall(self.not_Text):
+ for att in self.document.settings.expose_internals:
+ value = getattr(node, att, None)
+ if value is not None:
+ node['internal:' + att] = value
+
+
+class Messages(Transform):
+
+ """
+ Place any system messages generated after parsing into a dedicated section
+ of the document.
+ """
+
+ default_priority = 860
+
+ def apply(self):
+ messages = self.document.transform_messages
+ loose_messages = [msg for msg in messages if not msg.parent]
+ if loose_messages:
+ section = nodes.section(classes=['system-messages'])
+ # @@@ get this from the language module?
+ section += nodes.title('', 'Docutils System Messages')
+ section += loose_messages
+ self.document.transform_messages[:] = []
+ self.document += section
+
+
+class FilterMessages(Transform):
+
+ """
+ Remove system messages below verbosity threshold.
+
+ Also convert <problematic> nodes referencing removed messages
+ to <Text> nodes and remove "System Messages" section if empty.
+ """
+
+ default_priority = 870
+
+ def apply(self):
+ for node in tuple(self.document.findall(nodes.system_message)):
+ if node['level'] < self.document.reporter.report_level:
+ node.parent.remove(node)
+ try: # also remove id-entry
+ del self.document.ids[node['ids'][0]]
+ except (IndexError):
+ pass
+ for node in tuple(self.document.findall(nodes.problematic)):
+ if node['refid'] not in self.document.ids:
+ node.parent.replace(node, nodes.Text(node.astext()))
+ for node in self.document.findall(nodes.section):
+ if "system-messages" in node['classes'] and len(node) == 1:
+ node.parent.remove(node)
+
+
+class TestMessages(Transform):
+
+ """
+ Append all post-parse system messages to the end of the document.
+
+ Used for testing purposes.
+ """
+
+ # marker for pytest to ignore this class during test discovery
+ __test__ = False
+
+ default_priority = 880
+
+ def apply(self):
+ for msg in self.document.transform_messages:
+ if not msg.parent:
+ self.document += msg
+
+
+class StripComments(Transform):
+
+ """
+ Remove comment elements from the document tree (only if the
+ ``strip_comments`` setting is enabled).
+ """
+
+ default_priority = 740
+
+ def apply(self):
+ if self.document.settings.strip_comments:
+ for node in tuple(self.document.findall(nodes.comment)):
+ node.parent.remove(node)
+
+
+class StripClassesAndElements(Transform):
+
+ """
+ Remove from the document tree all elements with classes in
+ `self.document.settings.strip_elements_with_classes` and all "classes"
+ attribute values in `self.document.settings.strip_classes`.
+ """
+
+ default_priority = 420
+
+ def apply(self):
+ if self.document.settings.strip_elements_with_classes:
+ self.strip_elements = {*self.document.settings
+ .strip_elements_with_classes}
+ # Iterate over a tuple as removing the current node
+ # corrupts the iterator returned by `iter`:
+ for node in tuple(self.document.findall(self.check_classes)):
+ node.parent.remove(node)
+
+ if not self.document.settings.strip_classes:
+ return
+ strip_classes = self.document.settings.strip_classes
+ for node in self.document.findall(nodes.Element):
+ for class_value in strip_classes:
+ try:
+ node['classes'].remove(class_value)
+ except ValueError:
+ pass
+
+ def check_classes(self, node):
+ if not isinstance(node, nodes.Element):
+ return False
+ for class_value in node['classes'][:]:
+ if class_value in self.strip_elements:
+ return True
+ return False
+
+
+class SmartQuotes(Transform):
+
+ """
+ Replace ASCII quotation marks with typographic form.
+
+ Also replace multiple dashes with em-dash/en-dash characters.
+ """
+
+ default_priority = 855
+
+ nodes_to_skip = (nodes.FixedTextElement, nodes.Special)
+ """Do not apply "smartquotes" to instances of these block-level nodes."""
+
+ literal_nodes = (nodes.FixedTextElement, nodes.Special,
+ nodes.image, nodes.literal, nodes.math,
+ nodes.raw, nodes.problematic)
+ """Do not apply smartquotes to instances of these inline nodes."""
+
+ smartquotes_action = 'qDe'
+ """Setting to select smartquote transformations.
+
+ The default 'qDe' educates normal quote characters: (", '),
+ em- and en-dashes (---, --) and ellipses (...).
+ """
+
+ def __init__(self, document, startnode):
+ Transform.__init__(self, document, startnode=startnode)
+ self.unsupported_languages = set()
+
+ def get_tokens(self, txtnodes):
+ # A generator that yields ``(texttype, nodetext)`` tuples for a list
+ # of "Text" nodes (interface to ``smartquotes.educate_tokens()``).
+ for node in txtnodes:
+ if (isinstance(node.parent, self.literal_nodes)
+ or isinstance(node.parent.parent, self.literal_nodes)):
+ yield 'literal', str(node)
+ else:
+ # SmartQuotes uses backslash escapes instead of null-escapes
+ # Insert backslashes before escaped "active" characters.
+ txt = re.sub('(?<=\x00)([-\\\'".`])', r'\\\1', str(node))
+ yield 'plain', txt
+
+ def apply(self):
+ smart_quotes = self.document.settings.setdefault('smart_quotes',
+ False)
+ if not smart_quotes:
+ return
+ try:
+ alternative = smart_quotes.startswith('alt')
+ except AttributeError:
+ alternative = False
+
+ document_language = self.document.settings.language_code
+ lc_smartquotes = self.document.settings.smartquotes_locales
+ if lc_smartquotes:
+ smartquotes.smartchars.quotes.update(dict(lc_smartquotes))
+
+ # "Educate" quotes in normal text. Handle each block of text
+ # (TextElement node) as a unit to keep context around inline nodes:
+ for node in self.document.findall(nodes.TextElement):
+ # skip preformatted text blocks and special elements:
+ if isinstance(node, self.nodes_to_skip):
+ continue
+ # nested TextElements are not "block-level" elements:
+ if isinstance(node.parent, nodes.TextElement):
+ continue
+
+ # list of text nodes in the "text block":
+ txtnodes = [txtnode for txtnode in node.findall(nodes.Text)
+ if not isinstance(txtnode.parent,
+ nodes.option_string)]
+
+ # language: use typographical quotes for language "lang"
+ lang = node.get_language_code(document_language)
+ # use alternative form if `smart-quotes` setting starts with "alt":
+ if alternative:
+ if '-x-altquot' in lang:
+ lang = lang.replace('-x-altquot', '')
+ else:
+ lang += '-x-altquot'
+ # drop unsupported subtags:
+ for tag in utils.normalize_language_tag(lang):
+ if tag in smartquotes.smartchars.quotes:
+ lang = tag
+ break
+ else: # language not supported -- keep ASCII quotes
+ if lang not in self.unsupported_languages:
+ self.document.reporter.warning(
+ 'No smart quotes defined for language "%s".' % lang,
+ base_node=node)
+ self.unsupported_languages.add(lang)
+ lang = ''
+
+ # Iterator educating quotes in plain text:
+ # (see "utils/smartquotes.py" for the attribute setting)
+ teacher = smartquotes.educate_tokens(
+ self.get_tokens(txtnodes),
+ attr=self.smartquotes_action, language=lang)
+
+ for txtnode, newtext in zip(txtnodes, teacher):
+ txtnode.parent.replace(txtnode, nodes.Text(newtext))
+
+ self.unsupported_languages.clear()
diff --git a/.venv/lib/python3.12/site-packages/docutils/transforms/writer_aux.py b/.venv/lib/python3.12/site-packages/docutils/transforms/writer_aux.py
new file mode 100644
index 00000000..8f303be3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/transforms/writer_aux.py
@@ -0,0 +1,99 @@
+# $Id: writer_aux.py 9037 2022-03-05 23:31:10Z milde $
+# Author: Lea Wiemann <LeWiemann@gmail.com>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Auxiliary transforms mainly to be used by Writer components.
+
+This module is called "writer_aux" because otherwise there would be
+conflicting imports like this one::
+
+ from docutils import writers
+ from docutils.transforms import writers
+"""
+
+__docformat__ = 'reStructuredText'
+
+import warnings
+
+from docutils import nodes, languages
+from docutils.transforms import Transform
+
+
+class Compound(Transform):
+
+ """
+ .. warning:: This transform is not used by Docutils since Dec 2010
+ and will be removed in Docutils 0.21 or later.
+
+ Flatten all compound paragraphs. For example, transform ::
+
+ <compound>
+ <paragraph>
+ <literal_block>
+ <paragraph>
+
+ into ::
+
+ <paragraph>
+ <literal_block classes="continued">
+ <paragraph classes="continued">
+ """
+
+ default_priority = 910
+
+ def __init__(self, document, startnode=None):
+ warnings.warn('docutils.transforms.writer_aux.Compound is deprecated'
+ ' and will be removed in Docutils 0.21 or later.',
+ DeprecationWarning, stacklevel=2)
+ super().__init__(document, startnode)
+
+ def apply(self):
+ for compound in self.document.findall(nodes.compound):
+ first_child = True
+ for child in compound:
+ if first_child:
+ if not isinstance(child, nodes.Invisible):
+ first_child = False
+ else:
+ child['classes'].append('continued')
+ # Substitute children for compound.
+ compound.replace_self(compound[:])
+
+
+class Admonitions(Transform):
+
+ """
+ Transform specific admonitions, like this:
+
+ <note>
+ <paragraph>
+ Note contents ...
+
+ into generic admonitions, like this::
+
+ <admonition classes="note">
+ <title>
+ Note
+ <paragraph>
+ Note contents ...
+
+ The admonition title is localized.
+ """
+
+ default_priority = 920
+
+ def apply(self):
+ language = languages.get_language(self.document.settings.language_code,
+ self.document.reporter)
+ for node in self.document.findall(nodes.Admonition):
+ node_name = node.__class__.__name__
+ # Set class, so that we know what node this admonition came from.
+ node['classes'].append(node_name)
+ if not isinstance(node, nodes.admonition):
+ # Specific admonition. Transform into a generic admonition.
+ admonition = nodes.admonition(node.rawsource, *node.children,
+ **node.attributes)
+ title = nodes.title('', language.labels[node_name])
+ admonition.insert(0, title)
+ node.replace_self(admonition)
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/__init__.py b/.venv/lib/python3.12/site-packages/docutils/utils/__init__.py
new file mode 100644
index 00000000..777f8013
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/__init__.py
@@ -0,0 +1,861 @@
+# $Id: __init__.py 9544 2024-02-17 10:37:45Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+Miscellaneous utilities for the documentation utilities.
+"""
+
+__docformat__ = 'reStructuredText'
+
+import sys
+import os
+import os.path
+from pathlib import PurePath, Path
+import re
+import itertools
+import warnings
+import unicodedata
+
+from docutils import ApplicationError, DataError, __version_info__
+from docutils import io, nodes
+# for backwards compatibility
+from docutils.nodes import unescape # noqa: F401
+
+
+class SystemMessage(ApplicationError):
+
+ def __init__(self, system_message, level):
+ Exception.__init__(self, system_message.astext())
+ self.level = level
+
+
+class SystemMessagePropagation(ApplicationError):
+ pass
+
+
+class Reporter:
+
+ """
+ Info/warning/error reporter and ``system_message`` element generator.
+
+ Five levels of system messages are defined, along with corresponding
+ methods: `debug()`, `info()`, `warning()`, `error()`, and `severe()`.
+
+ There is typically one Reporter object per process. A Reporter object is
+ instantiated with thresholds for reporting (generating warnings) and
+ halting processing (raising exceptions), a switch to turn debug output on
+ or off, and an I/O stream for warnings. These are stored as instance
+ attributes.
+
+ When a system message is generated, its level is compared to the stored
+ thresholds, and a warning or error is generated as appropriate. Debug
+ messages are produced if the stored debug switch is on, independently of
+ other thresholds. Message output is sent to the stored warning stream if
+ not set to ''.
+
+ The Reporter class also employs a modified form of the "Observer" pattern
+ [GoF95]_ to track system messages generated. The `attach_observer` method
+ should be called before parsing, with a bound method or function which
+ accepts system messages. The observer can be removed with
+ `detach_observer`, and another added in its place.
+
+ .. [GoF95] Gamma, Helm, Johnson, Vlissides. *Design Patterns: Elements of
+ Reusable Object-Oriented Software*. Addison-Wesley, Reading, MA, USA,
+ 1995.
+ """
+
+ levels = 'DEBUG INFO WARNING ERROR SEVERE'.split()
+ """List of names for system message levels, indexed by level."""
+
+ # system message level constants:
+ (DEBUG_LEVEL,
+ INFO_LEVEL,
+ WARNING_LEVEL,
+ ERROR_LEVEL,
+ SEVERE_LEVEL) = range(5)
+
+ def __init__(self, source, report_level, halt_level, stream=None,
+ debug=False, encoding=None, error_handler='backslashreplace'):
+ """
+ :Parameters:
+ - `source`: The path to or description of the source data.
+ - `report_level`: The level at or above which warning output will
+ be sent to `stream`.
+ - `halt_level`: The level at or above which `SystemMessage`
+ exceptions will be raised, halting execution.
+ - `debug`: Show debug (level=0) system messages?
+ - `stream`: Where warning output is sent. Can be file-like (has a
+ ``.write`` method), a string (file name, opened for writing),
+ '' (empty string) or `False` (for discarding all stream messages)
+ or `None` (implies `sys.stderr`; default).
+ - `encoding`: The output encoding.
+ - `error_handler`: The error handler for stderr output encoding.
+ """
+
+ self.source = source
+ """The path to or description of the source data."""
+
+ self.error_handler = error_handler
+ """The character encoding error handler."""
+
+ self.debug_flag = debug
+ """Show debug (level=0) system messages?"""
+
+ self.report_level = report_level
+ """The level at or above which warning output will be sent
+ to `self.stream`."""
+
+ self.halt_level = halt_level
+ """The level at or above which `SystemMessage` exceptions
+ will be raised, halting execution."""
+
+ if not isinstance(stream, io.ErrorOutput):
+ stream = io.ErrorOutput(stream, encoding, error_handler)
+
+ self.stream = stream
+ """Where warning output is sent."""
+
+ self.encoding = encoding or getattr(stream, 'encoding', 'ascii')
+ """The output character encoding."""
+
+ self.observers = []
+ """List of bound methods or functions to call with each system_message
+ created."""
+
+ self.max_level = -1
+ """The highest level system message generated so far."""
+
+ def set_conditions(self, category, report_level, halt_level,
+ stream=None, debug=False):
+ warnings.warn('docutils.utils.Reporter.set_conditions() deprecated; '
+ 'Will be removed in Docutils 0.21 or later. '
+ 'Set attributes via configuration settings or directly.',
+ DeprecationWarning, stacklevel=2)
+ self.report_level = report_level
+ self.halt_level = halt_level
+ if not isinstance(stream, io.ErrorOutput):
+ stream = io.ErrorOutput(stream, self.encoding, self.error_handler)
+ self.stream = stream
+ self.debug_flag = debug
+
+ def attach_observer(self, observer):
+ """
+ The `observer` parameter is a function or bound method which takes one
+ argument, a `nodes.system_message` instance.
+ """
+ self.observers.append(observer)
+
+ def detach_observer(self, observer):
+ self.observers.remove(observer)
+
+ def notify_observers(self, message):
+ for observer in self.observers:
+ observer(message)
+
+ def system_message(self, level, message, *children, **kwargs):
+ """
+ Return a system_message object.
+
+ Raise an exception or generate a warning if appropriate.
+ """
+ # `message` can be a `str` or `Exception` instance.
+ if isinstance(message, Exception):
+ message = str(message)
+
+ attributes = kwargs.copy()
+ if 'base_node' in kwargs:
+ source, line = get_source_line(kwargs['base_node'])
+ del attributes['base_node']
+ if source is not None:
+ attributes.setdefault('source', source)
+ if line is not None:
+ attributes.setdefault('line', line)
+ # assert source is not None, "line- but no source-argument"
+ if 'source' not in attributes:
+ # 'line' is absolute line number
+ try:
+ source, line = self.get_source_and_line(attributes.get('line'))
+ except AttributeError:
+ source, line = None, None
+ if source is not None:
+ attributes['source'] = source
+ if line is not None:
+ attributes['line'] = line
+ # assert attributes['line'] is not None, (message, kwargs)
+ # assert attributes['source'] is not None, (message, kwargs)
+ attributes.setdefault('source', self.source)
+
+ msg = nodes.system_message(message, level=level,
+ type=self.levels[level],
+ *children, **attributes)
+ if self.stream and (level >= self.report_level
+ or self.debug_flag and level == self.DEBUG_LEVEL
+ or level >= self.halt_level):
+ self.stream.write(msg.astext() + '\n')
+ if level >= self.halt_level:
+ raise SystemMessage(msg, level)
+ if level > self.DEBUG_LEVEL or self.debug_flag:
+ self.notify_observers(msg)
+ self.max_level = max(level, self.max_level)
+ return msg
+
+ def debug(self, *args, **kwargs):
+ """
+ Level-0, "DEBUG": an internal reporting issue. Typically, there is no
+ effect on the processing. Level-0 system messages are handled
+ separately from the others.
+ """
+ if self.debug_flag:
+ return self.system_message(self.DEBUG_LEVEL, *args, **kwargs)
+
+ def info(self, *args, **kwargs):
+ """
+ Level-1, "INFO": a minor issue that can be ignored. Typically there is
+ no effect on processing, and level-1 system messages are not reported.
+ """
+ return self.system_message(self.INFO_LEVEL, *args, **kwargs)
+
+ def warning(self, *args, **kwargs):
+ """
+ Level-2, "WARNING": an issue that should be addressed. If ignored,
+ there may be unpredictable problems with the output.
+ """
+ return self.system_message(self.WARNING_LEVEL, *args, **kwargs)
+
+ def error(self, *args, **kwargs):
+ """
+ Level-3, "ERROR": an error that should be addressed. If ignored, the
+ output will contain errors.
+ """
+ return self.system_message(self.ERROR_LEVEL, *args, **kwargs)
+
+ def severe(self, *args, **kwargs):
+ """
+ Level-4, "SEVERE": a severe error that must be addressed. If ignored,
+ the output will contain severe errors. Typically level-4 system
+ messages are turned into exceptions which halt processing.
+ """
+ return self.system_message(self.SEVERE_LEVEL, *args, **kwargs)
+
+
+class ExtensionOptionError(DataError): pass
+class BadOptionError(ExtensionOptionError): pass
+class BadOptionDataError(ExtensionOptionError): pass
+class DuplicateOptionError(ExtensionOptionError): pass
+
+
+def extract_extension_options(field_list, options_spec):
+ """
+ Return a dictionary mapping extension option names to converted values.
+
+ :Parameters:
+ - `field_list`: A flat field list without field arguments, where each
+ field body consists of a single paragraph only.
+ - `options_spec`: Dictionary mapping known option names to a
+ conversion function such as `int` or `float`.
+
+ :Exceptions:
+ - `KeyError` for unknown option names.
+ - `ValueError` for invalid option values (raised by the conversion
+ function).
+ - `TypeError` for invalid option value types (raised by conversion
+ function).
+ - `DuplicateOptionError` for duplicate options.
+ - `BadOptionError` for invalid fields.
+ - `BadOptionDataError` for invalid option data (missing name,
+ missing data, bad quotes, etc.).
+ """
+ option_list = extract_options(field_list)
+ return assemble_option_dict(option_list, options_spec)
+
+
+def extract_options(field_list):
+ """
+ Return a list of option (name, value) pairs from field names & bodies.
+
+ :Parameter:
+ `field_list`: A flat field list, where each field name is a single
+ word and each field body consists of a single paragraph only.
+
+ :Exceptions:
+ - `BadOptionError` for invalid fields.
+ - `BadOptionDataError` for invalid option data (missing name,
+ missing data, bad quotes, etc.).
+ """
+ option_list = []
+ for field in field_list:
+ if len(field[0].astext().split()) != 1:
+ raise BadOptionError(
+ 'extension option field name may not contain multiple words')
+ name = str(field[0].astext().lower())
+ body = field[1]
+ if len(body) == 0:
+ data = None
+ elif (len(body) > 1
+ or not isinstance(body[0], nodes.paragraph)
+ or len(body[0]) != 1
+ or not isinstance(body[0][0], nodes.Text)):
+ raise BadOptionDataError(
+ 'extension option field body may contain\n'
+ 'a single paragraph only (option "%s")' % name)
+ else:
+ data = body[0][0].astext()
+ option_list.append((name, data))
+ return option_list
+
+
+def assemble_option_dict(option_list, options_spec):
+ """
+ Return a mapping of option names to values.
+
+ :Parameters:
+ - `option_list`: A list of (name, value) pairs (the output of
+ `extract_options()`).
+ - `options_spec`: Dictionary mapping known option names to a
+ conversion function such as `int` or `float`.
+
+ :Exceptions:
+ - `KeyError` for unknown option names.
+ - `DuplicateOptionError` for duplicate options.
+ - `ValueError` for invalid option values (raised by conversion
+ function).
+ - `TypeError` for invalid option value types (raised by conversion
+ function).
+ """
+ options = {}
+ for name, value in option_list:
+ convertor = options_spec[name] # raises KeyError if unknown
+ if convertor is None:
+ raise KeyError(name) # or if explicitly disabled
+ if name in options:
+ raise DuplicateOptionError('duplicate option "%s"' % name)
+ try:
+ options[name] = convertor(value)
+ except (ValueError, TypeError) as detail:
+ raise detail.__class__('(option: "%s"; value: %r)\n%s'
+ % (name, value, ' '.join(detail.args)))
+ return options
+
+
+class NameValueError(DataError): pass
+
+
+def decode_path(path):
+ """
+ Ensure `path` is Unicode. Return `str` instance.
+
+ Decode file/path string in a failsafe manner if not already done.
+ """
+ # TODO: is this still required with Python 3?
+ if isinstance(path, str):
+ return path
+ try:
+ path = path.decode(sys.getfilesystemencoding(), 'strict')
+ except AttributeError: # default value None has no decode method
+ if not path:
+ return ''
+ raise ValueError('`path` value must be a String or ``None``, '
+ f'not {path!r}')
+ except UnicodeDecodeError:
+ try:
+ path = path.decode('utf-8', 'strict')
+ except UnicodeDecodeError:
+ path = path.decode('ascii', 'replace')
+ return path
+
+
+def extract_name_value(line):
+ """
+ Return a list of (name, value) from a line of the form "name=value ...".
+
+ :Exception:
+ `NameValueError` for invalid input (missing name, missing data, bad
+ quotes, etc.).
+ """
+ attlist = []
+ while line:
+ equals = line.find('=')
+ if equals == -1:
+ raise NameValueError('missing "="')
+ attname = line[:equals].strip()
+ if equals == 0 or not attname:
+ raise NameValueError(
+ 'missing attribute name before "="')
+ line = line[equals+1:].lstrip()
+ if not line:
+ raise NameValueError(
+ 'missing value after "%s="' % attname)
+ if line[0] in '\'"':
+ endquote = line.find(line[0], 1)
+ if endquote == -1:
+ raise NameValueError(
+ 'attribute "%s" missing end quote (%s)'
+ % (attname, line[0]))
+ if len(line) > endquote + 1 and line[endquote + 1].strip():
+ raise NameValueError(
+ 'attribute "%s" end quote (%s) not followed by '
+ 'whitespace' % (attname, line[0]))
+ data = line[1:endquote]
+ line = line[endquote+1:].lstrip()
+ else:
+ space = line.find(' ')
+ if space == -1:
+ data = line
+ line = ''
+ else:
+ data = line[:space]
+ line = line[space+1:].lstrip()
+ attlist.append((attname.lower(), data))
+ return attlist
+
+
+def new_reporter(source_path, settings):
+ """
+ Return a new Reporter object.
+
+ :Parameters:
+ `source` : string
+ The path to or description of the source text of the document.
+ `settings` : optparse.Values object
+ Runtime settings.
+ """
+ reporter = Reporter(
+ source_path, settings.report_level, settings.halt_level,
+ stream=settings.warning_stream, debug=settings.debug,
+ encoding=settings.error_encoding,
+ error_handler=settings.error_encoding_error_handler)
+ return reporter
+
+
+def new_document(source_path, settings=None):
+ """
+ Return a new empty document object.
+
+ :Parameters:
+ `source_path` : string
+ The path to or description of the source text of the document.
+ `settings` : optparse.Values object
+ Runtime settings. If none are provided, a default core set will
+ be used. If you will use the document object with any Docutils
+ components, you must provide their default settings as well.
+
+ For example, if parsing rST, at least provide the rst-parser
+ settings, obtainable as follows:
+
+ Defaults for parser component::
+
+ settings = docutils.frontend.get_default_settings(
+ docutils.parsers.rst.Parser)
+
+ Defaults and configuration file customizations::
+
+ settings = docutils.core.Publisher(
+ parser=docutils.parsers.rst.Parser).get_settings()
+
+ """
+ # Import at top of module would lead to circular dependency!
+ from docutils import frontend
+ if settings is None:
+ settings = frontend.get_default_settings()
+ source_path = decode_path(source_path)
+ reporter = new_reporter(source_path, settings)
+ document = nodes.document(settings, reporter, source=source_path)
+ document.note_source(source_path, -1)
+ return document
+
+
+def clean_rcs_keywords(paragraph, keyword_substitutions):
+ if len(paragraph) == 1 and isinstance(paragraph[0], nodes.Text):
+ textnode = paragraph[0]
+ for pattern, substitution in keyword_substitutions:
+ match = pattern.search(textnode)
+ if match:
+ paragraph[0] = nodes.Text(pattern.sub(substitution, textnode))
+ return
+
+
+def relative_path(source, target):
+ """
+ Build and return a path to `target`, relative to `source` (both files).
+
+ The return value is a `str` suitable to be included in `source`
+ as a reference to `target`.
+
+ :Parameters:
+ `source` : path-like object or None
+ Path of a file in the start directory for the relative path
+ (the file does not need to exist).
+ The value ``None`` is replaced with "<cwd>/dummy_file".
+ `target` : path-like object
+ End point of the returned relative path.
+
+ Differences to `os.path.relpath()`:
+
+ * Inverse argument order.
+ * `source` is assumed to be a FILE in the start directory (add a "dummy"
+ file name to obtain the path relative from a directory)
+ while `os.path.relpath()` expects a DIRECTORY as `start` argument.
+ * Always use Posix path separator ("/") for the output.
+ * Use `os.sep` for parsing the input
+ (changing the value of `os.sep` is ignored by `os.relpath()`).
+ * If there is no common prefix, return the absolute path to `target`.
+
+ Differences to `pathlib.PurePath.relative_to(other)`:
+
+ * pathlib offers an object oriented interface.
+ * `source` expects path to a FILE while `other` expects a DIRECTORY.
+ * `target` defaults to the cwd, no default value for `other`.
+ * `relative_path()` always returns a path (relative or absolute),
+ while `PurePath.relative_to()` raises a ValueError
+ if `target` is not a subpath of `other` (no ".." inserted).
+ """
+ source_parts = os.path.abspath(source or type(target)('dummy_file')
+ ).split(os.sep)
+ target_parts = os.path.abspath(target).split(os.sep)
+ # Check first 2 parts because '/dir'.split('/') == ['', 'dir']:
+ if source_parts[:2] != target_parts[:2]:
+ # Nothing in common between paths.
+ # Return absolute path, using '/' for URLs:
+ return '/'.join(target_parts)
+ source_parts.reverse()
+ target_parts.reverse()
+ while (source_parts and target_parts
+ and source_parts[-1] == target_parts[-1]):
+ # Remove path components in common:
+ source_parts.pop()
+ target_parts.pop()
+ target_parts.reverse()
+ parts = ['..'] * (len(source_parts) - 1) + target_parts
+ return '/'.join(parts)
+
+
+def get_stylesheet_reference(settings, relative_to=None):
+ """
+ Retrieve a stylesheet reference from the settings object.
+
+ Deprecated. Use get_stylesheet_list() instead to
+ enable specification of multiple stylesheets as a comma-separated
+ list.
+ """
+ warnings.warn('utils.get_stylesheet_reference()'
+ ' is obsoleted by utils.get_stylesheet_list()'
+ ' and will be removed in Docutils 2.0.',
+ DeprecationWarning, stacklevel=2)
+ if settings.stylesheet_path:
+ assert not settings.stylesheet, (
+ 'stylesheet and stylesheet_path are mutually exclusive.')
+ if relative_to is None:
+ relative_to = settings._destination
+ return relative_path(relative_to, settings.stylesheet_path)
+ else:
+ return settings.stylesheet
+
+
+# Return 'stylesheet' or 'stylesheet_path' arguments as list.
+#
+# The original settings arguments are kept unchanged: you can test
+# with e.g. ``if settings.stylesheet_path: ...``.
+#
+# Differences to the depracated `get_stylesheet_reference()`:
+# * return value is a list
+# * no re-writing of the path (and therefore no optional argument)
+# (if required, use ``utils.relative_path(source, target)``
+# in the calling script)
+def get_stylesheet_list(settings):
+ """
+ Retrieve list of stylesheet references from the settings object.
+ """
+ assert not (settings.stylesheet and settings.stylesheet_path), (
+ 'stylesheet and stylesheet_path are mutually exclusive.')
+ stylesheets = settings.stylesheet_path or settings.stylesheet or []
+ # programmatically set default may be string with comma separated list:
+ if not isinstance(stylesheets, list):
+ stylesheets = [path.strip() for path in stylesheets.split(',')]
+ if settings.stylesheet_path:
+ # expand relative paths if found in stylesheet-dirs:
+ stylesheets = [find_file_in_dirs(path, settings.stylesheet_dirs)
+ for path in stylesheets]
+ return stylesheets
+
+
+def find_file_in_dirs(path, dirs):
+ """
+ Search for `path` in the list of directories `dirs`.
+
+ Return the first expansion that matches an existing file.
+ """
+ path = Path(path)
+ if path.is_absolute():
+ return path.as_posix()
+ for d in dirs:
+ f = Path(d).expanduser() / path
+ if f.exists():
+ return f.as_posix()
+ return path.as_posix()
+
+
+def get_trim_footnote_ref_space(settings):
+ """
+ Return whether or not to trim footnote space.
+
+ If trim_footnote_reference_space is not None, return it.
+
+ If trim_footnote_reference_space is None, return False unless the
+ footnote reference style is 'superscript'.
+ """
+ if settings.setdefault('trim_footnote_reference_space', None) is None:
+ return getattr(settings, 'footnote_references', None) == 'superscript'
+ else:
+ return settings.trim_footnote_reference_space
+
+
+def get_source_line(node):
+ """
+ Return the "source" and "line" attributes from the `node` given or from
+ its closest ancestor.
+ """
+ while node:
+ if node.source or node.line:
+ return node.source, node.line
+ node = node.parent
+ return None, None
+
+
+def escape2null(text):
+ """Return a string with escape-backslashes converted to nulls."""
+ parts = []
+ start = 0
+ while True:
+ found = text.find('\\', start)
+ if found == -1:
+ parts.append(text[start:])
+ return ''.join(parts)
+ parts.append(text[start:found])
+ parts.append('\x00' + text[found+1:found+2])
+ start = found + 2 # skip character after escape
+
+
+def split_escaped_whitespace(text):
+ """
+ Split `text` on escaped whitespace (null+space or null+newline).
+ Return a list of strings.
+ """
+ strings = text.split('\x00 ')
+ strings = [string.split('\x00\n') for string in strings]
+ # flatten list of lists of strings to list of strings:
+ return list(itertools.chain(*strings))
+
+
+def strip_combining_chars(text):
+ return ''.join(c for c in text if not unicodedata.combining(c))
+
+
+def find_combining_chars(text):
+ """Return indices of all combining chars in Unicode string `text`.
+
+ >>> from docutils.utils import find_combining_chars
+ >>> find_combining_chars('A t̆ab̆lĕ')
+ [3, 6, 9]
+
+ """
+ return [i for i, c in enumerate(text) if unicodedata.combining(c)]
+
+
+def column_indices(text):
+ """Indices of Unicode string `text` when skipping combining characters.
+
+ >>> from docutils.utils import column_indices
+ >>> column_indices('A t̆ab̆lĕ')
+ [0, 1, 2, 4, 5, 7, 8]
+
+ """
+ # TODO: account for asian wide chars here instead of using dummy
+ # replacements in the tableparser?
+ string_indices = list(range(len(text)))
+ for index in find_combining_chars(text):
+ string_indices[index] = None
+ return [i for i in string_indices if i is not None]
+
+
+east_asian_widths = {'W': 2, # Wide
+ 'F': 2, # Full-width (wide)
+ 'Na': 1, # Narrow
+ 'H': 1, # Half-width (narrow)
+ 'N': 1, # Neutral (not East Asian, treated as narrow)
+ 'A': 1, # Ambiguous (s/b wide in East Asian context,
+ } # narrow otherwise, but that doesn't work)
+"""Mapping of result codes from `unicodedata.east_asian_widt()` to character
+column widths."""
+
+
+def column_width(text):
+ """Return the column width of text.
+
+ Correct ``len(text)`` for wide East Asian and combining Unicode chars.
+ """
+ width = sum(east_asian_widths[unicodedata.east_asian_width(c)]
+ for c in text)
+ # correction for combining chars:
+ width -= len(find_combining_chars(text))
+ return width
+
+
+def uniq(L):
+ r = []
+ for item in L:
+ if item not in r:
+ r.append(item)
+ return r
+
+
+def normalize_language_tag(tag):
+ """Return a list of normalized combinations for a `BCP 47` language tag.
+
+ Example:
+
+ >>> from docutils.utils import normalize_language_tag
+ >>> normalize_language_tag('de_AT-1901')
+ ['de-at-1901', 'de-at', 'de-1901', 'de']
+ >>> normalize_language_tag('de-CH-x_altquot')
+ ['de-ch-x-altquot', 'de-ch', 'de-x-altquot', 'de']
+
+ """
+ # normalize:
+ tag = tag.lower().replace('-', '_')
+ # split (except singletons, which mark the following tag as non-standard):
+ tag = re.sub(r'_([a-zA-Z0-9])_', r'_\1-', tag)
+ subtags = [subtag for subtag in tag.split('_')]
+ base_tag = (subtags.pop(0),)
+ # find all combinations of subtags
+ taglist = []
+ for n in range(len(subtags), 0, -1):
+ for tags in itertools.combinations(subtags, n):
+ taglist.append('-'.join(base_tag+tags))
+ taglist += base_tag
+ return taglist
+
+
+def xml_declaration(encoding=None):
+ """Return an XML text declaration.
+
+ Include an encoding declaration, if `encoding`
+ is not 'unicode', '', or None.
+ """
+ if encoding and encoding.lower() != 'unicode':
+ encoding_declaration = f' encoding="{encoding}"'
+ else:
+ encoding_declaration = ''
+ return f'<?xml version="1.0"{encoding_declaration}?>\n'
+
+
+class DependencyList:
+
+ """
+ List of dependencies, with file recording support.
+
+ Note that the output file is not automatically closed. You have
+ to explicitly call the close() method.
+ """
+
+ def __init__(self, output_file=None, dependencies=()):
+ """
+ Initialize the dependency list, automatically setting the
+ output file to `output_file` (see `set_output()`) and adding
+ all supplied dependencies.
+
+ If output_file is None, no file output is done when calling add().
+ """
+ self.list = []
+ self.file = None
+ if output_file:
+ self.set_output(output_file)
+ self.add(*dependencies)
+
+ def set_output(self, output_file):
+ """
+ Set the output file and clear the list of already added
+ dependencies.
+
+ `output_file` must be a string. The specified file is
+ immediately overwritten.
+
+ If output_file is '-', the output will be written to stdout.
+ """
+ if output_file:
+ if output_file == '-':
+ self.file = sys.stdout
+ else:
+ self.file = open(output_file, 'w', encoding='utf-8')
+
+ def add(self, *paths):
+ """
+ Append `path` to `self.list` unless it is already there.
+
+ Also append to `self.file` unless it is already there
+ or `self.file is `None`.
+ """
+ for path in paths:
+ if isinstance(path, PurePath):
+ path = path.as_posix() # use '/' as separator
+ if path not in self.list:
+ self.list.append(path)
+ if self.file is not None:
+ self.file.write(path+'\n')
+
+ def close(self):
+ """
+ Close the output file.
+ """
+ if self.file is not sys.stdout:
+ self.file.close()
+ self.file = None
+
+ def __repr__(self):
+ try:
+ output_file = self.file.name
+ except AttributeError:
+ output_file = None
+ return '%s(%r, %s)' % (self.__class__.__name__, output_file, self.list)
+
+
+release_level_abbreviations = {
+ 'alpha': 'a',
+ 'beta': 'b',
+ 'candidate': 'rc',
+ 'final': ''}
+
+
+def version_identifier(version_info=None):
+ """
+ Return a version identifier string built from `version_info`, a
+ `docutils.VersionInfo` namedtuple instance or compatible tuple. If
+ `version_info` is not provided, by default return a version identifier
+ string based on `docutils.__version_info__` (i.e. the current Docutils
+ version).
+ """
+ if version_info is None:
+ version_info = __version_info__
+ if version_info.micro:
+ micro = '.%s' % version_info.micro
+ else:
+ # 0 is omitted:
+ micro = ''
+ releaselevel = release_level_abbreviations[version_info.releaselevel]
+ if version_info.serial:
+ serial = version_info.serial
+ else:
+ # 0 is omitted:
+ serial = ''
+ if version_info.release:
+ dev = ''
+ else:
+ dev = '.dev'
+ version = '%s.%s%s%s%s%s' % (
+ version_info.major,
+ version_info.minor,
+ micro,
+ releaselevel,
+ serial,
+ dev)
+ return version
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/code_analyzer.py b/.venv/lib/python3.12/site-packages/docutils/utils/code_analyzer.py
new file mode 100644
index 00000000..cd020bb1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/code_analyzer.py
@@ -0,0 +1,136 @@
+# :Author: Georg Brandl; Lea Wiemann; Günter Milde
+# :Date: $Date: 2022-11-16 15:01:31 +0100 (Mi, 16. Nov 2022) $
+# :Copyright: This module has been placed in the public domain.
+
+"""Lexical analysis of formal languages (i.e. code) using Pygments."""
+
+from docutils import ApplicationError
+try:
+ import pygments
+ from pygments.lexers import get_lexer_by_name
+ from pygments.formatters.html import _get_ttype_class
+ with_pygments = True
+except ImportError:
+ with_pygments = False
+
+# Filter the following token types from the list of class arguments:
+unstyled_tokens = ['token', # Token (base token type)
+ 'text', # Token.Text
+ ''] # short name for Token and Text
+# (Add, e.g., Token.Punctuation with ``unstyled_tokens += 'punctuation'``.)
+
+
+class LexerError(ApplicationError):
+ pass
+
+
+class Lexer:
+ """Parse `code` lines and yield "classified" tokens.
+
+ Arguments
+
+ code -- string of source code to parse,
+ language -- formal language the code is written in,
+ tokennames -- either 'long', 'short', or 'none' (see below).
+
+ Merge subsequent tokens of the same token-type.
+
+ Iterating over an instance yields the tokens as ``(tokentype, value)``
+ tuples. The value of `tokennames` configures the naming of the tokentype:
+
+ 'long': downcased full token type name,
+ 'short': short name defined by pygments.token.STANDARD_TYPES
+ (= class argument used in pygments html output),
+ 'none': skip lexical analysis.
+ """
+
+ def __init__(self, code, language, tokennames='short'):
+ """
+ Set up a lexical analyzer for `code` in `language`.
+ """
+ self.code = code
+ self.language = language
+ self.tokennames = tokennames
+ self.lexer = None
+ # get lexical analyzer for `language`:
+ if language in ('', 'text') or tokennames == 'none':
+ return
+ if not with_pygments:
+ raise LexerError('Cannot analyze code. '
+ 'Pygments package not found.')
+ try:
+ self.lexer = get_lexer_by_name(self.language)
+ except pygments.util.ClassNotFound:
+ raise LexerError('Cannot analyze code. '
+ 'No Pygments lexer found for "%s".' % language)
+ # self.lexer.add_filter('tokenmerge')
+ # Since version 1.2. (released Jan 01, 2010) Pygments has a
+ # TokenMergeFilter. # ``self.merge(tokens)`` in __iter__ could
+ # be replaced by ``self.lexer.add_filter('tokenmerge')`` in __init__.
+ # However, `merge` below also strips a final newline added by pygments.
+ #
+ # self.lexer.add_filter('tokenmerge')
+
+ def merge(self, tokens):
+ """Merge subsequent tokens of same token-type.
+
+ Also strip the final newline (added by pygments).
+ """
+ tokens = iter(tokens)
+ (lasttype, lastval) = next(tokens)
+ for ttype, value in tokens:
+ if ttype is lasttype:
+ lastval += value
+ else:
+ yield lasttype, lastval
+ (lasttype, lastval) = (ttype, value)
+ if lastval.endswith('\n'):
+ lastval = lastval[:-1]
+ if lastval:
+ yield lasttype, lastval
+
+ def __iter__(self):
+ """Parse self.code and yield "classified" tokens.
+ """
+ if self.lexer is None:
+ yield [], self.code
+ return
+ tokens = pygments.lex(self.code, self.lexer)
+ for tokentype, value in self.merge(tokens):
+ if self.tokennames == 'long': # long CSS class args
+ classes = str(tokentype).lower().split('.')
+ else: # short CSS class args
+ classes = [_get_ttype_class(tokentype)]
+ classes = [cls for cls in classes if cls not in unstyled_tokens]
+ yield classes, value
+
+
+class NumberLines:
+ """Insert linenumber-tokens at the start of every code line.
+
+ Arguments
+
+ tokens -- iterable of ``(classes, value)`` tuples
+ startline -- first line number
+ endline -- last line number
+
+ Iterating over an instance yields the tokens with a
+ ``(['ln'], '<the line number>')`` token added for every code line.
+ Multi-line tokens are split."""
+
+ def __init__(self, tokens, startline, endline):
+ self.tokens = tokens
+ self.startline = startline
+ # pad linenumbers, e.g. endline == 100 -> fmt_str = '%3d '
+ self.fmt_str = '%%%dd ' % len(str(endline))
+
+ def __iter__(self):
+ lineno = self.startline
+ yield ['ln'], self.fmt_str % lineno
+ for ttype, value in self.tokens:
+ lines = value.split('\n')
+ for line in lines[:-1]:
+ yield ttype, line + '\n'
+ lineno += 1
+ yield ['ln'], self.fmt_str % lineno
+ yield ttype, lines[-1]
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/error_reporting.py b/.venv/lib/python3.12/site-packages/docutils/utils/error_reporting.py
new file mode 100644
index 00000000..805c95bb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/error_reporting.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+# :Id: $Id: error_reporting.py 9078 2022-06-17 11:31:40Z milde $
+# :Copyright: © 2011 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
+
+"""
+Deprecated module to handle Exceptions across Python versions.
+
+.. warning::
+ This module is deprecated with the end of support for Python 2.7
+ and will be removed in Docutils 0.21 or later.
+
+ Replacements:
+ | SafeString -> str
+ | ErrorString -> docutils.io.error_string()
+ | ErrorOutput -> docutils.io.ErrorOutput
+
+Error reporting should be safe from encoding/decoding errors.
+However, implicit conversions of strings and exceptions like
+
+>>> u'%s world: %s' % ('H\xe4llo', Exception(u'H\xe4llo'))
+
+fail in some Python versions:
+
+* In Python <= 2.6, ``unicode(<exception instance>)`` uses
+ `__str__` and fails with non-ASCII chars in`unicode` arguments.
+ (work around http://bugs.python.org/issue2517):
+
+* In Python 2, unicode(<exception instance>) fails, with non-ASCII
+ chars in arguments. (Use case: in some locales, the errstr
+ argument of IOError contains non-ASCII chars.)
+
+* In Python 2, str(<exception instance>) fails, with non-ASCII chars
+ in `unicode` arguments.
+
+The `SafeString`, `ErrorString` and `ErrorOutput` classes handle
+common exceptions.
+"""
+
+import sys
+import warnings
+
+from docutils.io import _locale_encoding as locale_encoding # noqa
+
+warnings.warn('The `docutils.utils.error_reporting` module is deprecated '
+ 'and will be removed in Docutils 0.21 or later.\n'
+ 'Details with help("docutils.utils.error_reporting").',
+ DeprecationWarning, stacklevel=2)
+
+
+if sys.version_info >= (3, 0):
+ unicode = str # noqa
+
+
+class SafeString:
+ """
+ A wrapper providing robust conversion to `str` and `unicode`.
+ """
+
+ def __init__(self, data, encoding=None, encoding_errors='backslashreplace',
+ decoding_errors='replace'):
+ self.data = data
+ self.encoding = (encoding or getattr(data, 'encoding', None)
+ or locale_encoding or 'ascii')
+ self.encoding_errors = encoding_errors
+ self.decoding_errors = decoding_errors
+
+ def __str__(self):
+ try:
+ return str(self.data)
+ except UnicodeEncodeError:
+ if isinstance(self.data, Exception):
+ args = [str(SafeString(arg, self.encoding,
+ self.encoding_errors))
+ for arg in self.data.args]
+ return ', '.join(args)
+ if isinstance(self.data, unicode):
+ if sys.version_info > (3, 0):
+ return self.data
+ else:
+ return self.data.encode(self.encoding,
+ self.encoding_errors)
+ raise
+
+ def __unicode__(self):
+ """
+ Return unicode representation of `self.data`.
+
+ Try ``unicode(self.data)``, catch `UnicodeError` and
+
+ * if `self.data` is an Exception instance, work around
+ http://bugs.python.org/issue2517 with an emulation of
+ Exception.__unicode__,
+
+ * else decode with `self.encoding` and `self.decoding_errors`.
+ """
+ try:
+ u = unicode(self.data)
+ if isinstance(self.data, EnvironmentError):
+ u = u.replace(": u'", ": '") # normalize filename quoting
+ return u
+ except UnicodeError as error: # catch ..Encode.. and ..Decode.. errors
+ if isinstance(self.data, EnvironmentError):
+ return "[Errno %s] %s: '%s'" % (
+ self.data.errno,
+ SafeString(self.data.strerror, self.encoding,
+ self.decoding_errors),
+ SafeString(self.data.filename, self.encoding,
+ self.decoding_errors))
+ if isinstance(self.data, Exception):
+ args = [unicode(SafeString(
+ arg, self.encoding,
+ decoding_errors=self.decoding_errors))
+ for arg in self.data.args]
+ return u', '.join(args)
+ if isinstance(error, UnicodeDecodeError):
+ return unicode(self.data, self.encoding, self.decoding_errors)
+ raise
+
+
+class ErrorString(SafeString):
+ """
+ Safely report exception type and message.
+ """
+ def __str__(self):
+ return '%s: %s' % (self.data.__class__.__name__,
+ super(ErrorString, self).__str__())
+
+ def __unicode__(self):
+ return u'%s: %s' % (self.data.__class__.__name__,
+ super(ErrorString, self).__unicode__())
+
+
+class ErrorOutput:
+ """
+ Wrapper class for file-like error streams with
+ failsafe de- and encoding of `str`, `bytes`, `unicode` and
+ `Exception` instances.
+ """
+
+ def __init__(self, stream=None, encoding=None,
+ encoding_errors='backslashreplace',
+ decoding_errors='replace'):
+ """
+ :Parameters:
+ - `stream`: a file-like object,
+ a string (path to a file),
+ `None` (write to `sys.stderr`, default), or
+ evaluating to `False` (write() requests are ignored).
+ - `encoding`: `stream` text encoding. Guessed if None.
+ - `encoding_errors`: how to treat encoding errors.
+ """
+ if stream is None:
+ stream = sys.stderr
+ elif not stream:
+ stream = False
+ # if `stream` is a file name, open it
+ elif isinstance(stream, str):
+ stream = open(stream, 'w')
+ elif isinstance(stream, unicode):
+ stream = open(stream.encode(sys.getfilesystemencoding()), 'w')
+
+ self.stream = stream
+ """Where warning output is sent."""
+
+ self.encoding = (encoding or getattr(stream, 'encoding', None)
+ or locale_encoding or 'ascii')
+ """The output character encoding."""
+
+ self.encoding_errors = encoding_errors
+ """Encoding error handler."""
+
+ self.decoding_errors = decoding_errors
+ """Decoding error handler."""
+
+ def write(self, data):
+ """
+ Write `data` to self.stream. Ignore, if self.stream is False.
+
+ `data` can be a `string`, `unicode`, or `Exception` instance.
+ """
+ if self.stream is False:
+ return
+ if isinstance(data, Exception):
+ data = unicode(SafeString(data, self.encoding,
+ self.encoding_errors,
+ self.decoding_errors))
+ try:
+ self.stream.write(data)
+ except UnicodeEncodeError:
+ self.stream.write(data.encode(self.encoding, self.encoding_errors))
+ except TypeError:
+ if isinstance(data, unicode): # passed stream may expect bytes
+ self.stream.write(data.encode(self.encoding,
+ self.encoding_errors))
+ return
+ if self.stream in (sys.stderr, sys.stdout):
+ self.stream.buffer.write(data) # write bytes to raw stream
+ else:
+ self.stream.write(unicode(data, self.encoding,
+ self.decoding_errors))
+
+ def close(self):
+ """
+ Close the error-output stream.
+
+ Ignored if the stream is` sys.stderr` or `sys.stdout` or has no
+ close() method.
+ """
+ if self.stream in (sys.stdout, sys.stderr):
+ return
+ try:
+ self.stream.close()
+ except AttributeError:
+ pass
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/__init__.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/__init__.py
new file mode 100644
index 00000000..2ad43b42
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/__init__.py
@@ -0,0 +1,73 @@
+# :Id: $Id: __init__.py 9516 2024-01-15 16:11:08Z milde $
+# :Author: Guenter 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
+
+"""
+This is the Docutils (Python Documentation Utilities) "math" sub-package.
+
+It contains various modules for conversion between different math formats
+(LaTeX, MathML, HTML).
+
+:math2html: LaTeX math -> HTML conversion from eLyXer
+:latex2mathml: LaTeX math -> presentational MathML
+:unichar2tex: Unicode character to LaTeX math translation table
+:tex2unichar: LaTeX math to Unicode character translation dictionaries
+:mathalphabet2unichar: LaTeX math alphabets to Unicode character translation
+:tex2mathml_extern: Wrapper for 3rd party TeX -> MathML converters
+"""
+
+# helpers for Docutils math support
+# =================================
+
+
+class MathError(ValueError):
+ """Exception for math syntax and math conversion errors.
+
+ The additional attribute `details` may hold a list of Docutils
+ nodes suitable as children for a ``<system_message>``.
+ """
+ def __init__(self, msg, details=[]):
+ super().__init__(msg)
+ self.details = details
+
+
+def toplevel_code(code):
+ """Return string (LaTeX math) `code` with environments stripped out."""
+ chunks = code.split(r'\begin{')
+ return r'\begin{'.join(chunk.split(r'\end{')[-1]
+ for chunk in chunks)
+
+
+def pick_math_environment(code, numbered=False):
+ """Return the right math environment to display `code`.
+
+ The test simply looks for line-breaks (``\\``) outside environments.
+ Multi-line formulae are set with ``align``, one-liners with
+ ``equation``.
+
+ If `numbered` evaluates to ``False``, the "starred" versions are used
+ to suppress numbering.
+ """
+ if toplevel_code(code).find(r'\\') >= 0:
+ env = 'align'
+ else:
+ env = 'equation'
+ if not numbered:
+ env += '*'
+ return env
+
+
+def wrap_math_code(code, as_block):
+ # Wrap math-code in mode-switching TeX command/environment.
+ # If `as_block` is True, use environment for displayed equation(s).
+ if as_block:
+ env = pick_math_environment(code)
+ return '\\begin{%s}\n%s\n\\end{%s}' % (env, code, env)
+ return '$%s$' % code
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/latex2mathml.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/latex2mathml.py
new file mode 100644
index 00000000..b6ca3934
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/latex2mathml.py
@@ -0,0 +1,1252 @@
+# :Id: $Id: latex2mathml.py 9536 2024-02-01 13:04:22Z milde $
+# :Copyright: © 2005 Jens Jørgen Mortensen [1]_
+# © 2010, 2021, 2024 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
+#
+# .. [1] the original `rst2mathml.py` in `sandbox/jensj/latex_math`
+
+"""Convert LaTex maths code into presentational MathML.
+
+This module is provisional:
+the API is not settled and may change with any minor Docutils version.
+"""
+
+# Usage:
+#
+# >>> from latex2mathml import *
+
+import re
+import unicodedata
+
+from docutils.utils.math import (MathError, mathalphabet2unichar,
+ tex2unichar, toplevel_code)
+from docutils.utils.math.mathml_elements import (
+ math, mtable, mrow, mtr, mtd, menclose, mphantom, msqrt, mi, mn, mo,
+ mtext, msub, msup, msubsup, munder, mover, munderover, mroot, mfrac,
+ mspace, MathRow)
+
+
+# Character data
+# --------------
+
+# LaTeX math macro to Unicode mappings.
+# Character categories.
+
+# identifiers -> <mi>
+
+letters = {'hbar': 'ℏ'} # Compatibility mapping: \hbar resembles italic ħ
+# "unicode-math" unifies \hbar and \hslash to ℏ.
+letters.update(tex2unichar.mathalpha)
+
+ordinary = tex2unichar.mathord # Miscellaneous symbols
+
+# special case: Capital Greek letters: (upright in TeX style)
+greek_capitals = {
+ 'Phi': '\u03a6', 'Xi': '\u039e', 'Sigma': '\u03a3',
+ 'Psi': '\u03a8', 'Delta': '\u0394', 'Theta': '\u0398',
+ 'Upsilon': '\u03d2', 'Pi': '\u03a0', 'Omega': '\u03a9',
+ 'Gamma': '\u0393', 'Lambda': '\u039b'}
+
+# functions -> <mi>
+functions = {
+ # functions with a space in the name
+ 'liminf': 'lim\u202finf',
+ 'limsup': 'lim\u202fsup',
+ 'injlim': 'inj\u202flim',
+ 'projlim': 'proj\u202flim',
+ # embellished function names (see handle_cmd() below)
+ 'varlimsup': 'lim',
+ 'varliminf': 'lim',
+ 'varprojlim': 'lim',
+ 'varinjlim': 'lim',
+ # custom function name
+ 'operatorname': None,
+}
+functions.update((name, name) for name in
+ ('arccos', 'arcsin', 'arctan', 'arg', 'cos',
+ 'cosh', 'cot', 'coth', 'csc', 'deg',
+ 'det', 'dim', 'exp', 'gcd', 'hom',
+ 'ker', 'lg', 'ln', 'log', 'Pr',
+ 'sec', 'sin', 'sinh', 'tan', 'tanh'))
+# Function with limits: 'lim', 'sup', 'inf', 'max', 'min':
+# use <mo> to allow "movablelimits" attribute (see below).
+
+# modulo operator/arithmetic
+modulo_functions = {
+ # cmdname: (binary, named, parentheses, padding)
+ 'bmod': (True, True, False, '0.278em'), # a mod n
+ 'pmod': (False, True, True, '0.444em'), # a (mod n)
+ 'mod': (False, True, False, '0.667em'), # a mod n
+ 'pod': (False, False, True, '0.444em'), # a (n)
+ }
+
+
+# "mathematical alphabets": map identifiers to the corresponding
+# characters from the "Mathematical Alphanumeric Symbols" block
+math_alphabets = {
+ # 'cmdname': 'mathvariant value' # package
+ 'mathbb': 'double-struck', # amssymb
+ 'mathbf': 'bold',
+ 'mathbfit': 'bold-italic', # isomath
+ 'mathcal': 'script',
+ 'mathfrak': 'fraktur', # amssymb
+ 'mathit': 'italic',
+ 'mathrm': 'normal',
+ 'mathscr': 'script', # mathrsfs et al
+ 'mathsf': 'sans-serif',
+ 'mathbfsfit': 'sans-serif-bold-italic', # unicode-math
+ 'mathsfbfit': 'sans-serif-bold-italic', # isomath
+ 'mathsfit': 'sans-serif-italic', # isomath
+ 'mathtt': 'monospace',
+ # unsupported: bold-fraktur
+ # bold-script
+ # bold-sans-serif
+}
+
+# operator, fence, or separator -> <mo>
+
+stretchables = {
+ # extensible delimiters allowed in left/right cmds
+ 'backslash': '\\',
+ 'uparrow': '\u2191', # ↑ UPWARDS ARROW
+ 'downarrow': '\u2193', # ↓ DOWNWARDS ARROW
+ 'updownarrow': '\u2195', # ↕ UP DOWN ARROW
+ 'Uparrow': '\u21d1', # ⇑ UPWARDS DOUBLE ARROW
+ 'Downarrow': '\u21d3', # ⇓ DOWNWARDS DOUBLE ARROW
+ 'Updownarrow': '\u21d5', # ⇕ UP DOWN DOUBLE ARROW
+ 'lmoustache': '\u23b0', # ⎰ … CURLY BRACKET SECTION
+ 'rmoustache': '\u23b1', # ⎱ … LEFT CURLY BRACKET SECTION
+ 'arrowvert': '\u23d0', # ⏐ VERTICAL LINE EXTENSION
+ 'bracevert': '\u23aa', # ⎪ CURLY BRACKET EXTENSION
+ 'lvert': '|', # left |
+ 'lVert': '\u2016', # left ‖
+ 'rvert': '|', # right |
+ 'rVert': '\u2016', # right ‖
+ 'Arrowvert': '\u2016', # ‖
+}
+stretchables.update(tex2unichar.mathfence)
+stretchables.update(tex2unichar.mathopen) # Braces
+stretchables.update(tex2unichar.mathclose) # Braces
+
+# >>> print(' '.join(sorted(set(stretchables.values()))))
+# [ \ ] { | } ‖ ↑ ↓ ↕ ⇑ ⇓ ⇕ ⌈ ⌉ ⌊ ⌋ ⌜ ⌝ ⌞ ⌟ ⎪ ⎰ ⎱ ⏐ ⟅ ⟆ ⟦ ⟧ ⟨ ⟩ ⟮ ⟯ ⦇ ⦈
+
+operators = {
+ # negated symbols without pre-composed Unicode character
+ 'nleqq': '\u2266\u0338', # ≦̸
+ 'ngeqq': '\u2267\u0338', # ≧̸
+ 'nleqslant': '\u2a7d\u0338', # ⩽̸
+ 'ngeqslant': '\u2a7e\u0338', # ⩾̸
+ 'ngtrless': '\u2277\u0338', # txfonts
+ 'nlessgtr': '\u2276\u0338', # txfonts
+ 'nsubseteqq': '\u2AC5\u0338', # ⫅̸
+ 'nsupseteqq': '\u2AC6\u0338', # ⫆̸
+ # compatibility definitions:
+ 'centerdot': '\u2B1D', # BLACK VERY SMALL SQUARE | mathbin
+ 'varnothing': '\u2300', # ⌀ DIAMETER SIGN | empty set
+ 'varpropto': '\u221d', # ∝ PROPORTIONAL TO | sans serif
+ 'triangle': '\u25B3', # WHITE UP-POINTING TRIANGLE | mathord
+ 'triangledown': '\u25BD', # WHITE DOWN-POINTING TRIANGLE | mathord
+ # alias commands:
+ 'dotsb': '\u22ef', # ⋯ with binary operators/relations
+ 'dotsc': '\u2026', # … with commas
+ 'dotsi': '\u22ef', # ⋯ with integrals
+ 'dotsm': '\u22ef', # ⋯ multiplication dots
+ 'dotso': '\u2026', # … other dots
+ # functions with movable limits (requires <mo>)
+ 'lim': 'lim',
+ 'sup': 'sup',
+ 'inf': 'inf',
+ 'max': 'max',
+ 'min': 'min',
+}
+operators.update(tex2unichar.mathbin) # Binary symbols
+operators.update(tex2unichar.mathrel) # Relation symbols, arrow symbols
+operators.update(tex2unichar.mathpunct) # Punctuation
+operators.update(tex2unichar.mathop) # Variable-sized symbols
+operators.update(stretchables)
+
+
+# special cases
+
+thick_operators = {
+ # style='font-weight: bold;'
+ 'thicksim': '\u223C', # ∼
+ 'thickapprox': '\u2248', # ≈
+}
+
+small_operators = {
+ # mathsize='75%'
+ 'shortmid': '\u2223', # ∣
+ 'shortparallel': '\u2225', # ∥
+ 'nshortmid': '\u2224', # ∤
+ 'nshortparallel': '\u2226', # ∦
+ 'smallfrown': '\u2322', # ⌢ FROWN
+ 'smallsmile': '\u2323', # ⌣ SMILE
+ 'smallint': '\u222b', # ∫ INTEGRAL
+}
+
+# Operators and functions with limits above/below in display formulas
+# and in index position inline (movablelimits=True)
+movablelimits = ('bigcap', 'bigcup', 'bigodot', 'bigoplus', 'bigotimes',
+ 'bigsqcup', 'biguplus', 'bigvee', 'bigwedge',
+ 'coprod', 'intop', 'ointop', 'prod', 'sum',
+ 'lim', 'max', 'min', 'sup', 'inf')
+# Depending on settings, integrals may also be in this category.
+# (e.g. if "amsmath" is loaded with option "intlimits", see
+# http://mirror.ctan.org/macros/latex/required/amsmath/amsldoc.pdf)
+# movablelimits.extend(('fint', 'iiiint', 'iiint', 'iint', 'int', 'oiint',
+# 'oint', 'ointctrclockwise', 'sqint',
+# 'varointclockwise',))
+
+# horizontal space -> <mspace>
+
+spaces = {'qquad': '2em', # two \quad
+ 'quad': '1em', # 18 mu
+ 'thickspace': '0.2778em', # 5mu = 5/18em
+ ';': '0.2778em', # 5mu thickspace
+ ' ': '0.25em', # inter word space
+ '\n': '0.25em', # inter word space
+ 'medspace': '0.2222em', # 4mu = 2/9em
+ ':': '0.2222em', # 4mu medspace
+ 'thinspace': '0.1667em', # 3mu = 1/6em
+ ',': '0.1667em', # 3mu thinspace
+ 'negthinspace': '-0.1667em', # -3mu = -1/6em
+ '!': '-0.1667em', # negthinspace
+ 'negmedspace': '-0.2222em', # -4mu = -2/9em
+ 'negthickspace': '-0.2778em', # -5mu = -5/18em
+ }
+
+# accents: -> <mo stretchy="false"> in <mover>
+accents = {
+ # TeX: spacing combining
+ 'acute': '´', # '\u0301'
+ 'bar': 'ˉ', # '\u0304'
+ 'breve': '˘', # '\u0306'
+ 'check': 'ˇ', # '\u030C'
+ 'dot': '˙', # '\u0307'
+ 'ddot': '¨', # '\u0308'
+ 'dddot': '˙˙˙', # '\u20DB' # or … ?
+ 'ddddot': '˙˙˙˙', # '\u20DC' # or ¨¨ ?
+ 'grave': '`', # '\u0300'
+ 'hat': 'ˆ', # '\u0302'
+ 'mathring': '˚', # '\u030A'
+ 'tilde': '~', # '\u0303' # tilde ~ or small tilde ˜?
+ 'vec': '→', # '\u20d7' # → too heavy, use scriptlevel="+1"
+}
+
+# limits etc. -> <mo> in <mover> or <munder>
+over = {
+ # TeX: (char, offset-correction/em)
+ 'overbrace': ('\u23DE', -0.2), # DejaVu Math -0.6
+ 'overleftarrow': ('\u2190', -0.2),
+ 'overleftrightarrow': ('\u2194', -0.2),
+ 'overline': ('_', -0.2), # \u2012 does not stretch
+ 'overrightarrow': ('\u2192', -0.2),
+ 'widehat': ('^', -0.5),
+ 'widetilde': ('~', -0.3),
+}
+under = {'underbrace': ('\u23DF', 0.1), # DejaVu Math -0.7
+ 'underleftarrow': ('\u2190', -0.2),
+ 'underleftrightarrow': ('\u2194', -0.2),
+ 'underline': ('_', -0.8),
+ 'underrightarrow': ('\u2192', -0.2),
+ }
+
+# Character translations
+# ----------------------
+# characters with preferred alternative in mathematical use
+# cf. https://www.w3.org/TR/MathML3/chapter7.html#chars.anomalous
+anomalous_chars = {'-': '\u2212', # HYPHEN-MINUS -> MINUS SIGN
+ ':': '\u2236', # COLON -> RATIO
+ '~': '\u00a0', # NO-BREAK SPACE
+ }
+
+# blackboard bold (Greek characters not working with "mathvariant" (Firefox 78)
+mathbb = {'Γ': '\u213E', # ℾ
+ 'Π': '\u213F', # ℿ
+ 'Σ': '\u2140', # ⅀
+ 'γ': '\u213D', # ℽ
+ 'π': '\u213C', # ℼ
+ }
+
+# Matrix environments
+matrices = {
+ # name: fences
+ 'matrix': ('', ''),
+ 'smallmatrix': ('', ''), # smaller, see begin_environment()!
+ 'pmatrix': ('(', ')'),
+ 'bmatrix': ('[', ']'),
+ 'Bmatrix': ('{', '}'),
+ 'vmatrix': ('|', '|'),
+ 'Vmatrix': ('\u2016', '\u2016'), # ‖
+ 'aligned': ('', ''),
+ 'cases': ('{', ''),
+}
+
+layout_styles = {
+ 'displaystyle': {'displaystyle': True, 'scriptlevel': 0},
+ 'textstyle': {'displaystyle': False, 'scriptlevel': 0},
+ 'scriptstyle': {'displaystyle': False, 'scriptlevel': 1},
+ 'scriptscriptstyle': {'displaystyle': False, 'scriptlevel': 2},
+ }
+# See also https://www.w3.org/TR/MathML3/chapter3.html#presm.scriptlevel
+
+fractions = {
+ # name: attributes
+ 'frac': {},
+ 'cfrac': {'displaystyle': True, 'scriptlevel': 0,
+ 'class': 'cfrac'}, # in LaTeX with padding
+ 'dfrac': layout_styles['displaystyle'],
+ 'tfrac': layout_styles['textstyle'],
+ 'binom': {'linethickness': 0},
+ 'dbinom': layout_styles['displaystyle'] | {'linethickness': 0},
+ 'tbinom': layout_styles['textstyle'] | {'linethickness': 0},
+}
+
+delimiter_sizes = ['', '1.2em', '1.623em', '2.047em', '2.470em']
+bigdelimiters = {'left': 0,
+ 'right': 0,
+ 'bigl': 1,
+ 'bigr': 1,
+ 'Bigl': 2,
+ 'Bigr': 2,
+ 'biggl': 3,
+ 'biggr': 3,
+ 'Biggl': 4,
+ 'Biggr': 4,
+ }
+
+
+# LaTeX to MathML translation
+# ---------------------------
+
+# auxiliary functions
+# ~~~~~~~~~~~~~~~~~~~
+
+def tex_cmdname(string):
+ """Return leading TeX command name and remainder of `string`.
+
+ >>> tex_cmdname('mymacro2') # up to first non-letter
+ ('mymacro', '2')
+ >>> tex_cmdname('name 2') # strip trailing whitespace
+ ('name', '2')
+ >>> tex_cmdname('_2') # single non-letter character
+ ('_', '2')
+
+ """
+ m = re.match(r'([a-zA-Z]+)[ \n]*(.*)', string, re.DOTALL)
+ if m is None:
+ m = re.match(r'(.?)(.*)', string, re.DOTALL)
+ return m.group(1), m.group(2)
+
+
+# Test:
+#
+# >>> tex_cmdname('name\nnext') # strip trailing whitespace, also newlines
+# ('name', 'next')
+# >>> tex_cmdname('name_2') # first non-letter terminates
+# ('name', '_2')
+# >>> tex_cmdname('name_2\nnext line') # line-break allowed
+# ('name', '_2\nnext line')
+# >>> tex_cmdname(' next') # leading whitespace is returned
+# (' ', 'next')
+# >>> tex_cmdname('1 2') # whitespace after non-letter is kept
+# ('1', ' 2')
+# >>> tex_cmdname('1\n2\t3') # whitespace after non-letter is kept
+# ('1', '\n2\t3')
+# >>> tex_cmdname('') # empty string
+# ('', '')
+
+
+def tex_number(string):
+ """Return leading number literal and remainder of `string`.
+
+ >>> tex_number('123.4')
+ ('123.4', '')
+
+ """
+ m = re.match(r'([0-9.,]*[0-9]+)(.*)', string, re.DOTALL)
+ if m is None:
+ return '', string
+ return m.group(1), m.group(2)
+
+
+# Test:
+#
+# >>> tex_number(' 23.4b') # leading whitespace -> no number
+# ('', ' 23.4b')
+# >>> tex_number('23,400/2') # comma separator included
+# ('23,400', '/2')
+# >>> tex_number('23. 4/2') # trailing separator not included
+# ('23', '. 4/2')
+# >>> tex_number('4, 2') # trailing separator not included
+# ('4', ', 2')
+# >>> tex_number('1 000.4')
+# ('1', ' 000.4')
+
+
+def tex_token(string):
+ """Return first simple TeX token and remainder of `string`.
+
+ >>> tex_token('\\command{without argument}')
+ ('\\command', '{without argument}')
+ >>> tex_token('or first character')
+ ('o', 'r first character')
+
+ """
+ m = re.match(r"""((?P<cmd>\\[a-zA-Z]+)\s* # TeX command, skip whitespace
+ |(?P<chcmd>\\.) # one-character TeX command
+ |(?P<ch>.?)) # first character (or empty)
+ (?P<remainder>.*$) # remaining part of string
+ """, string, re.VERBOSE | re.DOTALL)
+ cmd, chcmd, ch, remainder = m.group('cmd', 'chcmd', 'ch', 'remainder')
+ return cmd or chcmd or ch, remainder
+
+# Test:
+#
+# >>> tex_token('{opening bracket of group}')
+# ('{', 'opening bracket of group}')
+# >>> tex_token('\\skip whitespace after macro name')
+# ('\\skip', 'whitespace after macro name')
+# >>> tex_token('. but not after single char')
+# ('.', ' but not after single char')
+# >>> tex_token('') # empty string.
+# ('', '')
+# >>> tex_token('\{escaped bracket')
+# ('\\{', 'escaped bracket')
+
+
+def tex_group(string):
+ """Return first TeX group or token and remainder of `string`.
+
+ >>> tex_group('{first group} returned without brackets')
+ ('first group', ' returned without brackets')
+
+ """
+ split_index = 0
+ nest_level = 0 # level of {{nested} groups}
+ escape = False # the next character is escaped (\)
+
+ if not string.startswith('{'):
+ # special case: there is no group, return first token and remainder
+ return string[:1], string[1:]
+ for c in string:
+ split_index += 1
+ if escape:
+ escape = False
+ elif c == '\\':
+ escape = True
+ elif c == '{':
+ nest_level += 1
+ elif c == '}':
+ nest_level -= 1
+ if nest_level == 0:
+ break
+ else:
+ raise MathError('Group without closing bracket!')
+ return string[1:split_index-1], string[split_index:]
+
+
+# >>> tex_group('{} empty group')
+# ('', ' empty group')
+# >>> tex_group('{group with {nested} group} ')
+# ('group with {nested} group', ' ')
+# >>> tex_group('{group with {nested group}} at the end')
+# ('group with {nested group}', ' at the end')
+# >>> tex_group('{{group} {with {{complex }nesting}} constructs}')
+# ('{group} {with {{complex }nesting}} constructs', '')
+# >>> tex_group('{group with \\{escaped\\} brackets}')
+# ('group with \\{escaped\\} brackets', '')
+# >>> tex_group('{group followed by closing bracket}} from outer group')
+# ('group followed by closing bracket', '} from outer group')
+# >>> tex_group('No group? Return first character.')
+# ('N', 'o group? Return first character.')
+# >>> tex_group(' {also whitespace}')
+# (' ', '{also whitespace}')
+
+
+def tex_token_or_group(string):
+ """Return first TeX group or token and remainder of `string`.
+
+ >>> tex_token_or_group('\\command{without argument}')
+ ('\\command', '{without argument}')
+ >>> tex_token_or_group('first character')
+ ('f', 'irst character')
+ >>> tex_token_or_group(' also whitespace')
+ (' ', 'also whitespace')
+ >>> tex_token_or_group('{first group} keep rest')
+ ('first group', ' keep rest')
+
+ """
+ arg, remainder = tex_token(string)
+ if arg == '{':
+ arg, remainder = tex_group(string.lstrip())
+ return arg, remainder
+
+# >>> tex_token_or_group('\{no group but left bracket')
+# ('\\{', 'no group but left bracket')
+
+
+def tex_optarg(string):
+ """Return optional argument and remainder.
+
+ >>> tex_optarg('[optional argument] returned without brackets')
+ ('optional argument', ' returned without brackets')
+ >>> tex_optarg('{empty string, if there is no optional arg}')
+ ('', '{empty string, if there is no optional arg}')
+
+ """
+ m = re.match(r"""\s* # leading whitespace
+ \[(?P<optarg>(\\]|[^\[\]]|\\])*)\] # [group] without nested groups
+ (?P<remainder>.*$)
+ """, string, re.VERBOSE | re.DOTALL)
+ if m is None and not string.startswith('['):
+ return '', string
+ try:
+ return m.group('optarg'), m.group('remainder')
+ except AttributeError:
+ raise MathError(f'Could not extract optional argument from "{string}"!')
+
+# Test:
+# >>> tex_optarg(' [optional argument] after whitespace')
+# ('optional argument', ' after whitespace')
+# >>> tex_optarg('[missing right bracket')
+# Traceback (most recent call last):
+# ...
+# docutils.utils.math.MathError: Could not extract optional argument from "[missing right bracket"!
+# >>> tex_optarg('[group with [nested group]]')
+# Traceback (most recent call last):
+# ...
+# docutils.utils.math.MathError: Could not extract optional argument from "[group with [nested group]]"!
+
+
+def parse_latex_math(root, source):
+ """Append MathML conversion of `string` to `node` and return it.
+
+ >>> parse_latex_math(math(), r'\alpha')
+ math(mi('α'))
+ >>> parse_latex_math(mrow(), r'x_{n}')
+ mrow(msub(mi('x'), mi('n')))
+
+ """
+ # Normalize white-space:
+ string = source # not-yet handled part of source
+ node = root # the current "insertion point"
+
+ # Loop over `string` while changing it.
+ while len(string) > 0:
+ # Take off first character:
+ c, string = string[0], string[1:]
+
+ if c in ' \n':
+ continue # whitespace is ignored in LaTeX math mode
+ if c == '\\': # start of a LaTeX macro
+ cmdname, string = tex_cmdname(string)
+ node, string = handle_cmd(cmdname, node, string)
+ elif c in "_^":
+ node = handle_script_or_limit(node, c)
+ elif c == '{':
+ if isinstance(node, MathRow) and node.nchildren == 1:
+ # LaTeX takes one arg, MathML node accepts a group
+ node.nchildren = None # allow appending until closed by '}'
+ else: # wrap group in an <mrow>
+ new_node = mrow()
+ node.append(new_node)
+ node = new_node
+ elif c == '}':
+ node = node.close()
+ elif c == '&':
+ new_node = mtd()
+ node.close().append(new_node)
+ node = new_node
+ elif c.isalpha():
+ node = node.append(mi(c))
+ elif c.isdigit():
+ number, string = tex_number(string)
+ node = node.append(mn(c+number))
+ elif c in anomalous_chars:
+ # characters with a special meaning in LaTeX math mode
+ # fix spacing before "unary" minus.
+ attributes = {}
+ if c == '-' and len(node):
+ previous_node = node[-1]
+ if (previous_node.text and previous_node.text in '([='
+ or previous_node.get('class') == 'mathopen'):
+ attributes['form'] = 'prefix'
+ node = node.append(mo(anomalous_chars[c], **attributes))
+ elif c in "/()[]|":
+ node = node.append(mo(c, stretchy=False))
+ elif c in "+*=<>,.!?`';@":
+ node = node.append(mo(c))
+ else:
+ raise MathError(f'Unsupported character: "{c}"!')
+ # TODO: append as <mi>?
+ if node is None:
+ if not string:
+ return root # ignore unbalanced braces
+ raise MathError(f'No insertion point for "{string}". '
+ f'Unbalanced braces in "{source[:-len(string)]}"?')
+ if node.nchildren and len(node) < node.nchildren:
+ raise MathError('Last node missing children. Source incomplete?')
+ return root
+
+# Test:
+
+# >>> parse_latex_math(math(), '')
+# math()
+# >>> parse_latex_math(math(), ' \\sqrt{ \\alpha}')
+# math(msqrt(mi('α')))
+# >>> parse_latex_math(math(), '23.4x')
+# math(mn('23.4'), mi('x'))
+# >>> parse_latex_math(math(), '\\sqrt 2 \\ne 3')
+# math(msqrt(mn('2')), mo('≠'), mn('3'))
+# >>> parse_latex_math(math(), '\\sqrt{2 + 3} < 10')
+# math(msqrt(mn('2'), mo('+'), mn('3'), nchildren=3), mo('<'), mn('10'))
+# >>> parse_latex_math(math(), '\\sqrt[3]{2 + 3}')
+# math(mroot(mrow(mn('2'), mo('+'), mn('3'), nchildren=3), mn('3')))
+# >>> parse_latex_math(math(), '\max_x') # function takes limits
+# math(munder(mo('max', movablelimits='true'), mi('x')))
+# >>> parse_latex_math(math(), 'x^j_i') # ensure correct order: base, sub, sup
+# math(msubsup(mi('x'), mi('i'), mi('j')))
+# >>> parse_latex_math(math(), '\int^j_i') # ensure correct order
+# math(msubsup(mo('∫'), mi('i'), mi('j')))
+# >>> parse_latex_math(math(), 'x_{\\alpha}')
+# math(msub(mi('x'), mi('α')))
+# >>> parse_latex_math(math(), 'x_\\text{in}')
+# math(msub(mi('x'), mtext('in')))
+# >>> parse_latex_math(math(), '2⌘')
+# Traceback (most recent call last):
+# docutils.utils.math.MathError: Unsupported character: "⌘"!
+# >>> parse_latex_math(math(), '23}x') # doctest: +ELLIPSIS
+# Traceback (most recent call last):
+# ...
+# docutils.utils.math.MathError: ... Unbalanced braces in "23}"?
+# >>> parse_latex_math(math(), '\\frac{2}')
+# Traceback (most recent call last):
+# ...
+# docutils.utils.math.MathError: Last node missing children. Source incomplete?
+
+
+def handle_cmd(name, node, string): # noqa: C901 TODO make this less complex
+ """Process LaTeX command `name` followed by `string`.
+
+ Append result to `node`.
+ If needed, parse `string` for command argument.
+ Return new current node and remainder of `string`:
+
+ >>> handle_cmd('hbar', math(), r' \frac')
+ (math(mi('ℏ')), ' \\frac')
+ >>> handle_cmd('hspace', math(), r'{1ex} (x)')
+ (math(mspace(width='1ex')), ' (x)')
+
+ """
+
+ # Token elements
+ # ==============
+
+ # identifier -> <mi>
+
+ if name in letters:
+ new_node = mi(letters[name])
+ if name in greek_capitals:
+ # upright in "TeX style" but MathML sets them italic ("ISO style").
+ # CSS styling does not change the font style in Firefox 78.
+ # Use 'mathvariant="normal"'?
+ new_node.set('class', 'capital-greek')
+ node = node.append(new_node)
+ return node, string
+
+ if name in ordinary:
+ # <mi mathvariant="normal"> well supported by Chromium but
+ # Firefox 115.5.0 puts additional space around the symbol, e.g.
+ # <mi mathvariant="normal">∂</mi><mi>t</mi> looks like ∂ t, not ∂t
+ # return node.append(mi(ordinary[name], mathvariant='normal')), string
+ return node.append(mi(ordinary[name])), string
+
+ if name in functions:
+ # use <mi> followed by invisible function applicator character
+ # (see https://www.w3.org/TR/MathML3/chapter3.html#presm.mi)
+ if name == 'operatorname':
+ # custom function name, e.g. ``\operatorname{abs}(x)``
+ # TODO: \operatorname* -> with limits
+ arg, string = tex_token_or_group(string)
+ new_node = mi(arg, mathvariant='normal')
+ else:
+ new_node = mi(functions[name])
+ # embellished function names:
+ if name == 'varliminf': # \underline\lim
+ new_node = munder(new_node, mo('_'))
+ elif name == 'varlimsup': # \overline\lim
+ new_node = mover(new_node, mo('¯'), accent=False)
+ elif name == 'varprojlim': # \underleftarrow\lim
+ new_node = munder(new_node, mo('\u2190'))
+ elif name == 'varinjlim': # \underrightarrow\lim
+ new_node = munder(new_node, mo('\u2192'))
+
+ node = node.append(new_node)
+ # add ApplyFunction when appropriate (not \sin^2(x), say)
+ # cf. https://www.w3.org/TR/MathML3/chapter3.html#presm.mi
+ if string and string[0] not in ('^', '_'):
+ node = node.append(mo('\u2061')) # &ApplyFunction;
+ return node, string
+
+ if name in modulo_functions:
+ (binary, named, parentheses, padding) = modulo_functions[name]
+ if binary:
+ node = node.append(mo('mod', lspace=padding, rspace=padding))
+ return node, string
+ # left padding
+ if node.in_block():
+ padding = '1em'
+ node = node.append(mspace(width=padding))
+ if parentheses:
+ node = node.append(mo('(', stretchy=False))
+ if named:
+ node = node.append(mi('mod'))
+ node = node.append(mspace(width='0.333em'))
+ arg, string = tex_token_or_group(string)
+ node = parse_latex_math(node, arg)
+ if parentheses:
+ node = node.append(mo(')', stretchy=False))
+ return node, string
+
+ # font changes or mathematical alphanumeric characters
+
+ if name in ('boldsymbol', 'pmb'): # \pmb is "poor mans bold"
+ new_node = mrow(CLASS='boldsymbol')
+ node.append(new_node)
+ return new_node, string
+
+ if name in math_alphabets:
+ return handle_math_alphabet(name, node, string)
+
+ # operator, fence, or separator -> <mo>
+
+ if name == 'colon': # trailing punctuation, not binary relation
+ node = node.append(mo(':', form='postfix', lspace='0', rspace='0.28em'))
+ return node, string
+
+ if name == 'idotsint': # AMS shortcut for ∫︀···∫︀
+ node = parse_latex_math(node, r'\int\dotsi\int')
+ return node, string
+
+ if name in thick_operators:
+ node = node.append(mo(thick_operators[name], style='font-weight: bold'))
+ return node, string
+
+ if name in small_operators:
+ node = node.append(mo(small_operators[name], mathsize='75%'))
+ return node, string
+
+ if name in operators:
+ attributes = {}
+ if name in movablelimits and string and string[0] in ' _^':
+ attributes['movablelimits'] = True
+ elif name in ('lvert', 'lVert'):
+ attributes['class'] = 'mathopen'
+ node = node.append(mo(operators[name], **attributes))
+ return node, string
+
+ if name in bigdelimiters:
+ delimiter_attributes = {}
+ size = delimiter_sizes[bigdelimiters[name]]
+ delimiter, string = tex_token_or_group(string)
+ if delimiter not in '()[]/|.':
+ try:
+ delimiter = stretchables[delimiter.lstrip('\\')]
+ except KeyError:
+ raise MathError(f'Unsupported "\\{name}" delimiter '
+ f'"{delimiter}"!')
+ if size:
+ delimiter_attributes['maxsize'] = size
+ delimiter_attributes['minsize'] = size
+ delimiter_attributes['symmetric'] = True
+ if name == 'left' or name.endswith('l'):
+ row = mrow()
+ node.append(row)
+ node = row
+ if delimiter != '.': # '.' stands for "empty delimiter"
+ node.append(mo(delimiter, **delimiter_attributes))
+ if name == 'right' or name.endswith('r'):
+ node = node.close()
+ return node, string
+
+ if name == 'not':
+ # negation: LaTeX just overlays next symbol with "/".
+ arg, string = tex_token(string)
+ if arg == '{':
+ return node, '{\\not ' + string
+ if arg.startswith('\\'): # LaTeX macro
+ try:
+ arg = operators[arg[1:]]
+ except KeyError:
+ raise MathError(rf'"\not" cannot negate: "{arg}"!')
+ arg = unicodedata.normalize('NFC', arg+'\u0338')
+ node = node.append(mo(arg))
+ return node, string
+
+ # arbitrary text (usually comments) -> <mtext>
+ if name in ('text', 'mbox', 'textrm'):
+ arg, string = tex_token_or_group(string)
+ parts = arg.split('$') # extract inline math
+ for i, part in enumerate(parts):
+ if i % 2 == 0: # i is even
+ # LaTeX keeps whitespace in, e.g., ``\text{ foo }``,
+ # <mtext> displays only internal whitespace.
+ # → replace marginal whitespace with NBSP
+ part = re.sub('(^[ \n]|[ \n]$)', '\u00a0', part)
+ node = node.append(mtext(part))
+ else:
+ parse_latex_math(node, part)
+ return node, string
+
+ # horizontal space -> <mspace>
+ if name in spaces:
+ node = node.append(mspace(width='%s'%spaces[name]))
+ return node, string
+
+ if name in ('hspace', 'mspace'):
+ arg, string = tex_group(string)
+ if arg.endswith('mu'):
+ # unit "mu" (1mu=1/18em) not supported by MathML
+ arg = '%sem' % (float(arg[:-2])/18)
+ node = node.append(mspace(width='%s'%arg))
+ return node, string
+
+ if name == 'phantom':
+ new_node = mphantom()
+ node.append(new_node)
+ return new_node, string
+
+ if name == 'boxed':
+ # CSS padding is broken in Firefox 115.6.0esr
+ # therefore we still need the deprecated <menclose> element
+ new_node = menclose(notation='box', CLASS='boxed')
+ node.append(new_node)
+ return new_node, string
+
+ # Complex elements (Layout schemata)
+ # ==================================
+
+ if name == 'sqrt':
+ radix, string = tex_optarg(string)
+ if radix:
+ indexnode = mrow()
+ new_node = mroot(indexnode, switch=True)
+ parse_latex_math(indexnode, radix)
+ indexnode.close()
+ else:
+ new_node = msqrt()
+ node.append(new_node)
+ return new_node, string
+
+ if name in fractions:
+ attributes = fractions[name]
+ if name == 'cfrac':
+ optarg, string = tex_optarg(string)
+ optargs = {'l': 'left', 'r': 'right'}
+ if optarg in optargs:
+ attributes = attributes.copy()
+ attributes['numalign'] = optargs[optarg] # "numalign" is deprecated
+ attributes['class'] += ' numalign-' + optargs[optarg]
+ new_node = frac = mfrac(**attributes)
+ if name.endswith('binom'):
+ new_node = mrow(mo('('), new_node, mo(')'), CLASS='binom')
+ new_node.nchildren = 3
+ node.append(new_node)
+ return frac, string
+
+ if name == '\\': # end of a row
+ entry = mtd()
+ new_node = mtr(entry)
+ node.close().close().append(new_node)
+ return entry, string
+
+ if name in accents:
+ accent_node = mo(accents[name], stretchy=False)
+ # mi() would be simpler, but semantically wrong
+ # --- https://w3c.github.io/mathml-core/#operator-fence-separator-or-accent-mo
+ if name == 'vec':
+ accent_node.set('scriptlevel', '+1') # scale down arrow
+ new_node = mover(accent_node, accent=True, switch=True)
+ node.append(new_node)
+ return new_node, string
+
+ if name in over:
+ # set "accent" to False (otherwise dots on i and j are dropped)
+ # but to True on accent node get "textstyle" (full size) symbols on top
+ new_node = mover(mo(over[name][0], accent=True),
+ switch=True, accent=False)
+ node.append(new_node)
+ return new_node, string
+
+ if name == 'overset':
+ new_node = mover(switch=True)
+ node.append(new_node)
+ return new_node, string
+
+ if name in under:
+ new_node = munder(mo(under[name][0]), switch=True)
+ node.append(new_node)
+ return new_node, string
+
+ if name == 'underset':
+ new_node = munder(switch=True)
+ node.append(new_node)
+ return new_node, string
+
+ if name in ('xleftarrow', 'xrightarrow'):
+ subscript, string = tex_optarg(string)
+ base = mo(operators['long'+name[1:]])
+ if subscript:
+ new_node = munderover(base)
+ sub_node = parse_latex_math(mrow(), subscript)
+ if len(sub_node) == 1:
+ sub_node = sub_node[0]
+ new_node.append(sub_node)
+ else:
+ new_node = mover(base)
+ node.append(new_node)
+ return new_node, string
+
+ if name in layout_styles: # 'displaystyle', 'textstyle', ...
+ if len(node) > 0:
+ raise MathError(rf'Declaration "\{name}" must be first command '
+ 'in a group!')
+ for k, v in layout_styles[name].items():
+ node.set(k, v)
+ return node, string
+
+ if name.endswith('limits'):
+ arg, remainder = tex_token(string)
+ if arg in '_^': # else ignore
+ string = remainder
+ node = handle_script_or_limit(node, arg, limits=name)
+ return node, string
+
+ # Environments
+
+ if name == 'begin':
+ return begin_environment(node, string)
+
+ if name == 'end':
+ return end_environment(node, string)
+
+ raise MathError(rf'Unknown LaTeX command "\{name}".')
+
+# >>> handle_cmd('left', math(), '[a\\right]')
+# (mrow(mo('[')), 'a\\right]')
+# >>> handle_cmd('left', math(), '. a)') # empty \left
+# (mrow(), ' a)')
+# >>> handle_cmd('left', math(), '\\uparrow a)') # cmd
+# (mrow(mo('↑')), 'a)')
+# >>> handle_cmd('not', math(), '\\equiv \\alpha)') # cmd
+# (math(mo('≢')), '\\alpha)')
+# >>> handle_cmd('text', math(), '{ for } i>0') # group
+# (math(mtext('\xa0for\xa0')), ' i>0')
+# >>> handle_cmd('text', math(), '{B}T') # group
+# (math(mtext('B')), 'T')
+# >>> handle_cmd('text', math(), '{number of apples}}') # group
+# (math(mtext('number of apples')), '}')
+# >>> handle_cmd('text', math(), 'i \\sin(x)') # single char
+# (math(mtext('i')), ' \\sin(x)')
+# >>> handle_cmd(' ', math(), ' next') # inter word space
+# (math(mspace(width='0.25em')), ' next')
+# >>> handle_cmd('\n', math(), '\nnext') # inter word space
+# (math(mspace(width='0.25em')), '\nnext')
+# >>> handle_cmd('sin', math(), '(\\alpha)')
+# (math(mi('sin'), mo('\u2061')), '(\\alpha)')
+# >>> handle_cmd('sin', math(), ' \\alpha')
+# (math(mi('sin'), mo('\u2061')), ' \\alpha')
+# >>> handle_cmd('operatorname', math(), '{abs}(x)')
+# (math(mi('abs', mathvariant='normal'), mo('\u2061')), '(x)')
+# >>> handle_cmd('overline', math(), '{981}')
+# (mover(mo('_', accent='true'), switch=True, accent='false'), '{981}')
+# >>> handle_cmd('bar', math(), '{x}')
+# (mover(mo('ˉ', stretchy='false'), switch=True, accent='true'), '{x}')
+# >>> handle_cmd('xleftarrow', math(), r'[\alpha]{10}')
+# (munderover(mo('⟵'), mi('α')), '{10}')
+# >>> handle_cmd('xleftarrow', math(), r'[\alpha=5]{10}')
+# (munderover(mo('⟵'), mrow(mi('α'), mo('='), mn('5'))), '{10}')
+# >>> handle_cmd('left', math(), '< a)')
+# Traceback (most recent call last):
+# docutils.utils.math.MathError: Unsupported "\left" delimiter "<"!
+# >>> handle_cmd('not', math(), '{< b} c') # LaTeX ignores the braces, too.
+# (math(), '{\\not < b} c')
+
+
+def handle_math_alphabet(name, node, string):
+ attributes = {}
+ if name == 'mathscr':
+ attributes['class'] = 'mathscr'
+ arg, string = tex_token_or_group(string)
+ # Shortcut for text arg like \mathrm{out} with more than one letter:
+ if name == 'mathrm' and arg.isalpha() and len(arg) > 1:
+ node = node.append(mi(arg)) # <mi> defaults to "normal" font
+ return node, string
+ # Parse into an <mrow>
+ container = mrow(**attributes)
+ node.append(container)
+ parse_latex_math(container, arg)
+ key = name.replace('mathscr', 'mathcal').replace('mathbfsfit', 'mathsfbfit')
+ a2ch = getattr(mathalphabet2unichar, key, {})
+ for subnode in container.iter():
+ if isinstance(subnode, mn):
+ # a number may consist of more than one digit
+ subnode.text = ''.join(a2ch.get(ch, ch) for ch in subnode.text)
+ elif isinstance(subnode, mi):
+ # don't convert multi-letter identifiers (functions)
+ subnode.text = a2ch.get(subnode.text, subnode.text)
+ if name == 'mathrm' and subnode.text.isalpha():
+ subnode.set('mathvariant', 'normal')
+ return container.close(), string
+
+# >>> handle_math_alphabet('mathrm', math(), '\\alpha')
+# (math(mi('α', mathvariant='normal')), '')
+# >>> handle_math_alphabet('mathbb', math(), '{R} = 3')
+# (math(mi('ℝ')), ' = 3')
+# >>> handle_math_alphabet('mathcal', math(), '{F = 3}')
+# (math(mrow(mi('ℱ'), mo('='), mn('3'), nchildren=3)), '')
+# >>> handle_math_alphabet('mathrm', math(), '{out} = 3') # drop <mrow>
+# (math(mi('out')), ' = 3')
+#
+# Single letters in \mathrm require "mathvariant='normal'":
+# >>> handle_math_alphabet('mathrm', math(), '{V = 3}') # doctest: +ELLIPSIS
+# (math(mrow(mi('V', mathvariant='normal'), mo('='), mn('3'), ...)), '')
+
+
+def handle_script_or_limit(node, c, limits=''):
+ """Append script or limit element to `node`."""
+ child = node.pop()
+ if limits == 'limits':
+ child.set('movablelimits', 'false')
+ elif (limits == 'movablelimits'
+ or getattr(child, 'text', '') in movablelimits):
+ child.set('movablelimits', 'true')
+
+ if c == '_':
+ if isinstance(child, mover):
+ new_node = munderover(*child, switch=True)
+ elif isinstance(child, msup):
+ new_node = msubsup(*child, switch=True)
+ elif (limits in ('limits', 'movablelimits')
+ or limits == '' and child.get('movablelimits', None)):
+ new_node = munder(child)
+ else:
+ new_node = msub(child)
+ elif c == '^':
+ if isinstance(child, munder):
+ new_node = munderover(*child)
+ elif isinstance(child, msub):
+ new_node = msubsup(*child)
+ elif (limits in ('limits', 'movablelimits')
+ or limits == '' and child.get('movablelimits', None)):
+ new_node = mover(child)
+ else:
+ new_node = msup(child)
+ node.append(new_node)
+ return new_node
+
+
+def begin_environment(node, string):
+ name, string = tex_group(string)
+ if name in matrices:
+ left_delimiter = matrices[name][0]
+ attributes = {}
+ if left_delimiter:
+ wrapper = mrow(mo(left_delimiter))
+ if name == 'cases':
+ wrapper = mrow(mo(left_delimiter, rspace='0.17em'))
+ attributes['columnalign'] = 'left'
+ attributes['class'] = 'cases'
+ node.append(wrapper)
+ node = wrapper
+ elif name == 'smallmatrix':
+ attributes['rowspacing'] = '0.02em'
+ attributes['columnspacing'] = '0.333em'
+ attributes['scriptlevel'] = '1'
+ elif name == 'aligned':
+ attributes['class'] = 'ams-align'
+ # TODO: array, aligned & alignedat take an optional [t], [b], or [c].
+ entry = mtd()
+ node.append(mtable(mtr(entry), **attributes))
+ node = entry
+ else:
+ raise MathError(f'Environment "{name}" not supported!')
+ return node, string
+
+
+def end_environment(node, string):
+ name, string = tex_group(string)
+ if name in matrices:
+ node = node.close().close().close() # close: mtd, mdr, mtable
+ right_delimiter = matrices[name][1]
+ if right_delimiter:
+ node = node.append(mo(right_delimiter))
+ node = node.close()
+ elif name == 'cases':
+ node = node.close()
+ else:
+ raise MathError(f'Environment "{name}" not supported!')
+ return node, string
+
+
+# Return the number of "equation_columns" in `code_lines`. cf. "alignat"
+# in http://mirror.ctan.org/macros/latex/required/amsmath/amsldoc.pdf
+def tex_equation_columns(rows):
+ tabs = max(row.count('&') - row.count(r'\&') for row in rows)
+ if tabs == 0:
+ return 0
+ return int(tabs/2 + 1)
+
+# >>> tex_equation_columns(['a = b'])
+# 0
+# >>> tex_equation_columns(['a &= b'])
+# 1
+# >>> tex_equation_columns(['a &= b & a \in S'])
+# 2
+# >>> tex_equation_columns(['a &= b & c &= d'])
+# 2
+
+
+# Return dictionary with attributes to style an <mtable> as align environment:
+# Not used with HTML. Replaced by CSS rule for "mtable.ams-align" in
+# "minimal.css" as "columnalign" is disregarded by Chromium and webkit.
+def align_attributes(rows):
+ atts = {'class': 'ams-align',
+ 'displaystyle': True}
+ # get maximal number of non-escaped "next column" markup characters:
+ tabs = max(row.count('&') - row.count(r'\&') for row in rows)
+ if tabs:
+ aligns = ['right', 'left'] * tabs
+ spacing = ['0', '2em'] * tabs
+ atts['columnalign'] = ' '.join(aligns[:tabs+1])
+ atts['columnspacing'] = ' '.join(spacing[:tabs])
+ return atts
+
+# >>> align_attributes(['a = b'])
+# {'class': 'ams-align', 'displaystyle': True}
+# >>> align_attributes(['a &= b'])
+# {'class': 'ams-align', 'displaystyle': True, 'columnalign': 'right left', 'columnspacing': '0'}
+# >>> align_attributes(['a &= b & a \in S'])
+# {'class': 'ams-align', 'displaystyle': True, 'columnalign': 'right left right', 'columnspacing': '0 2em'}
+# >>> align_attributes(['a &= b & c &= d'])
+# {'class': 'ams-align', 'displaystyle': True, 'columnalign': 'right left right left', 'columnspacing': '0 2em 0'}
+# >>> align_attributes([r'a &= b & c &= d \& e'])
+# {'class': 'ams-align', 'displaystyle': True, 'columnalign': 'right left right left', 'columnspacing': '0 2em 0'}
+# >>> align_attributes([r'a &= b & c &= d & e'])
+# {'class': 'ams-align', 'displaystyle': True, 'columnalign': 'right left right left right', 'columnspacing': '0 2em 0 2em'}
+
+
+def tex2mathml(tex_math, as_block=False):
+ """Return string with MathML code corresponding to `tex_math`.
+
+ Set `as_block` to ``True`` for displayed formulas.
+ """
+ # Set up tree
+ math_tree = math(xmlns='http://www.w3.org/1998/Math/MathML')
+ node = math_tree
+ if as_block:
+ math_tree.set('display', 'block')
+ rows = toplevel_code(tex_math).split(r'\\')
+ if len(rows) > 1:
+ # emulate "align*" environment with a math table
+ node = mtd()
+ math_tree.append(mtable(mtr(node), CLASS='ams-align',
+ displaystyle=True))
+ parse_latex_math(node, tex_math)
+ math_tree.indent_xml()
+ return math_tree.toxml()
+
+# >>> print(tex2mathml('3'))
+# <math xmlns="http://www.w3.org/1998/Math/MathML">
+# <mn>3</mn>
+# </math>
+# >>> print(tex2mathml('3', as_block=True))
+# <math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
+# <mn>3</mn>
+# </math>
+# >>> print(tex2mathml(r'a & b \\ c & d', as_block=True))
+# <math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
+# <mtable class="ams-align" displaystyle="true">
+# <mtr>
+# <mtd>
+# <mi>a</mi>
+# </mtd>
+# <mtd>
+# <mi>b</mi>
+# </mtd>
+# </mtr>
+# <mtr>
+# <mtd>
+# <mi>c</mi>
+# </mtd>
+# <mtd>
+# <mi>d</mi>
+# </mtd>
+# </mtr>
+# </mtable>
+# </math>
+# >>> print(tex2mathml(r'a \\ b', as_block=True))
+# <math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
+# <mtable class="ams-align" displaystyle="true">
+# <mtr>
+# <mtd>
+# <mi>a</mi>
+# </mtd>
+# </mtr>
+# <mtr>
+# <mtd>
+# <mi>b</mi>
+# </mtd>
+# </mtr>
+# </mtable>
+# </math>
+
+
+# TODO: look up more symbols from tr25, e.g.
+#
+#
+# Table 2.8 Using Vertical Line or Solidus Overlay
+# some of the negated forms of mathematical relations that can only be
+# encoded by using either U+0338 COMBINING LONG SOLIDUS OVERLAY or U+20D2
+# COMBINING LONG VERTICAL LINE OVERLAY . (For issues with using 0338 in
+# MathML, see Section 3.2.7, Combining Marks.
+#
+# Table 2.9 Variants of Mathematical Symbols using VS1?
+#
+# Sequence Description
+# 0030 + VS1 DIGIT ZERO - short diagonal stroke form
+# 2205 + VS1 EMPTY SET - zero with long diagonal stroke overlay form
+# 2229 + VS1 INTERSECTION - with serifs
+# 222A + VS1 UNION - with serifs
+# 2268 + VS1 LESS-THAN BUT NOT EQUAL TO - with vertical stroke
+# 2269 + VS1 GREATER-THAN BUT NOT EQUAL TO - with vertical stroke
+# 2272 + VS1 LESS-THAN OR EQUIVALENT TO - following the slant of the lower leg
+# 2273 + VS1 GREATER-THAN OR EQUIVALENT TO - following the slant of the lower leg
+# 228A + VS1 SUBSET OF WITH NOT EQUAL TO - variant with stroke through bottom members
+# 228B + VS1 SUPERSET OF WITH NOT EQUAL TO - variant with stroke through bottom members
+# 2293 + VS1 SQUARE CAP - with serifs
+# 2294 + VS1 SQUARE CUP - with serifs
+# 2295 + VS1 CIRCLED PLUS - with white rim
+# 2297 + VS1 CIRCLED TIMES - with white rim
+# 229C + VS1 CIRCLED EQUALS - equal sign inside and touching the circle
+# 22DA + VS1 LESS-THAN slanted EQUAL TO OR GREATER-THAN
+# 22DB + VS1 GREATER-THAN slanted EQUAL TO OR LESS-THAN
+# 2A3C + VS1 INTERIOR PRODUCT - tall variant with narrow foot
+# 2A3D + VS1 RIGHTHAND INTERIOR PRODUCT - tall variant with narrow foot
+# 2A9D + VS1 SIMILAR OR LESS-THAN - following the slant of the upper leg
+# 2A9E + VS1 SIMILAR OR GREATER-THAN - following the slant of the upper leg
+# 2AAC + VS1 SMALLER THAN OR slanted EQUAL
+# 2AAD + VS1 LARGER THAN OR slanted EQUAL
+# 2ACB + VS1 SUBSET OF ABOVE NOT EQUAL TO - variant with stroke through bottom members
+# 2ACC + VS1 SUPERSET OF ABOVE NOT EQUAL TO - variant with stroke through bottom members
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/math2html.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/math2html.py
new file mode 100755
index 00000000..dc94cff7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/math2html.py
@@ -0,0 +1,3165 @@
+#! /usr/bin/env python3
+# math2html: convert LaTeX equations to HTML output.
+#
+# Copyright (C) 2009-2011 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: https://opensource.org/licenses/BSD-2-Clause
+
+# Based on eLyXer: convert LyX source files to HTML output.
+# http://alexfernandez.github.io/elyxer/
+
+# Versions:
+# 1.2.5 2015-02-26 eLyXer standalone formula conversion to HTML.
+# 1.3 2021-06-02 Removed code for conversion of LyX files not
+# required for LaTeX math.
+# Support for more math commands from the AMS "math-guide".
+# 2.0 2021-12-31 Drop 2.7 compatibility code.
+
+import pathlib
+import sys
+import unicodedata
+
+from docutils.utils.math import tex2unichar
+
+
+__version__ = '1.3 (2021-06-02)'
+
+
+class Trace:
+ "A tracing class"
+
+ debugmode = False
+ quietmode = False
+ showlinesmode = False
+
+ prefix = None
+
+ def debug(cls, message):
+ "Show a debug message"
+ if not Trace.debugmode or Trace.quietmode:
+ return
+ Trace.show(message, sys.stdout)
+
+ def message(cls, message):
+ "Show a trace message"
+ if Trace.quietmode:
+ return
+ if Trace.prefix and Trace.showlinesmode:
+ message = Trace.prefix + message
+ Trace.show(message, sys.stdout)
+
+ def error(cls, message):
+ "Show an error message"
+ message = '* ' + message
+ if Trace.prefix and Trace.showlinesmode:
+ message = Trace.prefix + message
+ Trace.show(message, sys.stderr)
+
+ def show(cls, message, channel):
+ "Show a message out of a channel"
+ channel.write(message + '\n')
+
+ debug = classmethod(debug)
+ message = classmethod(message)
+ error = classmethod(error)
+ show = classmethod(show)
+
+
+class ContainerConfig:
+ "Configuration class from elyxer.config file"
+
+ extracttext = {
+ 'allowed': ['FormulaConstant'],
+ 'extracted': ['AlphaCommand',
+ 'Bracket',
+ 'BracketCommand',
+ 'CombiningFunction',
+ 'EmptyCommand',
+ 'FontFunction',
+ 'Formula',
+ 'FormulaNumber',
+ 'FormulaSymbol',
+ 'OneParamFunction',
+ 'OversetFunction',
+ 'RawText',
+ 'SpacedCommand',
+ 'SymbolFunction',
+ 'TextFunction',
+ 'UndersetFunction',
+ ],
+ }
+
+
+class EscapeConfig:
+ "Configuration class from elyxer.config file"
+
+ chars = {
+ '\n': '',
+ "'": '’',
+ '`': '‘',
+ }
+
+ entities = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ }
+
+
+class FormulaConfig:
+ "Configuration class from elyxer.config file"
+
+ alphacommands = {
+ '\\AmS': '<span class="textsc">AmS</span>',
+ '\\AA': 'Å',
+ '\\AE': 'Æ',
+ '\\DH': 'Ð',
+ '\\L': 'Ł',
+ '\\O': 'Ø',
+ '\\OE': 'Œ',
+ '\\TH': 'Þ',
+ '\\aa': 'å',
+ '\\ae': 'æ',
+ '\\dh': 'ð',
+ '\\i': 'ı',
+ '\\j': 'ȷ',
+ '\\l': 'ł',
+ '\\o': 'ø',
+ '\\oe': 'œ',
+ '\\ss': 'ß',
+ '\\th': 'þ',
+ '\\hbar': 'ħ', # cf. \hslash: ℏ in tex2unichar
+ }
+ for key, value in tex2unichar.mathalpha.items():
+ alphacommands['\\'+key] = value
+
+ array = {
+ 'begin': r'\begin',
+ 'cellseparator': '&',
+ 'end': r'\end',
+ 'rowseparator': r'\\',
+ }
+
+ bigbrackets = {'(': ['⎛', '⎜', '⎝'],
+ ')': ['⎞', '⎟', '⎠'],
+ '[': ['⎡', '⎢', '⎣'],
+ ']': ['⎤', '⎥', '⎦'],
+ '{': ['⎧', '⎪', '⎨', '⎩'],
+ '}': ['⎫', '⎪', '⎬', '⎭'],
+ # TODO: 2-row brackets with ⎰⎱ (\lmoustache \rmoustache)
+ '|': ['|'], # 007C VERTICAL LINE
+ # '|': ['⎮'], # 23AE INTEGRAL EXTENSION
+ # '|': ['⎪'], # 23AA CURLY BRACKET EXTENSION
+ '‖': ['‖'], # 2016 DOUBLE VERTICAL LINE
+ # '∥': ['∥'], # 2225 PARALLEL TO
+ }
+
+ bracketcommands = {
+ '\\left': 'span class="stretchy"',
+ '\\left.': '<span class="leftdot"></span>',
+ '\\middle': 'span class="stretchy"',
+ '\\right': 'span class="stretchy"',
+ '\\right.': '<span class="rightdot"></span>',
+ }
+
+ combiningfunctions = {
+ "\\'": '\u0301', # x́
+ '\\"': '\u0308', # ẍ
+ '\\^': '\u0302', # x̂
+ '\\`': '\u0300', # x̀
+ '\\~': '\u0303', # x̃
+ '\\c': '\u0327', # x̧
+ '\\r': '\u030a', # x̊
+ '\\s': '\u0329', # x̩
+ '\\textcircled': '\u20dd', # x⃝
+ '\\textsubring': '\u0325', # x̥
+ '\\v': '\u030c', # x̌
+ }
+ for key, value in tex2unichar.mathaccent.items():
+ combiningfunctions['\\'+key] = value
+
+ commands = {
+ '\\\\': '<br/>',
+ '\\\n': ' ', # escaped whitespace
+ '\\\t': ' ', # escaped whitespace
+ '\\centerdot': '\u2B1D', # BLACK VERY SMALL SQUARE, mathbin
+ '\\colon': ': ',
+ '\\copyright': '©',
+ '\\dotminus': '∸',
+ '\\dots': '…',
+ '\\dotsb': '⋯',
+ '\\dotsc': '…',
+ '\\dotsi': '⋯',
+ '\\dotsm': '⋯',
+ '\\dotso': '…',
+ '\\euro': '€',
+ '\\guillemotleft': '«',
+ '\\guillemotright': '»',
+ '\\lVert': '‖',
+ '\\Arrowvert': '‖',
+ '\\lvert': '|',
+ '\\newline': '<br/>',
+ '\\nobreakspace': ' ',
+ '\\nolimits': '',
+ '\\nonumber': '',
+ '\\qquad': '  ',
+ '\\rVert': '‖',
+ '\\rvert': '|',
+ '\\textasciicircum': '^',
+ '\\textasciitilde': '~',
+ '\\textbackslash': '\\',
+ '\\textcopyright': '©',
+ '\\textdegree': '°',
+ '\\textellipsis': '…',
+ '\\textemdash': '—',
+ '\\textendash': '—',
+ '\\texteuro': '€',
+ '\\textgreater': '>',
+ '\\textless': '<',
+ '\\textordfeminine': 'ª',
+ '\\textordmasculine': 'º',
+ '\\textquotedblleft': '“',
+ '\\textquotedblright': '”',
+ '\\textquoteright': '’',
+ '\\textregistered': '®',
+ '\\textrightarrow': '→',
+ '\\textsection': '§',
+ '\\texttrademark': '™',
+ '\\texttwosuperior': '²',
+ '\\textvisiblespace': ' ',
+ '\\thickspace': '<span class="thickspace"> </span>', # 5/13 em
+ '\\;': '<span class="thickspace"> </span>', # 5/13 em
+ '\\triangle': '\u25B3', # WHITE UP-POINTING TRIANGLE, mathord
+ '\\triangledown': '\u25BD', # WHITE DOWN-POINTING TRIANGLE, mathord
+ '\\varnothing': '\u2300', # ⌀ DIAMETER SIGN
+ # functions
+ '\\Pr': 'Pr',
+ '\\arccos': 'arccos',
+ '\\arcsin': 'arcsin',
+ '\\arctan': 'arctan',
+ '\\arg': 'arg',
+ '\\cos': 'cos',
+ '\\cosh': 'cosh',
+ '\\cot': 'cot',
+ '\\coth': 'coth',
+ '\\csc': 'csc',
+ '\\deg': 'deg',
+ '\\det': 'det',
+ '\\dim': 'dim',
+ '\\exp': 'exp',
+ '\\gcd': 'gcd',
+ '\\hom': 'hom',
+ '\\injlim': 'inj lim',
+ '\\ker': 'ker',
+ '\\lg': 'lg',
+ '\\liminf': 'lim inf',
+ '\\limsup': 'lim sup',
+ '\\ln': 'ln',
+ '\\log': 'log',
+ '\\projlim': 'proj lim',
+ '\\sec': 'sec',
+ '\\sin': 'sin',
+ '\\sinh': 'sinh',
+ '\\tan': 'tan',
+ '\\tanh': 'tanh',
+ }
+ cmddict = {}
+ cmddict.update(tex2unichar.mathbin) # TODO: spacing around binary operators
+ cmddict.update(tex2unichar.mathopen)
+ cmddict.update(tex2unichar.mathclose)
+ cmddict.update(tex2unichar.mathfence)
+ cmddict.update(tex2unichar.mathord)
+ cmddict.update(tex2unichar.mathpunct)
+ cmddict.update(tex2unichar.space)
+ commands.update(('\\' + key, value) for key, value in cmddict.items())
+
+ oversetfunctions = {
+ # math accents (cf. combiningfunctions)
+ # '\\acute': '´',
+ '\\bar': '‒', # FIGURE DASH
+ # '\\breve': '˘',
+ # '\\check': 'ˇ',
+ '\\dddot': '<span class="smallsymbol">⋯</span>',
+ # '\\ddot': '··', # ¨ too high
+ # '\\dot': '·',
+ # '\\grave': '`',
+ # '\\hat': '^',
+ # '\\mathring': '˚',
+ # '\\tilde': '~',
+ '\\vec': '<span class="smallsymbol">→</span>',
+ # embellishments
+ '\\overleftarrow': '⟵',
+ '\\overleftrightarrow': '⟷',
+ '\\overrightarrow': '⟶',
+ '\\widehat': '^',
+ '\\widetilde': '~',
+ }
+
+ undersetfunctions = {
+ '\\underleftarrow': '⟵',
+ '\\underleftrightarrow': '⟷',
+ '\\underrightarrow': '⟶',
+ }
+
+ endings = {
+ 'bracket': '}',
+ 'complex': '\\]',
+ 'endafter': '}',
+ 'endbefore': '\\end{',
+ 'squarebracket': ']',
+ }
+
+ environments = {
+ 'align': ['r', 'l'],
+ 'eqnarray': ['r', 'c', 'l'],
+ 'gathered': ['l', 'l'],
+ 'smallmatrix': ['c', 'c'],
+ }
+
+ fontfunctions = {
+ '\\boldsymbol': 'b', '\\mathbb': 'span class="blackboard"',
+ '\\mathbb{A}': '𝔸', '\\mathbb{B}': '𝔹', '\\mathbb{C}': 'ℂ',
+ '\\mathbb{D}': '𝔻', '\\mathbb{E}': '𝔼', '\\mathbb{F}': '𝔽',
+ '\\mathbb{G}': '𝔾', '\\mathbb{H}': 'ℍ', '\\mathbb{J}': '𝕁',
+ '\\mathbb{K}': '𝕂', '\\mathbb{L}': '𝕃', '\\mathbb{N}': 'ℕ',
+ '\\mathbb{O}': '𝕆', '\\mathbb{P}': 'ℙ', '\\mathbb{Q}': 'ℚ',
+ '\\mathbb{R}': 'ℝ', '\\mathbb{S}': '𝕊', '\\mathbb{T}': '𝕋',
+ '\\mathbb{W}': '𝕎', '\\mathbb{Z}': 'ℤ', '\\mathbf': 'b',
+ '\\mathcal': 'span class="scriptfont"',
+ '\\mathcal{B}': 'ℬ', '\\mathcal{E}': 'ℰ', '\\mathcal{F}':
+ 'ℱ', '\\mathcal{H}': 'ℋ', '\\mathcal{I}': 'ℐ',
+ '\\mathcal{L}': 'ℒ', '\\mathcal{M}': 'ℳ', '\\mathcal{R}': 'ℛ',
+ '\\mathfrak': 'span class="fraktur"',
+ '\\mathfrak{C}': 'ℭ', '\\mathfrak{F}': '𝔉', '\\mathfrak{H}': 'ℌ',
+ '\\mathfrak{I}': 'ℑ', '\\mathfrak{R}': 'ℜ', '\\mathfrak{Z}': 'ℨ',
+ '\\mathit': 'i',
+ '\\mathring{A}': 'Å', '\\mathring{U}': 'Ů',
+ '\\mathring{a}': 'å', '\\mathring{u}': 'ů', '\\mathring{w}': 'ẘ',
+ '\\mathring{y}': 'ẙ',
+ '\\mathrm': 'span class="mathrm"',
+ '\\mathscr': 'span class="mathscr"',
+ '\\mathscr{B}': 'ℬ', '\\mathscr{E}': 'ℰ', '\\mathscr{F}': 'ℱ',
+ '\\mathscr{H}': 'ℋ', '\\mathscr{I}': 'ℐ', '\\mathscr{L}': 'ℒ',
+ '\\mathscr{M}': 'ℳ', '\\mathscr{R}': 'ℛ',
+ '\\mathsf': 'span class="mathsf"',
+ '\\mathtt': 'span class="mathtt"',
+ '\\operatorname': 'span class="mathrm"',
+ }
+
+ hybridfunctions = {
+ '\\addcontentsline': ['{$p!}{$q!}{$r!}', 'f0{}', 'ignored'],
+ '\\addtocontents': ['{$p!}{$q!}', 'f0{}', 'ignored'],
+ '\\backmatter': ['', 'f0{}', 'ignored'],
+ '\\binom': ['{$1}{$2}', 'f2{(}f0{f1{$1}f1{$2}}f2{)}', 'span class="binom"', 'span class="binomstack"', 'span class="bigdelimiter size2"'],
+ '\\boxed': ['{$1}', 'f0{$1}', 'span class="boxed"'],
+ '\\cfrac': ['[$p!]{$1}{$2}', 'f0{f3{(}f1{$1}f3{)/(}f2{$2}f3{)}}', 'span class="fullfraction"', 'span class="numerator align-$p"', 'span class="denominator"', 'span class="ignored"'],
+ '\\color': ['{$p!}{$1}', 'f0{$1}', 'span style="color: $p;"'],
+ '\\colorbox': ['{$p!}{$1}', 'f0{$1}', 'span class="colorbox" style="background: $p;"'],
+ '\\dbinom': ['{$1}{$2}', '(f0{f1{f2{$1}}f1{f2{ }}f1{f2{$2}}})', 'span class="binomial"', 'span class="binomrow"', 'span class="binomcell"'],
+ '\\dfrac': ['{$1}{$2}', 'f0{f3{(}f1{$1}f3{)/(}f2{$2}f3{)}}', 'span class="fullfraction"', 'span class="numerator"', 'span class="denominator"', 'span class="ignored"'],
+ '\\displaystyle': ['{$1}', 'f0{$1}', 'span class="displaystyle"'],
+ '\\fancyfoot': ['[$p!]{$q!}', 'f0{}', 'ignored'],
+ '\\fancyhead': ['[$p!]{$q!}', 'f0{}', 'ignored'],
+ '\\fbox': ['{$1}', 'f0{$1}', 'span class="fbox"'],
+ '\\fboxrule': ['{$p!}', 'f0{}', 'ignored'],
+ '\\fboxsep': ['{$p!}', 'f0{}', 'ignored'],
+ '\\fcolorbox': ['{$p!}{$q!}{$1}', 'f0{$1}', 'span class="boxed" style="border-color: $p; background: $q;"'],
+ '\\frac': ['{$1}{$2}', 'f0{f3{(}f1{$1}f3{)/(}f2{$2}f3{)}}', 'span class="fraction"', 'span class="numerator"', 'span class="denominator"', 'span class="ignored"'],
+ '\\framebox': ['[$p!][$q!]{$1}', 'f0{$1}', 'span class="framebox align-$q" style="width: $p;"'],
+ '\\frontmatter': ['', 'f0{}', 'ignored'],
+ '\\href': ['[$o]{$u!}{$t!}', 'f0{$t}', 'a href="$u"'],
+ '\\hspace': ['{$p!}', 'f0{ }', 'span class="hspace" style="width: $p;"'],
+ '\\leftroot': ['{$p!}', 'f0{ }', 'span class="leftroot" style="width: $p;px"'],
+ # TODO: convert 1 mu to 1/18 em
+ # '\\mspace': ['{$p!}', 'f0{ }', 'span class="hspace" style="width: $p;"'],
+ '\\nicefrac': ['{$1}{$2}', 'f0{f1{$1}⁄f2{$2}}', 'span class="fraction"', 'sup class="numerator"', 'sub class="denominator"', 'span class="ignored"'],
+ '\\parbox': ['[$p!]{$w!}{$1}', 'f0{1}', 'div class="Boxed" style="width: $w;"'],
+ '\\raisebox': ['{$p!}{$1}', 'f0{$1.font}', 'span class="raisebox" style="vertical-align: $p;"'],
+ '\\renewenvironment': ['{$1!}{$2!}{$3!}', ''],
+ '\\rule': ['[$v!]{$w!}{$h!}', 'f0/', 'hr class="line" style="width: $w; height: $h;"'],
+ '\\scriptscriptstyle': ['{$1}', 'f0{$1}', 'span class="scriptscriptstyle"'],
+ '\\scriptstyle': ['{$1}', 'f0{$1}', 'span class="scriptstyle"'],
+ # TODO: increase √-size with argument (\frac in display mode, ...)
+ '\\sqrt': ['[$0]{$1}', 'f0{f1{$0}f2{√}f4{(}f3{$1}f4{)}}', 'span class="sqrt"', 'sup class="root"', 'span class="radical"', 'span class="root"', 'span class="ignored"'],
+ '\\stackrel': ['{$1}{$2}', 'f0{f1{$1}f2{$2}}', 'span class="stackrel"', 'span class="upstackrel"', 'span class="downstackrel"'],
+ '\\tbinom': ['{$1}{$2}', '(f0{f1{f2{$1}}f1{f2{ }}f1{f2{$2}}})', 'span class="binomial"', 'span class="binomrow"', 'span class="binomcell"'],
+ '\\tfrac': ['{$1}{$2}', 'f0{f3{(}f1{$1}f3{)/(}f2{$2}f3{)}}', 'span class="textfraction"', 'span class="numerator"', 'span class="denominator"', 'span class="ignored"'],
+ '\\textcolor': ['{$p!}{$1}', 'f0{$1}', 'span style="color: $p;"'],
+ '\\textstyle': ['{$1}', 'f0{$1}', 'span class="textstyle"'],
+ '\\thispagestyle': ['{$p!}', 'f0{}', 'ignored'],
+ '\\unit': ['[$0]{$1}', '$0f0{$1.font}', 'span class="unit"'],
+ '\\unitfrac': ['[$0]{$1}{$2}', '$0f0{f1{$1.font}⁄f2{$2.font}}', 'span class="fraction"', 'sup class="unit"', 'sub class="unit"'],
+ '\\uproot': ['{$p!}', 'f0{ }', 'span class="uproot" style="width: $p;px"'],
+ '\\url': ['{$u!}', 'f0{$u}', 'a href="$u"'],
+ '\\vspace': ['{$p!}', 'f0{ }', 'span class="vspace" style="height: $p;"'],
+ }
+
+ hybridsizes = {
+ '\\binom': '$1+$2', '\\cfrac': '$1+$2', '\\dbinom': '$1+$2+1',
+ '\\dfrac': '$1+$2', '\\frac': '$1+$2', '\\tbinom': '$1+$2+1',
+ }
+
+ labelfunctions = {
+ '\\label': 'a name="#"',
+ }
+
+ limitcommands = {
+ '\\biginterleave': '⫼',
+ '\\inf': 'inf',
+ '\\lim': 'lim',
+ '\\max': 'max',
+ '\\min': 'min',
+ '\\sup': 'sup',
+ '\\ointop': '<span class="bigoperator integral">∮</span>',
+ '\\bigcap': '<span class="bigoperator">⋂</span>',
+ '\\bigcup': '<span class="bigoperator">⋃</span>',
+ '\\bigodot': '<span class="bigoperator">⨀</span>',
+ '\\bigoplus': '<span class="bigoperator">⨁</span>',
+ '\\bigotimes': '<span class="bigoperator">⨂</span>',
+ '\\bigsqcap': '<span class="bigoperator">⨅</span>',
+ '\\bigsqcup': '<span class="bigoperator">⨆</span>',
+ '\\biguplus': '<span class="bigoperator">⨄</span>',
+ '\\bigvee': '<span class="bigoperator">⋁</span>',
+ '\\bigwedge': '<span class="bigoperator">⋀</span>',
+ '\\coprod': '<span class="bigoperator">∐</span>',
+ '\\intop': '<span class="bigoperator integral">∫</span>',
+ '\\prod': '<span class="bigoperator">∏</span>',
+ '\\sum': '<span class="bigoperator">∑</span>',
+ '\\varprod': '<span class="bigoperator">⨉</span>',
+ '\\zcmp': '⨟', '\\zhide': '⧹', '\\zpipe': '⨠', '\\zproject': '⨡',
+ # integrals have limits in index position with LaTeX default settings
+ # TODO: move to commands?
+ '\\int': '<span class="bigoperator integral">∫</span>',
+ '\\iint': '<span class="bigoperator integral">∬</span>',
+ '\\iiint': '<span class="bigoperator integral">∭</span>',
+ '\\iiiint': '<span class="bigoperator integral">⨌</span>',
+ '\\fint': '<span class="bigoperator integral">⨏</span>',
+ '\\idotsint': '<span class="bigoperator integral">∫⋯∫</span>',
+ '\\oint': '<span class="bigoperator integral">∮</span>',
+ '\\oiint': '<span class="bigoperator integral">∯</span>',
+ '\\oiiint': '<span class="bigoperator integral">∰</span>',
+ '\\ointclockwise': '<span class="bigoperator integral">∲</span>',
+ '\\ointctrclockwise': '<span class="bigoperator integral">∳</span>',
+ '\\smallint': '<span class="smallsymbol integral">∫</span>',
+ '\\sqint': '<span class="bigoperator integral">⨖</span>',
+ '\\varointclockwise': '<span class="bigoperator integral">∲</span>',
+ }
+
+ modified = {
+ '\n': '', ' ': '', '$': '', '&': ' ', '\'': '’', '+': '\u2009+\u2009',
+ ',': ',\u2009', '-': '\u2009−\u2009', '/': '\u2009⁄\u2009', ':': ' : ', '<': '\u2009&lt;\u2009',
+ '=': '\u2009=\u2009', '>': '\u2009&gt;\u2009', '@': '', '~': '\u00a0',
+ }
+
+ onefunctions = {
+ '\\big': 'span class="bigdelimiter size1"',
+ '\\bigl': 'span class="bigdelimiter size1"',
+ '\\bigr': 'span class="bigdelimiter size1"',
+ '\\Big': 'span class="bigdelimiter size2"',
+ '\\Bigl': 'span class="bigdelimiter size2"',
+ '\\Bigr': 'span class="bigdelimiter size2"',
+ '\\bigg': 'span class="bigdelimiter size3"',
+ '\\biggl': 'span class="bigdelimiter size3"',
+ '\\biggr': 'span class="bigdelimiter size3"',
+ '\\Bigg': 'span class="bigdelimiter size4"',
+ '\\Biggl': 'span class="bigdelimiter size4"',
+ '\\Biggr': 'span class="bigdelimiter size4"',
+ # '\\bar': 'span class="bar"',
+ '\\begin{array}': 'span class="arraydef"',
+ '\\centering': 'span class="align-center"',
+ '\\ensuremath': 'span class="ensuremath"',
+ '\\hphantom': 'span class="phantom"',
+ '\\noindent': 'span class="noindent"',
+ '\\overbrace': 'span class="overbrace"',
+ '\\overline': 'span class="overline"',
+ '\\phantom': 'span class="phantom"',
+ '\\underbrace': 'span class="underbrace"',
+ '\\underline': '',
+ '\\vphantom': 'span class="phantom"',
+ }
+
+ # relations (put additional space before and after the symbol)
+ spacedcommands = {
+ # negated symbols without pre-composed Unicode character
+ '\\nleqq': '\u2266\u0338', # ≦̸
+ '\\ngeqq': '\u2267\u0338', # ≧̸
+ '\\nleqslant': '\u2a7d\u0338', # ⩽̸
+ '\\ngeqslant': '\u2a7e\u0338', # ⩾̸
+ '\\nsubseteqq': '\u2AC5\u0338', # ⫅̸
+ '\\nsupseteqq': '\u2AC6\u0338', # ⫆̸
+ '\\nsqsubset': '\u2276\u228F', # ⊏̸
+ # modified glyphs
+ '\\shortmid': '<span class="smallsymbol">∣</span>',
+ '\\shortparallel': '<span class="smallsymbol">∥</span>',
+ '\\nshortmid': '<span class="smallsymbol">∤</span>',
+ '\\nshortparallel': '<span class="smallsymbol">∦</span>',
+ '\\smallfrown': '<span class="smallsymbol">⌢</span>',
+ '\\smallsmile': '<span class="smallsymbol">⌣</span>',
+ '\\thickapprox': '<span class="boldsymbol">≈</span>',
+ '\\thicksim': '<span class="boldsymbol">∼</span>',
+ '\\varpropto': '<span class="mathsf">\u221d</span>', # ∝ PROPORTIONAL TO
+ }
+ for key, value in tex2unichar.mathrel.items():
+ spacedcommands['\\'+key] = value
+ starts = {
+ 'beginafter': '}', 'beginbefore': '\\begin{', 'bracket': '{',
+ 'command': '\\', 'comment': '%', 'complex': '\\[', 'simple': '$',
+ 'squarebracket': '[', 'unnumbered': '*',
+ }
+
+ symbolfunctions = {
+ '^': 'sup', '_': 'sub',
+ }
+
+ textfunctions = {
+ '\\mbox': 'span class="mbox"',
+ '\\text': 'span class="text"',
+ '\\textbf': 'span class="textbf"',
+ '\\textit': 'span class="textit"',
+ '\\textnormal': 'span class="textnormal"',
+ '\\textrm': 'span class="textrm"',
+ '\\textsc': 'span class="textsc"',
+ '\\textsf': 'span class="textsf"',
+ '\\textsl': 'span class="textsl"',
+ '\\texttt': 'span class="texttt"',
+ '\\textup': 'span class="normal"',
+ }
+
+ unmodified = {
+ 'characters': ['.', '*', '€', '(', ')', '[', ']',
+ '·', '!', ';', '|', '§', '"', '?'],
+ }
+
+
+class CommandLineParser:
+ "A parser for runtime options"
+
+ def __init__(self, options):
+ self.options = options
+
+ def parseoptions(self, args):
+ "Parse command line options"
+ if len(args) == 0:
+ return None
+ while len(args) > 0 and args[0].startswith('--'):
+ key, value = self.readoption(args)
+ if not key:
+ return 'Option ' + value + ' not recognized'
+ if not value:
+ return 'Option ' + key + ' needs a value'
+ setattr(self.options, key, value)
+ return None
+
+ def readoption(self, args):
+ "Read the key and value for an option"
+ arg = args[0][2:]
+ del args[0]
+ if '=' in arg:
+ key = self.readequalskey(arg, args)
+ else:
+ key = arg.replace('-', '')
+ if not hasattr(self.options, key):
+ return None, key
+ current = getattr(self.options, key)
+ if isinstance(current, bool):
+ return key, True
+ # read value
+ if len(args) == 0:
+ return key, None
+ if args[0].startswith('"'):
+ initial = args[0]
+ del args[0]
+ return key, self.readquoted(args, initial)
+ value = args[0].decode('utf-8')
+ del args[0]
+ if isinstance(current, list):
+ current.append(value)
+ return key, current
+ return key, value
+
+ def readquoted(self, args, initial):
+ "Read a value between quotes"
+ Trace.error('Oops')
+ value = initial[1:]
+ while len(args) > 0 and not args[0].endswith('"') and not args[0].startswith('--'):
+ Trace.error('Appending ' + args[0])
+ value += ' ' + args[0]
+ del args[0]
+ if len(args) == 0 or args[0].startswith('--'):
+ return None
+ value += ' ' + args[0:-1]
+ return value
+
+ def readequalskey(self, arg, args):
+ "Read a key using equals"
+ split = arg.split('=', 1)
+ key = split[0]
+ value = split[1]
+ args.insert(0, value)
+ return key
+
+
+class Options:
+ "A set of runtime options"
+
+ location = None
+
+ debug = False
+ quiet = False
+ version = False
+ help = False
+ simplemath = False
+ showlines = True
+
+ branches = {}
+
+ def parseoptions(self, args):
+ "Parse command line options"
+ Options.location = args[0]
+ del args[0]
+ parser = CommandLineParser(Options)
+ result = parser.parseoptions(args)
+ if result:
+ Trace.error(result)
+ self.usage()
+ self.processoptions()
+
+ def processoptions(self):
+ "Process all options parsed."
+ if Options.help:
+ self.usage()
+ if Options.version:
+ self.showversion()
+ # set in Trace if necessary
+ for param in dir(Trace):
+ if param.endswith('mode'):
+ setattr(Trace, param, getattr(self, param[:-4]))
+
+ def usage(self):
+ "Show correct usage"
+ Trace.error(f'Usage: {pathlib.Path(Options.location).parent}'
+ ' [options] "input string"')
+ Trace.error('Convert input string with LaTeX math to MathML')
+ self.showoptions()
+
+ def showoptions(self):
+ "Show all possible options"
+ Trace.error(' --help: show this online help')
+ Trace.error(' --quiet: disables all runtime messages')
+ Trace.error(' --debug: enable debugging messages (for developers)')
+ Trace.error(' --version: show version number and release date')
+ Trace.error(' --simplemath: do not generate fancy math constructions')
+ sys.exit()
+
+ def showversion(self):
+ "Return the current eLyXer version string"
+ Trace.error('math2html '+__version__)
+ sys.exit()
+
+
+class Cloner:
+ "An object used to clone other objects."
+
+ def clone(cls, original):
+ "Return an exact copy of an object."
+ "The original object must have an empty constructor."
+ return cls.create(original.__class__)
+
+ def create(cls, type):
+ "Create an object of a given class."
+ clone = type.__new__(type)
+ clone.__init__()
+ return clone
+
+ clone = classmethod(clone)
+ create = classmethod(create)
+
+
+class ContainerExtractor:
+ """A class to extract certain containers.
+
+ The config parameter is a map containing three lists:
+ allowed, copied and extracted.
+ Each of the three is a list of class names for containers.
+ Allowed containers are included as is into the result.
+ Cloned containers are cloned and placed into the result.
+ Extracted containers are looked into.
+ All other containers are silently ignored.
+ """
+
+ def __init__(self, config):
+ self.allowed = config['allowed']
+ self.extracted = config['extracted']
+
+ def extract(self, container):
+ "Extract a group of selected containers from a container."
+ list = []
+ locate = lambda c: c.__class__.__name__ in self.allowed
+ recursive = lambda c: c.__class__.__name__ in self.extracted
+ process = lambda c: self.process(c, list)
+ container.recursivesearch(locate, recursive, process)
+ return list
+
+ def process(self, container, list):
+ "Add allowed containers."
+ name = container.__class__.__name__
+ if name in self.allowed:
+ list.append(container)
+ else:
+ Trace.error('Unknown container class ' + name)
+
+ def safeclone(self, container):
+ "Return a new container with contents only in a safe list, recursively."
+ clone = Cloner.clone(container)
+ clone.output = container.output
+ clone.contents = self.extract(container)
+ return clone
+
+
+class Parser:
+ "A generic parser"
+
+ def __init__(self):
+ self.begin = 0
+ self.parameters = {}
+
+ def parseheader(self, reader):
+ "Parse the header"
+ header = reader.currentline().split()
+ reader.nextline()
+ self.begin = reader.linenumber
+ return header
+
+ def parseparameter(self, reader):
+ "Parse a parameter"
+ split = reader.currentline().strip().split(' ', 1)
+ reader.nextline()
+ if len(split) == 0:
+ return
+ key = split[0]
+ if len(split) == 1:
+ self.parameters[key] = True
+ return
+ if '"' not in split[1]:
+ self.parameters[key] = split[1].strip()
+ return
+ doublesplit = split[1].split('"')
+ self.parameters[key] = doublesplit[1]
+
+ def parseending(self, reader, process):
+ "Parse until the current ending is found"
+ if not self.ending:
+ Trace.error('No ending for ' + str(self))
+ return
+ while not reader.currentline().startswith(self.ending):
+ process()
+
+ def parsecontainer(self, reader, contents):
+ container = self.factory.createcontainer(reader)
+ if container:
+ container.parent = self.parent
+ contents.append(container)
+
+ def __str__(self):
+ "Return a description"
+ return self.__class__.__name__ + ' (' + str(self.begin) + ')'
+
+
+class LoneCommand(Parser):
+ "A parser for just one command line"
+
+ def parse(self, reader):
+ "Read nothing"
+ return []
+
+
+class TextParser(Parser):
+ "A parser for a command and a bit of text"
+
+ stack = []
+
+ def __init__(self, container):
+ Parser.__init__(self)
+ self.ending = None
+ if container.__class__.__name__ in ContainerConfig.endings:
+ self.ending = ContainerConfig.endings[container.__class__.__name__]
+ self.endings = []
+
+ def parse(self, reader):
+ "Parse lines as long as they are text"
+ TextParser.stack.append(self.ending)
+ self.endings = TextParser.stack + [ContainerConfig.endings['Layout'],
+ ContainerConfig.endings['Inset'],
+ self.ending]
+ contents = []
+ while not self.isending(reader):
+ self.parsecontainer(reader, contents)
+ return contents
+
+ def isending(self, reader):
+ "Check if text is ending"
+ current = reader.currentline().split()
+ if len(current) == 0:
+ return False
+ if current[0] in self.endings:
+ if current[0] in TextParser.stack:
+ TextParser.stack.remove(current[0])
+ else:
+ TextParser.stack = []
+ return True
+ return False
+
+
+class ExcludingParser(Parser):
+ "A parser that excludes the final line"
+
+ def parse(self, reader):
+ "Parse everything up to (and excluding) the final line"
+ contents = []
+ self.parseending(reader, lambda: self.parsecontainer(reader, contents))
+ return contents
+
+
+class BoundedParser(ExcludingParser):
+ "A parser bound by a final line"
+
+ def parse(self, reader):
+ "Parse everything, including the final line"
+ contents = ExcludingParser.parse(self, reader)
+ # skip last line
+ reader.nextline()
+ return contents
+
+
+class BoundedDummy(Parser):
+ "A bound parser that ignores everything"
+
+ def parse(self, reader):
+ "Parse the contents of the container"
+ self.parseending(reader, lambda: reader.nextline())
+ # skip last line
+ reader.nextline()
+ return []
+
+
+class StringParser(Parser):
+ "Parses just a string"
+
+ def parseheader(self, reader):
+ "Do nothing, just take note"
+ self.begin = reader.linenumber + 1
+ return []
+
+ def parse(self, reader):
+ "Parse a single line"
+ contents = reader.currentline()
+ reader.nextline()
+ return contents
+
+
+class ContainerOutput:
+ "The generic HTML output for a container."
+
+ def gethtml(self, container):
+ "Show an error."
+ Trace.error('gethtml() not implemented for ' + str(self))
+
+ def isempty(self):
+ "Decide if the output is empty: by default, not empty."
+ return False
+
+
+class EmptyOutput(ContainerOutput):
+
+ def gethtml(self, container):
+ "Return empty HTML code."
+ return []
+
+ def isempty(self):
+ "This output is particularly empty."
+ return True
+
+
+class FixedOutput(ContainerOutput):
+ "Fixed output"
+
+ def gethtml(self, container):
+ "Return constant HTML code"
+ return container.html
+
+
+class ContentsOutput(ContainerOutput):
+ "Outputs the contents converted to HTML"
+
+ def gethtml(self, container):
+ "Return the HTML code"
+ html = []
+ if container.contents is None:
+ return html
+ for element in container.contents:
+ if not hasattr(element, 'gethtml'):
+ Trace.error('No html in ' + element.__class__.__name__ + ': ' + str(element))
+ return html
+ html += element.gethtml()
+ return html
+
+
+class TaggedOutput(ContentsOutput):
+ "Outputs an HTML tag surrounding the contents."
+
+ tag = None
+ breaklines = False
+ empty = False
+
+ def settag(self, tag, breaklines=False, empty=False):
+ "Set the value for the tag and other attributes."
+ self.tag = tag
+ if breaklines:
+ self.breaklines = breaklines
+ if empty:
+ self.empty = empty
+ return self
+
+ def setbreaklines(self, breaklines):
+ "Set the value for breaklines."
+ self.breaklines = breaklines
+ return self
+
+ def gethtml(self, container):
+ "Return the HTML code."
+ if self.empty:
+ return [self.selfclosing(container)]
+ html = [self.open(container)]
+ html += ContentsOutput.gethtml(self, container)
+ html.append(self.close(container))
+ return html
+
+ def open(self, container):
+ "Get opening line."
+ if not self.checktag(container):
+ return ''
+ open = '<' + self.tag + '>'
+ if self.breaklines:
+ return open + '\n'
+ return open
+
+ def close(self, container):
+ "Get closing line."
+ if not self.checktag(container):
+ return ''
+ close = '</' + self.tag.split()[0] + '>'
+ if self.breaklines:
+ return '\n' + close + '\n'
+ return close
+
+ def selfclosing(self, container):
+ "Get self-closing line."
+ if not self.checktag(container):
+ return ''
+ selfclosing = '<' + self.tag + '/>'
+ if self.breaklines:
+ return selfclosing + '\n'
+ return selfclosing
+
+ def checktag(self, container):
+ "Check that the tag is valid."
+ if not self.tag:
+ Trace.error('No tag in ' + str(container))
+ return False
+ if self.tag == '':
+ return False
+ return True
+
+
+class FilteredOutput(ContentsOutput):
+ "Returns the output in the contents, but filtered:"
+ "some strings are replaced by others."
+
+ def __init__(self):
+ "Initialize the filters."
+ self.filters = []
+
+ def addfilter(self, original, replacement):
+ "Add a new filter: replace the original by the replacement."
+ self.filters.append((original, replacement))
+
+ def gethtml(self, container):
+ "Return the HTML code"
+ result = []
+ html = ContentsOutput.gethtml(self, container)
+ for line in html:
+ result.append(self.filter(line))
+ return result
+
+ def filter(self, line):
+ "Filter a single line with all available filters."
+ for original, replacement in self.filters:
+ if original in line:
+ line = line.replace(original, replacement)
+ return line
+
+
+class StringOutput(ContainerOutput):
+ "Returns a bare string as output"
+
+ def gethtml(self, container):
+ "Return a bare string"
+ return [container.string]
+
+
+class Globable:
+ """A bit of text which can be globbed (lumped together in bits).
+ Methods current(), skipcurrent(), checkfor() and isout() have to be
+ implemented by subclasses."""
+
+ leavepending = False
+
+ def __init__(self):
+ self.endinglist = EndingList()
+
+ def checkbytemark(self):
+ "Check for a Unicode byte mark and skip it."
+ if self.finished():
+ return
+ if ord(self.current()) == 0xfeff:
+ self.skipcurrent()
+
+ def isout(self):
+ "Find out if we are out of the position yet."
+ Trace.error('Unimplemented isout()')
+ return True
+
+ def current(self):
+ "Return the current character."
+ Trace.error('Unimplemented current()')
+ return ''
+
+ def checkfor(self, string):
+ "Check for the given string in the current position."
+ Trace.error('Unimplemented checkfor()')
+ return False
+
+ def finished(self):
+ "Find out if the current text has finished."
+ if self.isout():
+ if not self.leavepending:
+ self.endinglist.checkpending()
+ return True
+ return self.endinglist.checkin(self)
+
+ def skipcurrent(self):
+ "Return the current character and skip it."
+ Trace.error('Unimplemented skipcurrent()')
+ return ''
+
+ def glob(self, currentcheck):
+ "Glob a bit of text that satisfies a check on the current char."
+ glob = ''
+ while not self.finished() and currentcheck():
+ glob += self.skipcurrent()
+ return glob
+
+ def globalpha(self):
+ "Glob a bit of alpha text"
+ return self.glob(lambda: self.current().isalpha())
+
+ def globnumber(self):
+ "Glob a row of digits."
+ return self.glob(lambda: self.current().isdigit())
+
+ def isidentifier(self):
+ "Return if the current character is alphanumeric or _."
+ if self.current().isalnum() or self.current() == '_':
+ return True
+ return False
+
+ def globidentifier(self):
+ "Glob alphanumeric and _ symbols."
+ return self.glob(self.isidentifier)
+
+ def isvalue(self):
+ "Return if the current character is a value character:"
+ "not a bracket or a space."
+ if self.current().isspace():
+ return False
+ if self.current() in '{}()':
+ return False
+ return True
+
+ def globvalue(self):
+ "Glob a value: any symbols but brackets."
+ return self.glob(self.isvalue)
+
+ def skipspace(self):
+ "Skip all whitespace at current position."
+ return self.glob(lambda: self.current().isspace())
+
+ def globincluding(self, magicchar):
+ "Glob a bit of text up to (including) the magic char."
+ glob = self.glob(lambda: self.current() != magicchar) + magicchar
+ self.skip(magicchar)
+ return glob
+
+ def globexcluding(self, excluded):
+ "Glob a bit of text up until (excluding) any excluded character."
+ return self.glob(lambda: self.current() not in excluded)
+
+ def pushending(self, ending, optional=False):
+ "Push a new ending to the bottom"
+ self.endinglist.add(ending, optional)
+
+ def popending(self, expected=None):
+ "Pop the ending found at the current position"
+ if self.isout() and self.leavepending:
+ return expected
+ ending = self.endinglist.pop(self)
+ if expected and expected != ending:
+ Trace.error('Expected ending ' + expected + ', got ' + ending)
+ self.skip(ending)
+ return ending
+
+ def nextending(self):
+ "Return the next ending in the queue."
+ nextending = self.endinglist.findending(self)
+ if not nextending:
+ return None
+ return nextending.ending
+
+
+class EndingList:
+ "A list of position endings"
+
+ def __init__(self):
+ self.endings = []
+
+ def add(self, ending, optional=False):
+ "Add a new ending to the list"
+ self.endings.append(PositionEnding(ending, optional))
+
+ def pickpending(self, pos):
+ "Pick any pending endings from a parse position."
+ self.endings += pos.endinglist.endings
+
+ def checkin(self, pos):
+ "Search for an ending"
+ if self.findending(pos):
+ return True
+ return False
+
+ def pop(self, pos):
+ "Remove the ending at the current position"
+ if pos.isout():
+ Trace.error('No ending out of bounds')
+ return ''
+ ending = self.findending(pos)
+ if not ending:
+ Trace.error('No ending at ' + pos.current())
+ return ''
+ for each in reversed(self.endings):
+ self.endings.remove(each)
+ if each == ending:
+ return each.ending
+ elif not each.optional:
+ Trace.error('Removed non-optional ending ' + each)
+ Trace.error('No endings left')
+ return ''
+
+ def findending(self, pos):
+ "Find the ending at the current position"
+ if len(self.endings) == 0:
+ return None
+ for index, ending in enumerate(reversed(self.endings)):
+ if ending.checkin(pos):
+ return ending
+ if not ending.optional:
+ return None
+ return None
+
+ def checkpending(self):
+ "Check if there are any pending endings"
+ if len(self.endings) != 0:
+ Trace.error('Pending ' + str(self) + ' left open')
+
+ def __str__(self):
+ "Printable representation"
+ string = 'endings ['
+ for ending in self.endings:
+ string += str(ending) + ','
+ if len(self.endings) > 0:
+ string = string[:-1]
+ return string + ']'
+
+
+class PositionEnding:
+ "An ending for a parsing position"
+
+ def __init__(self, ending, optional):
+ self.ending = ending
+ self.optional = optional
+
+ def checkin(self, pos):
+ "Check for the ending"
+ return pos.checkfor(self.ending)
+
+ def __str__(self):
+ "Printable representation"
+ string = 'Ending ' + self.ending
+ if self.optional:
+ string += ' (optional)'
+ return string
+
+
+class Position(Globable):
+ """A position in a text to parse.
+ Including those in Globable, functions to implement by subclasses are:
+ skip(), identifier(), extract(), isout() and current()."""
+
+ def __init__(self):
+ Globable.__init__(self)
+
+ def skip(self, string):
+ "Skip a string"
+ Trace.error('Unimplemented skip()')
+
+ def identifier(self):
+ "Return an identifier for the current position."
+ Trace.error('Unimplemented identifier()')
+ return 'Error'
+
+ def extract(self, length):
+ "Extract the next string of the given length, or None if not enough text,"
+ "without advancing the parse position."
+ Trace.error('Unimplemented extract()')
+ return None
+
+ def checkfor(self, string):
+ "Check for a string at the given position."
+ return string == self.extract(len(string))
+
+ def checkforlower(self, string):
+ "Check for a string in lower case."
+ extracted = self.extract(len(string))
+ if not extracted:
+ return False
+ return string.lower() == self.extract(len(string)).lower()
+
+ def skipcurrent(self):
+ "Return the current character and skip it."
+ current = self.current()
+ self.skip(current)
+ return current
+
+ def __next__(self):
+ "Advance the position and return the next character."
+ self.skipcurrent()
+ return self.current()
+
+ def checkskip(self, string):
+ "Check for a string at the given position; if there, skip it"
+ if not self.checkfor(string):
+ return False
+ self.skip(string)
+ return True
+
+ def error(self, message):
+ "Show an error message and the position identifier."
+ Trace.error(message + ': ' + self.identifier())
+
+
+class TextPosition(Position):
+ "A parse position based on a raw text."
+
+ def __init__(self, text):
+ "Create the position from some text."
+ Position.__init__(self)
+ self.pos = 0
+ self.text = text
+ self.checkbytemark()
+
+ def skip(self, string):
+ "Skip a string of characters."
+ self.pos += len(string)
+
+ def identifier(self):
+ "Return a sample of the remaining text."
+ length = 30
+ if self.pos + length > len(self.text):
+ length = len(self.text) - self.pos
+ return '*' + self.text[self.pos:self.pos + length] + '*'
+
+ def isout(self):
+ "Find out if we are out of the text yet."
+ return self.pos >= len(self.text)
+
+ def current(self):
+ "Return the current character, assuming we are not out."
+ return self.text[self.pos]
+
+ def extract(self, length):
+ "Extract the next string of the given length, or None if not enough text."
+ if self.pos + length > len(self.text):
+ return None
+ return self.text[self.pos : self.pos + length] # noqa: E203
+
+
+class Container:
+ "A container for text and objects in a lyx file"
+
+ partkey = None
+ parent = None
+ begin = None
+
+ def __init__(self):
+ self.contents = list()
+
+ def process(self):
+ "Process contents"
+ pass
+
+ def gethtml(self):
+ "Get the resulting HTML"
+ html = self.output.gethtml(self)
+ if isinstance(html, str):
+ Trace.error('Raw string ' + html)
+ html = [html]
+ return html
+
+ def escape(self, line, replacements=EscapeConfig.entities):
+ "Escape a line with replacements from a map"
+ pieces = sorted(replacements.keys())
+ # do them in order
+ for piece in pieces:
+ if piece in line:
+ line = line.replace(piece, replacements[piece])
+ return line
+
+ def escapeentities(self, line):
+ "Escape all Unicode characters to HTML entities."
+ result = ''
+ pos = TextPosition(line)
+ while not pos.finished():
+ if ord(pos.current()) > 128:
+ codepoint = hex(ord(pos.current()))
+ if codepoint == '0xd835':
+ codepoint = hex(ord(next(pos)) + 0xf800)
+ result += '&#' + codepoint[1:] + ';'
+ else:
+ result += pos.current()
+ pos.skipcurrent()
+ return result
+
+ def searchall(self, type):
+ "Search for all embedded containers of a given type"
+ list = []
+ self.searchprocess(type, lambda container: list.append(container))
+ return list
+
+ def searchremove(self, type):
+ "Search for all containers of a type and remove them"
+ list = self.searchall(type)
+ for container in list:
+ container.parent.contents.remove(container)
+ return list
+
+ def searchprocess(self, type, process):
+ "Search for elements of a given type and process them"
+ self.locateprocess(lambda container: isinstance(container, type), process)
+
+ def locateprocess(self, locate, process):
+ "Search for all embedded containers and process them"
+ for container in self.contents:
+ container.locateprocess(locate, process)
+ if locate(container):
+ process(container)
+
+ def recursivesearch(self, locate, recursive, process):
+ "Perform a recursive search in the container."
+ for container in self.contents:
+ if recursive(container):
+ container.recursivesearch(locate, recursive, process)
+ if locate(container):
+ process(container)
+
+ def extracttext(self):
+ "Extract all text from allowed containers."
+ constants = ContainerExtractor(ContainerConfig.extracttext).extract(self)
+ return ''.join(constant.string for constant in constants)
+
+ def group(self, index, group, isingroup):
+ "Group some adjoining elements into a group"
+ if index >= len(self.contents):
+ return
+ if hasattr(self.contents[index], 'grouped'):
+ return
+ while index < len(self.contents) and isingroup(self.contents[index]):
+ self.contents[index].grouped = True
+ group.contents.append(self.contents[index])
+ self.contents.pop(index)
+ self.contents.insert(index, group)
+
+ def remove(self, index):
+ "Remove a container but leave its contents"
+ container = self.contents[index]
+ self.contents.pop(index)
+ while len(container.contents) > 0:
+ self.contents.insert(index, container.contents.pop())
+
+ def tree(self, level=0):
+ "Show in a tree"
+ Trace.debug(" " * level + str(self))
+ for container in self.contents:
+ container.tree(level + 1)
+
+ def getparameter(self, name):
+ "Get the value of a parameter, if present."
+ if name not in self.parameters:
+ return None
+ return self.parameters[name]
+
+ def getparameterlist(self, name):
+ "Get the value of a comma-separated parameter as a list."
+ paramtext = self.getparameter(name)
+ if not paramtext:
+ return []
+ return paramtext.split(',')
+
+ def hasemptyoutput(self):
+ "Check if the parent's output is empty."
+ current = self.parent
+ while current:
+ if current.output.isempty():
+ return True
+ current = current.parent
+ return False
+
+ def __str__(self):
+ "Get a description"
+ if not self.begin:
+ return self.__class__.__name__
+ return self.__class__.__name__ + '@' + str(self.begin)
+
+
+class BlackBox(Container):
+ "A container that does not output anything"
+
+ def __init__(self):
+ self.parser = LoneCommand()
+ self.output = EmptyOutput()
+ self.contents = []
+
+
+class StringContainer(Container):
+ "A container for a single string"
+
+ parsed = None
+
+ def __init__(self):
+ self.parser = StringParser()
+ self.output = StringOutput()
+ self.string = ''
+
+ def process(self):
+ "Replace special chars from the contents."
+ if self.parsed:
+ self.string = self.replacespecial(self.parsed)
+ self.parsed = None
+
+ def replacespecial(self, line):
+ "Replace all special chars from a line"
+ replaced = self.escape(line, EscapeConfig.entities)
+ replaced = self.changeline(replaced)
+ if ContainerConfig.string['startcommand'] in replaced and len(replaced) > 1:
+ # unprocessed commands
+ if self.begin:
+ message = 'Unknown command at ' + str(self.begin) + ': '
+ else:
+ message = 'Unknown command: '
+ Trace.error(message + replaced.strip())
+ return replaced
+
+ def changeline(self, line):
+ return self.escape(line, EscapeConfig.chars)
+
+ def extracttext(self):
+ "Return all text."
+ return self.string
+
+ def __str__(self):
+ "Return a printable representation."
+ result = 'StringContainer'
+ if self.begin:
+ result += '@' + str(self.begin)
+ ellipsis = '...'
+ if len(self.string.strip()) <= 15:
+ ellipsis = ''
+ return result + ' (' + self.string.strip()[:15] + ellipsis + ')'
+
+
+class Constant(StringContainer):
+ "A constant string"
+
+ def __init__(self, text):
+ self.contents = []
+ self.string = text
+ self.output = StringOutput()
+
+ def __str__(self):
+ return 'Constant: ' + self.string
+
+
+class DocumentParameters:
+ "Global parameters for the document."
+
+ displaymode = False
+
+
+class FormulaParser(Parser):
+ "Parses a formula"
+
+ def parseheader(self, reader):
+ "See if the formula is inlined"
+ self.begin = reader.linenumber + 1
+ type = self.parsetype(reader)
+ if not type:
+ reader.nextline()
+ type = self.parsetype(reader)
+ if not type:
+ Trace.error('Unknown formula type in ' + reader.currentline().strip())
+ return ['unknown']
+ return [type]
+
+ def parsetype(self, reader):
+ "Get the formula type from the first line."
+ if reader.currentline().find(FormulaConfig.starts['simple']) >= 0:
+ return 'inline'
+ if reader.currentline().find(FormulaConfig.starts['complex']) >= 0:
+ return 'block'
+ if reader.currentline().find(FormulaConfig.starts['unnumbered']) >= 0:
+ return 'block'
+ if reader.currentline().find(FormulaConfig.starts['beginbefore']) >= 0:
+ return 'numbered'
+ return None
+
+ def parse(self, reader):
+ "Parse the formula until the end"
+ formula = self.parseformula(reader)
+ while not reader.currentline().startswith(self.ending):
+ stripped = reader.currentline().strip()
+ if len(stripped) > 0:
+ Trace.error('Unparsed formula line ' + stripped)
+ reader.nextline()
+ reader.nextline()
+ return formula
+
+ def parseformula(self, reader):
+ "Parse the formula contents"
+ simple = FormulaConfig.starts['simple']
+ if simple in reader.currentline():
+ rest = reader.currentline().split(simple, 1)[1]
+ if simple in rest:
+ # formula is $...$
+ return self.parsesingleliner(reader, simple, simple)
+ # formula is multiline $...$
+ return self.parsemultiliner(reader, simple, simple)
+ if FormulaConfig.starts['complex'] in reader.currentline():
+ # formula of the form \[...\]
+ return self.parsemultiliner(reader, FormulaConfig.starts['complex'],
+ FormulaConfig.endings['complex'])
+ beginbefore = FormulaConfig.starts['beginbefore']
+ beginafter = FormulaConfig.starts['beginafter']
+ if beginbefore in reader.currentline():
+ if reader.currentline().strip().endswith(beginafter):
+ current = reader.currentline().strip()
+ endsplit = current.split(beginbefore)[1].split(beginafter)
+ startpiece = beginbefore + endsplit[0] + beginafter
+ endbefore = FormulaConfig.endings['endbefore']
+ endafter = FormulaConfig.endings['endafter']
+ endpiece = endbefore + endsplit[0] + endafter
+ return startpiece + self.parsemultiliner(reader, startpiece, endpiece) + endpiece
+ Trace.error('Missing ' + beginafter + ' in ' + reader.currentline())
+ return ''
+ begincommand = FormulaConfig.starts['command']
+ beginbracket = FormulaConfig.starts['bracket']
+ if begincommand in reader.currentline() and beginbracket in reader.currentline():
+ endbracket = FormulaConfig.endings['bracket']
+ return self.parsemultiliner(reader, beginbracket, endbracket)
+ Trace.error('Formula beginning ' + reader.currentline() + ' is unknown')
+ return ''
+
+ def parsesingleliner(self, reader, start, ending):
+ "Parse a formula in one line"
+ line = reader.currentline().strip()
+ if start not in line:
+ Trace.error('Line ' + line + ' does not contain formula start ' + start)
+ return ''
+ if not line.endswith(ending):
+ Trace.error('Formula ' + line + ' does not end with ' + ending)
+ return ''
+ index = line.index(start)
+ rest = line[index + len(start):-len(ending)]
+ reader.nextline()
+ return rest
+
+ def parsemultiliner(self, reader, start, ending):
+ "Parse a formula in multiple lines"
+ formula = ''
+ line = reader.currentline()
+ if start not in line:
+ Trace.error('Line ' + line.strip() + ' does not contain formula start ' + start)
+ return ''
+ index = line.index(start)
+ line = line[index + len(start):].strip()
+ while not line.endswith(ending):
+ formula += line + '\n'
+ reader.nextline()
+ line = reader.currentline()
+ formula += line[:-len(ending)]
+ reader.nextline()
+ return formula
+
+
+class FormulaBit(Container):
+ "A bit of a formula"
+
+ type = None
+ size = 1
+ original = ''
+
+ def __init__(self):
+ "The formula bit type can be 'alpha', 'number', 'font'."
+ self.contents = []
+ self.output = ContentsOutput()
+
+ def setfactory(self, factory):
+ "Set the internal formula factory."
+ self.factory = factory
+ return self
+
+ def add(self, bit):
+ "Add any kind of formula bit already processed"
+ self.contents.append(bit)
+ self.original += bit.original
+ bit.parent = self
+
+ def skiporiginal(self, string, pos):
+ "Skip a string and add it to the original formula"
+ self.original += string
+ if not pos.checkskip(string):
+ Trace.error('String ' + string + ' not at ' + pos.identifier())
+
+ def computesize(self):
+ "Compute the size of the bit as the max of the sizes of all contents."
+ if len(self.contents) == 0:
+ return 1
+ self.size = max(element.size for element in self.contents)
+ return self.size
+
+ def clone(self):
+ "Return a copy of itself."
+ return self.factory.parseformula(self.original)
+
+ def __str__(self):
+ "Get a string representation"
+ return self.__class__.__name__ + ' read in ' + self.original
+
+
+class TaggedBit(FormulaBit):
+ "A tagged string in a formula"
+
+ def constant(self, constant, tag):
+ "Set the constant and the tag"
+ self.output = TaggedOutput().settag(tag)
+ self.add(FormulaConstant(constant))
+ return self
+
+ def complete(self, contents, tag, breaklines=False):
+ "Set the constant and the tag"
+ self.contents = contents
+ self.output = TaggedOutput().settag(tag, breaklines)
+ return self
+
+ def selfcomplete(self, tag):
+ "Set the self-closing tag, no contents (as in <hr/>)."
+ self.output = TaggedOutput().settag(tag, empty=True)
+ return self
+
+
+class FormulaConstant(Constant):
+ "A constant string in a formula"
+
+ def __init__(self, string):
+ "Set the constant string"
+ Constant.__init__(self, string)
+ self.original = string
+ self.size = 1
+ self.type = None
+
+ def computesize(self):
+ "Compute the size of the constant: always 1."
+ return self.size
+
+ def clone(self):
+ "Return a copy of itself."
+ return FormulaConstant(self.original)
+
+ def __str__(self):
+ "Return a printable representation."
+ return 'Formula constant: ' + self.string
+
+
+class RawText(FormulaBit):
+ "A bit of text inside a formula"
+
+ def detect(self, pos):
+ "Detect a bit of raw text"
+ return pos.current().isalpha()
+
+ def parsebit(self, pos):
+ "Parse alphabetic text"
+ alpha = pos.globalpha()
+ self.add(FormulaConstant(alpha))
+ self.type = 'alpha'
+
+
+class FormulaSymbol(FormulaBit):
+ "A symbol inside a formula"
+
+ modified = FormulaConfig.modified
+ unmodified = FormulaConfig.unmodified['characters']
+
+ def detect(self, pos):
+ "Detect a symbol"
+ if pos.current() in FormulaSymbol.unmodified:
+ return True
+ if pos.current() in FormulaSymbol.modified:
+ return True
+ return False
+
+ def parsebit(self, pos):
+ "Parse the symbol"
+ if pos.current() in FormulaSymbol.unmodified:
+ self.addsymbol(pos.current(), pos)
+ return
+ if pos.current() in FormulaSymbol.modified:
+ self.addsymbol(FormulaSymbol.modified[pos.current()], pos)
+ return
+ Trace.error('Symbol ' + pos.current() + ' not found')
+
+ def addsymbol(self, symbol, pos):
+ "Add a symbol"
+ self.skiporiginal(pos.current(), pos)
+ self.contents.append(FormulaConstant(symbol))
+
+
+class FormulaNumber(FormulaBit):
+ "A string of digits in a formula"
+
+ def detect(self, pos):
+ "Detect a digit"
+ return pos.current().isdigit()
+
+ def parsebit(self, pos):
+ "Parse a bunch of digits"
+ digits = pos.glob(lambda: pos.current().isdigit())
+ self.add(FormulaConstant(digits))
+ self.type = 'number'
+
+
+class Comment(FormulaBit):
+ "A LaTeX comment: % to the end of the line."
+
+ start = FormulaConfig.starts['comment']
+
+ def detect(self, pos):
+ "Detect the %."
+ return pos.current() == self.start
+
+ def parsebit(self, pos):
+ "Parse to the end of the line."
+ self.original += pos.globincluding('\n')
+
+
+class WhiteSpace(FormulaBit):
+ "Some white space inside a formula."
+
+ def detect(self, pos):
+ "Detect the white space."
+ return pos.current().isspace()
+
+ def parsebit(self, pos):
+ "Parse all whitespace."
+ self.original += pos.skipspace()
+
+ def __str__(self):
+ "Return a printable representation."
+ return 'Whitespace: *' + self.original + '*'
+
+
+class Bracket(FormulaBit):
+ "A {} bracket inside a formula"
+
+ start = FormulaConfig.starts['bracket']
+ ending = FormulaConfig.endings['bracket']
+
+ def __init__(self):
+ "Create a (possibly literal) new bracket"
+ FormulaBit.__init__(self)
+ self.inner = None
+
+ def detect(self, pos):
+ "Detect the start of a bracket"
+ return pos.checkfor(self.start)
+
+ def parsebit(self, pos):
+ "Parse the bracket"
+ self.parsecomplete(pos, self.innerformula)
+ return self
+
+ def parsetext(self, pos):
+ "Parse a text bracket"
+ self.parsecomplete(pos, self.innertext)
+ return self
+
+ def parseliteral(self, pos):
+ "Parse a literal bracket"
+ self.parsecomplete(pos, self.innerliteral)
+ return self
+
+ def parsecomplete(self, pos, innerparser):
+ "Parse the start and end marks"
+ if not pos.checkfor(self.start):
+ Trace.error('Bracket should start with ' + self.start + ' at ' + pos.identifier())
+ return None
+ self.skiporiginal(self.start, pos)
+ pos.pushending(self.ending)
+ innerparser(pos)
+ self.original += pos.popending(self.ending)
+ self.computesize()
+
+ def innerformula(self, pos):
+ "Parse a whole formula inside the bracket"
+ while not pos.finished():
+ self.add(self.factory.parseany(pos))
+
+ def innertext(self, pos):
+ "Parse some text inside the bracket, following textual rules."
+ specialchars = list(FormulaConfig.symbolfunctions.keys())
+ specialchars.append(FormulaConfig.starts['command'])
+ specialchars.append(FormulaConfig.starts['bracket'])
+ specialchars.append(Comment.start)
+ while not pos.finished():
+ if pos.current() in specialchars:
+ self.add(self.factory.parseany(pos))
+ if pos.checkskip(' '):
+ self.original += ' '
+ else:
+ self.add(FormulaConstant(pos.skipcurrent()))
+
+ def innerliteral(self, pos):
+ "Parse a literal inside the bracket, which does not generate HTML."
+ self.literal = ''
+ while not pos.finished() and not pos.current() == self.ending:
+ if pos.current() == self.start:
+ self.parseliteral(pos)
+ else:
+ self.literal += pos.skipcurrent()
+ self.original += self.literal
+
+
+class SquareBracket(Bracket):
+ "A [] bracket inside a formula"
+
+ start = FormulaConfig.starts['squarebracket']
+ ending = FormulaConfig.endings['squarebracket']
+
+ def clone(self):
+ "Return a new square bracket with the same contents."
+ bracket = SquareBracket()
+ bracket.contents = self.contents
+ return bracket
+
+
+class MathsProcessor:
+ "A processor for a maths construction inside the FormulaProcessor."
+
+ def process(self, contents, index):
+ "Process an element inside a formula."
+ Trace.error('Unimplemented process() in ' + str(self))
+
+ def __str__(self):
+ "Return a printable description."
+ return 'Maths processor ' + self.__class__.__name__
+
+
+class FormulaProcessor:
+ "A processor specifically for formulas."
+
+ processors = []
+
+ def process(self, bit):
+ "Process the contents of every formula bit, recursively."
+ self.processcontents(bit)
+ self.processinsides(bit)
+ self.traversewhole(bit)
+
+ def processcontents(self, bit):
+ "Process the contents of a formula bit."
+ if not isinstance(bit, FormulaBit):
+ return
+ bit.process()
+ for element in bit.contents:
+ self.processcontents(element)
+
+ def processinsides(self, bit):
+ "Process the insides (limits, brackets) in a formula bit."
+ if not isinstance(bit, FormulaBit):
+ return
+ for index, element in enumerate(bit.contents):
+ for processor in self.processors:
+ processor.process(bit.contents, index)
+ # continue with recursive processing
+ self.processinsides(element)
+
+ def traversewhole(self, formula):
+ "Traverse over the contents to alter variables and space units."
+ last = None
+ for bit, contents in self.traverse(formula):
+ if bit.type == 'alpha':
+ self.italicize(bit, contents)
+ elif bit.type == 'font' and last and last.type == 'number':
+ bit.contents.insert(0, FormulaConstant('\u2009'))
+ last = bit
+
+ def traverse(self, bit):
+ "Traverse a formula and yield a flattened structure of (bit, list) pairs."
+ for element in bit.contents:
+ if hasattr(element, 'type') and element.type:
+ yield element, bit.contents
+ elif isinstance(element, FormulaBit):
+ yield from self.traverse(element)
+
+ def italicize(self, bit, contents):
+ "Italicize the given bit of text."
+ index = contents.index(bit)
+ contents[index] = TaggedBit().complete([bit], 'i')
+
+
+class Formula(Container):
+ "A LaTeX formula"
+
+ def __init__(self):
+ self.parser = FormulaParser()
+ self.output = TaggedOutput().settag('span class="formula"')
+
+ def process(self):
+ "Convert the formula to tags"
+ if self.header[0] == 'inline':
+ DocumentParameters.displaymode = False
+ else:
+ DocumentParameters.displaymode = True
+ self.output.settag('div class="formula"', True)
+ self.classic()
+
+ def classic(self):
+ "Make the contents using classic output generation with XHTML and CSS."
+ whole = FormulaFactory().parseformula(self.parsed)
+ FormulaProcessor().process(whole)
+ whole.parent = self
+ self.contents = [whole]
+
+ def parse(self, pos):
+ "Parse using a parse position instead of self.parser."
+ if pos.checkskip('$$'):
+ self.parsedollarblock(pos)
+ elif pos.checkskip('$'):
+ self.parsedollarinline(pos)
+ elif pos.checkskip('\\('):
+ self.parseinlineto(pos, '\\)')
+ elif pos.checkskip('\\['):
+ self.parseblockto(pos, '\\]')
+ else:
+ pos.error('Unparseable formula')
+ self.process()
+ return self
+
+ def parsedollarinline(self, pos):
+ "Parse a $...$ formula."
+ self.header = ['inline']
+ self.parsedollar(pos)
+
+ def parsedollarblock(self, pos):
+ "Parse a $$...$$ formula."
+ self.header = ['block']
+ self.parsedollar(pos)
+ if not pos.checkskip('$'):
+ pos.error('Formula should be $$...$$, but last $ is missing.')
+
+ def parsedollar(self, pos):
+ "Parse to the next $."
+ pos.pushending('$')
+ self.parsed = pos.globexcluding('$')
+ pos.popending('$')
+
+ def parseinlineto(self, pos, limit):
+ "Parse a \\(...\\) formula."
+ self.header = ['inline']
+ self.parseupto(pos, limit)
+
+ def parseblockto(self, pos, limit):
+ "Parse a \\[...\\] formula."
+ self.header = ['block']
+ self.parseupto(pos, limit)
+
+ def parseupto(self, pos, limit):
+ "Parse a formula that ends with the given command."
+ pos.pushending(limit)
+ self.parsed = pos.glob(lambda: True)
+ pos.popending(limit)
+
+ def __str__(self):
+ "Return a printable representation."
+ if self.partkey and self.partkey.number:
+ return 'Formula (' + self.partkey.number + ')'
+ return 'Unnumbered formula'
+
+
+class WholeFormula(FormulaBit):
+ "Parse a whole formula"
+
+ def detect(self, pos):
+ "Not outside the formula is enough."
+ return not pos.finished()
+
+ def parsebit(self, pos):
+ "Parse with any formula bit"
+ while not pos.finished():
+ self.add(self.factory.parseany(pos))
+
+
+class FormulaFactory:
+ "Construct bits of formula"
+
+ # bit types will be appended later
+ types = [FormulaSymbol, RawText, FormulaNumber, Bracket, Comment, WhiteSpace]
+ skippedtypes = [Comment, WhiteSpace]
+ defining = False
+
+ def __init__(self):
+ "Initialize the map of instances."
+ self.instances = {}
+
+ def detecttype(self, type, pos):
+ "Detect a bit of a given type."
+ if pos.finished():
+ return False
+ return self.instance(type).detect(pos)
+
+ def instance(self, type):
+ "Get an instance of the given type."
+ if type not in self.instances or not self.instances[type]:
+ self.instances[type] = self.create(type)
+ return self.instances[type]
+
+ def create(self, type):
+ "Create a new formula bit of the given type."
+ return Cloner.create(type).setfactory(self)
+
+ def clearskipped(self, pos):
+ "Clear any skipped types."
+ while not pos.finished():
+ if not self.skipany(pos):
+ return
+ return
+
+ def skipany(self, pos):
+ "Skip any skipped types."
+ for type in self.skippedtypes:
+ if self.instance(type).detect(pos):
+ return self.parsetype(type, pos)
+ return None
+
+ def parseany(self, pos):
+ "Parse any formula bit at the current location."
+ for type in self.types + self.skippedtypes:
+ if self.detecttype(type, pos):
+ return self.parsetype(type, pos)
+ Trace.error('Unrecognized formula at ' + pos.identifier())
+ return FormulaConstant(pos.skipcurrent())
+
+ def parsetype(self, type, pos):
+ "Parse the given type and return it."
+ bit = self.instance(type)
+ self.instances[type] = None
+ returnedbit = bit.parsebit(pos)
+ if returnedbit:
+ return returnedbit.setfactory(self)
+ return bit
+
+ def parseformula(self, formula):
+ "Parse a string of text that contains a whole formula."
+ pos = TextPosition(formula)
+ whole = self.create(WholeFormula)
+ if whole.detect(pos):
+ whole.parsebit(pos)
+ return whole
+ # no formula found
+ if not pos.finished():
+ Trace.error('Unknown formula at: ' + pos.identifier())
+ whole.add(TaggedBit().constant(formula, 'span class="unknown"'))
+ return whole
+
+
+class FormulaCommand(FormulaBit):
+ "A LaTeX command inside a formula"
+
+ types = []
+ start = FormulaConfig.starts['command']
+ commandmap = None
+
+ def detect(self, pos):
+ "Find the current command."
+ return pos.checkfor(FormulaCommand.start)
+
+ def parsebit(self, pos):
+ "Parse the command."
+ command = self.extractcommand(pos)
+ bit = self.parsewithcommand(command, pos)
+ if bit:
+ return bit
+ if command.startswith('\\up') or command.startswith('\\Up'):
+ upgreek = self.parseupgreek(command, pos)
+ if upgreek:
+ return upgreek
+ if not self.factory.defining:
+ Trace.error('Unknown command ' + command)
+ self.output = TaggedOutput().settag('span class="unknown"')
+ self.add(FormulaConstant(command))
+ return None
+
+ def parsewithcommand(self, command, pos):
+ "Parse the command type once we have the command."
+ for type in FormulaCommand.types:
+ if command in type.commandmap:
+ return self.parsecommandtype(command, type, pos)
+ return None
+
+ def parsecommandtype(self, command, type, pos):
+ "Parse a given command type."
+ bit = self.factory.create(type)
+ bit.setcommand(command)
+ returned = bit.parsebit(pos)
+ if returned:
+ return returned
+ return bit
+
+ def extractcommand(self, pos):
+ "Extract the command from the current position."
+ if not pos.checkskip(FormulaCommand.start):
+ pos.error('Missing command start ' + FormulaCommand.start)
+ return
+ if pos.finished():
+ return self.emptycommand(pos)
+ if pos.current().isalpha():
+ # alpha command
+ command = FormulaCommand.start + pos.globalpha()
+ # skip mark of short command
+ pos.checkskip('*')
+ return command
+ # symbol command
+ return FormulaCommand.start + pos.skipcurrent()
+
+ def emptycommand(self, pos):
+ """Check for an empty command: look for command disguised as ending.
+ Special case against '{ \\{ \\} }' situation."""
+ command = ''
+ if not pos.isout():
+ ending = pos.nextending()
+ if ending and pos.checkskip(ending):
+ command = ending
+ return FormulaCommand.start + command
+
+ def parseupgreek(self, command, pos):
+ "Parse the Greek \\up command.."
+ if len(command) < 4:
+ return None
+ if command.startswith('\\up'):
+ upcommand = '\\' + command[3:]
+ elif pos.checkskip('\\Up'):
+ upcommand = '\\' + command[3:4].upper() + command[4:]
+ else:
+ Trace.error('Impossible upgreek command: ' + command)
+ return
+ upgreek = self.parsewithcommand(upcommand, pos)
+ if upgreek:
+ upgreek.type = 'font'
+ return upgreek
+
+
+class CommandBit(FormulaCommand):
+ "A formula bit that includes a command"
+
+ def setcommand(self, command):
+ "Set the command in the bit"
+ self.command = command
+ if self.commandmap:
+ self.original += command
+ self.translated = self.commandmap[self.command]
+
+ def parseparameter(self, pos):
+ "Parse a parameter at the current position"
+ self.factory.clearskipped(pos)
+ if pos.finished():
+ return None
+ parameter = self.factory.parseany(pos)
+ self.add(parameter)
+ return parameter
+
+ def parsesquare(self, pos):
+ "Parse a square bracket"
+ self.factory.clearskipped(pos)
+ if not self.factory.detecttype(SquareBracket, pos):
+ return None
+ bracket = self.factory.parsetype(SquareBracket, pos)
+ self.add(bracket)
+ return bracket
+
+ def parseliteral(self, pos):
+ "Parse a literal bracket."
+ self.factory.clearskipped(pos)
+ if not self.factory.detecttype(Bracket, pos):
+ if not pos.isvalue():
+ Trace.error('No literal parameter found at: ' + pos.identifier())
+ return None
+ return pos.globvalue()
+ bracket = Bracket().setfactory(self.factory)
+ self.add(bracket.parseliteral(pos))
+ return bracket.literal
+
+ def parsesquareliteral(self, pos):
+ "Parse a square bracket literally."
+ self.factory.clearskipped(pos)
+ if not self.factory.detecttype(SquareBracket, pos):
+ return None
+ bracket = SquareBracket().setfactory(self.factory)
+ self.add(bracket.parseliteral(pos))
+ return bracket.literal
+
+ def parsetext(self, pos):
+ "Parse a text parameter."
+ self.factory.clearskipped(pos)
+ if not self.factory.detecttype(Bracket, pos):
+ Trace.error('No text parameter for ' + self.command)
+ return None
+ bracket = Bracket().setfactory(self.factory).parsetext(pos)
+ self.add(bracket)
+ return bracket
+
+
+class EmptyCommand(CommandBit):
+ "An empty command (without parameters)"
+
+ commandmap = FormulaConfig.commands
+
+ def parsebit(self, pos):
+ "Parse a command without parameters"
+ self.contents = [FormulaConstant(self.translated)]
+
+
+class SpacedCommand(CommandBit):
+ """An empty command which should have math spacing in formulas."""
+
+ commandmap = FormulaConfig.spacedcommands
+
+ def parsebit(self, pos):
+ "Place as contents the command translated and spaced."
+ # pad with MEDIUM MATHEMATICAL SPACE (4/18 em): too wide in STIX fonts :(
+ # self.contents = [FormulaConstant('\u205f' + self.translated + '\u205f')]
+ # pad with THIN SPACE (1/5 em)
+ self.contents = [FormulaConstant('\u2009' + self.translated + '\u2009')]
+
+
+class AlphaCommand(EmptyCommand):
+ """A command without parameters whose result is alphabetical."""
+
+ commandmap = FormulaConfig.alphacommands
+ greek_capitals = ('\\Xi', '\\Theta', '\\Pi', '\\Sigma', '\\Gamma',
+ '\\Lambda', '\\Phi', '\\Psi', '\\Delta',
+ '\\Upsilon', '\\Omega')
+
+ def parsebit(self, pos):
+ "Parse the command and set type to alpha"
+ EmptyCommand.parsebit(self, pos)
+ if self.command not in self.greek_capitals:
+ # Greek Capital letters are upright in LaTeX default math-style.
+ # TODO: use italic, like in MathML and "iso" math-style?
+ self.type = 'alpha'
+
+
+class OneParamFunction(CommandBit):
+ "A function of one parameter"
+
+ commandmap = FormulaConfig.onefunctions
+ simplified = False
+
+ def parsebit(self, pos):
+ "Parse a function with one parameter"
+ self.output = TaggedOutput().settag(self.translated)
+ self.parseparameter(pos)
+ self.simplifyifpossible()
+
+ def simplifyifpossible(self):
+ "Try to simplify to a single character."
+ if self.original in self.commandmap:
+ self.output = FixedOutput()
+ self.html = [self.commandmap[self.original]]
+ self.simplified = True
+
+
+class SymbolFunction(CommandBit):
+ "Find a function which is represented by a symbol (like _ or ^)"
+
+ commandmap = FormulaConfig.symbolfunctions
+
+ def detect(self, pos):
+ "Find the symbol"
+ return pos.current() in SymbolFunction.commandmap
+
+ def parsebit(self, pos):
+ "Parse the symbol"
+ self.setcommand(pos.current())
+ pos.skip(self.command)
+ self.output = TaggedOutput().settag(self.translated)
+ self.parseparameter(pos)
+
+
+class TextFunction(CommandBit):
+ "A function where parameters are read as text."
+
+ commandmap = FormulaConfig.textfunctions
+
+ def parsebit(self, pos):
+ "Parse a text parameter"
+ self.output = TaggedOutput().settag(self.translated)
+ self.parsetext(pos)
+
+ def process(self):
+ "Set the type to font"
+ self.type = 'font'
+
+
+class FontFunction(OneParamFunction):
+ """A function of one parameter that changes the font."""
+ # TODO: keep letters italic with \boldsymbol.
+
+ commandmap = FormulaConfig.fontfunctions
+
+ def process(self):
+ "Simplify if possible using a single character."
+ self.type = 'font'
+ self.simplifyifpossible()
+
+
+FormulaFactory.types += [FormulaCommand, SymbolFunction]
+FormulaCommand.types = [
+ AlphaCommand, EmptyCommand, OneParamFunction, FontFunction,
+ TextFunction, SpacedCommand]
+
+
+class BigBracket:
+ "A big bracket generator."
+
+ def __init__(self, size, bracket, alignment='l'):
+ "Set the size and symbol for the bracket."
+ self.size = size
+ self.original = bracket
+ self.alignment = alignment
+ self.pieces = None
+ if bracket in FormulaConfig.bigbrackets:
+ self.pieces = FormulaConfig.bigbrackets[bracket]
+
+ def getpiece(self, index):
+ "Return the nth piece for the bracket."
+ function = getattr(self, 'getpiece' + str(len(self.pieces)))
+ return function(index)
+
+ def getpiece1(self, index):
+ "Return the only piece for a single-piece bracket."
+ return self.pieces[0]
+
+ def getpiece3(self, index):
+ "Get the nth piece for a 3-piece bracket: parenthesis or square bracket."
+ if index == 0:
+ return self.pieces[0]
+ if index == self.size - 1:
+ return self.pieces[-1]
+ return self.pieces[1]
+
+ def getpiece4(self, index):
+ "Get the nth piece for a 4-piece bracket: curly bracket."
+ if index == 0:
+ return self.pieces[0]
+ if index == self.size - 1:
+ return self.pieces[3]
+ if index == (self.size - 1)/2:
+ return self.pieces[2]
+ return self.pieces[1]
+
+ def getcell(self, index):
+ "Get the bracket piece as an array cell."
+ piece = self.getpiece(index)
+ span = 'span class="bracket align-' + self.alignment + '"'
+ return TaggedBit().constant(piece, span)
+
+ def getcontents(self):
+ "Get the bracket as an array or as a single bracket."
+ if self.size == 1 or not self.pieces:
+ return self.getsinglebracket()
+ rows = []
+ for index in range(self.size):
+ cell = self.getcell(index)
+ rows.append(TaggedBit().complete([cell], 'span class="arrayrow"'))
+ return [TaggedBit().complete(rows, 'span class="array"')]
+
+ def getsinglebracket(self):
+ "Return the bracket as a single sign."
+ if self.original == '.':
+ return [TaggedBit().constant('', 'span class="emptydot"')]
+ return [TaggedBit().constant(self.original, 'span class="stretchy"')]
+
+
+class FormulaEquation(CommandBit):
+ "A simple numbered equation."
+
+ piece = 'equation'
+
+ def parsebit(self, pos):
+ "Parse the array"
+ self.output = ContentsOutput()
+ self.add(self.factory.parsetype(WholeFormula, pos))
+
+
+class FormulaCell(FormulaCommand):
+ "An array cell inside a row"
+
+ def setalignment(self, alignment):
+ self.alignment = alignment
+ self.output = TaggedOutput().settag('span class="arraycell align-'
+ + alignment + '"', True)
+ return self
+
+ def parsebit(self, pos):
+ self.factory.clearskipped(pos)
+ if pos.finished():
+ return
+ self.add(self.factory.parsetype(WholeFormula, pos))
+
+
+class FormulaRow(FormulaCommand):
+ "An array row inside an array"
+
+ cellseparator = FormulaConfig.array['cellseparator']
+
+ def setalignments(self, alignments):
+ self.alignments = alignments
+ self.output = TaggedOutput().settag('span class="arrayrow"', True)
+ return self
+
+ def parsebit(self, pos):
+ "Parse a whole row"
+ index = 0
+ pos.pushending(self.cellseparator, optional=True)
+ while not pos.finished():
+ cell = self.createcell(index)
+ cell.parsebit(pos)
+ self.add(cell)
+ index += 1
+ pos.checkskip(self.cellseparator)
+ if len(self.contents) == 0:
+ self.output = EmptyOutput()
+
+ def createcell(self, index):
+ "Create the cell that corresponds to the given index."
+ alignment = self.alignments[index % len(self.alignments)]
+ return self.factory.create(FormulaCell).setalignment(alignment)
+
+
+class MultiRowFormula(CommandBit):
+ "A formula with multiple rows."
+
+ def parserows(self, pos):
+ "Parse all rows, finish when no more row ends"
+ self.rows = []
+ first = True
+ for row in self.iteraterows(pos):
+ if first:
+ first = False
+ else:
+ # intersparse empty rows
+ self.addempty()
+ row.parsebit(pos)
+ self.addrow(row)
+ self.size = len(self.rows)
+
+ def iteraterows(self, pos):
+ "Iterate over all rows, end when no more row ends"
+ rowseparator = FormulaConfig.array['rowseparator']
+ while True:
+ pos.pushending(rowseparator, True)
+ row = self.factory.create(FormulaRow)
+ yield row.setalignments(self.alignments)
+ if pos.checkfor(rowseparator):
+ self.original += pos.popending(rowseparator)
+ else:
+ return
+
+ def addempty(self):
+ "Add an empty row."
+ row = self.factory.create(FormulaRow).setalignments(self.alignments)
+ for index, originalcell in enumerate(self.rows[-1].contents):
+ cell = row.createcell(index)
+ cell.add(FormulaConstant(' '))
+ row.add(cell)
+ self.addrow(row)
+
+ def addrow(self, row):
+ "Add a row to the contents and to the list of rows."
+ self.rows.append(row)
+ self.add(row)
+
+
+class FormulaArray(MultiRowFormula):
+ "An array within a formula"
+
+ piece = 'array'
+
+ def parsebit(self, pos):
+ "Parse the array"
+ self.output = TaggedOutput().settag('span class="array"', False)
+ self.parsealignments(pos)
+ self.parserows(pos)
+
+ def parsealignments(self, pos):
+ "Parse the different alignments"
+ # vertical
+ self.valign = 'c'
+ literal = self.parsesquareliteral(pos)
+ if literal:
+ self.valign = literal
+ # horizontal
+ literal = self.parseliteral(pos)
+ self.alignments = []
+ for s in literal:
+ self.alignments.append(s)
+
+
+class FormulaMatrix(MultiRowFormula):
+ "A matrix (array with center alignment)."
+
+ piece = 'matrix'
+
+ def parsebit(self, pos):
+ "Parse the matrix, set alignments to 'c'."
+ self.output = TaggedOutput().settag('span class="array"', False)
+ self.valign = 'c'
+ self.alignments = ['c']
+ self.parserows(pos)
+
+
+class FormulaCases(MultiRowFormula):
+ "A cases statement"
+
+ piece = 'cases'
+
+ def parsebit(self, pos):
+ "Parse the cases"
+ self.output = ContentsOutput()
+ self.alignments = ['l', 'l']
+ self.parserows(pos)
+ for row in self.contents:
+ for cell in row.contents:
+ cell.output.settag('span class="case align-l"', True)
+ cell.contents.append(FormulaConstant(' '))
+ array = TaggedBit().complete(self.contents, 'span class="bracketcases"', True)
+ brace = BigBracket(len(self.contents), '{', 'l')
+ self.contents = brace.getcontents() + [array]
+
+
+class EquationEnvironment(MultiRowFormula):
+ "A \\begin{}...\\end equation environment with rows and cells."
+
+ def parsebit(self, pos):
+ "Parse the whole environment."
+ environment = self.piece.replace('*', '')
+ self.output = TaggedOutput().settag(
+ 'span class="environment %s"'%environment, False)
+ if environment in FormulaConfig.environments:
+ self.alignments = FormulaConfig.environments[environment]
+ else:
+ Trace.error('Unknown equation environment ' + self.piece)
+ # print in red
+ self.output = TaggedOutput().settag('span class="unknown"')
+ self.add(FormulaConstant('\\begin{%s} '%environment))
+
+ self.alignments = ['l']
+ self.parserows(pos)
+
+
+class BeginCommand(CommandBit):
+ "A \\begin{}...\\end command and what it entails (array, cases, aligned)"
+
+ commandmap = {FormulaConfig.array['begin']: ''}
+
+ types = [FormulaEquation, FormulaArray, FormulaCases, FormulaMatrix]
+
+ def parsebit(self, pos):
+ "Parse the begin command"
+ command = self.parseliteral(pos)
+ bit = self.findbit(command)
+ ending = FormulaConfig.array['end'] + '{' + command + '}'
+ pos.pushending(ending)
+ bit.parsebit(pos)
+ self.add(bit)
+ self.original += pos.popending(ending)
+ self.size = bit.size
+
+ def findbit(self, piece):
+ "Find the command bit corresponding to the \\begin{piece}"
+ for type in BeginCommand.types:
+ if piece.replace('*', '') == type.piece:
+ return self.factory.create(type)
+ bit = self.factory.create(EquationEnvironment)
+ bit.piece = piece
+ return bit
+
+
+FormulaCommand.types += [BeginCommand]
+
+
+class CombiningFunction(OneParamFunction):
+
+ commandmap = FormulaConfig.combiningfunctions
+
+ def parsebit(self, pos):
+ "Parse a combining function."
+ combining = self.translated
+ parameter = self.parsesingleparameter(pos)
+ if not parameter:
+ Trace.error('Missing parameter for combining function ' + self.command)
+ return
+ # Trace.message('apply %s to %r'%(self.command, parameter.extracttext()))
+ # parameter.tree()
+ if not isinstance(parameter, FormulaConstant):
+ try:
+ extractor = ContainerExtractor(ContainerConfig.extracttext)
+ parameter = extractor.extract(parameter)[0]
+ except IndexError:
+ Trace.error('No base character found for "%s".' % self.command)
+ return
+ # Trace.message(' basechar: %r' % parameter.string)
+ # Insert combining character after the first character:
+ if parameter.string.startswith('\u2009'):
+ i = 2 # skip padding by SpacedCommand and FormulaConfig.modified
+ else:
+ i = 1
+ parameter.string = parameter.string[:i] + combining + parameter.string[i:]
+ # Use pre-composed characters if possible: \not{=} -> ≠, say.
+ parameter.string = unicodedata.normalize('NFC', parameter.string)
+
+ def parsesingleparameter(self, pos):
+ "Parse a parameter, or a single letter."
+ self.factory.clearskipped(pos)
+ if pos.finished():
+ return None
+ return self.parseparameter(pos)
+
+
+class OversetFunction(OneParamFunction):
+ "A function that decorates some bit of text with an overset."
+
+ commandmap = FormulaConfig.oversetfunctions
+
+ def parsebit(self, pos):
+ "Parse an overset-function"
+ symbol = self.translated
+ self.symbol = TaggedBit().constant(symbol, 'sup')
+ self.parameter = self.parseparameter(pos)
+ self.output = TaggedOutput().settag('span class="embellished"')
+ self.contents.insert(0, self.symbol)
+ self.parameter.output = TaggedOutput().settag('span class="base"')
+ self.simplifyifpossible()
+
+
+class UndersetFunction(OneParamFunction):
+ "A function that decorates some bit of text with an underset."
+
+ commandmap = FormulaConfig.undersetfunctions
+
+ def parsebit(self, pos):
+ "Parse an underset-function"
+ symbol = self.translated
+ self.symbol = TaggedBit().constant(symbol, 'sub')
+ self.parameter = self.parseparameter(pos)
+ self.output = TaggedOutput().settag('span class="embellished"')
+ self.contents.insert(0, self.symbol)
+ self.parameter.output = TaggedOutput().settag('span class="base"')
+ self.simplifyifpossible()
+
+
+class LimitCommand(EmptyCommand):
+ "A command which accepts limits above and below, in display mode."
+
+ commandmap = FormulaConfig.limitcommands
+
+ def parsebit(self, pos):
+ "Parse a limit command."
+ self.output = TaggedOutput().settag('span class="limits"')
+ symbol = self.translated
+ self.contents.append(TaggedBit().constant(symbol, 'span class="limit"'))
+
+
+class LimitPreviousCommand(LimitCommand):
+ "A command to limit the previous command."
+
+ commandmap = None
+
+ def parsebit(self, pos):
+ "Do nothing."
+ self.output = TaggedOutput().settag('span class="limits"')
+ self.factory.clearskipped(pos)
+
+ def __str__(self):
+ "Return a printable representation."
+ return 'Limit previous command'
+
+
+class LimitsProcessor(MathsProcessor):
+ "A processor for limits inside an element."
+
+ def process(self, contents, index):
+ "Process the limits for an element."
+ if Options.simplemath:
+ return
+ if self.checklimits(contents, index):
+ self.modifylimits(contents, index)
+ if self.checkscript(contents, index) and self.checkscript(contents, index + 1):
+ self.modifyscripts(contents, index)
+
+ def checklimits(self, contents, index):
+ "Check if the current position has a limits command."
+ # TODO: check for \limits macro
+ if not DocumentParameters.displaymode:
+ return False
+ if self.checkcommand(contents, index + 1, LimitPreviousCommand):
+ self.limitsahead(contents, index)
+ return False
+ if not isinstance(contents[index], LimitCommand):
+ return False
+ return self.checkscript(contents, index + 1)
+
+ def limitsahead(self, contents, index):
+ "Limit the current element based on the next."
+ contents[index + 1].add(contents[index].clone())
+ contents[index].output = EmptyOutput()
+
+ def modifylimits(self, contents, index):
+ "Modify a limits commands so that the limits appear above and below."
+ limited = contents[index]
+ subscript = self.getlimit(contents, index + 1)
+ if self.checkscript(contents, index + 1):
+ superscript = self.getlimit(contents, index + 1)
+ else:
+ superscript = TaggedBit().constant('\u2009', 'sup class="limit"')
+ # fix order if source is x^i
+ if subscript.command == '^':
+ superscript, subscript = subscript, superscript
+ limited.contents.append(subscript)
+ limited.contents.insert(0, superscript)
+
+ def getlimit(self, contents, index):
+ "Get the limit for a limits command."
+ limit = self.getscript(contents, index)
+ limit.output.tag = limit.output.tag.replace('script', 'limit')
+ return limit
+
+ def modifyscripts(self, contents, index):
+ "Modify the super- and subscript to appear vertically aligned."
+ subscript = self.getscript(contents, index)
+ # subscript removed so instead of index + 1 we get index again
+ superscript = self.getscript(contents, index)
+ # super-/subscript are reversed if source is x^i_j
+ if subscript.command == '^':
+ superscript, subscript = subscript, superscript
+ scripts = TaggedBit().complete([superscript, subscript], 'span class="scripts"')
+ contents.insert(index, scripts)
+
+ def checkscript(self, contents, index):
+ "Check if the current element is a sub- or superscript."
+ return self.checkcommand(contents, index, SymbolFunction)
+
+ def checkcommand(self, contents, index, type):
+ "Check for the given type as the current element."
+ if len(contents) <= index:
+ return False
+ return isinstance(contents[index], type)
+
+ def getscript(self, contents, index):
+ "Get the sub- or superscript."
+ bit = contents[index]
+ bit.output.tag += ' class="script"'
+ del contents[index]
+ return bit
+
+
+class BracketCommand(OneParamFunction):
+ "A command which defines a bracket."
+
+ commandmap = FormulaConfig.bracketcommands
+
+ def parsebit(self, pos):
+ "Parse the bracket."
+ OneParamFunction.parsebit(self, pos)
+
+ def create(self, direction, character):
+ "Create the bracket for the given character."
+ self.original = character
+ self.command = '\\' + direction
+ self.contents = [FormulaConstant(character)]
+ return self
+
+
+class BracketProcessor(MathsProcessor):
+ "A processor for bracket commands."
+
+ def process(self, contents, index):
+ "Convert the bracket using Unicode pieces, if possible."
+ if Options.simplemath:
+ return
+ if self.checkleft(contents, index):
+ return self.processleft(contents, index)
+
+ def processleft(self, contents, index):
+ "Process a left bracket."
+ rightindex = self.findright(contents, index + 1)
+ if not rightindex:
+ return
+ size = self.findmax(contents, index, rightindex)
+ self.resize(contents[index], size)
+ self.resize(contents[rightindex], size)
+
+ def checkleft(self, contents, index):
+ "Check if the command at the given index is left."
+ return self.checkdirection(contents[index], '\\left')
+
+ def checkright(self, contents, index):
+ "Check if the command at the given index is right."
+ return self.checkdirection(contents[index], '\\right')
+
+ def checkdirection(self, bit, command):
+ "Check if the given bit is the desired bracket command."
+ if not isinstance(bit, BracketCommand):
+ return False
+ return bit.command == command
+
+ def findright(self, contents, index):
+ "Find the right bracket starting at the given index, or 0."
+ depth = 1
+ while index < len(contents):
+ if self.checkleft(contents, index):
+ depth += 1
+ if self.checkright(contents, index):
+ depth -= 1
+ if depth == 0:
+ return index
+ index += 1
+ return None
+
+ def findmax(self, contents, leftindex, rightindex):
+ "Find the max size of the contents between the two given indices."
+ sliced = contents[leftindex:rightindex]
+ return max(element.size for element in sliced)
+
+ def resize(self, command, size):
+ "Resize a bracket command to the given size."
+ character = command.extracttext()
+ alignment = command.command.replace('\\', '')
+ bracket = BigBracket(size, character, alignment)
+ command.output = ContentsOutput()
+ command.contents = bracket.getcontents()
+
+
+FormulaCommand.types += [OversetFunction, UndersetFunction,
+ CombiningFunction, LimitCommand, BracketCommand]
+
+FormulaProcessor.processors += [
+ LimitsProcessor(), BracketProcessor(),
+]
+
+
+class ParameterDefinition:
+ "The definition of a parameter in a hybrid function."
+ "[] parameters are optional, {} parameters are mandatory."
+ "Each parameter has a one-character name, like {$1} or {$p}."
+ "A parameter that ends in ! like {$p!} is a literal."
+ "Example: [$1]{$p!} reads an optional parameter $1 and a literal mandatory parameter p."
+
+ parambrackets = [('[', ']'), ('{', '}')]
+
+ def __init__(self):
+ self.name = None
+ self.literal = False
+ self.optional = False
+ self.value = None
+ self.literalvalue = None
+
+ def parse(self, pos):
+ "Parse a parameter definition: [$0], {$x}, {$1!}..."
+ for (opening, closing) in ParameterDefinition.parambrackets:
+ if pos.checkskip(opening):
+ if opening == '[':
+ self.optional = True
+ if not pos.checkskip('$'):
+ Trace.error('Wrong parameter name, did you mean $' + pos.current() + '?')
+ return None
+ self.name = pos.skipcurrent()
+ if pos.checkskip('!'):
+ self.literal = True
+ if not pos.checkskip(closing):
+ Trace.error('Wrong parameter closing ' + pos.skipcurrent())
+ return None
+ return self
+ Trace.error('Wrong character in parameter template: ' + pos.skipcurrent())
+ return None
+
+ def read(self, pos, function):
+ "Read the parameter itself using the definition."
+ if self.literal:
+ if self.optional:
+ self.literalvalue = function.parsesquareliteral(pos)
+ else:
+ self.literalvalue = function.parseliteral(pos)
+ if self.literalvalue:
+ self.value = FormulaConstant(self.literalvalue)
+ elif self.optional:
+ self.value = function.parsesquare(pos)
+ else:
+ self.value = function.parseparameter(pos)
+
+ def __str__(self):
+ "Return a printable representation."
+ result = 'param ' + self.name
+ if self.value:
+ result += ': ' + str(self.value)
+ else:
+ result += ' (empty)'
+ return result
+
+
+class ParameterFunction(CommandBit):
+ "A function with a variable number of parameters defined in a template."
+ "The parameters are defined as a parameter definition."
+
+ def readparams(self, readtemplate, pos):
+ "Read the params according to the template."
+ self.params = {}
+ for paramdef in self.paramdefs(readtemplate):
+ paramdef.read(pos, self)
+ self.params['$' + paramdef.name] = paramdef
+
+ def paramdefs(self, readtemplate):
+ "Read each param definition in the template"
+ pos = TextPosition(readtemplate)
+ while not pos.finished():
+ paramdef = ParameterDefinition().parse(pos)
+ if paramdef:
+ yield paramdef
+
+ def getparam(self, name):
+ "Get a parameter as parsed."
+ if name not in self.params:
+ return None
+ return self.params[name]
+
+ def getvalue(self, name):
+ "Get the value of a parameter."
+ return self.getparam(name).value
+
+ def getliteralvalue(self, name):
+ "Get the literal value of a parameter."
+ param = self.getparam(name)
+ if not param or not param.literalvalue:
+ return None
+ return param.literalvalue
+
+
+class HybridFunction(ParameterFunction):
+ """
+ A parameter function where the output is also defined using a template.
+ The template can use a number of functions; each function has an associated
+ tag.
+ Example: [f0{$1},span class="fbox"] defines a function f0 which corresponds
+ to a span of class fbox, yielding <span class="fbox">$1</span>.
+ Literal parameters can be used in tags definitions:
+ [f0{$1},span style="color: $p;"]
+ yields <span style="color: $p;">$1</span>, where $p is a literal parameter.
+ Sizes can be specified in hybridsizes, e.g. adding parameter sizes. By
+ default the resulting size is the max of all arguments. Sizes are used
+ to generate the right parameters.
+ A function followed by a single / is output as a self-closing XHTML tag:
+ [f0/,hr]
+ will generate <hr/>.
+ """
+
+ commandmap = FormulaConfig.hybridfunctions
+
+ def parsebit(self, pos):
+ "Parse a function with [] and {} parameters"
+ readtemplate = self.translated[0]
+ writetemplate = self.translated[1]
+ self.readparams(readtemplate, pos)
+ self.contents = self.writeparams(writetemplate)
+ self.computehybridsize()
+
+ def writeparams(self, writetemplate):
+ "Write all params according to the template"
+ return self.writepos(TextPosition(writetemplate))
+
+ def writepos(self, pos):
+ "Write all params as read in the parse position."
+ result = []
+ while not pos.finished():
+ if pos.checkskip('$'):
+ param = self.writeparam(pos)
+ if param:
+ result.append(param)
+ elif pos.checkskip('f'):
+ function = self.writefunction(pos)
+ if function:
+ function.type = None
+ result.append(function)
+ elif pos.checkskip('('):
+ result.append(self.writebracket('left', '('))
+ elif pos.checkskip(')'):
+ result.append(self.writebracket('right', ')'))
+ else:
+ result.append(FormulaConstant(pos.skipcurrent()))
+ return result
+
+ def writeparam(self, pos):
+ "Write a single param of the form $0, $x..."
+ name = '$' + pos.skipcurrent()
+ if name not in self.params:
+ Trace.error('Unknown parameter ' + name)
+ return None
+ if not self.params[name]:
+ return None
+ if pos.checkskip('.'):
+ self.params[name].value.type = pos.globalpha()
+ return self.params[name].value
+
+ def writefunction(self, pos):
+ "Write a single function f0,...,fn."
+ tag = self.readtag(pos)
+ if not tag:
+ return None
+ if pos.checkskip('/'):
+ # self-closing XHTML tag, such as <hr/>
+ return TaggedBit().selfcomplete(tag)
+ if not pos.checkskip('{'):
+ Trace.error('Function should be defined in {}')
+ return None
+ pos.pushending('}')
+ contents = self.writepos(pos)
+ pos.popending()
+ if len(contents) == 0:
+ return None
+ return TaggedBit().complete(contents, tag)
+
+ def readtag(self, pos):
+ "Get the tag corresponding to the given index. Does parameter substitution."
+ if not pos.current().isdigit():
+ Trace.error('Function should be f0,...,f9: f' + pos.current())
+ return None
+ index = int(pos.skipcurrent())
+ if 2 + index > len(self.translated):
+ Trace.error('Function f' + str(index) + ' is not defined')
+ return None
+ tag = self.translated[2 + index]
+ if '$' not in tag:
+ return tag
+ for variable in self.params:
+ if variable in tag:
+ param = self.params[variable]
+ if not param.literal:
+ Trace.error('Parameters in tag ' + tag + ' should be literal: {' + variable + '!}')
+ continue
+ if param.literalvalue:
+ value = param.literalvalue
+ else:
+ value = ''
+ tag = tag.replace(variable, value)
+ return tag
+
+ def writebracket(self, direction, character):
+ "Return a new bracket looking at the given direction."
+ return self.factory.create(BracketCommand).create(direction, character)
+
+ def computehybridsize(self):
+ "Compute the size of the hybrid function."
+ if self.command not in HybridSize.configsizes:
+ self.computesize()
+ return
+ self.size = HybridSize().getsize(self)
+ # set the size in all elements at first level
+ for element in self.contents:
+ element.size = self.size
+
+
+class HybridSize:
+ "The size associated with a hybrid function."
+
+ configsizes = FormulaConfig.hybridsizes
+
+ def getsize(self, function):
+ "Read the size for a function and parse it."
+ sizestring = self.configsizes[function.command]
+ for name in function.params:
+ if name in sizestring:
+ size = function.params[name].value.computesize()
+ sizestring = sizestring.replace(name, str(size))
+ if '$' in sizestring:
+ Trace.error('Unconverted variable in hybrid size: ' + sizestring)
+ return 1
+ return eval(sizestring)
+
+
+FormulaCommand.types += [HybridFunction]
+
+
+def math2html(formula):
+ "Convert some TeX math to HTML."
+ factory = FormulaFactory()
+ whole = factory.parseformula(formula)
+ FormulaProcessor().process(whole)
+ whole.process()
+ return ''.join(whole.gethtml())
+
+
+def main():
+ "Main function, called if invoked from the command line"
+ args = sys.argv
+ Options().parseoptions(args)
+ if len(args) != 1:
+ Trace.error('Usage: math2html.py escaped_string')
+ exit()
+ result = math2html(args[0])
+ Trace.message(result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/mathalphabet2unichar.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/mathalphabet2unichar.py
new file mode 100644
index 00000000..876cea47
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/mathalphabet2unichar.py
@@ -0,0 +1,892 @@
+#!/usr/bin/env python3
+#
+# LaTeX math to Unicode symbols translation dictionaries for
+# the content of math alphabet commands (\mathtt, \mathbf, ...).
+# Generated with ``write_mathalphabet2unichar.py`` from the data in
+# http://milde.users.sourceforge.net/LUCR/Math/
+#
+# :Copyright: © 2024 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.
+#
+# __ https://opensource.org/licenses/BSD-2-Clause
+
+mathbb = {
+ '0': '\U0001d7d8', # 𝟘 MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO
+ '1': '\U0001d7d9', # 𝟙 MATHEMATICAL DOUBLE-STRUCK DIGIT ONE
+ '2': '\U0001d7da', # 𝟚 MATHEMATICAL DOUBLE-STRUCK DIGIT TWO
+ '3': '\U0001d7db', # 𝟛 MATHEMATICAL DOUBLE-STRUCK DIGIT THREE
+ '4': '\U0001d7dc', # 𝟜 MATHEMATICAL DOUBLE-STRUCK DIGIT FOUR
+ '5': '\U0001d7dd', # 𝟝 MATHEMATICAL DOUBLE-STRUCK DIGIT FIVE
+ '6': '\U0001d7de', # 𝟞 MATHEMATICAL DOUBLE-STRUCK DIGIT SIX
+ '7': '\U0001d7df', # 𝟟 MATHEMATICAL DOUBLE-STRUCK DIGIT SEVEN
+ '8': '\U0001d7e0', # 𝟠 MATHEMATICAL DOUBLE-STRUCK DIGIT EIGHT
+ '9': '\U0001d7e1', # 𝟡 MATHEMATICAL DOUBLE-STRUCK DIGIT NINE
+ 'A': '\U0001d538', # 𝔸 MATHEMATICAL DOUBLE-STRUCK CAPITAL A
+ 'B': '\U0001d539', # 𝔹 MATHEMATICAL DOUBLE-STRUCK CAPITAL B
+ 'C': '\u2102', # ℂ DOUBLE-STRUCK CAPITAL C
+ 'D': '\U0001d53b', # 𝔻 MATHEMATICAL DOUBLE-STRUCK CAPITAL D
+ 'E': '\U0001d53c', # 𝔼 MATHEMATICAL DOUBLE-STRUCK CAPITAL E
+ 'F': '\U0001d53d', # 𝔽 MATHEMATICAL DOUBLE-STRUCK CAPITAL F
+ 'G': '\U0001d53e', # 𝔾 MATHEMATICAL DOUBLE-STRUCK CAPITAL G
+ 'H': '\u210d', # ℍ DOUBLE-STRUCK CAPITAL H
+ 'I': '\U0001d540', # 𝕀 MATHEMATICAL DOUBLE-STRUCK CAPITAL I
+ 'J': '\U0001d541', # 𝕁 MATHEMATICAL DOUBLE-STRUCK CAPITAL J
+ 'K': '\U0001d542', # 𝕂 MATHEMATICAL DOUBLE-STRUCK CAPITAL K
+ 'L': '\U0001d543', # 𝕃 MATHEMATICAL DOUBLE-STRUCK CAPITAL L
+ 'M': '\U0001d544', # 𝕄 MATHEMATICAL DOUBLE-STRUCK CAPITAL M
+ 'N': '\u2115', # ℕ DOUBLE-STRUCK CAPITAL N
+ 'O': '\U0001d546', # 𝕆 MATHEMATICAL DOUBLE-STRUCK CAPITAL O
+ 'P': '\u2119', # ℙ DOUBLE-STRUCK CAPITAL P
+ 'Q': '\u211a', # ℚ DOUBLE-STRUCK CAPITAL Q
+ 'R': '\u211d', # ℝ DOUBLE-STRUCK CAPITAL R
+ 'S': '\U0001d54a', # 𝕊 MATHEMATICAL DOUBLE-STRUCK CAPITAL S
+ 'T': '\U0001d54b', # 𝕋 MATHEMATICAL DOUBLE-STRUCK CAPITAL T
+ 'U': '\U0001d54c', # 𝕌 MATHEMATICAL DOUBLE-STRUCK CAPITAL U
+ 'V': '\U0001d54d', # 𝕍 MATHEMATICAL DOUBLE-STRUCK CAPITAL V
+ 'W': '\U0001d54e', # 𝕎 MATHEMATICAL DOUBLE-STRUCK CAPITAL W
+ 'X': '\U0001d54f', # 𝕏 MATHEMATICAL DOUBLE-STRUCK CAPITAL X
+ 'Y': '\U0001d550', # 𝕐 MATHEMATICAL DOUBLE-STRUCK CAPITAL Y
+ 'Z': '\u2124', # ℤ DOUBLE-STRUCK CAPITAL Z
+ 'a': '\U0001d552', # 𝕒 MATHEMATICAL DOUBLE-STRUCK SMALL A
+ 'b': '\U0001d553', # 𝕓 MATHEMATICAL DOUBLE-STRUCK SMALL B
+ 'c': '\U0001d554', # 𝕔 MATHEMATICAL DOUBLE-STRUCK SMALL C
+ 'd': '\U0001d555', # 𝕕 MATHEMATICAL DOUBLE-STRUCK SMALL D
+ 'e': '\U0001d556', # 𝕖 MATHEMATICAL DOUBLE-STRUCK SMALL E
+ 'f': '\U0001d557', # 𝕗 MATHEMATICAL DOUBLE-STRUCK SMALL F
+ 'g': '\U0001d558', # 𝕘 MATHEMATICAL DOUBLE-STRUCK SMALL G
+ 'h': '\U0001d559', # 𝕙 MATHEMATICAL DOUBLE-STRUCK SMALL H
+ 'i': '\U0001d55a', # 𝕚 MATHEMATICAL DOUBLE-STRUCK SMALL I
+ 'j': '\U0001d55b', # 𝕛 MATHEMATICAL DOUBLE-STRUCK SMALL J
+ 'k': '\U0001d55c', # 𝕜 MATHEMATICAL DOUBLE-STRUCK SMALL K
+ 'l': '\U0001d55d', # 𝕝 MATHEMATICAL DOUBLE-STRUCK SMALL L
+ 'm': '\U0001d55e', # 𝕞 MATHEMATICAL DOUBLE-STRUCK SMALL M
+ 'n': '\U0001d55f', # 𝕟 MATHEMATICAL DOUBLE-STRUCK SMALL N
+ 'o': '\U0001d560', # 𝕠 MATHEMATICAL DOUBLE-STRUCK SMALL O
+ 'p': '\U0001d561', # 𝕡 MATHEMATICAL DOUBLE-STRUCK SMALL P
+ 'q': '\U0001d562', # 𝕢 MATHEMATICAL DOUBLE-STRUCK SMALL Q
+ 'r': '\U0001d563', # 𝕣 MATHEMATICAL DOUBLE-STRUCK SMALL R
+ 's': '\U0001d564', # 𝕤 MATHEMATICAL DOUBLE-STRUCK SMALL S
+ 't': '\U0001d565', # 𝕥 MATHEMATICAL DOUBLE-STRUCK SMALL T
+ 'u': '\U0001d566', # 𝕦 MATHEMATICAL DOUBLE-STRUCK SMALL U
+ 'v': '\U0001d567', # 𝕧 MATHEMATICAL DOUBLE-STRUCK SMALL V
+ 'w': '\U0001d568', # 𝕨 MATHEMATICAL DOUBLE-STRUCK SMALL W
+ 'x': '\U0001d569', # 𝕩 MATHEMATICAL DOUBLE-STRUCK SMALL X
+ 'y': '\U0001d56a', # 𝕪 MATHEMATICAL DOUBLE-STRUCK SMALL Y
+ 'z': '\U0001d56b', # 𝕫 MATHEMATICAL DOUBLE-STRUCK SMALL Z
+ 'Γ': '\u213e', # ℾ DOUBLE-STRUCK CAPITAL GAMMA
+ 'Π': '\u213f', # ℿ DOUBLE-STRUCK CAPITAL PI
+ 'Σ': '\u2140', # ⅀ DOUBLE-STRUCK N-ARY SUMMATION
+ 'γ': '\u213d', # ℽ DOUBLE-STRUCK SMALL GAMMA
+ 'π': '\u213c', # ℼ DOUBLE-STRUCK SMALL PI
+ }
+
+mathbf = {
+ '0': '\U0001d7ce', # 𝟎 MATHEMATICAL BOLD DIGIT ZERO
+ '1': '\U0001d7cf', # 𝟏 MATHEMATICAL BOLD DIGIT ONE
+ '2': '\U0001d7d0', # 𝟐 MATHEMATICAL BOLD DIGIT TWO
+ '3': '\U0001d7d1', # 𝟑 MATHEMATICAL BOLD DIGIT THREE
+ '4': '\U0001d7d2', # 𝟒 MATHEMATICAL BOLD DIGIT FOUR
+ '5': '\U0001d7d3', # 𝟓 MATHEMATICAL BOLD DIGIT FIVE
+ '6': '\U0001d7d4', # 𝟔 MATHEMATICAL BOLD DIGIT SIX
+ '7': '\U0001d7d5', # 𝟕 MATHEMATICAL BOLD DIGIT SEVEN
+ '8': '\U0001d7d6', # 𝟖 MATHEMATICAL BOLD DIGIT EIGHT
+ '9': '\U0001d7d7', # 𝟗 MATHEMATICAL BOLD DIGIT NINE
+ 'A': '\U0001d400', # 𝐀 MATHEMATICAL BOLD CAPITAL A
+ 'B': '\U0001d401', # 𝐁 MATHEMATICAL BOLD CAPITAL B
+ 'C': '\U0001d402', # 𝐂 MATHEMATICAL BOLD CAPITAL C
+ 'D': '\U0001d403', # 𝐃 MATHEMATICAL BOLD CAPITAL D
+ 'E': '\U0001d404', # 𝐄 MATHEMATICAL BOLD CAPITAL E
+ 'F': '\U0001d405', # 𝐅 MATHEMATICAL BOLD CAPITAL F
+ 'G': '\U0001d406', # 𝐆 MATHEMATICAL BOLD CAPITAL G
+ 'H': '\U0001d407', # 𝐇 MATHEMATICAL BOLD CAPITAL H
+ 'I': '\U0001d408', # 𝐈 MATHEMATICAL BOLD CAPITAL I
+ 'J': '\U0001d409', # 𝐉 MATHEMATICAL BOLD CAPITAL J
+ 'K': '\U0001d40a', # 𝐊 MATHEMATICAL BOLD CAPITAL K
+ 'L': '\U0001d40b', # 𝐋 MATHEMATICAL BOLD CAPITAL L
+ 'M': '\U0001d40c', # 𝐌 MATHEMATICAL BOLD CAPITAL M
+ 'N': '\U0001d40d', # 𝐍 MATHEMATICAL BOLD CAPITAL N
+ 'O': '\U0001d40e', # 𝐎 MATHEMATICAL BOLD CAPITAL O
+ 'P': '\U0001d40f', # 𝐏 MATHEMATICAL BOLD CAPITAL P
+ 'Q': '\U0001d410', # 𝐐 MATHEMATICAL BOLD CAPITAL Q
+ 'R': '\U0001d411', # 𝐑 MATHEMATICAL BOLD CAPITAL R
+ 'S': '\U0001d412', # 𝐒 MATHEMATICAL BOLD CAPITAL S
+ 'T': '\U0001d413', # 𝐓 MATHEMATICAL BOLD CAPITAL T
+ 'U': '\U0001d414', # 𝐔 MATHEMATICAL BOLD CAPITAL U
+ 'V': '\U0001d415', # 𝐕 MATHEMATICAL BOLD CAPITAL V
+ 'W': '\U0001d416', # 𝐖 MATHEMATICAL BOLD CAPITAL W
+ 'X': '\U0001d417', # 𝐗 MATHEMATICAL BOLD CAPITAL X
+ 'Y': '\U0001d418', # 𝐘 MATHEMATICAL BOLD CAPITAL Y
+ 'Z': '\U0001d419', # 𝐙 MATHEMATICAL BOLD CAPITAL Z
+ 'a': '\U0001d41a', # 𝐚 MATHEMATICAL BOLD SMALL A
+ 'b': '\U0001d41b', # 𝐛 MATHEMATICAL BOLD SMALL B
+ 'c': '\U0001d41c', # 𝐜 MATHEMATICAL BOLD SMALL C
+ 'd': '\U0001d41d', # 𝐝 MATHEMATICAL BOLD SMALL D
+ 'e': '\U0001d41e', # 𝐞 MATHEMATICAL BOLD SMALL E
+ 'f': '\U0001d41f', # 𝐟 MATHEMATICAL BOLD SMALL F
+ 'g': '\U0001d420', # 𝐠 MATHEMATICAL BOLD SMALL G
+ 'h': '\U0001d421', # 𝐡 MATHEMATICAL BOLD SMALL H
+ 'i': '\U0001d422', # 𝐢 MATHEMATICAL BOLD SMALL I
+ 'j': '\U0001d423', # 𝐣 MATHEMATICAL BOLD SMALL J
+ 'k': '\U0001d424', # 𝐤 MATHEMATICAL BOLD SMALL K
+ 'l': '\U0001d425', # 𝐥 MATHEMATICAL BOLD SMALL L
+ 'm': '\U0001d426', # 𝐦 MATHEMATICAL BOLD SMALL M
+ 'n': '\U0001d427', # 𝐧 MATHEMATICAL BOLD SMALL N
+ 'o': '\U0001d428', # 𝐨 MATHEMATICAL BOLD SMALL O
+ 'p': '\U0001d429', # 𝐩 MATHEMATICAL BOLD SMALL P
+ 'q': '\U0001d42a', # 𝐪 MATHEMATICAL BOLD SMALL Q
+ 'r': '\U0001d42b', # 𝐫 MATHEMATICAL BOLD SMALL R
+ 's': '\U0001d42c', # 𝐬 MATHEMATICAL BOLD SMALL S
+ 't': '\U0001d42d', # 𝐭 MATHEMATICAL BOLD SMALL T
+ 'u': '\U0001d42e', # 𝐮 MATHEMATICAL BOLD SMALL U
+ 'v': '\U0001d42f', # 𝐯 MATHEMATICAL BOLD SMALL V
+ 'w': '\U0001d430', # 𝐰 MATHEMATICAL BOLD SMALL W
+ 'x': '\U0001d431', # 𝐱 MATHEMATICAL BOLD SMALL X
+ 'y': '\U0001d432', # 𝐲 MATHEMATICAL BOLD SMALL Y
+ 'z': '\U0001d433', # 𝐳 MATHEMATICAL BOLD SMALL Z
+ 'Γ': '\U0001d6aa', # 𝚪 MATHEMATICAL BOLD CAPITAL GAMMA
+ 'Δ': '\U0001d6ab', # 𝚫 MATHEMATICAL BOLD CAPITAL DELTA
+ 'Θ': '\U0001d6af', # 𝚯 MATHEMATICAL BOLD CAPITAL THETA
+ 'Λ': '\U0001d6b2', # 𝚲 MATHEMATICAL BOLD CAPITAL LAMDA
+ 'Ξ': '\U0001d6b5', # 𝚵 MATHEMATICAL BOLD CAPITAL XI
+ 'Π': '\U0001d6b7', # 𝚷 MATHEMATICAL BOLD CAPITAL PI
+ 'Σ': '\U0001d6ba', # 𝚺 MATHEMATICAL BOLD CAPITAL SIGMA
+ 'Υ': '\U0001d6bc', # 𝚼 MATHEMATICAL BOLD CAPITAL UPSILON
+ 'Φ': '\U0001d6bd', # 𝚽 MATHEMATICAL BOLD CAPITAL PHI
+ 'Ψ': '\U0001d6bf', # 𝚿 MATHEMATICAL BOLD CAPITAL PSI
+ 'Ω': '\U0001d6c0', # 𝛀 MATHEMATICAL BOLD CAPITAL OMEGA
+ 'α': '\U0001d6c2', # 𝛂 MATHEMATICAL BOLD SMALL ALPHA
+ 'β': '\U0001d6c3', # 𝛃 MATHEMATICAL BOLD SMALL BETA
+ 'γ': '\U0001d6c4', # 𝛄 MATHEMATICAL BOLD SMALL GAMMA
+ 'δ': '\U0001d6c5', # 𝛅 MATHEMATICAL BOLD SMALL DELTA
+ 'ε': '\U0001d6c6', # 𝛆 MATHEMATICAL BOLD SMALL EPSILON
+ 'ζ': '\U0001d6c7', # 𝛇 MATHEMATICAL BOLD SMALL ZETA
+ 'η': '\U0001d6c8', # 𝛈 MATHEMATICAL BOLD SMALL ETA
+ 'θ': '\U0001d6c9', # 𝛉 MATHEMATICAL BOLD SMALL THETA
+ 'ι': '\U0001d6ca', # 𝛊 MATHEMATICAL BOLD SMALL IOTA
+ 'κ': '\U0001d6cb', # 𝛋 MATHEMATICAL BOLD SMALL KAPPA
+ 'λ': '\U0001d6cc', # 𝛌 MATHEMATICAL BOLD SMALL LAMDA
+ 'μ': '\U0001d6cd', # 𝛍 MATHEMATICAL BOLD SMALL MU
+ 'ν': '\U0001d6ce', # 𝛎 MATHEMATICAL BOLD SMALL NU
+ 'ξ': '\U0001d6cf', # 𝛏 MATHEMATICAL BOLD SMALL XI
+ 'π': '\U0001d6d1', # 𝛑 MATHEMATICAL BOLD SMALL PI
+ 'ρ': '\U0001d6d2', # 𝛒 MATHEMATICAL BOLD SMALL RHO
+ 'ς': '\U0001d6d3', # 𝛓 MATHEMATICAL BOLD SMALL FINAL SIGMA
+ 'σ': '\U0001d6d4', # 𝛔 MATHEMATICAL BOLD SMALL SIGMA
+ 'τ': '\U0001d6d5', # 𝛕 MATHEMATICAL BOLD SMALL TAU
+ 'υ': '\U0001d6d6', # 𝛖 MATHEMATICAL BOLD SMALL UPSILON
+ 'φ': '\U0001d6d7', # 𝛗 MATHEMATICAL BOLD SMALL PHI
+ 'χ': '\U0001d6d8', # 𝛘 MATHEMATICAL BOLD SMALL CHI
+ 'ψ': '\U0001d6d9', # 𝛙 MATHEMATICAL BOLD SMALL PSI
+ 'ω': '\U0001d6da', # 𝛚 MATHEMATICAL BOLD SMALL OMEGA
+ 'ϑ': '\U0001d6dd', # 𝛝 MATHEMATICAL BOLD THETA SYMBOL
+ 'ϕ': '\U0001d6df', # 𝛟 MATHEMATICAL BOLD PHI SYMBOL
+ 'ϖ': '\U0001d6e1', # 𝛡 MATHEMATICAL BOLD PI SYMBOL
+ 'Ϝ': '\U0001d7ca', # 𝟊 MATHEMATICAL BOLD CAPITAL DIGAMMA
+ 'ϝ': '\U0001d7cb', # 𝟋 MATHEMATICAL BOLD SMALL DIGAMMA
+ 'ϰ': '\U0001d6de', # 𝛞 MATHEMATICAL BOLD KAPPA SYMBOL
+ 'ϱ': '\U0001d6e0', # 𝛠 MATHEMATICAL BOLD RHO SYMBOL
+ 'ϵ': '\U0001d6dc', # 𝛜 MATHEMATICAL BOLD EPSILON SYMBOL
+ '∂': '\U0001d6db', # 𝛛 MATHEMATICAL BOLD PARTIAL DIFFERENTIAL
+ '∇': '\U0001d6c1', # 𝛁 MATHEMATICAL BOLD NABLA
+ }
+
+mathbfit = {
+ 'A': '\U0001d468', # 𝑨 MATHEMATICAL BOLD ITALIC CAPITAL A
+ 'B': '\U0001d469', # 𝑩 MATHEMATICAL BOLD ITALIC CAPITAL B
+ 'C': '\U0001d46a', # 𝑪 MATHEMATICAL BOLD ITALIC CAPITAL C
+ 'D': '\U0001d46b', # 𝑫 MATHEMATICAL BOLD ITALIC CAPITAL D
+ 'E': '\U0001d46c', # 𝑬 MATHEMATICAL BOLD ITALIC CAPITAL E
+ 'F': '\U0001d46d', # 𝑭 MATHEMATICAL BOLD ITALIC CAPITAL F
+ 'G': '\U0001d46e', # 𝑮 MATHEMATICAL BOLD ITALIC CAPITAL G
+ 'H': '\U0001d46f', # 𝑯 MATHEMATICAL BOLD ITALIC CAPITAL H
+ 'I': '\U0001d470', # 𝑰 MATHEMATICAL BOLD ITALIC CAPITAL I
+ 'J': '\U0001d471', # 𝑱 MATHEMATICAL BOLD ITALIC CAPITAL J
+ 'K': '\U0001d472', # 𝑲 MATHEMATICAL BOLD ITALIC CAPITAL K
+ 'L': '\U0001d473', # 𝑳 MATHEMATICAL BOLD ITALIC CAPITAL L
+ 'M': '\U0001d474', # 𝑴 MATHEMATICAL BOLD ITALIC CAPITAL M
+ 'N': '\U0001d475', # 𝑵 MATHEMATICAL BOLD ITALIC CAPITAL N
+ 'O': '\U0001d476', # 𝑶 MATHEMATICAL BOLD ITALIC CAPITAL O
+ 'P': '\U0001d477', # 𝑷 MATHEMATICAL BOLD ITALIC CAPITAL P
+ 'Q': '\U0001d478', # 𝑸 MATHEMATICAL BOLD ITALIC CAPITAL Q
+ 'R': '\U0001d479', # 𝑹 MATHEMATICAL BOLD ITALIC CAPITAL R
+ 'S': '\U0001d47a', # 𝑺 MATHEMATICAL BOLD ITALIC CAPITAL S
+ 'T': '\U0001d47b', # 𝑻 MATHEMATICAL BOLD ITALIC CAPITAL T
+ 'U': '\U0001d47c', # 𝑼 MATHEMATICAL BOLD ITALIC CAPITAL U
+ 'V': '\U0001d47d', # 𝑽 MATHEMATICAL BOLD ITALIC CAPITAL V
+ 'W': '\U0001d47e', # 𝑾 MATHEMATICAL BOLD ITALIC CAPITAL W
+ 'X': '\U0001d47f', # 𝑿 MATHEMATICAL BOLD ITALIC CAPITAL X
+ 'Y': '\U0001d480', # 𝒀 MATHEMATICAL BOLD ITALIC CAPITAL Y
+ 'Z': '\U0001d481', # 𝒁 MATHEMATICAL BOLD ITALIC CAPITAL Z
+ 'a': '\U0001d482', # 𝒂 MATHEMATICAL BOLD ITALIC SMALL A
+ 'b': '\U0001d483', # 𝒃 MATHEMATICAL BOLD ITALIC SMALL B
+ 'c': '\U0001d484', # 𝒄 MATHEMATICAL BOLD ITALIC SMALL C
+ 'd': '\U0001d485', # 𝒅 MATHEMATICAL BOLD ITALIC SMALL D
+ 'e': '\U0001d486', # 𝒆 MATHEMATICAL BOLD ITALIC SMALL E
+ 'f': '\U0001d487', # 𝒇 MATHEMATICAL BOLD ITALIC SMALL F
+ 'g': '\U0001d488', # 𝒈 MATHEMATICAL BOLD ITALIC SMALL G
+ 'h': '\U0001d489', # 𝒉 MATHEMATICAL BOLD ITALIC SMALL H
+ 'i': '\U0001d48a', # 𝒊 MATHEMATICAL BOLD ITALIC SMALL I
+ 'j': '\U0001d48b', # 𝒋 MATHEMATICAL BOLD ITALIC SMALL J
+ 'k': '\U0001d48c', # 𝒌 MATHEMATICAL BOLD ITALIC SMALL K
+ 'l': '\U0001d48d', # 𝒍 MATHEMATICAL BOLD ITALIC SMALL L
+ 'm': '\U0001d48e', # 𝒎 MATHEMATICAL BOLD ITALIC SMALL M
+ 'n': '\U0001d48f', # 𝒏 MATHEMATICAL BOLD ITALIC SMALL N
+ 'o': '\U0001d490', # 𝒐 MATHEMATICAL BOLD ITALIC SMALL O
+ 'p': '\U0001d491', # 𝒑 MATHEMATICAL BOLD ITALIC SMALL P
+ 'q': '\U0001d492', # 𝒒 MATHEMATICAL BOLD ITALIC SMALL Q
+ 'r': '\U0001d493', # 𝒓 MATHEMATICAL BOLD ITALIC SMALL R
+ 's': '\U0001d494', # 𝒔 MATHEMATICAL BOLD ITALIC SMALL S
+ 't': '\U0001d495', # 𝒕 MATHEMATICAL BOLD ITALIC SMALL T
+ 'u': '\U0001d496', # 𝒖 MATHEMATICAL BOLD ITALIC SMALL U
+ 'v': '\U0001d497', # 𝒗 MATHEMATICAL BOLD ITALIC SMALL V
+ 'w': '\U0001d498', # 𝒘 MATHEMATICAL BOLD ITALIC SMALL W
+ 'x': '\U0001d499', # 𝒙 MATHEMATICAL BOLD ITALIC SMALL X
+ 'y': '\U0001d49a', # 𝒚 MATHEMATICAL BOLD ITALIC SMALL Y
+ 'z': '\U0001d49b', # 𝒛 MATHEMATICAL BOLD ITALIC SMALL Z
+ 'Γ': '\U0001d71e', # 𝜞 MATHEMATICAL BOLD ITALIC CAPITAL GAMMA
+ 'Δ': '\U0001d71f', # 𝜟 MATHEMATICAL BOLD ITALIC CAPITAL DELTA
+ 'Θ': '\U0001d723', # 𝜣 MATHEMATICAL BOLD ITALIC CAPITAL THETA
+ 'Λ': '\U0001d726', # 𝜦 MATHEMATICAL BOLD ITALIC CAPITAL LAMDA
+ 'Ξ': '\U0001d729', # 𝜩 MATHEMATICAL BOLD ITALIC CAPITAL XI
+ 'Π': '\U0001d72b', # 𝜫 MATHEMATICAL BOLD ITALIC CAPITAL PI
+ 'Σ': '\U0001d72e', # 𝜮 MATHEMATICAL BOLD ITALIC CAPITAL SIGMA
+ 'Υ': '\U0001d730', # 𝜰 MATHEMATICAL BOLD ITALIC CAPITAL UPSILON
+ 'Φ': '\U0001d731', # 𝜱 MATHEMATICAL BOLD ITALIC CAPITAL PHI
+ 'Ψ': '\U0001d733', # 𝜳 MATHEMATICAL BOLD ITALIC CAPITAL PSI
+ 'Ω': '\U0001d734', # 𝜴 MATHEMATICAL BOLD ITALIC CAPITAL OMEGA
+ 'α': '\U0001d736', # 𝜶 MATHEMATICAL BOLD ITALIC SMALL ALPHA
+ 'β': '\U0001d737', # 𝜷 MATHEMATICAL BOLD ITALIC SMALL BETA
+ 'γ': '\U0001d738', # 𝜸 MATHEMATICAL BOLD ITALIC SMALL GAMMA
+ 'δ': '\U0001d739', # 𝜹 MATHEMATICAL BOLD ITALIC SMALL DELTA
+ 'ε': '\U0001d73a', # 𝜺 MATHEMATICAL BOLD ITALIC SMALL EPSILON
+ 'ζ': '\U0001d73b', # 𝜻 MATHEMATICAL BOLD ITALIC SMALL ZETA
+ 'η': '\U0001d73c', # 𝜼 MATHEMATICAL BOLD ITALIC SMALL ETA
+ 'θ': '\U0001d73d', # 𝜽 MATHEMATICAL BOLD ITALIC SMALL THETA
+ 'ι': '\U0001d73e', # 𝜾 MATHEMATICAL BOLD ITALIC SMALL IOTA
+ 'κ': '\U0001d73f', # 𝜿 MATHEMATICAL BOLD ITALIC SMALL KAPPA
+ 'λ': '\U0001d740', # 𝝀 MATHEMATICAL BOLD ITALIC SMALL LAMDA
+ 'μ': '\U0001d741', # 𝝁 MATHEMATICAL BOLD ITALIC SMALL MU
+ 'ν': '\U0001d742', # 𝝂 MATHEMATICAL BOLD ITALIC SMALL NU
+ 'ξ': '\U0001d743', # 𝝃 MATHEMATICAL BOLD ITALIC SMALL XI
+ 'π': '\U0001d745', # 𝝅 MATHEMATICAL BOLD ITALIC SMALL PI
+ 'ρ': '\U0001d746', # 𝝆 MATHEMATICAL BOLD ITALIC SMALL RHO
+ 'ς': '\U0001d747', # 𝝇 MATHEMATICAL BOLD ITALIC SMALL FINAL SIGMA
+ 'σ': '\U0001d748', # 𝝈 MATHEMATICAL BOLD ITALIC SMALL SIGMA
+ 'τ': '\U0001d749', # 𝝉 MATHEMATICAL BOLD ITALIC SMALL TAU
+ 'υ': '\U0001d74a', # 𝝊 MATHEMATICAL BOLD ITALIC SMALL UPSILON
+ 'φ': '\U0001d74b', # 𝝋 MATHEMATICAL BOLD ITALIC SMALL PHI
+ 'χ': '\U0001d74c', # 𝝌 MATHEMATICAL BOLD ITALIC SMALL CHI
+ 'ψ': '\U0001d74d', # 𝝍 MATHEMATICAL BOLD ITALIC SMALL PSI
+ 'ω': '\U0001d74e', # 𝝎 MATHEMATICAL BOLD ITALIC SMALL OMEGA
+ 'ϑ': '\U0001d751', # 𝝑 MATHEMATICAL BOLD ITALIC THETA SYMBOL
+ 'ϕ': '\U0001d753', # 𝝓 MATHEMATICAL BOLD ITALIC PHI SYMBOL
+ 'ϖ': '\U0001d755', # 𝝕 MATHEMATICAL BOLD ITALIC PI SYMBOL
+ 'ϰ': '\U0001d752', # 𝝒 MATHEMATICAL BOLD ITALIC KAPPA SYMBOL
+ 'ϱ': '\U0001d754', # 𝝔 MATHEMATICAL BOLD ITALIC RHO SYMBOL
+ 'ϵ': '\U0001d750', # 𝝐 MATHEMATICAL BOLD ITALIC EPSILON SYMBOL
+ '∂': '\U0001d74f', # 𝝏 MATHEMATICAL BOLD ITALIC PARTIAL DIFFERENTIAL
+ '∇': '\U0001d735', # 𝜵 MATHEMATICAL BOLD ITALIC NABLA
+ }
+
+mathcal = {
+ 'A': '\U0001d49c', # 𝒜 MATHEMATICAL SCRIPT CAPITAL A
+ 'B': '\u212c', # ℬ SCRIPT CAPITAL B
+ 'C': '\U0001d49e', # 𝒞 MATHEMATICAL SCRIPT CAPITAL C
+ 'D': '\U0001d49f', # 𝒟 MATHEMATICAL SCRIPT CAPITAL D
+ 'E': '\u2130', # ℰ SCRIPT CAPITAL E
+ 'F': '\u2131', # ℱ SCRIPT CAPITAL F
+ 'G': '\U0001d4a2', # 𝒢 MATHEMATICAL SCRIPT CAPITAL G
+ 'H': '\u210b', # ℋ SCRIPT CAPITAL H
+ 'I': '\u2110', # ℐ SCRIPT CAPITAL I
+ 'J': '\U0001d4a5', # 𝒥 MATHEMATICAL SCRIPT CAPITAL J
+ 'K': '\U0001d4a6', # 𝒦 MATHEMATICAL SCRIPT CAPITAL K
+ 'L': '\u2112', # ℒ SCRIPT CAPITAL L
+ 'M': '\u2133', # ℳ SCRIPT CAPITAL M
+ 'N': '\U0001d4a9', # 𝒩 MATHEMATICAL SCRIPT CAPITAL N
+ 'O': '\U0001d4aa', # 𝒪 MATHEMATICAL SCRIPT CAPITAL O
+ 'P': '\U0001d4ab', # 𝒫 MATHEMATICAL SCRIPT CAPITAL P
+ 'Q': '\U0001d4ac', # 𝒬 MATHEMATICAL SCRIPT CAPITAL Q
+ 'R': '\u211b', # ℛ SCRIPT CAPITAL R
+ 'S': '\U0001d4ae', # 𝒮 MATHEMATICAL SCRIPT CAPITAL S
+ 'T': '\U0001d4af', # 𝒯 MATHEMATICAL SCRIPT CAPITAL T
+ 'U': '\U0001d4b0', # 𝒰 MATHEMATICAL SCRIPT CAPITAL U
+ 'V': '\U0001d4b1', # 𝒱 MATHEMATICAL SCRIPT CAPITAL V
+ 'W': '\U0001d4b2', # 𝒲 MATHEMATICAL SCRIPT CAPITAL W
+ 'X': '\U0001d4b3', # 𝒳 MATHEMATICAL SCRIPT CAPITAL X
+ 'Y': '\U0001d4b4', # 𝒴 MATHEMATICAL SCRIPT CAPITAL Y
+ 'Z': '\U0001d4b5', # 𝒵 MATHEMATICAL SCRIPT CAPITAL Z
+ 'a': '\U0001d4b6', # 𝒶 MATHEMATICAL SCRIPT SMALL A
+ 'b': '\U0001d4b7', # 𝒷 MATHEMATICAL SCRIPT SMALL B
+ 'c': '\U0001d4b8', # 𝒸 MATHEMATICAL SCRIPT SMALL C
+ 'd': '\U0001d4b9', # 𝒹 MATHEMATICAL SCRIPT SMALL D
+ 'e': '\u212f', # ℯ SCRIPT SMALL E
+ 'f': '\U0001d4bb', # 𝒻 MATHEMATICAL SCRIPT SMALL F
+ 'g': '\u210a', # ℊ SCRIPT SMALL G
+ 'h': '\U0001d4bd', # 𝒽 MATHEMATICAL SCRIPT SMALL H
+ 'i': '\U0001d4be', # 𝒾 MATHEMATICAL SCRIPT SMALL I
+ 'j': '\U0001d4bf', # 𝒿 MATHEMATICAL SCRIPT SMALL J
+ 'k': '\U0001d4c0', # 𝓀 MATHEMATICAL SCRIPT SMALL K
+ 'l': '\U0001d4c1', # 𝓁 MATHEMATICAL SCRIPT SMALL L
+ 'm': '\U0001d4c2', # 𝓂 MATHEMATICAL SCRIPT SMALL M
+ 'n': '\U0001d4c3', # 𝓃 MATHEMATICAL SCRIPT SMALL N
+ 'o': '\u2134', # ℴ SCRIPT SMALL O
+ 'p': '\U0001d4c5', # 𝓅 MATHEMATICAL SCRIPT SMALL P
+ 'q': '\U0001d4c6', # 𝓆 MATHEMATICAL SCRIPT SMALL Q
+ 'r': '\U0001d4c7', # 𝓇 MATHEMATICAL SCRIPT SMALL R
+ 's': '\U0001d4c8', # 𝓈 MATHEMATICAL SCRIPT SMALL S
+ 't': '\U0001d4c9', # 𝓉 MATHEMATICAL SCRIPT SMALL T
+ 'u': '\U0001d4ca', # 𝓊 MATHEMATICAL SCRIPT SMALL U
+ 'v': '\U0001d4cb', # 𝓋 MATHEMATICAL SCRIPT SMALL V
+ 'w': '\U0001d4cc', # 𝓌 MATHEMATICAL SCRIPT SMALL W
+ 'x': '\U0001d4cd', # 𝓍 MATHEMATICAL SCRIPT SMALL X
+ 'y': '\U0001d4ce', # 𝓎 MATHEMATICAL SCRIPT SMALL Y
+ 'z': '\U0001d4cf', # 𝓏 MATHEMATICAL SCRIPT SMALL Z
+ }
+
+mathfrak = {
+ 'A': '\U0001d504', # 𝔄 MATHEMATICAL FRAKTUR CAPITAL A
+ 'B': '\U0001d505', # 𝔅 MATHEMATICAL FRAKTUR CAPITAL B
+ 'C': '\u212d', # ℭ BLACK-LETTER CAPITAL C
+ 'D': '\U0001d507', # 𝔇 MATHEMATICAL FRAKTUR CAPITAL D
+ 'E': '\U0001d508', # 𝔈 MATHEMATICAL FRAKTUR CAPITAL E
+ 'F': '\U0001d509', # 𝔉 MATHEMATICAL FRAKTUR CAPITAL F
+ 'G': '\U0001d50a', # 𝔊 MATHEMATICAL FRAKTUR CAPITAL G
+ 'H': '\u210c', # ℌ BLACK-LETTER CAPITAL H
+ 'I': '\u2111', # ℑ BLACK-LETTER CAPITAL I
+ 'J': '\U0001d50d', # 𝔍 MATHEMATICAL FRAKTUR CAPITAL J
+ 'K': '\U0001d50e', # 𝔎 MATHEMATICAL FRAKTUR CAPITAL K
+ 'L': '\U0001d50f', # 𝔏 MATHEMATICAL FRAKTUR CAPITAL L
+ 'M': '\U0001d510', # 𝔐 MATHEMATICAL FRAKTUR CAPITAL M
+ 'N': '\U0001d511', # 𝔑 MATHEMATICAL FRAKTUR CAPITAL N
+ 'O': '\U0001d512', # 𝔒 MATHEMATICAL FRAKTUR CAPITAL O
+ 'P': '\U0001d513', # 𝔓 MATHEMATICAL FRAKTUR CAPITAL P
+ 'Q': '\U0001d514', # 𝔔 MATHEMATICAL FRAKTUR CAPITAL Q
+ 'R': '\u211c', # ℜ BLACK-LETTER CAPITAL R
+ 'S': '\U0001d516', # 𝔖 MATHEMATICAL FRAKTUR CAPITAL S
+ 'T': '\U0001d517', # 𝔗 MATHEMATICAL FRAKTUR CAPITAL T
+ 'U': '\U0001d518', # 𝔘 MATHEMATICAL FRAKTUR CAPITAL U
+ 'V': '\U0001d519', # 𝔙 MATHEMATICAL FRAKTUR CAPITAL V
+ 'W': '\U0001d51a', # 𝔚 MATHEMATICAL FRAKTUR CAPITAL W
+ 'X': '\U0001d51b', # 𝔛 MATHEMATICAL FRAKTUR CAPITAL X
+ 'Y': '\U0001d51c', # 𝔜 MATHEMATICAL FRAKTUR CAPITAL Y
+ 'Z': '\u2128', # ℨ BLACK-LETTER CAPITAL Z
+ 'a': '\U0001d51e', # 𝔞 MATHEMATICAL FRAKTUR SMALL A
+ 'b': '\U0001d51f', # 𝔟 MATHEMATICAL FRAKTUR SMALL B
+ 'c': '\U0001d520', # 𝔠 MATHEMATICAL FRAKTUR SMALL C
+ 'd': '\U0001d521', # 𝔡 MATHEMATICAL FRAKTUR SMALL D
+ 'e': '\U0001d522', # 𝔢 MATHEMATICAL FRAKTUR SMALL E
+ 'f': '\U0001d523', # 𝔣 MATHEMATICAL FRAKTUR SMALL F
+ 'g': '\U0001d524', # 𝔤 MATHEMATICAL FRAKTUR SMALL G
+ 'h': '\U0001d525', # 𝔥 MATHEMATICAL FRAKTUR SMALL H
+ 'i': '\U0001d526', # 𝔦 MATHEMATICAL FRAKTUR SMALL I
+ 'j': '\U0001d527', # 𝔧 MATHEMATICAL FRAKTUR SMALL J
+ 'k': '\U0001d528', # 𝔨 MATHEMATICAL FRAKTUR SMALL K
+ 'l': '\U0001d529', # 𝔩 MATHEMATICAL FRAKTUR SMALL L
+ 'm': '\U0001d52a', # 𝔪 MATHEMATICAL FRAKTUR SMALL M
+ 'n': '\U0001d52b', # 𝔫 MATHEMATICAL FRAKTUR SMALL N
+ 'o': '\U0001d52c', # 𝔬 MATHEMATICAL FRAKTUR SMALL O
+ 'p': '\U0001d52d', # 𝔭 MATHEMATICAL FRAKTUR SMALL P
+ 'q': '\U0001d52e', # 𝔮 MATHEMATICAL FRAKTUR SMALL Q
+ 'r': '\U0001d52f', # 𝔯 MATHEMATICAL FRAKTUR SMALL R
+ 's': '\U0001d530', # 𝔰 MATHEMATICAL FRAKTUR SMALL S
+ 't': '\U0001d531', # 𝔱 MATHEMATICAL FRAKTUR SMALL T
+ 'u': '\U0001d532', # 𝔲 MATHEMATICAL FRAKTUR SMALL U
+ 'v': '\U0001d533', # 𝔳 MATHEMATICAL FRAKTUR SMALL V
+ 'w': '\U0001d534', # 𝔴 MATHEMATICAL FRAKTUR SMALL W
+ 'x': '\U0001d535', # 𝔵 MATHEMATICAL FRAKTUR SMALL X
+ 'y': '\U0001d536', # 𝔶 MATHEMATICAL FRAKTUR SMALL Y
+ 'z': '\U0001d537', # 𝔷 MATHEMATICAL FRAKTUR SMALL Z
+ }
+
+mathit = {
+ 'A': '\U0001d434', # 𝐴 MATHEMATICAL ITALIC CAPITAL A
+ 'B': '\U0001d435', # 𝐵 MATHEMATICAL ITALIC CAPITAL B
+ 'C': '\U0001d436', # 𝐶 MATHEMATICAL ITALIC CAPITAL C
+ 'D': '\U0001d437', # 𝐷 MATHEMATICAL ITALIC CAPITAL D
+ 'E': '\U0001d438', # 𝐸 MATHEMATICAL ITALIC CAPITAL E
+ 'F': '\U0001d439', # 𝐹 MATHEMATICAL ITALIC CAPITAL F
+ 'G': '\U0001d43a', # 𝐺 MATHEMATICAL ITALIC CAPITAL G
+ 'H': '\U0001d43b', # 𝐻 MATHEMATICAL ITALIC CAPITAL H
+ 'I': '\U0001d43c', # 𝐼 MATHEMATICAL ITALIC CAPITAL I
+ 'J': '\U0001d43d', # 𝐽 MATHEMATICAL ITALIC CAPITAL J
+ 'K': '\U0001d43e', # 𝐾 MATHEMATICAL ITALIC CAPITAL K
+ 'L': '\U0001d43f', # 𝐿 MATHEMATICAL ITALIC CAPITAL L
+ 'M': '\U0001d440', # 𝑀 MATHEMATICAL ITALIC CAPITAL M
+ 'N': '\U0001d441', # 𝑁 MATHEMATICAL ITALIC CAPITAL N
+ 'O': '\U0001d442', # 𝑂 MATHEMATICAL ITALIC CAPITAL O
+ 'P': '\U0001d443', # 𝑃 MATHEMATICAL ITALIC CAPITAL P
+ 'Q': '\U0001d444', # 𝑄 MATHEMATICAL ITALIC CAPITAL Q
+ 'R': '\U0001d445', # 𝑅 MATHEMATICAL ITALIC CAPITAL R
+ 'S': '\U0001d446', # 𝑆 MATHEMATICAL ITALIC CAPITAL S
+ 'T': '\U0001d447', # 𝑇 MATHEMATICAL ITALIC CAPITAL T
+ 'U': '\U0001d448', # 𝑈 MATHEMATICAL ITALIC CAPITAL U
+ 'V': '\U0001d449', # 𝑉 MATHEMATICAL ITALIC CAPITAL V
+ 'W': '\U0001d44a', # 𝑊 MATHEMATICAL ITALIC CAPITAL W
+ 'X': '\U0001d44b', # 𝑋 MATHEMATICAL ITALIC CAPITAL X
+ 'Y': '\U0001d44c', # 𝑌 MATHEMATICAL ITALIC CAPITAL Y
+ 'Z': '\U0001d44d', # 𝑍 MATHEMATICAL ITALIC CAPITAL Z
+ 'a': '\U0001d44e', # 𝑎 MATHEMATICAL ITALIC SMALL A
+ 'b': '\U0001d44f', # 𝑏 MATHEMATICAL ITALIC SMALL B
+ 'c': '\U0001d450', # 𝑐 MATHEMATICAL ITALIC SMALL C
+ 'd': '\U0001d451', # 𝑑 MATHEMATICAL ITALIC SMALL D
+ 'e': '\U0001d452', # 𝑒 MATHEMATICAL ITALIC SMALL E
+ 'f': '\U0001d453', # 𝑓 MATHEMATICAL ITALIC SMALL F
+ 'g': '\U0001d454', # 𝑔 MATHEMATICAL ITALIC SMALL G
+ 'h': '\u210e', # ℎ PLANCK CONSTANT
+ 'i': '\U0001d456', # 𝑖 MATHEMATICAL ITALIC SMALL I
+ 'j': '\U0001d457', # 𝑗 MATHEMATICAL ITALIC SMALL J
+ 'k': '\U0001d458', # 𝑘 MATHEMATICAL ITALIC SMALL K
+ 'l': '\U0001d459', # 𝑙 MATHEMATICAL ITALIC SMALL L
+ 'm': '\U0001d45a', # 𝑚 MATHEMATICAL ITALIC SMALL M
+ 'n': '\U0001d45b', # 𝑛 MATHEMATICAL ITALIC SMALL N
+ 'o': '\U0001d45c', # 𝑜 MATHEMATICAL ITALIC SMALL O
+ 'p': '\U0001d45d', # 𝑝 MATHEMATICAL ITALIC SMALL P
+ 'q': '\U0001d45e', # 𝑞 MATHEMATICAL ITALIC SMALL Q
+ 'r': '\U0001d45f', # 𝑟 MATHEMATICAL ITALIC SMALL R
+ 's': '\U0001d460', # 𝑠 MATHEMATICAL ITALIC SMALL S
+ 't': '\U0001d461', # 𝑡 MATHEMATICAL ITALIC SMALL T
+ 'u': '\U0001d462', # 𝑢 MATHEMATICAL ITALIC SMALL U
+ 'v': '\U0001d463', # 𝑣 MATHEMATICAL ITALIC SMALL V
+ 'w': '\U0001d464', # 𝑤 MATHEMATICAL ITALIC SMALL W
+ 'x': '\U0001d465', # 𝑥 MATHEMATICAL ITALIC SMALL X
+ 'y': '\U0001d466', # 𝑦 MATHEMATICAL ITALIC SMALL Y
+ 'z': '\U0001d467', # 𝑧 MATHEMATICAL ITALIC SMALL Z
+ 'ı': '\U0001d6a4', # 𝚤 MATHEMATICAL ITALIC SMALL DOTLESS I
+ 'ȷ': '\U0001d6a5', # 𝚥 MATHEMATICAL ITALIC SMALL DOTLESS J
+ 'Γ': '\U0001d6e4', # 𝛤 MATHEMATICAL ITALIC CAPITAL GAMMA
+ 'Δ': '\U0001d6e5', # 𝛥 MATHEMATICAL ITALIC CAPITAL DELTA
+ 'Θ': '\U0001d6e9', # 𝛩 MATHEMATICAL ITALIC CAPITAL THETA
+ 'Λ': '\U0001d6ec', # 𝛬 MATHEMATICAL ITALIC CAPITAL LAMDA
+ 'Ξ': '\U0001d6ef', # 𝛯 MATHEMATICAL ITALIC CAPITAL XI
+ 'Π': '\U0001d6f1', # 𝛱 MATHEMATICAL ITALIC CAPITAL PI
+ 'Σ': '\U0001d6f4', # 𝛴 MATHEMATICAL ITALIC CAPITAL SIGMA
+ 'Υ': '\U0001d6f6', # 𝛶 MATHEMATICAL ITALIC CAPITAL UPSILON
+ 'Φ': '\U0001d6f7', # 𝛷 MATHEMATICAL ITALIC CAPITAL PHI
+ 'Ψ': '\U0001d6f9', # 𝛹 MATHEMATICAL ITALIC CAPITAL PSI
+ 'Ω': '\U0001d6fa', # 𝛺 MATHEMATICAL ITALIC CAPITAL OMEGA
+ 'α': '\U0001d6fc', # 𝛼 MATHEMATICAL ITALIC SMALL ALPHA
+ 'β': '\U0001d6fd', # 𝛽 MATHEMATICAL ITALIC SMALL BETA
+ 'γ': '\U0001d6fe', # 𝛾 MATHEMATICAL ITALIC SMALL GAMMA
+ 'δ': '\U0001d6ff', # 𝛿 MATHEMATICAL ITALIC SMALL DELTA
+ 'ε': '\U0001d700', # 𝜀 MATHEMATICAL ITALIC SMALL EPSILON
+ 'ζ': '\U0001d701', # 𝜁 MATHEMATICAL ITALIC SMALL ZETA
+ 'η': '\U0001d702', # 𝜂 MATHEMATICAL ITALIC SMALL ETA
+ 'θ': '\U0001d703', # 𝜃 MATHEMATICAL ITALIC SMALL THETA
+ 'ι': '\U0001d704', # 𝜄 MATHEMATICAL ITALIC SMALL IOTA
+ 'κ': '\U0001d705', # 𝜅 MATHEMATICAL ITALIC SMALL KAPPA
+ 'λ': '\U0001d706', # 𝜆 MATHEMATICAL ITALIC SMALL LAMDA
+ 'μ': '\U0001d707', # 𝜇 MATHEMATICAL ITALIC SMALL MU
+ 'ν': '\U0001d708', # 𝜈 MATHEMATICAL ITALIC SMALL NU
+ 'ξ': '\U0001d709', # 𝜉 MATHEMATICAL ITALIC SMALL XI
+ 'π': '\U0001d70b', # 𝜋 MATHEMATICAL ITALIC SMALL PI
+ 'ρ': '\U0001d70c', # 𝜌 MATHEMATICAL ITALIC SMALL RHO
+ 'ς': '\U0001d70d', # 𝜍 MATHEMATICAL ITALIC SMALL FINAL SIGMA
+ 'σ': '\U0001d70e', # 𝜎 MATHEMATICAL ITALIC SMALL SIGMA
+ 'τ': '\U0001d70f', # 𝜏 MATHEMATICAL ITALIC SMALL TAU
+ 'υ': '\U0001d710', # 𝜐 MATHEMATICAL ITALIC SMALL UPSILON
+ 'φ': '\U0001d711', # 𝜑 MATHEMATICAL ITALIC SMALL PHI
+ 'χ': '\U0001d712', # 𝜒 MATHEMATICAL ITALIC SMALL CHI
+ 'ψ': '\U0001d713', # 𝜓 MATHEMATICAL ITALIC SMALL PSI
+ 'ω': '\U0001d714', # 𝜔 MATHEMATICAL ITALIC SMALL OMEGA
+ 'ϑ': '\U0001d717', # 𝜗 MATHEMATICAL ITALIC THETA SYMBOL
+ 'ϕ': '\U0001d719', # 𝜙 MATHEMATICAL ITALIC PHI SYMBOL
+ 'ϖ': '\U0001d71b', # 𝜛 MATHEMATICAL ITALIC PI SYMBOL
+ 'ϱ': '\U0001d71a', # 𝜚 MATHEMATICAL ITALIC RHO SYMBOL
+ 'ϵ': '\U0001d716', # 𝜖 MATHEMATICAL ITALIC EPSILON SYMBOL
+ '∂': '\U0001d715', # 𝜕 MATHEMATICAL ITALIC PARTIAL DIFFERENTIAL
+ '∇': '\U0001d6fb', # 𝛻 MATHEMATICAL ITALIC NABLA
+ }
+
+mathsf = {
+ '0': '\U0001d7e2', # 𝟢 MATHEMATICAL SANS-SERIF DIGIT ZERO
+ '1': '\U0001d7e3', # 𝟣 MATHEMATICAL SANS-SERIF DIGIT ONE
+ '2': '\U0001d7e4', # 𝟤 MATHEMATICAL SANS-SERIF DIGIT TWO
+ '3': '\U0001d7e5', # 𝟥 MATHEMATICAL SANS-SERIF DIGIT THREE
+ '4': '\U0001d7e6', # 𝟦 MATHEMATICAL SANS-SERIF DIGIT FOUR
+ '5': '\U0001d7e7', # 𝟧 MATHEMATICAL SANS-SERIF DIGIT FIVE
+ '6': '\U0001d7e8', # 𝟨 MATHEMATICAL SANS-SERIF DIGIT SIX
+ '7': '\U0001d7e9', # 𝟩 MATHEMATICAL SANS-SERIF DIGIT SEVEN
+ '8': '\U0001d7ea', # 𝟪 MATHEMATICAL SANS-SERIF DIGIT EIGHT
+ '9': '\U0001d7eb', # 𝟫 MATHEMATICAL SANS-SERIF DIGIT NINE
+ 'A': '\U0001d5a0', # 𝖠 MATHEMATICAL SANS-SERIF CAPITAL A
+ 'B': '\U0001d5a1', # 𝖡 MATHEMATICAL SANS-SERIF CAPITAL B
+ 'C': '\U0001d5a2', # 𝖢 MATHEMATICAL SANS-SERIF CAPITAL C
+ 'D': '\U0001d5a3', # 𝖣 MATHEMATICAL SANS-SERIF CAPITAL D
+ 'E': '\U0001d5a4', # 𝖤 MATHEMATICAL SANS-SERIF CAPITAL E
+ 'F': '\U0001d5a5', # 𝖥 MATHEMATICAL SANS-SERIF CAPITAL F
+ 'G': '\U0001d5a6', # 𝖦 MATHEMATICAL SANS-SERIF CAPITAL G
+ 'H': '\U0001d5a7', # 𝖧 MATHEMATICAL SANS-SERIF CAPITAL H
+ 'I': '\U0001d5a8', # 𝖨 MATHEMATICAL SANS-SERIF CAPITAL I
+ 'J': '\U0001d5a9', # 𝖩 MATHEMATICAL SANS-SERIF CAPITAL J
+ 'K': '\U0001d5aa', # 𝖪 MATHEMATICAL SANS-SERIF CAPITAL K
+ 'L': '\U0001d5ab', # 𝖫 MATHEMATICAL SANS-SERIF CAPITAL L
+ 'M': '\U0001d5ac', # 𝖬 MATHEMATICAL SANS-SERIF CAPITAL M
+ 'N': '\U0001d5ad', # 𝖭 MATHEMATICAL SANS-SERIF CAPITAL N
+ 'O': '\U0001d5ae', # 𝖮 MATHEMATICAL SANS-SERIF CAPITAL O
+ 'P': '\U0001d5af', # 𝖯 MATHEMATICAL SANS-SERIF CAPITAL P
+ 'Q': '\U0001d5b0', # 𝖰 MATHEMATICAL SANS-SERIF CAPITAL Q
+ 'R': '\U0001d5b1', # 𝖱 MATHEMATICAL SANS-SERIF CAPITAL R
+ 'S': '\U0001d5b2', # 𝖲 MATHEMATICAL SANS-SERIF CAPITAL S
+ 'T': '\U0001d5b3', # 𝖳 MATHEMATICAL SANS-SERIF CAPITAL T
+ 'U': '\U0001d5b4', # 𝖴 MATHEMATICAL SANS-SERIF CAPITAL U
+ 'V': '\U0001d5b5', # 𝖵 MATHEMATICAL SANS-SERIF CAPITAL V
+ 'W': '\U0001d5b6', # 𝖶 MATHEMATICAL SANS-SERIF CAPITAL W
+ 'X': '\U0001d5b7', # 𝖷 MATHEMATICAL SANS-SERIF CAPITAL X
+ 'Y': '\U0001d5b8', # 𝖸 MATHEMATICAL SANS-SERIF CAPITAL Y
+ 'Z': '\U0001d5b9', # 𝖹 MATHEMATICAL SANS-SERIF CAPITAL Z
+ 'a': '\U0001d5ba', # 𝖺 MATHEMATICAL SANS-SERIF SMALL A
+ 'b': '\U0001d5bb', # 𝖻 MATHEMATICAL SANS-SERIF SMALL B
+ 'c': '\U0001d5bc', # 𝖼 MATHEMATICAL SANS-SERIF SMALL C
+ 'd': '\U0001d5bd', # 𝖽 MATHEMATICAL SANS-SERIF SMALL D
+ 'e': '\U0001d5be', # 𝖾 MATHEMATICAL SANS-SERIF SMALL E
+ 'f': '\U0001d5bf', # 𝖿 MATHEMATICAL SANS-SERIF SMALL F
+ 'g': '\U0001d5c0', # 𝗀 MATHEMATICAL SANS-SERIF SMALL G
+ 'h': '\U0001d5c1', # 𝗁 MATHEMATICAL SANS-SERIF SMALL H
+ 'i': '\U0001d5c2', # 𝗂 MATHEMATICAL SANS-SERIF SMALL I
+ 'j': '\U0001d5c3', # 𝗃 MATHEMATICAL SANS-SERIF SMALL J
+ 'k': '\U0001d5c4', # 𝗄 MATHEMATICAL SANS-SERIF SMALL K
+ 'l': '\U0001d5c5', # 𝗅 MATHEMATICAL SANS-SERIF SMALL L
+ 'm': '\U0001d5c6', # 𝗆 MATHEMATICAL SANS-SERIF SMALL M
+ 'n': '\U0001d5c7', # 𝗇 MATHEMATICAL SANS-SERIF SMALL N
+ 'o': '\U0001d5c8', # 𝗈 MATHEMATICAL SANS-SERIF SMALL O
+ 'p': '\U0001d5c9', # 𝗉 MATHEMATICAL SANS-SERIF SMALL P
+ 'q': '\U0001d5ca', # 𝗊 MATHEMATICAL SANS-SERIF SMALL Q
+ 'r': '\U0001d5cb', # 𝗋 MATHEMATICAL SANS-SERIF SMALL R
+ 's': '\U0001d5cc', # 𝗌 MATHEMATICAL SANS-SERIF SMALL S
+ 't': '\U0001d5cd', # 𝗍 MATHEMATICAL SANS-SERIF SMALL T
+ 'u': '\U0001d5ce', # 𝗎 MATHEMATICAL SANS-SERIF SMALL U
+ 'v': '\U0001d5cf', # 𝗏 MATHEMATICAL SANS-SERIF SMALL V
+ 'w': '\U0001d5d0', # 𝗐 MATHEMATICAL SANS-SERIF SMALL W
+ 'x': '\U0001d5d1', # 𝗑 MATHEMATICAL SANS-SERIF SMALL X
+ 'y': '\U0001d5d2', # 𝗒 MATHEMATICAL SANS-SERIF SMALL Y
+ 'z': '\U0001d5d3', # 𝗓 MATHEMATICAL SANS-SERIF SMALL Z
+ }
+
+mathsfbf = {
+ '0': '\U0001d7ec', # 𝟬 MATHEMATICAL SANS-SERIF BOLD DIGIT ZERO
+ '1': '\U0001d7ed', # 𝟭 MATHEMATICAL SANS-SERIF BOLD DIGIT ONE
+ '2': '\U0001d7ee', # 𝟮 MATHEMATICAL SANS-SERIF BOLD DIGIT TWO
+ '3': '\U0001d7ef', # 𝟯 MATHEMATICAL SANS-SERIF BOLD DIGIT THREE
+ '4': '\U0001d7f0', # 𝟰 MATHEMATICAL SANS-SERIF BOLD DIGIT FOUR
+ '5': '\U0001d7f1', # 𝟱 MATHEMATICAL SANS-SERIF BOLD DIGIT FIVE
+ '6': '\U0001d7f2', # 𝟲 MATHEMATICAL SANS-SERIF BOLD DIGIT SIX
+ '7': '\U0001d7f3', # 𝟳 MATHEMATICAL SANS-SERIF BOLD DIGIT SEVEN
+ '8': '\U0001d7f4', # 𝟴 MATHEMATICAL SANS-SERIF BOLD DIGIT EIGHT
+ '9': '\U0001d7f5', # 𝟵 MATHEMATICAL SANS-SERIF BOLD DIGIT NINE
+ 'A': '\U0001d5d4', # 𝗔 MATHEMATICAL SANS-SERIF BOLD CAPITAL A
+ 'B': '\U0001d5d5', # 𝗕 MATHEMATICAL SANS-SERIF BOLD CAPITAL B
+ 'C': '\U0001d5d6', # 𝗖 MATHEMATICAL SANS-SERIF BOLD CAPITAL C
+ 'D': '\U0001d5d7', # 𝗗 MATHEMATICAL SANS-SERIF BOLD CAPITAL D
+ 'E': '\U0001d5d8', # 𝗘 MATHEMATICAL SANS-SERIF BOLD CAPITAL E
+ 'F': '\U0001d5d9', # 𝗙 MATHEMATICAL SANS-SERIF BOLD CAPITAL F
+ 'G': '\U0001d5da', # 𝗚 MATHEMATICAL SANS-SERIF BOLD CAPITAL G
+ 'H': '\U0001d5db', # 𝗛 MATHEMATICAL SANS-SERIF BOLD CAPITAL H
+ 'I': '\U0001d5dc', # 𝗜 MATHEMATICAL SANS-SERIF BOLD CAPITAL I
+ 'J': '\U0001d5dd', # 𝗝 MATHEMATICAL SANS-SERIF BOLD CAPITAL J
+ 'K': '\U0001d5de', # 𝗞 MATHEMATICAL SANS-SERIF BOLD CAPITAL K
+ 'L': '\U0001d5df', # 𝗟 MATHEMATICAL SANS-SERIF BOLD CAPITAL L
+ 'M': '\U0001d5e0', # 𝗠 MATHEMATICAL SANS-SERIF BOLD CAPITAL M
+ 'N': '\U0001d5e1', # 𝗡 MATHEMATICAL SANS-SERIF BOLD CAPITAL N
+ 'O': '\U0001d5e2', # 𝗢 MATHEMATICAL SANS-SERIF BOLD CAPITAL O
+ 'P': '\U0001d5e3', # 𝗣 MATHEMATICAL SANS-SERIF BOLD CAPITAL P
+ 'Q': '\U0001d5e4', # 𝗤 MATHEMATICAL SANS-SERIF BOLD CAPITAL Q
+ 'R': '\U0001d5e5', # 𝗥 MATHEMATICAL SANS-SERIF BOLD CAPITAL R
+ 'S': '\U0001d5e6', # 𝗦 MATHEMATICAL SANS-SERIF BOLD CAPITAL S
+ 'T': '\U0001d5e7', # 𝗧 MATHEMATICAL SANS-SERIF BOLD CAPITAL T
+ 'U': '\U0001d5e8', # 𝗨 MATHEMATICAL SANS-SERIF BOLD CAPITAL U
+ 'V': '\U0001d5e9', # 𝗩 MATHEMATICAL SANS-SERIF BOLD CAPITAL V
+ 'W': '\U0001d5ea', # 𝗪 MATHEMATICAL SANS-SERIF BOLD CAPITAL W
+ 'X': '\U0001d5eb', # 𝗫 MATHEMATICAL SANS-SERIF BOLD CAPITAL X
+ 'Y': '\U0001d5ec', # 𝗬 MATHEMATICAL SANS-SERIF BOLD CAPITAL Y
+ 'Z': '\U0001d5ed', # 𝗭 MATHEMATICAL SANS-SERIF BOLD CAPITAL Z
+ 'a': '\U0001d5ee', # 𝗮 MATHEMATICAL SANS-SERIF BOLD SMALL A
+ 'b': '\U0001d5ef', # 𝗯 MATHEMATICAL SANS-SERIF BOLD SMALL B
+ 'c': '\U0001d5f0', # 𝗰 MATHEMATICAL SANS-SERIF BOLD SMALL C
+ 'd': '\U0001d5f1', # 𝗱 MATHEMATICAL SANS-SERIF BOLD SMALL D
+ 'e': '\U0001d5f2', # 𝗲 MATHEMATICAL SANS-SERIF BOLD SMALL E
+ 'f': '\U0001d5f3', # 𝗳 MATHEMATICAL SANS-SERIF BOLD SMALL F
+ 'g': '\U0001d5f4', # 𝗴 MATHEMATICAL SANS-SERIF BOLD SMALL G
+ 'h': '\U0001d5f5', # 𝗵 MATHEMATICAL SANS-SERIF BOLD SMALL H
+ 'i': '\U0001d5f6', # 𝗶 MATHEMATICAL SANS-SERIF BOLD SMALL I
+ 'j': '\U0001d5f7', # 𝗷 MATHEMATICAL SANS-SERIF BOLD SMALL J
+ 'k': '\U0001d5f8', # 𝗸 MATHEMATICAL SANS-SERIF BOLD SMALL K
+ 'l': '\U0001d5f9', # 𝗹 MATHEMATICAL SANS-SERIF BOLD SMALL L
+ 'm': '\U0001d5fa', # 𝗺 MATHEMATICAL SANS-SERIF BOLD SMALL M
+ 'n': '\U0001d5fb', # 𝗻 MATHEMATICAL SANS-SERIF BOLD SMALL N
+ 'o': '\U0001d5fc', # 𝗼 MATHEMATICAL SANS-SERIF BOLD SMALL O
+ 'p': '\U0001d5fd', # 𝗽 MATHEMATICAL SANS-SERIF BOLD SMALL P
+ 'q': '\U0001d5fe', # 𝗾 MATHEMATICAL SANS-SERIF BOLD SMALL Q
+ 'r': '\U0001d5ff', # 𝗿 MATHEMATICAL SANS-SERIF BOLD SMALL R
+ 's': '\U0001d600', # 𝘀 MATHEMATICAL SANS-SERIF BOLD SMALL S
+ 't': '\U0001d601', # 𝘁 MATHEMATICAL SANS-SERIF BOLD SMALL T
+ 'u': '\U0001d602', # 𝘂 MATHEMATICAL SANS-SERIF BOLD SMALL U
+ 'v': '\U0001d603', # 𝘃 MATHEMATICAL SANS-SERIF BOLD SMALL V
+ 'w': '\U0001d604', # 𝘄 MATHEMATICAL SANS-SERIF BOLD SMALL W
+ 'x': '\U0001d605', # 𝘅 MATHEMATICAL SANS-SERIF BOLD SMALL X
+ 'y': '\U0001d606', # 𝘆 MATHEMATICAL SANS-SERIF BOLD SMALL Y
+ 'z': '\U0001d607', # 𝘇 MATHEMATICAL SANS-SERIF BOLD SMALL Z
+ 'Γ': '\U0001d758', # 𝝘 MATHEMATICAL SANS-SERIF BOLD CAPITAL GAMMA
+ 'Δ': '\U0001d759', # 𝝙 MATHEMATICAL SANS-SERIF BOLD CAPITAL DELTA
+ 'Θ': '\U0001d75d', # 𝝝 MATHEMATICAL SANS-SERIF BOLD CAPITAL THETA
+ 'Λ': '\U0001d760', # 𝝠 MATHEMATICAL SANS-SERIF BOLD CAPITAL LAMDA
+ 'Ξ': '\U0001d763', # 𝝣 MATHEMATICAL SANS-SERIF BOLD CAPITAL XI
+ 'Π': '\U0001d765', # 𝝥 MATHEMATICAL SANS-SERIF BOLD CAPITAL PI
+ 'Σ': '\U0001d768', # 𝝨 MATHEMATICAL SANS-SERIF BOLD CAPITAL SIGMA
+ 'Υ': '\U0001d76a', # 𝝪 MATHEMATICAL SANS-SERIF BOLD CAPITAL UPSILON
+ 'Φ': '\U0001d76b', # 𝝫 MATHEMATICAL SANS-SERIF BOLD CAPITAL PHI
+ 'Ψ': '\U0001d76d', # 𝝭 MATHEMATICAL SANS-SERIF BOLD CAPITAL PSI
+ 'Ω': '\U0001d76e', # 𝝮 MATHEMATICAL SANS-SERIF BOLD CAPITAL OMEGA
+ 'α': '\U0001d770', # 𝝰 MATHEMATICAL SANS-SERIF BOLD SMALL ALPHA
+ 'β': '\U0001d771', # 𝝱 MATHEMATICAL SANS-SERIF BOLD SMALL BETA
+ 'γ': '\U0001d772', # 𝝲 MATHEMATICAL SANS-SERIF BOLD SMALL GAMMA
+ 'δ': '\U0001d773', # 𝝳 MATHEMATICAL SANS-SERIF BOLD SMALL DELTA
+ 'ε': '\U0001d774', # 𝝴 MATHEMATICAL SANS-SERIF BOLD SMALL EPSILON
+ 'ζ': '\U0001d775', # 𝝵 MATHEMATICAL SANS-SERIF BOLD SMALL ZETA
+ 'η': '\U0001d776', # 𝝶 MATHEMATICAL SANS-SERIF BOLD SMALL ETA
+ 'θ': '\U0001d777', # 𝝷 MATHEMATICAL SANS-SERIF BOLD SMALL THETA
+ 'ι': '\U0001d778', # 𝝸 MATHEMATICAL SANS-SERIF BOLD SMALL IOTA
+ 'κ': '\U0001d779', # 𝝹 MATHEMATICAL SANS-SERIF BOLD SMALL KAPPA
+ 'λ': '\U0001d77a', # 𝝺 MATHEMATICAL SANS-SERIF BOLD SMALL LAMDA
+ 'μ': '\U0001d77b', # 𝝻 MATHEMATICAL SANS-SERIF BOLD SMALL MU
+ 'ν': '\U0001d77c', # 𝝼 MATHEMATICAL SANS-SERIF BOLD SMALL NU
+ 'ξ': '\U0001d77d', # 𝝽 MATHEMATICAL SANS-SERIF BOLD SMALL XI
+ 'π': '\U0001d77f', # 𝝿 MATHEMATICAL SANS-SERIF BOLD SMALL PI
+ 'ρ': '\U0001d780', # 𝞀 MATHEMATICAL SANS-SERIF BOLD SMALL RHO
+ 'ς': '\U0001d781', # 𝞁 MATHEMATICAL SANS-SERIF BOLD SMALL FINAL SIGMA
+ 'σ': '\U0001d782', # 𝞂 MATHEMATICAL SANS-SERIF BOLD SMALL SIGMA
+ 'τ': '\U0001d783', # 𝞃 MATHEMATICAL SANS-SERIF BOLD SMALL TAU
+ 'υ': '\U0001d784', # 𝞄 MATHEMATICAL SANS-SERIF BOLD SMALL UPSILON
+ 'φ': '\U0001d785', # 𝞅 MATHEMATICAL SANS-SERIF BOLD SMALL PHI
+ 'χ': '\U0001d786', # 𝞆 MATHEMATICAL SANS-SERIF BOLD SMALL CHI
+ 'ψ': '\U0001d787', # 𝞇 MATHEMATICAL SANS-SERIF BOLD SMALL PSI
+ 'ω': '\U0001d788', # 𝞈 MATHEMATICAL SANS-SERIF BOLD SMALL OMEGA
+ 'ϑ': '\U0001d78b', # 𝞋 MATHEMATICAL SANS-SERIF BOLD THETA SYMBOL
+ 'ϕ': '\U0001d78d', # 𝞍 MATHEMATICAL SANS-SERIF BOLD PHI SYMBOL
+ 'ϖ': '\U0001d78f', # 𝞏 MATHEMATICAL SANS-SERIF BOLD PI SYMBOL
+ 'ϱ': '\U0001d78e', # 𝞎 MATHEMATICAL SANS-SERIF BOLD RHO SYMBOL
+ 'ϵ': '\U0001d78a', # 𝞊 MATHEMATICAL SANS-SERIF BOLD EPSILON SYMBOL
+ '∇': '\U0001d76f', # 𝝯 MATHEMATICAL SANS-SERIF BOLD NABLA
+ }
+
+mathsfbfit = {
+ 'A': '\U0001d63c', # 𝘼 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL A
+ 'B': '\U0001d63d', # 𝘽 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL B
+ 'C': '\U0001d63e', # 𝘾 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL C
+ 'D': '\U0001d63f', # 𝘿 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL D
+ 'E': '\U0001d640', # 𝙀 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL E
+ 'F': '\U0001d641', # 𝙁 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL F
+ 'G': '\U0001d642', # 𝙂 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL G
+ 'H': '\U0001d643', # 𝙃 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL H
+ 'I': '\U0001d644', # 𝙄 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL I
+ 'J': '\U0001d645', # 𝙅 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL J
+ 'K': '\U0001d646', # 𝙆 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL K
+ 'L': '\U0001d647', # 𝙇 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL L
+ 'M': '\U0001d648', # 𝙈 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL M
+ 'N': '\U0001d649', # 𝙉 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL N
+ 'O': '\U0001d64a', # 𝙊 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL O
+ 'P': '\U0001d64b', # 𝙋 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL P
+ 'Q': '\U0001d64c', # 𝙌 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Q
+ 'R': '\U0001d64d', # 𝙍 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL R
+ 'S': '\U0001d64e', # 𝙎 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL S
+ 'T': '\U0001d64f', # 𝙏 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL T
+ 'U': '\U0001d650', # 𝙐 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL U
+ 'V': '\U0001d651', # 𝙑 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL V
+ 'W': '\U0001d652', # 𝙒 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL W
+ 'X': '\U0001d653', # 𝙓 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL X
+ 'Y': '\U0001d654', # 𝙔 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Y
+ 'Z': '\U0001d655', # 𝙕 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Z
+ 'a': '\U0001d656', # 𝙖 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL A
+ 'b': '\U0001d657', # 𝙗 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL B
+ 'c': '\U0001d658', # 𝙘 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL C
+ 'd': '\U0001d659', # 𝙙 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL D
+ 'e': '\U0001d65a', # 𝙚 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL E
+ 'f': '\U0001d65b', # 𝙛 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL F
+ 'g': '\U0001d65c', # 𝙜 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL G
+ 'h': '\U0001d65d', # 𝙝 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL H
+ 'i': '\U0001d65e', # 𝙞 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL I
+ 'j': '\U0001d65f', # 𝙟 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL J
+ 'k': '\U0001d660', # 𝙠 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL K
+ 'l': '\U0001d661', # 𝙡 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL L
+ 'm': '\U0001d662', # 𝙢 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL M
+ 'n': '\U0001d663', # 𝙣 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL N
+ 'o': '\U0001d664', # 𝙤 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL O
+ 'p': '\U0001d665', # 𝙥 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL P
+ 'q': '\U0001d666', # 𝙦 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Q
+ 'r': '\U0001d667', # 𝙧 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL R
+ 's': '\U0001d668', # 𝙨 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL S
+ 't': '\U0001d669', # 𝙩 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL T
+ 'u': '\U0001d66a', # 𝙪 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL U
+ 'v': '\U0001d66b', # 𝙫 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL V
+ 'w': '\U0001d66c', # 𝙬 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL W
+ 'x': '\U0001d66d', # 𝙭 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL X
+ 'y': '\U0001d66e', # 𝙮 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Y
+ 'z': '\U0001d66f', # 𝙯 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Z
+ 'Γ': '\U0001d792', # 𝞒 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL GAMMA
+ 'Δ': '\U0001d793', # 𝞓 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL DELTA
+ 'Θ': '\U0001d797', # 𝞗 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL THETA
+ 'Λ': '\U0001d79a', # 𝞚 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL LAMDA
+ 'Ξ': '\U0001d79d', # 𝞝 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL XI
+ 'Π': '\U0001d79f', # 𝞟 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PI
+ 'Σ': '\U0001d7a2', # 𝞢 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL SIGMA
+ 'Υ': '\U0001d7a4', # 𝞤 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL UPSILON
+ 'Φ': '\U0001d7a5', # 𝞥 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PHI
+ 'Ψ': '\U0001d7a7', # 𝞧 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PSI
+ 'Ω': '\U0001d7a8', # 𝞨 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMEGA
+ 'α': '\U0001d7aa', # 𝞪 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ALPHA
+ 'β': '\U0001d7ab', # 𝞫 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL BETA
+ 'γ': '\U0001d7ac', # 𝞬 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL GAMMA
+ 'δ': '\U0001d7ad', # 𝞭 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL DELTA
+ 'ε': '\U0001d7ae', # 𝞮 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL EPSILON
+ 'ζ': '\U0001d7af', # 𝞯 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ZETA
+ 'η': '\U0001d7b0', # 𝞰 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ETA
+ 'θ': '\U0001d7b1', # 𝞱 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL THETA
+ 'ι': '\U0001d7b2', # 𝞲 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL IOTA
+ 'κ': '\U0001d7b3', # 𝞳 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL KAPPA
+ 'λ': '\U0001d7b4', # 𝞴 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL LAMDA
+ 'μ': '\U0001d7b5', # 𝞵 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL MU
+ 'ν': '\U0001d7b6', # 𝞶 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL NU
+ 'ξ': '\U0001d7b7', # 𝞷 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL XI
+ 'π': '\U0001d7b9', # 𝞹 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PI
+ 'ρ': '\U0001d7ba', # 𝞺 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL RHO
+ 'ς': '\U0001d7bb', # 𝞻 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL FINAL SIGMA
+ 'σ': '\U0001d7bc', # 𝞼 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL SIGMA
+ 'τ': '\U0001d7bd', # 𝞽 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL TAU
+ 'υ': '\U0001d7be', # 𝞾 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL UPSILON
+ 'φ': '\U0001d7bf', # 𝞿 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PHI
+ 'χ': '\U0001d7c0', # 𝟀 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL CHI
+ 'ψ': '\U0001d7c1', # 𝟁 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PSI
+ 'ω': '\U0001d7c2', # 𝟂 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMEGA
+ 'ϑ': '\U0001d7c5', # 𝟅 MATHEMATICAL SANS-SERIF BOLD ITALIC THETA SYMBOL
+ 'ϕ': '\U0001d7c7', # 𝟇 MATHEMATICAL SANS-SERIF BOLD ITALIC PHI SYMBOL
+ 'ϖ': '\U0001d7c9', # 𝟉 MATHEMATICAL SANS-SERIF BOLD ITALIC PI SYMBOL
+ 'ϰ': '\U0001d7c6', # 𝟆 MATHEMATICAL SANS-SERIF BOLD ITALIC KAPPA SYMBOL
+ 'ϱ': '\U0001d7c8', # 𝟈 MATHEMATICAL SANS-SERIF BOLD ITALIC RHO SYMBOL
+ 'ϵ': '\U0001d7c4', # 𝟄 MATHEMATICAL SANS-SERIF BOLD ITALIC EPSILON SYMBOL
+ '∂': '\U0001d7c3', # 𝟃 MATHEMATICAL SANS-SERIF BOLD ITALIC PARTIAL DIFFERENTIAL
+ '∇': '\U0001d7a9', # 𝞩 MATHEMATICAL SANS-SERIF BOLD ITALIC NABLA
+ }
+
+mathsfit = {
+ 'A': '\U0001d608', # 𝘈 MATHEMATICAL SANS-SERIF ITALIC CAPITAL A
+ 'B': '\U0001d609', # 𝘉 MATHEMATICAL SANS-SERIF ITALIC CAPITAL B
+ 'C': '\U0001d60a', # 𝘊 MATHEMATICAL SANS-SERIF ITALIC CAPITAL C
+ 'D': '\U0001d60b', # 𝘋 MATHEMATICAL SANS-SERIF ITALIC CAPITAL D
+ 'E': '\U0001d60c', # 𝘌 MATHEMATICAL SANS-SERIF ITALIC CAPITAL E
+ 'F': '\U0001d60d', # 𝘍 MATHEMATICAL SANS-SERIF ITALIC CAPITAL F
+ 'G': '\U0001d60e', # 𝘎 MATHEMATICAL SANS-SERIF ITALIC CAPITAL G
+ 'H': '\U0001d60f', # 𝘏 MATHEMATICAL SANS-SERIF ITALIC CAPITAL H
+ 'I': '\U0001d610', # 𝘐 MATHEMATICAL SANS-SERIF ITALIC CAPITAL I
+ 'J': '\U0001d611', # 𝘑 MATHEMATICAL SANS-SERIF ITALIC CAPITAL J
+ 'K': '\U0001d612', # 𝘒 MATHEMATICAL SANS-SERIF ITALIC CAPITAL K
+ 'L': '\U0001d613', # 𝘓 MATHEMATICAL SANS-SERIF ITALIC CAPITAL L
+ 'M': '\U0001d614', # 𝘔 MATHEMATICAL SANS-SERIF ITALIC CAPITAL M
+ 'N': '\U0001d615', # 𝘕 MATHEMATICAL SANS-SERIF ITALIC CAPITAL N
+ 'O': '\U0001d616', # 𝘖 MATHEMATICAL SANS-SERIF ITALIC CAPITAL O
+ 'P': '\U0001d617', # 𝘗 MATHEMATICAL SANS-SERIF ITALIC CAPITAL P
+ 'Q': '\U0001d618', # 𝘘 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Q
+ 'R': '\U0001d619', # 𝘙 MATHEMATICAL SANS-SERIF ITALIC CAPITAL R
+ 'S': '\U0001d61a', # 𝘚 MATHEMATICAL SANS-SERIF ITALIC CAPITAL S
+ 'T': '\U0001d61b', # 𝘛 MATHEMATICAL SANS-SERIF ITALIC CAPITAL T
+ 'U': '\U0001d61c', # 𝘜 MATHEMATICAL SANS-SERIF ITALIC CAPITAL U
+ 'V': '\U0001d61d', # 𝘝 MATHEMATICAL SANS-SERIF ITALIC CAPITAL V
+ 'W': '\U0001d61e', # 𝘞 MATHEMATICAL SANS-SERIF ITALIC CAPITAL W
+ 'X': '\U0001d61f', # 𝘟 MATHEMATICAL SANS-SERIF ITALIC CAPITAL X
+ 'Y': '\U0001d620', # 𝘠 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Y
+ 'Z': '\U0001d621', # 𝘡 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Z
+ 'a': '\U0001d622', # 𝘢 MATHEMATICAL SANS-SERIF ITALIC SMALL A
+ 'b': '\U0001d623', # 𝘣 MATHEMATICAL SANS-SERIF ITALIC SMALL B
+ 'c': '\U0001d624', # 𝘤 MATHEMATICAL SANS-SERIF ITALIC SMALL C
+ 'd': '\U0001d625', # 𝘥 MATHEMATICAL SANS-SERIF ITALIC SMALL D
+ 'e': '\U0001d626', # 𝘦 MATHEMATICAL SANS-SERIF ITALIC SMALL E
+ 'f': '\U0001d627', # 𝘧 MATHEMATICAL SANS-SERIF ITALIC SMALL F
+ 'g': '\U0001d628', # 𝘨 MATHEMATICAL SANS-SERIF ITALIC SMALL G
+ 'h': '\U0001d629', # 𝘩 MATHEMATICAL SANS-SERIF ITALIC SMALL H
+ 'i': '\U0001d62a', # 𝘪 MATHEMATICAL SANS-SERIF ITALIC SMALL I
+ 'j': '\U0001d62b', # 𝘫 MATHEMATICAL SANS-SERIF ITALIC SMALL J
+ 'k': '\U0001d62c', # 𝘬 MATHEMATICAL SANS-SERIF ITALIC SMALL K
+ 'l': '\U0001d62d', # 𝘭 MATHEMATICAL SANS-SERIF ITALIC SMALL L
+ 'm': '\U0001d62e', # 𝘮 MATHEMATICAL SANS-SERIF ITALIC SMALL M
+ 'n': '\U0001d62f', # 𝘯 MATHEMATICAL SANS-SERIF ITALIC SMALL N
+ 'o': '\U0001d630', # 𝘰 MATHEMATICAL SANS-SERIF ITALIC SMALL O
+ 'p': '\U0001d631', # 𝘱 MATHEMATICAL SANS-SERIF ITALIC SMALL P
+ 'q': '\U0001d632', # 𝘲 MATHEMATICAL SANS-SERIF ITALIC SMALL Q
+ 'r': '\U0001d633', # 𝘳 MATHEMATICAL SANS-SERIF ITALIC SMALL R
+ 's': '\U0001d634', # 𝘴 MATHEMATICAL SANS-SERIF ITALIC SMALL S
+ 't': '\U0001d635', # 𝘵 MATHEMATICAL SANS-SERIF ITALIC SMALL T
+ 'u': '\U0001d636', # 𝘶 MATHEMATICAL SANS-SERIF ITALIC SMALL U
+ 'v': '\U0001d637', # 𝘷 MATHEMATICAL SANS-SERIF ITALIC SMALL V
+ 'w': '\U0001d638', # 𝘸 MATHEMATICAL SANS-SERIF ITALIC SMALL W
+ 'x': '\U0001d639', # 𝘹 MATHEMATICAL SANS-SERIF ITALIC SMALL X
+ 'y': '\U0001d63a', # 𝘺 MATHEMATICAL SANS-SERIF ITALIC SMALL Y
+ 'z': '\U0001d63b', # 𝘻 MATHEMATICAL SANS-SERIF ITALIC SMALL Z
+ }
+
+mathtt = {
+ '0': '\U0001d7f6', # 𝟶 MATHEMATICAL MONOSPACE DIGIT ZERO
+ '1': '\U0001d7f7', # 𝟷 MATHEMATICAL MONOSPACE DIGIT ONE
+ '2': '\U0001d7f8', # 𝟸 MATHEMATICAL MONOSPACE DIGIT TWO
+ '3': '\U0001d7f9', # 𝟹 MATHEMATICAL MONOSPACE DIGIT THREE
+ '4': '\U0001d7fa', # 𝟺 MATHEMATICAL MONOSPACE DIGIT FOUR
+ '5': '\U0001d7fb', # 𝟻 MATHEMATICAL MONOSPACE DIGIT FIVE
+ '6': '\U0001d7fc', # 𝟼 MATHEMATICAL MONOSPACE DIGIT SIX
+ '7': '\U0001d7fd', # 𝟽 MATHEMATICAL MONOSPACE DIGIT SEVEN
+ '8': '\U0001d7fe', # 𝟾 MATHEMATICAL MONOSPACE DIGIT EIGHT
+ '9': '\U0001d7ff', # 𝟿 MATHEMATICAL MONOSPACE DIGIT NINE
+ 'A': '\U0001d670', # 𝙰 MATHEMATICAL MONOSPACE CAPITAL A
+ 'B': '\U0001d671', # 𝙱 MATHEMATICAL MONOSPACE CAPITAL B
+ 'C': '\U0001d672', # 𝙲 MATHEMATICAL MONOSPACE CAPITAL C
+ 'D': '\U0001d673', # 𝙳 MATHEMATICAL MONOSPACE CAPITAL D
+ 'E': '\U0001d674', # 𝙴 MATHEMATICAL MONOSPACE CAPITAL E
+ 'F': '\U0001d675', # 𝙵 MATHEMATICAL MONOSPACE CAPITAL F
+ 'G': '\U0001d676', # 𝙶 MATHEMATICAL MONOSPACE CAPITAL G
+ 'H': '\U0001d677', # 𝙷 MATHEMATICAL MONOSPACE CAPITAL H
+ 'I': '\U0001d678', # 𝙸 MATHEMATICAL MONOSPACE CAPITAL I
+ 'J': '\U0001d679', # 𝙹 MATHEMATICAL MONOSPACE CAPITAL J
+ 'K': '\U0001d67a', # 𝙺 MATHEMATICAL MONOSPACE CAPITAL K
+ 'L': '\U0001d67b', # 𝙻 MATHEMATICAL MONOSPACE CAPITAL L
+ 'M': '\U0001d67c', # 𝙼 MATHEMATICAL MONOSPACE CAPITAL M
+ 'N': '\U0001d67d', # 𝙽 MATHEMATICAL MONOSPACE CAPITAL N
+ 'O': '\U0001d67e', # 𝙾 MATHEMATICAL MONOSPACE CAPITAL O
+ 'P': '\U0001d67f', # 𝙿 MATHEMATICAL MONOSPACE CAPITAL P
+ 'Q': '\U0001d680', # 𝚀 MATHEMATICAL MONOSPACE CAPITAL Q
+ 'R': '\U0001d681', # 𝚁 MATHEMATICAL MONOSPACE CAPITAL R
+ 'S': '\U0001d682', # 𝚂 MATHEMATICAL MONOSPACE CAPITAL S
+ 'T': '\U0001d683', # 𝚃 MATHEMATICAL MONOSPACE CAPITAL T
+ 'U': '\U0001d684', # 𝚄 MATHEMATICAL MONOSPACE CAPITAL U
+ 'V': '\U0001d685', # 𝚅 MATHEMATICAL MONOSPACE CAPITAL V
+ 'W': '\U0001d686', # 𝚆 MATHEMATICAL MONOSPACE CAPITAL W
+ 'X': '\U0001d687', # 𝚇 MATHEMATICAL MONOSPACE CAPITAL X
+ 'Y': '\U0001d688', # 𝚈 MATHEMATICAL MONOSPACE CAPITAL Y
+ 'Z': '\U0001d689', # 𝚉 MATHEMATICAL MONOSPACE CAPITAL Z
+ 'a': '\U0001d68a', # 𝚊 MATHEMATICAL MONOSPACE SMALL A
+ 'b': '\U0001d68b', # 𝚋 MATHEMATICAL MONOSPACE SMALL B
+ 'c': '\U0001d68c', # 𝚌 MATHEMATICAL MONOSPACE SMALL C
+ 'd': '\U0001d68d', # 𝚍 MATHEMATICAL MONOSPACE SMALL D
+ 'e': '\U0001d68e', # 𝚎 MATHEMATICAL MONOSPACE SMALL E
+ 'f': '\U0001d68f', # 𝚏 MATHEMATICAL MONOSPACE SMALL F
+ 'g': '\U0001d690', # 𝚐 MATHEMATICAL MONOSPACE SMALL G
+ 'h': '\U0001d691', # 𝚑 MATHEMATICAL MONOSPACE SMALL H
+ 'i': '\U0001d692', # 𝚒 MATHEMATICAL MONOSPACE SMALL I
+ 'j': '\U0001d693', # 𝚓 MATHEMATICAL MONOSPACE SMALL J
+ 'k': '\U0001d694', # 𝚔 MATHEMATICAL MONOSPACE SMALL K
+ 'l': '\U0001d695', # 𝚕 MATHEMATICAL MONOSPACE SMALL L
+ 'm': '\U0001d696', # 𝚖 MATHEMATICAL MONOSPACE SMALL M
+ 'n': '\U0001d697', # 𝚗 MATHEMATICAL MONOSPACE SMALL N
+ 'o': '\U0001d698', # 𝚘 MATHEMATICAL MONOSPACE SMALL O
+ 'p': '\U0001d699', # 𝚙 MATHEMATICAL MONOSPACE SMALL P
+ 'q': '\U0001d69a', # 𝚚 MATHEMATICAL MONOSPACE SMALL Q
+ 'r': '\U0001d69b', # 𝚛 MATHEMATICAL MONOSPACE SMALL R
+ 's': '\U0001d69c', # 𝚜 MATHEMATICAL MONOSPACE SMALL S
+ 't': '\U0001d69d', # 𝚝 MATHEMATICAL MONOSPACE SMALL T
+ 'u': '\U0001d69e', # 𝚞 MATHEMATICAL MONOSPACE SMALL U
+ 'v': '\U0001d69f', # 𝚟 MATHEMATICAL MONOSPACE SMALL V
+ 'w': '\U0001d6a0', # 𝚠 MATHEMATICAL MONOSPACE SMALL W
+ 'x': '\U0001d6a1', # 𝚡 MATHEMATICAL MONOSPACE SMALL X
+ 'y': '\U0001d6a2', # 𝚢 MATHEMATICAL MONOSPACE SMALL Y
+ 'z': '\U0001d6a3', # 𝚣 MATHEMATICAL MONOSPACE SMALL Z
+ }
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/mathml_elements.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/mathml_elements.py
new file mode 100644
index 00000000..f2059c9f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/mathml_elements.py
@@ -0,0 +1,478 @@
+# :Id: $Id: mathml_elements.py 9561 2024-03-14 16:34:48Z milde $
+# :Copyright: 2024 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
+
+"""MathML element classes based on `xml.etree`.
+
+The module is intended for programmatic generation of MathML
+and covers the part of `MathML Core`_ that is required by
+Docutil's *TeX math to MathML* converter.
+
+This module is PROVISIONAL:
+the API is not settled and may change with any minor Docutils version.
+
+.. _MathML Core: https://www.w3.org/TR/mathml-core/
+"""
+
+# Usage:
+#
+# >>> from mathml_elements import *
+
+import numbers
+import xml.etree.ElementTree as ET
+
+
+GLOBAL_ATTRIBUTES = (
+ 'class', # space-separated list of element classes
+ # 'data-*', # custom data attributes (see HTML)
+ 'dir', # directionality ('ltr', 'rtl')
+ 'displaystyle', # True: normal, False: compact
+ 'id', # unique identifier
+ # 'mathbackground', # color definition, deprecated
+ # 'mathcolor', # color definition, deprecated
+ # 'mathsize', # font-size, deprecated
+ 'nonce', # cryptographic nonce ("number used once")
+ 'scriptlevel', # math-depth for the element
+ 'style', # CSS styling declarations
+ 'tabindex', # indicate if the element takes input focus
+ )
+"""Global MathML attributes
+
+https://w3c.github.io/mathml-core/#global-attributes
+"""
+
+
+# Base classes
+# ------------
+
+class MathElement(ET.Element):
+ """Base class for MathML elements."""
+
+ nchildren = None
+ """Expected number of children or None"""
+ # cf. https://www.w3.org/TR/MathML3/chapter3.html#id.3.1.3.2
+ parent = None
+ """Parent node in MathML element tree."""
+
+ def __init__(self, *children, **attributes):
+ """Set up node with `children` and `attributes`.
+
+ Attribute names are normalised to lowercase.
+ You may use "CLASS" to set a "class" attribute.
+ Attribute values are converted to strings
+ (with True -> "true" and False -> "false").
+
+ >>> math(CLASS='test', level=3, split=True)
+ math(class='test', level='3', split='true')
+ >>> math(CLASS='test', level=3, split=True).toxml()
+ '<math class="test" level="3" split="true"></math>'
+
+ """
+ attrib = {k.lower(): self.a_str(v) for k, v in attributes.items()}
+ super().__init__(self.__class__.__name__, **attrib)
+ self.extend(children)
+
+ @staticmethod
+ def a_str(v):
+ # Return string representation for attribute value `v`.
+ if isinstance(v, bool):
+ return str(v).lower()
+ return str(v)
+
+ def __repr__(self):
+ """Return full string representation."""
+ args = [repr(child) for child in self]
+ if self.text:
+ args.append(repr(self.text))
+ if self.nchildren != self.__class__.nchildren:
+ args.append(f'nchildren={self.nchildren}')
+ if getattr(self, 'switch', None):
+ args.append('switch=True')
+ args += [f'{k}={v!r}' for k, v in self.items() if v is not None]
+ return f'{self.tag}({", ".join(args)})'
+
+ def __str__(self):
+ """Return concise, informal string representation."""
+ if self.text:
+ args = repr(self.text)
+ else:
+ args = ', '.join(f'{child}' for child in self)
+ return f'{self.tag}({args})'
+
+ def set(self, key, value):
+ super().set(key, self.a_str(value))
+
+ def __setitem__(self, key, value):
+ if self.nchildren == 0:
+ raise TypeError(f'Element "{self}" does not take children.')
+ if isinstance(value, MathElement):
+ value.parent = self
+ else: # value may be an iterable
+ if self.nchildren and len(self) + len(value) > self.nchildren:
+ raise TypeError(f'Element "{self}" takes only {self.nchildren}'
+ ' children')
+ for e in value:
+ e.parent = self
+ super().__setitem__(key, value)
+
+ def is_full(self):
+ """Return boolean indicating whether children may be appended."""
+ return self.nchildren is not None and len(self) >= self.nchildren
+
+ def close(self):
+ """Close element and return first non-full anchestor or None."""
+ self.nchildren = len(self) # mark node as full
+ parent = self.parent
+ while parent is not None and parent.is_full():
+ parent = parent.parent
+ return parent
+
+ def append(self, element):
+ """Append `element` and return new "current node" (insertion point).
+
+ Append as child element and set the internal `parent` attribute.
+
+ If self is already full, raise TypeError.
+
+ If self is full after appending, call `self.close()`
+ (returns first non-full anchestor or None) else return `self`.
+ """
+ if self.is_full():
+ if self.nchildren:
+ status = f'takes only {self.nchildren} children'
+ else:
+ status = 'does not take children'
+ raise TypeError(f'Element "{self}" {status}.')
+ super().append(element)
+ element.parent = self
+ if self.is_full():
+ return self.close()
+ return self
+
+ def extend(self, elements):
+ """Sequentially append `elements`. Return new "current node".
+
+ Raise TypeError if overfull.
+ """
+ current_node = self
+ for element in elements:
+ current_node = self.append(element)
+ return current_node
+
+ def pop(self, index=-1):
+ element = self[index]
+ del self[index]
+ return element
+
+ def in_block(self):
+ """Return True, if `self` or an ancestor has ``display='block'``.
+
+ Used to find out whether we are in inline vs. displayed maths.
+ """
+ if self.get('display') is None:
+ try:
+ return self.parent.in_block()
+ except AttributeError:
+ return False
+ return self.get('display') == 'block'
+
+ # XML output:
+
+ def indent_xml(self, space=' ', level=0):
+ """Format XML output with indents.
+
+ Use with care:
+ Formatting whitespace is permanently added to the
+ `text` and `tail` attributes of `self` and anchestors!
+ """
+ ET.indent(self, space, level)
+
+ def unindent_xml(self):
+ """Strip whitespace at the end of `text` and `tail` attributes...
+
+ to revert changes made by the `indent_xml()` method.
+ Use with care, trailing whitespace from the original may be lost.
+ """
+ for e in self.iter():
+ if not isinstance(e, MathToken) and e.text:
+ e.text = e.text.rstrip()
+ if e.tail:
+ e.tail = e.tail.rstrip()
+
+ def toxml(self, encoding=None):
+ """Return an XML representation of the element.
+
+ By default, the return value is a `str` instance. With an explicit
+ `encoding` argument, the result is a `bytes` instance in the
+ specified encoding. The XML default encoding is UTF-8, any other
+ encoding must be specified in an XML document header.
+
+ Name and encoding handling match `xml.dom.minidom.Node.toxml()`
+ while `etree.Element.tostring()` returns `bytes` by default.
+ """
+ xml = ET.tostring(self, encoding or 'unicode',
+ short_empty_elements=False)
+ # Visible representation for "Apply Function" character:
+ try:
+ xml = xml.replace('\u2061', '&ApplyFunction;')
+ except TypeError:
+ xml = xml.replace('\u2061'.encode(encoding), b'&ApplyFunction;')
+ return xml
+
+
+# Group sub-expressions in a horizontal row
+#
+# The elements <msqrt>, <mstyle>, <merror>, <mpadded>, <mphantom>,
+# <menclose>, <mtd>, <mscarry>, and <math> treat their contents
+# as a single inferred mrow formed from all their children.
+# (https://www.w3.org/TR/mathml4/#presm_inferredmrow)
+#
+# MathML Core uses the term "anonymous mrow element".
+
+class MathRow(MathElement):
+ """Base class for elements treating content as a single mrow."""
+
+
+# 2d Schemata
+
+class MathSchema(MathElement):
+ """Base class for schemata expecting 2 or more children.
+
+ The special attribute `switch` indicates that the last two child
+ elements are in reversed order and must be switched before XML-export.
+ See `msub` for an example.
+ """
+ nchildren = 2
+
+ def __init__(self, *children, **kwargs):
+ self.switch = kwargs.pop('switch', False)
+ super().__init__(*children, **kwargs)
+
+ def append(self, element):
+ """Append element. Normalize order and close if full."""
+ current_node = super().append(element)
+ if self.switch and self.is_full():
+ self[-1], self[-2] = self[-2], self[-1]
+ self.switch = False
+ return current_node
+
+
+# Token elements represent the smallest units of mathematical notation which
+# carry meaning.
+
+class MathToken(MathElement):
+ """Token Element: contains textual data instead of children.
+
+ Expect text data on initialisation.
+ """
+ nchildren = 0
+
+ def __init__(self, text, **attributes):
+ super().__init__(**attributes)
+ if not isinstance(text, (str, numbers.Number)):
+ raise ValueError('MathToken element expects `str` or number,'
+ f' not "{text}".')
+ self.text = str(text)
+
+
+# MathML element classes
+# ----------------------
+
+class math(MathRow):
+ """Top-level MathML element, a single mathematical formula."""
+
+
+# Token elements
+# ~~~~~~~~~~~~~~
+
+class mtext(MathToken):
+ """Arbitrary text with no notational meaning."""
+
+
+class mi(MathToken):
+ """Identifier, such as a function name, variable or symbolic constant."""
+
+
+class mn(MathToken):
+ """Numeric literal.
+
+ >>> mn(3.41).toxml()
+ '<mn>3.41</mn>'
+
+ Normally a sequence of digits with a possible separator (a dot or a comma).
+ (Values with comma must be specified as `str`.)
+ """
+
+
+class mo(MathToken):
+ """Operator, Fence, Separator, or Accent.
+
+ >>> mo('<').toxml()
+ '<mo>&lt;</mo>'
+
+ Besides operators in strict mathematical meaning, this element also
+ includes "operators" like parentheses, separators like comma and
+ semicolon, or "absolute value" bars.
+ """
+
+
+class mspace(MathElement):
+ """Blank space, whose size is set by its attributes.
+
+ Takes additional attributes `depth`, `height`, `width`.
+ Takes no children and no text.
+
+ See also `mphantom`.
+ """
+ nchildren = 0
+
+
+# General Layout Schemata
+# ~~~~~~~~~~~~~~~~~~~~~~~
+
+class mrow(MathRow):
+ """Generic element to group children as a horizontal row.
+
+ Removed on closing if not required (see `mrow.close()`).
+ """
+
+ def transfer_attributes(self, other):
+ """Transfer attributes from self to other.
+
+ "List values" (class, style) are appended to existing values,
+ other values replace existing values.
+ """
+ delimiters = {'class': ' ', 'style': '; '}
+ for k, v in self.items():
+ if k in ('class', 'style') and v:
+ if other.get(k):
+ v = delimiters[k].join(
+ (other.get(k).rstrip(delimiters[k]), v))
+ other.set(k, v)
+
+ def close(self):
+ """Close element and return first non-full anchestor or None.
+
+ Remove <mrow> if it has only one child element.
+ """
+ parent = self.parent
+ # replace `self` with single child
+ if parent is not None and len(self) == 1:
+ child = self[0]
+ try:
+ parent[list(parent).index(self)] = child
+ child.parent = parent
+ except (AttributeError, ValueError):
+ return None
+ self.transfer_attributes(child)
+ return super().close()
+
+
+class mfrac(MathSchema):
+ """Fractions or fraction-like objects such as binomial coefficients."""
+
+
+class msqrt(MathRow):
+ """Square root. See also `mroot`."""
+ nchildren = 1 # \sqrt expects one argument or a group
+
+
+class mroot(MathSchema):
+ """Roots with an explicit index. See also `msqrt`."""
+
+
+class mstyle(MathRow):
+ """Style Change.
+
+ In modern browsers, <mstyle> is equivalent to an <mrow> element.
+ However, <mstyle> may still be relevant for compatibility with
+ MathML implementations outside browsers.
+ """
+
+
+class merror(MathRow):
+ """Display contents as error messages."""
+
+
+class menclose(MathRow):
+ """Renders content inside an enclosing notation...
+
+ ... specified by the notation attribute.
+
+ Non-standard but still required by Firefox for boxed expressions.
+ """
+ nchildren = 1 # \boxed expects one argument or a group
+
+
+class mpadded(MathRow):
+ """Adjust space around content."""
+ # nchildren = 1 # currently not used by latex2mathml
+
+
+class mphantom(MathRow):
+ """Placeholder: Rendered invisibly but dimensions are kept."""
+ nchildren = 1 # \phantom expects one argument or a group
+
+
+# Script and Limit Schemata
+# ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+class msub(MathSchema):
+ """Attach a subscript to an expression."""
+
+
+class msup(MathSchema):
+ """Attach a superscript to an expression."""
+
+
+class msubsup(MathSchema):
+ """Attach both a subscript and a superscript to an expression."""
+ nchildren = 3
+
+# Examples:
+#
+# The `switch` attribute reverses the order of the last two children:
+# >>> msub(mn(1), mn(2)).toxml()
+# '<msub><mn>1</mn><mn>2</mn></msub>'
+# >>> msub(mn(1), mn(2), switch=True).toxml()
+# '<msub><mn>2</mn><mn>1</mn></msub>'
+#
+# >>> msubsup(mi('base'), mn(1), mn(2)).toxml()
+# '<msubsup><mi>base</mi><mn>1</mn><mn>2</mn></msubsup>'
+# >>> msubsup(mi('base'), mn(1), mn(2), switch=True).toxml()
+# '<msubsup><mi>base</mi><mn>2</mn><mn>1</mn></msubsup>'
+
+
+class munder(msub):
+ """Attach an accent or a limit under an expression."""
+
+
+class mover(msup):
+ """Attach an accent or a limit over an expression."""
+
+
+class munderover(msubsup):
+ """Attach accents or limits both under and over an expression."""
+
+
+# Tabular Math
+# ~~~~~~~~~~~~
+
+class mtable(MathElement):
+ """Table or matrix element."""
+
+
+class mtr(MathRow):
+ """Row in a table or a matrix."""
+
+
+class mtd(MathRow):
+ """Cell in a table or a matrix"""
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py
new file mode 100644
index 00000000..11f9ab3e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2mathml_extern.py
@@ -0,0 +1,261 @@
+# :Id: $Id: tex2mathml_extern.py 9536 2024-02-01 13:04:22Z 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.
+#
+# __ https://opensource.org/licenses/BSD-2-Clause
+
+"""Wrappers for TeX->MathML conversion by external tools
+
+This module is provisional:
+the API is not settled and may change with any minor Docutils version.
+"""
+
+import subprocess
+
+from docutils import nodes
+from docutils.utils.math import MathError, wrap_math_code
+
+# `latexml` expects a complete document:
+document_template = r"""\documentclass{article}
+\begin{document}
+%s
+\end{document}
+"""
+
+
+def _check_result(result, details=[]):
+ # raise MathError if the conversion went wrong
+ # :details: list of doctree nodes with additional info
+ msg = ''
+ if not details and result.stderr:
+ details = [nodes.paragraph('', result.stderr, classes=['pre-wrap'])]
+ if details:
+ msg = f'TeX to MathML converter `{result.args[0]}` failed:'
+ elif result.returncode:
+ msg = (f'TeX to MathMl converter `{result.args[0]}` '
+ f'exited with Errno {result.returncode}.')
+ elif not result.stdout:
+ msg = f'TeX to MathML converter `{result.args[0]}` returned no MathML.'
+ if msg:
+ raise MathError(msg, details=details)
+
+
+def blahtexml(math_code, as_block=False):
+ """Convert LaTeX math code to MathML with blahtexml__.
+
+ __ http://gva.noekeon.org/blahtexml/
+ """
+ args = ['blahtexml',
+ '--mathml',
+ '--indented',
+ '--spacing', 'moderate',
+ '--mathml-encoding', 'raw',
+ '--other-encoding', 'raw',
+ '--doctype-xhtml+mathml',
+ '--annotate-TeX',
+ ]
+ # "blahtexml" expects LaTeX code without math-mode-switch.
+ # We still need to tell it about displayed equation(s).
+ mathml_args = ' display="block"' if as_block else ''
+ _wrapped = wrap_math_code(math_code, as_block)
+ if '{align*}' in _wrapped:
+ math_code = _wrapped.replace('{align*}', '{aligned}')
+
+ result = subprocess.run(args, input=math_code,
+ capture_output=True, text=True)
+
+ # blahtexml writes <error> messages to stdout
+ if '<error>' in result.stdout:
+ result.stderr = result.stdout[result.stdout.find('<message>')+9:
+ result.stdout.find('</message>')]
+ else:
+ result.stdout = result.stdout[result.stdout.find('<markup>')+9:
+ result.stdout.find('</markup>')]
+ _check_result(result)
+ return (f'<math xmlns="http://www.w3.org/1998/Math/MathML"{mathml_args}>'
+ f'\n{result.stdout}</math>')
+
+
+def latexml(math_code, as_block=False):
+ """Convert LaTeX math code to MathML with LaTeXML__.
+
+ Comprehensive macro support but **very** slow.
+
+ __ http://dlmf.nist.gov/LaTeXML/
+ """
+
+ # LaTeXML works in 2 stages, expects complete documents.
+ #
+ # The `latexmlmath`__ convenience wrapper does not support block-level
+ # (displayed) equations.
+ #
+ # __ https://metacpan.org/dist/LaTeXML/view/bin/latexmlmath
+ args1 = ['latexml',
+ '-', # read from stdin
+ '--preload=amsmath',
+ '--preload=amssymb', # also loads amsfonts
+ '--inputencoding=utf8',
+ '--',
+ ]
+ math_code = document_template % wrap_math_code(math_code, as_block)
+
+ result1 = subprocess.run(args1, input=math_code,
+ capture_output=True, text=True)
+ if result1.stderr:
+ result1.stderr = '\n'.join(line for line in result1.stderr.splitlines()
+ if line.startswith('Error:')
+ or line.startswith('Warning:')
+ or line.startswith('Fatal:'))
+ _check_result(result1)
+
+ args2 = ['latexmlpost',
+ '-',
+ '--nonumbersections',
+ '--format=html5', # maths included as MathML
+ '--omitdoctype', # Make it simple, we only need the maths.
+ '--noscan', # ...
+ '--nocrossref',
+ '--nographicimages',
+ '--nopictureimages',
+ '--nodefaultresources', # do not copy *.css files to output dir
+ '--'
+ ]
+ result2 = subprocess.run(args2, input=result1.stdout,
+ capture_output=True, text=True)
+ # Extract MathML from HTML document:
+ # <table> with <math> in cells for "align", <math> element else.
+ start = result2.stdout.find('<table class="ltx_equationgroup')
+ if start != -1:
+ stop = result2.stdout.find('</table>', start)+8
+ result2.stdout = result2.stdout[start:stop].replace(
+ 'ltx_equationgroup', 'borderless align-center')
+ else:
+ result2.stdout = result2.stdout[result2.stdout.find('<math'):
+ result2.stdout.find('</math>')+7]
+ # Search for error messages
+ if result2.stdout:
+ _msg_source = result2.stdout # latexmlpost reports errors in output
+ else:
+ _msg_source = result2.stderr # just in case
+ result2.stderr = '\n'.join(line for line in _msg_source.splitlines()
+ if line.startswith('Error:')
+ or line.startswith('Warning:')
+ or line.startswith('Fatal:'))
+ _check_result(result2)
+ return result2.stdout
+
+
+def pandoc(math_code, as_block=False):
+ """Convert LaTeX math code to MathML with pandoc__.
+
+ __ https://pandoc.org/
+ """
+ args = ['pandoc',
+ '--mathml',
+ '--from=latex',
+ ]
+ result = subprocess.run(args, input=wrap_math_code(math_code, as_block),
+ capture_output=True, text=True)
+
+ result.stdout = result.stdout[result.stdout.find('<math'):
+ result.stdout.find('</math>')+7]
+ # Pandoc (2.9.2.1) messages are pre-formatted for the terminal:
+ # 1. summary
+ # 2. math source (part)
+ # 3. error spot indicator '^' (works only in a literal block)
+ # 4. assumed problem
+ # 5. assumed solution (may be wrong or confusing)
+ # Construct a "details" list:
+ details = []
+ if result.stderr:
+ lines = result.stderr.splitlines()
+ details.append(nodes.paragraph('', lines[0]))
+ details.append(nodes.literal_block('', '\n'.join(lines[1:3])))
+ details.append(nodes.paragraph('', '\n'.join(lines[3:]),
+ classes=['pre-wrap']))
+ _check_result(result, details=details)
+ return result.stdout
+
+
+def ttm(math_code, as_block=False):
+ """Convert LaTeX math code to MathML with TtM__.
+
+ Aged, limited, but fast.
+
+ __ http://silas.psfc.mit.edu/tth/mml/
+ """
+ args = ['ttm',
+ '-L', # source is LaTeX snippet
+ '-r'] # output MathML snippet
+ math_code = wrap_math_code(math_code, as_block)
+
+ # "ttm" does not support UTF-8 input. (Docutils converts most math
+ # characters to LaTeX commands before calling this function.)
+ try:
+ result = subprocess.run(args, input=math_code,
+ capture_output=True, text=True,
+ encoding='ISO-8859-1')
+ except UnicodeEncodeError as err:
+ raise MathError(err)
+
+ result.stdout = result.stdout[result.stdout.find('<math'):
+ result.stdout.find('</math>')+7]
+ if as_block:
+ result.stdout = result.stdout.replace('<math xmlns=',
+ '<math display="block" xmlns=')
+ result.stderr = '\n'.join(line[5:] + '.'
+ for line in result.stderr.splitlines()
+ if line.startswith('**** '))
+ _check_result(result)
+ return result.stdout
+
+
+# self-test
+
+if __name__ == "__main__":
+ example = (r'\frac{\partial \sin^2(\alpha)}{\partial \vec r}'
+ r'\varpi \mathbb{R} \, \text{Grüße}')
+
+ print("""<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<title>test external mathml converters</title>
+</head>
+<body>
+<p>Test external converters</p>
+<p>
+""")
+ print(f'latexml: {latexml(example)},')
+ print(f'ttm: {ttm(example.replace("mathbb", "mathbf"))},')
+ print(f'blahtexml: {blahtexml(example)},')
+ print(f'pandoc: {pandoc(example)}.')
+ print('</p>')
+
+ print('<p>latexml:</p>')
+ print(latexml(example, as_block=True))
+ print('<p>ttm:</p>')
+ print(ttm(example.replace('mathbb', 'mathbf'), as_block=True))
+ print('<p>blahtexml:</p>')
+ print(blahtexml(example, as_block=True))
+ print('<p>pandoc:</p>')
+ print(pandoc(example, as_block=True))
+
+ print('</main>\n</body>\n</html>')
+
+ buggy = r'\sinc \phy'
+ # buggy = '\sqrt[e]'
+ try:
+ # print(blahtexml(buggy))
+ # print(latexml(f'${buggy}$'))
+ print(pandoc(f'${buggy}$'))
+ # print(ttm(f'${buggy}$'))
+ except MathError as err:
+ print(err)
+ print(err.details)
+ for node in err.details:
+ print(node.astext())
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2unichar.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2unichar.py
new file mode 100644
index 00000000..c84e8a6f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/tex2unichar.py
@@ -0,0 +1,730 @@
+#!/usr/bin/env python3
+
+# LaTeX math to Unicode symbols translation dictionaries.
+# Generated with ``write_tex2unichar.py`` from the data in
+# http://milde.users.sourceforge.net/LUCR/Math/
+
+# Includes commands from:
+# standard LaTeX
+# amssymb
+# amsmath
+# amsxtra
+# bbold
+# esint
+# mathabx
+# mathdots
+# txfonts
+# stmaryrd
+# wasysym
+
+mathaccent = {
+ 'acute': '\u0301', #  ́ COMBINING ACUTE ACCENT
+ 'bar': '\u0304', #  ̄ COMBINING MACRON
+ 'breve': '\u0306', #  ̆ COMBINING BREVE
+ 'check': '\u030c', #  ̌ COMBINING CARON
+ 'ddddot': '\u20dc', #  ⃜ COMBINING FOUR DOTS ABOVE
+ 'dddot': '\u20db', #  ⃛ COMBINING THREE DOTS ABOVE
+ 'ddot': '\u0308', #  ̈ COMBINING DIAERESIS
+ 'dot': '\u0307', #  ̇ COMBINING DOT ABOVE
+ 'grave': '\u0300', #  ̀ COMBINING GRAVE ACCENT
+ 'hat': '\u0302', #  ̂ COMBINING CIRCUMFLEX ACCENT
+ 'mathring': '\u030a', #  ̊ COMBINING RING ABOVE
+ 'not': '\u0338', #  ̸ COMBINING LONG SOLIDUS OVERLAY
+ 'overleftrightarrow': '\u20e1', #  ⃡ COMBINING LEFT RIGHT ARROW ABOVE
+ 'overline': '\u0305', #  ̅ COMBINING OVERLINE
+ 'tilde': '\u0303', #  ̃ COMBINING TILDE
+ 'underbar': '\u0331', #  ̱ COMBINING MACRON BELOW
+ 'underleftarrow': '\u20ee', #  ⃮ COMBINING LEFT ARROW BELOW
+ 'underline': '\u0332', #  ̲ COMBINING LOW LINE
+ 'underrightarrow': '\u20ef', #  ⃯ COMBINING RIGHT ARROW BELOW
+ 'vec': '\u20d7', #  ⃗ COMBINING RIGHT ARROW ABOVE
+ }
+
+mathalpha = {
+ 'Bbbk': '\U0001d55c', # 𝕜 MATHEMATICAL DOUBLE-STRUCK SMALL K
+ 'Delta': '\u0394', # Δ GREEK CAPITAL LETTER DELTA
+ 'Gamma': '\u0393', # Γ GREEK CAPITAL LETTER GAMMA
+ 'Im': '\u2111', # ℑ BLACK-LETTER CAPITAL I
+ 'Lambda': '\u039b', # Λ GREEK CAPITAL LETTER LAMDA
+ 'Omega': '\u03a9', # Ω GREEK CAPITAL LETTER OMEGA
+ 'Phi': '\u03a6', # Φ GREEK CAPITAL LETTER PHI
+ 'Pi': '\u03a0', # Π GREEK CAPITAL LETTER PI
+ 'Psi': '\u03a8', # Ψ GREEK CAPITAL LETTER PSI
+ 'Re': '\u211c', # ℜ BLACK-LETTER CAPITAL R
+ 'Sigma': '\u03a3', # Σ GREEK CAPITAL LETTER SIGMA
+ 'Theta': '\u0398', # Θ GREEK CAPITAL LETTER THETA
+ 'Upsilon': '\u03a5', # Υ GREEK CAPITAL LETTER UPSILON
+ 'Xi': '\u039e', # Ξ GREEK CAPITAL LETTER XI
+ 'aleph': '\u2135', # ℵ ALEF SYMBOL
+ 'alpha': '\u03b1', # α GREEK SMALL LETTER ALPHA
+ 'beta': '\u03b2', # β GREEK SMALL LETTER BETA
+ 'beth': '\u2136', # ℶ BET SYMBOL
+ 'chi': '\u03c7', # χ GREEK SMALL LETTER CHI
+ 'daleth': '\u2138', # ℸ DALET SYMBOL
+ 'delta': '\u03b4', # δ GREEK SMALL LETTER DELTA
+ 'digamma': '\u03dd', # ϝ GREEK SMALL LETTER DIGAMMA
+ 'ell': '\u2113', # ℓ SCRIPT SMALL L
+ 'epsilon': '\u03f5', # ϵ GREEK LUNATE EPSILON SYMBOL
+ 'eta': '\u03b7', # η GREEK SMALL LETTER ETA
+ 'eth': '\xf0', # ð LATIN SMALL LETTER ETH
+ 'gamma': '\u03b3', # γ GREEK SMALL LETTER GAMMA
+ 'gimel': '\u2137', # ℷ GIMEL SYMBOL
+ 'imath': '\u0131', # ı LATIN SMALL LETTER DOTLESS I
+ 'iota': '\u03b9', # ι GREEK SMALL LETTER IOTA
+ 'jmath': '\u0237', # ȷ LATIN SMALL LETTER DOTLESS J
+ 'kappa': '\u03ba', # κ GREEK SMALL LETTER KAPPA
+ 'lambda': '\u03bb', # λ GREEK SMALL LETTER LAMDA
+ 'mu': '\u03bc', # μ GREEK SMALL LETTER MU
+ 'nu': '\u03bd', # ν GREEK SMALL LETTER NU
+ 'omega': '\u03c9', # ω GREEK SMALL LETTER OMEGA
+ 'phi': '\u03d5', # ϕ GREEK PHI SYMBOL
+ 'pi': '\u03c0', # π GREEK SMALL LETTER PI
+ 'psi': '\u03c8', # ψ GREEK SMALL LETTER PSI
+ 'rho': '\u03c1', # ρ GREEK SMALL LETTER RHO
+ 'sigma': '\u03c3', # σ GREEK SMALL LETTER SIGMA
+ 'tau': '\u03c4', # τ GREEK SMALL LETTER TAU
+ 'theta': '\u03b8', # θ GREEK SMALL LETTER THETA
+ 'upsilon': '\u03c5', # υ GREEK SMALL LETTER UPSILON
+ 'varDelta': '\U0001d6e5', # 𝛥 MATHEMATICAL ITALIC CAPITAL DELTA
+ 'varGamma': '\U0001d6e4', # 𝛤 MATHEMATICAL ITALIC CAPITAL GAMMA
+ 'varLambda': '\U0001d6ec', # 𝛬 MATHEMATICAL ITALIC CAPITAL LAMDA
+ 'varOmega': '\U0001d6fa', # 𝛺 MATHEMATICAL ITALIC CAPITAL OMEGA
+ 'varPhi': '\U0001d6f7', # 𝛷 MATHEMATICAL ITALIC CAPITAL PHI
+ 'varPi': '\U0001d6f1', # 𝛱 MATHEMATICAL ITALIC CAPITAL PI
+ 'varPsi': '\U0001d6f9', # 𝛹 MATHEMATICAL ITALIC CAPITAL PSI
+ 'varSigma': '\U0001d6f4', # 𝛴 MATHEMATICAL ITALIC CAPITAL SIGMA
+ 'varTheta': '\U0001d6e9', # 𝛩 MATHEMATICAL ITALIC CAPITAL THETA
+ 'varUpsilon': '\U0001d6f6', # 𝛶 MATHEMATICAL ITALIC CAPITAL UPSILON
+ 'varXi': '\U0001d6ef', # 𝛯 MATHEMATICAL ITALIC CAPITAL XI
+ 'varepsilon': '\u03b5', # ε GREEK SMALL LETTER EPSILON
+ 'varkappa': '\u03f0', # ϰ GREEK KAPPA SYMBOL
+ 'varphi': '\u03c6', # φ GREEK SMALL LETTER PHI
+ 'varpi': '\u03d6', # ϖ GREEK PI SYMBOL
+ 'varrho': '\u03f1', # ϱ GREEK RHO SYMBOL
+ 'varsigma': '\u03c2', # ς GREEK SMALL LETTER FINAL SIGMA
+ 'vartheta': '\u03d1', # ϑ GREEK THETA SYMBOL
+ 'wp': '\u2118', # ℘ SCRIPT CAPITAL P
+ 'xi': '\u03be', # ξ GREEK SMALL LETTER XI
+ 'zeta': '\u03b6', # ζ GREEK SMALL LETTER ZETA
+ }
+
+mathbin = {
+ 'Cap': '\u22d2', # ⋒ DOUBLE INTERSECTION
+ 'Circle': '\u25cb', # ○ WHITE CIRCLE
+ 'Cup': '\u22d3', # ⋓ DOUBLE UNION
+ 'LHD': '\u25c0', # ◀ BLACK LEFT-POINTING TRIANGLE
+ 'RHD': '\u25b6', # ▶ BLACK RIGHT-POINTING TRIANGLE
+ 'amalg': '\u2a3f', # ⨿ AMALGAMATION OR COPRODUCT
+ 'ast': '\u2217', # ∗ ASTERISK OPERATOR
+ 'barwedge': '\u22bc', # ⊼ NAND
+ 'bigcirc': '\u25ef', # ◯ LARGE CIRCLE
+ 'bigtriangledown': '\u25bd', # ▽ WHITE DOWN-POINTING TRIANGLE
+ 'bigtriangleup': '\u25b3', # △ WHITE UP-POINTING TRIANGLE
+ 'bindnasrepma': '\u214b', # ⅋ TURNED AMPERSAND
+ 'blacklozenge': '\u29eb', # ⧫ BLACK LOZENGE
+ 'boxast': '\u29c6', # ⧆ SQUARED ASTERISK
+ 'boxbar': '\u25eb', # ◫ WHITE SQUARE WITH VERTICAL BISECTING LINE
+ 'boxbox': '\u29c8', # ⧈ SQUARED SQUARE
+ 'boxbslash': '\u29c5', # ⧅ SQUARED FALLING DIAGONAL SLASH
+ 'boxcircle': '\u29c7', # ⧇ SQUARED SMALL CIRCLE
+ 'boxdot': '\u22a1', # ⊡ SQUARED DOT OPERATOR
+ 'boxminus': '\u229f', # ⊟ SQUARED MINUS
+ 'boxplus': '\u229e', # ⊞ SQUARED PLUS
+ 'boxslash': '\u29c4', # ⧄ SQUARED RISING DIAGONAL SLASH
+ 'boxtimes': '\u22a0', # ⊠ SQUARED TIMES
+ 'bullet': '\u2022', # • BULLET
+ 'cap': '\u2229', # ∩ INTERSECTION
+ 'cdot': '\u22c5', # ⋅ DOT OPERATOR
+ 'circ': '\u2218', # ∘ RING OPERATOR
+ 'circledast': '\u229b', # ⊛ CIRCLED ASTERISK OPERATOR
+ 'circledbslash': '\u29b8', # ⦸ CIRCLED REVERSE SOLIDUS
+ 'circledcirc': '\u229a', # ⊚ CIRCLED RING OPERATOR
+ 'circleddash': '\u229d', # ⊝ CIRCLED DASH
+ 'circledgtr': '\u29c1', # ⧁ CIRCLED GREATER-THAN
+ 'circledless': '\u29c0', # ⧀ CIRCLED LESS-THAN
+ 'cup': '\u222a', # ∪ UNION
+ 'curlyvee': '\u22ce', # ⋎ CURLY LOGICAL OR
+ 'curlywedge': '\u22cf', # ⋏ CURLY LOGICAL AND
+ 'dagger': '\u2020', # † DAGGER
+ 'ddagger': '\u2021', # ‡ DOUBLE DAGGER
+ 'diamond': '\u22c4', # ⋄ DIAMOND OPERATOR
+ 'div': '\xf7', # ÷ DIVISION SIGN
+ 'divideontimes': '\u22c7', # ⋇ DIVISION TIMES
+ 'dotplus': '\u2214', # ∔ DOT PLUS
+ 'doublebarwedge': '\u2a5e', # ⩞ LOGICAL AND WITH DOUBLE OVERBAR
+ 'gtrdot': '\u22d7', # ⋗ GREATER-THAN WITH DOT
+ 'intercal': '\u22ba', # ⊺ INTERCALATE
+ 'interleave': '\u2af4', # ⫴ TRIPLE VERTICAL BAR BINARY RELATION
+ 'invamp': '\u214b', # ⅋ TURNED AMPERSAND
+ 'land': '\u2227', # ∧ LOGICAL AND
+ 'leftthreetimes': '\u22cb', # ⋋ LEFT SEMIDIRECT PRODUCT
+ 'lessdot': '\u22d6', # ⋖ LESS-THAN WITH DOT
+ 'lor': '\u2228', # ∨ LOGICAL OR
+ 'ltimes': '\u22c9', # ⋉ LEFT NORMAL FACTOR SEMIDIRECT PRODUCT
+ 'mp': '\u2213', # ∓ MINUS-OR-PLUS SIGN
+ 'odot': '\u2299', # ⊙ CIRCLED DOT OPERATOR
+ 'ominus': '\u2296', # ⊖ CIRCLED MINUS
+ 'oplus': '\u2295', # ⊕ CIRCLED PLUS
+ 'oslash': '\u2298', # ⊘ CIRCLED DIVISION SLASH
+ 'otimes': '\u2297', # ⊗ CIRCLED TIMES
+ 'pm': '\xb1', # ± PLUS-MINUS SIGN
+ 'rightthreetimes': '\u22cc', # ⋌ RIGHT SEMIDIRECT PRODUCT
+ 'rtimes': '\u22ca', # ⋊ RIGHT NORMAL FACTOR SEMIDIRECT PRODUCT
+ 'setminus': '\u29f5', # ⧵ REVERSE SOLIDUS OPERATOR
+ 'slash': '\u2215', # ∕ DIVISION SLASH
+ 'smallsetminus': '\u2216', # ∖ SET MINUS
+ 'smalltriangledown': '\u25bf', # ▿ WHITE DOWN-POINTING SMALL TRIANGLE
+ 'smalltriangleleft': '\u25c3', # ◃ WHITE LEFT-POINTING SMALL TRIANGLE
+ 'smalltriangleright': '\u25b9', # ▹ WHITE RIGHT-POINTING SMALL TRIANGLE
+ 'sqcap': '\u2293', # ⊓ SQUARE CAP
+ 'sqcup': '\u2294', # ⊔ SQUARE CUP
+ 'sslash': '\u2afd', # ⫽ DOUBLE SOLIDUS OPERATOR
+ 'star': '\u22c6', # ⋆ STAR OPERATOR
+ 'talloblong': '\u2afe', # ⫾ WHITE VERTICAL BAR
+ 'times': '\xd7', # × MULTIPLICATION SIGN
+ 'triangleleft': '\u25c3', # ◃ WHITE LEFT-POINTING SMALL TRIANGLE
+ 'triangleright': '\u25b9', # ▹ WHITE RIGHT-POINTING SMALL TRIANGLE
+ 'uplus': '\u228e', # ⊎ MULTISET UNION
+ 'vee': '\u2228', # ∨ LOGICAL OR
+ 'veebar': '\u22bb', # ⊻ XOR
+ 'wedge': '\u2227', # ∧ LOGICAL AND
+ 'wr': '\u2240', # ≀ WREATH PRODUCT
+ }
+
+mathclose = {
+ 'Rbag': '\u27c6', # ⟆ RIGHT S-SHAPED BAG DELIMITER
+ 'lrcorner': '\u231f', # ⌟ BOTTOM RIGHT CORNER
+ 'rangle': '\u27e9', # ⟩ MATHEMATICAL RIGHT ANGLE BRACKET
+ 'rbag': '\u27c6', # ⟆ RIGHT S-SHAPED BAG DELIMITER
+ 'rbrace': '}', # } RIGHT CURLY BRACKET
+ 'rbrack': ']', # ] RIGHT SQUARE BRACKET
+ 'rceil': '\u2309', # ⌉ RIGHT CEILING
+ 'rfloor': '\u230b', # ⌋ RIGHT FLOOR
+ 'rgroup': '\u27ef', # ⟯ MATHEMATICAL RIGHT FLATTENED PARENTHESIS
+ 'rrbracket': '\u27e7', # ⟧ MATHEMATICAL RIGHT WHITE SQUARE BRACKET
+ 'rrparenthesis': '\u2988', # ⦈ Z NOTATION RIGHT IMAGE BRACKET
+ 'urcorner': '\u231d', # ⌝ TOP RIGHT CORNER
+ '}': '}', # } RIGHT CURLY BRACKET
+ }
+
+mathfence = {
+ 'Vert': '\u2016', # ‖ DOUBLE VERTICAL LINE
+ 'vert': '|', # | VERTICAL LINE
+ '|': '\u2016', # ‖ DOUBLE VERTICAL LINE
+ }
+
+mathop = {
+ 'bigcap': '\u22c2', # ⋂ N-ARY INTERSECTION
+ 'bigcup': '\u22c3', # ⋃ N-ARY UNION
+ 'biginterleave': '\u2afc', # ⫼ LARGE TRIPLE VERTICAL BAR OPERATOR
+ 'bigodot': '\u2a00', # ⨀ N-ARY CIRCLED DOT OPERATOR
+ 'bigoplus': '\u2a01', # ⨁ N-ARY CIRCLED PLUS OPERATOR
+ 'bigotimes': '\u2a02', # ⨂ N-ARY CIRCLED TIMES OPERATOR
+ 'bigsqcap': '\u2a05', # ⨅ N-ARY SQUARE INTERSECTION OPERATOR
+ 'bigsqcup': '\u2a06', # ⨆ N-ARY SQUARE UNION OPERATOR
+ 'biguplus': '\u2a04', # ⨄ N-ARY UNION OPERATOR WITH PLUS
+ 'bigvee': '\u22c1', # ⋁ N-ARY LOGICAL OR
+ 'bigwedge': '\u22c0', # ⋀ N-ARY LOGICAL AND
+ 'coprod': '\u2210', # ∐ N-ARY COPRODUCT
+ 'fatsemi': '\u2a1f', # ⨟ Z NOTATION SCHEMA COMPOSITION
+ 'fint': '\u2a0f', # ⨏ INTEGRAL AVERAGE WITH SLASH
+ 'iiiint': '\u2a0c', # ⨌ QUADRUPLE INTEGRAL OPERATOR
+ 'iiint': '\u222d', # ∭ TRIPLE INTEGRAL
+ 'iint': '\u222c', # ∬ DOUBLE INTEGRAL
+ 'int': '\u222b', # ∫ INTEGRAL
+ 'intop': '\u222b', # ∫ INTEGRAL
+ 'oiiint': '\u2230', # ∰ VOLUME INTEGRAL
+ 'oiint': '\u222f', # ∯ SURFACE INTEGRAL
+ 'oint': '\u222e', # ∮ CONTOUR INTEGRAL
+ 'ointctrclockwise': '\u2233', # ∳ ANTICLOCKWISE CONTOUR INTEGRAL
+ 'ointop': '\u222e', # ∮ CONTOUR INTEGRAL
+ 'prod': '\u220f', # ∏ N-ARY PRODUCT
+ 'sqint': '\u2a16', # ⨖ QUATERNION INTEGRAL OPERATOR
+ 'sum': '\u2211', # ∑ N-ARY SUMMATION
+ 'varointclockwise': '\u2232', # ∲ CLOCKWISE CONTOUR INTEGRAL
+ 'varprod': '\u2a09', # ⨉ N-ARY TIMES OPERATOR
+ }
+
+mathopen = {
+ 'Lbag': '\u27c5', # ⟅ LEFT S-SHAPED BAG DELIMITER
+ 'langle': '\u27e8', # ⟨ MATHEMATICAL LEFT ANGLE BRACKET
+ 'lbag': '\u27c5', # ⟅ LEFT S-SHAPED BAG DELIMITER
+ 'lbrace': '{', # { LEFT CURLY BRACKET
+ 'lbrack': '[', # [ LEFT SQUARE BRACKET
+ 'lceil': '\u2308', # ⌈ LEFT CEILING
+ 'lfloor': '\u230a', # ⌊ LEFT FLOOR
+ 'lgroup': '\u27ee', # ⟮ MATHEMATICAL LEFT FLATTENED PARENTHESIS
+ 'llbracket': '\u27e6', # ⟦ MATHEMATICAL LEFT WHITE SQUARE BRACKET
+ 'llcorner': '\u231e', # ⌞ BOTTOM LEFT CORNER
+ 'llparenthesis': '\u2987', # ⦇ Z NOTATION LEFT IMAGE BRACKET
+ 'ulcorner': '\u231c', # ⌜ TOP LEFT CORNER
+ '{': '{', # { LEFT CURLY BRACKET
+ }
+
+mathord = {
+ '#': '#', # # NUMBER SIGN
+ '$': '$', # $ DOLLAR SIGN
+ '%': '%', # % PERCENT SIGN
+ '&': '&', # & AMPERSAND
+ 'AC': '\u223f', # ∿ SINE WAVE
+ 'APLcomment': '\u235d', # ⍝ APL FUNCTIONAL SYMBOL UP SHOE JOT
+ 'APLdownarrowbox': '\u2357', # ⍗ APL FUNCTIONAL SYMBOL QUAD DOWNWARDS ARROW
+ 'APLinput': '\u235e', # ⍞ APL FUNCTIONAL SYMBOL QUOTE QUAD
+ 'APLinv': '\u2339', # ⌹ APL FUNCTIONAL SYMBOL QUAD DIVIDE
+ 'APLleftarrowbox': '\u2347', # ⍇ APL FUNCTIONAL SYMBOL QUAD LEFTWARDS ARROW
+ 'APLlog': '\u235f', # ⍟ APL FUNCTIONAL SYMBOL CIRCLE STAR
+ 'APLrightarrowbox': '\u2348', # ⍈ APL FUNCTIONAL SYMBOL QUAD RIGHTWARDS ARROW
+ 'APLuparrowbox': '\u2350', # ⍐ APL FUNCTIONAL SYMBOL QUAD UPWARDS ARROW
+ 'Aries': '\u2648', # ♈ ARIES
+ 'Box': '\u2b1c', # ⬜ WHITE LARGE SQUARE
+ 'CIRCLE': '\u25cf', # ● BLACK CIRCLE
+ 'CheckedBox': '\u2611', # ☑ BALLOT BOX WITH CHECK
+ 'Diamond': '\u25c7', # ◇ WHITE DIAMOND
+ 'Diamondblack': '\u25c6', # ◆ BLACK DIAMOND
+ 'Diamonddot': '\u27d0', # ⟐ WHITE DIAMOND WITH CENTRED DOT
+ 'Finv': '\u2132', # Ⅎ TURNED CAPITAL F
+ 'Game': '\u2141', # ⅁ TURNED SANS-SERIF CAPITAL G
+ 'Gemini': '\u264a', # ♊ GEMINI
+ 'Jupiter': '\u2643', # ♃ JUPITER
+ 'LEFTCIRCLE': '\u25d6', # ◖ LEFT HALF BLACK CIRCLE
+ 'LEFTcircle': '\u25d0', # ◐ CIRCLE WITH LEFT HALF BLACK
+ 'Leo': '\u264c', # ♌ LEO
+ 'Libra': '\u264e', # ♎ LIBRA
+ 'Mars': '\u2642', # ♂ MALE SIGN
+ 'Mercury': '\u263f', # ☿ MERCURY
+ 'Neptune': '\u2646', # ♆ NEPTUNE
+ 'P': '\xb6', # ¶ PILCROW SIGN
+ 'Pluto': '\u2647', # ♇ PLUTO
+ 'RIGHTCIRCLE': '\u25d7', # ◗ RIGHT HALF BLACK CIRCLE
+ 'RIGHTcircle': '\u25d1', # ◑ CIRCLE WITH RIGHT HALF BLACK
+ 'S': '\xa7', # § SECTION SIGN
+ 'Saturn': '\u2644', # ♄ SATURN
+ 'Scorpio': '\u264f', # ♏ SCORPIUS
+ 'Square': '\u2610', # ☐ BALLOT BOX
+ 'Sun': '\u2609', # ☉ SUN
+ 'Taurus': '\u2649', # ♉ TAURUS
+ 'Uranus': '\u2645', # ♅ URANUS
+ 'Venus': '\u2640', # ♀ FEMALE SIGN
+ 'XBox': '\u2612', # ☒ BALLOT BOX WITH X
+ 'Yup': '\u2144', # ⅄ TURNED SANS-SERIF CAPITAL Y
+ '_': '_', # _ LOW LINE
+ 'angle': '\u2220', # ∠ ANGLE
+ 'aquarius': '\u2652', # ♒ AQUARIUS
+ 'aries': '\u2648', # ♈ ARIES
+ 'arrowvert': '\u23d0', # ⏐ VERTICAL LINE EXTENSION
+ 'backprime': '\u2035', # ‵ REVERSED PRIME
+ 'backslash': '\\', # \ REVERSE SOLIDUS
+ 'bigstar': '\u2605', # ★ BLACK STAR
+ 'blacksmiley': '\u263b', # ☻ BLACK SMILING FACE
+ 'blacksquare': '\u25fc', # ◼ BLACK MEDIUM SQUARE
+ 'blacktriangle': '\u25b4', # ▴ BLACK UP-POINTING SMALL TRIANGLE
+ 'blacktriangledown': '\u25be', # ▾ BLACK DOWN-POINTING SMALL TRIANGLE
+ 'blacktriangleup': '\u25b4', # ▴ BLACK UP-POINTING SMALL TRIANGLE
+ 'bot': '\u22a5', # ⊥ UP TACK
+ 'boy': '\u2642', # ♂ MALE SIGN
+ 'bracevert': '\u23aa', # ⎪ CURLY BRACKET EXTENSION
+ 'cancer': '\u264b', # ♋ CANCER
+ 'capricornus': '\u2651', # ♑ CAPRICORN
+ 'cdots': '\u22ef', # ⋯ MIDLINE HORIZONTAL ELLIPSIS
+ 'cent': '\xa2', # ¢ CENT SIGN
+ 'checkmark': '\u2713', # ✓ CHECK MARK
+ 'circledR': '\u24c7', # Ⓡ CIRCLED LATIN CAPITAL LETTER R
+ 'circledS': '\u24c8', # Ⓢ CIRCLED LATIN CAPITAL LETTER S
+ 'clubsuit': '\u2663', # ♣ BLACK CLUB SUIT
+ 'complement': '\u2201', # ∁ COMPLEMENT
+ 'diagdown': '\u27cd', # ⟍ MATHEMATICAL FALLING DIAGONAL
+ 'diagup': '\u27cb', # ⟋ MATHEMATICAL RISING DIAGONAL
+ 'diameter': '\u2300', # ⌀ DIAMETER SIGN
+ 'diamondsuit': '\u2662', # ♢ WHITE DIAMOND SUIT
+ 'earth': '\u2641', # ♁ EARTH
+ 'emptyset': '\u2205', # ∅ EMPTY SET
+ 'exists': '\u2203', # ∃ THERE EXISTS
+ 'female': '\u2640', # ♀ FEMALE SIGN
+ 'flat': '\u266d', # ♭ MUSIC FLAT SIGN
+ 'forall': '\u2200', # ∀ FOR ALL
+ 'fourth': '\u2057', # ⁗ QUADRUPLE PRIME
+ 'frownie': '\u2639', # ☹ WHITE FROWNING FACE
+ 'gemini': '\u264a', # ♊ GEMINI
+ 'girl': '\u2640', # ♀ FEMALE SIGN
+ 'heartsuit': '\u2661', # ♡ WHITE HEART SUIT
+ 'hslash': '\u210f', # ℏ PLANCK CONSTANT OVER TWO PI
+ 'infty': '\u221e', # ∞ INFINITY
+ 'invdiameter': '\u2349', # ⍉ APL FUNCTIONAL SYMBOL CIRCLE BACKSLASH
+ 'invneg': '\u2310', # ⌐ REVERSED NOT SIGN
+ 'jupiter': '\u2643', # ♃ JUPITER
+ 'ldots': '\u2026', # … HORIZONTAL ELLIPSIS
+ 'leftmoon': '\u263e', # ☾ LAST QUARTER MOON
+ 'leo': '\u264c', # ♌ LEO
+ 'libra': '\u264e', # ♎ LIBRA
+ 'lmoustache': '\u23b0', # ⎰ UPPER LEFT OR LOWER RIGHT CURLY BRACKET SECTION
+ 'lnot': '\xac', # ¬ NOT SIGN
+ 'lozenge': '\u25ca', # ◊ LOZENGE
+ 'male': '\u2642', # ♂ MALE SIGN
+ 'maltese': '\u2720', # ✠ MALTESE CROSS
+ 'mathcent': '\xa2', # ¢ CENT SIGN
+ 'mathdollar': '$', # $ DOLLAR SIGN
+ 'mathsterling': '\xa3', # £ POUND SIGN
+ 'measuredangle': '\u2221', # ∡ MEASURED ANGLE
+ 'medbullet': '\u26ab', # ⚫ MEDIUM BLACK CIRCLE
+ 'medcirc': '\u26aa', # ⚪ MEDIUM WHITE CIRCLE
+ 'mercury': '\u263f', # ☿ MERCURY
+ 'mho': '\u2127', # ℧ INVERTED OHM SIGN
+ 'nabla': '\u2207', # ∇ NABLA
+ 'natural': '\u266e', # ♮ MUSIC NATURAL SIGN
+ 'neg': '\xac', # ¬ NOT SIGN
+ 'neptune': '\u2646', # ♆ NEPTUNE
+ 'nexists': '\u2204', # ∄ THERE DOES NOT EXIST
+ 'notbackslash': '\u2340', # ⍀ APL FUNCTIONAL SYMBOL BACKSLASH BAR
+ 'partial': '\u2202', # ∂ PARTIAL DIFFERENTIAL
+ 'pisces': '\u2653', # ♓ PISCES
+ 'pluto': '\u2647', # ♇ PLUTO
+ 'pounds': '\xa3', # £ POUND SIGN
+ 'prime': '\u2032', # ′ PRIME
+ 'quarternote': '\u2669', # ♩ QUARTER NOTE
+ 'rightmoon': '\u263d', # ☽ FIRST QUARTER MOON
+ 'rmoustache': '\u23b1', # ⎱ UPPER RIGHT OR LOWER LEFT CURLY BRACKET SECTION
+ 'sagittarius': '\u2650', # ♐ SAGITTARIUS
+ 'saturn': '\u2644', # ♄ SATURN
+ 'scorpio': '\u264f', # ♏ SCORPIUS
+ 'second': '\u2033', # ″ DOUBLE PRIME
+ 'sharp': '\u266f', # ♯ MUSIC SHARP SIGN
+ 'smiley': '\u263a', # ☺ WHITE SMILING FACE
+ 'spadesuit': '\u2660', # ♠ BLACK SPADE SUIT
+ 'spddot': '\xa8', # ¨ DIAERESIS
+ 'sphat': '^', # ^ CIRCUMFLEX ACCENT
+ 'sphericalangle': '\u2222', # ∢ SPHERICAL ANGLE
+ 'sptilde': '~', # ~ TILDE
+ 'square': '\u25fb', # ◻ WHITE MEDIUM SQUARE
+ 'sun': '\u263c', # ☼ WHITE SUN WITH RAYS
+ 'surd': '\u221a', # √ SQUARE ROOT
+ 'taurus': '\u2649', # ♉ TAURUS
+ 'third': '\u2034', # ‴ TRIPLE PRIME
+ 'top': '\u22a4', # ⊤ DOWN TACK
+ 'twonotes': '\u266b', # ♫ BEAMED EIGHTH NOTES
+ 'uranus': '\u2645', # ♅ URANUS
+ 'varEarth': '\u2641', # ♁ EARTH
+ 'varclubsuit': '\u2667', # ♧ WHITE CLUB SUIT
+ 'vardiamondsuit': '\u2666', # ♦ BLACK DIAMOND SUIT
+ 'varheartsuit': '\u2665', # ♥ BLACK HEART SUIT
+ 'varspadesuit': '\u2664', # ♤ WHITE SPADE SUIT
+ 'virgo': '\u264d', # ♍ VIRGO
+ 'wasylozenge': '\u2311', # ⌑ SQUARE LOZENGE
+ 'yen': '\xa5', # ¥ YEN SIGN
+ }
+
+mathover = {
+ 'overbrace': '\u23de', # ⏞ TOP CURLY BRACKET
+ 'wideparen': '\u23dc', # ⏜ TOP PARENTHESIS
+ }
+
+mathpunct = {
+ 'ddots': '\u22f1', # ⋱ DOWN RIGHT DIAGONAL ELLIPSIS
+ 'vdots': '\u22ee', # ⋮ VERTICAL ELLIPSIS
+ }
+
+mathradical = {
+ 'sqrt[3]': '\u221b', # ∛ CUBE ROOT
+ 'sqrt[4]': '\u221c', # ∜ FOURTH ROOT
+ }
+
+mathrel = {
+ 'Bot': '\u2aeb', # ⫫ DOUBLE UP TACK
+ 'Bumpeq': '\u224e', # ≎ GEOMETRICALLY EQUIVALENT TO
+ 'Coloneqq': '\u2a74', # ⩴ DOUBLE COLON EQUAL
+ 'Doteq': '\u2251', # ≑ GEOMETRICALLY EQUAL TO
+ 'Downarrow': '\u21d3', # ⇓ DOWNWARDS DOUBLE ARROW
+ 'Leftarrow': '\u21d0', # ⇐ LEFTWARDS DOUBLE ARROW
+ 'Leftrightarrow': '\u21d4', # ⇔ LEFT RIGHT DOUBLE ARROW
+ 'Lleftarrow': '\u21da', # ⇚ LEFTWARDS TRIPLE ARROW
+ 'Longleftarrow': '\u27f8', # ⟸ LONG LEFTWARDS DOUBLE ARROW
+ 'Longleftrightarrow': '\u27fa', # ⟺ LONG LEFT RIGHT DOUBLE ARROW
+ 'Longmapsfrom': '\u27fd', # ⟽ LONG LEFTWARDS DOUBLE ARROW FROM BAR
+ 'Longmapsto': '\u27fe', # ⟾ LONG RIGHTWARDS DOUBLE ARROW FROM BAR
+ 'Longrightarrow': '\u27f9', # ⟹ LONG RIGHTWARDS DOUBLE ARROW
+ 'Lsh': '\u21b0', # ↰ UPWARDS ARROW WITH TIP LEFTWARDS
+ 'Mapsfrom': '\u2906', # ⤆ LEFTWARDS DOUBLE ARROW FROM BAR
+ 'Mapsto': '\u2907', # ⤇ RIGHTWARDS DOUBLE ARROW FROM BAR
+ 'Nearrow': '\u21d7', # ⇗ NORTH EAST DOUBLE ARROW
+ 'Nwarrow': '\u21d6', # ⇖ NORTH WEST DOUBLE ARROW
+ 'Perp': '\u2aeb', # ⫫ DOUBLE UP TACK
+ 'Rightarrow': '\u21d2', # ⇒ RIGHTWARDS DOUBLE ARROW
+ 'Rrightarrow': '\u21db', # ⇛ RIGHTWARDS TRIPLE ARROW
+ 'Rsh': '\u21b1', # ↱ UPWARDS ARROW WITH TIP RIGHTWARDS
+ 'Searrow': '\u21d8', # ⇘ SOUTH EAST DOUBLE ARROW
+ 'Subset': '\u22d0', # ⋐ DOUBLE SUBSET
+ 'Supset': '\u22d1', # ⋑ DOUBLE SUPERSET
+ 'Swarrow': '\u21d9', # ⇙ SOUTH WEST DOUBLE ARROW
+ 'Top': '\u2aea', # ⫪ DOUBLE DOWN TACK
+ 'Uparrow': '\u21d1', # ⇑ UPWARDS DOUBLE ARROW
+ 'Updownarrow': '\u21d5', # ⇕ UP DOWN DOUBLE ARROW
+ 'VDash': '\u22ab', # ⊫ DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE
+ 'Vdash': '\u22a9', # ⊩ FORCES
+ 'Vvdash': '\u22aa', # ⊪ TRIPLE VERTICAL BAR RIGHT TURNSTILE
+ 'apprge': '\u2273', # ≳ GREATER-THAN OR EQUIVALENT TO
+ 'apprle': '\u2272', # ≲ LESS-THAN OR EQUIVALENT TO
+ 'approx': '\u2248', # ≈ ALMOST EQUAL TO
+ 'approxeq': '\u224a', # ≊ ALMOST EQUAL OR EQUAL TO
+ 'asymp': '\u224d', # ≍ EQUIVALENT TO
+ 'backepsilon': '\u220d', # ∍ SMALL CONTAINS AS MEMBER
+ 'backsim': '\u223d', # ∽ REVERSED TILDE
+ 'backsimeq': '\u22cd', # ⋍ REVERSED TILDE EQUALS
+ 'barin': '\u22f6', # ⋶ ELEMENT OF WITH OVERBAR
+ 'barleftharpoon': '\u296b', # ⥫ LEFTWARDS HARPOON WITH BARB DOWN BELOW LONG DASH
+ 'barrightharpoon': '\u296d', # ⥭ RIGHTWARDS HARPOON WITH BARB DOWN BELOW LONG DASH
+ 'because': '\u2235', # ∵ BECAUSE
+ 'between': '\u226c', # ≬ BETWEEN
+ 'blacktriangleleft': '\u25c2', # ◂ BLACK LEFT-POINTING SMALL TRIANGLE
+ 'blacktriangleright': '\u25b8', # ▸ BLACK RIGHT-POINTING SMALL TRIANGLE
+ 'bowtie': '\u22c8', # ⋈ BOWTIE
+ 'bumpeq': '\u224f', # ≏ DIFFERENCE BETWEEN
+ 'circeq': '\u2257', # ≗ RING EQUAL TO
+ 'circlearrowleft': '\u21ba', # ↺ ANTICLOCKWISE OPEN CIRCLE ARROW
+ 'circlearrowright': '\u21bb', # ↻ CLOCKWISE OPEN CIRCLE ARROW
+ 'coloneq': '\u2254', # ≔ COLON EQUALS
+ 'coloneqq': '\u2254', # ≔ COLON EQUALS
+ 'cong': '\u2245', # ≅ APPROXIMATELY EQUAL TO
+ 'corresponds': '\u2259', # ≙ ESTIMATES
+ 'curlyeqprec': '\u22de', # ⋞ EQUAL TO OR PRECEDES
+ 'curlyeqsucc': '\u22df', # ⋟ EQUAL TO OR SUCCEEDS
+ 'curvearrowleft': '\u21b6', # ↶ ANTICLOCKWISE TOP SEMICIRCLE ARROW
+ 'curvearrowright': '\u21b7', # ↷ CLOCKWISE TOP SEMICIRCLE ARROW
+ 'dasharrow': '\u21e2', # ⇢ RIGHTWARDS DASHED ARROW
+ 'dashleftarrow': '\u21e0', # ⇠ LEFTWARDS DASHED ARROW
+ 'dashrightarrow': '\u21e2', # ⇢ RIGHTWARDS DASHED ARROW
+ 'dashv': '\u22a3', # ⊣ LEFT TACK
+ 'dlsh': '\u21b2', # ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
+ 'doteq': '\u2250', # ≐ APPROACHES THE LIMIT
+ 'doteqdot': '\u2251', # ≑ GEOMETRICALLY EQUAL TO
+ 'downarrow': '\u2193', # ↓ DOWNWARDS ARROW
+ 'downdownarrows': '\u21ca', # ⇊ DOWNWARDS PAIRED ARROWS
+ 'downdownharpoons': '\u2965', # ⥥ DOWNWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT
+ 'downharpoonleft': '\u21c3', # ⇃ DOWNWARDS HARPOON WITH BARB LEFTWARDS
+ 'downharpoonright': '\u21c2', # ⇂ DOWNWARDS HARPOON WITH BARB RIGHTWARDS
+ 'downuparrows': '\u21f5', # ⇵ DOWNWARDS ARROW LEFTWARDS OF UPWARDS ARROW
+ 'downupharpoons': '\u296f', # ⥯ DOWNWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT
+ 'drsh': '\u21b3', # ↳ DOWNWARDS ARROW WITH TIP RIGHTWARDS
+ 'eqcirc': '\u2256', # ≖ RING IN EQUAL TO
+ 'eqcolon': '\u2255', # ≕ EQUALS COLON
+ 'eqqcolon': '\u2255', # ≕ EQUALS COLON
+ 'eqsim': '\u2242', # ≂ MINUS TILDE
+ 'eqslantgtr': '\u2a96', # ⪖ SLANTED EQUAL TO OR GREATER-THAN
+ 'eqslantless': '\u2a95', # ⪕ SLANTED EQUAL TO OR LESS-THAN
+ 'equiv': '\u2261', # ≡ IDENTICAL TO
+ 'fallingdotseq': '\u2252', # ≒ APPROXIMATELY EQUAL TO OR THE IMAGE OF
+ 'frown': '\u2322', # ⌢ FROWN
+ 'ge': '\u2265', # ≥ GREATER-THAN OR EQUAL TO
+ 'geq': '\u2265', # ≥ GREATER-THAN OR EQUAL TO
+ 'geqq': '\u2267', # ≧ GREATER-THAN OVER EQUAL TO
+ 'geqslant': '\u2a7e', # ⩾ GREATER-THAN OR SLANTED EQUAL TO
+ 'gets': '\u2190', # ← LEFTWARDS ARROW
+ 'gg': '\u226b', # ≫ MUCH GREATER-THAN
+ 'ggcurly': '\u2abc', # ⪼ DOUBLE SUCCEEDS
+ 'ggg': '\u22d9', # ⋙ VERY MUCH GREATER-THAN
+ 'gggtr': '\u22d9', # ⋙ VERY MUCH GREATER-THAN
+ 'gnapprox': '\u2a8a', # ⪊ GREATER-THAN AND NOT APPROXIMATE
+ 'gneq': '\u2a88', # ⪈ GREATER-THAN AND SINGLE-LINE NOT EQUAL TO
+ 'gneqq': '\u2269', # ≩ GREATER-THAN BUT NOT EQUAL TO
+ 'gnsim': '\u22e7', # ⋧ GREATER-THAN BUT NOT EQUIVALENT TO
+ 'gtrapprox': '\u2a86', # ⪆ GREATER-THAN OR APPROXIMATE
+ 'gtreqless': '\u22db', # ⋛ GREATER-THAN EQUAL TO OR LESS-THAN
+ 'gtreqqless': '\u2a8c', # ⪌ GREATER-THAN ABOVE DOUBLE-LINE EQUAL ABOVE LESS-THAN
+ 'gtrless': '\u2277', # ≷ GREATER-THAN OR LESS-THAN
+ 'gtrsim': '\u2273', # ≳ GREATER-THAN OR EQUIVALENT TO
+ 'hash': '\u22d5', # ⋕ EQUAL AND PARALLEL TO
+ 'hookleftarrow': '\u21a9', # ↩ LEFTWARDS ARROW WITH HOOK
+ 'hookrightarrow': '\u21aa', # ↪ RIGHTWARDS ARROW WITH HOOK
+ 'iddots': '\u22f0', # ⋰ UP RIGHT DIAGONAL ELLIPSIS
+ 'impliedby': '\u27f8', # ⟸ LONG LEFTWARDS DOUBLE ARROW
+ 'implies': '\u27f9', # ⟹ LONG RIGHTWARDS DOUBLE ARROW
+ 'in': '\u2208', # ∈ ELEMENT OF
+ 'le': '\u2264', # ≤ LESS-THAN OR EQUAL TO
+ 'leadsto': '\u2933', # ⤳ WAVE ARROW POINTING DIRECTLY RIGHT
+ 'leftarrow': '\u2190', # ← LEFTWARDS ARROW
+ 'leftarrowtail': '\u21a2', # ↢ LEFTWARDS ARROW WITH TAIL
+ 'leftarrowtriangle': '\u21fd', # ⇽ LEFTWARDS OPEN-HEADED ARROW
+ 'leftbarharpoon': '\u296a', # ⥪ LEFTWARDS HARPOON WITH BARB UP ABOVE LONG DASH
+ 'leftharpoondown': '\u21bd', # ↽ LEFTWARDS HARPOON WITH BARB DOWNWARDS
+ 'leftharpoonup': '\u21bc', # ↼ LEFTWARDS HARPOON WITH BARB UPWARDS
+ 'leftleftarrows': '\u21c7', # ⇇ LEFTWARDS PAIRED ARROWS
+ 'leftleftharpoons': '\u2962', # ⥢ LEFTWARDS HARPOON WITH BARB UP ABOVE LEFTWARDS HARPOON WITH BARB DOWN
+ 'leftrightarrow': '\u2194', # ↔ LEFT RIGHT ARROW
+ 'leftrightarrows': '\u21c6', # ⇆ LEFTWARDS ARROW OVER RIGHTWARDS ARROW
+ 'leftrightarrowtriangle': '\u21ff', # ⇿ LEFT RIGHT OPEN-HEADED ARROW
+ 'leftrightharpoon': '\u294a', # ⥊ LEFT BARB UP RIGHT BARB DOWN HARPOON
+ 'leftrightharpoons': '\u21cb', # ⇋ LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON
+ 'leftrightsquigarrow': '\u21ad', # ↭ LEFT RIGHT WAVE ARROW
+ 'leftslice': '\u2aa6', # ⪦ LESS-THAN CLOSED BY CURVE
+ 'leftsquigarrow': '\u21dc', # ⇜ LEFTWARDS SQUIGGLE ARROW
+ 'leftturn': '\u21ba', # ↺ ANTICLOCKWISE OPEN CIRCLE ARROW
+ 'leq': '\u2264', # ≤ LESS-THAN OR EQUAL TO
+ 'leqq': '\u2266', # ≦ LESS-THAN OVER EQUAL TO
+ 'leqslant': '\u2a7d', # ⩽ LESS-THAN OR SLANTED EQUAL TO
+ 'lessapprox': '\u2a85', # ⪅ LESS-THAN OR APPROXIMATE
+ 'lesseqgtr': '\u22da', # ⋚ LESS-THAN EQUAL TO OR GREATER-THAN
+ 'lesseqqgtr': '\u2a8b', # ⪋ LESS-THAN ABOVE DOUBLE-LINE EQUAL ABOVE GREATER-THAN
+ 'lessgtr': '\u2276', # ≶ LESS-THAN OR GREATER-THAN
+ 'lesssim': '\u2272', # ≲ LESS-THAN OR EQUIVALENT TO
+ 'lhd': '\u22b2', # ⊲ NORMAL SUBGROUP OF
+ 'lightning': '\u21af', # ↯ DOWNWARDS ZIGZAG ARROW
+ 'll': '\u226a', # ≪ MUCH LESS-THAN
+ 'llcurly': '\u2abb', # ⪻ DOUBLE PRECEDES
+ 'lll': '\u22d8', # ⋘ VERY MUCH LESS-THAN
+ 'llless': '\u22d8', # ⋘ VERY MUCH LESS-THAN
+ 'lnapprox': '\u2a89', # ⪉ LESS-THAN AND NOT APPROXIMATE
+ 'lneq': '\u2a87', # ⪇ LESS-THAN AND SINGLE-LINE NOT EQUAL TO
+ 'lneqq': '\u2268', # ≨ LESS-THAN BUT NOT EQUAL TO
+ 'lnsim': '\u22e6', # ⋦ LESS-THAN BUT NOT EQUIVALENT TO
+ 'longleftarrow': '\u27f5', # ⟵ LONG LEFTWARDS ARROW
+ 'longleftrightarrow': '\u27f7', # ⟷ LONG LEFT RIGHT ARROW
+ 'longmapsfrom': '\u27fb', # ⟻ LONG LEFTWARDS ARROW FROM BAR
+ 'longmapsto': '\u27fc', # ⟼ LONG RIGHTWARDS ARROW FROM BAR
+ 'longrightarrow': '\u27f6', # ⟶ LONG RIGHTWARDS ARROW
+ 'looparrowleft': '\u21ab', # ↫ LEFTWARDS ARROW WITH LOOP
+ 'looparrowright': '\u21ac', # ↬ RIGHTWARDS ARROW WITH LOOP
+ 'lrtimes': '\u22c8', # ⋈ BOWTIE
+ 'mapsfrom': '\u21a4', # ↤ LEFTWARDS ARROW FROM BAR
+ 'mapsto': '\u21a6', # ↦ RIGHTWARDS ARROW FROM BAR
+ 'mid': '\u2223', # ∣ DIVIDES
+ 'models': '\u22a7', # ⊧ MODELS
+ 'multimap': '\u22b8', # ⊸ MULTIMAP
+ 'multimapboth': '\u29df', # ⧟ DOUBLE-ENDED MULTIMAP
+ 'multimapdotbothA': '\u22b6', # ⊶ ORIGINAL OF
+ 'multimapdotbothB': '\u22b7', # ⊷ IMAGE OF
+ 'multimapinv': '\u27dc', # ⟜ LEFT MULTIMAP
+ 'nLeftarrow': '\u21cd', # ⇍ LEFTWARDS DOUBLE ARROW WITH STROKE
+ 'nLeftrightarrow': '\u21ce', # ⇎ LEFT RIGHT DOUBLE ARROW WITH STROKE
+ 'nRightarrow': '\u21cf', # ⇏ RIGHTWARDS DOUBLE ARROW WITH STROKE
+ 'nVDash': '\u22af', # ⊯ NEGATED DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE
+ 'nVdash': '\u22ae', # ⊮ DOES NOT FORCE
+ 'ncong': '\u2247', # ≇ NEITHER APPROXIMATELY NOR ACTUALLY EQUAL TO
+ 'ne': '\u2260', # ≠ NOT EQUAL TO
+ 'nearrow': '\u2197', # ↗ NORTH EAST ARROW
+ 'neq': '\u2260', # ≠ NOT EQUAL TO
+ 'ngeq': '\u2271', # ≱ NEITHER GREATER-THAN NOR EQUAL TO
+ 'ngtr': '\u226f', # ≯ NOT GREATER-THAN
+ 'ngtrless': '\u2279', # ≹ NEITHER GREATER-THAN NOR LESS-THAN
+ 'ni': '\u220b', # ∋ CONTAINS AS MEMBER
+ 'nleftarrow': '\u219a', # ↚ LEFTWARDS ARROW WITH STROKE
+ 'nleftrightarrow': '\u21ae', # ↮ LEFT RIGHT ARROW WITH STROKE
+ 'nleq': '\u2270', # ≰ NEITHER LESS-THAN NOR EQUAL TO
+ 'nless': '\u226e', # ≮ NOT LESS-THAN
+ 'nlessgtr': '\u2278', # ≸ NEITHER LESS-THAN NOR GREATER-THAN
+ 'nmid': '\u2224', # ∤ DOES NOT DIVIDE
+ 'notasymp': '\u226d', # ≭ NOT EQUIVALENT TO
+ 'notin': '\u2209', # ∉ NOT AN ELEMENT OF
+ 'notni': '\u220c', # ∌ DOES NOT CONTAIN AS MEMBER
+ 'notowner': '\u220c', # ∌ DOES NOT CONTAIN AS MEMBER
+ 'notslash': '\u233f', # ⌿ APL FUNCTIONAL SYMBOL SLASH BAR
+ 'nparallel': '\u2226', # ∦ NOT PARALLEL TO
+ 'nprec': '\u2280', # ⊀ DOES NOT PRECEDE
+ 'npreceq': '\u22e0', # ⋠ DOES NOT PRECEDE OR EQUAL
+ 'nrightarrow': '\u219b', # ↛ RIGHTWARDS ARROW WITH STROKE
+ 'nsim': '\u2241', # ≁ NOT TILDE
+ 'nsimeq': '\u2244', # ≄ NOT ASYMPTOTICALLY EQUAL TO
+ 'nsubseteq': '\u2288', # ⊈ NEITHER A SUBSET OF NOR EQUAL TO
+ 'nsucc': '\u2281', # ⊁ DOES NOT SUCCEED
+ 'nsucceq': '\u22e1', # ⋡ DOES NOT SUCCEED OR EQUAL
+ 'nsupseteq': '\u2289', # ⊉ NEITHER A SUPERSET OF NOR EQUAL TO
+ 'ntriangleleft': '\u22ea', # ⋪ NOT NORMAL SUBGROUP OF
+ 'ntrianglelefteq': '\u22ec', # ⋬ NOT NORMAL SUBGROUP OF OR EQUAL TO
+ 'ntriangleright': '\u22eb', # ⋫ DOES NOT CONTAIN AS NORMAL SUBGROUP
+ 'ntrianglerighteq': '\u22ed', # ⋭ DOES NOT CONTAIN AS NORMAL SUBGROUP OR EQUAL
+ 'nvDash': '\u22ad', # ⊭ NOT TRUE
+ 'nvdash': '\u22ac', # ⊬ DOES NOT PROVE
+ 'nwarrow': '\u2196', # ↖ NORTH WEST ARROW
+ 'owns': '\u220b', # ∋ CONTAINS AS MEMBER
+ 'parallel': '\u2225', # ∥ PARALLEL TO
+ 'perp': '\u27c2', # ⟂ PERPENDICULAR
+ 'pitchfork': '\u22d4', # ⋔ PITCHFORK
+ 'prec': '\u227a', # ≺ PRECEDES
+ 'precapprox': '\u2ab7', # ⪷ PRECEDES ABOVE ALMOST EQUAL TO
+ 'preccurlyeq': '\u227c', # ≼ PRECEDES OR EQUAL TO
+ 'preceq': '\u2aaf', # ⪯ PRECEDES ABOVE SINGLE-LINE EQUALS SIGN
+ 'preceqq': '\u2ab3', # ⪳ PRECEDES ABOVE EQUALS SIGN
+ 'precnapprox': '\u2ab9', # ⪹ PRECEDES ABOVE NOT ALMOST EQUAL TO
+ 'precneqq': '\u2ab5', # ⪵ PRECEDES ABOVE NOT EQUAL TO
+ 'precnsim': '\u22e8', # ⋨ PRECEDES BUT NOT EQUIVALENT TO
+ 'precsim': '\u227e', # ≾ PRECEDES OR EQUIVALENT TO
+ 'propto': '\u221d', # ∝ PROPORTIONAL TO
+ 'restriction': '\u21be', # ↾ UPWARDS HARPOON WITH BARB RIGHTWARDS
+ 'rhd': '\u22b3', # ⊳ CONTAINS AS NORMAL SUBGROUP
+ 'rightarrow': '\u2192', # → RIGHTWARDS ARROW
+ 'rightarrowtail': '\u21a3', # ↣ RIGHTWARDS ARROW WITH TAIL
+ 'rightarrowtriangle': '\u21fe', # ⇾ RIGHTWARDS OPEN-HEADED ARROW
+ 'rightbarharpoon': '\u296c', # ⥬ RIGHTWARDS HARPOON WITH BARB UP ABOVE LONG DASH
+ 'rightharpoondown': '\u21c1', # ⇁ RIGHTWARDS HARPOON WITH BARB DOWNWARDS
+ 'rightharpoonup': '\u21c0', # ⇀ RIGHTWARDS HARPOON WITH BARB UPWARDS
+ 'rightleftarrows': '\u21c4', # ⇄ RIGHTWARDS ARROW OVER LEFTWARDS ARROW
+ 'rightleftharpoon': '\u294b', # ⥋ LEFT BARB DOWN RIGHT BARB UP HARPOON
+ 'rightleftharpoons': '\u21cc', # ⇌ RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON
+ 'rightrightarrows': '\u21c9', # ⇉ RIGHTWARDS PAIRED ARROWS
+ 'rightrightharpoons': '\u2964', # ⥤ RIGHTWARDS HARPOON WITH BARB UP ABOVE RIGHTWARDS HARPOON WITH BARB DOWN
+ 'rightslice': '\u2aa7', # ⪧ GREATER-THAN CLOSED BY CURVE
+ 'rightsquigarrow': '\u21dd', # ⇝ RIGHTWARDS SQUIGGLE ARROW
+ 'rightturn': '\u21bb', # ↻ CLOCKWISE OPEN CIRCLE ARROW
+ 'risingdotseq': '\u2253', # ≓ IMAGE OF OR APPROXIMATELY EQUAL TO
+ 'searrow': '\u2198', # ↘ SOUTH EAST ARROW
+ 'sim': '\u223c', # ∼ TILDE OPERATOR
+ 'simeq': '\u2243', # ≃ ASYMPTOTICALLY EQUAL TO
+ 'smile': '\u2323', # ⌣ SMILE
+ 'sqsubset': '\u228f', # ⊏ SQUARE IMAGE OF
+ 'sqsubseteq': '\u2291', # ⊑ SQUARE IMAGE OF OR EQUAL TO
+ 'sqsupset': '\u2290', # ⊐ SQUARE ORIGINAL OF
+ 'sqsupseteq': '\u2292', # ⊒ SQUARE ORIGINAL OF OR EQUAL TO
+ 'strictfi': '\u297c', # ⥼ LEFT FISH TAIL
+ 'strictif': '\u297d', # ⥽ RIGHT FISH TAIL
+ 'subset': '\u2282', # ⊂ SUBSET OF
+ 'subseteq': '\u2286', # ⊆ SUBSET OF OR EQUAL TO
+ 'subseteqq': '\u2ac5', # ⫅ SUBSET OF ABOVE EQUALS SIGN
+ 'subsetneq': '\u228a', # ⊊ SUBSET OF WITH NOT EQUAL TO
+ 'subsetneqq': '\u2acb', # ⫋ SUBSET OF ABOVE NOT EQUAL TO
+ 'succ': '\u227b', # ≻ SUCCEEDS
+ 'succapprox': '\u2ab8', # ⪸ SUCCEEDS ABOVE ALMOST EQUAL TO
+ 'succcurlyeq': '\u227d', # ≽ SUCCEEDS OR EQUAL TO
+ 'succeq': '\u2ab0', # ⪰ SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN
+ 'succeqq': '\u2ab4', # ⪴ SUCCEEDS ABOVE EQUALS SIGN
+ 'succnapprox': '\u2aba', # ⪺ SUCCEEDS ABOVE NOT ALMOST EQUAL TO
+ 'succneqq': '\u2ab6', # ⪶ SUCCEEDS ABOVE NOT EQUAL TO
+ 'succnsim': '\u22e9', # ⋩ SUCCEEDS BUT NOT EQUIVALENT TO
+ 'succsim': '\u227f', # ≿ SUCCEEDS OR EQUIVALENT TO
+ 'supset': '\u2283', # ⊃ SUPERSET OF
+ 'supseteq': '\u2287', # ⊇ SUPERSET OF OR EQUAL TO
+ 'supseteqq': '\u2ac6', # ⫆ SUPERSET OF ABOVE EQUALS SIGN
+ 'supsetneq': '\u228b', # ⊋ SUPERSET OF WITH NOT EQUAL TO
+ 'supsetneqq': '\u2acc', # ⫌ SUPERSET OF ABOVE NOT EQUAL TO
+ 'swarrow': '\u2199', # ↙ SOUTH WEST ARROW
+ 'therefore': '\u2234', # ∴ THEREFORE
+ 'to': '\u2192', # → RIGHTWARDS ARROW
+ 'trianglelefteq': '\u22b4', # ⊴ NORMAL SUBGROUP OF OR EQUAL TO
+ 'triangleq': '\u225c', # ≜ DELTA EQUAL TO
+ 'trianglerighteq': '\u22b5', # ⊵ CONTAINS AS NORMAL SUBGROUP OR EQUAL TO
+ 'twoheadleftarrow': '\u219e', # ↞ LEFTWARDS TWO HEADED ARROW
+ 'twoheadrightarrow': '\u21a0', # ↠ RIGHTWARDS TWO HEADED ARROW
+ 'uparrow': '\u2191', # ↑ UPWARDS ARROW
+ 'updownarrow': '\u2195', # ↕ UP DOWN ARROW
+ 'updownarrows': '\u21c5', # ⇅ UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW
+ 'updownharpoons': '\u296e', # ⥮ UPWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT
+ 'upharpoonleft': '\u21bf', # ↿ UPWARDS HARPOON WITH BARB LEFTWARDS
+ 'upharpoonright': '\u21be', # ↾ UPWARDS HARPOON WITH BARB RIGHTWARDS
+ 'upuparrows': '\u21c8', # ⇈ UPWARDS PAIRED ARROWS
+ 'upupharpoons': '\u2963', # ⥣ UPWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT
+ 'vDash': '\u22a8', # ⊨ TRUE
+ 'vartriangle': '\u25b5', # ▵ WHITE UP-POINTING SMALL TRIANGLE
+ 'vartriangleleft': '\u22b2', # ⊲ NORMAL SUBGROUP OF
+ 'vartriangleright': '\u22b3', # ⊳ CONTAINS AS NORMAL SUBGROUP
+ 'vdash': '\u22a2', # ⊢ RIGHT TACK
+ 'wasytherefore': '\u2234', # ∴ THEREFORE
+ }
+
+mathunder = {
+ 'underbrace': '\u23df', # ⏟ BOTTOM CURLY BRACKET
+ }
+
+space = {
+ ' ': ' ', # SPACE
+ ',': '\u2006', #   SIX-PER-EM SPACE
+ ':': '\u205f', #   MEDIUM MATHEMATICAL SPACE
+ 'medspace': '\u205f', #   MEDIUM MATHEMATICAL SPACE
+ 'quad': '\u2001', #   EM QUAD
+ 'thinspace': '\u2006', #   SIX-PER-EM SPACE
+ }
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/math/unichar2tex.py b/.venv/lib/python3.12/site-packages/docutils/utils/math/unichar2tex.py
new file mode 100644
index 00000000..da1f828a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/math/unichar2tex.py
@@ -0,0 +1,808 @@
+# LaTeX math to Unicode symbols translation table
+# for use with the translate() method of unicode objects.
+# Generated with ``write_unichar2tex.py`` from the data in
+# http://milde.users.sourceforge.net/LUCR/Math/
+
+# Includes commands from: standard LaTeX, amssymb, amsmath
+
+uni2tex_table = {
+0xa0: '~',
+0xa3: '\\pounds ',
+0xa5: '\\yen ',
+0xa7: '\\S ',
+0xac: '\\neg ',
+0xb1: '\\pm ',
+0xb6: '\\P ',
+0xd7: '\\times ',
+0xf0: '\\eth ',
+0xf7: '\\div ',
+0x131: '\\imath ',
+0x237: '\\jmath ',
+0x393: '\\Gamma ',
+0x394: '\\Delta ',
+0x398: '\\Theta ',
+0x39b: '\\Lambda ',
+0x39e: '\\Xi ',
+0x3a0: '\\Pi ',
+0x3a3: '\\Sigma ',
+0x3a5: '\\Upsilon ',
+0x3a6: '\\Phi ',
+0x3a8: '\\Psi ',
+0x3a9: '\\Omega ',
+0x3b1: '\\alpha ',
+0x3b2: '\\beta ',
+0x3b3: '\\gamma ',
+0x3b4: '\\delta ',
+0x3b5: '\\varepsilon ',
+0x3b6: '\\zeta ',
+0x3b7: '\\eta ',
+0x3b8: '\\theta ',
+0x3b9: '\\iota ',
+0x3ba: '\\kappa ',
+0x3bb: '\\lambda ',
+0x3bc: '\\mu ',
+0x3bd: '\\nu ',
+0x3be: '\\xi ',
+0x3c0: '\\pi ',
+0x3c1: '\\rho ',
+0x3c2: '\\varsigma ',
+0x3c3: '\\sigma ',
+0x3c4: '\\tau ',
+0x3c5: '\\upsilon ',
+0x3c6: '\\varphi ',
+0x3c7: '\\chi ',
+0x3c8: '\\psi ',
+0x3c9: '\\omega ',
+0x3d1: '\\vartheta ',
+0x3d5: '\\phi ',
+0x3d6: '\\varpi ',
+0x3dd: '\\digamma ',
+0x3f0: '\\varkappa ',
+0x3f1: '\\varrho ',
+0x3f5: '\\epsilon ',
+0x3f6: '\\backepsilon ',
+0x2001: '\\quad ',
+0x2003: '\\quad ',
+0x2006: '\\, ',
+0x2016: '\\| ',
+0x2020: '\\dagger ',
+0x2021: '\\ddagger ',
+0x2022: '\\bullet ',
+0x2026: '\\ldots ',
+0x2032: '\\prime ',
+0x2035: '\\backprime ',
+0x205f: '\\: ',
+0x2102: '\\mathbb{C}',
+0x210b: '\\mathcal{H}',
+0x210c: '\\mathfrak{H}',
+0x210d: '\\mathbb{H}',
+0x210f: '\\hslash ',
+0x2110: '\\mathcal{I}',
+0x2111: '\\Im ',
+0x2112: '\\mathcal{L}',
+0x2113: '\\ell ',
+0x2115: '\\mathbb{N}',
+0x2118: '\\wp ',
+0x2119: '\\mathbb{P}',
+0x211a: '\\mathbb{Q}',
+0x211b: '\\mathcal{R}',
+0x211c: '\\Re ',
+0x211d: '\\mathbb{R}',
+0x2124: '\\mathbb{Z}',
+0x2127: '\\mho ',
+0x2128: '\\mathfrak{Z}',
+0x212c: '\\mathcal{B}',
+0x212d: '\\mathfrak{C}',
+0x2130: '\\mathcal{E}',
+0x2131: '\\mathcal{F}',
+0x2132: '\\Finv ',
+0x2133: '\\mathcal{M}',
+0x2135: '\\aleph ',
+0x2136: '\\beth ',
+0x2137: '\\gimel ',
+0x2138: '\\daleth ',
+0x2190: '\\leftarrow ',
+0x2191: '\\uparrow ',
+0x2192: '\\rightarrow ',
+0x2193: '\\downarrow ',
+0x2194: '\\leftrightarrow ',
+0x2195: '\\updownarrow ',
+0x2196: '\\nwarrow ',
+0x2197: '\\nearrow ',
+0x2198: '\\searrow ',
+0x2199: '\\swarrow ',
+0x219a: '\\nleftarrow ',
+0x219b: '\\nrightarrow ',
+0x219e: '\\twoheadleftarrow ',
+0x21a0: '\\twoheadrightarrow ',
+0x21a2: '\\leftarrowtail ',
+0x21a3: '\\rightarrowtail ',
+0x21a6: '\\mapsto ',
+0x21a9: '\\hookleftarrow ',
+0x21aa: '\\hookrightarrow ',
+0x21ab: '\\looparrowleft ',
+0x21ac: '\\looparrowright ',
+0x21ad: '\\leftrightsquigarrow ',
+0x21ae: '\\nleftrightarrow ',
+0x21b0: '\\Lsh ',
+0x21b1: '\\Rsh ',
+0x21b6: '\\curvearrowleft ',
+0x21b7: '\\curvearrowright ',
+0x21ba: '\\circlearrowleft ',
+0x21bb: '\\circlearrowright ',
+0x21bc: '\\leftharpoonup ',
+0x21bd: '\\leftharpoondown ',
+0x21be: '\\upharpoonright ',
+0x21bf: '\\upharpoonleft ',
+0x21c0: '\\rightharpoonup ',
+0x21c1: '\\rightharpoondown ',
+0x21c2: '\\downharpoonright ',
+0x21c3: '\\downharpoonleft ',
+0x21c4: '\\rightleftarrows ',
+0x21c6: '\\leftrightarrows ',
+0x21c7: '\\leftleftarrows ',
+0x21c8: '\\upuparrows ',
+0x21c9: '\\rightrightarrows ',
+0x21ca: '\\downdownarrows ',
+0x21cb: '\\leftrightharpoons ',
+0x21cc: '\\rightleftharpoons ',
+0x21cd: '\\nLeftarrow ',
+0x21ce: '\\nLeftrightarrow ',
+0x21cf: '\\nRightarrow ',
+0x21d0: '\\Leftarrow ',
+0x21d1: '\\Uparrow ',
+0x21d2: '\\Rightarrow ',
+0x21d3: '\\Downarrow ',
+0x21d4: '\\Leftrightarrow ',
+0x21d5: '\\Updownarrow ',
+0x21da: '\\Lleftarrow ',
+0x21db: '\\Rrightarrow ',
+0x21dd: '\\rightsquigarrow ',
+0x21e0: '\\dashleftarrow ',
+0x21e2: '\\dashrightarrow ',
+0x2200: '\\forall ',
+0x2201: '\\complement ',
+0x2202: '\\partial ',
+0x2203: '\\exists ',
+0x2204: '\\nexists ',
+0x2205: '\\emptyset ',
+0x2207: '\\nabla ',
+0x2208: '\\in ',
+0x2209: '\\notin ',
+0x220b: '\\ni ',
+0x220f: '\\prod ',
+0x2210: '\\coprod ',
+0x2211: '\\sum ',
+0x2212: '-',
+0x2213: '\\mp ',
+0x2214: '\\dotplus ',
+0x2215: '\\slash ',
+0x2216: '\\smallsetminus ',
+0x2217: '\\ast ',
+0x2218: '\\circ ',
+0x2219: '\\bullet ',
+0x221a: '\\surd ',
+0x221b: '\\sqrt[3] ',
+0x221c: '\\sqrt[4] ',
+0x221d: '\\propto ',
+0x221e: '\\infty ',
+0x2220: '\\angle ',
+0x2221: '\\measuredangle ',
+0x2222: '\\sphericalangle ',
+0x2223: '\\mid ',
+0x2224: '\\nmid ',
+0x2225: '\\parallel ',
+0x2226: '\\nparallel ',
+0x2227: '\\wedge ',
+0x2228: '\\vee ',
+0x2229: '\\cap ',
+0x222a: '\\cup ',
+0x222b: '\\int ',
+0x222c: '\\iint ',
+0x222d: '\\iiint ',
+0x222e: '\\oint ',
+0x2234: '\\therefore ',
+0x2235: '\\because ',
+0x2236: ':',
+0x223c: '\\sim ',
+0x223d: '\\backsim ',
+0x2240: '\\wr ',
+0x2241: '\\nsim ',
+0x2242: '\\eqsim ',
+0x2243: '\\simeq ',
+0x2245: '\\cong ',
+0x2247: '\\ncong ',
+0x2248: '\\approx ',
+0x224a: '\\approxeq ',
+0x224d: '\\asymp ',
+0x224e: '\\Bumpeq ',
+0x224f: '\\bumpeq ',
+0x2250: '\\doteq ',
+0x2251: '\\Doteq ',
+0x2252: '\\fallingdotseq ',
+0x2253: '\\risingdotseq ',
+0x2256: '\\eqcirc ',
+0x2257: '\\circeq ',
+0x225c: '\\triangleq ',
+0x2260: '\\neq ',
+0x2261: '\\equiv ',
+0x2264: '\\leq ',
+0x2265: '\\geq ',
+0x2266: '\\leqq ',
+0x2267: '\\geqq ',
+0x2268: '\\lneqq ',
+0x2269: '\\gneqq ',
+0x226a: '\\ll ',
+0x226b: '\\gg ',
+0x226c: '\\between ',
+0x226e: '\\nless ',
+0x226f: '\\ngtr ',
+0x2270: '\\nleq ',
+0x2271: '\\ngeq ',
+0x2272: '\\lesssim ',
+0x2273: '\\gtrsim ',
+0x2276: '\\lessgtr ',
+0x2277: '\\gtrless ',
+0x227a: '\\prec ',
+0x227b: '\\succ ',
+0x227c: '\\preccurlyeq ',
+0x227d: '\\succcurlyeq ',
+0x227e: '\\precsim ',
+0x227f: '\\succsim ',
+0x2280: '\\nprec ',
+0x2281: '\\nsucc ',
+0x2282: '\\subset ',
+0x2283: '\\supset ',
+0x2286: '\\subseteq ',
+0x2287: '\\supseteq ',
+0x2288: '\\nsubseteq ',
+0x2289: '\\nsupseteq ',
+0x228a: '\\subsetneq ',
+0x228b: '\\supsetneq ',
+0x228e: '\\uplus ',
+0x228f: '\\sqsubset ',
+0x2290: '\\sqsupset ',
+0x2291: '\\sqsubseteq ',
+0x2292: '\\sqsupseteq ',
+0x2293: '\\sqcap ',
+0x2294: '\\sqcup ',
+0x2295: '\\oplus ',
+0x2296: '\\ominus ',
+0x2297: '\\otimes ',
+0x2298: '\\oslash ',
+0x2299: '\\odot ',
+0x229a: '\\circledcirc ',
+0x229b: '\\circledast ',
+0x229d: '\\circleddash ',
+0x229e: '\\boxplus ',
+0x229f: '\\boxminus ',
+0x22a0: '\\boxtimes ',
+0x22a1: '\\boxdot ',
+0x22a2: '\\vdash ',
+0x22a3: '\\dashv ',
+0x22a4: '\\top ',
+0x22a5: '\\bot ',
+0x22a7: '\\models ',
+0x22a8: '\\vDash ',
+0x22a9: '\\Vdash ',
+0x22aa: '\\Vvdash ',
+0x22ac: '\\nvdash ',
+0x22ad: '\\nvDash ',
+0x22ae: '\\nVdash ',
+0x22af: '\\nVDash ',
+0x22b2: '\\vartriangleleft ',
+0x22b3: '\\vartriangleright ',
+0x22b4: '\\trianglelefteq ',
+0x22b5: '\\trianglerighteq ',
+0x22b8: '\\multimap ',
+0x22ba: '\\intercal ',
+0x22bb: '\\veebar ',
+0x22bc: '\\barwedge ',
+0x22c0: '\\bigwedge ',
+0x22c1: '\\bigvee ',
+0x22c2: '\\bigcap ',
+0x22c3: '\\bigcup ',
+0x22c4: '\\diamond ',
+0x22c5: '\\cdot ',
+0x22c6: '\\star ',
+0x22c7: '\\divideontimes ',
+0x22c8: '\\bowtie ',
+0x22c9: '\\ltimes ',
+0x22ca: '\\rtimes ',
+0x22cb: '\\leftthreetimes ',
+0x22cc: '\\rightthreetimes ',
+0x22cd: '\\backsimeq ',
+0x22ce: '\\curlyvee ',
+0x22cf: '\\curlywedge ',
+0x22d0: '\\Subset ',
+0x22d1: '\\Supset ',
+0x22d2: '\\Cap ',
+0x22d3: '\\Cup ',
+0x22d4: '\\pitchfork ',
+0x22d6: '\\lessdot ',
+0x22d7: '\\gtrdot ',
+0x22d8: '\\lll ',
+0x22d9: '\\ggg ',
+0x22da: '\\lesseqgtr ',
+0x22db: '\\gtreqless ',
+0x22de: '\\curlyeqprec ',
+0x22df: '\\curlyeqsucc ',
+0x22e0: '\\npreceq ',
+0x22e1: '\\nsucceq ',
+0x22e6: '\\lnsim ',
+0x22e7: '\\gnsim ',
+0x22e8: '\\precnsim ',
+0x22e9: '\\succnsim ',
+0x22ea: '\\ntriangleleft ',
+0x22eb: '\\ntriangleright ',
+0x22ec: '\\ntrianglelefteq ',
+0x22ed: '\\ntrianglerighteq ',
+0x22ee: '\\vdots ',
+0x22ef: '\\cdots ',
+0x22f1: '\\ddots ',
+0x2308: '\\lceil ',
+0x2309: '\\rceil ',
+0x230a: '\\lfloor ',
+0x230b: '\\rfloor ',
+0x231c: '\\ulcorner ',
+0x231d: '\\urcorner ',
+0x231e: '\\llcorner ',
+0x231f: '\\lrcorner ',
+0x2322: '\\frown ',
+0x2323: '\\smile ',
+0x23aa: '\\bracevert ',
+0x23b0: '\\lmoustache ',
+0x23b1: '\\rmoustache ',
+0x23d0: '\\arrowvert ',
+0x23de: '\\overbrace ',
+0x23df: '\\underbrace ',
+0x24c7: '\\circledR ',
+0x24c8: '\\circledS ',
+0x25b2: '\\blacktriangle ',
+0x25b3: '\\bigtriangleup ',
+0x25b7: '\\triangleright ',
+0x25bc: '\\blacktriangledown ',
+0x25bd: '\\bigtriangledown ',
+0x25c1: '\\triangleleft ',
+0x25c7: '\\Diamond ',
+0x25ca: '\\lozenge ',
+0x25ef: '\\bigcirc ',
+0x25fb: '\\square ',
+0x25fc: '\\blacksquare ',
+0x2605: '\\bigstar ',
+0x2660: '\\spadesuit ',
+0x2661: '\\heartsuit ',
+0x2662: '\\diamondsuit ',
+0x2663: '\\clubsuit ',
+0x266d: '\\flat ',
+0x266e: '\\natural ',
+0x266f: '\\sharp ',
+0x2713: '\\checkmark ',
+0x2720: '\\maltese ',
+0x27c2: '\\perp ',
+0x27cb: '\\diagup ',
+0x27cd: '\\diagdown ',
+0x27e8: '\\langle ',
+0x27e9: '\\rangle ',
+0x27ee: '\\lgroup ',
+0x27ef: '\\rgroup ',
+0x27f5: '\\longleftarrow ',
+0x27f6: '\\longrightarrow ',
+0x27f7: '\\longleftrightarrow ',
+0x27f8: '\\Longleftarrow ',
+0x27f9: '\\Longrightarrow ',
+0x27fa: '\\Longleftrightarrow ',
+0x27fc: '\\longmapsto ',
+0x29eb: '\\blacklozenge ',
+0x29f5: '\\setminus ',
+0x2a00: '\\bigodot ',
+0x2a01: '\\bigoplus ',
+0x2a02: '\\bigotimes ',
+0x2a04: '\\biguplus ',
+0x2a06: '\\bigsqcup ',
+0x2a0c: '\\iiiint ',
+0x2a3f: '\\amalg ',
+0x2a5e: '\\doublebarwedge ',
+0x2a7d: '\\leqslant ',
+0x2a7e: '\\geqslant ',
+0x2a85: '\\lessapprox ',
+0x2a86: '\\gtrapprox ',
+0x2a87: '\\lneq ',
+0x2a88: '\\gneq ',
+0x2a89: '\\lnapprox ',
+0x2a8a: '\\gnapprox ',
+0x2a8b: '\\lesseqqgtr ',
+0x2a8c: '\\gtreqqless ',
+0x2a95: '\\eqslantless ',
+0x2a96: '\\eqslantgtr ',
+0x2aaf: '\\preceq ',
+0x2ab0: '\\succeq ',
+0x2ab5: '\\precneqq ',
+0x2ab6: '\\succneqq ',
+0x2ab7: '\\precapprox ',
+0x2ab8: '\\succapprox ',
+0x2ab9: '\\precnapprox ',
+0x2aba: '\\succnapprox ',
+0x2ac5: '\\subseteqq ',
+0x2ac6: '\\supseteqq ',
+0x2acb: '\\subsetneqq ',
+0x2acc: '\\supsetneqq ',
+0x2b1c: '\\Box ',
+0x1d400: '\\mathbf{A}',
+0x1d401: '\\mathbf{B}',
+0x1d402: '\\mathbf{C}',
+0x1d403: '\\mathbf{D}',
+0x1d404: '\\mathbf{E}',
+0x1d405: '\\mathbf{F}',
+0x1d406: '\\mathbf{G}',
+0x1d407: '\\mathbf{H}',
+0x1d408: '\\mathbf{I}',
+0x1d409: '\\mathbf{J}',
+0x1d40a: '\\mathbf{K}',
+0x1d40b: '\\mathbf{L}',
+0x1d40c: '\\mathbf{M}',
+0x1d40d: '\\mathbf{N}',
+0x1d40e: '\\mathbf{O}',
+0x1d40f: '\\mathbf{P}',
+0x1d410: '\\mathbf{Q}',
+0x1d411: '\\mathbf{R}',
+0x1d412: '\\mathbf{S}',
+0x1d413: '\\mathbf{T}',
+0x1d414: '\\mathbf{U}',
+0x1d415: '\\mathbf{V}',
+0x1d416: '\\mathbf{W}',
+0x1d417: '\\mathbf{X}',
+0x1d418: '\\mathbf{Y}',
+0x1d419: '\\mathbf{Z}',
+0x1d41a: '\\mathbf{a}',
+0x1d41b: '\\mathbf{b}',
+0x1d41c: '\\mathbf{c}',
+0x1d41d: '\\mathbf{d}',
+0x1d41e: '\\mathbf{e}',
+0x1d41f: '\\mathbf{f}',
+0x1d420: '\\mathbf{g}',
+0x1d421: '\\mathbf{h}',
+0x1d422: '\\mathbf{i}',
+0x1d423: '\\mathbf{j}',
+0x1d424: '\\mathbf{k}',
+0x1d425: '\\mathbf{l}',
+0x1d426: '\\mathbf{m}',
+0x1d427: '\\mathbf{n}',
+0x1d428: '\\mathbf{o}',
+0x1d429: '\\mathbf{p}',
+0x1d42a: '\\mathbf{q}',
+0x1d42b: '\\mathbf{r}',
+0x1d42c: '\\mathbf{s}',
+0x1d42d: '\\mathbf{t}',
+0x1d42e: '\\mathbf{u}',
+0x1d42f: '\\mathbf{v}',
+0x1d430: '\\mathbf{w}',
+0x1d431: '\\mathbf{x}',
+0x1d432: '\\mathbf{y}',
+0x1d433: '\\mathbf{z}',
+0x1d434: 'A',
+0x1d435: 'B',
+0x1d436: 'C',
+0x1d437: 'D',
+0x1d438: 'E',
+0x1d439: 'F',
+0x1d43a: 'G',
+0x1d43b: 'H',
+0x1d43c: 'I',
+0x1d43d: 'J',
+0x1d43e: 'K',
+0x1d43f: 'L',
+0x1d440: 'M',
+0x1d441: 'N',
+0x1d442: 'O',
+0x1d443: 'P',
+0x1d444: 'Q',
+0x1d445: 'R',
+0x1d446: 'S',
+0x1d447: 'T',
+0x1d448: 'U',
+0x1d449: 'V',
+0x1d44a: 'W',
+0x1d44b: 'X',
+0x1d44c: 'Y',
+0x1d44d: 'Z',
+0x1d44e: 'a',
+0x1d44f: 'b',
+0x1d450: 'c',
+0x1d451: 'd',
+0x1d452: 'e',
+0x1d453: 'f',
+0x1d454: 'g',
+0x1d456: 'i',
+0x1d457: 'j',
+0x1d458: 'k',
+0x1d459: 'l',
+0x1d45a: 'm',
+0x1d45b: 'n',
+0x1d45c: 'o',
+0x1d45d: 'p',
+0x1d45e: 'q',
+0x1d45f: 'r',
+0x1d460: 's',
+0x1d461: 't',
+0x1d462: 'u',
+0x1d463: 'v',
+0x1d464: 'w',
+0x1d465: 'x',
+0x1d466: 'y',
+0x1d467: 'z',
+0x1d49c: '\\mathcal{A}',
+0x1d49e: '\\mathcal{C}',
+0x1d49f: '\\mathcal{D}',
+0x1d4a2: '\\mathcal{G}',
+0x1d4a5: '\\mathcal{J}',
+0x1d4a6: '\\mathcal{K}',
+0x1d4a9: '\\mathcal{N}',
+0x1d4aa: '\\mathcal{O}',
+0x1d4ab: '\\mathcal{P}',
+0x1d4ac: '\\mathcal{Q}',
+0x1d4ae: '\\mathcal{S}',
+0x1d4af: '\\mathcal{T}',
+0x1d4b0: '\\mathcal{U}',
+0x1d4b1: '\\mathcal{V}',
+0x1d4b2: '\\mathcal{W}',
+0x1d4b3: '\\mathcal{X}',
+0x1d4b4: '\\mathcal{Y}',
+0x1d4b5: '\\mathcal{Z}',
+0x1d504: '\\mathfrak{A}',
+0x1d505: '\\mathfrak{B}',
+0x1d507: '\\mathfrak{D}',
+0x1d508: '\\mathfrak{E}',
+0x1d509: '\\mathfrak{F}',
+0x1d50a: '\\mathfrak{G}',
+0x1d50d: '\\mathfrak{J}',
+0x1d50e: '\\mathfrak{K}',
+0x1d50f: '\\mathfrak{L}',
+0x1d510: '\\mathfrak{M}',
+0x1d511: '\\mathfrak{N}',
+0x1d512: '\\mathfrak{O}',
+0x1d513: '\\mathfrak{P}',
+0x1d514: '\\mathfrak{Q}',
+0x1d516: '\\mathfrak{S}',
+0x1d517: '\\mathfrak{T}',
+0x1d518: '\\mathfrak{U}',
+0x1d519: '\\mathfrak{V}',
+0x1d51a: '\\mathfrak{W}',
+0x1d51b: '\\mathfrak{X}',
+0x1d51c: '\\mathfrak{Y}',
+0x1d51e: '\\mathfrak{a}',
+0x1d51f: '\\mathfrak{b}',
+0x1d520: '\\mathfrak{c}',
+0x1d521: '\\mathfrak{d}',
+0x1d522: '\\mathfrak{e}',
+0x1d523: '\\mathfrak{f}',
+0x1d524: '\\mathfrak{g}',
+0x1d525: '\\mathfrak{h}',
+0x1d526: '\\mathfrak{i}',
+0x1d527: '\\mathfrak{j}',
+0x1d528: '\\mathfrak{k}',
+0x1d529: '\\mathfrak{l}',
+0x1d52a: '\\mathfrak{m}',
+0x1d52b: '\\mathfrak{n}',
+0x1d52c: '\\mathfrak{o}',
+0x1d52d: '\\mathfrak{p}',
+0x1d52e: '\\mathfrak{q}',
+0x1d52f: '\\mathfrak{r}',
+0x1d530: '\\mathfrak{s}',
+0x1d531: '\\mathfrak{t}',
+0x1d532: '\\mathfrak{u}',
+0x1d533: '\\mathfrak{v}',
+0x1d534: '\\mathfrak{w}',
+0x1d535: '\\mathfrak{x}',
+0x1d536: '\\mathfrak{y}',
+0x1d537: '\\mathfrak{z}',
+0x1d538: '\\mathbb{A}',
+0x1d539: '\\mathbb{B}',
+0x1d53b: '\\mathbb{D}',
+0x1d53c: '\\mathbb{E}',
+0x1d53d: '\\mathbb{F}',
+0x1d53e: '\\mathbb{G}',
+0x1d540: '\\mathbb{I}',
+0x1d541: '\\mathbb{J}',
+0x1d542: '\\mathbb{K}',
+0x1d543: '\\mathbb{L}',
+0x1d544: '\\mathbb{M}',
+0x1d546: '\\mathbb{O}',
+0x1d54a: '\\mathbb{S}',
+0x1d54b: '\\mathbb{T}',
+0x1d54c: '\\mathbb{U}',
+0x1d54d: '\\mathbb{V}',
+0x1d54e: '\\mathbb{W}',
+0x1d54f: '\\mathbb{X}',
+0x1d550: '\\mathbb{Y}',
+0x1d55c: '\\Bbbk ',
+0x1d5a0: '\\mathsf{A}',
+0x1d5a1: '\\mathsf{B}',
+0x1d5a2: '\\mathsf{C}',
+0x1d5a3: '\\mathsf{D}',
+0x1d5a4: '\\mathsf{E}',
+0x1d5a5: '\\mathsf{F}',
+0x1d5a6: '\\mathsf{G}',
+0x1d5a7: '\\mathsf{H}',
+0x1d5a8: '\\mathsf{I}',
+0x1d5a9: '\\mathsf{J}',
+0x1d5aa: '\\mathsf{K}',
+0x1d5ab: '\\mathsf{L}',
+0x1d5ac: '\\mathsf{M}',
+0x1d5ad: '\\mathsf{N}',
+0x1d5ae: '\\mathsf{O}',
+0x1d5af: '\\mathsf{P}',
+0x1d5b0: '\\mathsf{Q}',
+0x1d5b1: '\\mathsf{R}',
+0x1d5b2: '\\mathsf{S}',
+0x1d5b3: '\\mathsf{T}',
+0x1d5b4: '\\mathsf{U}',
+0x1d5b5: '\\mathsf{V}',
+0x1d5b6: '\\mathsf{W}',
+0x1d5b7: '\\mathsf{X}',
+0x1d5b8: '\\mathsf{Y}',
+0x1d5b9: '\\mathsf{Z}',
+0x1d5ba: '\\mathsf{a}',
+0x1d5bb: '\\mathsf{b}',
+0x1d5bc: '\\mathsf{c}',
+0x1d5bd: '\\mathsf{d}',
+0x1d5be: '\\mathsf{e}',
+0x1d5bf: '\\mathsf{f}',
+0x1d5c0: '\\mathsf{g}',
+0x1d5c1: '\\mathsf{h}',
+0x1d5c2: '\\mathsf{i}',
+0x1d5c3: '\\mathsf{j}',
+0x1d5c4: '\\mathsf{k}',
+0x1d5c5: '\\mathsf{l}',
+0x1d5c6: '\\mathsf{m}',
+0x1d5c7: '\\mathsf{n}',
+0x1d5c8: '\\mathsf{o}',
+0x1d5c9: '\\mathsf{p}',
+0x1d5ca: '\\mathsf{q}',
+0x1d5cb: '\\mathsf{r}',
+0x1d5cc: '\\mathsf{s}',
+0x1d5cd: '\\mathsf{t}',
+0x1d5ce: '\\mathsf{u}',
+0x1d5cf: '\\mathsf{v}',
+0x1d5d0: '\\mathsf{w}',
+0x1d5d1: '\\mathsf{x}',
+0x1d5d2: '\\mathsf{y}',
+0x1d5d3: '\\mathsf{z}',
+0x1d670: '\\mathtt{A}',
+0x1d671: '\\mathtt{B}',
+0x1d672: '\\mathtt{C}',
+0x1d673: '\\mathtt{D}',
+0x1d674: '\\mathtt{E}',
+0x1d675: '\\mathtt{F}',
+0x1d676: '\\mathtt{G}',
+0x1d677: '\\mathtt{H}',
+0x1d678: '\\mathtt{I}',
+0x1d679: '\\mathtt{J}',
+0x1d67a: '\\mathtt{K}',
+0x1d67b: '\\mathtt{L}',
+0x1d67c: '\\mathtt{M}',
+0x1d67d: '\\mathtt{N}',
+0x1d67e: '\\mathtt{O}',
+0x1d67f: '\\mathtt{P}',
+0x1d680: '\\mathtt{Q}',
+0x1d681: '\\mathtt{R}',
+0x1d682: '\\mathtt{S}',
+0x1d683: '\\mathtt{T}',
+0x1d684: '\\mathtt{U}',
+0x1d685: '\\mathtt{V}',
+0x1d686: '\\mathtt{W}',
+0x1d687: '\\mathtt{X}',
+0x1d688: '\\mathtt{Y}',
+0x1d689: '\\mathtt{Z}',
+0x1d68a: '\\mathtt{a}',
+0x1d68b: '\\mathtt{b}',
+0x1d68c: '\\mathtt{c}',
+0x1d68d: '\\mathtt{d}',
+0x1d68e: '\\mathtt{e}',
+0x1d68f: '\\mathtt{f}',
+0x1d690: '\\mathtt{g}',
+0x1d691: '\\mathtt{h}',
+0x1d692: '\\mathtt{i}',
+0x1d693: '\\mathtt{j}',
+0x1d694: '\\mathtt{k}',
+0x1d695: '\\mathtt{l}',
+0x1d696: '\\mathtt{m}',
+0x1d697: '\\mathtt{n}',
+0x1d698: '\\mathtt{o}',
+0x1d699: '\\mathtt{p}',
+0x1d69a: '\\mathtt{q}',
+0x1d69b: '\\mathtt{r}',
+0x1d69c: '\\mathtt{s}',
+0x1d69d: '\\mathtt{t}',
+0x1d69e: '\\mathtt{u}',
+0x1d69f: '\\mathtt{v}',
+0x1d6a0: '\\mathtt{w}',
+0x1d6a1: '\\mathtt{x}',
+0x1d6a2: '\\mathtt{y}',
+0x1d6a3: '\\mathtt{z}',
+0x1d6a4: '\\imath ',
+0x1d6a5: '\\jmath ',
+0x1d6aa: '\\mathbf{\\Gamma}',
+0x1d6ab: '\\mathbf{\\Delta}',
+0x1d6af: '\\mathbf{\\Theta}',
+0x1d6b2: '\\mathbf{\\Lambda}',
+0x1d6b5: '\\mathbf{\\Xi}',
+0x1d6b7: '\\mathbf{\\Pi}',
+0x1d6ba: '\\mathbf{\\Sigma}',
+0x1d6bc: '\\mathbf{\\Upsilon}',
+0x1d6bd: '\\mathbf{\\Phi}',
+0x1d6bf: '\\mathbf{\\Psi}',
+0x1d6c0: '\\mathbf{\\Omega}',
+0x1d6e4: '\\mathit{\\Gamma}',
+0x1d6e5: '\\mathit{\\Delta}',
+0x1d6e9: '\\mathit{\\Theta}',
+0x1d6ec: '\\mathit{\\Lambda}',
+0x1d6ef: '\\mathit{\\Xi}',
+0x1d6f1: '\\mathit{\\Pi}',
+0x1d6f4: '\\mathit{\\Sigma}',
+0x1d6f6: '\\mathit{\\Upsilon}',
+0x1d6f7: '\\mathit{\\Phi}',
+0x1d6f9: '\\mathit{\\Psi}',
+0x1d6fa: '\\mathit{\\Omega}',
+0x1d6fc: '\\alpha ',
+0x1d6fd: '\\beta ',
+0x1d6fe: '\\gamma ',
+0x1d6ff: '\\delta ',
+0x1d700: '\\varepsilon ',
+0x1d701: '\\zeta ',
+0x1d702: '\\eta ',
+0x1d703: '\\theta ',
+0x1d704: '\\iota ',
+0x1d705: '\\kappa ',
+0x1d706: '\\lambda ',
+0x1d707: '\\mu ',
+0x1d708: '\\nu ',
+0x1d709: '\\xi ',
+0x1d70b: '\\pi ',
+0x1d70c: '\\rho ',
+0x1d70d: '\\varsigma ',
+0x1d70e: '\\sigma ',
+0x1d70f: '\\tau ',
+0x1d710: '\\upsilon ',
+0x1d711: '\\varphi ',
+0x1d712: '\\chi ',
+0x1d713: '\\psi ',
+0x1d714: '\\omega ',
+0x1d715: '\\partial ',
+0x1d716: '\\epsilon ',
+0x1d717: '\\vartheta ',
+0x1d718: '\\varkappa ',
+0x1d719: '\\phi ',
+0x1d71a: '\\varrho ',
+0x1d71b: '\\varpi ',
+0x1d7ce: '\\mathbf{0}',
+0x1d7cf: '\\mathbf{1}',
+0x1d7d0: '\\mathbf{2}',
+0x1d7d1: '\\mathbf{3}',
+0x1d7d2: '\\mathbf{4}',
+0x1d7d3: '\\mathbf{5}',
+0x1d7d4: '\\mathbf{6}',
+0x1d7d5: '\\mathbf{7}',
+0x1d7d6: '\\mathbf{8}',
+0x1d7d7: '\\mathbf{9}',
+0x1d7e2: '\\mathsf{0}',
+0x1d7e3: '\\mathsf{1}',
+0x1d7e4: '\\mathsf{2}',
+0x1d7e5: '\\mathsf{3}',
+0x1d7e6: '\\mathsf{4}',
+0x1d7e7: '\\mathsf{5}',
+0x1d7e8: '\\mathsf{6}',
+0x1d7e9: '\\mathsf{7}',
+0x1d7ea: '\\mathsf{8}',
+0x1d7eb: '\\mathsf{9}',
+0x1d7f6: '\\mathtt{0}',
+0x1d7f7: '\\mathtt{1}',
+0x1d7f8: '\\mathtt{2}',
+0x1d7f9: '\\mathtt{3}',
+0x1d7fa: '\\mathtt{4}',
+0x1d7fb: '\\mathtt{5}',
+0x1d7fc: '\\mathtt{6}',
+0x1d7fd: '\\mathtt{7}',
+0x1d7fe: '\\mathtt{8}',
+0x1d7ff: '\\mathtt{9}',
+}
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/punctuation_chars.py b/.venv/lib/python3.12/site-packages/docutils/utils/punctuation_chars.py
new file mode 100644
index 00000000..9dd21404
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/punctuation_chars.py
@@ -0,0 +1,123 @@
+# :Id: $Id: punctuation_chars.py 9270 2022-11-24 20:28:03Z milde $
+# :Copyright: © 2011, 2017 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
+#
+# This file is generated by
+# ``docutils/tools/dev/generate_punctuation_chars.py``.
+# ::
+
+"""Docutils character category patterns.
+
+ Patterns for the implementation of the `inline markup recognition rules`_
+ in the reStructuredText parser `docutils.parsers.rst.states.py` based
+ on Unicode character categories.
+ The patterns are used inside ``[ ]`` in regular expressions.
+
+ Rule (5) requires determination of matching open/close pairs. However, the
+ pairing of open/close quotes is ambiguous due to different typographic
+ conventions in different languages. The ``quote_pairs`` function tests
+ whether two characters form an open/close pair.
+
+ The patterns are generated by
+ ``docutils/tools/dev/generate_punctuation_chars.py`` to prevent dependence
+ on the Python version and avoid the time-consuming generation with every
+ Docutils run. See there for motives and implementation details.
+
+ The category of some characters changed with the development of the
+ Unicode standard. The current lists are generated with the help of the
+ "unicodedata" module of Python 2.7.13 (based on Unicode version 5.2.0).
+
+ .. _inline markup recognition rules:
+ https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html
+ #inline-markup-recognition-rules
+"""
+
+openers = (
+ '"\'(<\\[{\u0f3a\u0f3c\u169b\u2045\u207d\u208d\u2329\u2768'
+ '\u276a\u276c\u276e\u2770\u2772\u2774\u27c5\u27e6\u27e8\u27ea'
+ '\u27ec\u27ee\u2983\u2985\u2987\u2989\u298b\u298d\u298f\u2991'
+ '\u2993\u2995\u2997\u29d8\u29da\u29fc\u2e22\u2e24\u2e26\u2e28'
+ '\u3008\u300a\u300c\u300e\u3010\u3014\u3016\u3018\u301a\u301d'
+ '\u301d\ufd3e\ufe17\ufe35\ufe37\ufe39\ufe3b\ufe3d\ufe3f\ufe41'
+ '\ufe43\ufe47\ufe59\ufe5b\ufe5d\uff08\uff3b\uff5b\uff5f\uff62'
+ '\xab\u2018\u201c\u2039\u2e02\u2e04\u2e09\u2e0c\u2e1c\u2e20'
+ '\u201a\u201e\xbb\u2019\u201d\u203a\u2e03\u2e05\u2e0a\u2e0d'
+ '\u2e1d\u2e21\u201b\u201f'
+ )
+closers = (
+ '"\')>\\]}\u0f3b\u0f3d\u169c\u2046\u207e\u208e\u232a\u2769'
+ '\u276b\u276d\u276f\u2771\u2773\u2775\u27c6\u27e7\u27e9\u27eb'
+ '\u27ed\u27ef\u2984\u2986\u2988\u298a\u298c\u298e\u2990\u2992'
+ '\u2994\u2996\u2998\u29d9\u29db\u29fd\u2e23\u2e25\u2e27\u2e29'
+ '\u3009\u300b\u300d\u300f\u3011\u3015\u3017\u3019\u301b\u301e'
+ '\u301f\ufd3f\ufe18\ufe36\ufe38\ufe3a\ufe3c\ufe3e\ufe40\ufe42'
+ '\ufe44\ufe48\ufe5a\ufe5c\ufe5e\uff09\uff3d\uff5d\uff60\uff63'
+ '\xbb\u2019\u201d\u203a\u2e03\u2e05\u2e0a\u2e0d\u2e1d\u2e21'
+ '\u201b\u201f\xab\u2018\u201c\u2039\u2e02\u2e04\u2e09\u2e0c'
+ '\u2e1c\u2e20\u201a\u201e'
+ )
+delimiters = (
+ '\\-/:\u058a\xa1\xb7\xbf\u037e\u0387\u055a-\u055f\u0589'
+ '\u05be\u05c0\u05c3\u05c6\u05f3\u05f4\u0609\u060a\u060c'
+ '\u060d\u061b\u061e\u061f\u066a-\u066d\u06d4\u0700-\u070d'
+ '\u07f7-\u07f9\u0830-\u083e\u0964\u0965\u0970\u0df4\u0e4f'
+ '\u0e5a\u0e5b\u0f04-\u0f12\u0f85\u0fd0-\u0fd4\u104a-\u104f'
+ '\u10fb\u1361-\u1368\u1400\u166d\u166e\u16eb-\u16ed\u1735'
+ '\u1736\u17d4-\u17d6\u17d8-\u17da\u1800-\u180a\u1944\u1945'
+ '\u19de\u19df\u1a1e\u1a1f\u1aa0-\u1aa6\u1aa8-\u1aad\u1b5a-'
+ '\u1b60\u1c3b-\u1c3f\u1c7e\u1c7f\u1cd3\u2010-\u2017\u2020-'
+ '\u2027\u2030-\u2038\u203b-\u203e\u2041-\u2043\u2047-'
+ '\u2051\u2053\u2055-\u205e\u2cf9-\u2cfc\u2cfe\u2cff\u2e00'
+ '\u2e01\u2e06-\u2e08\u2e0b\u2e0e-\u2e1b\u2e1e\u2e1f\u2e2a-'
+ '\u2e2e\u2e30\u2e31\u3001-\u3003\u301c\u3030\u303d\u30a0'
+ '\u30fb\ua4fe\ua4ff\ua60d-\ua60f\ua673\ua67e\ua6f2-\ua6f7'
+ '\ua874-\ua877\ua8ce\ua8cf\ua8f8-\ua8fa\ua92e\ua92f\ua95f'
+ '\ua9c1-\ua9cd\ua9de\ua9df\uaa5c-\uaa5f\uaade\uaadf\uabeb'
+ '\ufe10-\ufe16\ufe19\ufe30-\ufe32\ufe45\ufe46\ufe49-\ufe4c'
+ '\ufe50-\ufe52\ufe54-\ufe58\ufe5f-\ufe61\ufe63\ufe68\ufe6a'
+ '\ufe6b\uff01-\uff03\uff05-\uff07\uff0a\uff0c-\uff0f\uff1a'
+ '\uff1b\uff1f\uff20\uff3c\uff61\uff64\uff65'
+ '\U00010100\U00010101\U0001039f\U000103d0\U00010857'
+ '\U0001091f\U0001093f\U00010a50-\U00010a58\U00010a7f'
+ '\U00010b39-\U00010b3f\U000110bb\U000110bc\U000110be-'
+ '\U000110c1\U00012470-\U00012473'
+ )
+closing_delimiters = r'\\.,;!?'
+
+
+# Matching open/close quotes
+# --------------------------
+
+# Matching open/close pairs are at the same position in
+# `punctuation_chars.openers` and `punctuation_chars.closers`.
+# Additional matches (due to different typographic conventions
+# in different languages) are stored in `quote_pairs`.
+
+quote_pairs = {
+ # open char: matching closing characters # use case
+ '\xbb': '\xbb', # » » Swedish
+ '\u2018': '\u201a', # ‘ ‚ Albanian/Greek/Turkish
+ '\u2019': '\u2019', # ’ ’ Swedish
+ '\u201a': '\u2018\u2019', # ‚ ‘ German, ‚ ’ Polish
+ '\u201c': '\u201e', # “ „ Albanian/Greek/Turkish
+ '\u201e': '\u201c\u201d', # „ “ German, „ ” Polish
+ '\u201d': '\u201d', # ” ” Swedish
+ '\u203a': '\u203a', # › › Swedish
+ }
+"""Additional open/close quote pairs."""
+
+
+def match_chars(c1, c2):
+ """Test whether `c1` and `c2` are a matching open/close character pair."""
+ try:
+ i = openers.index(c1)
+ except ValueError: # c1 not in openers
+ return False
+ return c2 == closers[i] or c2 in quote_pairs.get(c1, '')
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/roman.py b/.venv/lib/python3.12/site-packages/docutils/utils/roman.py
new file mode 100644
index 00000000..df0c5b33
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/roman.py
@@ -0,0 +1,154 @@
+##############################################################################
+#
+# Copyright (c) 2001 Mark Pilgrim and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Convert to and from Roman numerals"""
+
+__author__ = "Mark Pilgrim (f8dy@diveintopython.org)"
+__version__ = "1.4"
+__date__ = "8 August 2001"
+__copyright__ = """Copyright (c) 2001 Mark Pilgrim
+
+This program is part of "Dive Into Python", a free Python tutorial for
+experienced programmers. Visit http://diveintopython.org/ for the
+latest version.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the Python 2.1.1 license, available at
+http://www.python.org/2.1.1/license.html
+"""
+
+import argparse
+import re
+import sys
+
+
+# Define exceptions
+class RomanError(Exception):
+ pass
+
+
+class OutOfRangeError(RomanError):
+ pass
+
+
+class NotIntegerError(RomanError):
+ pass
+
+
+class InvalidRomanNumeralError(RomanError):
+ pass
+
+
+# Define digit mapping
+romanNumeralMap = (('M', 1000),
+ ('CM', 900),
+ ('D', 500),
+ ('CD', 400),
+ ('C', 100),
+ ('XC', 90),
+ ('L', 50),
+ ('XL', 40),
+ ('X', 10),
+ ('IX', 9),
+ ('V', 5),
+ ('IV', 4),
+ ('I', 1))
+
+
+def toRoman(n):
+ """convert integer to Roman numeral"""
+ if not isinstance(n, int):
+ raise NotIntegerError("decimals can not be converted")
+ if not (-1 < n < 5000):
+ raise OutOfRangeError("number out of range (must be 0..4999)")
+
+ # special case
+ if n == 0:
+ return 'N'
+
+ result = ""
+ for numeral, integer in romanNumeralMap:
+ while n >= integer:
+ result += numeral
+ n -= integer
+ return result
+
+
+# Define pattern to detect valid Roman numerals
+romanNumeralPattern = re.compile("""
+ ^ # beginning of string
+ M{0,4} # thousands - 0 to 4 M's
+ (CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
+ # or 500-800 (D, followed by 0 to 3 C's)
+ (XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
+ # or 50-80 (L, followed by 0 to 3 X's)
+ (IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
+ # or 5-8 (V, followed by 0 to 3 I's)
+ $ # end of string
+ """, re.VERBOSE)
+
+
+def fromRoman(s):
+ """convert Roman numeral to integer"""
+ if not s:
+ raise InvalidRomanNumeralError('Input can not be blank')
+
+ # special case
+ if s == 'N':
+ return 0
+
+ if not romanNumeralPattern.search(s):
+ raise InvalidRomanNumeralError('Invalid Roman numeral: %s' % s)
+
+ result = 0
+ index = 0
+ for numeral, integer in romanNumeralMap:
+ while s[index:index + len(numeral)] == numeral:
+ result += integer
+ index += len(numeral)
+ return result
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(
+ prog='roman',
+ description='convert between roman and arabic numerals'
+ )
+ parser.add_argument('number', help='the value to convert')
+ parser.add_argument(
+ '-r', '--reverse',
+ action='store_true',
+ default=False,
+ help='convert roman to numeral (case insensitive) [default: False]')
+
+ args = parser.parse_args()
+ args.number = args.number
+ return args
+
+
+def main():
+ args = parse_args()
+ if args.reverse:
+ u = args.number.upper()
+ r = fromRoman(u)
+ print(r)
+ else:
+ i = int(args.number)
+ n = toRoman(i)
+ print(n)
+
+ return 0
+
+
+if __name__ == "__main__": # pragma: no cover
+ sys.exit(main()) # pragma: no cover
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/smartquotes.py b/.venv/lib/python3.12/site-packages/docutils/utils/smartquotes.py
new file mode 100755
index 00000000..b8766db2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/smartquotes.py
@@ -0,0 +1,1004 @@
+#!/usr/bin/python3
+# :Id: $Id: smartquotes.py 9481 2023-11-19 21:19:20Z milde $
+# :Copyright: © 2010-2023 Günter Milde,
+# original `SmartyPants`_: © 2003 John Gruber
+# smartypants.py: © 2004, 2007 Chad Miller
+# :Maintainer: docutils-develop@lists.sourceforge.net
+# :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
+# notices 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
+
+
+r"""
+=========================
+Smart Quotes for Docutils
+=========================
+
+Synopsis
+========
+
+"SmartyPants" is a free web publishing plug-in for Movable Type, Blosxom, and
+BBEdit that easily translates plain ASCII punctuation characters into "smart"
+typographic punctuation characters.
+
+``smartquotes.py`` is an adaption of "SmartyPants" to Docutils_.
+
+* Using Unicode instead of HTML entities for typographic punctuation
+ characters, it works for any output format that supports Unicode.
+* Supports `language specific quote characters`__.
+
+__ https://en.wikipedia.org/wiki/Non-English_usage_of_quotation_marks
+
+
+Authors
+=======
+
+`John Gruber`_ did all of the hard work of writing this software in Perl for
+`Movable Type`_ and almost all of this useful documentation. `Chad Miller`_
+ported it to Python to use with Pyblosxom_.
+Adapted to Docutils_ by Günter Milde.
+
+Additional Credits
+==================
+
+Portions of the SmartyPants original work are based on Brad Choate's nifty
+MTRegex plug-in. `Brad Choate`_ also contributed a few bits of source code to
+this plug-in. Brad Choate is a fine hacker indeed.
+
+`Jeremy Hedley`_ and `Charles Wiltgen`_ deserve mention for exemplary beta
+testing of the original SmartyPants.
+
+`Rael Dornfest`_ ported SmartyPants to Blosxom.
+
+.. _Brad Choate: http://bradchoate.com/
+.. _Jeremy Hedley: http://antipixel.com/
+.. _Charles Wiltgen: http://playbacktime.com/
+.. _Rael Dornfest: http://raelity.org/
+
+
+Copyright and License
+=====================
+
+SmartyPants_ license (3-Clause BSD license):
+
+ Copyright (c) 2003 John Gruber (http://daringfireball.net/)
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name "SmartyPants" nor the names of its contributors
+ may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ This software is provided by the copyright holders and contributors
+ "as is" and any express or implied warranties, including, but not
+ limited to, the implied warranties of merchantability and fitness for
+ a particular purpose are disclaimed. In no event shall the copyright
+ owner or contributors be liable for any direct, indirect, incidental,
+ special, exemplary, or consequential damages (including, but not
+ limited to, procurement of substitute goods or services; loss of use,
+ data, or profits; or business interruption) however caused and on any
+ theory of liability, whether in contract, strict liability, or tort
+ (including negligence or otherwise) arising in any way out of the use
+ of this software, even if advised of the possibility of such damage.
+
+smartypants.py license (2-Clause BSD license):
+
+ smartypants.py is a derivative work of SmartyPants.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ This software is provided by the copyright holders and contributors
+ "as is" and any express or implied warranties, including, but not
+ limited to, the implied warranties of merchantability and fitness for
+ a particular purpose are disclaimed. In no event shall the copyright
+ owner or contributors be liable for any direct, indirect, incidental,
+ special, exemplary, or consequential damages (including, but not
+ limited to, procurement of substitute goods or services; loss of use,
+ data, or profits; or business interruption) however caused and on any
+ theory of liability, whether in contract, strict liability, or tort
+ (including negligence or otherwise) arising in any way out of the use
+ of this software, even if advised of the possibility of such damage.
+
+.. _John Gruber: http://daringfireball.net/
+.. _Chad Miller: http://web.chad.org/
+
+.. _Pyblosxom: http://pyblosxom.bluesock.org/
+.. _SmartyPants: http://daringfireball.net/projects/smartypants/
+.. _Movable Type: http://www.movabletype.org/
+.. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+.. _Docutils: https://docutils.sourceforge.io/
+
+Description
+===========
+
+SmartyPants can perform the following transformations:
+
+- Straight quotes ( " and ' ) into "curly" quote characters
+- Backticks-style quotes (\`\`like this'') into "curly" quote characters
+- Dashes (``--`` and ``---``) into en- and em-dash entities
+- Three consecutive dots (``...`` or ``. . .``) into an ellipsis ``…``.
+
+This means you can write, edit, and save your posts using plain old
+ASCII straight quotes, plain dashes, and plain dots, but your published
+posts (and final HTML output) will appear with smart quotes, em-dashes,
+and proper ellipses.
+
+Backslash Escapes
+=================
+
+If you need to use literal straight quotes (or plain hyphens and periods),
+`smartquotes` accepts the following backslash escape sequences to force
+ASCII-punctuation. Mind, that you need two backslashes in "docstrings", as
+Python expands them, too.
+
+======== =========
+Escape Character
+======== =========
+``\\`` \\
+``\\"`` \\"
+``\\'`` \\'
+``\\.`` \\.
+``\\-`` \\-
+``\\``` \\`
+======== =========
+
+This is useful, for example, when you want to use straight quotes as
+foot and inch marks: 6\\'2\\" tall; a 17\\" iMac.
+
+
+Caveats
+=======
+
+Why You Might Not Want to Use Smart Quotes in Your Weblog
+---------------------------------------------------------
+
+For one thing, you might not care.
+
+Most normal, mentally stable individuals do not take notice of proper
+typographic punctuation. Many design and typography nerds, however, break
+out in a nasty rash when they encounter, say, a restaurant sign that uses
+a straight apostrophe to spell "Joe's".
+
+If you're the sort of person who just doesn't care, you might well want to
+continue not caring. Using straight quotes -- and sticking to the 7-bit
+ASCII character set in general -- is certainly a simpler way to live.
+
+Even if you *do* care about accurate typography, you still might want to
+think twice before educating the quote characters in your weblog. One side
+effect of publishing curly quote characters is that it makes your
+weblog a bit harder for others to quote from using copy-and-paste. What
+happens is that when someone copies text from your blog, the copied text
+contains the 8-bit curly quote characters (as well as the 8-bit characters
+for em-dashes and ellipses, if you use these options). These characters
+are not standard across different text encoding methods, which is why they
+need to be encoded as characters.
+
+People copying text from your weblog, however, may not notice that you're
+using curly quotes, and they'll go ahead and paste the unencoded 8-bit
+characters copied from their browser into an email message or their own
+weblog. When pasted as raw "smart quotes", these characters are likely to
+get mangled beyond recognition.
+
+That said, my own opinion is that any decent text editor or email client
+makes it easy to stupefy smart quote characters into their 7-bit
+equivalents, and I don't consider it my problem if you're using an
+indecent text editor or email client.
+
+
+Algorithmic Shortcomings
+------------------------
+
+One situation in which quotes will get curled the wrong way is when
+apostrophes are used at the start of leading contractions. For example::
+
+ 'Twas the night before Christmas.
+
+In the case above, SmartyPants will turn the apostrophe into an opening
+secondary quote, when in fact it should be the `RIGHT SINGLE QUOTATION MARK`
+character which is also "the preferred character to use for apostrophe"
+(Unicode). I don't think this problem can be solved in the general case --
+every word processor I've tried gets this wrong as well. In such cases, it's
+best to inset the `RIGHT SINGLE QUOTATION MARK` (’) by hand.
+
+In English, the same character is used for apostrophe and closing secondary
+quote (both plain and "smart" ones). For other locales (French, Italean,
+Swiss, ...) "smart" secondary closing quotes differ from the curly apostrophe.
+
+ .. class:: language-fr
+
+ Il dit : "C'est 'super' !"
+
+If the apostrophe is used at the end of a word, it cannot be distinguished
+from a secondary quote by the algorithm. Therefore, a text like::
+
+ .. class:: language-de-CH
+
+ "Er sagt: 'Ich fass' es nicht.'"
+
+will get a single closing guillemet instead of an apostrophe.
+
+This can be prevented by use use of the `RIGHT SINGLE QUOTATION MARK` in
+the source::
+
+ - "Er sagt: 'Ich fass' es nicht.'"
+ + "Er sagt: 'Ich fass’ es nicht.'"
+
+
+Version History
+===============
+
+1.10 2023-11-18
+ - Pre-compile regexps once, not with every call of `educateQuotes()`
+ (patch #206 by Chris Sewell). Simplify regexps.
+
+1.9 2022-03-04
+ - Code cleanup. Require Python 3.
+
+1.8.1 2017-10-25
+ - Use open quote after Unicode whitespace, ZWSP, and ZWNJ.
+ - Code cleanup.
+
+1.8: 2017-04-24
+ - Command line front-end.
+
+1.7.1: 2017-03-19
+ - Update and extend language-dependent quotes.
+ - Differentiate apostrophe from single quote.
+
+1.7: 2012-11-19
+ - Internationalization: language-dependent quotes.
+
+1.6.1: 2012-11-06
+ - Refactor code, code cleanup,
+ - `educate_tokens()` generator as interface for Docutils.
+
+1.6: 2010-08-26
+ - Adaption to Docutils:
+ - Use Unicode instead of HTML entities,
+ - Remove code special to pyblosxom.
+
+1.5_1.6: Fri, 27 Jul 2007 07:06:40 -0400
+ - Fixed bug where blocks of precious unalterable text was instead
+ interpreted. Thanks to Le Roux and Dirk van Oosterbosch.
+
+1.5_1.5: Sat, 13 Aug 2005 15:50:24 -0400
+ - Fix bogus magical quotation when there is no hint that the
+ user wants it, e.g., in "21st century". Thanks to Nathan Hamblen.
+ - Be smarter about quotes before terminating numbers in an en-dash'ed
+ range.
+
+1.5_1.4: Thu, 10 Feb 2005 20:24:36 -0500
+ - Fix a date-processing bug, as reported by jacob childress.
+ - Begin a test-suite for ensuring correct output.
+ - Removed import of "string", since I didn't really need it.
+ (This was my first every Python program. Sue me!)
+
+1.5_1.3: Wed, 15 Sep 2004 18:25:58 -0400
+ - Abort processing if the flavour is in forbidden-list. Default of
+ [ "rss" ] (Idea of Wolfgang SCHNERRING.)
+ - Remove stray virgules from en-dashes. Patch by Wolfgang SCHNERRING.
+
+1.5_1.2: Mon, 24 May 2004 08:14:54 -0400
+ - Some single quotes weren't replaced properly. Diff-tesuji played
+ by Benjamin GEIGER.
+
+1.5_1.1: Sun, 14 Mar 2004 14:38:28 -0500
+ - Support upcoming pyblosxom 0.9 plugin verification feature.
+
+1.5_1.0: Tue, 09 Mar 2004 08:08:35 -0500
+ - Initial release
+"""
+
+import re
+import sys
+
+
+options = r"""
+Options
+=======
+
+Numeric values are the easiest way to configure SmartyPants' behavior:
+
+:0: Suppress all transformations. (Do nothing.)
+
+:1: Performs default SmartyPants transformations: quotes (including
+ \`\`backticks'' -style), em-dashes, and ellipses. "``--``" (dash dash)
+ is used to signify an em-dash; there is no support for en-dashes
+
+:2: Same as smarty_pants="1", except that it uses the old-school typewriter
+ shorthand for dashes: "``--``" (dash dash) for en-dashes, "``---``"
+ (dash dash dash)
+ for em-dashes.
+
+:3: Same as smarty_pants="2", but inverts the shorthand for dashes:
+ "``--``" (dash dash) for em-dashes, and "``---``" (dash dash dash) for
+ en-dashes.
+
+:-1: Stupefy mode. Reverses the SmartyPants transformation process, turning
+ the characters produced by SmartyPants into their ASCII equivalents.
+ E.g. the LEFT DOUBLE QUOTATION MARK (“) is turned into a simple
+ double-quote (\"), "—" is turned into two dashes, etc.
+
+
+The following single-character attribute values can be combined to toggle
+individual transformations from within the smarty_pants attribute. For
+example, ``"1"`` is equivalent to ``"qBde"``.
+
+:q: Educates normal quote characters: (") and (').
+
+:b: Educates \`\`backticks'' -style double quotes.
+
+:B: Educates \`\`backticks'' -style double quotes and \`single' quotes.
+
+:d: Educates em-dashes.
+
+:D: Educates em-dashes and en-dashes, using old-school typewriter
+ shorthand: (dash dash) for en-dashes, (dash dash dash) for em-dashes.
+
+:i: Educates em-dashes and en-dashes, using inverted old-school typewriter
+ shorthand: (dash dash) for em-dashes, (dash dash dash) for en-dashes.
+
+:e: Educates ellipses.
+
+:w: Translates any instance of ``&quot;`` into a normal double-quote
+ character. This should be of no interest to most people, but
+ of particular interest to anyone who writes their posts using
+ Dreamweaver, as Dreamweaver inexplicably uses this entity to represent
+ a literal double-quote character. SmartyPants only educates normal
+ quotes, not entities (because ordinarily, entities are used for
+ the explicit purpose of representing the specific character they
+ represent). The "w" option must be used in conjunction with one (or
+ both) of the other quote options ("q" or "b"). Thus, if you wish to
+ apply all SmartyPants transformations (quotes, en- and em-dashes, and
+ ellipses) and also translate ``&quot;`` entities into regular quotes
+ so SmartyPants can educate them, you should pass the following to the
+ smarty_pants attribute:
+"""
+
+
+class smartchars:
+ """Smart quotes and dashes"""
+
+ endash = '–' # EN DASH
+ emdash = '—' # EM DASH
+ ellipsis = '…' # HORIZONTAL ELLIPSIS
+ apostrophe = '’' # RIGHT SINGLE QUOTATION MARK
+
+ # quote characters (language-specific, set in __init__())
+ # https://en.wikipedia.org/wiki/Non-English_usage_of_quotation_marks
+ # https://de.wikipedia.org/wiki/Anf%C3%BChrungszeichen#Andere_Sprachen
+ # https://fr.wikipedia.org/wiki/Guillemet
+ # https://typographisme.net/post/Les-espaces-typographiques-et-le-web
+ # https://www.btb.termiumplus.gc.ca/tpv2guides/guides/redac/index-fra.html
+ # https://en.wikipedia.org/wiki/Hebrew_punctuation#Quotation_marks
+ # [7] https://www.tustep.uni-tuebingen.de/bi/bi00/bi001t1-anfuehrung.pdf
+ # [8] https://www.korrekturavdelingen.no/anforselstegn.htm
+ # [9] Typografisk håndbok. Oslo: Spartacus. 2000. s. 67. ISBN 8243001530.
+ # [10] https://www.typografi.org/sitat/sitatart.html
+ # [11] https://mk.wikipedia.org/wiki/Правопис_и_правоговор_на_македонскиот_јазик # noqa:E501
+ # [12] https://hrvatska-tipografija.com/polunavodnici/
+ # [13] https://pl.wikipedia.org/wiki/Cudzys%C5%82%C3%B3w
+ #
+ # See also configuration option "smartquote-locales".
+ quotes = {
+ 'af': '“”‘’',
+ 'af-x-altquot': '„”‚’',
+ 'bg': '„“‚‘', # https://bg.wikipedia.org/wiki/Кавички
+ 'ca': '«»“”',
+ 'ca-x-altquot': '“”‘’',
+ 'cs': '„“‚‘',
+ 'cs-x-altquot': '»«›‹',
+ 'da': '»«›‹',
+ 'da-x-altquot': '„“‚‘',
+ # 'da-x-altquot2': '””’’',
+ 'de': '„“‚‘',
+ 'de-x-altquot': '»«›‹',
+ 'de-ch': '«»‹›',
+ 'el': '«»“”', # '«»‟”' https://hal.science/hal-02101618
+ 'en': '“”‘’',
+ 'en-uk-x-altquot': '‘’“”', # Attention: " → ‘ and ' → “ !
+ 'eo': '“”‘’',
+ 'es': '«»“”',
+ 'es-x-altquot': '“”‘’',
+ 'et': '„“‚‘', # no secondary quote listed in
+ 'et-x-altquot': '«»‹›', # the sources above (wikipedia.org)
+ 'eu': '«»‹›',
+ 'fi': '””’’',
+ 'fi-x-altquot': '»»››',
+ 'fr': ('« ', ' »', '“', '”'), # full no-break space
+ 'fr-x-altquot': ('« ', ' »', '“', '”'), # narrow no-break space
+ 'fr-ch': '«»‹›', # https://typoguide.ch/
+ 'fr-ch-x-altquot': ('« ', ' »', '‹ ', ' ›'), # narrow no-break space # noqa:E501
+ 'gl': '«»“”',
+ 'he': '”“»«', # Hebrew is RTL, test position:
+ 'he-x-altquot': '„”‚’', # low quotation marks are opening.
+ # 'he-x-altquot': '“„‘‚', # RTL: low quotation marks opening
+ 'hr': '„”‘’', # Croatian [12]
+ 'hr-x-altquot': '»«›‹',
+ 'hsb': '„“‚‘',
+ 'hsb-x-altquot': '»«›‹',
+ 'hu': '„”«»',
+ 'is': '„“‚‘',
+ 'it': '«»“”',
+ 'it-ch': '«»‹›',
+ 'it-x-altquot': '“”‘’',
+ # 'it-x-altquot2': '“„‘‚', # [7] in headlines
+ 'ja': '「」『』',
+ 'ko': '“”‘’',
+ 'lt': '„“‚‘',
+ 'lv': '„“‚‘',
+ 'mk': '„“‚‘', # Macedonian [11]
+ 'nl': '“”‘’',
+ 'nl-x-altquot': '„”‚’',
+ # 'nl-x-altquot2': '””’’',
+ 'nb': '«»’’', # Norsk bokmål (canonical form 'no')
+ 'nn': '«»’’', # Nynorsk [10]
+ 'nn-x-altquot': '«»‘’', # [8], [10]
+ # 'nn-x-altquot2': '«»«»', # [9], [10]
+ # 'nn-x-altquot3': '„“‚‘', # [10]
+ 'no': '«»’’', # Norsk bokmål [10]
+ 'no-x-altquot': '«»‘’', # [8], [10]
+ # 'no-x-altquot2': '«»«»', # [9], [10
+ # 'no-x-altquot3': '„“‚‘', # [10]
+ 'pl': '„”«»',
+ 'pl-x-altquot': '«»‚’',
+ # 'pl-x-altquot2': '„”‚’', # [13]
+ 'pt': '«»“”',
+ 'pt-br': '“”‘’',
+ 'ro': '„”«»',
+ 'ru': '«»„“',
+ 'sh': '„”‚’', # Serbo-Croatian
+ 'sh-x-altquot': '»«›‹',
+ 'sk': '„“‚‘', # Slovak
+ 'sk-x-altquot': '»«›‹',
+ 'sl': '„“‚‘', # Slovenian
+ 'sl-x-altquot': '»«›‹',
+ 'sq': '«»‹›', # Albanian
+ 'sq-x-altquot': '“„‘‚',
+ 'sr': '„”’’',
+ 'sr-x-altquot': '»«›‹',
+ 'sv': '””’’',
+ 'sv-x-altquot': '»»››',
+ 'tr': '“”‘’',
+ 'tr-x-altquot': '«»‹›',
+ # 'tr-x-altquot2': '“„‘‚', # [7] antiquated?
+ 'uk': '«»„“',
+ 'uk-x-altquot': '„“‚‘',
+ 'zh-cn': '“”‘’',
+ 'zh-tw': '「」『』',
+ }
+
+ def __init__(self, language='en'):
+ self.language = language
+ try:
+ (self.opquote, self.cpquote,
+ self.osquote, self.csquote) = self.quotes[language.lower()]
+ except KeyError:
+ self.opquote, self.cpquote, self.osquote, self.csquote = '""\'\''
+
+
+class RegularExpressions:
+ # character classes:
+ _CH_CLASSES = {'open': '[([{]', # opening braces
+ 'close': r'[^\s]', # everything except whitespace
+ 'punct': r"""[-!" #\$\%'()*+,.\/:;<=>?\@\[\\\]\^_`{|}~]""",
+ 'dash': r'[-–—]',
+ 'sep': '[\\s\u200B\u200C]', # Whitespace, ZWSP, ZWNJ
+ }
+ START_SINGLE = re.compile(r"^'(?=%s\\B)" % _CH_CLASSES['punct'])
+ START_DOUBLE = re.compile(r'^"(?=%s\\B)' % _CH_CLASSES['punct'])
+ ADJACENT_1 = re.compile('"\'(?=\\w)')
+ ADJACENT_2 = re.compile('\'"(?=\\w)')
+ OPEN_SINGLE = re.compile(r"(%(open)s|%(dash)s)'(?=%(punct)s? )"
+ % _CH_CLASSES)
+ OPEN_DOUBLE = re.compile(r'(%(open)s|%(dash)s)"(?=%(punct)s? )'
+ % _CH_CLASSES)
+ DECADE = re.compile(r"'(?=\d{2}s)")
+ APOSTROPHE = re.compile(r"(?<=(\w|\d))'(?=\w)")
+ OPENING_SECONDARY = re.compile("""
+ (# ?<= # look behind fails: requires fixed-width pattern
+ %(sep)s | # a whitespace char, or
+ %(open)s | # opening brace, or
+ %(dash)s # em/en-dash
+ )
+ ' # the quote
+ (?=\\w|%(punct)s) # word character or punctuation
+ """ % _CH_CLASSES, re.VERBOSE)
+ CLOSING_SECONDARY = re.compile(r"(?<!\s)'")
+ OPENING_PRIMARY = re.compile("""
+ (
+ %(sep)s | # a whitespace char, or
+ %(open)s | # zero width separating char, or
+ %(dash)s # em/en-dash
+ )
+ " # the quote, followed by
+ (?=\\w|%(punct)s) # a word character or punctuation
+ """ % _CH_CLASSES, re.VERBOSE)
+ CLOSING_PRIMARY = re.compile(r"""
+ (
+ (?<!\s)" | # no whitespace before
+ "(?=\s) # whitespace behind
+ )
+ """, re.VERBOSE)
+
+
+regexes = RegularExpressions()
+
+
+default_smartypants_attr = '1'
+
+
+def smartyPants(text, attr=default_smartypants_attr, language='en'):
+ """Main function for "traditional" use."""
+
+ return "".join(t for t in educate_tokens(tokenize(text), attr, language))
+
+
+def educate_tokens(text_tokens, attr=default_smartypants_attr, language='en'):
+ """Return iterator that "educates" the items of `text_tokens`."""
+ # Parse attributes:
+ # 0 : do nothing
+ # 1 : set all
+ # 2 : set all, using old school en- and em- dash shortcuts
+ # 3 : set all, using inverted old school en and em- dash shortcuts
+ #
+ # q : quotes
+ # b : backtick quotes (``double'' only)
+ # B : backtick quotes (``double'' and `single')
+ # d : dashes
+ # D : old school dashes
+ # i : inverted old school dashes
+ # e : ellipses
+ # w : convert &quot; entities to " for Dreamweaver users
+
+ convert_quot = False # translate &quot; entities into normal quotes?
+ do_dashes = False
+ do_backticks = False
+ do_quotes = False
+ do_ellipses = False
+ do_stupefy = False
+
+ # if attr == "0": # pass tokens unchanged (see below).
+ if attr == '1': # Do everything, turn all options on.
+ do_quotes = True
+ do_backticks = True
+ do_dashes = 1
+ do_ellipses = True
+ elif attr == '2':
+ # Do everything, turn all options on, use old school dash shorthand.
+ do_quotes = True
+ do_backticks = True
+ do_dashes = 2
+ do_ellipses = True
+ elif attr == '3':
+ # Do everything, use inverted old school dash shorthand.
+ do_quotes = True
+ do_backticks = True
+ do_dashes = 3
+ do_ellipses = True
+ elif attr == '-1': # Special "stupefy" mode.
+ do_stupefy = True
+ else:
+ if 'q' in attr: do_quotes = True # noqa: E701
+ if 'b' in attr: do_backticks = True # noqa: E701
+ if 'B' in attr: do_backticks = 2 # noqa: E701
+ if 'd' in attr: do_dashes = 1 # noqa: E701
+ if 'D' in attr: do_dashes = 2 # noqa: E701
+ if 'i' in attr: do_dashes = 3 # noqa: E701
+ if 'e' in attr: do_ellipses = True # noqa: E701
+ if 'w' in attr: convert_quot = True # noqa: E701
+
+ prev_token_last_char = ' '
+ # Last character of the previous text token. Used as
+ # context to curl leading quote characters correctly.
+
+ for (ttype, text) in text_tokens:
+
+ # skip HTML and/or XML tags as well as empty text tokens
+ # without updating the last character
+ if ttype == 'tag' or not text:
+ yield text
+ continue
+
+ # skip literal text (math, literal, raw, ...)
+ if ttype == 'literal':
+ prev_token_last_char = text[-1:]
+ yield text
+ continue
+
+ last_char = text[-1:] # Remember last char before processing.
+
+ text = processEscapes(text)
+
+ if convert_quot:
+ text = text.replace('&quot;', '"')
+
+ if do_dashes == 1:
+ text = educateDashes(text)
+ elif do_dashes == 2:
+ text = educateDashesOldSchool(text)
+ elif do_dashes == 3:
+ text = educateDashesOldSchoolInverted(text)
+
+ if do_ellipses:
+ text = educateEllipses(text)
+
+ # Note: backticks need to be processed before quotes.
+ if do_backticks:
+ text = educateBackticks(text, language)
+
+ if do_backticks == 2:
+ text = educateSingleBackticks(text, language)
+
+ if do_quotes:
+ # Replace plain quotes in context to prevent conversion to
+ # 2-character sequence in French.
+ context = prev_token_last_char.replace('"', ';').replace("'", ';')
+ text = educateQuotes(context+text, language)[1:]
+
+ if do_stupefy:
+ text = stupefyEntities(text, language)
+
+ # Remember last char as context for the next token
+ prev_token_last_char = last_char
+
+ text = processEscapes(text, restore=True)
+
+ yield text
+
+
+def educateQuotes(text, language='en'):
+ """
+ Parameter: - text string (unicode or bytes).
+ - language (`BCP 47` language tag.)
+ Returns: The `text`, with "educated" curly quote characters.
+
+ Example input: "Isn't this fun?"
+ Example output: “Isn’t this fun?“
+ """
+ smart = smartchars(language)
+
+ if not re.search('[-"\']', text):
+ return text
+
+ # Special case if the very first character is a quote
+ # followed by punctuation at a non-word-break. Use closing quotes.
+ # TODO: example (when does this match?)
+ text = regexes.START_SINGLE.sub(smart.csquote, text)
+ text = regexes.START_DOUBLE.sub(smart.cpquote, text)
+
+ # Special case for adjacent quotes
+ # like "'Quoted' words in a larger quote."
+ text = regexes.ADJACENT_1.sub(smart.opquote+smart.osquote, text)
+ text = regexes.ADJACENT_2.sub(smart.osquote+smart.opquote, text)
+
+ # Special case: "opening character" followed by quote,
+ # optional punctuation and space like "[", '(', or '-'.
+ text = regexes.OPEN_SINGLE.sub(r'\1%s'%smart.csquote, text)
+ text = regexes.OPEN_DOUBLE.sub(r'\1%s'%smart.cpquote, text)
+
+ # Special case for decade abbreviations (the '80s):
+ if language.startswith('en'): # TODO similar cases in other languages?
+ text = regexes.DECADE.sub(smart.apostrophe, text)
+
+ # Get most opening secondary quotes:
+ text = regexes.OPENING_SECONDARY.sub(r'\1'+smart.osquote, text)
+
+ # In many locales, secondary closing quotes are different from apostrophe:
+ if smart.csquote != smart.apostrophe:
+ text = regexes.APOSTROPHE.sub(smart.apostrophe, text)
+ # TODO: keep track of quoting level to recognize apostrophe in, e.g.,
+ # "Ich fass' es nicht."
+
+ text = regexes.CLOSING_SECONDARY.sub(smart.csquote, text)
+
+ # Any remaining secondary quotes should be opening ones:
+ text = text.replace(r"'", smart.osquote)
+
+ # Get most opening primary quotes:
+ text = regexes.OPENING_PRIMARY.sub(r'\1'+smart.opquote, text)
+
+ # primary closing quotes:
+ text = regexes.CLOSING_PRIMARY.sub(smart.cpquote, text)
+
+ # Any remaining quotes should be opening ones.
+ text = text.replace(r'"', smart.opquote)
+
+ return text
+
+
+def educateBackticks(text, language='en'):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with ``backticks'' -style double quotes
+ translated into HTML curly quote entities.
+ Example input: ``Isn't this fun?''
+ Example output: “Isn't this fun?“
+ """
+ smart = smartchars(language)
+
+ text = text.replace(r'``', smart.opquote)
+ text = text.replace(r"''", smart.cpquote)
+ return text
+
+
+def educateSingleBackticks(text, language='en'):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with `backticks' -style single quotes
+ translated into HTML curly quote entities.
+
+ Example input: `Isn't this fun?'
+ Example output: ‘Isn’t this fun?’
+ """
+ smart = smartchars(language)
+
+ text = text.replace(r'`', smart.osquote)
+ text = text.replace(r"'", smart.csquote)
+ return text
+
+
+def educateDashes(text):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with each instance of "--" translated to
+ an em-dash character.
+ """
+
+ text = text.replace(r'---', smartchars.endash) # en (yes, backwards)
+ text = text.replace(r'--', smartchars.emdash) # em (yes, backwards)
+ return text
+
+
+def educateDashesOldSchool(text):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with each instance of "--" translated to
+ an en-dash character, and each "---" translated to
+ an em-dash character.
+ """
+
+ text = text.replace(r'---', smartchars.emdash)
+ text = text.replace(r'--', smartchars.endash)
+ return text
+
+
+def educateDashesOldSchoolInverted(text):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with each instance of "--" translated to
+ an em-dash character, and each "---" translated to
+ an en-dash character. Two reasons why: First, unlike the
+ en- and em-dash syntax supported by
+ EducateDashesOldSchool(), it's compatible with existing
+ entries written before SmartyPants 1.1, back when "--" was
+ only used for em-dashes. Second, em-dashes are more
+ common than en-dashes, and so it sort of makes sense that
+ the shortcut should be shorter to type. (Thanks to Aaron
+ Swartz for the idea.)
+ """
+ text = text.replace(r'---', smartchars.endash) # em
+ text = text.replace(r'--', smartchars.emdash) # en
+ return text
+
+
+def educateEllipses(text):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with each instance of "..." translated to
+ an ellipsis character.
+
+ Example input: Huh...?
+ Example output: Huh…?
+ """
+
+ text = text.replace(r'...', smartchars.ellipsis)
+ text = text.replace(r'. . .', smartchars.ellipsis)
+ return text
+
+
+def stupefyEntities(text, language='en'):
+ """
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with each SmartyPants character translated to
+ its ASCII counterpart.
+
+ Example input: “Hello — world.”
+ Example output: "Hello -- world."
+ """
+ smart = smartchars(language)
+
+ text = text.replace(smart.endash, "-")
+ text = text.replace(smart.emdash, "--")
+ text = text.replace(smart.osquote, "'") # open secondary quote
+ text = text.replace(smart.csquote, "'") # close secondary quote
+ text = text.replace(smart.opquote, '"') # open primary quote
+ text = text.replace(smart.cpquote, '"') # close primary quote
+ text = text.replace(smart.ellipsis, '...')
+
+ return text
+
+
+def processEscapes(text, restore=False):
+ r"""
+ Parameter: String (unicode or bytes).
+ Returns: The `text`, with after processing the following backslash
+ escape sequences. This is useful if you want to force a "dumb"
+ quote or other character to appear.
+
+ Escape Value
+ ------ -----
+ \\ &#92;
+ \" &#34;
+ \' &#39;
+ \. &#46;
+ \- &#45;
+ \` &#96;
+ """
+ replacements = ((r'\\', r'&#92;'),
+ (r'\"', r'&#34;'),
+ (r"\'", r'&#39;'),
+ (r'\.', r'&#46;'),
+ (r'\-', r'&#45;'),
+ (r'\`', r'&#96;'))
+ if restore:
+ for (ch, rep) in replacements:
+ text = text.replace(rep, ch[1])
+ else:
+ for (ch, rep) in replacements:
+ text = text.replace(ch, rep)
+
+ return text
+
+
+def tokenize(text):
+ """
+ Parameter: String containing HTML markup.
+ Returns: An iterator that yields the tokens comprising the input
+ string. Each token is either a tag (possibly with nested,
+ tags contained therein, such as <a href="<MTFoo>">, or a
+ run of text between tags. Each yielded element is a
+ two-element tuple; the first is either 'tag' or 'text';
+ the second is the actual value.
+
+ Based on the _tokenize() subroutine from Brad Choate's MTRegex plugin.
+ """
+ tag_soup = re.compile(r'([^<]*)(<[^>]*>)')
+ token_match = tag_soup.search(text)
+ previous_end = 0
+
+ while token_match is not None:
+ if token_match.group(1):
+ yield 'text', token_match.group(1)
+ yield 'tag', token_match.group(2)
+ previous_end = token_match.end()
+ token_match = tag_soup.search(text, token_match.end())
+
+ if previous_end < len(text):
+ yield 'text', text[previous_end:]
+
+
+if __name__ == "__main__":
+
+ import itertools
+ import locale
+ try:
+ locale.setlocale(locale.LC_ALL, '') # set to user defaults
+ defaultlanguage = locale.getlocale()[0]
+ except: # noqa catchall
+ defaultlanguage = 'en'
+
+ # Normalize and drop unsupported subtags:
+ defaultlanguage = defaultlanguage.lower().replace('-', '_')
+ # split (except singletons, which mark the following tag as non-standard):
+ defaultlanguage = re.sub(r'_([a-zA-Z0-9])_', r'_\1-', defaultlanguage)
+ _subtags = [subtag for subtag in defaultlanguage.split('_')]
+ _basetag = _subtags.pop(0)
+ # find all combinations of subtags
+ for n in range(len(_subtags), 0, -1):
+ for tags in itertools.combinations(_subtags, n):
+ _tag = '-'.join((_basetag, *tags))
+ if _tag in smartchars.quotes:
+ defaultlanguage = _tag
+ break
+ else:
+ if _basetag in smartchars.quotes:
+ defaultlanguage = _basetag
+ else:
+ defaultlanguage = 'en'
+
+ import argparse
+ parser = argparse.ArgumentParser(
+ description='Filter <input> making ASCII punctuation "smart".')
+ # TODO: require input arg or other means to print USAGE instead of waiting.
+ # parser.add_argument("input", help="Input stream, use '-' for stdin.")
+ parser.add_argument("-a", "--action", default="1",
+ help="what to do with the input (see --actionhelp)")
+ parser.add_argument("-e", "--encoding", default="utf-8",
+ help="text encoding")
+ parser.add_argument("-l", "--language", default=defaultlanguage,
+ help="text language (BCP47 tag), "
+ f"Default: {defaultlanguage}")
+ parser.add_argument("-q", "--alternative-quotes", action="store_true",
+ help="use alternative quote style")
+ parser.add_argument("--doc", action="store_true",
+ help="print documentation")
+ parser.add_argument("--actionhelp", action="store_true",
+ help="list available actions")
+ parser.add_argument("--stylehelp", action="store_true",
+ help="list available quote styles")
+ parser.add_argument("--test", action="store_true",
+ help="perform short self-test")
+ args = parser.parse_args()
+
+ if args.doc:
+ print(__doc__)
+ elif args.actionhelp:
+ print(options)
+ elif args.stylehelp:
+ print()
+ print("Available styles (primary open/close, secondary open/close)")
+ print("language tag quotes")
+ print("============ ======")
+ for key in sorted(smartchars.quotes.keys()):
+ print("%-14s %s" % (key, smartchars.quotes[key]))
+ elif args.test:
+ # Unit test output goes to stderr.
+ import unittest
+
+ class TestSmartypantsAllAttributes(unittest.TestCase):
+ # the default attribute is "1", which means "all".
+ def test_dates(self):
+ self.assertEqual(smartyPants("1440-80's"), "1440-80’s")
+ self.assertEqual(smartyPants("1440-'80s"), "1440-’80s")
+ self.assertEqual(smartyPants("1440---'80s"), "1440–’80s")
+ self.assertEqual(smartyPants("1960's"), "1960’s")
+ self.assertEqual(smartyPants("one two '60s"), "one two ’60s")
+ self.assertEqual(smartyPants("'60s"), "’60s")
+
+ def test_educated_quotes(self):
+ self.assertEqual(smartyPants('"Isn\'t this fun?"'),
+ '“Isn’t this fun?”')
+
+ def test_html_tags(self):
+ text = '<a src="foo">more</a>'
+ self.assertEqual(smartyPants(text), text)
+
+ suite = unittest.TestLoader().loadTestsFromTestCase(
+ TestSmartypantsAllAttributes)
+ unittest.TextTestRunner().run(suite)
+
+ else:
+ if args.alternative_quotes:
+ if '-x-altquot' in args.language:
+ args.language = args.language.replace('-x-altquot', '')
+ else:
+ args.language += '-x-altquot'
+ text = sys.stdin.read()
+ print(smartyPants(text, attr=args.action, language=args.language))
diff --git a/.venv/lib/python3.12/site-packages/docutils/utils/urischemes.py b/.venv/lib/python3.12/site-packages/docutils/utils/urischemes.py
new file mode 100644
index 00000000..a0435c02
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docutils/utils/urischemes.py
@@ -0,0 +1,138 @@
+# $Id: urischemes.py 9315 2023-01-15 19:27:55Z milde $
+# Author: David Goodger <goodger@python.org>
+# Copyright: This module has been placed in the public domain.
+
+"""
+`schemes` is a dictionary with lowercase URI addressing schemes as
+keys and descriptions as values. It was compiled from the index at
+http://www.iana.org/assignments/uri-schemes (revised 2005-11-28)
+and an older list at https://www.w3.org/Addressing/schemes.html.
+"""
+
+# Many values are blank and should be filled in with useful descriptions.
+
+schemes = {
+ 'about': 'provides information on Navigator',
+ 'acap': 'Application Configuration Access Protocol; RFC 2244',
+ 'addbook': "To add vCard entries to Communicator's Address Book",
+ 'afp': 'Apple Filing Protocol',
+ 'afs': 'Andrew File System global file names',
+ 'aim': 'AOL Instant Messenger',
+ 'callto': 'for NetMeeting links',
+ 'castanet': 'Castanet Tuner URLs for Netcaster',
+ 'chttp': 'cached HTTP supported by RealPlayer',
+ 'cid': 'content identifier; RFC 2392',
+ 'crid': 'TV-Anytime Content Reference Identifier; RFC 4078',
+ 'data': 'allows inclusion of small data items as "immediate" data; '
+ 'RFC 2397',
+ 'dav': 'Distributed Authoring and Versioning Protocol; RFC 2518',
+ 'dict': 'dictionary service protocol; RFC 2229',
+ 'dns': 'Domain Name System resources',
+ 'eid': 'External ID; non-URL data; general escape mechanism to allow '
+ 'access to information for applications that are too '
+ 'specialized to justify their own schemes',
+ 'fax': 'a connection to a terminal that can handle telefaxes '
+ '(facsimiles); RFC 2806',
+ 'feed': 'NetNewsWire feed',
+ 'file': 'Host-specific file names; RFC 1738',
+ 'finger': '',
+ 'freenet': '',
+ 'ftp': 'File Transfer Protocol; RFC 1738',
+ 'go': 'go; RFC 3368',
+ 'gopher': 'The Gopher Protocol',
+ 'gsm-sms': 'Global System for Mobile Communications Short Message '
+ 'Service',
+ 'h323': 'video (audiovisual) communication on local area networks; '
+ 'RFC 3508',
+ 'h324': 'video and audio communications over low bitrate connections '
+ 'such as POTS modem connections',
+ 'hdl': 'CNRI handle system',
+ 'hnews': 'an HTTP-tunneling variant of the NNTP news protocol',
+ 'http': 'Hypertext Transfer Protocol; RFC 2616',
+ 'https': 'HTTP over SSL; RFC 2818',
+ 'hydra': 'SubEthaEdit URI. '
+ 'See http://www.codingmonkeys.de/subethaedit.',
+ 'iioploc': 'Internet Inter-ORB Protocol Location?',
+ 'ilu': 'Inter-Language Unification',
+ 'im': 'Instant Messaging; RFC 3860',
+ 'imap': 'Internet Message Access Protocol; RFC 2192',
+ 'info': 'Information Assets with Identifiers in Public Namespaces',
+ 'ior': 'CORBA interoperable object reference',
+ 'ipp': 'Internet Printing Protocol; RFC 3510',
+ 'irc': 'Internet Relay Chat',
+ 'iris.beep': 'iris.beep; RFC 3983',
+ 'iseek': 'See www.ambrosiasw.com; a little util for OS X.',
+ 'jar': 'Java archive',
+ 'javascript': 'JavaScript code; '
+ 'evaluates the expression after the colon',
+ 'jdbc': 'JDBC connection URI.',
+ 'ldap': 'Lightweight Directory Access Protocol',
+ 'lifn': '',
+ 'livescript': '',
+ 'lrq': '',
+ 'mailbox': 'Mail folder access',
+ 'mailserver': 'Access to data available from mail servers',
+ 'mailto': 'Electronic mail address; RFC 2368',
+ 'md5': '',
+ 'mid': 'message identifier; RFC 2392',
+ 'mocha': '',
+ 'modem': 'a connection to a terminal that can handle incoming data '
+ 'calls; RFC 2806',
+ 'mtqp': 'Message Tracking Query Protocol; RFC 3887',
+ 'mupdate': 'Mailbox Update (MUPDATE) Protocol; RFC 3656',
+ 'news': 'USENET news; RFC 1738',
+ 'nfs': 'Network File System protocol; RFC 2224',
+ 'nntp': 'USENET news using NNTP access; RFC 1738',
+ 'opaquelocktoken': 'RFC 2518',
+ 'phone': '',
+ 'pop': 'Post Office Protocol; RFC 2384',
+ 'pop3': 'Post Office Protocol v3',
+ 'pres': 'Presence; RFC 3859',
+ 'printer': '',
+ 'prospero': 'Prospero Directory Service; RFC 4157',
+ 'rdar': 'URLs found in Darwin source '
+ '(http://www.opensource.apple.com/darwinsource/).',
+ 'res': '',
+ 'rtsp': 'real time streaming protocol; RFC 2326',
+ 'rvp': '',
+ 'rwhois': '',
+ 'rx': 'Remote Execution',
+ 'sdp': '',
+ 'service': 'service location; RFC 2609',
+ 'shttp': 'secure hypertext transfer protocol',
+ 'sip': 'Session Initiation Protocol; RFC 3261',
+ 'sips': 'secure session intitiaion protocol; RFC 3261',
+ 'smb': 'SAMBA filesystems.',
+ 'snews': 'For NNTP postings via SSL',
+ 'snmp': 'Simple Network Management Protocol; RFC 4088',
+ 'soap.beep': 'RFC 3288',
+ 'soap.beeps': 'RFC 3288',
+ 'ssh': 'Reference to interactive sessions via ssh.',
+ 't120': 'real time data conferencing (audiographics)',
+ 'tag': 'RFC 4151',
+ 'tcp': '',
+ 'tel': 'a connection to a terminal that handles normal voice '
+ 'telephone calls, a voice mailbox or another voice messaging '
+ 'system or a service that can be operated using DTMF tones; '
+ 'RFC 3966.',
+ 'telephone': 'telephone',
+ 'telnet': 'Reference to interactive sessions; RFC 4248',
+ 'tftp': 'Trivial File Transfer Protocol; RFC 3617',
+ 'tip': 'Transaction Internet Protocol; RFC 2371',
+ 'tn3270': 'Interactive 3270 emulation sessions',
+ 'tv': '',
+ 'urn': 'Uniform Resource Name; RFC 2141',
+ 'uuid': '',
+ 'vemmi': 'versatile multimedia interface; RFC 2122',
+ 'videotex': '',
+ 'view-source': 'displays HTML code that was generated with JavaScript',
+ 'wais': 'Wide Area Information Servers; RFC 4156',
+ 'whodp': '',
+ 'whois++': 'Distributed directory service.',
+ 'x-man-page': 'Opens man page in Terminal.app on OS X '
+ '(see macosxhints.com)',
+ 'xmlrpc.beep': 'RFC 3529',
+ 'xmlrpc.beeps': 'RFC 3529',
+ 'z39.50r': 'Z39.50 Retrieval; RFC 2056',
+ 'z39.50s': 'Z39.50 Session; RFC 2056',
+ }
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)