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
19 changes: 19 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,25 @@ The [jinja template](https://jinja.palletsprojects.com) to use for formatting DO

### Miscellaneous

:::{confval} tippy_glossary_base_url
The base URL for glossary term links within tooltips. This fixes broken
links when using `:term:` references within glossary term definitions.

When a glossary term definition contains references to other terms (e.g.,
`:term:`Other Term``), those links use relative anchors like
`#term-Other-Term`. Without this configuration, clicking such links in
tooltips will try to navigate to the anchor on the current page instead
of the glossary page.

For example, if your glossary is in `glossary.rst`:

```python
tippy_glossary_base_url = "glossary.html"
```

This will rewrite tooltip links from `#term-Something` to `glossary.html#term-Something`.
:::

:::{confval} tippy_custom_tips
A dictionary, mapping URLs to HTML strings, which will be used to create custom tips.

Expand Down
48 changes: 39 additions & 9 deletions src/sphinx_tippy.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def setup(app: Sphinx):
[list, tuple],
)
app.add_config_value("tippy_add_class", "", "html")
app.add_config_value("tippy_glossary_base_url", "", "html")

app.connect("builder-inited", compile_config)
app.connect("html-page-context", collect_tips, priority=450) # before mathjax
Expand All @@ -108,6 +109,7 @@ class TippyConfig:
doi_api: str
js_files: tuple[str, ...]
tippy_add_class: str
glossary_base_url: str


def get_tippy_config(app: Sphinx) -> TippyConfig:
Expand Down Expand Up @@ -197,12 +199,12 @@ def compile_config(app: Sphinx):
doi_api=app.config.tippy_doi_api,
js_files=app.config.tippy_js,
tippy_add_class=app.config.tippy_add_class,
glossary_base_url=app.config.tippy_glossary_base_url,
)
if app.builder.name != "html":
return
if (
app.config.tippy_enable_mathjax
and app.builder.math_renderer_name != "mathjax" # type: ignore[attr-defined]
app.config.tippy_enable_mathjax and app.builder.math_renderer_name != "mathjax" # type: ignore[attr-defined]
):
raise ExtensionError("tippy_enable_mathjax=True requires mathjax to be enabled")

Expand Down Expand Up @@ -478,6 +480,31 @@ def _get_header_html(header: Tag | NavigableString, _start: bool = True) -> str:


SCHEMA_REGEX = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*:")
GLOSSARY_TERM_REGEX = re.compile(r"^#term-")


def rewrite_glossary_term_links(content: str, glossary_base_url: str) -> str:
"""Rewrite glossary term anchor links to use the glossary base URL.

When tooltip content contains relative links like #term-Something,
they need to be rewritten to point to the glossary page instead of
the current page.

Args:
content: The HTML content of the tooltip.
glossary_base_url: The base URL for glossary terms (e.g., "glossary.html").

Returns:
The HTML content with glossary term links rewritten.
"""
if not glossary_base_url:
return content
soup = BeautifulSoup(content, "html.parser")
for anchor in soup.find_all("a", {"href": True}):
href = anchor.get("href", "")
if isinstance(href, str) and GLOSSARY_TERM_REGEX.match(href):
anchor["href"] = glossary_base_url + href
return str(soup)


def rewrite_local_attrs(content: str, rel_path: str) -> str:
Expand Down Expand Up @@ -694,13 +721,18 @@ def write_tippy_props_page(
tippy_page_data[refpage]["element_id_map"][target]
]
html_str = rewrite_local_attrs(html_str, relfolder)
html_str = rewrite_glossary_term_links(
html_str, tippy_config.glossary_base_url
)
selector_to_html[f'a[href="{relpage}.html#{target}"]'] = html_str
elif target is None:
selector_to_html['a[href="#"]'] = local_id_to_html[None]
elif target in local_id_map and local_id_map[target] in local_id_to_html:
selector_to_html[f'a[href="#{target}"]'] = local_id_to_html[
local_id_map[target]
]
html_str = local_id_to_html[local_id_map[target]]
html_str = rewrite_glossary_term_links(
html_str, tippy_config.glossary_base_url
)
selector_to_html[f'a[href="#{target}"]'] = html_str

# custom tips take priority over other tips
selector_to_html.update(
Expand All @@ -713,11 +745,9 @@ def write_tippy_props_page(
pselector = tippy_config.anchor_parent_selector
mathjax = (
(
"onShow(instance) "
"{MathJax.typesetPromise([instance.popper]).then(() => {});},"
"onShow(instance) {MathJax.typesetPromise([instance.popper]).then(() => {});},"
)
if tippy_config.enable_mathjax
and app.builder.math_renderer_name == "mathjax" # type: ignore[attr-defined]
if tippy_config.enable_mathjax and app.builder.math_renderer_name == "mathjax" # type: ignore[attr-defined]
else ""
)
# TODO need to only enable when math,
Expand Down
60 changes: 60 additions & 0 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from sphinx_pytest.plugin import CreateDoctree

from sphinx_tippy import rewrite_glossary_term_links


def test_basic(sphinx_doctree: CreateDoctree, data_regression):
sphinx_doctree.set_conf({"extensions": ["sphinx_tippy"]})
Expand All @@ -22,3 +24,61 @@ def test_basic(sphinx_doctree: CreateDoctree, data_regression):
for value in result.app.env.tippy_data["pages"].values():
value.pop("js_path", None)
data_regression.check(result.app.env.tippy_data)


def test_rewrite_glossary_term_links_empty_base_url():
"""Test that no rewriting happens when glossary_base_url is empty."""
content = '<p>See <a href="#term-Other">Other</a></p>'
result = rewrite_glossary_term_links(content, "")
assert "#term-Other" in result


def test_rewrite_glossary_term_links_with_base_url():
"""Test that glossary term links are rewritten with the base URL."""
content = '<p>See <a href="#term-Compound-Operation">Compound Operation</a></p>'
result = rewrite_glossary_term_links(content, "glossary.html")
assert 'href="glossary.html#term-Compound-Operation"' in result


def test_rewrite_glossary_term_links_preserves_other_links():
"""Test that non-glossary links are preserved."""
content = """<p>
<a href="#term-Something">Term</a>
<a href="#other-anchor">Other</a>
<a href="page.html#section">External</a>
</p>"""
result = rewrite_glossary_term_links(content, "glossary.html")
assert 'href="glossary.html#term-Something"' in result
assert 'href="#other-anchor"' in result
assert 'href="page.html#section"' in result


def test_rewrite_glossary_term_links_multiple_terms():
"""Test that multiple glossary term links are all rewritten."""
content = """<p>
<a href="#term-First">First</a> and
<a href="#term-Second">Second</a>
</p>"""
result = rewrite_glossary_term_links(content, "glossary.html")
assert 'href="glossary.html#term-First"' in result
assert 'href="glossary.html#term-Second"' in result


def test_glossary_base_url_config(sphinx_doctree: CreateDoctree):
"""Test that tippy_glossary_base_url config is properly set."""
sphinx_doctree.set_conf(
{
"extensions": ["sphinx_tippy"],
"tippy_glossary_base_url": "glossary.html",
}
)
sphinx_doctree.buildername = "html"
result = sphinx_doctree(
"""
Test
----

Some content.
""",
)
assert result.app.env.tippy_config.glossary_base_url == "glossary.html"