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
=======