Skip to content

Commit 679193f

Browse files
committed
feat: Add strip_title_tags option
The plugin records heading titles, and sets them as `title` attribute of cross-references (HTML links). Sometimes these titles contain HTML, and some MkDocs themes do not support HTML in them (they are shown as tooltips when hovering on links). This option allows to strip tags (while keeping text) from titles. When set to `"auto"` (default value), it only strips tags for unsupported themes (all except Material for MkDocs). Issue-33: #33
1 parent 42698bf commit 679193f

File tree

4 files changed

+92
-5
lines changed

4 files changed

+92
-5
lines changed

Diff for: src/mkdocs_autorefs/plugin.py

+18
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ class AutorefsConfig(Config):
7676
optionally appending the identifier for API objects.
7777
"""
7878

79+
strip_title_tags: bool | Literal["auto"] = Choice((True, False, "auto"), default="auto") # type: ignore[assignment]
80+
"""Whether to strip HTML tags from link titles.
81+
82+
Some themes support HTML in link titles, but others do not.
83+
84+
- `"auto"`: strip tags unless the Material for MkDocs theme is detected.
85+
"""
86+
7987

8088
class AutorefsPlugin(BasePlugin[AutorefsConfig]):
8189
"""The `autorefs` plugin for `mkdocs`.
@@ -132,6 +140,7 @@ def __init__(self) -> None:
132140
self._get_fallback_anchor: Callable[[str], tuple[str, ...]] | None = None
133141

134142
self._link_titles: bool | Literal["external"] = True
143+
self._strip_title_tags: bool = False
135144

136145
# YORE: Bump 2: Remove block.
137146
@property
@@ -313,6 +322,14 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
313322
else:
314323
self._link_titles = self.config.link_titles
315324

325+
if self.config.strip_title_tags == "auto":
326+
if getattr(config.theme, "name", None) == "material":
327+
self._strip_title_tags = False
328+
else:
329+
self._strip_title_tags = True
330+
else:
331+
self._strip_title_tags = self.config.strip_title_tags
332+
316333
return config
317334

318335
def on_page_markdown(self, markdown: str, page: Page, **kwargs: Any) -> str: # noqa: ARG002
@@ -399,6 +416,7 @@ def on_env(self, env: Environment, /, *, config: MkDocsConfig, files: Files) ->
399416
file.page.content,
400417
url_mapper,
401418
link_titles=self._link_titles,
419+
strip_title_tags=self._strip_title_tags,
402420
_legacy_refs=self.legacy_refs,
403421
)
404422

Diff for: src/mkdocs_autorefs/references.py

+34-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from functools import lru_cache
1111
from html import escape, unescape
1212
from html.parser import HTMLParser
13+
from io import StringIO
1314
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal
1415
from urllib.parse import urlsplit
1516
from xml.etree.ElementTree import Element
@@ -376,22 +377,48 @@ def _find_url(
376377
raise KeyError(f"None of the identifiers {identifiers} were found")
377378

378379

379-
def _tooltip(identifier: str, title: str | None) -> str:
380+
def _tooltip(identifier: str, title: str | None, *, strip_tags: bool = False) -> str:
380381
if title:
381382
# Don't append identifier if it's already in the title.
382383
if identifier in title:
383384
return title
384385
# Append identifier (useful for API objects).
386+
if strip_tags:
387+
return f"{title} ({identifier})"
385388
return f"{title} (<code>{identifier}</code>)"
386389
# No title, just return the identifier.
390+
if strip_tags:
391+
return identifier
387392
return f"<code>{identifier}</code>"
388393

389394

395+
class _TagStripper(HTMLParser):
396+
def __init__(self) -> None:
397+
super().__init__()
398+
self.reset()
399+
self.strict = False
400+
self.convert_charrefs = True
401+
self.text = StringIO()
402+
403+
def handle_data(self, data: str) -> None:
404+
self.text.write(data)
405+
406+
def get_data(self) -> str:
407+
return self.text.getvalue()
408+
409+
410+
def _strip_tags(html: str) -> str:
411+
stripper = _TagStripper()
412+
stripper.feed(html)
413+
return stripper.get_data()
414+
415+
390416
def fix_ref(
391417
url_mapper: Callable[[str], tuple[str, str | None]],
392418
unmapped: list[tuple[str, AutorefsHookInterface.Context | None]],
393419
*,
394420
link_titles: bool | Literal["external"] = True,
421+
strip_title_tags: bool = False,
395422
) -> Callable:
396423
"""Return a `repl` function for [`re.sub`](https://docs.python.org/3/library/re.html#re.sub).
397424
@@ -406,6 +433,7 @@ def fix_ref(
406433
such as [mkdocs_autorefs.plugin.AutorefsPlugin.get_item_url][].
407434
unmapped: A list to store unmapped identifiers.
408435
link_titles: How to set HTML titles on links. Always (`True`), never (`False`), or external-only (`"external"`).
436+
strip_title_tags: Whether to strip HTML tags from link titles.
409437
410438
Returns:
411439
The actual function accepting a [`Match` object](https://docs.python.org/3/library/re.html#match-objects)
@@ -449,14 +477,14 @@ def inner(match: Match) -> str:
449477
if optional:
450478
# The `optional` attribute is generally only added by mkdocstrings handlers,
451479
# for API objects, meaning we can and should append the full identifier.
452-
tooltip = _tooltip(identifier, original_title)
480+
tooltip = _tooltip(identifier, original_title, strip_tags=strip_title_tags)
453481
else:
454482
# Autorefs without `optional` are generally user-written ones,
455483
# so we should only use the original title.
456484
tooltip = original_title or ""
457485

458486
if tooltip and tooltip not in f"<code>{title}</code>":
459-
title_attr = f' title="{escape(tooltip)}"'
487+
title_attr = f' title="{_strip_tags(tooltip) if strip_title_tags else escape(tooltip)}"'
460488

461489
return f'<a class="{class_attr}"{title_attr} href="{escape(url)}"{remaining}>{title}</a>'
462490

@@ -468,6 +496,7 @@ def fix_refs(
468496
url_mapper: Callable[[str], tuple[str, str | None]],
469497
*,
470498
link_titles: bool | Literal["external"] = True,
499+
strip_title_tags: bool = False,
471500
# YORE: Bump 2: Remove line.
472501
_legacy_refs: bool = True,
473502
) -> tuple[str, list[tuple[str, AutorefsHookInterface.Context | None]]]:
@@ -478,13 +507,14 @@ def fix_refs(
478507
url_mapper: A callable that gets an object's site URL by its identifier,
479508
such as [mkdocs_autorefs.plugin.AutorefsPlugin.get_item_url][].
480509
link_titles: How to set HTML titles on links. Always (`True`), never (`False`), or external-only (`"external"`).
510+
strip_title_tags: Whether to strip HTML tags from link titles.
481511
482512
Returns:
483513
The fixed HTML, and a list of unmapped identifiers (string and optional context).
484514
"""
485515
unmapped: list[tuple[str, AutorefsHookInterface.Context | None]] = []
486516
html = AUTOREF_RE.sub(
487-
fix_ref(url_mapper, unmapped, link_titles=link_titles),
517+
fix_ref(url_mapper, unmapped, link_titles=link_titles, strip_title_tags=strip_title_tags),
488518
html,
489519
)
490520

Diff for: tests/test_plugin.py

+37
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ def test_use_closest_url(caplog: pytest.LogCaptureFixture, primary: bool) -> Non
128128
qualifier = "primary" if primary else "secondary"
129129
assert f"Multiple {qualifier} URLs found for 'foo': ['foo.html#foo', 'bar.html#foo']" not in caplog.text
130130

131+
131132
def test_on_config_hook() -> None:
132133
"""Check that the `on_config` hook runs without issue."""
133134
plugin = AutorefsPlugin()
@@ -175,3 +176,39 @@ def test_explicit_link_titles(link_titles: bool | Literal["external"]) -> None:
175176
plugin.on_config(config=MkDocsConfig())
176177
assert plugin._link_titles is link_titles
177178

179+
180+
def test_auto_strip_title_tags_false() -> None:
181+
"""Check that `strip_title_tags` is made false when Material is detected."""
182+
plugin = AutorefsPlugin()
183+
plugin.config = AutorefsConfig()
184+
plugin.config.strip_title_tags = "auto"
185+
config = MkDocsConfig()
186+
config.theme = Theme(name="material")
187+
plugin.on_config(config=config)
188+
assert plugin._strip_title_tags is False
189+
190+
191+
def test_auto_strip_title_tags_true() -> None:
192+
"""Check that `strip_title_tags` are made true when automatic and Material is not detected."""
193+
plugin = AutorefsPlugin()
194+
plugin.config = AutorefsConfig()
195+
plugin.config.strip_title_tags = "auto"
196+
config = MkDocsConfig()
197+
198+
config.theme = Theme("mkdocs")
199+
plugin.on_config(config=config)
200+
assert plugin._strip_title_tags is True
201+
202+
config.theme = Theme("readthedocs")
203+
plugin.on_config(config=config)
204+
assert plugin._strip_title_tags is True
205+
206+
207+
@pytest.mark.parametrize("strip_title_tags", [True, False])
208+
def test_explicit_strip_tags(strip_title_tags: bool) -> None:
209+
"""Check that explicit `_strip_title_tags` are kept unchanged."""
210+
plugin = AutorefsPlugin()
211+
plugin.config = AutorefsConfig()
212+
plugin.config.strip_title_tags = strip_title_tags
213+
plugin.on_config(config=MkDocsConfig())
214+
assert plugin._strip_title_tags is strip_title_tags

Diff for: tests/test_references.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def run_references_test(
5353
from_url: str = "page.html",
5454
extensions: Mapping[str, Mapping[str, Any]] | None = None,
5555
title_map: Mapping[str, str] | None = None,
56+
*,
57+
strip_tags: bool = True,
5658
) -> None:
5759
"""Help running tests about references.
5860
@@ -71,7 +73,7 @@ def run_references_test(
7173
def url_mapper(identifier: str) -> tuple[str, str | None]:
7274
return relative_url(from_url, url_map[identifier]), title_map.get(identifier, None)
7375

74-
actual_output, actual_unmapped = fix_refs(content, url_mapper)
76+
actual_output, actual_unmapped = fix_refs(content, url_mapper, strip_title_tags=strip_tags)
7577
assert actual_output == output
7678
assert actual_unmapped == (unmapped or [])
7779

0 commit comments

Comments
 (0)