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
22 changes: 21 additions & 1 deletion doc/usage/extensions/autosectionlabel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ 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.
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.
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved


Configuration
Expand All @@ -50,6 +52,24 @@ Configuration
only for top level sections, and deeper sections are not labeled. It
defaults to ``None`` (i.e. all sections are labeled).

.. confval:: autosectionlabel_full_reference
:type: :code-py:`bool`
:default: :code-py:`False`

True to make each section label include all the parent sections separated by
a colon. For example::

Title
=====

Section
-------

Sub Section
~~~~~~~~~~~
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved

Then the reference of the third level section is
``index:Title:Section:Sub Section``.
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved

Debugging
---------
Expand Down
24 changes: 22 additions & 2 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
from sphinx.util.nodes import clean_astext, make_id

if TYPE_CHECKING:
from docutils.nodes import Node
Expand All @@ -29,6 +29,17 @@ def get_node_depth(node: Node) -> int:
return i


def get_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])
ref_name = getattr(title, 'rawsource', title.astext())
parents.append(ref_name)
return parents
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved


def register_sections_as_label(app: Sphinx, document: Node) -> None:
domain = app.env.domains.standard_domain
for node in document.findall(nodes.section):
Expand All @@ -40,7 +51,15 @@ def register_sections_as_label(app: Sphinx, document: Node) -> None:
title = cast(nodes.title, node[0])
ref_name = getattr(title, 'rawsource', title.astext())
if app.config.autosectionlabel_prefix_document:
name = nodes.fully_normalize_name(docname + ':' + ref_name)
if app.config.autosectionlabel_full_reference:
id_array = get_node_parents(node)
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved
id_array.append(docname)
id_array.reverse()
id_array.append(ref_name)
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved
name = nodes.fully_normalize_name(':'.join(id_array))
labelid = make_id(app.env, node.document, '', '.'.join(id_array))
else:
name = nodes.fully_normalize_name(docname + ':' + ref_name)
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved
else:
name = nodes.fully_normalize_name(ref_name)
sectname = clean_astext(title)
Expand All @@ -60,6 +79,7 @@ def register_sections_as_label(app: Sphinx, document: Node) -> None:
def setup(app: Sphinx) -> ExtensionMetadata:
app.add_config_value('autosectionlabel_prefix_document', False, 'env')
app.add_config_value('autosectionlabel_maxdepth', None, 'env')
app.add_config_value('autosectionlabel_full_reference', False, 'env')
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved
app.connect('doctree-read', register_sections_as_label)

return {
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
@@ -0,0 +1,3 @@
extensions = ['sphinx.ext.autosectionlabel']
autosectionlabel_prefix_document = True
autosectionlabel_full_reference = True
48 changes: 48 additions & 0 deletions tests/roots/test-ext-autosectionlabel-full-reference/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Introduction of Sphinx
======================

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

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

Windows
^^^^^^^

Command
'''''''

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

Linux
^^^^^

Command
'''''''

FreeBSD
^^^^^^^

Command
'''''''

This one's got an apostrophe
----------------------------


References
==========

* :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`
Copy link
Member

Choose a reason for hiding this comment

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

Can you check check with numbers in titles as well as with :math: blocks please? and with literal (double backquotes). Can we also use :ref: in a title? (if not, no need for the coverage).

Additionally, can you test when you specify a custom label before an inner title? as well as content in-between two titles (in other words, try to see if it's possible to construct a doctree with the following tree: "section -> non-section -> section") so that the parent of the second section is a non-section node.

Copy link
Author

@andrewjmaguire andrewjmaguire Oct 28, 2024

Choose a reason for hiding this comment

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

Would adding an image in between suffice? e.g.


Section
-------

Here is an image or I could use a figure?

.. image:: test.png

Sub Section
~~~~~~~~~~~

See previous image...

Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately, the image and subsection would be sibling nodes and not parent->child nodes. Maybe it's not possible to actually have a parent node that is not a section's node but I need to check that (it's midnight here so I won't do it today).

Copy link
Author

Choose a reason for hiding this comment

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

I have tested with numbers in sections

Copy link
Author

Choose a reason for hiding this comment

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

I have tried to added all the other comments, and added texinfo and latex tests and the test includes another rst file, to check how references worked there.
How is it looking now?

75 changes: 75 additions & 0 deletions tests/test_extensions/test_ext_autosectionlabel.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,78 @@ def test_autosectionlabel_maxdepth(app):
assert re.search(html, content, re.DOTALL)

assert "WARNING: undefined label: 'linux'" in app.warning.getvalue()


@pytest.mark.sphinx('html', testroot='ext-autosectionlabel-full-reference')
def test_autosectionlabel_full_reference(app):
andrewjmaguire marked this conversation as resolved.
Show resolved Hide resolved
app.build(force_all=True)

content = (app.outdir / 'index.html').read_text(encoding='utf8')
html = (
'<li><p><a class="reference internal" href="#index.Introduction-of-Sphinx">'
'<span class="std std-ref">Introduction of Sphinx</span></a></p></li>'
)
assert re.search(html, content, re.DOTALL)

html = (
'<li><p><a class="reference internal" href="#index.Installation">'
'<span class="std std-ref">Installation</span></a></p></li>'
)
assert re.search(html, content, re.DOTALL)

html = (
'<li><p><a class="reference internal" href="#index.Installation.For-Windows-users">'
'<span class="std std-ref">For Windows users</span></a></p></li>'
)
assert re.search(html, content, re.DOTALL)

html = (
'<li><p><a class="reference internal" href="#index.Installation.For-Windows-users.Windows">'
'<span class="std std-ref">Windows</span></a></p></li>'
)
assert re.search(html, content, re.DOTALL)

html = (
'<li><p><a class="reference internal" href="#index.Installation.For-Windows-users.Windows.Command">'
'<span class="std std-ref">Command</span></a></p></li>'
)
assert re.search(html, content, re.DOTALL)

html = (
'<li><p><a class="reference internal" href="#index.Installation.For-UNIX-users">'
'<span class="std std-ref">For UNIX users</span></a></p></li>'
)
assert re.search(html, content, re.DOTALL)

html = (
'<li><p><a class="reference internal" href="#index.Installation.For-UNIX-users.Linux">'
'<span class="std std-ref">Linux</span></a></p></li>'
)
assert re.search(html, content, re.DOTALL)

html = (
'<li><p><a class="reference internal" href="#index.Installation.For-UNIX-users.Linux.Command">'
'<span class="std std-ref">Command</span></a></p></li>'
)
assert re.search(html, content, re.DOTALL)

html = (
'<li><p><a class="reference internal" href="#index.Installation.For-UNIX-users.FreeBSD">'
'<span class="std std-ref">FreeBSD</span></a></p></li>'
)
assert re.search(html, content, re.DOTALL)

html = (
'<li><p><a class="reference internal" href="#index.Installation.For-UNIX-users.FreeBSD.Command">'
'<span class="std std-ref">Command</span></a></p></li>'
)
assert re.search(html, content, re.DOTALL)

# for smart_quotes (refs: #4027)
html = (
'<li><p><a class="reference internal" '
'href="#index.Installation.This-one-s-got-an-apostrophe">'
'<span class="std std-ref">This one’s got an apostrophe'
'</span></a></p></li>'
)
assert re.search(html, content, re.DOTALL)
Loading