Skip to content

Commit 80728f2

Browse files
committed
Add latex tab styling
1 parent b1330a6 commit 80728f2

2 files changed

Lines changed: 167 additions & 0 deletions

File tree

doc/_ext/latex_tabs.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"""PDF/LaTeX styling for sphinx-tabs directives.
2+
3+
sphinx-tabs only registers HTML visitors for its custom node types. For all
4+
other builders it falls back to plain ``nodes.container`` nodes, so the tab
5+
labels render as unstyled text with no visual separation between variants.
6+
7+
This extension inserts a ``SphinxTransform`` (LaTeX/rinoh builds only) that
8+
rewrites the generic container tree produced by sphinx-tabs into custom
9+
``LatexTabsGroup`` / ``LatexTabEntry`` nodes, and registers LaTeX visitors
10+
that emit a small coloured label box before each tab's content.
11+
12+
Expected result in PDF::
13+
14+
┌──────────────────────────────┐
15+
│ [Git] ← coloured pill │
16+
│ content … │
17+
│ ────────────────────────── │
18+
│ [SVN] │
19+
│ content … │
20+
│ ────────────────────────── │
21+
│ [Archive] │
22+
│ content … │
23+
└──────────────────────────────┘
24+
25+
The styling uses the ``dfaccent`` and ``dfnearblack`` colours already defined
26+
in conf.py's LaTeX preamble.
27+
"""
28+
29+
import re
30+
31+
from docutils import nodes
32+
from sphinx.transforms import SphinxTransform
33+
34+
# ---------------------------------------------------------------------------
35+
# Custom node types
36+
# ---------------------------------------------------------------------------
37+
38+
39+
class LatexTabsGroup(nodes.General, nodes.Element):
40+
"""Outer wrapper replacing a ``sphinx-tabs`` container in LaTeX output."""
41+
42+
43+
class LatexTabEntry(nodes.General, nodes.Element):
44+
"""One tab (label + content) inside a ``LatexTabsGroup``."""
45+
46+
47+
# ---------------------------------------------------------------------------
48+
# Transform: rewrite sphinx-tabs containers for LaTeX
49+
# ---------------------------------------------------------------------------
50+
51+
_LATEX_SPECIAL = re.compile(r"([&%$#_{}~^\\])")
52+
53+
54+
def _escape_latex(text: str) -> str:
55+
return _LATEX_SPECIAL.sub(lambda m: "\\" + m.group(1), text)
56+
57+
58+
class LatexTabsTransform(SphinxTransform):
59+
"""Convert sphinx-tabs containers into styled LatexTabsGroup nodes.
60+
61+
Only runs for latex/rinoh builders. The sphinx-tabs fallback structure
62+
for non-HTML builders is::
63+
64+
nodes.container [classes: ['sphinx-tabs']]
65+
nodes.container (outer per tab)
66+
nodes.container (tab — holds the label)
67+
nodes.container (panel — holds the content)
68+
69+
This transform converts that into::
70+
71+
LatexTabsGroup
72+
LatexTabEntry [label="Git"]
73+
<panel content nodes>
74+
LatexTabEntry [label="SVN"]
75+
<panel content nodes>
76+
77+
"""
78+
79+
default_priority = 500
80+
81+
def apply(self, **kwargs) -> None:
82+
if self.app.builder.name not in ("latex", "rinoh"):
83+
return
84+
85+
for tabs_node in self.document.traverse(
86+
lambda n: isinstance(n, nodes.container)
87+
and "sphinx-tabs" in n.get("classes", [])
88+
):
89+
group = LatexTabsGroup()
90+
for outer in list(tabs_node.children):
91+
if not isinstance(outer, nodes.container) or len(outer.children) < 2:
92+
continue
93+
tab_container = outer.children[0]
94+
panel = outer.children[1]
95+
96+
entry = LatexTabEntry()
97+
entry["label"] = tab_container.astext().strip()
98+
entry += list(panel.children)
99+
group += entry
100+
101+
tabs_node.replace_self(group)
102+
103+
104+
# ---------------------------------------------------------------------------
105+
# LaTeX visitors
106+
# ---------------------------------------------------------------------------
107+
108+
109+
def visit_latex_tabs_group(translator, _node) -> None:
110+
"""Emit an opening rule before the tabs group."""
111+
translator.body.append(
112+
"\n\\vspace{4pt}\\noindent{\\color{dfaccent!40}\\rule{\\linewidth}{0.6pt}}\\vspace{-2pt}\n"
113+
)
114+
115+
116+
def depart_latex_tabs_group(translator, _node) -> None:
117+
"""Emit a closing rule after the tabs group."""
118+
translator.body.append(
119+
"\n{\\color{dfaccent!40}\\rule{\\linewidth}{0.6pt}}\\vspace{4pt}\n"
120+
)
121+
122+
123+
def visit_latex_tab_entry(translator, node) -> None:
124+
"""Emit a coloured pill label before the tab content."""
125+
label = _escape_latex(node["label"])
126+
translator.body.append(
127+
f"\n\\noindent\\colorbox{{dfaccent!15}}{{\\strut\\textbf{{\\small\\textcolor{{dfaccent}}{{{label}}}}}}}"
128+
f"\\par\\vspace{{2pt}}\n"
129+
)
130+
131+
132+
def depart_latex_tab_entry(translator, _node) -> None:
133+
"""Emit a separator rule after the tab content."""
134+
translator.body.append(
135+
"\n\\vspace{2pt}\\noindent{\\color{dfaccent!25}\\rule{\\linewidth}{0.4pt}}\\vspace{-2pt}\n"
136+
)
137+
138+
139+
def _html_skip(translator, node) -> None:
140+
raise nodes.SkipNode
141+
142+
143+
# ---------------------------------------------------------------------------
144+
# Extension setup
145+
# ---------------------------------------------------------------------------
146+
147+
148+
def setup(app):
149+
"""Register nodes, visitors, and the LaTeX transform."""
150+
app.add_node(
151+
LatexTabsGroup,
152+
latex=(visit_latex_tabs_group, depart_latex_tabs_group),
153+
html=(_html_skip, None),
154+
)
155+
app.add_node(
156+
LatexTabEntry,
157+
latex=(visit_latex_tab_entry, depart_latex_tab_entry),
158+
html=(_html_skip, None),
159+
)
160+
app.add_transform(LatexTabsTransform)
161+
162+
return {
163+
"version": "0.1",
164+
"parallel_read_safe": True,
165+
"parallel_write_safe": True,
166+
}

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"sphinx_design",
4242
"plantweb.directive",
4343
"scenario_directive",
44+
"latex_tabs",
4445
"unique_section_ids",
4546
"sphinx.ext.autodoc",
4647
"sphinx.ext.autosectionlabel",

0 commit comments

Comments
 (0)