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

milde--- via Docutils-checkins <[email protected]> Thu, 16 Jul 2026 10:43:24 +0000
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 10385
          http://sourceforge.net/p/docutils/code/10385
Author:   milde
Date:     2026-07-16 10:43:24 +0000 (Thu, 16 Jul 2026)
Log Message:
-----------
"lazy IDs": handle "chained" targets.

Recursively test the next nodes of a to-be-propagated target; only set IDs
if it is an explicit internal target.

Modified Paths:
--------------
    trunk/docutils/HISTORY.rst
    trunk/docutils/RELEASE-NOTES.rst
    trunk/docutils/docutils/transforms/references.py
    trunk/docutils/test/test_transforms/test_hyperlinks.py

Modified: trunk/docutils/HISTORY.rst
===================================================================
--- trunk/docutils/HISTORY.rst	2026-07-16 10:43:14 UTC (rev 10384)
+++ trunk/docutils/HISTORY.rst	2026-07-16 10:43:24 UTC (rev 10385)
@@ -99,18 +99,18 @@
 
 * docutils/transforms/parts.py:
 
-  - `Contents.build_contents()`: ensure sections have an ID and prefer
-    IDs from external targets with "lazy IDs" (`legacy_ids`_ False).
+  - "lazy IDs": `Contents.build_contents()` ensures sections have an ID
+    and prefers IDs from external targets if `legacy_ids`_ is False.
 
 * docutils/transforms/references.py
 
   - `IndirectHyperlinks.resolve_indirect_target()` no longer calls
-    `unknown_reference_resolvers` and only sets IDs if required.
+    the `unknown_reference_resolvers` hook.
   - 3 new transforms, `MatchReferences`, `ReportDanglingReferences`,
     and `ReportUnreferencedLinks` obsolete `DanglingReferences`.
   - Add INFO system_message if a <target> cannot be propagated
     to the next node.
-  - support "lazy IDs": Handle hyperlink targets without ID;
+  - "lazy IDs": Handle hyperlink targets without ID;
     if required, generate and set one.
 
 * docutils/transforms/universal.py

Modified: trunk/docutils/RELEASE-NOTES.rst
===================================================================
--- trunk/docutils/RELEASE-NOTES.rst	2026-07-16 10:43:14 UTC (rev 10384)
+++ trunk/docutils/RELEASE-NOTES.rst	2026-07-16 10:43:24 UTC (rev 10385)
@@ -233,11 +233,9 @@
 
 rST parser:
   - Warn if a `"figure"`_ directive is missing both caption and legend.
-  - Don't generate identifiers for indirect or external targets
-    (unless legacy_ids_ is True).
-  - Generate identifiers for implicit targets (mainly sections) only if
-    there is a cross-link to the target [#cross-links]_ and no "explicit"
-    identifier (unless legacy_ids_ is True).
+  - "lazy IDs": Generate target ids_ in transforms -- after parsing and
+    only if required in the output document.
+    Keep behaviour backwards compatible with the legacy_ids_ setting.
 
 HTML5 writer:
   - Use normal font size and colour for informal titles of type "rubric".
@@ -298,6 +296,7 @@
    "`section self-links <section_self_link_>`_" added by the HTML5
    writer.
 
+
 Release 0.23 (2026-05-27)
 =========================
 
@@ -1609,8 +1608,9 @@
 .. _Docutils Document Model:
 .. _Docutils XML: docs/ref/doctree.html
 .. _"colwidth" attribute: docs/ref/doctree.html#colwidth
+.. _<doctest_block>: docs/ref/doctree.html#doctest-block
 .. _identifier: docs/ref/doctree.html#identifiers
-.. _<doctest_block>: docs/ref/doctree.html#doctest-block
+.. _ids: docs/ref/doctree.html#ids
 .. _reference names:  docs/ref/doctree.html#reference-names
 .. _<target>: docs/ref/doctree.html#target
 

Modified: trunk/docutils/docutils/transforms/references.py
===================================================================
--- trunk/docutils/docutils/transforms/references.py	2026-07-16 10:43:14 UTC (rev 10384)
+++ trunk/docutils/docutils/transforms/references.py	2026-07-16 10:43:24 UTC (rev 10385)
@@ -69,7 +69,7 @@
                 if node is None or not self.document.nametypes[name]:
                     continue
                 # Skip external or indirect targets:
-                if 'refid' in node or 'refname' in node or 'refuri' in node:
+                if not self.is_internal(node):
                     continue
                 self.document.set_id(node)
         # Now propatate internal <target>s:
@@ -80,15 +80,8 @@
                 or 'refname' in target
                 or 'refuri' in target):
                 continue
-            next_node = target.next_node(ascend=True)
-            # skip system messages (may be removed by universal.FilterMessages)
-            while isinstance(next_node, nodes.system_message):
-                next_node = next_node.next_node(ascend=True, descend=False)
-            # Do not move names and ids into Invisibles (we'd lose the
-            # attributes) or different Targetables (e.g. footnotes).
-            if (next_node is None
-                or isinstance(next_node, (nodes.Invisible, nodes.Targetable))
-                and not isinstance(next_node, nodes.target)):
+            next_node = self.next_suitable_node(target)
+            if next_node is None:
                 self.document.reporter.info(
                     f'Cannot propagate target "{" ".join(target["names"])}" '
                     'to next element', base_node=target)
@@ -135,13 +128,37 @@
                 self.document.note_refname(target)
             elif next_node['names']:
                 target['refname'] = next_node['names'][0]
-            else:
+            elif 'anonymous' not in next_node:
                 target['refid'] = self.document.set_id(next_node)
             target['names'] = []
 
+    def next_suitable_node(self, target: nodes.Element) -> nodes.Element:
+        if not isinstance(target, nodes.target):
+            return None  # only <target> ids/names are propagated
+        candidate = target.next_node(ascend=True)
+        # skip system messages (may be removed by universal.FilterMessages)
+        while isinstance(candidate, nodes.system_message):
+            candidate = candidate.next_node(ascend=True, descend=False)
+        # Do not move names and ids into Invisibles (we'd lose the
+        # attributes) or Targetables (<citation>, <footnote>).
+        # Other <target>s are OK.  TODO: why no citations and footnotes?
+        if (isinstance(candidate, (nodes.Invisible, nodes.Targetable))
+            and not isinstance(candidate, nodes.target)):
+            return None
+        return candidate
 
-class AnonymousHyperlinks(Transform):
+    def is_internal(self, node: nodes.Element) -> bool:
+        # Return True, if `node` is an internal hyperlink target.
+        if 'refid' in node or 'refname' in node or 'refuri' in node:
+            return False  # node is indirect or external target
+        # check destination of chained targets:
+        if next_node := self.next_suitable_node(node):
+            return self.is_internal(next_node)
+        return True
 
+
+class AnonymousHyperlinks(PropagateTargets):
+
     """
     Link anonymous references to targets.  Given::
 
@@ -186,6 +203,7 @@
                 msg.add_backref(prbid)
                 ref.replace_self(prb)
             return
+
         for ref, target in zip(anonymous_refs, anonymous_targets):
             if ref.hasattr('refid') or ref.hasattr('refuri'):
                 continue
@@ -201,6 +219,8 @@
                             target = self.document.ids[target['refid']]
                         elif 'refname' in target:  # indirect target
                             target = self.document.names[target['refname']]
+                        elif next_node := self.next_suitable_node(target):
+                            target = next_node
                         else:
                             self.document.set_id(target)
                         continue
@@ -1032,8 +1052,8 @@
                 naming = target['ids'][0]
             else:
                 # Propagated target: "ids" and "names" attributes moved
-                # to the node indicated by "refid" or "refname".
-                naming = target.get('refid') or target.get('refname', '???')
+                # to the node indicated by "refname" or "refid".
+                naming = target.get('refname') or target.get('refid', '???')
             self.document.reporter.info(
                 f'Hyperlink target "{naming}" is not referenced.',
                 base_node=target)

Modified: trunk/docutils/test/test_transforms/test_hyperlinks.py
===================================================================
--- trunk/docutils/test/test_transforms/test_hyperlinks.py	2026-07-16 10:43:14 UTC (rev 10384)
+++ trunk/docutils/test/test_transforms/test_hyperlinks.py	2026-07-16 10:43:24 UTC (rev 10385)
@@ -1300,8 +1300,8 @@
         <reference refuri="URI">
             target2
         , not the Title.
-    <target refid="target1">
-    <target ids="target1" names="target2 target1" refuri="URI">
+    <target refuri="URI">
+    <target names="target2 target1" refuri="URI">
     <section names="title">
         <title>
             Title
@@ -1401,8 +1401,8 @@
 """,
 """\
 <document source="test data">
-    <target refid="chained">
-    <target ids="chained" names="external\\ hyperlink chained" refuri="http://uri">
+    <target refuri="http://uri">
+    <target names="external\\ hyperlink chained" refuri="http://uri">
     <paragraph>
         <reference refuri="http://uri">
             External hyperlink
@@ -1450,7 +1450,7 @@
 <document source="test data">
     <target names="external\\ hyperlink" refuri="http://uri">
     <target refuri="http://uri">
-    <target ids="chained" names="indirect\\ hyperlink chained" refuri="http://uri">
+    <target names="indirect\\ hyperlink chained" refuri="http://uri">
     <paragraph>
         <reference refuri="http://uri">
             Chained
@@ -1477,7 +1477,7 @@
 <document source="test data">
     <target names="external" refuri="http://uri">
     <target names="indirect" refuri="http://uri">
-    <target refid="internal">
+    <target refname="internal">
     <reference ids="internal" names="internal" refuri="http://uri">
         <image uri="picture.png">
     <reference refuri="http://uri">
@@ -1503,7 +1503,7 @@
 """,
 """\
 <document source="test data">
-    <target refid="img1">
+    <target refname="img1">
     <reference ids="img1" names="img1" refuri="uri1.html">
         <image uri="pic1.png">
     <target anonymous="1" refid="image-1">
@@ -1548,9 +1548,9 @@
 """,
 """\
 <document source="test data">
-    <target anonymous="1" refid="target-1">
-    <target anonymous="1" refid="target-1">
-    <reference ids="target-1" refuri="uri1.html">
+    <target anonymous="1">
+    <target anonymous="1" refid="reference-1">
+    <reference ids="reference-1" refuri="uri1.html">
         <image uri="pic1.png">
     <paragraph>
         Two \n\
@@ -1560,8 +1560,8 @@
         <reference anonymous="1" refuri="uri1.html">
             image with target
          (sic!).
-    <target refid="named">
-    <target anonymous="1" refid="named">
+    <target refname="named">
+    <target anonymous="1" refname="named">
     <reference ids="named" names="named" refuri="uri2.html">
         <image uri="pic2.png">
     <paragraph>
@@ -1572,7 +1572,7 @@
             anonymous
          link to an image with target (sic!).
     <target anonymous="1" refname="named link">
-    <target refid="named-link">
+    <target refname="named link">
     <reference ids="named-link" names="named\\ link" refuri="uri3.html">
         <image uri="pic3.png">
     <paragraph>
@@ -1590,20 +1590,23 @@
 .. _external: http://indirect.external
 __ external_
 __
+__
 
 `Full syntax anonymous external hyperlink reference`__,
 `chained anonymous external reference`__,
 `simplified syntax anonymous external hyperlink reference`__,
 `indirect anonymous hyperlink reference`__,
-`internal anonymous hyperlink reference`__.
+`internal anonymous hyperlink reference`__,
+`second internal anonymous hyperlink reference`__.
 """,
 """\
 <document source="test data">
     <target anonymous="1" refuri="http://full">
-    <target anonymous="1" refid="target-1">
-    <target anonymous="1" ids="target-1" refuri="http://simplified">
+    <target anonymous="1">
+    <target anonymous="1" refuri="http://simplified">
     <target names="external" refuri="http://indirect.external">
     <target anonymous="1" refuri="http://indirect.external">
+    <target anonymous="1">
     <target anonymous="1" refid="paragraph-1">
     <paragraph ids="paragraph-1">
         <reference anonymous="1" refuri="http://full">
@@ -1620,6 +1623,9 @@
         ,
         <reference anonymous="1" refid="paragraph-1">
             internal anonymous hyperlink reference
+        ,
+        <reference anonymous="1" refid="paragraph-1">
+            second internal anonymous hyperlink reference
         .
 """],
 ["""\
@@ -1630,8 +1636,8 @@
 """,
 """\
 <document source="test data">
-    <target refid="chained">
-    <target anonymous="1" ids="chained" names="chained" refuri="http://anonymous">
+    <target refuri="http://anonymous">
+    <target anonymous="1" names="chained" refuri="http://anonymous">
     <paragraph>
         <reference anonymous="1" refuri="http://anonymous">
             Anonymous

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