Skip to content

Commit

Permalink
refactor: Backup anchors with id and no href, for compatibility with …
Browse files Browse the repository at this point in the history
…autorefs' Markdown anchors

PR-#651: #651
Related-to-mkdocs-autorefs#39: mkdocstrings/autorefs#39
Co-authored-by: Oleh Prypin <oleh@pryp.in>
  • Loading branch information
pawamoy and oprypin committed Feb 22, 2024
1 parent 628f3af commit b5236b4
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 8 deletions.
32 changes: 24 additions & 8 deletions src/mkdocstrings/handlers/rendering.py
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)
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)
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
@@ -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
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

0 comments on commit b5236b4

Please sign in to comment.