SF.net SVN: docutils:[9725] trunk/docutils/docutils/nodes.py

milde--- via Docutils-checkins <[email protected]>
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 9725
          http://sourceforge.net/p/docutils/code/9725
Author:   milde
Date:     2024-06-05 10:22:44 +0000 (Wed, 05 Jun 2024)
Log Message:
-----------
Doctree validation: new attribute `Element.content_model`.

A Python-representation of the element's content model.

Replaces attributes `Element.valid_len` and `Element.valid_children`.

Allows validating category, number, and order of the children.

Work in progress. Still missing:

* validating method
* representation for complex content models.

Modified Paths:
--------------
    trunk/docutils/docutils/nodes.py

Modified: trunk/docutils/docutils/nodes.py
===================================================================
--- trunk/docutils/docutils/nodes.py	2024-06-05 10:22:36 UTC (rev 9724)
+++ trunk/docutils/docutils/nodes.py	2024-06-05 10:22:44 UTC (rev 9725)
@@ -479,22 +479,22 @@
     local_attributes = ('backrefs',)
     """Obsolete. Will be removed in Docutils 2.0."""
 
-    valid_children = tuple()
-    """Valid class or tuple of valid classes for child elements.
+    content_model = tuple()
+    """Python representation of the element's content model (cf. docutils.dtd).
 
-    NOTE: Derived classes should update this value
-    when supporting child elements.
-    """
+    A tuple of ``(category, quantifier)`` tuples with
 
-    valid_len = (1, None)
-    """Tuple of minimal and maximal number of child elements.
+    :category:   class or tuple of classes that are expected at this place(s)
+                 in the list of children
+    :quantifier: string representation stating how many elements
+                 of `category` are expected. Value is one of:
+                 '.' (exactly one), '?' (zero or one),
+                 '+' (one or more), '*' (zero or more).
 
-    A maximal value of None stands for "no upper limit".
+    NOTE: The default describes the empty element. Derived classes should
+    update this value to match teir content model.
 
-    Default: one or more child elements.
-
-    NOTE: Derived classes should update this value when there are different
-    restrictions to the number of child elements.
+    Provisional.
     """
 
     tagname = None
@@ -1156,24 +1156,9 @@
         Provisional (work in progress).
         """
         self.validate_attributes()
+        
+        # TODO: validate content
 
-        # test number of children
-        messages = []
-        n_min, n_max = self.valid_len
-        if len(self.children) < n_min:
-            messages.append(f'Expects at least {n_min} children, '
-                            f'not {len(self.children)}.')
-        if n_max is not None and len(self.children) > n_max:
-            messages.append(f'Expects at most {n_max} children, '
-                            f'not {len(self.children)}.')
-        for child in self.children:
-            if not isinstance(child, self.valid_children):
-                messages.append(f'May not contain "{child.tagname}" elements.')
-            child.validate()
-        if messages:
-            raise ValueError(f'Element <{self.tagname}> invalid:\n  '
-                             + '\n  '.join(messages))
-
         if recursive:
             for child in self:
                 child.validate()
@@ -1220,7 +1205,7 @@
 
 
 class SubStructural(SubRoot):
-    """`Structural subelements`__ are children of structural elements.
+    """`Structural subelements`__ are children of `Structural` elements.
 
     Most Structural elements accept only specific `SubStructural` elements.
 
@@ -1246,7 +1231,7 @@
 
 class Admonition(Body):
     """Admonitions (distinctive and self-contained notices)."""
-    valid_children = Body  # (%body.elements;)
+    content_model = ((Body, '+'),)  # (%body.elements;)+
 
 
 class Sequential(Body):
@@ -1273,7 +1258,7 @@
 
     Children of `decoration`.
     """
-    valid_children = Body  # (%body.elements;)
+    content_model = ((Body, '+'),)  # (%body.elements;)+
 
 
 class Inline:
@@ -1329,8 +1314,9 @@
     If passing children to `__init__()`, make sure to set `text` to
     ``''`` or some other suitable value.
     """
-    valid_children = (Text, Inline)  # (#PCDATA | %inline.elements;)*
-    valid_len = (0, None)
+    content_model = (  # (#PCDATA | %inline.elements;)*
+                     ((Text, Inline), '*'),)
+
     child_text_separator = ''
     """Separator for child nodes, used by `astext()` method."""
 
@@ -1355,7 +1341,7 @@
 
 class PureTextElement(TextElement):
     """An element which only contains text, no children."""
-    valid_children = Text  # (#PCDATA)
+    content_model = ((Text, '?'),)  # (#PCDATA)
 
 
 # ==============
@@ -1370,13 +1356,13 @@
     `docutils.utils.new_document()` instead.
     """
     valid_attributes = Element.valid_attributes + ('title',)
-    # content model: ( (title, subtitle?)?,
-    #                   meta*,
-    #                   decoration?,
-    #                   (docinfo, transition?)?,
-    #                   %structure.model; )
-    valid_children = (Structural, SubRoot, Body)
-    valid_len = (0, None)  # may be empty
+    content_model = (  # ( (title, subtitle?)?,
+                       #    meta*,
+                       #    decoration?,
+                       #    (docinfo, transition?)?,
+                       #    %structure.model; )
+                     ((Structural, SubRoot, Body), '*'),
+                     )  # TODO complex model
 
     def __init__(self, settings, reporter, *args, **kwargs):
         Element.__init__(self, *args, **kwargs)
@@ -1746,7 +1732,6 @@
     """Container for "invisible" bibliographic data, or meta-data."""
     valid_attributes = Element.valid_attributes + (
         'content', 'dir', 'http-equiv', 'lang', 'media', 'name', 'scheme')
-    valid_len = (0, 0)  # The <meta> element has no content.
 
 
 # ========================
@@ -1755,7 +1740,8 @@
 
 class docinfo(SubRoot, Element):
     """Container for displayed document meta-data."""
-    valid_children = Bibliographic  # (%bibliographic.elements;)+
+    content_model = (  # (%bibliographic.elements;)+
+                     (Bibliographic, '+'),)
 
 
 class author(Bibliographic, TextElement): pass
@@ -1772,8 +1758,11 @@
 class authors(Bibliographic, Element):
     """Container for author information for documents with multiple authors.
     """
-    # content model: (author, organization?, address?, contact?)+
-    valid_children = (author, organization, address, contact)
+    content_model = (  # (author, organization?, address?, contact?)+
+                     (author, '+'),
+                     (organization, '?'),
+                     (address, '?'),
+                     (contact, '?'))
 
 
 # =====================
@@ -1781,10 +1770,15 @@
 # =====================
 
 
+class header(Decorative, Element): pass
+class footer(Decorative, Element): pass
+
+
 class decoration(PreBibliographic, SubRoot, Element):
     """Container for `header` and `footer`."""
-    valid_children = Decorative  # (header?, footer?)
-    valid_len = (0, 2)           # TODO: empty element does not make sense.
+    content_model = (  # (header?, footer?)
+                     (header, '?'),
+                     (footer, '?'))  # TODO: empty element does not make sense.
 
     def get_header(self):
         if not len(self.children) or not isinstance(self.children[0], header):
@@ -1797,20 +1791,10 @@
         return self.children[-1]
 
 
-class header(Decorative, Element): pass
-class footer(Decorative, Element): pass
-
-
 # =====================
 #  Structural Elements
 # =====================
 
-class section(Structural, Element):
-    """Document section. The main unit of hierarchy."""
-    # content model: (title, subtitle?, %structure.model;)
-    valid_children = (Structural, SubStructural, Body)
-
-
 class topic(Structural, Element):
     """
     Topics are terminal, "leaf" mini-sections, like block quotes with titles,
@@ -1823,7 +1807,9 @@
     Topics cannot nest inside topics, or body elements; you can't have
     a topic inside a table, list, block quote, etc.
     """
-    valid_children = (title, Body)  # (title?, (%body.elements;)+)
+    content_model = (  # (title?, (%body.elements;)+)
+                     (title, '?'),
+                     (Body, '+'))
 
 
 class sidebar(Structural, Element):
@@ -1840,8 +1826,10 @@
     cannot nest inside sidebars, topics, or body elements; you can't have a
     sidebar inside a table, list, block quote, etc.
     """
-    # content model: ((title, subtitle?)?, (%body.elements; | topic)+)
-    valid_children = (title, subtitle, topic, Body)
+    content_model = (  # ((title, subtitle?)?, (%body.elements; | topic)+)
+                     (title, '?'),
+                     (subtitle, '?'),
+                     ((topic, Body), '+'))  # TODO complex model
 
 
 class transition(SubStructural, Element):
@@ -1850,9 +1838,21 @@
     A transition may not begin or end a section or document, nor may two
     transitions be immediately adjacent.
     """
-    valid_len = (0, 0)  # empty element
 
 
+class section(Structural, Element):
+    """Document section. The main unit of hierarchy."""
+    # recursive content model, see below
+
+
+section.content_model = (  # (title, subtitle?, %structure.model;)
+                         (title, '.'),
+                         (subtitle, '?'),
+                         ((Body, topic, sidebar, transition), '*'),
+                         ((section, transition), '*'),
+                         )  # TODO complex model
+
+
 # ===============
 #  Body Elements
 # ===============
@@ -1861,11 +1861,11 @@
 
 
 class compound(General, Element):
-    valid_children = Body  # (%body.elements;)+
+    content_model = ((Body, '+'),)  # (%body.elements;)+
 
 
 class container(General, Element):
-    valid_children = Body  # (%body.elements;)+
+    content_model = ((Body, '+'),)  # (%body.elements;)+
 
 
 class attribution(Part, TextElement):
@@ -1874,7 +1874,9 @@
 
 class block_quote(General, Element):
     """An extended quotation, set off from the main text."""
-    valid_children = (Body, attribution)  # ((%body.elements;)+, attribution?)
+    content_model = (  # ((%body.elements;)+, attribution?)
+                     (Body, '+'),
+                     (attribution, '?'))
 
 
 # Lists
@@ -1883,19 +1885,18 @@
 # Lists (Sequential) and related Body Subelements (Part)
 
 class list_item(Part, Element):
-    valid_children = Body  # (%body.elements;)*
-    valid_len = (0, None)
+    content_model = ((Body, '*'),)  # (%body.elements;)*
 
 
 class bullet_list(Sequential, Element):
     valid_attributes = Element.valid_attributes + ('bullet',)
-    valid_children = list_item  # (list_item+)
+    content_model = ((list_item, '+'),)  # (list_item+)
 
 
 class enumerated_list(Sequential, Element):
     valid_attributes = Element.valid_attributes + (
         'enumtype', 'prefix', 'suffix', 'start')
-    valid_children = list_item  # (list_item+)
+    content_model = ((list_item, '+'),)  # (list_item+)
 
 
 class term(Part, TextElement): pass
@@ -1904,12 +1905,14 @@
 
 class definition(Part, Element):
     """Definition of a `term` in a `definition_list`."""
-    valid_children = Body  # (%body.elements;)+
+    content_model = ((Body, '+'),)  # (%body.elements;)+
 
 
 class definition_list_item(Part, Element):
-    valid_children = (term, classifier, definition)
-    valid_len = (2, None)  # (term, classifier*, definition)
+    content_model = (  # (term, classifier*, definition)
+                     (term, '.'),
+                     (classifier, '*'),
+                     (definition, '.'))
 
 
 class definition_list(Sequential, Element):
@@ -1918,7 +1921,7 @@
     Can be used for glossaries or dictionaries, to describe or
     classify things, for dialogues, or to itemize subtopics.
     """
-    valid_children = definition_list_item  # (definition_list_item+)
+    content_model = ((definition_list_item, '+'),)  # (definition_list_item+)
 
 
 class field_name(Part, TextElement): pass
@@ -1925,13 +1928,13 @@
 
 
 class field_body(Part, Element):
-    valid_children = Body  # (%body.elements;)*
-    valid_len = (0, None)
+    content_model = ((Body, '*'),)  # (%body.elements;)*
 
 
 class field(Part, Bibliographic, Element):
-    valid_children = (field_name, field_body)  # (field_name, field_body)
-    valid_len = (2, 2)
+    content_model = (  # (field_name, field_body)
+                     (field_name, '.'),
+                     (field_body, '.'))
 
 
 class field_list(Sequential, Element):
@@ -1940,7 +1943,7 @@
     Typically rendered as a two-column list.
     Also used for extension syntax or special processing.
     """
-    valid_children = field  # (field+)
+    content_model = ((field, '+'),)  # (field+)
 
 
 class option_string(Part, PureTextElement):
@@ -1961,19 +1964,20 @@
     Groups an option string with zero or more option argument placeholders.
     """
     child_text_separator = ''
-    # content model: (option_string, option_argument*)
-    valid_children = (option_string, option_argument)
+    content_model = (  # (option_string, option_argument*)
+                     (option_string, '.'),
+                     (option_argument, '*'))
 
 
 class option_group(Part, Element):
     """Groups together one or more `option` elements, all synonyms."""
     child_text_separator = ', '
-    valid_children = option  # (option+)
+    content_model = ((option, '+'),)  # (option+)
 
 
 class description(Part, Element):
     """Describtion of a command-line option."""
-    valid_children = Body  # (%body.elements;)+
+    content_model = ((Body, '+'),)  # (%body.elements;)+
 
 
 class option_list_item(Part, Element):
@@ -1980,13 +1984,14 @@
     """Container for a pair of `option_group` and `description` elements.
     """
     child_text_separator = '  '
-    valid_children = (option_group, description)  # (option_group, description)
-    valid_len = (2, 2)
+    content_model = (  # (option_group, description)
+                     (option_group, '.'),
+                     (description, '.'))
 
 
 class option_list(Sequential, Element):
     """Two-column list of command-line options and descriptions."""
-    valid_children = option_list_item  # (option_list_item+)
+    content_model = ((option_list_item, '+'),)  # (option_list_item+)
 
 
 # Pre-formatted text blocks
@@ -2011,7 +2016,7 @@
     # recursive content model: (line | line_block)+
 
 
-line_block.valid_children = (line, line_block)
+line_block.content_model = (((line, line_block), '+'),)
 
 
 # Admonitions
@@ -2030,8 +2035,9 @@
 
 
 class admonition(Admonition, Element):
-    valid_children = (title, Body)  # (title, (%body.elements;)+)
-    valid_len = (2, None)
+    content_model = (  # (title, (%body.elements;)+)
+                     (title, '.'),
+                     (Body, '+'))
 
 
 # Invisible elements
@@ -2060,12 +2066,15 @@
 class footnote(General, BackLinkable, Element, Labeled, Targetable):
     """Labelled note providing additional context (footnote or endnote)."""
     valid_attributes = Element.valid_attributes + ('auto', 'backrefs')
-    valid_children = (label, Body)  # (label?, (%body.elements;)+)
+    content_model = (  # (label?, (%body.elements;)+)
+                     (label, '?'),
+                     (Body, '+'))
 
 
 class citation(General, BackLinkable, Element, Labeled, Targetable):
-    valid_children = (label, Body)  # (label, (%body.elements;)+)
-    valid_len = (2, None)
+    content_model = (  # (label, (%body.elements;)+)
+                     (label, '.'),
+                     (Body, '+'))
     # TODO: DTD requires both label and content but rST allows empty citation
     #       (see test_rst/test_citations.py).  Is this sensible?
 
@@ -2080,7 +2089,6 @@
     """
     valid_attributes = Element.valid_attributes + (
         'uri', 'alt', 'align', 'height', 'width', 'scale', 'loading')
-    valid_len = (0, 0)  # emtpy element
 
     def astext(self):
         return self.get('alt', '')
@@ -2091,15 +2099,16 @@
 
 class legend(Part, Element):
     """A wrapper for text accompanying a `figure` that is not the caption."""
-    valid_children = Body  # (%body.elements;)
+    content_model = ((Body, '+'),)  # (%body.elements;)+
 
 
 class figure(General, Element):
     """A formal figure, generally an illustration, with a title."""
     valid_attributes = Element.valid_attributes + ('align', 'width')
-    # content model: (image, ((caption, legend?) | legend))
-    valid_children = (image, caption, legend)
-    valid_len = (1, 3)
+    content_model = (  # (image, ((caption, legend?) | legend))
+                     (image, '.'),
+                     (caption, '?'),
+                     (legend, '?'))
     # TODO: According to the DTD, a caption or legend is required
     # but rST allows "bare" figures which are formatted differently from
     # images (floating in LaTeX, nested in a <figure> in HTML).
@@ -2113,14 +2122,13 @@
     valid_attributes = Element.valid_attributes + (
         'align', 'char', 'charoff', 'colname', 'colsep', 'morecols',
         'morerows', 'namest', 'nameend', 'rowsep', 'valign')
-    valid_children = Body  # %tbl.entry.mdl -> (%body.elements;)*
-    valid_len = (0, None)  # may be empty
+    content_model = ((Body, '*'),)  # %tbl.entry.mdl -> (%body.elements;)*
 
 
 class row(Part, Element):
     """Row of table cells."""
     valid_attributes = Element.valid_attributes + ('rowsep', 'valign')
-    valid_children = entry  # (%tbl.row.mdl;) -> entry+
+    content_model = ((entry, '+'),)  # (%tbl.row.mdl;) -> entry+
 
 
 class colspec(Part, Element):
@@ -2128,19 +2136,18 @@
     valid_attributes = Element.valid_attributes + (
         'align', 'char', 'charoff', 'colname', 'colnum',
         'colsep', 'colwidth', 'rowsep', 'stub')
-    valid_len = (0, 0)  # empty element
 
 
 class thead(Part, Element):
     """Row(s) that form the head of a `tgroup`."""
     valid_attributes = Element.valid_attributes + ('valign',)
-    valid_children = row  # (row+)
+    content_model = ((row, '+'),)  # (row+)
 
 
 class tbody(Part, Element):
     """Body of a `tgroup`."""
     valid_attributes = Element.valid_attributes + ('valign',)
-    valid_children = row  # (row+)
+    content_model = ((row, '+'),)  # (row+)
 
 
 class tgroup(Part, Element):
@@ -2147,7 +2154,10 @@
     """A portion of a table. Most tables have just one `tgroup`."""
     valid_attributes = Element.valid_attributes + (
         'align', 'cols', 'colsep', 'rowsep')
-    valid_children = (colspec, thead, tbody)  # (colspec*, thead?, tbody)
+    content_model = (  # (colspec*, thead?, tbody)
+                     (colspec, '*'),
+                     (thead, '?'),
+                     (tbody, '.'))
 
 
 class table(General, Element):
@@ -2154,7 +2164,9 @@
     """A data arrangement with rows and columns."""
     valid_attributes = Element.valid_attributes + (
         'align', 'colsep', 'frame', 'pgwide', 'rowsep', 'width')
-    valid_children = (title, tgroup)  # (title?, tgroup+)
+    content_model = (  # (title?, tgroup+)
+                     (title, '?'),
+                     (tgroup, '+'))
 
 
 # Special purpose elements
@@ -2169,7 +2181,7 @@
     """
     valid_attributes = BackLinkable.valid_attributes + (
                            'level', 'line', 'type')
-    valid_children = Body  # (%body.elements;)+
+    content_model = ((Body, '+'),)  # (%body.elements;)+
 
     def __init__(self, message=None, *children, **attributes):
         rawsource = attributes.pop('rawsource', '')
@@ -2216,7 +2228,6 @@
     `docutils.transforms.Transformer` stage of processing can run all pending
     transforms.
     """
-    valid_len = (0, 0)  # empty element
 
     def __init__(self, transform, details=None,
                  rawsource='', *children, **attributes):

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.