diff --git a/docs/src/release_notes.rst b/docs/src/release_notes.rst index 2b0c43b..29c4b95 100644 --- a/docs/src/release_notes.rst +++ b/docs/src/release_notes.rst @@ -8,6 +8,10 @@ These release notes are based on sphinx-codeautolink adheres to `Semantic Versioning `_. +Unreleased +---------- +- Fix backreference links using relative URIs (:issue:`190`) + 0.17.3 (2025-03-06) ------------------- - Fix Sphinx InventoryItem deprecation warning (:issue:`173`) diff --git a/src/sphinx_codeautolink/extension/__init__.py b/src/sphinx_codeautolink/extension/__init__.py index 001bc3c..f691b2e 100644 --- a/src/sphinx_codeautolink/extension/__init__.py +++ b/src/sphinx_codeautolink/extension/__init__.py @@ -271,7 +271,8 @@ def generate_backref_tables(self, app, doctree, docname): visitor = CodeRefsVisitor( doctree, code_refs=self.code_refs, - builder=app.builder.name, + docname=docname, + builder=app.builder, warn_no_backreference=self.warn_no_backreference, ) doctree.walk(visitor) diff --git a/src/sphinx_codeautolink/extension/backref.py b/src/sphinx_codeautolink/extension/backref.py index e6b1379..cb122e1 100644 --- a/src/sphinx_codeautolink/extension/backref.py +++ b/src/sphinx_codeautolink/extension/backref.py @@ -1,9 +1,9 @@ """Backreference tables implementation.""" from dataclasses import dataclass -from pathlib import Path from docutils import nodes +from sphinx.builders import Builder from sphinx_codeautolink.warn import logger, warn_type @@ -63,12 +63,14 @@ def __init__( self, *args, code_refs: dict[str, list[CodeExample]], - builder: str, + docname: str, + builder: Builder, warn_no_backreference: bool = False, **kwargs, ) -> None: super().__init__(*args, **kwargs) self.code_refs = code_refs + self.docname = docname self.builder = builder self.warn_no_backreference = warn_no_backreference @@ -82,10 +84,7 @@ def unknown_visit(self, node) -> None: items = [] for ref in self.code_refs.get(node.ref, []): - if self.builder == "dirhtml" and Path(ref.document).name != "index": - link = ref.document + "/index.html" - else: - link = ref.document + ".html" + link = self.builder.get_relative_uri(self.docname, ref.document) if ref.ref_id is not None: link += f"#{ref.ref_id}" items.append((link, " / ".join(ref.headings))) diff --git a/tests/extension/__init__.py b/tests/extension/__init__.py index 87fb758..0279bee 100644 --- a/tests/extension/__init__.py +++ b/tests/extension/__init__.py @@ -9,7 +9,7 @@ from bs4 import BeautifulSoup from sphinx.cmd.build import main as sphinx_main -from ._check import check_link_targets +from ._check import check_link_targets, check_reference_targets_exist # Insert test package root to path for all tests sys.path.insert(0, str(Path(__file__).parent / "src")) @@ -365,6 +365,58 @@ def test_dirhtml_builder(tmp_path: Path): assert_links(result_dir / "subdir/page3/index.html", links) assert check_link_targets(result_dir) == len(links) * 4 + check_reference_targets_exist(result_dir) + + +def test_html_subdir_reference(tmp_path: Path): + index = """ +Test project +============ + +.. toctree:: + + subdir/page1 + subdir/subdir2/page2 + +Index Page +---------- + +.. code:: python + + import test_project + test_project.bar() + +.. automodule:: test_project +""" + + page = """ +Page {idx} +=========== + +.. code:: python + + import test_project + test_project.bar() + +.. autolink-examples:: test_project.bar +""" + + files = { + "conf.py": default_conf, + "index.rst": index, + "subdir/page1.rst": page.format(idx=1), + "subdir/subdir2/page2.rst": page.format(idx=2), + } + links = ["test_project", "test_project.bar"] + + result_dir = _sphinx_build(tmp_path, "html", files) + + assert_links(result_dir / "index.html", links) + assert_links(result_dir / "subdir/page1.html", links) + assert_links(result_dir / "subdir/subdir2/page2.html", links) + + assert check_link_targets(result_dir) == len(links) * 3 + check_reference_targets_exist(result_dir) def _sphinx_build( diff --git a/tests/extension/_check.py b/tests/extension/_check.py index 6060c4e..c5b497a 100644 --- a/tests/extension/_check.py +++ b/tests/extension/_check.py @@ -44,6 +44,25 @@ def check_link_targets(root: Path) -> int: return total +def check_reference_targets_exist(root: Path): + site_docs = { + p: BeautifulSoup(p.read_text("utf-8"), "html.parser") + for p in root.glob("**/*.html") + } + for doc, soup in site_docs.items(): + for link in soup.find_all("a", attrs={"class": "reference internal"}): + base = link["href"].split("#")[0] + if any(base.startswith(s) for s in ("http://", "https://")): + continue + target_path = doc if base == "" else (doc.parent / base).resolve() + if target_path.is_dir(): + target_path /= "index.html" + assert target_path.exists(), ( + f"Target path {target_path!s} not found while validating" + f" link for `{link.string}` in {doc.relative_to(root)!s}!" + ) + + def gather_ids(soup: BeautifulSoup) -> set: """Gather all HTML IDs from a given page.""" return {tag["id"] for tag in soup.find_all(id=True)}