From dd64f3737f136f4296cf639a1185f1d964a2cc8f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 14:16:32 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20keep=20target=20anchors?= =?UTF-8?q?=20rendered=20in=20tab-set=20and=20tab-set-code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #271, which preserved target nodes but not their anchors: - docutils PropagateTargets moves a preceding target's ids onto the tab-item container, which TabSetHtmlTransform destroys — copy the container's ids onto the rendered label so href targets exist - tab-set-code dropped targets entirely (asymmetric with tab-set) — preserve them the same way - strengthen the target test to assert a rendered node carries the id and that it appears in the built HTML (the previous assertions passed against the broken output); add a tab-set-code variant - get_started: clarify that examples may need extending myst_enable_extensions beyond the snippet shown --- docs/get_started.md | 2 +- sphinx_design/tabs.py | 10 ++++++++++ tests/test_misc.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/get_started.md b/docs/get_started.md index f429116..b2ab95b 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -25,7 +25,7 @@ myst_enable_extensions = ["colon_fence"] ``` :::{note} -The MyST Markdown examples in this documentation assume that certain optional [MyST syntax extensions](https://myst-parser.readthedocs.io/en/latest/syntax/optional.html) have been enabled, *via* the `myst_enable_extensions` configuration above: +The MyST Markdown examples in this documentation assume that certain optional [MyST syntax extensions](https://myst-parser.readthedocs.io/en/latest/syntax/optional.html) are enabled — extend the `myst_enable_extensions` list shown above with them as needed: - `colon_fence`: used by all examples, to write directives delimited by `:::` fences - `html_image`: only for examples using raw HTML `` tags, such as the avatar images in [CSS Classes](./css_classes.md) diff --git a/sphinx_design/tabs.py b/sphinx_design/tabs.py index 03ba1e5..d1e9261 100644 --- a/sphinx_design/tabs.py +++ b/sphinx_design/tabs.py @@ -159,6 +159,11 @@ def run_with_defaults(self) -> list[nodes.Node]: new_children = [] for item in tab_set.children: if is_ignorable_child(item): + # comments and system messages can be safely dropped, + # but hyperlink targets must be kept, + # so that references to them still resolve + if isinstance(item, nodes.target): + new_children.append(item) continue if not isinstance(item, nodes.literal_block): LOGGER.warning( @@ -304,6 +309,11 @@ def run(self, **kwargs: Any) -> None: ) if tab_label.get("ids"): label_node["ids"] += tab_label["ids"] + if tab_item.get("ids"): + # ids propagated onto the container (e.g. from a preceding + # hyperlink target, via docutils PropagateTargets) must + # survive the container's removal, or anchors break + label_node["ids"] += tab_item["ids"] if "sync_group" in tab_label and "sync_id" in tab_label: label_node["sync_group"] = tab_label["sync_group"] label_node["sync_id"] = tab_label["sync_id"] diff --git a/tests/test_misc.py b/tests/test_misc.py index 162dee9..c9f351b 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -337,6 +337,20 @@ def test_tab_set_with_target(fmt, sphinx_builder): # the reference resolves to the target references = list(findall(doctree)(nodes.reference)) assert len(references) == 1 + # crucially, some rendered node must still CARRY the id + # (docutils PropagateTargets moves it onto the tab-item container, + # which the HTML transform destroys), or the anchor breaks + refid = references[0]["refid"] + carriers = [ + node + for node in findall(doctree)() + if isinstance(node, nodes.Element) + and not isinstance(node, nodes.target) + and refid in node.get("ids", []) + ] + assert carriers, f"no rendered node carries id {refid!r}" + html = (builder.out_path / "index.html").read_text(encoding="utf8") + assert f'id="{refid}"' in html assert references[0]["refid"] == "my-target" @@ -363,6 +377,37 @@ def test_tab_set_with_paragraph_warns(sphinx_builder): assert "[design.tab]" in builder.warnings +TAB_SET_CODE_WITH_TARGET = """ +Test +==== + +:ref:`my code ` + +.. tab-set-code:: + + .. _code-target: + + .. code-block:: python + + a = 1 +""" + + +def test_tab_set_code_with_target(sphinx_builder): + """A hyperlink target inside a tab-set-code must keep a working anchor.""" + builder = sphinx_builder(conf_kwargs={"extensions": ["sphinx_design"]}) + builder.src_path.joinpath("index.rst").write_text( + TAB_SET_CODE_WITH_TARGET, encoding="utf8" + ) + builder.build() # asserts no warnings + doctree = builder.get_doctree("index", post_transforms=True) + references = [ref for ref in findall(doctree)(nodes.reference) if ref.get("refid")] + assert len(references) == 1 + refid = references[0]["refid"] + html = (builder.out_path / "index.html").read_text(encoding="utf8") + assert f'id="{refid}"' in html + + I18N_INDEX_RST = """\ Heading =======