LaTeX.py
Summary
- Lines:
90 of 90 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, \
45 Landscape, LineBreak, ReportIndentation, TaskListCheckbox, visitFunc, \
46 departFunc
47
48
49__all__ = [
50 "translateLandscape", "translateCoverageListing", "translateDocCoverageListing",
51 "translateTaskListCheckbox", "translateLineBreak", "COVERAGE_MARKERS", "DOCUMENTATION_MARKERS", "TAB_WIDTH"
52]
53
54COVERAGE_MARKERS = {
55 "covered": ("green!50!black", "+"),
56 "partial": ("orange", "~"),
57 "uncovered": ("red", "-"),
58 "excluded": ("gray", "x"),
59 "": ("gray", " "),
60}
61"""Per coverage state of a line, the color of its number - an ``xcolor`` expression - and the marker behind it."""
62
63DOCUMENTATION_MARKERS = {
64 "documented": ("green!50!black", "+"),
65 "undocumented": ("red", "-"),
66 "ignored": ("gray", "o"),
67 "excluded": ("gray", "x"),
68 "": ("gray", " "),
69}
70"""Per documentation state of a line, the color of its number - an ``xcolor`` expression - and the marker behind it."""
71
72TAB_WIDTH = 4
73"""Number of columns a tab advances to, as LaTeX doesn't expand tabs in a listing."""
74
75
76@export
77def visit_Landscape(translator: LaTeXTranslator, node: Landscape) -> None:
78 """
79 Open a landscape container in LaTeX: a ``landscape`` environment, which starts a new page in landscape orientation.
80
81 :param translator: The LaTeX translator writing the document.
82 :param node: The landscape container.
83 """
84 translator.body.append(dedent("""
85 \\begin{landscape}
86 """)
87 )
88
89
90@export
91def depart_Landscape(translator: LaTeXTranslator, node: Landscape) -> None:
92 """
93 Close a landscape container in LaTeX: the end of the ``landscape`` environment, which returns to portrait pages.
94
95 :param translator: The LaTeX translator writing the document.
96 :param node: The landscape container.
97 """
98 translator.body.append(dedent("""
99 \\end{landscape}
100 """)
101 )
102
103
104translateLandscape: tuple[visitFunc, departFunc] = (visit_Landscape, depart_Landscape)
105"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.Landscape` in LaTeX."""
106
107
108@export
109def highlightTokens(tokens: list[tuple[str, str]]) -> str:
110 """
111 Write a listing line's tokens in LaTeX, as Sphinx writes highlighted code - ``\\PYG{<class>}{<text>}`` -, so the
112 configured Pygments style colors them.
113
114 Tabs are expanded to :data:`TAB_WIDTH` columns, as LaTeX doesn't expand them in a listing.
115
116 :param tokens: The line's tokens as ``(CSS class, text)`` pairs; a token without a class is written as text.
117 :returns: The line's LaTeX.
118 """
119 code: list[str] = []
120 column = 0
121 for cssClass, text in tokens:
122 if "\t" in text:
123 padding = column % TAB_WIDTH
124 text = (" " * padding + text).expandtabs(TAB_WIDTH)[padding:]
125 column += len(text)
126
127 escaped = escape_tex(text, "PYG")
128 code.append(escaped if cssClass == "" else f"\\PYG{{{cssClass}}}{{{escaped}}}")
129
130 return "".join(code)
131
132
133@export
134def writeListing(
135 translator: LaTeXTranslator,
136 node: nodes.Element,
137 lines: list[str],
138 markers: dict[str, tuple[str, str]],
139 states: dict[str, str]
140) -> None:
141 """
142 Write a listing in LaTeX: a line with the file's path and a legend of the markers, then a ``sphinxVerbatim``
143 environment of the lines.
144
145 :param translator: The LaTeX translator writing the document.
146 :param node: The listing, whose attribute ``path`` holds the file's path.
147 :param lines: The lines, each written as LaTeX.
148 :param markers: The color and marker per state.
149 :param states: The label per state, which the legend shows; the state ``""`` has no entry in the legend.
150 """
151 legend = "\\quad{}".join(
152 f"\\textcolor{{{color}}}{{\\texttt{{{escape_tex(marker, 'PYG')}}}}}~{states[state]}"
153 for state, (color, marker) in markers.items() if state != ""
154 )
155 translator.body.append(
156 f"\n\\par\\noindent\\sphinxcode{{{translator.encode(node['path'])}}}\\hfill{{}}{{\\small{{}}{legend}}}\\par\n"
157 )
158 translator.body.append("\n\\begin{sphinxVerbatim}[commandchars=\\\\\\{\\}]\n")
159 translator.body.append("\n".join(lines))
160 translator.body.append("\n\\end{sphinxVerbatim}\n")
161
162
163@export
164def visit_CoverageListing(translator: LaTeXTranslator, node: CoverageListing) -> None:
165 """
166 Write a source file's code coverage in LaTeX, completely.
167
168 The listing is a ``sphinxVerbatim`` environment with the tokens written as Sphinx writes highlighted code (see
169 :func:`highlightTokens`). Each line starts with its number, colored by its coverage state, and a marker of
170 :data:`COVERAGE_MARKERS`. A line above the listing shows the file's path and a legend of the markers. The node's
171 text - the plain source - isn't written again.
172
173 :param translator: The LaTeX translator writing the document.
174 :param node: The listing.
175 :raises SkipNode: Always, so the node's text isn't written again.
176 """
177 width = len(str(max((number for number, *_ in node["lines"]), default=1)))
178
179 lines: list[str] = []
180 for number, status, _, _, _, tokens in node["lines"]:
181 color, marker = COVERAGE_MARKERS[status]
182 lines.append(f"\\textcolor{{{color}}}{{{number:>{width}} {marker}}} {highlightTokens(tokens)}")
183
184 writeListing(translator, node, lines, COVERAGE_MARKERS, COVERAGE_STATES)
185
186 raise nodes.SkipNode
187
188
189@export
190def depart_CoverageListing(translator: LaTeXTranslator, node: CoverageListing) -> None:
191 """
192 Close a source file's code coverage in LaTeX, which writes nothing: :func:`visit_CoverageListing` wrote it all.
193
194 :param translator: The LaTeX translator writing the document.
195 :param node: The listing.
196 """
197
198
199translateCoverageListing: tuple[visitFunc, departFunc] = (visit_CoverageListing, depart_CoverageListing)
200"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.CoverageListing` in LaTeX."""
201
202
203@export
204def visit_DocCoverageListing(translator: LaTeXTranslator, node: DocCoverageListing) -> None:
205 """
206 Write a Python module's documentation coverage in LaTeX, completely.
207
208 Like a source file's code coverage (see :func:`visit_CoverageListing`): each line starts with its number, colored
209 by its documentation state, and a marker of :data:`DOCUMENTATION_MARKERS`; an elided body's ``...`` has no number.
210 The node's text - the condensed source - isn't written again.
211
212 :param translator: The LaTeX translator writing the document.
213 :param node: The listing.
214 :raises SkipNode: Always, so the node's text isn't written again.
215 """
216 width = len(str(max((number for number, *_ in node["lines"] if number is not None), default=1)))
217
218 lines: list[str] = []
219 for number, status, tokens in node["lines"]:
220 color, marker = DOCUMENTATION_MARKERS[status]
221 numberText = "" if number is None else f"{number}"
222 lines.append(f"\\textcolor{{{color}}}{{{numberText:>{width}} {marker}}} {highlightTokens(tokens)}")
223
224 writeListing(translator, node, lines, DOCUMENTATION_MARKERS, DOCUMENTATION_STATES)
225
226 raise nodes.SkipNode
227
228
229@export
230def depart_DocCoverageListing(translator: LaTeXTranslator, node: DocCoverageListing) -> None:
231 """
232 Close a Python module's documentation coverage in LaTeX, which writes nothing: :func:`visit_DocCoverageListing`
233 wrote it all.
234
235 :param translator: The LaTeX translator writing the document.
236 :param node: The listing.
237 """
238
239
240translateDocCoverageListing: tuple[visitFunc, departFunc] = (visit_DocCoverageListing, depart_DocCoverageListing)
241"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.DocCoverageListing` in LaTeX."""
242
243
244@export
245def visit_TaskListCheckbox(translator: LaTeXTranslator, node: TaskListCheckbox) -> None:
246 """
247 Write a task list item's checkbox in LaTeX, completely: ``\\boxtimes`` if the task is done, else ``\\square`` - both
248 of package ``amssymb``, which Sphinx loads.
249
250 :param translator: The LaTeX translator writing the document.
251 :param node: The checkbox.
252 :raises SkipNode: Always, so the node's text isn't written.
253 """
254 translator.body.append("\\(\\boxtimes\\)" if node["checked"] else "\\(\\square\\)")
255
256 raise nodes.SkipNode
257
258
259@export
260def depart_TaskListCheckbox(translator: LaTeXTranslator, node: TaskListCheckbox) -> None:
261 """
262 Close a task list item's checkbox in LaTeX, which writes nothing: :func:`visit_TaskListCheckbox` wrote it all.
263
264 :param translator: The LaTeX translator writing the document.
265 :param node: The checkbox.
266 """
267
268
269translateTaskListCheckbox: tuple[visitFunc, departFunc] = (visit_TaskListCheckbox, depart_TaskListCheckbox)
270"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.TaskListCheckbox` in LaTeX."""
271
272
273@export
274def visit_LineBreak(translator: LaTeXTranslator, node: LineBreak) -> None:
275 """
276 Write a line break in LaTeX, completely: ``\\newline``, which breaks the line in any environment.
277
278 :param translator: The LaTeX translator writing the document.
279 :param node: The line break.
280 :raises SkipNode: Always, so the node's text isn't written.
281 """
282 translator.body.append("\\newline{}")
283
284 raise nodes.SkipNode
285
286
287@export
288def depart_LineBreak(translator: LaTeXTranslator, node: LineBreak) -> None:
289 """
290 Close a line break in LaTeX, which writes nothing: :func:`visit_LineBreak` wrote it all.
291
292 :param translator: The LaTeX translator writing the document.
293 :param node: The line break.
294 """
295
296
297translateLineBreak: tuple[visitFunc, departFunc] = (visit_LineBreak, depart_LineBreak)
298"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.LineBreak` in LaTeX."""
299
300
301@export
302def visit_ReportIndentation(translator: LaTeXTranslator, node: ReportIndentation) -> None:
303 """
304 Open an indented name of a report table in LaTeX: macro ``\\pyToolingReportIndentation`` of package
305 :file:`pyTooling.Sphinx.sty` with the number of indentation steps.
306
307 :param translator: The LaTeX translator writing the document.
308 :param node: The indented name.
309 """
310 translator.body.append(f"\\pyToolingReportIndentation{{{node['level']}}}")
311
312
313@export
314def depart_ReportIndentation(translator: LaTeXTranslator, node: ReportIndentation) -> None:
315 """
316 Close an indented name of a report table in LaTeX, which writes nothing: the macro indents the rest of the cell.
317
318 :param translator: The LaTeX translator writing the document.
319 :param node: The indented name.
320 """
321
322
323translateReportIndentation: tuple[visitFunc, departFunc] = (visit_ReportIndentation, depart_ReportIndentation)
324"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.ReportIndentation` in LaTeX."""