Skip to content
Draft
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
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Contributors
* Joel Wurtz -- cellspanning support in LaTeX
* John Waltman -- Texinfo builder
* Jon Dufresne -- modernisation
* Jorge Marques -- unique ids in singlehtml
* Josip Dzolonga -- coverage builder
* Juan Luis Cano Rodríguez -- new tutorial (2021)
* Julien Palard -- Colspan and rowspan in text builder
Expand Down
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ Features added
Patch by Adam Turner.
* #13805: LaTeX: add support for ``fontawesome7`` package.
Patch by Jean-François B.
* #13739: singlehtml builder: make all 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``.

Bugs fixed
----------
Expand Down
15 changes: 9 additions & 6 deletions sphinx/builders/singlehtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,17 @@ def assemble_toc_secnumbers(self) -> dict[str, dict[str, tuple[int, ...]]]:
# Assemble toc_secnumbers to resolve section numbers on SingleHTML.
# Merge all secnumbers to single secnumber.
#
# Note: current Sphinx has refid confliction in singlehtml mode.
# To avoid the problem, it replaces key of secnumbers to
# Note: current Sphinx patches refid with docname to avoid confliction
# in singlehtml mode.
# To match the patch, it replaces key of secnumbers to
# tuple of docname and refid.
#
# There are related codes in inline_all_toctres() and
# HTMLTranslter#add_secnumber().
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'document-{docname}{id}'
new_secnumbers[alias] = secnum

return {self.config.root_doc: new_secnumbers}
Expand All @@ -121,8 +122,9 @@ def assemble_toc_fignumbers(
# Assemble toc_fignumbers to resolve figure numbers on SingleHTML.
# Merge all fignumbers to single fignumber.
#
# Note: current Sphinx has refid confliction in singlehtml mode.
# To avoid the problem, it replaces key of secnumbers to
# Note: current Sphinx patches refid with docname to avoid confliction
# in singlehtml mode.
# To match the patch, it replaces key of secnumbers to
# tuple of docname and refid.
#
# There are related codes in inline_all_toctres() and
Expand All @@ -131,9 +133,10 @@ def assemble_toc_fignumbers(
# {'foo': {'figure': {'id2': (2,), 'id1': (1,)}}, 'bar': {'figure': {'id1': (3,)}}}
for docname, fignumlist in self.env.toc_fignumbers.items():
for figtype, fignums in fignumlist.items():
alias = f'{docname}/{figtype}'
alias = f'document-{docname}#{figtype}'
new_fignumbers.setdefault(alias, {})
for id, fignum in fignums.items():
id = f'document-{docname}#{id}'
new_fignumbers[alias][id] = fignum

return {self.config.root_doc: new_fignumbers}
Expand Down
19 changes: 19 additions & 0 deletions sphinx/transforms/post_transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,11 +390,30 @@ def run(self, **kwargs: Any) -> None:
node['classes'].append(node.parent['domain'])


class PrefixIdsWithDocname(SphinxPostTransform):
"""Prefix all ids with the docname, to ensure unique refid in singlehtml."""

default_priority = 300

def run(self, **kwargs: Any) -> None:
builder = self.env._app.builder
if builder.name != 'singlehtml':
return
for node in self.document.findall():
if 'refid' in node or 'ids' in node:
docname = node.document.settings.env.path2doc(node.document['source'])
if 'refid' in node:
node['refid'] = 'document-' + docname + '#' + node['refid']
if 'ids' in node:
node['ids'] = ['document-' + docname + '#' + id for id in node['ids']]


def setup(app: Sphinx) -> ExtensionMetadata:
app.add_post_transform(ReferencesResolver)
app.add_post_transform(OnlyNodeTransform)
app.add_post_transform(SigElementFallbackTransform)
app.add_post_transform(PropagateDescDomain)
app.add_post_transform(PrefixIdsWithDocname)

return {
'version': 'builtin',
Expand Down
6 changes: 3 additions & 3 deletions sphinx/writers/html5.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,10 @@ 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]}'
anchorname = node.parent['ids'][0]
if anchorname not in self.builder.secnumbers:
# try first heading which has no anchor
anchorname = f'{docname}/'
anchorname = f'document-{docname}'
else:
anchorname = '#' + node.parent['ids'][0]
if anchorname not in self.builder.secnumbers:
Expand All @@ -420,7 +420,7 @@ def add_secnumber(self, node: Element) -> None:
def add_fignumber(self, node: Element) -> None:
def append_fignumber(figtype: str, figure_id: str) -> None:
if self.builder.name == 'singlehtml':
key = f'{self.docnames[-1]}/{figtype}'
key = f'document-{self.docnames[-1]}#{figtype}'
else:
key = figtype

Expand Down
5 changes: 5 additions & 0 deletions tests/roots/test-tocdepth/bar.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ Bar B1

should be 2.2.1

FooBar B1
---------

should be 2.2.2

5 changes: 5 additions & 0 deletions tests/roots/test-tocdepth/foo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ Foo B1

should be 1.2.1

FooBar B1
---------

should be 1.2.2

18 changes: 16 additions & 2 deletions tests/test_builders/test_build_html_tocdepth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, cast

import pytest

Expand All @@ -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

Expand Down Expand Up @@ -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))
Loading