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
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
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ Features added
Patch by Adam Turner.
* #13805: LaTeX: add support for ``fontawesome7`` package.
Patch by Jean-François B.
* #13739: singlehtml builder: append the docname to ids with format
``/<docname>/#<id>``, to ensure uniqueness. For example, ``id3`` becomes
``/path/to/doc/#id3``. This will break existing hyperlinks to ``singlehtml``
HTML documents since it alters the format of the ids in both the content body
and the toctree. Fixes toctree refid format ``document-<docname>`` that did
not match the id in the body.

Bugs fixed
----------
Expand Down
25 changes: 22 additions & 3 deletions sphinx/builders/singlehtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def get_outdated_docs(self) -> str | list[str]: # type: ignore[override]
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
if docname in self.env.all_docs:
# all references are on the same page...
return '#document-' + docname
return '#/' + docname + '/'
else:
# chances are this is a html_additional_page
return docname + self.out_suffix
Expand Down Expand Up @@ -88,13 +88,31 @@ def _get_local_toctree(
)
return self.render_partial(toctree)['fragment']

def ensure_fully_qualified_refids(self, tree: nodes.document) -> None:
"""Append docname to refids and ids using format
document-<docname>#<id>. Compensates for loss of the pathname section
of the href, that ensures uniqueness in the html builder.
"""
for node in tree.findall(nodes.Element):
doc = node.document
if doc is None:
continue
env = doc.settings.env
if 'refid' in node or 'ids' in node:
docname = env.path2doc(doc['source'])
if 'refid' in node:
node['refid'] = f'/{docname}/#{node["refid"]}'
if 'ids' in node:
node['ids'] = [f'/{docname}/#{id}' for id in node['ids']]

def assemble_doctree(self) -> nodes.document:
master = self.config.root_doc
tree = self.env.get_doctree(master)
logger.info(darkgreen(master))
tree = inline_all_toctrees(self, set(), master, tree, darkgreen, [master])
tree['docname'] = master
self.env.resolve_references(tree, master, self)
self.ensure_fully_qualified_refids(tree)
return tree

def assemble_toc_secnumbers(self) -> dict[str, dict[str, tuple[int, ...]]]:
Expand All @@ -110,7 +128,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}
Expand All @@ -131,9 +149,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'/{docname}/#{figtype}'
new_fignumbers.setdefault(alias, {})
for id, fignum in fignums.items():
id = f'/{docname}/#{id}'
new_fignumbers[alias][id] = fignum

return {self.config.root_doc: new_fignumbers}
Expand Down
8 changes: 4 additions & 4 deletions sphinx/writers/html5.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def __init__(self, document: nodes.document, builder: Builder) -> None:
def visit_start_of_file(self, node: Element) -> None:
# only occurs in the single-file builder
self.docnames.append(node['docname'])
self.body.append('<span id="document-%s"></span>' % node['docname'])
self.body.append('<span id="/%s/"></span>' % node['docname'])

def depart_start_of_file(self, node: Element) -> None:
self.docnames.pop()
Expand Down 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 = '/' + 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'/{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