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.