# ==================================================================================================================== #
# _____ _ _ ____ _ _ #
# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| _ __ | |__ (_)_ __ __ __ #
# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | \___ \| '_ \| '_ \| | '_ \\ \/ / #
# | |_) | |_| || | (_) | (_) | | | | | | (_| |_ ___) | |_) | | | | | | | |> < #
# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/| .__/|_| |_|_|_| |_/_/\_\ #
# |_| |___/ |___/ |_| #
# ==================================================================================================================== #
# Authors: #
# Patrick Lehmann #
# #
# License: #
# ==================================================================================================================== #
# Copyright 2026-2026 Patrick Lehmann - Bötzingen, Germany #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); #
# you may not use this file except in compliance with the License. #
# You may obtain a copy of the License at #
# #
# http://www.apache.org/licenses/LICENSE-2.0 #
# #
# Unless required by applicable law or agreed to in writing, software #
# distributed under the License is distributed on an "AS IS" BASIS, #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and #
# limitations under the License. #
# #
# SPDX-License-Identifier: Apache-2.0 #
# ==================================================================================================================== #
#
"""
Visitors writing pyTooling.Sphinx's nodes in LaTeX.
Each node has a ``visit_*`` and a ``depart_*`` function and a ``translate*`` pair of both, which
:data:`~pyTooling.Sphinx.NODES` registers. A node without visitors here is written by the visitors of its base-class.
"""
from textwrap import dedent
from docutils import nodes
from pygments.formatters.latex import escape_tex
from sphinx.writers.latex import LaTeXTranslator
from pyTooling.Decorators import export
from pyTooling.Sphinx.Node import COVERAGE_STATES, DOCUMENTATION_STATES, CoverageListing, DocCoverageListing, \
Landscape, LineBreak, ReportIndentation, TaskListCheckbox, visitFunc, \
departFunc
__all__ = [
"translateLandscape", "translateCoverageListing", "translateDocCoverageListing",
"translateTaskListCheckbox", "translateLineBreak", "COVERAGE_MARKERS", "DOCUMENTATION_MARKERS", "TAB_WIDTH"
]
#: Per coverage state of a line, the color of its number - an ``xcolor`` expression - and the marker behind it.
COVERAGE_MARKERS = {
"covered": ("green!50!black", "+"),
"partial": ("orange", "~"),
"uncovered": ("red", "-"),
"excluded": ("gray", "x"),
"": ("gray", " "),
}
#: Per documentation state of a line, the color of its number - an ``xcolor`` expression - and the marker behind it.
DOCUMENTATION_MARKERS = {
"documented": ("green!50!black", "+"),
"undocumented": ("red", "-"),
"ignored": ("gray", "o"),
"excluded": ("gray", "x"),
"": ("gray", " "),
}
TAB_WIDTH = 4 #: Number of columns a tab advances to, as LaTeX doesn't expand tabs in a listing.
[docs]
@export
def visit_Landscape(translator: LaTeXTranslator, node: Landscape) -> None:
"""
Open a landscape container in LaTeX: a ``landscape`` environment, which starts a new page in landscape orientation.
:param translator: The LaTeX translator writing the document.
:param node: The landscape container.
"""
translator.body.append(dedent("""
\\begin{landscape}
""")
)
[docs]
@export
def depart_Landscape(translator: LaTeXTranslator, node: Landscape) -> None:
"""
Close a landscape container in LaTeX: the end of the ``landscape`` environment, which returns to portrait pages.
:param translator: The LaTeX translator writing the document.
:param node: The landscape container.
"""
translator.body.append(dedent("""
\\end{landscape}
""")
)
translateLandscape: tuple[visitFunc, departFunc] = (visit_Landscape, depart_Landscape)
"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.Landscape` in LaTeX."""
[docs]
@export
def highlightTokens(tokens: list[tuple[str, str]]) -> str:
"""
Write a listing line's tokens in LaTeX, as Sphinx writes highlighted code - ``\\PYG{<class>}{<text>}`` -, so the
configured Pygments style colors them.
Tabs are expanded to :data:`TAB_WIDTH` columns, as LaTeX doesn't expand them in a listing.
:param tokens: The line's tokens as ``(CSS class, text)`` pairs; a token without a class is written as text.
:returns: The line's LaTeX.
"""
code: list[str] = []
column = 0
for cssClass, text in tokens:
if "\t" in text:
padding = column % TAB_WIDTH
text = (" " * padding + text).expandtabs(TAB_WIDTH)[padding:]
column += len(text)
escaped = escape_tex(text, "PYG")
code.append(escaped if cssClass == "" else f"\\PYG{{{cssClass}}}{{{escaped}}}")
return "".join(code)
[docs]
@export
def writeListing(
translator: LaTeXTranslator,
node: nodes.Element,
lines: list[str],
markers: dict[str, tuple[str, str]],
states: dict[str, str]
) -> None:
"""
Write a listing in LaTeX: a line with the file's path and a legend of the markers, then a ``sphinxVerbatim``
environment of the lines.
:param translator: The LaTeX translator writing the document.
:param node: The listing, whose attribute ``path`` holds the file's path.
:param lines: The lines, each written as LaTeX.
:param markers: The color and marker per state.
:param states: The label per state, which the legend shows; the state ``""`` has no entry in the legend.
"""
legend = "\\quad{}".join(
f"\\textcolor{{{color}}}{{\\texttt{{{escape_tex(marker, 'PYG')}}}}}~{states[state]}"
for state, (color, marker) in markers.items() if state != ""
)
translator.body.append(
f"\n\\par\\noindent\\sphinxcode{{{translator.encode(node['path'])}}}\\hfill{{}}{{\\small{{}}{legend}}}\\par\n"
)
translator.body.append("\n\\begin{sphinxVerbatim}[commandchars=\\\\\\{\\}]\n")
translator.body.append("\n".join(lines))
translator.body.append("\n\\end{sphinxVerbatim}\n")
[docs]
@export
def visit_CoverageListing(translator: LaTeXTranslator, node: CoverageListing) -> None:
"""
Write a source file's code coverage in LaTeX, completely.
The listing is a ``sphinxVerbatim`` environment with the tokens written as Sphinx writes highlighted code (see
:func:`highlightTokens`). Each line starts with its number, colored by its coverage state, and a marker of
:data:`COVERAGE_MARKERS`. A line above the listing shows the file's path and a legend of the markers. The node's
text - the plain source - isn't written again.
:param translator: The LaTeX translator writing the document.
:param node: The listing.
:raises SkipNode: Always, so the node's text isn't written again.
"""
width = len(str(max((number for number, *_ in node["lines"]), default=1)))
lines: list[str] = []
for number, status, _, _, _, tokens in node["lines"]:
color, marker = COVERAGE_MARKERS[status]
lines.append(f"\\textcolor{{{color}}}{{{number:>{width}} {marker}}} {highlightTokens(tokens)}")
writeListing(translator, node, lines, COVERAGE_MARKERS, COVERAGE_STATES)
raise nodes.SkipNode
[docs]
@export
def depart_CoverageListing(translator: LaTeXTranslator, node: CoverageListing) -> None:
"""
Close a source file's code coverage in LaTeX, which writes nothing: :func:`visit_CoverageListing` wrote it all.
:param translator: The LaTeX translator writing the document.
:param node: The listing.
"""
translateCoverageListing: tuple[visitFunc, departFunc] = (visit_CoverageListing, depart_CoverageListing)
"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.CoverageListing` in LaTeX."""
[docs]
@export
def visit_DocCoverageListing(translator: LaTeXTranslator, node: DocCoverageListing) -> None:
"""
Write a Python module's documentation coverage in LaTeX, completely.
Like a source file's code coverage (see :func:`visit_CoverageListing`): each line starts with its number, colored
by its documentation state, and a marker of :data:`DOCUMENTATION_MARKERS`; an elided body's ``...`` has no number.
The node's text - the condensed source - isn't written again.
:param translator: The LaTeX translator writing the document.
:param node: The listing.
:raises SkipNode: Always, so the node's text isn't written again.
"""
width = len(str(max((number for number, *_ in node["lines"] if number is not None), default=1)))
lines: list[str] = []
for number, status, tokens in node["lines"]:
color, marker = DOCUMENTATION_MARKERS[status]
numberText = "" if number is None else f"{number}"
lines.append(f"\\textcolor{{{color}}}{{{numberText:>{width}} {marker}}} {highlightTokens(tokens)}")
writeListing(translator, node, lines, DOCUMENTATION_MARKERS, DOCUMENTATION_STATES)
raise nodes.SkipNode
[docs]
@export
def depart_DocCoverageListing(translator: LaTeXTranslator, node: DocCoverageListing) -> None:
"""
Close a Python module's documentation coverage in LaTeX, which writes nothing: :func:`visit_DocCoverageListing`
wrote it all.
:param translator: The LaTeX translator writing the document.
:param node: The listing.
"""
translateDocCoverageListing: tuple[visitFunc, departFunc] = (visit_DocCoverageListing, depart_DocCoverageListing)
"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.DocCoverageListing` in LaTeX."""
[docs]
@export
def visit_TaskListCheckbox(translator: LaTeXTranslator, node: TaskListCheckbox) -> None:
"""
Write a task list item's checkbox in LaTeX, completely: ``\\boxtimes`` if the task is done, else ``\\square`` - both
of package ``amssymb``, which Sphinx loads.
:param translator: The LaTeX translator writing the document.
:param node: The checkbox.
:raises SkipNode: Always, so the node's text isn't written.
"""
translator.body.append("\\(\\boxtimes\\)" if node["checked"] else "\\(\\square\\)")
raise nodes.SkipNode
[docs]
@export
def depart_TaskListCheckbox(translator: LaTeXTranslator, node: TaskListCheckbox) -> None:
"""
Close a task list item's checkbox in LaTeX, which writes nothing: :func:`visit_TaskListCheckbox` wrote it all.
:param translator: The LaTeX translator writing the document.
:param node: The checkbox.
"""
translateTaskListCheckbox: tuple[visitFunc, departFunc] = (visit_TaskListCheckbox, depart_TaskListCheckbox)
"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.TaskListCheckbox` in LaTeX."""
[docs]
@export
def visit_LineBreak(translator: LaTeXTranslator, node: LineBreak) -> None:
"""
Write a line break in LaTeX, completely: ``\\newline``, which breaks the line in any environment.
:param translator: The LaTeX translator writing the document.
:param node: The line break.
:raises SkipNode: Always, so the node's text isn't written.
"""
translator.body.append("\\newline{}")
raise nodes.SkipNode
[docs]
@export
def depart_LineBreak(translator: LaTeXTranslator, node: LineBreak) -> None:
"""
Close a line break in LaTeX, which writes nothing: :func:`visit_LineBreak` wrote it all.
:param translator: The LaTeX translator writing the document.
:param node: The line break.
"""
translateLineBreak: tuple[visitFunc, departFunc] = (visit_LineBreak, depart_LineBreak)
"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.LineBreak` in LaTeX."""
[docs]
@export
def visit_ReportIndentation(translator: LaTeXTranslator, node: ReportIndentation) -> None:
"""
Open an indented name of a report table in LaTeX: macro ``\\pyToolingReportIndentation`` of package
:file:`pyTooling.Sphinx.sty` with the number of indentation steps.
:param translator: The LaTeX translator writing the document.
:param node: The indented name.
"""
translator.body.append(f"\\pyToolingReportIndentation{{{node['level']}}}")
[docs]
@export
def depart_ReportIndentation(translator: LaTeXTranslator, node: ReportIndentation) -> None:
"""
Close an indented name of a report table in LaTeX, which writes nothing: the macro indents the rest of the cell.
:param translator: The LaTeX translator writing the document.
:param node: The indented name.
"""
translateReportIndentation: tuple[visitFunc, departFunc] = (visit_ReportIndentation, depart_ReportIndentation)
"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.ReportIndentation` in LaTeX."""