Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add autosectionlabel_full_reference configuration variable #13076

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Deprecated
Features added
--------------

* #13076: Add autosectionlabel_full_reference configuration variable.
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved

Bugs fixed
----------

Expand Down
16 changes: 11 additions & 5 deletions doc/usage/extensions/autosectionlabel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ Internally, this extension generates the labels for each section. If same
section names are used in whole of document, any one is used for a target by
default. The ``autosectionlabel_prefix_document`` configuration variable can be
used to make headings which appear multiple times but in different documents
unique. Also the ``autosectionlabel_full_reference`` configuration variable can
help ensure the generated refereences are unique across a document with similar
section names at different levels.
unique.

Use the :conf:`autosectionlabel_full_reference` configuration variable
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved
to guarantee that the generated references are unique across a document
with similar section names at different levels.


Configuration
Expand Down Expand Up @@ -56,8 +58,12 @@ Configuration
:type: :code-py:`bool`
:default: :code-py:`False`

.. versionadded:: 8.2

True to make each section label include all the parent sections separated by
a colon. For example::
a colon. For instance, if the following appears in document ``index.rst``:

.. code-block:: rst

Title
=====
Expand All @@ -68,7 +74,7 @@ Configuration
Sub Section
~~~~~~~~~~~

Then the reference of the third level section is
The reference of the third level section ("Sub Section") will be
``index:Title:Section:Sub Section``.

Debugging
Expand Down
50 changes: 32 additions & 18 deletions sphinx/ext/autosectionlabel.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import sphinx
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.nodes import clean_astext, make_id
from sphinx.util.nodes import clean_astext, make_id, traverse_parent

if TYPE_CHECKING:
from docutils.nodes import Node
Expand All @@ -20,7 +20,7 @@
logger = logging.getLogger(__name__)


def get_node_depth(node: Node) -> int:
def _get_node_depth(node: Node) -> int:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah maybe I wasn't clear. Old APIs are fine to be kept untouched (because we don't want to break packages) but it's good if new APIs can be designed with a good scope.

i = 0
cur_node = node
while cur_node.parent != node.document:
Expand All @@ -29,43 +29,57 @@
return i


def get_node_parents(node: Node) -> list[str]:
def _find_node_parents(node: Node) -> list[str]:
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved
parents = []
cur_node = node
while cur_node.parent != node.document:
cur_node = cur_node.parent
title = cast(nodes.title, cur_node[0])
for pnode in traverse_parent(node.parent, nodes.section):
title = cast(nodes.title, pnode[0])
ref_name = getattr(title, 'rawsource', title.astext())
parents.append(ref_name)

return parents


def register_sections_as_label(app: Sphinx, document: Node) -> None:
def register_sections_as_label(app: Sphinx, doctree: nodes.document) -> None:
domain = app.env.domains.standard_domain
for node in document.findall(nodes.section):
id_prefix = None

settings = doctree.settings
for node in doctree.findall(nodes.section):
if (app.config.autosectionlabel_maxdepth and
get_node_depth(node) >= app.config.autosectionlabel_maxdepth):
_get_node_depth(node) >= app.config.autosectionlabel_maxdepth):
continue

labelid = node['ids'][0]
docname = app.env.docname
title = cast(nodes.title, node[0])
ref_name = getattr(title, 'rawsource', title.astext())

if app.config.autosectionlabel_prefix_document:
if app.config.autosectionlabel_full_reference:
id_array = get_node_parents(node)
id_array = _find_node_parents(node)
id_array.append(docname)
id_array.reverse()
id_array.append(ref_name)
name = nodes.fully_normalize_name(':'.join(id_array))
labelid = make_id(app.env, node.document, '', '.'.join(id_array))
# replace id_prefix temporarily
id_prefix = settings.id_prefix
settings.id_prefix = '.'.join(id_array)

labelid = make_id(app.env, doctree, '', '.'.join(id_array))
node['ids'][0] = labelid
doctree.ids[labelid] = labelid
name = nodes.fully_normalize_name(labelid.replace('.', ':'))

# restore id_prefix
settings.id_prefix = id_prefix
else:
name = nodes.fully_normalize_name(docname + ':' + ref_name)
name = nodes.fully_normalize_name(f'{docname}:{ref_name}')
else:
name = nodes.fully_normalize_name(ref_name)

sectname = clean_astext(title)

logger.debug(__('section "%s" gets labeled as "%s"'),
ref_name, name,
logger.debug(__('section "%s" gets labeled as "%s" with id as "%s" and prefix is "%s"'),

Check failure on line 81 in sphinx/ext/autosectionlabel.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

sphinx/ext/autosectionlabel.py:81:96: E501 Line too long (96 > 95)
ref_name, name, labelid, id_prefix,
location=node, type='autosectionlabel', subtype=docname)
if name in domain.labels:
logger.warning(__('duplicate label %s, other instance in %s'),
Expand All @@ -77,9 +91,9 @@


def setup(app: Sphinx) -> ExtensionMetadata:
app.add_config_value('autosectionlabel_prefix_document', False, 'env')
app.add_config_value('autosectionlabel_prefix_document', False, 'env', bool)
app.add_config_value('autosectionlabel_maxdepth', None, 'env')
app.add_config_value('autosectionlabel_full_reference', False, 'env')
app.add_config_value('autosectionlabel_full_reference', False, 'env', bool)
app.connect('doctree-read', register_sections_as_label)

return {
Expand Down
6 changes: 5 additions & 1 deletion sphinx/util/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,11 @@
node_id = None # fallback to None

while node_id is None or node_id in document.ids:
node_id = idformat % env.new_serialno(prefix)
node_id = _make_id(idformat % env.new_serialno(prefix))

logger.debug(__('NODE "%s" gets term "%s" with ID format "%s" and prefix is "%s" and ids is "s"'),

Check failure on line 602 in sphinx/util/nodes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

sphinx/util/nodes.py:602:96: E501 Line too long (102 > 95)
node_id, term, idformat, prefix,# document.ids,

Check failure on line 603 in sphinx/util/nodes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E231)

sphinx/util/nodes.py:603:49: E231 Missing whitespace after ','

Check failure on line 603 in sphinx/util/nodes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E261)

sphinx/util/nodes.py:603:50: E261 Insert at least two spaces before an inline comment
location=document, type='make_id')

return node_id

Expand Down
7 changes: 6 additions & 1 deletion sphinx/writers/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,9 @@ def astext(self) -> str:
return self.render('latex.tex.jinja', self.elements)

def hypertarget(self, id: str, withdoc: bool = True, anchor: bool = True) -> str:
if withdoc:
if self.config.autosectionlabel_full_reference:
pass
elif withdoc:
id = self.curfilestack[-1] + ':' + id
escaped_id = self.idescape(id)
return (r'\phantomsection' if anchor else '') + r'\label{%s}' % escaped_id
Expand Down Expand Up @@ -1971,9 +1973,12 @@ def visit_reference(self, node: Element) -> None:
if hashindex == -1:
# reference to the document
id = uri[1:] + '::doc'
elif self.config.autosectionlabel_full_reference:
id = uri[hashindex + 1:]
else:
# reference to a label
id = uri[1:].replace('#', ':')

self.body.append(self.hyperlink(id))
if (
len(node)
Expand Down
3 changes: 2 additions & 1 deletion sphinx/writers/texinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,8 @@ def get_short_id(self, id: str) -> str:
def add_anchor(self, id: str, node: Node) -> None:
if id.startswith('index-'):
return
id = self.curfilestack[-1] + ':' + id
if not self.config.autosectionlabel_full_reference:
id = self.curfilestack[-1] + ':' + id
eid = self.escape_id(id)
sid = self.get_short_id(id)
for id in (eid, sid):
Expand Down
3 changes: 3 additions & 0 deletions tests/roots/test-ext-autosectionlabel-full-reference/conf.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
extensions = ['sphinx.ext.autosectionlabel']
autosectionlabel_prefix_document = True
autosectionlabel_full_reference = True
latex_documents = [
('index', 'test.tex', '', 'Sphinx', 'report')
]
45 changes: 23 additions & 22 deletions tests/roots/test-ext-autosectionlabel-full-reference/index.rst
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
Introduction of Sphinx
======================

Installation
============

For Windows users
-----------------
Directives
----------

Windows
^^^^^^^

Command
'''''''
Installation
============

.. include:: windows.rst

For UNIX users
--------------

Linux
^^^^^

Command
'''''''
Command1
''''''''

FreeBSD
^^^^^^^

Command
'''''''
2nd Command
'''''''''''

This one's got an apostrophe
----------------------------
Expand All @@ -35,14 +33,17 @@ This one's got an apostrophe
References
==========

* :ref:`index:Introduction of Sphinx`
* :ref:`index:Introduction-of-Sphinx`
* :ref:`index:Installation`
* :ref:`index:Installation:For Windows users`
* :ref:`index:Installation:For Windows users:Windows`
* :ref:`index:Installation:For Windows users:Windows:Command`
* :ref:`index:Installation:For UNIX users`
* :ref:`index:Installation:For UNIX users:Linux`
* :ref:`index:Installation:For UNIX users:Linux:Command`
* :ref:`index:Installation:For UNIX users:FreeBSD`
* :ref:`index:Installation:For UNIX users:FreeBSD:Command`
* :ref:`index:Installation:This one's got an apostrophe`
* :ref:`index:Installation:For-Windows-users`
* :ref:`index:Installation:For-Windows-users:Windows`
* :ref:`index:Installation:For-Windows-users:Windows:Command`
* :ref:`index:Installation:For-Windows-users:Windows:Command0`
* :ref:`index:Installation:For-Windows-users:Windows:Command1`
* :ref:`index:Installation:For-UNIX-users`
* :ref:`index:Installation:For-UNIX-users:Linux`
* :ref:`index:Installation:For-UNIX-users:Linux:Command1`
* :ref:`index:Installation:For-UNIX-users:FreeBSD`
* :ref:`index:Installation:For-UNIX-users:FreeBSD:2nd-Command`
* :ref:`index:Installation:This-one-s-got-an-apostrophe`

27 changes: 27 additions & 0 deletions tests/roots/test-ext-autosectionlabel-full-reference/windows.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
For Windows users
-----------------

Windows
^^^^^^^

Command
'''''''

Command
'''''''

This has the same name as the section before but a unique label id is required.

Command
'''''''

This has the same name as the two sections before but a unique label id is required.

Local References
^^^^^^^^^^^^^^^^

* :ref:`windows:For-Windows-users`
* :ref:`windows:For-Windows-users:Windows`
* :ref:`windows:For-Windows-users:Windows:Command`
* :ref:`windows:For-Windows-users:Windows:Command0`
* :ref:`windows:For-Windows-users:Windows:Command1`
Loading
Loading