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

milde--- via Docutils-checkins <[email protected]> Fri, 05 Sep 2025 14:07:31 +0000
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 10227
          http://sourceforge.net/p/docutils/code/10227
Author:   milde
Date:     2025-09-05 14:07:30 +0000 (Fri, 05 Sep 2025)
Log Message:
-----------
Adjustments for nested_parse().

* Ensure valid output and update the "current node", if the base node is
  the "current node".

  If the base node is the state machine's "current node", we can safely assume
  that it is attached to the document and check if sections are valid. We can
  also update the "current node" of the parent state machine(s) after parsing.
  This prevents invalid or mixed up document trees.

* Make the "current node" the default value for the "node" argument.
  (This allows simple use for content included from external sources.)

* Restore `memo.section_level` after nested parsing into a custom node.
  The calling code must ensure that section_level and current node stay in sync.

* Update unit tests.

Modified Paths:
--------------
    trunk/docutils/HISTORY.rst
    trunk/docutils/docutils/nodes.py
    trunk/docutils/docutils/parsers/rst/states.py
    trunk/docutils/test/test_nodes.py
    trunk/docutils/test/test_parsers/test_rst/test_nested_parsing.py

Modified: trunk/docutils/HISTORY.rst
===================================================================
--- trunk/docutils/HISTORY.rst	2025-09-05 09:10:58 UTC (rev 10226)
+++ trunk/docutils/HISTORY.rst	2025-09-05 14:07:30 UTC (rev 10227)
@@ -32,8 +32,8 @@
   - Revert to using `document.memo.section_level` to fix behaviour with
     nested parsing into a detached node (cf. bugs #508 and #509).
   - Set `parent_state_machine` attribute when creating nested state machines.
-    Allows passing an updated "current node" to the parent state machine,
-    e.g. for changing the section level in a directive.
+    Use it to update the "current node" of the parent state machines after
+    nested parsing.
   - Better error messages for grid table markup errors (bug #504),
     based on patch #214 by Jynn Nelson.
 

Modified: trunk/docutils/docutils/nodes.py
===================================================================
--- trunk/docutils/docutils/nodes.py	2025-09-05 09:10:58 UTC (rev 10226)
+++ trunk/docutils/docutils/nodes.py	2025-09-05 14:07:30 UTC (rev 10227)
@@ -818,7 +818,7 @@
         return self.parent[i-1] if i > 0 else None
 
     def section_hierarchy(self) -> list[section]:
-        """Return the element's section hierarchy.
+        """Return the element's section anchestors.
 
         Return a list of all <section> elements that contain `self`
         (including `self` if it is a <section>) and have a parent node.

Modified: trunk/docutils/docutils/parsers/rst/states.py
===================================================================
--- trunk/docutils/docutils/parsers/rst/states.py	2025-09-05 09:10:58 UTC (rev 10226)
+++ trunk/docutils/docutils/parsers/rst/states.py	2025-09-05 14:07:30 UTC (rev 10227)
@@ -265,7 +265,7 @@
     def nested_parse(self,
                      block: StringList,
                      input_offset: int,
-                     node: nodes.Element,
+                     node: nodes.Element|None = None,
                      match_titles: bool = False,
                      state_machine_class: StateMachineWS|None = None,
                      state_machine_kwargs: dict|None = None
@@ -279,9 +279,11 @@
             Line number at start of the block.
         :node:
             Base node. Generated nodes will be appended to this node.
+            Default: the "current node" (`self.state_machine.node`).
         :match_titles:
             Allow section titles?
-            Caution: May lead to an invalid or mixed up document tree. [#]_
+            Caution: With a custom base node, this may lead to an invalid
+            or mixed up document tree. [#]_
         :state_machine_class:
             Default: `NestedStateMachine`.
         :state_machine_kwargs:
@@ -297,6 +299,8 @@
         __ https://www.sphinx-doc.org/en/master/extdev/utils.html
            #sphinx.util.parsing.nested_parse_to_nodes
         """
+        if node is None:
+            node = self.state_machine.node
         use_default = 0
         if state_machine_class is None:
             state_machine_class = self.nested_sm
@@ -316,10 +320,30 @@
                                   debug=self.debug,
                                   parent_state_machine=self.state_machine,
                                   **state_machine_kwargs)
+        # Check if we may use sections (with a caveat for custom nodes
+        # that may be dummies to collect children):
+        if (node == self.state_machine.node
+                and not isinstance(node, (nodes.document, nodes.section))):
+            match_titles = False  # avoid invalid sections
+
         # run the state machine and populate `node`:
         block_length = len(block)
+        old_section_level = self.memo.section_level
         my_state_machine.run(block, input_offset, memo=self.memo,
                              node=node, match_titles=match_titles)
+
+        if match_titles:
+            if node == self.state_machine.node:
+                # Pass on the new "current node" to parent state machines:
+                sm = self.state_machine
+                try:
+                    while True:
+                        sm.node = my_state_machine.node
+                        sm = sm.parent_state_machine
+                except AttributeError:
+                    pass
+            else:
+                self.memo.section_level = old_section_level
         # clean up
         new_offset = my_state_machine.abs_line_offset()
         if use_default == 2:
@@ -431,10 +455,9 @@
                     line=lineno)
                 return False
             self.parent = new_parent
-        # Update memo:
+            self.memo.section_level = newlevel - 1
         if newlevel > len(title_styles):
             title_styles.append(style)
-        self.memo.section_level = newlevel
         return True
 
     def title_inconsistent(self, sourcetext, lineno):
@@ -458,6 +481,7 @@
         self.document.note_implicit_target(section_node, section_node)
         # Update state:
         self.parent = section_node
+        self.memo.section_level += 1
 
     def paragraph(self, lines, lineno):
         """

Modified: trunk/docutils/test/test_nodes.py
===================================================================
--- trunk/docutils/test/test_nodes.py	2025-09-05 09:10:58 UTC (rev 10226)
+++ trunk/docutils/test/test_nodes.py	2025-09-05 14:07:30 UTC (rev 10227)
@@ -233,6 +233,25 @@
         self.assertEqual(c1.previous_sibling(), None)
         self.assertEqual(c2.previous_sibling(), c1)
 
+    def test_section_hierarchy(self):
+        p = nodes.paragraph()
+        a = nodes.admonition('', p)
+        self.assertEqual(p.section_hierarchy(), [])
+        s2_1 = nodes.section('', a)
+        self.assertEqual(p.section_hierarchy(), [])
+        s2_2 = nodes.section()
+        s1 = nodes.section()
+        s2 = nodes.section('', s2_1, s2_2)
+        self.assertEqual(p.section_hierarchy(), [s2_1])
+        d = utils.new_document('test data')
+        d += [nodes.paragraph(), s1, s2]
+        self.assertEqual(d.section_hierarchy(), [])
+        self.assertEqual(d[0].section_hierarchy(), [])
+        self.assertEqual(s2.section_hierarchy(), [s2])
+        self.assertEqual(s2_1.section_hierarchy(), [s2, s2_1])
+        self.assertEqual(a.section_hierarchy(), [s2, s2_1])
+        self.assertEqual(p.section_hierarchy(), [s2, s2_1])
+
     def test_clear(self):
         element = nodes.Element()
         element += nodes.Element()

Modified: trunk/docutils/test/test_parsers/test_rst/test_nested_parsing.py
===================================================================
--- trunk/docutils/test/test_parsers/test_rst/test_nested_parsing.py	2025-09-05 09:10:58 UTC (rev 10226)
+++ trunk/docutils/test/test_parsers/test_rst/test_nested_parsing.py	2025-09-05 14:07:30 UTC (rev 10227)
@@ -46,17 +46,17 @@
     has_content = True
 
     def run(self):
-        # cf. sphinx.util.parsing.nested_parse_to_nodes()
         node = nodes.Element()
         node.document = self.state.document
-        # support sections (unless we know it is invalid):
+        # Support sections (unless we know it is invalid):
         match_titles = isinstance(self.state_machine.node,
                                   (nodes.document, nodes.section))
-        self.state.nested_parse(self.content, input_offset=0,
+
+        self.state.nested_parse(self.content, input_offset=self.content_offset,
                                 node=node, match_titles=match_titles)
-        # Append and move the "insertion point" to the last nested section.
         self.state_machine.node += node.children
-        # print(self.state_machine, self.state_machine.node[-1].shortrepr())
+
+        # Move the "insertion point" to the last nested section.
         try:
             while isinstance(self.state_machine.node[-1], nodes.section):
                 self.state_machine.node = self.state_machine.node[-1]
@@ -70,16 +70,18 @@
                 sm.node = self.state_machine.node
         except AttributeError:
             pass
+        # Update section level:
+        self.state_machine.memo.section_level = len(
+            self.state_machine.node.section_hierarchy())
         return []  # node already attached to document
 
 
 class ParseIntoCurrentNode(ParseIntoNode):
-    # Attention: this directive is flawed:
-    # * no check for section validity,
-    # * "current" node not updated! -> element order may get lost.
+    # If `node` is the "current node", `nested_parse()` ensures validity
+    # and updates the "current node".
     def run(self):
-        node = self.state_machine.node  # the current "insertion point"
-        self.state.nested_parse(self.content, 0, node, match_titles=True)
+        self.state.nested_parse(self.content, self.content_offset,
+                                match_titles=True)
         return []  # node already attached to document
 
 
@@ -91,7 +93,8 @@
     # * "current" node not updated! -> element order may get lost.
     def run(self):
         node = nodes.section()
-        self.state.nested_parse(self.content, 0, node, match_titles=True)
+        self.state.nested_parse(self.content, self.content_offset,
+                                node, match_titles=True)
         return node.children
 
 
@@ -169,7 +172,7 @@
     <section ids="sec2" names="sec2">
         <title>
             sec2
-        <system_message level="3" line="1" source="test data" type="ERROR">
+        <system_message level="3" line="16" source="test data" type="ERROR">
             <paragraph>
                 Inconsistent title style: skip from level 1 to 3.
             <literal_block xml:space="preserve">
@@ -180,7 +183,7 @@
         <section ids="nested2-1" names="nested2.1">
             <title>
                 nested2.1
-            <system_message level="3" line="5" source="test data" type="ERROR">
+            <system_message level="3" line="20" source="test data" type="ERROR">
                 <paragraph>
                     A level 1 section cannot be used here.
                 <literal_block xml:space="preserve">
@@ -211,7 +214,9 @@
 
   nested1
   *******
-  nested1.1
+  nested2
+  *******
+  nested2.1
   ---------
 
 This paragraph belongs to the last nested section.
@@ -221,9 +226,12 @@
     <section ids="nested1" names="nested1">
         <title>
             nested1
-        <section ids="nested1-1" names="nested1.1">
+    <section ids="nested2" names="nested2">
+        <title>
+            nested2
+        <section ids="nested2-1" names="nested2.1">
             <title>
-                nested1.1
+                nested2.1
             <paragraph>
                 This paragraph belongs to the last nested section.
 """],
@@ -304,9 +312,6 @@
             <section ids="nc1-1-1" names="nc1.1.1">
                 <title>
                     nc1.1.1
-            <section ids="sec2-2" names="sec2.2">
-                <title>
-                    sec2.2
         <section ids="nc1-2" names="nc1.2">
             <title>
                 nc1.2
@@ -313,6 +318,9 @@
     <section ids="nc2" names="nc2">
         <title>
             nc2
+        <section ids="sec2-2" names="sec2.2">
+            <title>
+                sec2.2
 """],
 # Flawed directive (no update of "current node"):
 ["""\
@@ -326,6 +334,9 @@
   *******************
 
 This paragraph belongs to the last nested section (sic!).
+
+sec2
+====
 """,
 """\
 <document source="test data">
@@ -340,6 +351,9 @@
                     nested-section1.1.1
             <paragraph>
                 This paragraph belongs to the last nested section (sic!).
+    <section ids="sec2" names="sec2">
+        <title>
+            sec2
     <system_message level="2" line="10" source="test data" type="WARNING">
         <paragraph>
             Element <section ids="sec1-1" names="sec1.1"> invalid:
@@ -362,7 +376,7 @@
     <section ids="sec1" names="sec1">
         <title>
             sec1
-        <system_message level="3" line="1" source="test data" type="ERROR">
+        <system_message level="3" line="5" source="test data" type="ERROR">
             <paragraph>
                 A level 1 section cannot be used here.
             <literal_block xml:space="preserve">
@@ -388,8 +402,8 @@
 
   .. nested-current::
 
-    invalid, too (sic!)
-    ===================
+    invalid, too
+    ============
 
   .. nested-section::
 
@@ -409,9 +423,12 @@
             <literal_block xml:space="preserve">
                 invalid section
                 ---------------
-        <section ids="invalid-too-sic" names="invalid,\\ too\\ (sic!)">
-            <title>
-                invalid, too (sic!)
+        <system_message level="3" line="11" source="test data" type="ERROR">
+            <paragraph>
+                Unexpected section title.
+            <literal_block xml:space="preserve">
+                invalid, too
+                ============
         <paragraph>
             The <section> base node is discarded.
         <section ids="invalid-section-sic" names="invalid\\ section\\ (sic!)">
@@ -420,7 +437,7 @@
     <system_message level="2" line="1" source="test data" type="WARNING">
         <paragraph>
             Element <block_quote> invalid:
-              Child element <section ids="invalid-too-sic" names="invalid,\\ too\\ (sic!)"> not allowed at this position.
+              Child element <section ids="invalid-section-sic" names="invalid\\ section\\ (sic!)"> not allowed at this position.
 """],
 ]
 

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