Skip to content

Commit 50d8b8a

Browse files
committed
refactor(api-style[toolbar]): Group badges and [source] in a toolbar container
why: Badges and [source] were separate flex items that could wrap independently, causing [source] to land on a new line away from its badges. The headerlink (¶) was also being pulled into the toolbar, displacing it from its natural position next to the signature name. what: - Add gas-toolbar CSS class wrapping badges + [source] as a single flex item - Toolbar uses flex-shrink: 0, margin-left: auto, order: 99 to stay right-aligned and always after the headerlink visually - Stop manipulating headerlink nodes — Sphinx adds ¶ as raw HTML in depart_desc_signature, so it naturally appears after sig children - Remove old per-element CSS order rules (replaced by toolbar grouping) - Add test_inject_badges_headerlink_not_in_toolbar ensuring ¶ stays as a direct child of sig and never enters the toolbar - Update existing tests for new toolbar node structure
1 parent 2755e95 commit 50d8b8a

File tree

4 files changed

+104
-39
lines changed

4 files changed

+104
-39
lines changed

packages/sphinx-autodoc-api-style/src/sphinx_autodoc_api_style/_css.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class _CSS:
3232
>>> _CSS.BADGE_GROUP
3333
'gas-badge-group'
3434
35+
>>> _CSS.TOOLBAR
36+
'gas-toolbar'
37+
3538
>>> _CSS.obj_type("class")
3639
'gas-type-class'
3740
"""
@@ -50,6 +53,8 @@ class _CSS:
5053
MOD_FINAL = f"{PREFIX}-mod-final"
5154
DEPRECATED = f"{PREFIX}-deprecated"
5255

56+
TOOLBAR = f"{PREFIX}-toolbar"
57+
5358
@staticmethod
5459
def obj_type(name: str) -> str:
5560
"""Return the type-specific CSS class, e.g. ``gas-type-function``.

packages/sphinx-autodoc-api-style/src/sphinx_autodoc_api_style/_static/css/api_style.css

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,26 +191,31 @@ body[data-theme="dark"] {
191191
--gas-deprecated-border: #c06060;
192192
}
193193

194-
/* ── Badge group container ─────────────────────────────── */
194+
/* ── Signature flex layout ─────────────────────────────── */
195195
dl.py:not(.fixture) > dt {
196196
display: flex;
197197
align-items: center;
198198
gap: 0.35rem;
199199
flex-wrap: wrap;
200200
}
201201

202-
dl.py:not(.fixture) > dt > .headerlink { order: 1; }
203-
dl.py:not(.fixture) > dt > .gas-badge-group { order: 2; }
204-
dl.py:not(.fixture) > dt > a.reference.external { order: 3; }
205-
206-
dl.py:not(.fixture) > dt .gas-badge-group {
202+
/* ── Toolbar: badges + [source] ────────────────────────── */
203+
dl.py:not(.fixture) > dt .gas-toolbar {
207204
display: inline-flex;
208205
align-items: center;
209-
gap: 0.3rem;
206+
gap: 0.35rem;
210207
flex-shrink: 0;
211208
margin-left: auto;
212209
white-space: nowrap;
213210
text-indent: 0;
211+
order: 99;
212+
}
213+
214+
dl.py:not(.fixture) > dt .gas-badge-group {
215+
display: inline-flex;
216+
align-items: center;
217+
gap: 0.3rem;
218+
white-space: nowrap;
214219
}
215220

216221
/* ── Shared badge base ─────────────────────────────────── */

packages/sphinx-autodoc-api-style/src/sphinx_autodoc_api_style/_transforms.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from sphinx.writers.html5 import HTML5Translator
1616

1717
from sphinx_autodoc_api_style._badges import build_badge_group
18+
from sphinx_autodoc_api_style._css import _CSS
1819

1920
if t.TYPE_CHECKING:
2021
from sphinx.application import Sphinx
@@ -132,11 +133,11 @@ def _detect_deprecated(desc_node: addnodes.desc) -> bool:
132133

133134

134135
def _inject_badges(sig_node: addnodes.desc_signature, objtype: str) -> None:
135-
"""Inject badges and reorder signature children.
136+
"""Inject a toolbar containing badges, viewcode, and headerlink.
136137
137-
Appends a badge group to *sig_node* and reorders the headerlink and
138-
viewcode link so the visual layout is:
139-
``name -> return -> headerlink -> badges (right-aligned) -> [source]``.
138+
Builds a toolbar container (``gas-toolbar``) that groups the badge
139+
group, ``[source]`` link, and permalink into a single flex item so
140+
they stay together on the right side of the signature header.
140141
141142
Guarded by ``gas_badges_injected`` flag.
142143
@@ -168,25 +169,24 @@ def _inject_badges(sig_node: addnodes.desc_signature, objtype: str) -> None:
168169
badge_group = build_badge_group(objtype, modifiers=mods)
169170

170171
viewcode_ref = None
171-
headerlink_ref = None
172172
for child in list(sig_node.children):
173-
if isinstance(child, nodes.reference):
174-
if child.get("internal") is not True and any(
173+
if (
174+
isinstance(child, nodes.reference)
175+
and child.get("internal") is not True
176+
and any(
175177
"viewcode-link" in getattr(gc, "get", lambda *_: "")("classes", [])
176178
for gc in child.children
177179
if isinstance(gc, nodes.inline)
178-
):
179-
viewcode_ref = child
180-
sig_node.remove(child)
181-
elif "headerlink" in child.get("classes", []):
182-
headerlink_ref = child
183-
sig_node.remove(child)
184-
185-
if headerlink_ref is not None:
186-
sig_node += headerlink_ref
187-
sig_node += badge_group
180+
)
181+
):
182+
viewcode_ref = child
183+
sig_node.remove(child)
184+
185+
toolbar = nodes.inline(classes=[_CSS.TOOLBAR])
186+
toolbar += badge_group
188187
if viewcode_ref is not None:
189-
sig_node += viewcode_ref
188+
toolbar += viewcode_ref
189+
sig_node += toolbar
190190

191191

192192
def on_doctree_resolved(

tests/ext/api_style/test_api_style.py

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -304,17 +304,20 @@ def test_inject_badges_idempotent() -> None:
304304
assert badge_count_1 == badge_count_2
305305

306306

307-
def test_inject_badges_adds_badge_group() -> None:
308-
"""Badge group is added to the signature."""
307+
def test_inject_badges_adds_toolbar_with_badge_group() -> None:
308+
"""Toolbar containing badge group is added to the signature."""
309309
sig = addnodes.desc_signature()
310310
sig += addnodes.desc_name("", "my_func")
311311
_inject_badges(sig, "function")
312-
groups = [
312+
toolbars = [
313313
c
314314
for c in sig.children
315-
if isinstance(c, nodes.inline) and _CSS.BADGE_GROUP in c.get("classes", [])
315+
if isinstance(c, nodes.inline) and _CSS.TOOLBAR in c.get("classes", [])
316316
]
317-
assert len(groups) == 1
317+
assert len(toolbars) == 1
318+
groups = list(toolbars[0].findall(nodes.inline))
319+
badge_groups = [g for g in groups if _CSS.BADGE_GROUP in g.get("classes", [])]
320+
assert len(badge_groups) == 1
318321

319322

320323
def test_inject_badges_detects_deprecated_parent() -> None:
@@ -336,8 +339,8 @@ def test_inject_badges_detects_deprecated_parent() -> None:
336339
assert "deprecated" in labels
337340

338341

339-
def test_inject_badges_reorders_viewcode() -> None:
340-
"""Viewcode [source] link is moved after badge group."""
342+
def test_inject_badges_toolbar_contains_viewcode() -> None:
343+
"""Viewcode [source] link is inside the toolbar, after badge group."""
341344
sig = addnodes.desc_signature()
342345
sig += addnodes.desc_name("", "my_func")
343346
viewcode_span = nodes.inline(classes=["viewcode-link"])
@@ -347,14 +350,66 @@ def test_inject_badges_reorders_viewcode() -> None:
347350

348351
_inject_badges(sig, "function")
349352

350-
# Badge group should come before viewcode ref
351-
children_classes: list[str] = []
352-
for c in sig.children:
353+
toolbars = [
354+
c
355+
for c in sig.children
356+
if isinstance(c, nodes.inline) and _CSS.TOOLBAR in c.get("classes", [])
357+
]
358+
assert len(toolbars) == 1
359+
toolbar = toolbars[0]
360+
361+
toolbar_items: list[str] = []
362+
for c in toolbar.children:
353363
if isinstance(c, nodes.inline) and _CSS.BADGE_GROUP in c.get("classes", []):
354-
children_classes.append("badge_group")
355-
elif isinstance(c, nodes.reference) and c.get("internal") is not True:
356-
children_classes.append("viewcode")
357-
assert children_classes.index("badge_group") < children_classes.index("viewcode")
364+
toolbar_items.append("badge_group")
365+
elif isinstance(c, nodes.reference):
366+
toolbar_items.append("viewcode")
367+
assert toolbar_items == ["badge_group", "viewcode"]
368+
369+
370+
def test_inject_badges_headerlink_not_in_toolbar() -> None:
371+
"""Headerlink stays as a direct child of sig, never inside the toolbar.
372+
373+
Sphinx's HTML writer adds the headerlink as raw HTML during
374+
``depart_desc_signature``, so it's not a doctree node during our
375+
transform. But if a theme or extension adds one as a node, we must
376+
leave it alone — it belongs next to the signature name, not grouped
377+
with badges and [source] in the toolbar.
378+
"""
379+
sig = addnodes.desc_signature()
380+
sig += addnodes.desc_name("", "Server")
381+
382+
headerlink = nodes.reference(
383+
"", "\u00b6", refuri="#libtmux.Server", classes=["headerlink"]
384+
)
385+
sig += headerlink
386+
387+
viewcode_span = nodes.inline(classes=["viewcode-link"])
388+
viewcode_span += nodes.Text("[source]")
389+
viewcode_ref = nodes.reference("", "", viewcode_span, internal=False)
390+
sig += viewcode_ref
391+
392+
_inject_badges(sig, "class")
393+
394+
toolbar = None
395+
for c in sig.children:
396+
if isinstance(c, nodes.inline) and _CSS.TOOLBAR in c.get("classes", []):
397+
toolbar = c
398+
break
399+
assert toolbar is not None
400+
401+
toolbar_refs = list(toolbar.findall(nodes.reference))
402+
for ref in toolbar_refs:
403+
assert "headerlink" not in ref.get("classes", []), (
404+
"headerlink must not be inside the toolbar"
405+
)
406+
407+
sig_direct_refs = [
408+
c
409+
for c in sig.children
410+
if isinstance(c, nodes.reference) and "headerlink" in c.get("classes", [])
411+
]
412+
assert len(sig_direct_refs) == 1, "headerlink should remain a direct child of sig"
358413

359414

360415
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)