HTML.py

Summary

Lines:

185 of 185 covered (100.0%)

Branches:

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

Coverage:

100.0%

Directory:

Sphinx

Source

pyTooling/Sphinx/HTML.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 HTML.
33
34Each node has a ``visit_*`` and a ``depart_*`` function and a ``translate*`` pair of both, which
35:data:`~pyTooling.Sphinx.NODES` registers.
36"""
37from docutils              import nodes
38from sphinx.writers.html5  import HTML5Translator
39
40from pyTooling.Decorators  import export
41from pyTooling.Sphinx.Node import Abbreviation, CopyButton, Landscape, TreeDescription, TreeItem, TreeLabel
42from pyTooling.Sphinx.Node import TreeLegend, TreeLegendItem, TreeSeparator
43from pyTooling.Sphinx.Node import COVERAGE_STATES, DOCUMENTATION_STATES, CoverageListing, DocCoverageListing
44from pyTooling.Sphinx.Node import Emoji, LineBreak, ReportIndentation, TaskListCheckbox
45from pyTooling.Sphinx.Node import visitFunc, departFunc
46
47
48__all__ = [
49	"translateLandscape", "translateTreeItem", "translateTreeLabel", "translateTreeSeparator", "translateTreeDescription",
50	"translateTreeLegend", "translateTreeLegendItem", "translateAbbreviation", "translateCoverageListing",
51	"translateDocCoverageListing", "translateCopyButton", "translateEmoji", "translateTaskListCheckbox",
52	"translateLineBreak", "COPY_ICONS"
53]
54
55_SVG = (
56	'aria-hidden="true" focusable="false" width="1em" height="1em" viewBox="0 0 24 24" fill="none" '
57	'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"'
58)
59"""The attributes of a copy button's icons: hidden from a screen reader, as large as the text, and drawn as lines."""
60
61#: The icons of a copy button, as inline SVG hidden from a screen reader: two sheets of paper, and a check mark, which
62#: the stylesheet shows instead while the button reports a copied text.
63COPY_ICONS = (
64	f'<svg class="copy-icon" {_SVG}><rect x="8" y="8" width="12" height="12" rx="2"/>'
65	'<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></svg>'
66	f'<svg class="copied-icon" {_SVG}><path d="M5 12l5 5L20 7"/></svg>'
67)
68
69
70@export
71def visit_TreeItem(translator: HTML5Translator, node: TreeItem) -> None:
72	"""
73	Open an entry in HTML: a list item, and a ``<details>`` element if the entry has children.
74
75	The root of a tree with descriptions states the width of the column of the entries' texts, as CSS custom property
76	``--pyTooling-tree-column``, which its descendants inherit.
77
78	:param translator: The HTML translator writing the page.
79	:param node:       The entry.
80	"""
81	if node["columnWidth"] is None:
82		translator.body.append(translator.starttag(node, "li", ""))
83	else:
84		translator.body.append(
85			translator.starttag(node, "li", "", style=f"--pyTooling-tree-column: {node['columnWidth']}")
86		)
87
88	if node["expanded"] is not None:
89		translator.body.append("<details open>" if node["expanded"] else "<details>")
90
91
92@export
93def depart_TreeItem(translator: HTML5Translator, node: TreeItem) -> None:
94	"""
95	Close an entry in HTML.
96
97	:param translator: The HTML translator writing the page.
98	:param node:       The entry.
99	"""
100	if node["expanded"] is not None:
101		translator.body.append("</details>")
102	translator.body.append("</li>\n")
103
104
105@export
106def visit_TreeLabel(translator: HTML5Translator, node: TreeLabel) -> None:
107	"""
108	Open an entry's text in HTML, behind its icons.
109
110	An entry with children opens a ``<summary>`` and writes both expander icons, the stylesheet showing one of them.
111	Any other entry writes an empty expander instead, so its text is aligned with its siblings'. Icons are hidden from
112	a screen reader.
113
114	In a tree with descriptions, the text ends with a :class:`~pyTooling.Sphinx.Node.TreeDescription`: the entry is a
115	row, whose first column holds the icons and the text, and whose second column the description. The row states the
116	entry's level as CSS custom property ``--pyTooling-tree-depth``, from which the stylesheet computes how wide the
117	first column is at this level.
118
119	:param translator: The HTML translator writing the page.
120	:param node:       The entry's text.
121	"""
122	if node["expandedIcon"] is not None:
123		translator.body.append("<summary>")
124
125	if isinstance(node.children[-1], TreeDescription):
126		translator.body.append(
127			f'<span class="tree-row" style="--pyTooling-tree-depth: {node["level"]}"><span class="tree-label">'
128		)
129
130	if node["expandedIcon"] is None:
131		translator.body.append('<span class="tree-expander" aria-hidden="true"></span>')
132	else:
133		for state, stateIcon in (("expanded", node["expandedIcon"]), ("collapsed", node["collapsedIcon"])):
134			translator.body.append(
135				f'<span class="tree-expander tree-{state}" aria-hidden="true">{translator.encode(stateIcon)}</span>'
136			)
137
138	if node["icon"] != "":
139		translator.body.append(f'<span class="tree-icon" aria-hidden="true">{translator.encode(node["icon"])}</span>')
140
141	translator.body.append('<span class="tree-text">')
142
143
144@export
145def depart_TreeLabel(translator: HTML5Translator, node: TreeLabel) -> None:
146	"""
147	Close an entry's text in HTML - or its row, in a tree with descriptions -, and its ``<summary>`` if the entry has
148	children.
149
150	:param translator: The HTML translator writing the page.
151	:param node:       The entry's text.
152	"""
153	translator.body.append("</span>")
154	if node["expandedIcon"] is not None:
155		translator.body.append("</summary>")
156
157
158@export
159def visit_TreeSeparator(translator: HTML5Translator, node: TreeSeparator) -> None:
160	"""
161	Skip the separator between an entry's text and its description in HTML, which draws the description in a column of
162	its own.
163
164	:param translator: The HTML translator writing the page.
165	:param node:       The separator.
166	:raises SkipNode:  Always, so neither the separator's text nor its depart function is written.
167	"""
168	raise nodes.SkipNode()
169
170
171@export
172def depart_TreeSeparator(translator: HTML5Translator, node: TreeSeparator) -> None:
173	"""
174	Close the separator between an entry's text and its description in HTML, which is never called:
175	:func:`visit_TreeSeparator` skips the node.
176
177	:param translator: The HTML translator writing the page.
178	:param node:       The separator.
179	"""
180
181
182@export
183def visit_TreeDescription(translator: HTML5Translator, node: TreeDescription) -> None:
184	"""
185	Open an entry's description in HTML: close the entry's text and the first column, and open the second column.
186
187	An entry without a description has an empty one, so every row of a tree with descriptions has both columns.
188
189	:param translator: The HTML translator writing the page.
190	:param node:       The description.
191	"""
192	translator.body.append('</span></span><span class="tree-description">')
193
194
195@export
196def depart_TreeDescription(translator: HTML5Translator, node: TreeDescription) -> None:
197	"""
198	Close an entry's description in HTML.
199
200	:param translator: The HTML translator writing the page.
201	:param node:       The description.
202	"""
203	translator.body.append("</span>")
204
205
206@export
207def visit_TreeLegend(translator: HTML5Translator, node: TreeLegend) -> None:
208	"""
209	Open a tree's legend in HTML: a paragraph, hidden from a screen reader like the icons it explains.
210
211	:param translator: The HTML translator writing the page.
212	:param node:       The legend.
213	"""
214	translator.body.append(translator.starttag(node, "p", "", **{"aria-hidden": "true"}))
215
216
217@export
218def depart_TreeLegend(translator: HTML5Translator, node: TreeLegend) -> None:
219	"""
220	Close a tree's legend in HTML.
221
222	:param translator: The HTML translator writing the page.
223	:param node:       The legend.
224	"""
225	translator.body.append("</p>\n")
226
227
228@export
229def visit_TreeLegendItem(translator: HTML5Translator, node: TreeLegendItem) -> None:
230	"""
231	Open an item of a tree's legend in HTML: its icon, and the text explaining it.
232
233	:param translator: The HTML translator writing the page.
234	:param node:       The legend's item.
235	"""
236	translator.body.append(
237		f'<span class="tree-legend-item"><span class="tree-icon">{translator.encode(node["icon"])}</span>'
238		'<span class="tree-text">'
239	)
240
241
242@export
243def depart_TreeLegendItem(translator: HTML5Translator, node: TreeLegendItem) -> None:
244	"""
245	Close an item of a tree's legend in HTML.
246
247	:param translator: The HTML translator writing the page.
248	:param node:       The legend's item.
249	"""
250	translator.body.append("</span></span>")
251
252
253@export
254def visit_Landscape(translator: HTML5Translator, node: Landscape) -> None:
255	"""
256	Open a landscape container in HTML, which writes nothing: a web page has no page orientation.
257
258	:param translator: The HTML translator writing the page.
259	:param node:       The landscape container.
260	"""
261
262
263@export
264def depart_Landscape(translator: HTML5Translator, node: Landscape) -> None:
265	"""
266	Close a landscape container in HTML, which writes nothing.
267
268	:param translator: The HTML translator writing the page.
269	:param node:       The landscape container.
270	"""
271
272
273translateLandscape: tuple[visitFunc, departFunc] = (visit_Landscape, depart_Landscape)
274"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.Landscape` in HTML."""
275
276translateTreeItem: tuple[visitFunc, departFunc] = (visit_TreeItem, depart_TreeItem)
277"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.TreeItem` in HTML."""
278
279translateTreeLabel: tuple[visitFunc, departFunc] = (visit_TreeLabel, depart_TreeLabel)
280"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.TreeLabel` in HTML."""
281
282translateTreeSeparator: tuple[visitFunc, departFunc] = (visit_TreeSeparator, depart_TreeSeparator)
283"""Visit and depart function skipping a :class:`~pyTooling.Sphinx.Node.TreeSeparator` in HTML."""
284
285translateTreeDescription: tuple[visitFunc, departFunc] = (visit_TreeDescription, depart_TreeDescription)
286"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.TreeDescription` in HTML."""
287
288translateTreeLegend: tuple[visitFunc, departFunc] = (visit_TreeLegend, depart_TreeLegend)
289"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.TreeLegend` in HTML."""
290
291translateTreeLegendItem: tuple[visitFunc, departFunc] = (visit_TreeLegendItem, depart_TreeLegendItem)
292"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.TreeLegendItem` in HTML."""
293
294
295@export
296def visit_Abbreviation(translator: HTML5Translator, node: Abbreviation) -> None:
297	"""
298	Open an abbreviation in HTML: an ``<abbr>`` element.
299
300	:param translator: The HTML translator writing the page.
301	:param node:       The abbreviation.
302	"""
303	translator.body.append(translator.starttag(node, "abbr", "", CLASS="pytooling-abbreviation"))
304
305
306@export
307def depart_Abbreviation(translator: HTML5Translator, node: Abbreviation) -> None:
308	"""
309	Close an abbreviation in HTML, behind the box the stylesheet shows on hover.
310
311	The box shows the long form and, if the node carries one, the summary of the abbreviation's description. The short
312	form is shown in front of the long form only if a title replaced it in the text.
313
314	:param translator: The HTML translator writing the page.
315	:param node:       The abbreviation.
316	"""
317	long = translator.encode(node["long"])
318	short = "" if node.astext() == node["short"] else f'<strong>{translator.encode(node["short"])}</strong> '
319	summary = node.get("summary", "")
320	summary = f'<span class="pytooling-abbreviation-summary">{translator.encode(summary)}</span>' if summary != "" else ""
321	translator.body.append(
322		f'<span class="pytooling-abbreviation-box" role="tooltip">{short}{long}{summary}</span></abbr>'
323	)
324
325
326translateAbbreviation: tuple[visitFunc, departFunc] = (visit_Abbreviation, depart_Abbreviation)
327"""Visit and depart function writing an :class:`~pyTooling.Sphinx.Node.Abbreviation` in HTML."""
328
329
330@export
331def highlightTokens(translator: HTML5Translator, tokens: list[tuple[str, str]]) -> str:
332	"""
333	Write a listing line's tokens in HTML, each in a ``<span>`` of its Pygments class, so Sphinx' Pygments stylesheet
334	colors it.
335
336	:param translator: The HTML translator writing the page.
337	:param tokens:     The line's tokens as ``(CSS class, text)`` pairs; a token without a class is written as text.
338	:returns:          The line's HTML.
339	"""
340	return "".join(
341		translator.encode(text) if cssClass == "" else f'<span class="{cssClass}">{translator.encode(text)}</span>'
342		for cssClass, text in tokens
343	)
344
345
346@export
347def writeListing(translator: HTML5Translator, node: nodes.Element, lines: list[str], states: dict[str, str]) -> None:
348	"""
349	Write a listing in HTML: a header with the file's path and a legend of the states, then the lines.
350
351	The lines are a ``<pre>`` inside the ``<div>`` elements of Sphinx' highlighting, so Sphinx' Pygments stylesheet colors
352	the tokens.
353
354	:param translator: The HTML translator writing the page.
355	:param node:       The listing, whose attribute ``path`` holds the file's path.
356	:param lines:      The lines, each written as HTML.
357	:param states:     The states the legend explains, by CSS class suffix: the label of each.
358	"""
359	classes = "highlight-default notranslate report-coverage-listing"
360	legend = "".join(
361		f'<span class="report-coverage-legend-{state or "none"}">{translator.encode(label)}</span>'
362		for state, label in states.items()
363	)
364	translator.body.append(translator.starttag(node, "div", "", CLASS=classes))
365	translator.body.append(
366		f'<div class="report-coverage-header"><span class="report-coverage-path">{translator.encode(node["path"])}</span>'
367		f'<span class="report-coverage-legend">{legend}</span></div>'
368	)
369	translator.body.append('<div class="highlight"><pre>')
370	translator.body.extend(lines)
371	translator.body.append("</pre></div></div>\n")
372
373
374@export
375def visit_CoverageListing(translator: HTML5Translator, node: CoverageListing) -> None:
376	"""
377	Write a source file's code coverage in HTML, completely.
378
379	A header shows the file's path and a legend of the coverage states (see :func:`writeListing`). Each line is a
380	``<span>`` with class ``report-line`` and ``report-line-<state>``, holding its number, its hits - if the report has
381	any - and its tokens. A line, which ran without taking all its branches, says on hover how many were taken. The
382	node's text - the plain source - isn't written again.
383
384	:param translator: The HTML translator writing the page.
385	:param node:       The listing.
386	:raises SkipNode:  Always, so the node's text isn't written again.
387	"""
388	withHits = any(hits is not None for _, _, hits, _, _, _ in node["lines"])
389
390	lines: list[str] = []
391	for number, status, hits, branches, coveredBranches, tokens in node["lines"]:
392		classes = "report-line" if status == "" else f"report-line report-line-{status}"
393		attributes = f' id="L{number}"' if node["anchors"] else ""
394		if status == "partial":
395			attributes += f' title="{coveredBranches} of {branches} branches taken"'
396
397		hitsText = "" if hits is None else f"{hits}"
398		gutter = f'<span class="linenos">{number}</span>'
399		if withHits:
400			gutter += f'<span class="linenos report-hits">{hitsText}</span>'
401
402		lines.append(f'<span class="{classes}"{attributes}>{gutter}{highlightTokens(translator, tokens)}\n</span>')
403
404	writeListing(translator, node, lines, COVERAGE_STATES)
405
406	raise nodes.SkipNode
407
408
409@export
410def depart_CoverageListing(translator: HTML5Translator, node: CoverageListing) -> None:
411	"""
412	Close a source file's code coverage in HTML, which writes nothing: :func:`visit_CoverageListing` wrote it all.
413
414	:param translator: The HTML translator writing the page.
415	:param node:       The listing.
416	"""
417
418
419translateCoverageListing: tuple[visitFunc, departFunc] = (visit_CoverageListing, depart_CoverageListing)
420"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.CoverageListing` in HTML."""
421
422
423@export
424def visit_DocCoverageListing(translator: HTML5Translator, node: DocCoverageListing) -> None:
425	"""
426	Write a Python module's documentation coverage in HTML, completely.
427
428	A header shows the module's path and a legend of the documentation states (see :func:`writeListing`). Each line is
429	a ``<span>`` with class ``report-line`` and ``report-line-<state>``, holding its number and its tokens; an elided
430	body's ``...`` has no number and the class ``report-line-elided``. The node's text - the condensed source - isn't
431	written again.
432
433	:param translator: The HTML translator writing the page.
434	:param node:       The listing.
435	:raises SkipNode:  Always, so the node's text isn't written again.
436	"""
437	lines: list[str] = []
438	for number, status, tokens in node["lines"]:
439		classes = "report-line" if status == "" else f"report-line report-line-{status}"
440		if number is None:
441			classes += " report-line-elided"
442
443		gutter = f'<span class="linenos">{"" if number is None else number}</span>'
444		lines.append(f'<span class="{classes}">{gutter}{highlightTokens(translator, tokens)}\n</span>')
445
446	writeListing(translator, node, lines, DOCUMENTATION_STATES)
447
448	raise nodes.SkipNode
449
450
451@export
452def visit_CopyButton(translator: HTML5Translator, node: CopyButton) -> None:
453	"""
454	Write a copy button in HTML, completely: a ``<button>`` with the text to copy as attribute ``data-copy``.
455
456	The button's label is its accessible name and its tooltip; its icons are :data:`COPY_ICONS`. The script
457	:file:`pyTooling.js` copies the text on a click.
458
459	:param translator: The HTML translator writing the page.
460	:param node:       The copy button.
461	:raises SkipNode:  Always, as the button is written completely.
462	"""
463	translator.body.append(translator.starttag(
464		node, "button", "", type="button", CLASS="pytooling-copy", title=node["label"],
465		**{"aria-label": node["label"], "data-copy": node["copy"]}
466	))
467	translator.body.append(f"{COPY_ICONS}</button>")
468
469	raise nodes.SkipNode
470
471
472@export
473def depart_DocCoverageListing(translator: HTML5Translator, node: DocCoverageListing) -> None:
474	"""
475	Close a Python module's documentation coverage in HTML, which writes nothing: :func:`visit_DocCoverageListing` wrote
476	it all.
477
478	:param translator: The HTML translator writing the page.
479	:param node:       The listing.
480	"""
481
482
483translateDocCoverageListing: tuple[visitFunc, departFunc] = (visit_DocCoverageListing, depart_DocCoverageListing)
484"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.DocCoverageListing` in HTML."""
485
486
487@export
488def depart_CopyButton(translator: HTML5Translator, node: CopyButton) -> None:
489	"""
490	Close a copy button in HTML, which writes nothing: :func:`visit_CopyButton` wrote it all.
491
492	:param translator: The HTML translator writing the page.
493	:param node:       The copy button.
494	"""
495
496
497translateCopyButton: tuple[visitFunc, departFunc] = (visit_CopyButton, depart_CopyButton)
498"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.CopyButton` in HTML."""
499
500
501@export
502def visit_Emoji(translator: HTML5Translator, node: Emoji) -> None:
503	"""
504	Open an emoji in HTML: a ``<span>`` with the emoji's shortcode as tooltip.
505
506	:param translator: The HTML translator writing the page.
507	:param node:       The emoji.
508	"""
509	translator.body.append(translator.starttag(node, "span", "", CLASS="github-emoji", title=f":{node['shortcode']}:"))
510
511
512@export
513def depart_Emoji(translator: HTML5Translator, node: Emoji) -> None:
514	"""
515	Close an emoji in HTML.
516
517	:param translator: The HTML translator writing the page.
518	:param node:       The emoji.
519	"""
520	translator.body.append("</span>")
521
522
523translateEmoji: tuple[visitFunc, departFunc] = (visit_Emoji, depart_Emoji)
524"""Visit and depart function writing an :class:`~pyTooling.Sphinx.Node.Emoji` in HTML."""
525
526
527@export
528def visit_TaskListCheckbox(translator: HTML5Translator, node: TaskListCheckbox) -> None:
529	"""
530	Write a task list item's checkbox in HTML, completely: a disabled ``<input>`` of type ``checkbox``, checked if the
531	task is done.
532
533	:param translator: The HTML translator writing the page.
534	:param node:       The checkbox.
535	:raises SkipNode:  Always, so the node's text isn't written.
536	"""
537	checked = ' checked=""' if node["checked"] else ""
538	label = "Completed task" if node["checked"] else "Incomplete task"
539	translator.body.append(
540		f'<input class="github-task-list-checkbox" type="checkbox" disabled=""{checked} aria-label="{label}" />'
541	)
542
543	raise nodes.SkipNode
544
545
546@export
547def depart_TaskListCheckbox(translator: HTML5Translator, node: TaskListCheckbox) -> None:
548	"""
549	Close a task list item's checkbox in HTML, which writes nothing: :func:`visit_TaskListCheckbox` wrote it all.
550
551	:param translator: The HTML translator writing the page.
552	:param node:       The checkbox.
553	"""
554
555
556translateTaskListCheckbox: tuple[visitFunc, departFunc] = (visit_TaskListCheckbox, depart_TaskListCheckbox)
557"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.TaskListCheckbox` in HTML."""
558
559
560@export
561def visit_LineBreak(translator: HTML5Translator, node: LineBreak) -> None:
562	"""
563	Write a line break in HTML, completely: ``<br />``.
564
565	:param translator: The HTML translator writing the page.
566	:param node:       The line break.
567	:raises SkipNode:  Always, so the node's text isn't written.
568	"""
569	translator.body.append("<br />\n")
570
571	raise nodes.SkipNode
572
573
574@export
575def depart_LineBreak(translator: HTML5Translator, node: LineBreak) -> None:
576	"""
577	Close a line break in HTML, which writes nothing: :func:`visit_LineBreak` wrote it all.
578
579	:param translator: The HTML translator writing the page.
580	:param node:       The line break.
581	"""
582
583
584translateLineBreak: tuple[visitFunc, departFunc] = (visit_LineBreak, depart_LineBreak)
585"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.LineBreak` in HTML."""
586
587
588@export
589def visit_ReportIndentation(translator: HTML5Translator, node: ReportIndentation) -> None:
590	"""
591	Open an indented name of a report table in HTML: a ``<span>`` setting the CSS custom property
592	``--pyTooling-report-level`` to the number of indentation steps.
593
594	:param translator: The HTML translator writing the page.
595	:param node:       The indented name.
596	"""
597	translator.body.append(translator.starttag(
598		node, "span", "", CLASS="report-indentation", style=f"--pyTooling-report-level: {node['level']}"
599	))
600
601
602@export
603def depart_ReportIndentation(translator: HTML5Translator, node: ReportIndentation) -> None:
604	"""
605	Close an indented name of a report table in HTML.
606
607	:param translator: The HTML translator writing the page.
608	:param node:       The indented name.
609	"""
610	translator.body.append("</span>")
611
612
613translateReportIndentation: tuple[visitFunc, departFunc] = (visit_ReportIndentation, depart_ReportIndentation)
614"""Visit and depart function writing a :class:`~pyTooling.Sphinx.Node.ReportIndentation` in HTML."""