SF.net SVN: docutils:[10303 ] trunk/docutils

milde--- via Docutils-checkins <[email protected]> Fri, 27 Mar 2026 08:41:11 +0000
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 10303
          http://sourceforge.net/p/docutils/code/10303
Author:   milde
Date:     2026-03-27 08:41:10 +0000 (Fri, 27 Mar 2026)
Log Message:
-----------
Generate identifiers for sections and ToC in transforms.

If the setting "legacy_ids" is False, the rST parser does not generate
IDs for implicit targets. OTOH, writers and the `parts.Contents` transform
rely on section IDs. `transforms.parts.Contents` also expects an identifier
for the `<topic>` containing the table of contents.

New and changed transforms:

`references.SectionIDs`
   new, ensures all sections have an identifier.

`references.IndirectHyperlinks?
   don't break, if the "relay target" points to an element with
   reference-name but no identifier.

`parts.Contents`
   sets an identifier for the table of contents.

Modified Paths:
--------------
    trunk/docutils/HISTORY.rst
    trunk/docutils/RELEASE-NOTES.rst
    trunk/docutils/docs/api/transforms.rst
    trunk/docutils/docutils/parsers/rst/__init__.py
    trunk/docutils/docutils/transforms/parts.py
    trunk/docutils/docutils/transforms/references.py
    trunk/docutils/test/test_transforms/test_contents.py
    trunk/docutils/test/test_transforms/test_hyperlinks.py
    trunk/docutils/test/test_transforms/test_sectnum.py

Modified: trunk/docutils/HISTORY.rst
===================================================================
--- trunk/docutils/HISTORY.rst	2026-03-27 08:40:56 UTC (rev 10302)
+++ trunk/docutils/HISTORY.rst	2026-03-27 08:41:10 UTC (rev 10303)
@@ -41,6 +41,7 @@
 * docutils/parsers/rst/__init__.py
 
   - New configuration setting `legacy_ids`_.
+  - Load new transform `references.SectionIDs`.
 
 * docutils/parsers/rst/directives/body.py
 
@@ -74,6 +75,10 @@
   - Use `nodes.transition.validate_position()` to warn about transitions
     at the beginning or end of the document or a section.
 
+* docutils/transforms/references.py:
+
+  - New transform `SectionIDs`: ensure all sections have an "identifier".
+
 * docutils/writers/html5_polyglot/__init__.py
 
   - Use a section's last "ids" attribute for the "section-self-link".

Modified: trunk/docutils/RELEASE-NOTES.rst
===================================================================
--- trunk/docutils/RELEASE-NOTES.rst	2026-03-27 08:40:56 UTC (rev 10302)
+++ trunk/docutils/RELEASE-NOTES.rst	2026-03-27 08:41:10 UTC (rev 10303)
@@ -323,6 +323,8 @@
   `nodes.document.set_duplicate_name()`
     Called by `nodes.document.note_names()` to handle duplicate names.
     Provisional.
+  `transforms.SectionIDs`:
+    Ensure all sections have an identifier_.
 
 Removed objects
   `parsers.rst.directives.tables.CSVTable.check_requirements()`
@@ -1595,6 +1597,7 @@
 .. _Docutils Document Model:
 .. _Docutils XML: docs/ref/doctree.html
 .. _"colwidth" attribute: docs/ref/doctree.html#colwidth
+.. _identifier: docs/ref/doctree.html#identifiers
 .. _<doctest_block>: docs/ref/doctree.html#doctest-block
 .. _reference names:  docs/ref/doctree.html#reference-names
 .. _<target>: docs/ref/doctree.html#target

Modified: trunk/docutils/docs/api/transforms.rst
===================================================================
--- trunk/docutils/docs/api/transforms.rst	2026-03-27 08:40:56 UTC (rev 10302)
+++ trunk/docutils/docs/api/transforms.rst	2026-03-27 08:41:10 UTC (rev 10303)
@@ -57,6 +57,8 @@
 
 references_.Substitutions           standalone_ (r), pep_ (r)     _`220`
 
+references_.SectionIDs              rst_ (p)                      _`240`
+
 references_.PropagateTargets        standalone_ (r), pep_ (r)     _`260`
 
 frontmatter.\ DocTitle_             standalone_ (r)               _`320`
@@ -220,7 +222,8 @@
   .. _rst:
 
 parsers.rst.Parser
-  universal.SmartQuotes                 (855_)
+  | references.SectionIDs               (240_)
+  | universal.SmartQuotes               (855_)
 
   .. _writers:
 

Modified: trunk/docutils/docutils/parsers/rst/__init__.py
===================================================================
--- trunk/docutils/docutils/parsers/rst/__init__.py	2026-03-27 08:40:56 UTC (rev 10302)
+++ trunk/docutils/docutils/parsers/rst/__init__.py	2026-03-27 08:41:10 UTC (rev 10303)
@@ -75,7 +75,7 @@
 import docutils.statemachine
 from docutils.parsers.rst import roles, states
 from docutils import frontend, nodes
-from docutils.transforms import universal
+from docutils.transforms import references, universal
 
 
 class Parser(docutils.parsers.Parser):
@@ -169,7 +169,8 @@
         self.inliner = inliner
 
     def get_transforms(self):
-        return super().get_transforms() + [universal.SmartQuotes]
+        return [*super().get_transforms(),
+                references.SectionIDs, universal.SmartQuotes]
 
     def parse(self, inputstring, document) -> None:
         """Parse `inputstring` and populate `document`, a document tree."""

Modified: trunk/docutils/docutils/transforms/parts.py
===================================================================
--- trunk/docutils/docutils/transforms/parts.py	2026-03-27 08:40:56 UTC (rev 10302)
+++ trunk/docutils/docutils/transforms/parts.py	2026-03-27 08:41:10 UTC (rev 10303)
@@ -86,6 +86,8 @@
     default_priority = 720
 
     def apply(self) -> None:
+        # ensure the "ToC topic" wrapper element has an identifier:
+        self.toc_id = self.document.set_id(self.startnode.parent)
         # let the writer (or output software) build the contents list?
         toc_by_writer = getattr(self.document.settings, 'use_latex_toc', False)
         # TODO: handle "generate_oowriter_toc" setting of the "ODT" writer.
@@ -100,7 +102,6 @@
                 startnode = startnode.parent
         else:
             startnode = self.document
-        self.toc_id = self.startnode.parent['ids'][0]
         if 'backlinks' in details:
             self.backlinks = details['backlinks']
         else:

Modified: trunk/docutils/docutils/transforms/references.py
===================================================================
--- trunk/docutils/docutils/transforms/references.py	2026-03-27 08:40:56 UTC (rev 10302)
+++ trunk/docutils/docutils/transforms/references.py	2026-03-27 08:41:10 UTC (rev 10303)
@@ -14,9 +14,29 @@
 from docutils.transforms import Transform
 
 
-class PropagateTargets(Transform):
+class SectionIDs(Transform):
+    """
+    Add identifiers to sections.
 
+    If the "legacy_ids" configuration setting is False, the rST parser
+    does not generate identifiers for implicit targets (e.g. sections)
+    in order to give explicit targets preferential access to identifiers
+    matching their reference name.
+
+    However, the `parts.Contents` transform and most writers
+    expect sections to have an identifier, so this transform adds them.
     """
+    default_priority = 240
+
+    def apply(self) -> None:
+        if getattr(self.document.settings, "legacy_ids", True):
+            return
+        for node in self.document.findall(nodes.section):
+            self.document.set_id(node)
+
+
+class PropagateTargets(Transform):
+    """
     Propagate empty internal targets to the next element.
 
     Given the following nodes::
@@ -222,22 +242,26 @@
             self.resolve_indirect_references(target)
 
     def resolve_indirect_target(self, target) -> None:
+        # indirect targets have either a refname or refid attribute
         refname = target.get('refname')
-        if refname is None:
-            reftarget_id = target['refid']
+        refid = target.get('refid')
+        if refid:
+            reftarget = self.document.ids.get(refid)
         else:
-            reftarget_id = self.document.nameids.get(refname)
-            if not reftarget_id:
-                # Check the unknown_reference_resolvers
-                for resolver_function in \
-                        self.document.transformer.unknown_reference_resolvers:
-                    if resolver_function(target):
-                        break
-                else:
-                    self.nonexistent_indirect_target(target)
-                return
-        reftarget = self.document.ids[reftarget_id]
-        reftarget.note_referenced_by(id=reftarget_id)
+            reftarget = self.document.names.get(refname)
+            refid = self.document.nameids.get(refname)
+            if reftarget and not refid:
+                refid = self.document.set_id(reftarget)
+        if not reftarget:
+            # Check the unknown_reference_resolvers
+            for resolver_function in \
+                self.document.transformer.unknown_reference_resolvers:
+                if resolver_function(target):
+                    break
+            else:
+                self.nonexistent_indirect_target(target)
+            return
+        reftarget.note_referenced_by(id=refid)
         if (isinstance(reftarget, nodes.target)
             and not reftarget.resolved
             and reftarget.hasattr('refname')):
@@ -256,7 +280,7 @@
             self.document.note_refid(target)
         else:
             if reftarget['ids']:
-                target['refid'] = reftarget_id
+                target['refid'] = refid
                 self.document.note_refid(target)
             else:
                 self.nonexistent_indirect_target(target)
@@ -266,7 +290,7 @@
         target.resolved = True
 
     def nonexistent_indirect_target(self, target) -> None:
-        if target['refname'] in self.document.nameids:
+        if self.document.names.get(target['refname'], '') is None:
             self.indirect_target_error(target, 'which is a duplicate, and '
                                        'cannot be used as a unique reference')
         else:

Modified: trunk/docutils/test/test_transforms/test_contents.py
===================================================================
--- trunk/docutils/test/test_transforms/test_contents.py	2026-03-27 08:40:56 UTC (rev 10302)
+++ trunk/docutils/test/test_transforms/test_contents.py	2026-03-27 08:41:10 UTC (rev 10303)
@@ -20,7 +20,7 @@
 
 from docutils.frontend import get_default_settings
 from docutils.parsers.rst import Parser
-from docutils.transforms.references import Substitutions
+from docutils.transforms.references import SectionIDs, Substitutions
 from docutils.transforms.universal import TestMessages
 from docutils.utils import new_document
 
@@ -46,7 +46,7 @@
 
 totest = {}
 
-totest['tables_of_contents'] = ((Substitutions,), [
+totest['tables_of_contents'] = ((SectionIDs, Substitutions,), [
 ["""\
 .. contents::
 

Modified: trunk/docutils/test/test_transforms/test_hyperlinks.py
===================================================================
--- trunk/docutils/test/test_transforms/test_hyperlinks.py	2026-03-27 08:40:56 UTC (rev 10302)
+++ trunk/docutils/test/test_transforms/test_hyperlinks.py	2026-03-27 08:41:10 UTC (rev 10303)
@@ -21,7 +21,7 @@
 from docutils.parsers.rst import Parser
 from docutils.transforms.references import PropagateTargets, \
      AnonymousHyperlinks, IndirectHyperlinks, ExternalTargets, \
-     InternalTargets, DanglingReferences
+     InternalTargets, DanglingReferences, SectionIDs
 from docutils.transforms.universal import TestMessages
 from docutils.utils import new_document
 
@@ -31,7 +31,7 @@
 
     transforms = (PropagateTargets, AnonymousHyperlinks, IndirectHyperlinks,
                   ExternalTargets, InternalTargets, DanglingReferences,
-                  TestMessages)
+                  SectionIDs, TestMessages)
 
     def test_transforms(self):
         parser = Parser()
@@ -1100,7 +1100,9 @@
 """],
 ])
 
-totest['hyperlinks'] = ({'legacy_ids': False}, [
+totest['hyperlinks'] = ({'legacy_ids': False},
+                        # all but the last sample compile as before
+                        totest['hyperlinks legacy'][1][:-1] + [
 ["""\
 foo
 ---
@@ -1114,7 +1116,7 @@
 """,
 """\
 <document source="test data">
-    <section dupnames="foo">
+    <section dupnames="foo" ids="foo-1">
         <title>
             foo
         <system_message backrefs="foo" level="1" line="5" source="test data" type="INFO">

Modified: trunk/docutils/test/test_transforms/test_sectnum.py
===================================================================
--- trunk/docutils/test/test_transforms/test_sectnum.py	2026-03-27 08:40:56 UTC (rev 10302)
+++ trunk/docutils/test/test_transforms/test_sectnum.py	2026-03-27 08:41:10 UTC (rev 10303)
@@ -20,7 +20,7 @@
 
 from docutils.frontend import get_default_settings
 from docutils.parsers.rst import Parser
-from docutils.transforms.references import Substitutions
+from docutils.transforms.references import SectionIDs, Substitutions
 from docutils.transforms.universal import TestMessages
 from docutils.utils import new_document
 
@@ -46,7 +46,7 @@
 
 totest = {}
 
-totest['section_numbers'] = ((Substitutions,), [
+totest['section_numbers'] = ((SectionIDs, Substitutions), [
 ["""\
 .. sectnum::
 

This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.