LaTeX.py
Summary
- Lines:
88 of 88 covered (100.0%)
- Branches:
8 of 8 taken (100.0%), 0 lines partially
- Coverage:
100.0%
- Directory:
Source
pyTooling/Sphinx/LaTeX.pycoveredpartialuncoveredexcludednot executable
1# ==================================================================================================================== #
2# _____ _ _ ____ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| _ __ | |__ (_)_ __ __ __ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | \___ \| '_ \| '_ \| | '_ \\ \/ / #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_ ___) | |_) | | | | | | | |> < #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/| .__/|_| |_|_|_| |_/_/\_\ #
7# |_| |___/ |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2026-2026 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32Visitors writing pyTooling.Sphinx's nodes in LaTeX.
33
34Each node has a ``visit_*`` and a ``depart_*`` function and a ``translate*`` pair of both, which
35:data:`~pyTooling.Sphinx.NODES` registers. A node without visitors here is written by the visitors of its base-class.
36"""
37from textwrap import dedent
38
39from docutils import nodes
40from pygments.formatters.latex import escape_tex
41from sphinx.writers.latex import LaTeXTranslator
42
43from pyTooling.Decorators import export
44from pyTooling.Sphinx.Node import COVERAGE_STATES, DOCUMENTATION_STATES, CoverageListing, DocCoverageListing
45from pyTooling.Sphinx.Node import Landscape, LineBreak, ReportIndentation, TaskListCheckbox, visitFunc, departFunc
46
47
48__all__ = [
49 "translateLandscape", "translateCoverageListing", "translateDocCoverageListing",
50 "translateTaskListCheckbox", "translateLineBreak", "COVERAGE_MARKERS", "DOCUMENTATION_MARKERS", "TAB_WIDTH"
51]
52
53#: Per coverage state of a line, the color of its number - an ``xcolor`` expression - and the marker behind it.
54COVERAGE_MARKERS = {
55 "covered": ("green!50!black", "+"),
56 "partial": ("orange", "~"),
57 "uncovered": ("red", "-"),
58 "excluded": ("gray", "x"),
59 "": ("gray", " "),
60}
61
62#: Per documentation state of a line, the color of its number - an ``xcolor`` expression - and the marker behind it.
63DOCUMENTATION_MARKERS = {
64 "documented": ("green!50!black", "+"),
65 "undocumented": ("red", "-"),
66 "ignored": ("gray", "o"),
67 "excluded": ("gray", "x"),
68 "": ("gray", " "),
69}
70
71TAB_WIDTH = 4 #: Number of columns a tab advances to, as LaTeX doesn't expand tabs in a listing.
72
73
74@export
75def visit_Landscape(translator: LaTeXTranslator, node: Landscape) -> None:
76 """
77 Open a landscape container in LaTeX: a ``landscape`` environment, which starts a new page in landscape orientation.
78
79 :param translator: The LaTeX translator writing the document.
80 :param node: The landscape container.
81 """
82 translator.body.append(dedent("""
83 \\begin{landscape}
84 """)
85 )
86
87
88@export
89def depart_Landscape(translator: LaTeXTranslator, node: Landscape) -> None:
90 """
91 Close a landscape container in LaTeX: the end of the ``landscape`` environment, which returns to portrait pages.
92
93 :param translator: The LaTeX translator writing the document.
94 :param node: The landscape container.
95 """
96 translator.body.append(dedent("""
97 \\end{landscape}
98 """)
99 )
100
101
102translateLandscape: tuple[visitFunc, departFunc] = (visit_Landscape, depart_Landscape)
103"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.Landscape` in LaTeX."""
104
105
106@export
107def highlightTokens(tokens: list[tuple[str, str]]) -> str:
108 """
109 Write a listing line's tokens in LaTeX, as Sphinx writes highlighted code - ``\\PYG{<class>}{<text>}`` -, so the
110 configured Pygments style colors them.
111
112 Tabs are expanded to :data:`TAB_WIDTH` columns, as LaTeX doesn't expand them in a listing.
113
114 :param tokens: The line's tokens as ``(CSS class, text)`` pairs; a token without a class is written as text.
115 :returns: The line's LaTeX.
116 """
117 code: list[str] = []
118 column = 0
119 for cssClass, text in tokens:
120 if "\t" in text:
121 padding = column % TAB_WIDTH
122 text = (" " * padding + text).expandtabs(TAB_WIDTH)[padding:]
123 column += len(text)
124
125 escaped = escape_tex(text, "PYG")
126 code.append(escaped if cssClass == "" else f"\\PYG{{{cssClass}}}{{{escaped}}}")
127
128 return "".join(code)
129
130
131@export
132def writeListing(
133 translator: LaTeXTranslator,
134 node: nodes.Element,
135 lines: list[str],
136 markers: dict[str, tuple[str, str]],
137 states: dict[str, str]
138) -> None:
139 """
140 Write a listing in LaTeX: a line with the file's path and a legend of the markers, then a ``sphinxVerbatim``
141 environment of the lines.
142
143 :param translator: The LaTeX translator writing the document.
144 :param node: The listing, whose attribute ``path`` holds the file's path.
145 :param lines: The lines, each written as LaTeX.
146 :param markers: The color and marker per state.
147 :param states: The label per state, which the legend shows; the state ``""`` has no entry in the legend.
148 """
149 legend = "\\quad{}".join(
150 f"\\textcolor{{{color}}}{{\\texttt{{{escape_tex(marker, 'PYG')}}}}}~{states[state]}"
151 for state, (color, marker) in markers.items() if state != ""
152 )
153 translator.body.append(
154 f"\n\\par\\noindent\\sphinxcode{{{translator.encode(node['path'])}}}\\hfill{{}}{{\\small{{}}{legend}}}\\par\n"
155 )
156 translator.body.append("\n\\begin{sphinxVerbatim}[commandchars=\\\\\\{\\}]\n")
157 translator.body.append("\n".join(lines))
158 translator.body.append("\n\\end{sphinxVerbatim}\n")
159
160
161@export
162def visit_CoverageListing(translator: LaTeXTranslator, node: CoverageListing) -> None:
163 """
164 Write a source file's code coverage in LaTeX, completely.
165
166 The listing is a ``sphinxVerbatim`` environment with the tokens written as Sphinx writes highlighted code (see
167 :func:`highlightTokens`). Each line starts with its number, colored by its coverage state, and a marker of
168 :data:`COVERAGE_MARKERS`. A line above the listing shows the file's path and a legend of the markers. The node's
169 text - the plain source - isn't written again.
170
171 :param translator: The LaTeX translator writing the document.
172 :param node: The listing.
173 :raises SkipNode: Always, so the node's text isn't written again.
174 """
175 width = len(str(max((number for number, *_ in node["lines"]), default=1)))
176
177 lines: list[str] = []
178 for number, status, _, _, _, tokens in node["lines"]:
179 color, marker = COVERAGE_MARKERS[status]
180 lines.append(f"\\textcolor{{{color}}}{{{number:>{width}} {marker}}} {highlightTokens(tokens)}")
181
182 writeListing(translator, node, lines, COVERAGE_MARKERS, COVERAGE_STATES)
183
184 raise nodes.SkipNode
185
186
187@export
188def depart_CoverageListing(translator: LaTeXTranslator, node: CoverageListing) -> None:
189 """
190 Close a source file's code coverage in LaTeX, which writes nothing: :func:`visit_CoverageListing` wrote it all.
191
192 :param translator: The LaTeX translator writing the document.
193 :param node: The listing.
194 """
195
196
197translateCoverageListing: tuple[visitFunc, departFunc] = (visit_CoverageListing, depart_CoverageListing)
198"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.CoverageListing` in LaTeX."""
199
200
201@export
202def visit_DocCoverageListing(translator: LaTeXTranslator, node: DocCoverageListing) -> None:
203 """
204 Write a Python module's documentation coverage in LaTeX, completely.
205
206 Like a source file's code coverage (see :func:`visit_CoverageListing`): each line starts with its number, colored
207 by its documentation state, and a marker of :data:`DOCUMENTATION_MARKERS`; an elided body's ``...`` has no number.
208 The node's text - the condensed source - isn't written again.
209
210 :param translator: The LaTeX translator writing the document.
211 :param node: The listing.
212 :raises SkipNode: Always, so the node's text isn't written again.
213 """
214 width = len(str(max((number for number, *_ in node["lines"] if number is not None), default=1)))
215
216 lines: list[str] = []
217 for number, status, tokens in node["lines"]:
218 color, marker = DOCUMENTATION_MARKERS[status]
219 numberText = "" if number is None else f"{number}"
220 lines.append(f"\\textcolor{{{color}}}{{{numberText:>{width}} {marker}}} {highlightTokens(tokens)}")
221
222 writeListing(translator, node, lines, DOCUMENTATION_MARKERS, DOCUMENTATION_STATES)
223
224 raise nodes.SkipNode
225
226
227@export
228def depart_DocCoverageListing(translator: LaTeXTranslator, node: DocCoverageListing) -> None:
229 """
230 Close a Python module's documentation coverage in LaTeX, which writes nothing: :func:`visit_DocCoverageListing`
231 wrote it all.
232
233 :param translator: The LaTeX translator writing the document.
234 :param node: The listing.
235 """
236
237
238translateDocCoverageListing: tuple[visitFunc, departFunc] = (visit_DocCoverageListing, depart_DocCoverageListing)
239"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.DocCoverageListing` in LaTeX."""
240
241
242@export
243def visit_TaskListCheckbox(translator: LaTeXTranslator, node: TaskListCheckbox) -> None:
244 """
245 Write a task list item's checkbox in LaTeX, completely: ``\\boxtimes`` if the task is done, else ``\\square`` - both
246 of package ``amssymb``, which Sphinx loads.
247
248 :param translator: The LaTeX translator writing the document.
249 :param node: The checkbox.
250 :raises SkipNode: Always, so the node's text isn't written.
251 """
252 translator.body.append("\\(\\boxtimes\\)" if node["checked"] else "\\(\\square\\)")
253
254 raise nodes.SkipNode
255
256
257@export
258def depart_TaskListCheckbox(translator: LaTeXTranslator, node: TaskListCheckbox) -> None:
259 """
260 Close a task list item's checkbox in LaTeX, which writes nothing: :func:`visit_TaskListCheckbox` wrote it all.
261
262 :param translator: The LaTeX translator writing the document.
263 :param node: The checkbox.
264 """
265
266
267translateTaskListCheckbox: tuple[visitFunc, departFunc] = (visit_TaskListCheckbox, depart_TaskListCheckbox)
268"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.TaskListCheckbox` in LaTeX."""
269
270
271@export
272def visit_LineBreak(translator: LaTeXTranslator, node: LineBreak) -> None:
273 """
274 Write a line break in LaTeX, completely: ``\\newline``, which breaks the line in any environment.
275
276 :param translator: The LaTeX translator writing the document.
277 :param node: The line break.
278 :raises SkipNode: Always, so the node's text isn't written.
279 """
280 translator.body.append("\\newline{}")
281
282 raise nodes.SkipNode
283
284
285@export
286def depart_LineBreak(translator: LaTeXTranslator, node: LineBreak) -> None:
287 """
288 Close a line break in LaTeX, which writes nothing: :func:`visit_LineBreak` wrote it all.
289
290 :param translator: The LaTeX translator writing the document.
291 :param node: The line break.
292 """
293
294
295translateLineBreak: tuple[visitFunc, departFunc] = (visit_LineBreak, depart_LineBreak)
296"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.LineBreak` in LaTeX."""
297
298
299@export
300def visit_ReportIndentation(translator: LaTeXTranslator, node: ReportIndentation) -> None:
301 """
302 Open an indented name of a report table in LaTeX: macro ``\\pyToolingReportIndentation`` of package
303 :file:`pyTooling.Sphinx.sty` with the number of indentation steps.
304
305 :param translator: The LaTeX translator writing the document.
306 :param node: The indented name.
307 """
308 translator.body.append(f"\\pyToolingReportIndentation{{{node['level']}}}")
309
310
311@export
312def depart_ReportIndentation(translator: LaTeXTranslator, node: ReportIndentation) -> None:
313 """
314 Close an indented name of a report table in LaTeX, which writes nothing: the macro indents the rest of the cell.
315
316 :param translator: The LaTeX translator writing the document.
317 :param node: The indented name.
318 """
319
320
321translateReportIndentation: tuple[visitFunc, departFunc] = (visit_ReportIndentation, depart_ReportIndentation)
322"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.ReportIndentation` in LaTeX."""