Skip to content

Commit e35c251

Browse files
spoorccclaude
andauthored
Fix tabs-to-subsubsection transform for HTML-cached doctrees in PDF build (#1267)
The Sphinx Makefile uses -M mode, which shares a single _build/.doctrees/ directory across all builders. When `make html` runs first, doctrees are written with sphinx-tabs HTML node types (SphinxTabsTablist, SphinxTabsTab, SphinxTabsPanel). When `make latexpdf` runs next, the LatexTabsTransform encountered this HTML structure instead of the non-HTML fallback it expected, causing tab labels to render as plain paragraph text rather than \subsubsection* headings. Fix by detecting which structure is present (HTML-cached vs non-HTML fallback) and handling each path correctly. Also replace the deprecated self.app access with self.env.app to silence the Sphinx 8 deprecation warning. https://claude.ai/code/session_01UGnrctHsi8vRVecbAm4ncn Co-authored-by: Claude <noreply@anthropic.com>
1 parent fe1ced6 commit e35c251

1 file changed

Lines changed: 64 additions & 15 deletions

File tree

doc/_ext/latex_tabs.py

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,32 @@
55
labels render as unstyled text with no visual separation between variants.
66
77
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 render each tab as an unnumbered subsection heading followed by its
11-
content.
8+
rewrites the sphinx-tabs tree into custom ``LatexTabsGroup`` / ``LatexTabEntry``
9+
nodes, and registers LaTeX visitors that render each tab as an unnumbered
10+
subsubsection heading followed by its content.
11+
12+
The transform handles two possible document-tree structures:
13+
14+
* **Non-HTML fallback** — produced when the LaTeX builder reads fresh RST.
15+
sphinx-tabs emits plain ``nodes.container`` outer_nodes, each holding a tab
16+
label container and a content container::
17+
18+
nodes.container[sphinx-tabs]
19+
nodes.container ← outer_node (one per tab)
20+
nodes.container ← tab (holds label)
21+
nodes.container ← panel (holds content)
22+
23+
24+
* **HTML-cached doctrees** — produced when a previous ``make html`` run has
25+
already written ``.doctrees/`` files. The Makefile ``-M`` flag shares a
26+
single ``_build/.doctrees/`` directory across all builders. Those cached
27+
trees contain sphinx-tabs HTML node types::
28+
29+
nodes.container[sphinx-tabs]
30+
SphinxTabsTablist ← children are SphinxTabsTab paragraph nodes
31+
SphinxTabsPanel ← one per tab (content)
32+
SphinxTabsPanel
33+
1234
1335
Expected result in PDF::
1436
@@ -54,6 +76,22 @@ def _escape_latex(text: str) -> str:
5476
return _LATEX_SPECIAL.sub(lambda m: "\\" + m.group(1), text)
5577

5678

79+
def _is_html_cached_structure(children: list) -> bool:
80+
"""Return True if children look like an HTML-cached sphinx-tabs doctree.
81+
82+
The HTML doctree structure has a tablist container as first child, whose
83+
children are sphinx-tabs tab nodes carrying class ``sphinx-tabs-tab``.
84+
The non-HTML fallback has plain outer_node containers instead.
85+
"""
86+
if not children:
87+
return False
88+
first = children[0]
89+
return isinstance(first, nodes.Element) and any(
90+
isinstance(c, nodes.Element) and "sphinx-tabs-tab" in c.get("classes", [])
91+
for c in first.children
92+
)
93+
94+
5795
class LatexTabsTransform(SphinxTransform):
5896
"""Convert sphinx-tabs containers into styled LatexTabsGroup nodes.
5997
@@ -78,24 +116,35 @@ class LatexTabsTransform(SphinxTransform):
78116
default_priority = 500
79117

80118
def apply(self, **kwargs) -> None:
81-
if self.app.builder.name not in ("latex", "rinoh"):
119+
if self.env.app.builder.name not in ("latex", "rinoh"):
82120
return
83121

84122
for tabs_node in self.document.traverse(
85123
lambda n: isinstance(n, nodes.container)
86124
and "sphinx-tabs" in n.get("classes", [])
87125
):
88126
group = LatexTabsGroup()
89-
for outer in list(tabs_node.children):
90-
if not isinstance(outer, nodes.container) or len(outer.children) < 2:
91-
continue
92-
tab_container = outer.children[0]
93-
panel = outer.children[1]
94-
95-
entry = LatexTabEntry()
96-
entry["label"] = tab_container.astext().strip()
97-
entry += list(panel.children)
98-
group += entry
127+
children = list(tabs_node.children)
128+
129+
if _is_html_cached_structure(children):
130+
# HTML-cached doctree: tablist of SphinxTabsTab nodes + SphinxTabsPanel nodes.
131+
tablist, panels = children[0], children[1:]
132+
for tab_node, panel in zip(tablist.children, panels):
133+
entry = LatexTabEntry()
134+
entry["label"] = tab_node.astext().strip()
135+
entry += list(panel.children)
136+
group += entry
137+
else:
138+
# Non-HTML fallback: plain container outer_nodes with [tab, panel] children.
139+
for outer in children:
140+
if not isinstance(outer, nodes.container) or len(outer.children) < 2:
141+
continue
142+
tab_container = outer.children[0]
143+
panel = outer.children[1]
144+
entry = LatexTabEntry()
145+
entry["label"] = tab_container.astext().strip()
146+
entry += list(panel.children)
147+
group += entry
99148

100149
tabs_node.replace_self(group)
101150

0 commit comments

Comments
 (0)