LaTeX.py

Summary

Lines:

88 of 88 covered (100.0%)

Branches:

8 of 8 taken (100.0%), 0 lines partially

Coverage:

100.0%

Directory:

Sphinx

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."""