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

milde--- via Docutils-checkins <[email protected]> Thu, 16 Jul 2026 10:43:33 +0000
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 10386
          http://sourceforge.net/p/docutils/code/10386
Author:   milde
Date:     2026-07-16 10:43:33 +0000 (Thu, 16 Jul 2026)
Log Message:
-----------
Fix issue with inline targets containing substring "refname" or "refuri".

`nodes.Node.next_node()` may return a `nodes.Text` instance
where ``if "refname" in node`` does not test for attributes but
the substring "refname" in the text content.

This led to `<target names="%refname.att">%refname.att</target>`
beeing wrongly identified as an indirect target and a subsequent
AssertionError because the corresponding `<reference>` did not get
a "refid" attribute.

Adding a check makes `PropagateTargets.next_suitable_target()`
return None if called with a non-empty start target.

For added robustness, `PropagateTargets.is_internal()` checks if
the given argument is a `nodes.Element` instance.

Modified Paths:
--------------
    trunk/docutils/docutils/nodes.py
    trunk/docutils/docutils/transforms/references.py
    trunk/docutils/docutils/writers/_html_base.py
    trunk/docutils/test/test_nodes.py

Modified: trunk/docutils/docutils/nodes.py
===================================================================
--- trunk/docutils/docutils/nodes.py	2026-07-16 10:43:24 UTC (rev 10385)
+++ trunk/docutils/docutils/nodes.py	2026-07-16 10:43:33 UTC (rev 10386)
@@ -688,6 +688,9 @@
 
     def __contains__(self, key: str | Node) -> bool:
         # Test for both, children and attributes with operator ``in``.
+        #
+        # Caution: Document Tree traversal also returns Text nodes where
+        # ``in`` looks for substrings in the text content.
         if isinstance(key, str):
             return key in self.attributes
         return key in self.children

Modified: trunk/docutils/docutils/transforms/references.py
===================================================================
--- trunk/docutils/docutils/transforms/references.py	2026-07-16 10:43:24 UTC (rev 10385)
+++ trunk/docutils/docutils/transforms/references.py	2026-07-16 10:43:33 UTC (rev 10386)
@@ -133,8 +133,11 @@
             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
+        # Return the next suitable element for the transfer of the
+        # "names" and "ids" attributes from an empty "internal" <target>
+        # (target propagation).
+        if not isinstance(target, nodes.target) or len(target):
+            return None
         candidate = target.next_node(ascend=True)
         # skip system messages (may be removed by universal.FilterMessages)
         while isinstance(candidate, nodes.system_message):
@@ -147,8 +150,10 @@
             return None
         return candidate
 
-    def is_internal(self, node: nodes.Element) -> bool:
-        # Return True, if `node` is an internal hyperlink target.
+    def is_internal(self, node: nodes.Node) -> bool:
+        # Return True, if `node` is a potential internal hyperlink target.
+        if not isinstance(node, nodes.Element):
+            return False
         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:

Modified: trunk/docutils/docutils/writers/_html_base.py
===================================================================
--- trunk/docutils/docutils/writers/_html_base.py	2026-07-16 10:43:24 UTC (rev 10385)
+++ trunk/docutils/docutils/writers/_html_base.py	2026-07-16 10:43:33 UTC (rev 10386)
@@ -1526,7 +1526,7 @@
             atts['classes'].append('external')
         else:
             assert 'refid' in node, \
-                   'References must have "refuri" or "refid" attribute.'
+                   f'Reference without "refuri" or "refid" attribute: {node}'
             atts['href'] = '#' + node['refid']
             atts['classes'].append('internal')
         if len(node) == 1 and isinstance(node[0], nodes.image):

Modified: trunk/docutils/test/test_nodes.py
===================================================================
--- trunk/docutils/test/test_nodes.py	2026-07-16 10:43:24 UTC (rev 10385)
+++ trunk/docutils/test/test_nodes.py	2026-07-16 10:43:33 UTC (rev 10386)
@@ -94,6 +94,14 @@
         e[0][1] += nodes.Text('some text')
         e += nodes.Element()
         e += nodes.Element()
+        # Tree                      Index         testlist
+        # <Element>                 e
+        #     <Element>             e[0]          skip
+        #         <Element>         e[0][0]
+        #         <TextElement>     e[0][1]       skip
+        #             <Text>        e[0][1][0]
+        #     <Element>             e[1]          skip
+        #     <Element>             e[2]
         self.testlist = [e[0], e[0][1], e[1]]
         compare = [(e, e[0][0]),
                    (e[0], e[0][0]),

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