# $Id: __init__.py 10266 2025-11-28 13:52:09Z 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."""

from __future__ import annotations

__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. ##.

import re
import string
import warnings
from pathlib import Path

from docutils import frontend, nodes, languages, writers, utils
from docutils.transforms import references, writer_aux
from docutils.utils._roman_numerals import RomanNumeral
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'],
          {'metavar': '', 'default': 'article'}),
         ('Specify document options.  Multiple options can be given, '
          'separated by commas.  Default: "a4paper".',
          ['--documentoptions'],
          {'metavar': '', 'default': 'a4paper'}),
         ('Format for footnote references: one of "superscript" or '
          '"brackets".  Default: "superscript".',
          ['--footnote-references'],
          {'choices': ['superscript', 'brackets'], 'default': 'superscript',
           'metavar': '',
           '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': ''}),
         ('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': '',
           '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': '', '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': '',
           'validator': frontend.validate_comma_separated_list,
           'default': ['.']}),
         ('Customization by LaTeX code in the preamble. '
          'Default: select PDF standard fonts (Times, Helvetica, Courier).',
          ['--latex-preamble'],
          {'metavar': '', 'default': default_preamble}),
         ('Specify the template file. Default: "%s".' % default_template,
          ['--template'],
          {'default': default_template, 'metavar': ''}),
         ('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'],
          {'metavar': '', 'default': 'blue'}),
         ('Additional options to the "hyperref" package.',
          ['--hyperref-options'],
          {'metavar': '', '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': ''}),
         ('When possible, use the specified environment for literal-blocks. '
          'Default: "" (fall back to "alltt").',
          ['--literal-block-env'],
          {'metavar': '', 'default': ''}),
         (frontend.SUPPRESS_HELP,  # deprecated legacy option
          ['--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': '',
           'action': 'append',
           'validator': frontend.validate_comma_separated_list,
           'choices': table_style_values}),
         ('LaTeX graphicx package option. Default: "".',
          ['--graphicx-option'],
          {'metavar': '
# in all three cases the may also be nested in a # TODO: "classes" attribute currently ignored! self.requirements['graphicx'] = self.graphicx_package attrs = node.attributes # convert image URI to filesystem path, do not adjust relative path: imagepath = self.uri2path(attrs['uri'], output_path='') # alignment defaults: if 'align' not in attrs: # Set default align of image in a figure to 'center' if (isinstance(node.parent, nodes.figure) or isinstance(node.parent, nodes.reference) and isinstance(node.parent.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 = [] if isinstance(node.parent, nodes.reference): pre.append(self.out.pop()) # move \href behind alignment code 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( f"height={self.to_latex_length(attrs['height'], node)}") if 'scale' in attrs: include_graphics_options.append(f"scale={attrs['scale']/100:g}") if 'width' in attrs: include_graphics_options.append( f"width={self.to_latex_length(attrs['width'], node)}") pre.append(''.join(self.ids_to_labels(node, newline=True))) if not (self.is_inline(node) or isinstance(node.parent, nodes.compound)): pre.append('\n') if not self.is_inline(node): post.append('\n') pre.reverse() # now insert image code self.out.extend(pre) if imagepath.suffix == '.svg' and 'svg' in self.settings.stylesheet: cmd = 'includesvg' else: cmd = 'includegraphics' options = '' if include_graphics_options: options = f"[{','.join(include_graphics_options)}]" self.out.append(f'\\{cmd}{options}{{{imagepath.as_posix()}}}') self.out.extend(post) def depart_image(self, node) -> None: pass def visit_inline(self, node) -> None: # This function is also called by the visiting functions for # specific inline elements, , and . # Handle "ids" attribute: # do we need a \phantomsection? anchor_nodes = (nodes.caption, nodes.subtitle, nodes.title) set_anchor = not (isinstance(node.parent, anchor_nodes) or isinstance(node, anchor_nodes)) add_newline = isinstance(node, nodes.paragraph) self.out += self.ids_to_labels(node, set_anchor, newline=add_newline) # Handle "classes" attribute: 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: self.provide_fallback('inline') self.out.append(r'\DUrole{%s}{' % cls) def depart_inline(self, node) -> None: self.out.append('}' * len(node['classes'])) def visit_legend(self, node) -> None: self.provide_fallback('legend') self.out.append('\\begin{DUlegend}') def depart_legend(self, node) -> None: self.out.append('\\end{DUlegend}\n') def visit_line(self, node) -> None: self.out.append(r'\item[] ') def depart_line(self, node) -> None: self.out.append('\n') def visit_line_block(self, node) -> None: self.provide_fallback('providelength', '_providelength') self.provide_fallback('lineblock') self.set_align_from_classes(node) if isinstance(node.parent, nodes.line_block): self.out.append('\\item[]\n' '\\begin{DUlineblock}{\\DUlineblockindent}\n') # In rST, nested line-blocks cannot be given class arguments else: self.out += self.ids_to_labels(node, pre_nl=True) self.duclass_open(node) self.out.append('\\begin{DUlineblock}{0em}\n') self.insert_align_declaration(node) def depart_line_block(self, node) -> None: self.out.append('\\end{DUlineblock}\n') self.duclass_close(node) def visit_list_item(self, node) -> None: self.out += self.ids_to_labels(node, pre_nl=True) self.out.append('\n\\item ') def depart_list_item(self, node) -> None: pass def visit_literal(self, node) -> None: self.literal = True if ('code' in node['classes'] and self.settings.syntax_highlight != 'none'): self.requirements['color'] = PreambleCmds.color self.provide_fallback('highlight_rules', 'code') self.out.append('\\texttt{') self.visit_inline(node) def depart_literal(self, node) -> None: self.literal = False self.depart_inline(node) self.out.append('}') # elements are used # for literal blocks (where content is not parsed), # for the "code" directive (where content may be parsed by Pygments), and # for the "parsed-literal" directive (where rST inline markup is parsed). # # In all cases, we want to use a monospaced typeface. # If the contains only text, we can use a "verbatim-like" # environment, for mixed content we must use \ttfamily and \raggedright. # # We can distinguish between the use cases looking at the element's # children: if there is a single child, it is either # * a "real" literal block or # * a parsed-literal that does not contain any markup. def is_plaintext(self, node): """Check whether `node` contains only text""" return (len(node) == 1) and isinstance(node[0], nodes.Text) def visit_literal_block(self, node) -> None: """Render a literal block. Corresponding rST elements: literal block, parsed-literal, code. """ packages = {'lstlisting': '\\usepackage{listings}\n' '\\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.out += self.ids_to_labels(node, pre_nl=True) self.duclass_open(node) # Highlight code? if (not _plaintext and 'code' in node['classes'] and self.settings.syntax_highlight != 'none'): self.requirements['color'] = PreambleCmds.color self.provide_fallback('highlight_rules', 'code') # 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) -> None: 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) -> None: if 'name' not in node or 'content' not in node: raise nodes.SkipNode # HTML specific or empty metadata # TODO: Filter nodes with additional fields ("lang", "http-equiv", …)? # They are HTML-specific and may override equally named keys. # Only if name already present? See also ODT writer. name = node['name'] content = self.encode(node['content']) if name in ('author', 'keywords', 'producer', 'subject', 'title'): # fields with dedicated hyperref options: self.pdfinfo.append(' pdf%s={%s},'%(name, content)) elif name == 'creator': self.pdfinfo.append(' addtopdfcreator={%s},'%content) else: # generic interface (case sensitive!) self.pdfinfo.append(' pdfinfo={%s={%s}},'%(name, content)) def depart_meta(self, node) -> None: pass def visit_math(self, node, math_env='$') -> None: """ Provisional: the `math_env` argument will be dropped in Docutils 2.0. """ self.requirements['amsmath'] = r'\usepackage{amsmath}' self.visit_inline(node) math_code = node.astext().translate(unichar2tex.uni2tex_table) if self.alltt: self.out += ['\\(', math_code, '\\)'] else: self.out += ['$', math_code, '$'] self.depart_inline(node) raise nodes.SkipNode # content already processed def depart_math(self, node) -> None: pass # never reached def visit_math_block(self, node) -> None: self.requirements['amsmath'] = r'\usepackage{amsmath}' math_env = pick_math_environment(node.astext()) self.out.append('%\n') if node['ids'] and math_env.endswith('*'): # non-numbered equation self.out.append('\\phantomsection\n') for cls in node['classes']: self.provide_fallback('inline') self.out.append(f'\\DUrole{{{cls}}}{{%\n') self.out += [f'\\begin{{{math_env}}}\n', node.astext().translate(unichar2tex.uni2tex_table), '\n', *self.ids_to_labels(node, set_anchor=False, newline=True), f'\\end{{{math_env}}}'] if node['classes']: self.out.append('\n' + '}' * len(node['classes'])) raise nodes.SkipNode # content already processed def depart_math_block(self, node) -> None: pass # never reached def visit_option(self, node) -> None: if self.context[-1]: # this is not the first option self.out.append(', ') def depart_option(self, node) -> None: # flag that the first option is done. self.context[-1] += 1 def visit_option_argument(self, node) -> None: """Append the delimiter between an option and its argument to body.""" self.out.append(node.get('delimiter', ' ')) def depart_option_argument(self, node) -> None: pass def visit_option_group(self, node) -> None: self.out.append('\\item[') # flag for first option self.context.append(0) def depart_option_group(self, node) -> None: self.context.pop() # the flag self.out.append('] ') def visit_option_list(self, node) -> None: self.provide_fallback('providelength', '_providelength') self.provide_fallback('optionlist') self.out += self.ids_to_labels(node, pre_nl=True) self.duclass_open(node) self.out.append('\\begin{DUoptionlist}\n') def depart_option_list(self, node) -> None: self.out.append('\\end{DUoptionlist}\n') self.duclass_close(node) def visit_option_list_item(self, node) -> None: self.out += self.ids_to_labels(node, newline=True) def depart_option_list_item(self, node) -> None: pass def visit_option_string(self, node) -> None: ## self.out.append(self.starttag(node, 'span', '', CLASS='option')) pass def depart_option_string(self, node) -> None: ## self.out.append('') pass def visit_organization(self, node) -> None: self.visit_docinfo_item(node) def depart_organization(self, node) -> None: self.depart_docinfo_item(node) def visit_paragraph(self, node) -> None: # 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.visit_inline(node) def depart_paragraph(self, node) -> None: self.depart_inline(node) if not self.active_table.colwidths_auto: self.out.append('\n') def visit_problematic(self, node) -> None: 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) -> None: 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) -> None: 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('\\'): '\\\\', } if not self.is_inline(node): self.out.append('\n') # external reference (URL) if 'refuri' in node: href = str(node['refuri']).translate(special_chars) # problematic chars double caret and unbalanced braces: if '^^' in href 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 self.reference_label: self.out.append('\\%s{%s}' % (self.reference_label, href.replace('#', ''))) raise nodes.SkipNode self.out.append('\\hyperref[%s]{' % href) def depart_reference(self, node) -> None: self.out.append('}') if not self.is_inline(node): self.out.append('\n') def visit_revision(self, node) -> None: self.visit_docinfo_item(node) def depart_revision(self, node) -> None: self.depart_docinfo_item(node) def visit_rubric(self, node) -> None: self.provide_fallback('rubric') # class wrapper would interfere with ``\section*"`` type commands # (spacing/indent of first paragraph) self.out += self.ids_to_labels(node, pre_nl=True) self.duclass_open(node) self.out.append('\\DUrubric{') def depart_rubric(self, node) -> None: self.out.append('}\n') self.duclass_close(node) def visit_section(self, node) -> None: # Update counter-prefix for compound enumerators 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 # Section numbering configuration if 'sectnum' in self.requirements: return # already done # sectnum_depth values: # 0 no section numbering or section numbering by Docutils # >0 value of "sectnum"'s :depth: option (1 = top level section) # None "sectnum" directive without depth arg -> keep default if self.settings.sectnum_xform: # section numbering by Docutils sectnum_depth = 0 # suppress LaTeX section numbers else: sectnum_depth = getattr(self.settings, 'sectnum_depth', 0) if isinstance(sectnum_depth, str): sectnum_depth = 0 # ignore values from config files sectnum_start = getattr(self.settings, 'sectnum_start', 1) if isinstance(sectnum_start, str): sectnum_start = 1 # ignore values from config files if sectnum_start != 1: self.requirements['sectnum_start'] = r'\setcounter{%s}{%d}' % ( self.d_class.sections[0], sectnum_start-1) # TODO: currently ignored (configure in a stylesheet): # settings.sectnum_prefix # settings.sectnum_suffix if sectnum_depth is not None: self.requirements['sectnum'] = r'\setcounter{secnumdepth}{%d}' % ( self.d_class.latex_section_depth(sectnum_depth)) def depart_section(self, node) -> None: # Remove counter for potential subsections: self._section_number.pop() self.section_level -= 1 def visit_sidebar(self, node) -> None: self.out += self.ids_to_labels(node, pre_nl=True) self.duclass_open(node) self.requirements['color'] = PreambleCmds.color self.provide_fallback('sidebar') self.out.append('\\DUsidebar{') def depart_sidebar(self, node) -> None: self.out.append('}\n') self.duclass_close(node) attribution_formats = {'dash': ('—', ''), # EM DASH 'parentheses': ('(', ')'), 'parens': ('(', ')'), 'none': ('', '')} def visit_attribution(self, node) -> None: prefix, suffix = self.attribution_formats[self.settings.attribution] self.out.append('\\nopagebreak\n') self.out += self.ids_to_labels(node, pre_nl=True) self.duclass_open(node) self.out.append(f'\\raggedleft {prefix}') self.context.append(suffix) def depart_attribution(self, node) -> None: self.out.append(self.context.pop() + '\n') self.duclass_close(node) def visit_status(self, node) -> None: self.visit_docinfo_item(node) def depart_status(self, node) -> None: self.depart_docinfo_item(node) def visit_strong(self, node) -> None: self.out.append('\\textbf{') self.visit_inline(node) def depart_strong(self, node) -> None: self.depart_inline(node) self.out.append('}') def visit_substitution_definition(self, node): raise nodes.SkipNode def visit_substitution_reference(self, node) -> None: self.unimplemented_visit(node) def visit_subtitle(self, node) -> None: if isinstance(node.parent, nodes.document): self.push_output_collector(self.subtitle) self.provide_fallback('documentsubtitle') 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: self.provide_fallback('subtitle') self.out.append('\n\\DUsubtitle{') def depart_subtitle(self, node) -> None: if isinstance(node.parent, nodes.document): self.pop_output_collector() else: self.out.append('}\n') def visit_system_message(self, node) -> None: self.requirements['color'] = PreambleCmds.color self.provide_fallback('title') if self.settings.legacy_class_functions: self.fallbacks['title'] = PreambleCmds.title_legacy node['classes'] = ['system-message'] self.out.append('\n') self.append_hypertargets(node) self.visit_admonition(node) if self.settings.legacy_class_functions: self.out.append('\n\\DUtitle[system-message]' '{\\color{red}System Message}\n') else: self.out.append('\n\\DUtitle{\\color{red}System Message}\n') try: line = f", line~{node['line']}" except KeyError: line = '' self.out.append(f"\n{node['type']}/{node['level']} in " f"\\texttt{{{self.encode(node['source'])}}}{line}\n") if len(node['backrefs']) == 1: self.out.append(f"\n\\hyperlink{{{node['backrefs'][0]}}}{{") self.context.append('}\n') else: backrefs = [f'\\hyperlink{{{href}}}{{{i+1}}}\n' for (i, href) in enumerate(node['backrefs'])] self.context.append('backrefs: ' + ' '.join(backrefs)) def depart_system_message(self, node) -> None: self.out.append(self.context.pop()) self.depart_admonition(node) def visit_table(self, node) -> None: 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 'paragraph' in self.d_class.section(self.section_level)): 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) -> None: # complete and write the table content = self.pop_output_collector() try: width = self.to_latex_length(node['width'], node) 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: set_anchor = (len(self.table_stack) != 1) self.out += self.ids_to_labels(node, set_anchor, pre_nl=True) self.duclass_open(node) # 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) -> None: # 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 if self.is_inline(node): self.out.append('%') self.out.append('\n') self.visit_inline(node) def depart_target(self, node) -> None: self.depart_inline(node) def visit_tbody(self, node) -> None: # 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) -> None: pass def visit_term(self, node) -> None: """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) -> None: 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) -> None: pass def depart_tgroup(self, node) -> None: pass _thead_depth = 0 def thead_depth(self): return self._thead_depth def visit_thead(self, node) -> None: 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) -> None: 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) -> None: """Append section and other titles.""" # Document title if isinstance(node.parent, nodes.document): self.push_output_collector(self.title) self.context.append('') # 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: self.provide_fallback('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: 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]' % RomanNumeral(level) else: self.provide_fallback('title') self.provide_fallback('duclass', 'DUclass') self.out.append('\\begin{DUclass}{section%s}\n' % RomanNumeral(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) -> None: 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 len(node['names']) > 1: # don't add labels just for the auto-id 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 try: details = node.next_node(nodes.pending).details except AttributeError: self.warn('Setting "use_latex_toc" is True but "contents" details ' 'are missing. Directive option values may be lost.') details = {} depth = details.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) -> None: # 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']: self.provide_fallback('abstract') if self.settings.legacy_class_functions: self.fallbacks['abstract'] = PreambleCmds.abstract_legacy self.push_output_collector(self.abstract) elif 'dedication' in node['classes']: self.provide_fallback('dedication') self.push_output_collector(self.dedication) else: node['classes'].insert(0, 'topic') self.visit_block_quote(node) def depart_topic(self, node) -> None: 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) -> None: self.provide_fallback('transition') self.out.append('\n%' + '_' * 75 + '\n') self.out.append('\\DUtransition\n') def depart_transition(self, node) -> None: pass def visit_version(self, node) -> None: self.visit_docinfo_item(node) def depart_version(self, node) -> None: 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 :