Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions myst_parser/sphinx_ext/myst_refs.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,22 +146,25 @@ def run(self, **kwargs: Any) -> None:

node.replace_self(newnode)

def _std_label_id_in_doc(self, docname: str, ref_id: str) -> str | None:
def _std_label_in_doc(self, docname: str, ref_id: str) -> tuple[str, str] | None:
"""Resolve ``ref_id`` against the std-domain labels of ``docname``.

Returns the label's id (the actual anchor) if ``ref_id`` is either
a label id or a label name in that document, else None.
:param docname: The document to search for the label.
:param ref_id: A label id or a label name in that document.
:return: The label's id (the actual anchor) and the title of the
section it names (empty for anonymous labels), or None.
"""
std = self.env.domaindata.get("std", {})
for store in ("labels", "anonlabels"):
for name, entry in std.get(store, {}).items():
if entry[0] != docname:
continue
title = entry[2] if len(entry) > 2 else ""
if entry[1] == ref_id:
return ref_id
return ref_id, title
if name == ref_id:
# referenced by label name: point at its actual anchor
return entry[1]
return entry[1], title
return None

def resolve_myst_ref_doc(self, node: pending_xref):
Expand Down Expand Up @@ -190,14 +193,24 @@ def resolve_myst_ref_doc(self, node: pending_xref):
slug_to_section = self.env.metadata[ref_docname].get("myst_slugs", {})
if ref_id in slug_to_section:
_, targetid, implicit_text = slug_to_section[ref_id]
elif any(sect_id == ref_id for _, sect_id, _ in slug_to_section.values()):
elif (
section_text := next(
(
text
for _, sect_id, text in slug_to_section.values()
if sect_id == ref_id
),
None,
)
) is not None:
# the id demonstrably exists in the target document
# (a section's docutils id), so resolve silently
targetid = ref_id
elif (std_id := self._std_label_id_in_doc(ref_docname, ref_id)) is not None:
implicit_text = section_text
elif (std_label := self._std_label_in_doc(ref_docname, ref_id)) is not None:
# an explicit target in the document, referenced by its
# id or name: point at its actual anchor
targetid = std_id
targetid, implicit_text = std_label
else:
self.log_warning(
ref_id,
Expand All @@ -214,10 +227,16 @@ def resolve_myst_ref_doc(self, node: pending_xref):
caption = node.astext()
innernode = nodes.inline(caption, "", classes=inner_classes)
innernode.extend(node[0].children)
else:
elif implicit_text:
innernode = nodes.inline(
implicit_text, implicit_text, classes=inner_classes
)
else:
# no title to use (e.g. an anonymous label, or an unresolved id),
# so show the target rather than rendering an empty link
innernode = nodes.inline("", "", classes=inner_classes)
shown = ref_id or ref_docname
innernode += nodes.literal(shown, shown)

assert self.app.builder
try:
Expand Down
39 changes: 39 additions & 0 deletions tests/test_renderers/test_myst_refs.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,42 @@ def test_slug_id_stays_secondary_under_sortids(sphinx_doctree: CreateDoctree):
for section in doctree.findall(docutils_nodes.section):
assert section["ids"][0].startswith("id"), section["ids"]
assert section["slug"] in section["ids"][1:], section["ids"]


@pytest.mark.parametrize(
"link,expected",
[
# an explicit label on a heading, which becomes the section's id
("[](other.md#my-label)", "Labelled heading"),
# a label referenced by name, rather than by its id
("[](other.md#colon:label)", "Colon heading"),
("[](other.md#colon-label)", "Colon heading"),
# explicit link text is kept
("[custom](other.md#my-label)", "custom"),
# an anonymous label has no title, so the target is shown
("[](other.md#para-label)", "para-label"),
],
)
def test_doc_with_target_link_text(
link: str, expected: str, sphinx_doctree: CreateDoctree
):
"""`[](doc.md#target)` to an explicit target uses the target's title.

Regression: links that resolved through a label or a section id, rather
than a heading slug, rendered with no link text at all.
"""
from docutils import nodes as docutils_nodes

sphinx_doctree.set_conf({"extensions": ["myst_parser"], "myst_heading_anchors": 2})
sphinx_doctree.srcdir.joinpath("other.md").write_text(
"# Other\n\n"
"(my-label)=\n## Labelled heading\n\n"
"(colon:label)=\n## Colon heading\n\n"
"(para-label)=\nA paragraph.\n",
encoding="utf8",
)
result = sphinx_doctree(f"# Index\n\n{link}\n", "index.md")
assert not result.warnings
doctree = result.get_resolved_doctree("index")
ref = next(doctree.findall(docutils_nodes.reference))
assert ref.astext() == expected, ref.pformat()
1 change: 1 addition & 0 deletions tests/test_renderers/test_myst_refs/doc_with_target_id.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
<paragraph>
<reference internal="True" refid="ref">
<inline classes="std std-ref">
Title
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
<paragraph>
<reference internal="True" refid="ref-colon">
<inline classes="std std-ref">
Title