Skip to content

Commit b5236b4

Browse files
pawamoyoprypin
andauthored
refactor: Backup anchors with id and no href, for compatibility with autorefs' Markdown anchors
PR-#651: #651 Related-to-mkdocs-autorefs#39: mkdocstrings/autorefs#39 Co-authored-by: Oleh Prypin <oleh@pryp.in>
1 parent 628f3af commit b5236b4

3 files changed

Lines changed: 87 additions & 8 deletions

File tree

src/mkdocstrings/handlers/rendering.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,19 +146,35 @@ def __init__(self, md: Markdown, id_prefix: str):
146146
self.id_prefix = id_prefix
147147

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

152+
def _prefix_ids(self, root: Element) -> None:
153+
index = len(root)
154+
for el in reversed(root): # Reversed mainly for the ability to mutate during iteration.
155+
index -= 1
156+
157+
self._prefix_ids(el)
156158
href_attr = el.get("href")
159+
160+
if id_attr := el.get("id"):
161+
if el.tag == "a" and not href_attr:
162+
# An anchor with id and no href is used by autorefs:
163+
# leave it untouched and insert a copy with updated id after it.
164+
new_el = copy.deepcopy(el)
165+
new_el.set("id", self.id_prefix + id_attr)
166+
root.insert(index + 1, new_el)
167+
else:
168+
# Anchors with id and href are not used by autorefs:
169+
# update in place.
170+
el.set("id", self.id_prefix + id_attr)
171+
172+
# Always update hrefs, names and labels-for:
173+
# there will always be a corresponding id.
157174
if href_attr and href_attr.startswith("#"):
158175
el.set("href", "#" + self.id_prefix + href_attr[1:])
159176

160-
name_attr = el.get("name")
161-
if name_attr:
177+
if name_attr := el.get("name"):
162178
el.set("name", self.id_prefix + name_attr)
163179

164180
if el.tag == "label":

tests/fixtures/markdown_anchors.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Module docstring.
2+
3+
[](){#anchor}
4+
5+
Paragraph.
6+
7+
[](){#heading-anchor-1}
8+
[](){#heading-anchor-2}
9+
[](){#heading-anchor-3}
10+
## Heading
11+
12+
[](#has-href1)
13+
[](#has-href2){#with-id}
14+
15+
Pararaph.
16+
"""

tests/test_extension.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,50 @@ def test_removing_duplicated_headings(ext_markdown: Markdown) -> None:
172172
assert output.count(">Heading two<") == 1
173173
assert output.count(">Heading three<") == 1
174174
assert output.count('class="mkdocstrings') == 0
175+
176+
177+
def _assert_contains_in_order(items: list[str], string: str) -> None:
178+
index = 0
179+
for item in items:
180+
assert item in string[index:]
181+
index = string.index(item, index) + len(item)
182+
183+
184+
@pytest.mark.parametrize("ext_markdown", [{"markdown_extensions": [{"attr_list": {}}]}], indirect=["ext_markdown"])
185+
def test_backup_of_anchors(ext_markdown: Markdown) -> None:
186+
"""Anchors with empty `href` are backed up."""
187+
output = ext_markdown.convert("::: tests.fixtures.markdown_anchors")
188+
189+
# Anchors with id and no href have been backed up and updated.
190+
_assert_contains_in_order(
191+
[
192+
'id="anchor"',
193+
'id="tests.fixtures.markdown_anchors--anchor"',
194+
'id="heading-anchor-1"',
195+
'id="tests.fixtures.markdown_anchors--heading-anchor-1"',
196+
'id="heading-anchor-2"',
197+
'id="tests.fixtures.markdown_anchors--heading-anchor-2"',
198+
'id="heading-anchor-3"',
199+
'id="tests.fixtures.markdown_anchors--heading-anchor-3"',
200+
],
201+
output,
202+
)
203+
204+
# Anchors with href and with or without id have been updated but not backed up.
205+
_assert_contains_in_order(
206+
[
207+
'id="tests.fixtures.markdown_anchors--with-id"',
208+
],
209+
output,
210+
)
211+
assert 'id="with-id"' not in output
212+
213+
_assert_contains_in_order(
214+
[
215+
'href="#tests.fixtures.markdown_anchors--has-href1"',
216+
'href="#tests.fixtures.markdown_anchors--has-href2"',
217+
],
218+
output,
219+
)
220+
assert 'href="#has-href1"' not in output
221+
assert 'href="#has-href2"' not in output

0 commit comments

Comments
 (0)