From 0bfaadf6c9adb54ddbed1c7e0b0c11d3c4bf475f Mon Sep 17 00:00:00 2001 From: James Addison <55152140+jayaddison@users.noreply.github.com> Date: Sun, 11 Aug 2024 22:43:48 +0100 Subject: [PATCH] singlehtml: Use same-document hyperlinks for internal project references (#12551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com> --- CHANGES.rst | 5 +++++ sphinx/builders/singlehtml.py | 4 ++-- .../test_builders/test_build_html_tocdepth.py | 2 ++ .../test_build_html_toctree.py} | 17 +++++++++++++++++ tests/test_builders/xpath_html_util.py | 19 +++++++++++++++++++ 5 files changed, 45 insertions(+), 2 deletions(-) rename tests/{test_toctree.py => test_builders/test_build_html_toctree.py} (74%) create mode 100644 tests/test_builders/xpath_html_util.py diff --git a/CHANGES.rst b/CHANGES.rst index e8ae58b425d..c2c74365e3e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -54,6 +54,11 @@ Bugs fixed get passed to :program:`latexmk`. Let :option:`-Q ` (silent) apply as well to the PDF build phase. Patch by Jean-François B. +* #11970, #12551: singlehtml builder: make target URIs to be same-document + references in the sense of :rfc:`RFC 3986, §4.4 <3986#section-4.4>`, + e.g., ``index.html#foo`` becomes ``#foo``. + (note: continuation of a partial fix added in Sphinx 7.3.0) + Patch by James Addison (with reference to prior work by Eric Norige) Testing ------- diff --git a/sphinx/builders/singlehtml.py b/sphinx/builders/singlehtml.py index d222184b52f..4d58d554cf6 100644 --- a/sphinx/builders/singlehtml.py +++ b/sphinx/builders/singlehtml.py @@ -52,7 +52,6 @@ def get_relative_uri(self, from_: str, to: str, typ: str | None = None) -> str: def fix_refuris(self, tree: Node) -> None: # fix refuris with double anchor - fname = self.config.root_doc + self.out_suffix for refnode in tree.findall(nodes.reference): if 'refuri' not in refnode: continue @@ -62,7 +61,8 @@ def fix_refuris(self, tree: Node) -> None: continue hashindex = refuri.find('#', hashindex + 1) if hashindex >= 0: - refnode['refuri'] = fname + refuri[hashindex:] + # all references are on the same page... + refnode['refuri'] = refuri[hashindex:] def _get_local_toctree(self, docname: str, collapse: bool = True, **kwargs: Any) -> str: if isinstance(includehidden := kwargs.get('includehidden'), str): diff --git a/tests/test_builders/test_build_html_tocdepth.py b/tests/test_builders/test_build_html_tocdepth.py index dda049c47ff..7d6afc4c1c3 100644 --- a/tests/test_builders/test_build_html_tocdepth.py +++ b/tests/test_builders/test_build_html_tocdepth.py @@ -2,6 +2,7 @@ import pytest +from tests.test_builders.xpath_html_util import _intradocument_hyperlink_check from tests.test_builders.xpath_util import check_xpath @@ -78,6 +79,7 @@ def test_tocdepth(app, cached_etree_parse, fname, path, check, be_found): (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True), (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False), (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False), + (".//ul/li[@class='toctree-l1']/..//a", _intradocument_hyperlink_check), # index.rst ('.//h1', 'test-tocdepth', True), # foo.rst diff --git a/tests/test_toctree.py b/tests/test_builders/test_build_html_toctree.py similarity index 74% rename from tests/test_toctree.py rename to tests/test_builders/test_build_html_toctree.py index 39aac47a679..4195ac2e41b 100644 --- a/tests/test_toctree.py +++ b/tests/test_builders/test_build_html_toctree.py @@ -4,6 +4,9 @@ import pytest +from tests.test_builders.xpath_html_util import _intradocument_hyperlink_check +from tests.test_builders.xpath_util import check_xpath + @pytest.mark.sphinx(testroot='toctree-glob') def test_relations(app): @@ -45,3 +48,17 @@ def test_numbered_toctree(app): index = re.sub(':numbered:.*', ':numbered: 1', index) (app.srcdir / 'index.rst').write_text(index, encoding='utf8') app.build(force_all=True) + + +@pytest.mark.parametrize( + 'expect', + [ + # internal references should be same-document; external should not + (".//a[@class='reference internal']", _intradocument_hyperlink_check), + (".//a[@class='reference external']", r'https?://'), + ], +) +@pytest.mark.sphinx('singlehtml', testroot='toctree') +def test_singlehtml_hyperlinks(app, cached_etree_parse, expect): + app.build() + check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect) diff --git a/tests/test_builders/xpath_html_util.py b/tests/test_builders/xpath_html_util.py new file mode 100644 index 00000000000..b0949c80429 --- /dev/null +++ b/tests/test_builders/xpath_html_util.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Sequence + from xml.etree.ElementTree import Element + + +def _intradocument_hyperlink_check(nodes: Sequence[Element]) -> None: + """Confirm that a series of nodes are all HTML hyperlinks to the current page""" + assert nodes, 'Expected at least one node to check' + for node in nodes: + assert node.tag == 'a', 'Attempted to check hyperlink on a non-anchor element' + href = node.attrib.get('href') + # Allow Sphinx index and table hyperlinks to be non-same-document, as exceptions. + if href in {'genindex.html', 'py-modindex.html', 'search.html'}: + continue + assert not href or href.startswith('#'), 'Hyperlink failed same-document check'