Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Backup anchors without hrefs, for compatibility with autorefs' Markdown anchors #651

Merged
merged 9 commits into from
Feb 22, 2024
32 changes: 24 additions & 8 deletions src/mkdocstrings/handlers/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,19 +146,35 @@ def __init__(self, md: Markdown, id_prefix: str):
self.id_prefix = id_prefix

def run(self, root: Element) -> None: # noqa: D102 (ignore missing docstring)
if not self.id_prefix:
return
for el in root.iter():
id_attr = el.get("id")
if id_attr:
el.set("id", self.id_prefix + id_attr)
if self.id_prefix:
self._prefix_ids(root)

def _prefix_ids(self, root: Element) -> None:
index = len(root)
for el in reversed(root): # Reversed mainly for the ability to mutate during iteration.
index -= 1

self._prefix_ids(el)
oprypin marked this conversation as resolved.
Show resolved Hide resolved
href_attr = el.get("href")

if id_attr := el.get("id"):
if el.tag == "a" and not href_attr:
# An anchor with id and no href is used by autorefs:
# leave it untouched and insert a copy with updated id after it.
new_el = copy.deepcopy(el)
oprypin marked this conversation as resolved.
Show resolved Hide resolved
new_el.set("id", self.id_prefix + id_attr)
root.insert(index + 1, new_el)
else:
# Anchors with id and href are not used by autorefs:
# update in place.
el.set("id", self.id_prefix + id_attr)

# Always update hrefs, names and labels-for:
# there will always be a corresponding id.
if href_attr and href_attr.startswith("#"):
el.set("href", "#" + self.id_prefix + href_attr[1:])

name_attr = el.get("name")
if name_attr:
if name_attr := el.get("name"):
el.set("name", self.id_prefix + name_attr)

if el.tag == "label":
Expand Down
16 changes: 16 additions & 0 deletions tests/fixtures/markdown_anchors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Module docstring.

[](){#anchor}

Paragraph.

[](){#heading-anchor-1}
[](){#heading-anchor-2}
[](){#heading-anchor-3}
## Heading

[](#has-href1)
[](#has-href2){#with-id}

Pararaph.
"""
47 changes: 47 additions & 0 deletions tests/test_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,50 @@ def test_removing_duplicated_headings(ext_markdown: Markdown) -> None:
assert output.count(">Heading two<") == 1
assert output.count(">Heading three<") == 1
assert output.count('class="mkdocstrings') == 0


def _assert_contains_in_order(items: list[str], string: str) -> None:
index = 0
for item in items:
assert item in string[index:]
index = string.index(item, index) + len(item)


@pytest.mark.parametrize("ext_markdown", [{"markdown_extensions": [{"attr_list": {}}]}], indirect=["ext_markdown"])
def test_backup_of_anchors(ext_markdown: Markdown) -> None:
"""Anchors with empty `href` are backed up."""
output = ext_markdown.convert("::: tests.fixtures.markdown_anchors")

# Anchors with id and no href have been backed up and updated.
_assert_contains_in_order(
[
'id="anchor"',
'id="tests.fixtures.markdown_anchors--anchor"',
'id="heading-anchor-1"',
'id="tests.fixtures.markdown_anchors--heading-anchor-1"',
'id="heading-anchor-2"',
'id="tests.fixtures.markdown_anchors--heading-anchor-2"',
'id="heading-anchor-3"',
'id="tests.fixtures.markdown_anchors--heading-anchor-3"',
],
output,
)

# Anchors with href and with or without id have been updated but not backed up.
_assert_contains_in_order(
[
'id="tests.fixtures.markdown_anchors--with-id"',
],
output,
)
assert 'id="with-id"' not in output

_assert_contains_in_order(
[
'href="#tests.fixtures.markdown_anchors--has-href1"',
'href="#tests.fixtures.markdown_anchors--has-href2"',
],
output,
)
assert 'href="#has-href1"' not in output
assert 'href="#has-href2"' not in output