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
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ Bugs fixed
English stemmer) and Dutch (which uses the Dutch Porter stemmer).
Patch by Hugo van Kemenade

* #14221: Fix a regression from Sphinx v8.2.0 onwards that could cause links
produced by ``singlehtml`` builds to contain multiple fragment delimiters
(``#``), and withdraw the deprecation of
``SingleFileHTMLBuilder.fix_refuris`` because the builder still requires it.
Patch by James Addison

Release 9.1.0 (released Dec 31, 2025)
=====================================
Expand Down
5 changes: 0 additions & 5 deletions doc/extdev/deprecated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,6 @@ The following is a list of deprecated interfaces.
- 11.0
- N/A

* - ``sphinx.builders.singlehtml.SingleFileHTMLBuilder.fix_refuris``
- 8.2
- 10.0
- N/A

* - ``sphinx.util.FilenameUniqDict``
- 8.1
- 10.0
Expand Down
14 changes: 4 additions & 10 deletions sphinx/builders/singlehtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@

from __future__ import annotations

import warnings
from typing import TYPE_CHECKING

from docutils import nodes

from sphinx._cli.util.colour import darkgreen
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.deprecation import RemovedInSphinx10Warning
from sphinx.environment.adapters.toctree import global_toctree_for_doc
from sphinx.locale import __
from sphinx.util import logging
Expand Down Expand Up @@ -52,14 +50,6 @@ def get_relative_uri(self, from_: str, to: str, typ: str | None = None) -> str:
return self.get_target_uri(to, typ)

def fix_refuris(self, tree: Node) -> None:
deprecation_msg = (
"The 'SingleFileHTMLBuilder.fix_refuris' method is no longer used "
'within the builder and is planned for removal in Sphinx 10. '
'Please report malformed URIs generated by the Sphinx singlehtml '
'builder as bugreports.'
)
warnings.warn(deprecation_msg, RemovedInSphinx10Warning, stacklevel=2)

# fix refuris with double anchor
for refnode in tree.findall(nodes.reference):
if 'refuri' not in refnode:
Expand All @@ -86,6 +76,8 @@ def _get_local_toctree(
toctree = global_toctree_for_doc(
self.env, docname, self, tags=self.tags, collapse=collapse, **kwargs
)
if toctree is not None:
self.fix_refuris(toctree)
Comment thread
jdillard marked this conversation as resolved.
return self.render_partial(toctree)['fragment']

def assemble_doctree(self) -> nodes.document:
Expand All @@ -95,6 +87,7 @@ def assemble_doctree(self) -> nodes.document:
tree = inline_all_toctrees(self, set(), master, tree, darkgreen, [master])
tree['docname'] = master
self.env.resolve_references(tree, master, self)
self.fix_refuris(tree)
return tree

def assemble_toc_secnumbers(self) -> dict[str, dict[str, tuple[int, ...]]]:
Expand Down Expand Up @@ -145,6 +138,7 @@ def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, A
)
# if there is no toctree, toc is None
if toctree:
self.fix_refuris(toctree)
toc = self.render_partial(toctree)['fragment']
display_toc = True
else:
Expand Down
2 changes: 2 additions & 0 deletions tests/roots/test-refuris/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
project = 'Glossary Test'
extensions = []
7 changes: 7 additions & 0 deletions tests/roots/test-refuris/glossary/term1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Glossary
========

.. glossary::
Comment thread
jdillard marked this conversation as resolved.

API
Lorem Ipsum.
12 changes: 12 additions & 0 deletions tests/roots/test-refuris/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Main Documentation
==================

This documentation discusses :term:`API` design.

We use :term:`API` throughout our application to communicate between
different services.

.. toctree::
:maxdepth: 2

glossary/term1
58 changes: 58 additions & 0 deletions tests/test_builders/test_build_html_refuris.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Test output of reference URIs when building single-page HTML output."""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from tests.test_builders.xpath_util import check_xpath

if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from pathlib import Path
from xml.etree.ElementTree import Element, ElementTree

from sphinx.testing.util import SphinxTestApp


def _internal_reference_fragment_check(nodes: Sequence[Element]) -> None:
"""Confirm that internal references do not contain duplicate fragment symbols"""
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')
if not href:
continue
assert href.count('#') < 2, 'Hyperlink contains duplicate fragments'


@pytest.mark.sphinx('singlehtml', testroot='refuris')
def test_singlehtml_refuris_check_fragments(
app: SphinxTestApp,
cached_etree_parse: Callable[[Path], ElementTree],
) -> None:
app.build()
check_xpath(
cached_etree_parse(app.outdir / 'index.html'),
'index.html',
".//a[@class='reference internal']",
_internal_reference_fragment_check,
)


@pytest.mark.sphinx('singlehtml', testroot='refuris')
def test_singlehtml_refuris_check_term_anchor(
app: SphinxTestApp,
cached_etree_parse: Callable[[Path], ElementTree],
) -> None:
app.build()
etree = cached_etree_parse(app.outdir / 'index.html')
api_refs = [
node
for node in etree.findall(".//a[@class='reference internal']")
if ''.join(node.itertext()) == 'API'
]
assert api_refs # the API term is referenced twice from the document text
assert all(ref.get('href') == '#term-API' for ref in api_refs)
assert etree.find(".//*[@id='term-API']") is not None
Loading