diff --git a/AUTHORS.rst b/AUTHORS.rst index 5bcd74c943b..614b4b4aacd 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -72,6 +72,7 @@ Contributors * Joel Wurtz -- cellspanning support in LaTeX * John Waltman -- Texinfo builder * Jon Dufresne -- modernisation +* Jorge Marques -- singlehtml unique section ids * Josip Dzolonga -- coverage builder * Juan Luis Cano Rodríguez -- new tutorial (2021) * Julien Palard -- Colspan and rowspan in text builder diff --git a/CHANGES.rst b/CHANGES.rst index b5812ef2f23..4e3b7a5a089 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -116,6 +116,9 @@ Bugs fixed * #10785: Autodoc: Allow type aliases defined in the project to be properly cross-referenced when used as type annotations. This makes it possible for objects documented as ``:py:data:`` to be hyperlinked in function signatures. +* #13738: singlehtml builder: make section ids unique by appending the docname, + matching ``sphinx/environment/adapters/toctree.py``'s ``_resolve_toctree()`` + format. E.g., ``id3`` becomes ``document-path/to/doc#id3``. Testing ------- diff --git a/sphinx/builders/singlehtml.py b/sphinx/builders/singlehtml.py index 1888f6679d1..4fd04b05fc0 100644 --- a/sphinx/builders/singlehtml.py +++ b/sphinx/builders/singlehtml.py @@ -110,7 +110,7 @@ def assemble_toc_secnumbers(self) -> dict[str, dict[str, tuple[int, ...]]]: new_secnumbers: dict[str, tuple[int, ...]] = {} for docname, secnums in self.env.toc_secnumbers.items(): for id, secnum in secnums.items(): - alias = f'{docname}/{id}' + alias = f'{docname}{id}' new_secnumbers[alias] = secnum return {self.config.root_doc: new_secnumbers} diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index bbcd247e33c..14b5eff2914 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -17,7 +17,7 @@ from sphinx.util.images import get_image_size if TYPE_CHECKING: - from docutils.nodes import Element, Node, Text + from docutils.nodes import Element, Node, Text, section from sphinx.builders import Builder from sphinx.builders.html import StandaloneHTMLBuilder @@ -395,10 +395,11 @@ def get_secnumber(self, node: Element) -> tuple[int, ...] | None: if isinstance(node.parent, nodes.section): if self.builder.name == 'singlehtml': docname = self.docnames[-1] - anchorname = f'{docname}/#{node.parent["ids"][0]}' + # Remove document- + anchorname = node.parent['ids'][0][9:] if anchorname not in self.builder.secnumbers: # try first heading which has no anchor - anchorname = f'{docname}/' + anchorname = docname else: anchorname = '#' + node.parent['ids'][0] if anchorname not in self.builder.secnumbers: @@ -497,6 +498,15 @@ def depart_term(self, node: Element) -> None: self.body.append('') + def visit_section(self, node: section) -> None: + if self.builder.name == 'singlehtml' and node['ids']: + docname = self.docnames[-1] + node['ids'][0] = 'document-' + docname + '#' + node['ids'][0] + super().visit_section(node) + + def depart_section(self, node: section) -> None: + super().depart_section(node) + # overwritten def visit_title(self, node: nodes.title) -> None: if ( diff --git a/tests/roots/test-tocdepth/bar.rst b/tests/roots/test-tocdepth/bar.rst index d70dec90dd3..2bb869e93c5 100644 --- a/tests/roots/test-tocdepth/bar.rst +++ b/tests/roots/test-tocdepth/bar.rst @@ -25,3 +25,8 @@ Bar B1 should be 2.2.1 +FooBar B1 +--------- + +should be 2.2.2 + diff --git a/tests/roots/test-tocdepth/foo.rst b/tests/roots/test-tocdepth/foo.rst index 61fd539ffea..4834cb6eb14 100644 --- a/tests/roots/test-tocdepth/foo.rst +++ b/tests/roots/test-tocdepth/foo.rst @@ -24,3 +24,8 @@ Foo B1 should be 1.2.1 +FooBar B1 +--------- + +should be 1.2.2 + diff --git a/tests/test_builders/test_build_html_tocdepth.py b/tests/test_builders/test_build_html_tocdepth.py index 0fe83e0ff34..1c02b0da415 100644 --- a/tests/test_builders/test_build_html_tocdepth.py +++ b/tests/test_builders/test_build_html_tocdepth.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import pytest @@ -12,7 +12,7 @@ if TYPE_CHECKING: from collections.abc import Callable from pathlib import Path - from xml.etree.ElementTree import ElementTree + from xml.etree.ElementTree import Element, ElementTree from sphinx.testing.util import SphinxTestApp @@ -134,3 +134,17 @@ def test_tocdepth_singlehtml( ) -> None: app.build() check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect) + + +@pytest.mark.sphinx('singlehtml', testroot='tocdepth') +@pytest.mark.test_params(shared_result='test_build_html_tocdepth') +def test_unique_ids_singlehtml( + app: SphinxTestApp, + cached_etree_parse: Callable[[Path], ElementTree], +) -> None: + app.build() + tree = cached_etree_parse(app.outdir / 'index.html') + root = cast('Element', tree.getroot()) + + ids = [el.attrib['id'] for el in root.findall('.//*[@id]')] + assert len(ids) == len(set(ids))