diff --git a/docs/src/about.rst b/docs/src/about.rst index a9222f8..11ab301 100644 --- a/docs/src/about.rst +++ b/docs/src/about.rst @@ -16,8 +16,8 @@ is injected after the ordinary Sphinx build is finished. Caveats ------- -- **Only works with HTML documentation**, disabled otherwise. If the extension - is off, it silently removes directives that would produce output. +- **Only works with HTML or DIRHTML documentation**, disabled otherwise. If the + extension is off, it silently removes directives that would produce output. - **Only processes blocks, not inline code**. Sphinx has great tools for linking definitions inline, and longer code should be in a block anyway. - **Doesn't run example code**. Therefore all possible resolvable types are not diff --git a/docs/src/index.rst b/docs/src/index.rst index c3aa9e8..04404e4 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -68,7 +68,7 @@ Caveats ------- For a more thorough explanation, see :ref:`about`. -- Only works with HTML documentation +- Only works with HTML or DIRHTML documentation - Only processes blocks, not inline code - Doesn't run example code - Parsing and type hint resolving is incomplete diff --git a/docs/src/release_notes.rst b/docs/src/release_notes.rst index 2564c19..5bffde3 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 +---------- +- Add support for the DIRHTML Sphinx builder (:issue:`188`) + 0.17.2 (2025-03-02) ------------------- - Support :rst:dir:`testsetup` from ``sphinx.ext.doctest`` as another diff --git a/src/sphinx_codeautolink/extension/__init__.py b/src/sphinx_codeautolink/extension/__init__.py index a003c0d..b98c3f5 100644 --- a/src/sphinx_codeautolink/extension/__init__.py +++ b/src/sphinx_codeautolink/extension/__init__.py @@ -89,7 +89,7 @@ def __init__(self) -> None: @print_exceptions() def build_inited(self, app) -> None: """Handle initial setup.""" - if app.builder.name != "html": + if app.builder.name not in ("html", "dirhtml"): self.do_nothing = True return @@ -268,6 +268,7 @@ def generate_backref_tables(self, app, doctree, docname): visitor = CodeRefsVisitor( doctree, code_refs=self.code_refs, + builder=app.builder.name, warn_no_backreference=self.warn_no_backreference, ) doctree.walk(visitor) @@ -289,6 +290,7 @@ def apply_links(self, app, exception) -> None: self.inventory, self.custom_blocks, self.search_css_classes, + builder_name=app.builder.name, ) self.cache.write() diff --git a/src/sphinx_codeautolink/extension/backref.py b/src/sphinx_codeautolink/extension/backref.py index 43cf811..e6b1379 100644 --- a/src/sphinx_codeautolink/extension/backref.py +++ b/src/sphinx_codeautolink/extension/backref.py @@ -1,6 +1,7 @@ """Backreference tables implementation.""" from dataclasses import dataclass +from pathlib import Path from docutils import nodes @@ -62,11 +63,13 @@ def __init__( self, *args, code_refs: dict[str, list[CodeExample]], + builder: str, warn_no_backreference: bool = False, **kwargs, ) -> None: super().__init__(*args, **kwargs) self.code_refs = code_refs + self.builder = builder self.warn_no_backreference = warn_no_backreference def unknown_departure(self, node) -> None: @@ -79,7 +82,10 @@ def unknown_visit(self, node) -> None: items = [] for ref in self.code_refs.get(node.ref, []): - link = ref.document + ".html" + if self.builder == "dirhtml" and Path(ref.document).name != "index": + link = ref.document + "/index.html" + else: + link = ref.document + ".html" if ref.ref_id is not None: link += f"#{ref.ref_id}" items.append((link, " / ".join(ref.headings))) diff --git a/src/sphinx_codeautolink/extension/block.py b/src/sphinx_codeautolink/extension/block.py index 38f1c39..a1ffc95 100644 --- a/src/sphinx_codeautolink/extension/block.py +++ b/src/sphinx_codeautolink/extension/block.py @@ -375,9 +375,14 @@ def link_html( inventory: dict, custom_blocks: dict, search_css_classes: list, + builder_name: str = "html", ) -> None: """Inject links to code blocks on disk.""" - html_file = Path(out_dir) / (document + ".html") + if builder_name == "dirhtml" and Path(document).name != "index": + html_file = Path(out_dir) / document / "index.html" + else: + html_file = Path(out_dir) / (document + ".html") + text = html_file.read_text("utf-8") soup = BeautifulSoup(text, "html.parser") diff --git a/tests/extension/__init__.py b/tests/extension/__init__.py index d036668..87fb758 100644 --- a/tests/extension/__init__.py +++ b/tests/extension/__init__.py @@ -278,6 +278,7 @@ def test_parallel_build(tmp_path: Path): for file in subfiles: assert_links(result_dir / (file + ".html"), links) + assert check_link_targets(result_dir) == n_subfiles * len(links) @@ -312,6 +313,60 @@ def test_skip_identical_code(tmp_path: Path): assert "sphinx-codeautolink-a" in str(blocks[1]) +def test_dirhtml_builder(tmp_path: Path): + index = """ +Test project +============ + +.. toctree:: + :maxdepth: 2 + + page1/index + page2 + subdir/page3 + +Index Page Code +--------------- + +.. 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, + "page1/index.rst": page.format(idx=1), + "page2.rst": page.format(idx=2), + "subdir/page3.rst": page.format(idx=3), + } + links = ["test_project", "test_project.bar"] + + result_dir = _sphinx_build(tmp_path, "dirhtml", files) + + assert_links(result_dir / "index.html", links) + assert_links(result_dir / "page1/index.html", links) + assert_links(result_dir / "page2/index.html", links) + assert_links(result_dir / "subdir/page3/index.html", links) + + assert check_link_targets(result_dir) == len(links) * 4 + + def _sphinx_build( folder: Path, builder: str, files: dict[str, str], n_processes: int | None = None ) -> Path: @@ -319,7 +374,9 @@ def _sphinx_build( src_dir = folder / "src" src_dir.mkdir(exist_ok=True) for name, content in files.items(): - (src_dir / name).write_text(content, "utf-8") + path = src_dir / name + path.parent.mkdir(exist_ok=True, parents=True) + path.write_text(content, "utf-8") build_dir = folder / "build" args = ["-M", builder, str(src_dir), str(build_dir), "-W"] diff --git a/tests/extension/_check.py b/tests/extension/_check.py index dfe3094..6060c4e 100644 --- a/tests/extension/_check.py +++ b/tests/extension/_check.py @@ -12,7 +12,7 @@ def check_link_targets(root: Path) -> int: """Validate links in HTML site at root, return number of links found.""" site_docs = { - p.relative_to(root): BeautifulSoup(p.read_text("utf-8"), "html.parser") + p: BeautifulSoup(p.read_text("utf-8"), "html.parser") for p in root.glob("**/*.html") } site_ids = {k: gather_ids(v) for k, v in site_docs.items()} @@ -27,10 +27,18 @@ def check_link_targets(root: Path) -> int: external_site_ids[base] = gather_ids(sub_soup) ids = external_site_ids[base] else: - ids = site_ids[Path(base)] + target_path = (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}!" + ) + ids = site_ids[target_path] + assert id_ in ids, ( - f"ID {id_} not found in {base}" - f" while validating link for `{link.string}` in {doc!s}!" + f"ID {id_} not found in {base} while validating link" + f" for `{link.string}` in {doc.relative_to(root)!s}!" ) total += 1 return total