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

milde--- via Docutils-checkins <[email protected]>
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 9732
          http://sourceforge.net/p/docutils/code/9732
Author:   milde
Date:     2024-06-05 15:54:27 +0000 (Wed, 05 Jun 2024)
Log Message:
-----------
Doctree validation: additional checks for `subtitle` and `transition` position.

A `subtitle` must not be used without preceding `title`.

A `transition` must not be used at the start or end of a section or document,
or after another transition.

Modified Paths:
--------------
    trunk/docutils/HISTORY.txt
    trunk/docutils/docutils/nodes.py
    trunk/docutils/test/test_nodes.py

Modified: trunk/docutils/HISTORY.txt
===================================================================
--- trunk/docutils/HISTORY.txt	2024-06-05 15:54:16 UTC (rev 9731)
+++ trunk/docutils/HISTORY.txt	2024-06-05 15:54:27 UTC (rev 9732)
@@ -30,8 +30,8 @@
   - New element category classes `SubStructural` and `PureTextElement`.
   - Fix element categories.
   - New method `Element.validate()`: raise `nodes.ValidationError` if
-    the element does not comply with the "Docutils Document Model"
-    (work in progress).
+    the element does not comply with the "Docutils Document Model".
+    Provisional.
   - New "attribute validating functions"
     convert string representations to correct data type,
     normalize values,

Modified: trunk/docutils/docutils/nodes.py
===================================================================
--- trunk/docutils/docutils/nodes.py	2024-06-05 15:54:16 UTC (rev 9731)
+++ trunk/docutils/docutils/nodes.py	2024-06-05 15:54:27 UTC (rev 9732)
@@ -399,9 +399,14 @@
         return self.__class__(str.lstrip(self, chars))
 
     def validate(self, recursive=True):
-        pass  # Text nodes have no attributes and no children.
+        """Validate Docutils Document Tree element ("doctree")."""
+        # Text nodes have no attributes and no children.
 
+    def check_position(self):
+        """Hook for additional checks of the parent's content model."""
+        # no special placement requirements for Text nodes
 
+
 class Element(Node):
     """
     `Element` is the superclass to all specific elements.
@@ -1166,8 +1171,9 @@
                                           problematic_element=child)
                 else:  # quantifier in ('?', '*') -> optional child
                     continue  # try same child with next part of content model
-            # TODO: check additional placement constraints (if applicable)
-            # child.check_position()
+            else:
+                # Check additional placement constraints (if applicable):
+                child.check_position()
             # advance:
             if quantifier in ('.', '?'):  # go to next element
                 child = next(ichildren, None)
@@ -1175,6 +1181,7 @@
                 for child in ichildren:
                     if not isinstance(child, category):
                         break
+                    child.check_position()
                 else:
                     child = None
         return [] if child is None else [child, *ichildren]
@@ -1194,6 +1201,13 @@
         return (f'{msg}  Expecting child of type <{type}>, '
                 f'not {child.starttag()}.')
 
+    def check_position(self):
+        """Hook for additional checks of the parent's content model.
+
+        Raise ValidationError, if `self` is at an invalid position.
+        See `subtitle.check_position()` and `transition.check_position()`.
+        """
+
     def validate(self, recursive=True):
         """Validate Docutils Document Tree element ("doctree").
 
@@ -1420,9 +1434,17 @@
     valid_attributes = Element.valid_attributes + ('auto', 'refid')
 
 
-class subtitle(Titular, PreBibliographic, SubStructural, TextElement): pass
+class subtitle(Titular, PreBibliographic, SubStructural, TextElement):
+    """Sub-title of `document`, `section` and `sidebar`."""
 
+    def check_position(self):
+        """Check position of subtitle: must follow a title."""
+        if self.parent and self.parent.index(self) == 0:
+            raise ValidationError(f'Element {self.parent.starttag()} invalid:'
+                                  '\n  <subtitle> only allowed after <title>.',
+                                  problematic_element=self)
 
+
 class meta(PreBibliographic, SubStructural, Element):
     """Container for "invisible" bibliographic data, or meta-data."""
     valid_attributes = Element.valid_attributes + (
@@ -1453,13 +1475,36 @@
 
 
 class transition(SubStructural, Element):
-    """Transitions are breaks between untitled text parts.
+    """Transitions__ are breaks between untitled text parts.
 
-    A transition may not begin or end a section or document, nor may two
-    transitions be immediately adjacent.
+    __ https://docutils.sourceforge.io/docs/ref/doctree.html#transition
     """
 
+    def check_position(self):
+        """Check additional constraints on `transition` placement.
 
+        A transition may not begin or end a section or document,
+        nor may two transitions be immediately adjacent.
+        """
+        messages = [f'Element {self.parent.starttag()} invalid:']
+        predecessor = self.previous_sibling()
+        if (predecessor is None  # index == 0
+            or isinstance(predecessor, (title, subtitle, meta, decoration))
+            # A transition following these elements still counts as
+            # "at the beginning of a document or section".
+            ):
+            messages.append(
+                '<transition> may not begin a section or document.')
+        if self.parent.index(self) == len(self.parent) - 1:
+            messages.append('<transition> may not end a section or document.')
+        if isinstance(predecessor, transition):
+            messages.append(
+                '<transition> may not directly follow another transition.')
+        if len(messages) > 1:
+            raise ValidationError('\n  '.join(messages),
+                                  problematic_element=self)
+
+
 # Structural Elements
 # ===================
 
@@ -1486,11 +1531,15 @@
     content_model = (  # ((title, subtitle?)?, (%body.elements; | topic)+)
                      (title, '?'),
                      (subtitle, '?'),
-                     ((topic, Body), '+'))  # TODO complex model
+                     ((topic, Body), '+'))
+    # "subtitle only after title" is ensured in `subtitle.check_position()`.
 
 
 class section(Structural, Element):
-    """Document section. The main unit of hierarchy."""
+    """Document section__. The main unit of hierarchy.
+
+    __ https://docutils.sourceforge.io/docs/ref/doctree.html#section
+    """
     # recursive content model, see below
 
 
@@ -1499,7 +1548,8 @@
                          (subtitle, '?'),
                          ((Body, topic, sidebar, transition), '*'),
                          ((section, transition), '*'),
-                         )  # TODO complex model
+                         )
+# Correct transition placement is ensured in `transition.check_position()`.
 
 
 # Root Element
@@ -1527,7 +1577,7 @@
                       ((Body, topic, sidebar, transition), '*'),
                       ((section, transition), '*'),
                      )
-    # additional restrictions for `subtitle` and `transition` will be tested
+    # Additional restrictions for `subtitle` and `transition` are tested
     # with the respective `check_position()` methods.
 
     def __init__(self, settings, reporter, *args, **kwargs):

Modified: trunk/docutils/test/test_nodes.py
===================================================================
--- trunk/docutils/test/test_nodes.py	2024-06-05 15:54:16 UTC (rev 9731)
+++ trunk/docutils/test/test_nodes.py	2024-06-05 15:54:27 UTC (rev 9732)
@@ -762,8 +762,9 @@
         subtitle = nodes.subtitle()
         paragraph = nodes.paragraph()
         sidebar = nodes.sidebar('', subtitle, paragraph)
-        # TODO additional restriction "only after title"
-        sidebar.validate_content()
+        with self.assertRaisesRegex(nodes.ValidationError,
+                                    '<subtitle> only allowed after <title>.'):
+            sidebar.validate_content()
 
     def test_validate_content_transition(self):
         """Test additional constraints on <transition> placement:
@@ -773,8 +774,20 @@
         transition = nodes.transition()
         paragraph = nodes.paragraph()
         section = nodes.section('', nodes.title(), transition, paragraph)
-        # TODO: additional restrictions on transition placement.
-        section.validate_content()
+        with self.assertRaisesRegex(nodes.ValidationError,
+                                    '<transition> may not begin a section '):
+            section.validate_content()
+        section = nodes.section('', nodes.title(), paragraph, transition)
+        with self.assertRaisesRegex(nodes.ValidationError,
+                                    '<transition> may not end a section '):
+            section.validate_content()
+        section = nodes.section('', nodes.title(), paragraph,
+                                nodes.transition(), transition)
+        with self.assertRaisesRegex(nodes.ValidationError,
+                                    'Element <section> invalid:\n'
+                                    '  <transition> may not end .*\n'
+                                    '  <transition> may not directly '):
+            section.validate_content()
 
 
 class MiscTests(unittest.TestCase):

This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.