Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/docutils/nodes.py: 58%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# $Id: nodes.py 10391 2026-07-22 20:59:24Z milde $
2# Author: David Goodger <goodger@python.org>
3# Maintainer: docutils-develop@lists.sourceforge.net
4# Copyright: This module has been placed in the public domain.
6"""
7Docutils document tree element class library.
9The relationships and semantics of elements and attributes is documented in
10`The Docutils Document Tree`__.
12Classes in CamelCase are abstract base classes or auxiliary classes. The one
13exception is `Text`, for a text (PCDATA) node; uppercase is used to
14differentiate from element classes. Classes in lower_case_with_underscores
15are element classes, matching the XML element generic identifiers in the DTD_.
17The position of each node (the level at which it can occur) is significant and
18is represented by abstract base classes (`Root`, `Structural`, `Body`,
19`Inline`, etc.). Certain transformations will be easier because we can use
20``isinstance(node, base_class)`` to determine the position of the node in the
21hierarchy.
23__ https://docutils.sourceforge.io/docs/ref/doctree.html
24.. _DTD: https://docutils.sourceforge.io/docs/ref/docutils.dtd
25"""
27from __future__ import annotations
29__docformat__ = 'reStructuredText'
31import os
32import re
33import sys
34import unicodedata
35import warnings
36from collections import Counter
37# import xml.dom.minidom as dom # -> conditional import in Node.asdom()
38# and document.asdom()
40# import docutils.transforms # -> delayed import in document.__init__()
42TYPE_CHECKING = False
43if TYPE_CHECKING:
44 from collections.abc import (Callable, Iterable, Iterator,
45 Mapping, Sequence)
46 from types import ModuleType
47 from typing import Any, ClassVar, Final, Literal, Self, SupportsIndex
49 from docutils.utils._typing import TypeAlias
51 from xml.dom import minidom
53 from docutils.frontend import Values
54 from docutils.transforms import Transformer, Transform
55 from docutils.utils import Reporter
57 _ContentModelCategory: TypeAlias = tuple['Element' | tuple['Element', ...]]
58 _ContentModelQuantifier = Literal['.', '?', '+', '*']
59 _ContentModelItem: TypeAlias = tuple[_ContentModelCategory,
60 _ContentModelQuantifier]
61 _ContentModelTuple: TypeAlias = tuple[_ContentModelItem, ...]
63 StrPath: TypeAlias = str | os.PathLike[str]
64 """File system path. No bytes!"""
66 _UpdateFun: TypeAlias = Callable[[str, Any, bool], None]
69# ==============================
70# Functional Node Base Classes
71# ==============================
73class Node:
74 """Abstract base class of nodes in a document tree."""
76 parent: Element | None = None
77 """Back-reference to the Node immediately containing this Node."""
79 children: Sequence # defined in subclasses
80 """List of child nodes (Elements or Text).
82 Override in subclass instances that are not terminal nodes.
83 """
85 source: StrPath | None = None
86 """Path or description of the input source which generated this Node."""
88 line: int | None = None
89 """The line number (1-based) of the beginning of this Node in `source`."""
91 tagname: str # defined in subclasses
92 """The element generic identifier."""
94 _document: document | None = None
96 @property
97 def document(self) -> document | None:
98 """Return the `document` root node of the tree containing this Node.
99 """
100 try:
101 return self._document or self.parent.document
102 except AttributeError:
103 return None
105 @document.setter
106 def document(self, value: document) -> None:
107 self._document = value
109 def __bool__(self) -> Literal[True]:
110 """
111 Node instances are always true, even if they're empty. A node is more
112 than a simple container. Its boolean "truth" does not depend on
113 having one or more subnodes in the doctree.
115 Use `len()` to check node length.
116 """
117 return True
119 def asdom(self,
120 dom: ModuleType | None = None,
121 ) -> minidom.Document | minidom.Element | minidom.Text:
122 # TODO: minidom.Document is only returned by document.asdom()
123 # (which overwrites this base-class implementation)
124 """Return a DOM **fragment** representation of this Node."""
125 if dom is None:
126 import xml.dom.minidom as dom
127 domroot = dom.Document()
128 return self._dom_node(domroot)
130 def pformat(self, indent: str = ' ', level: int = 0) -> str:
131 """
132 Return an indented pseudo-XML representation, for test purposes.
134 Override in subclasses.
135 """
136 raise NotImplementedError
138 def copy(self) -> Self:
139 """Return a copy of self."""
140 raise NotImplementedError
142 def deepcopy(self) -> Self:
143 """Return a deep copy of self (also copying children)."""
144 raise NotImplementedError
146 def astext(self) -> str:
147 """Return a string representation of this Node."""
148 raise NotImplementedError
150 def setup_child(self, child) -> None:
151 child.parent = self
152 if self.document:
153 child.document = self.document
154 if child.source is None:
155 child.source = self.document.current_source
156 if child.line is None:
157 child.line = self.document.current_line
159 def walk(self, visitor: NodeVisitor) -> bool:
160 """
161 Traverse a tree of `Node` objects, calling the
162 `dispatch_visit()` method of `visitor` when entering each
163 node. (The `walkabout()` method is similar, except it also
164 calls the `dispatch_departure()` method before exiting each
165 node.)
167 This tree traversal supports limited in-place tree
168 modifications. Replacing one node with one or more nodes is
169 OK, as is removing an element. However, if the node removed
170 or replaced occurs after the current node, the old node will
171 still be traversed, and any new nodes will not.
173 Within ``visit`` methods (and ``depart`` methods for
174 `walkabout()`), `TreePruningException` subclasses may be raised
175 (`SkipChildren`, `SkipSiblings`, `SkipNode`, `SkipDeparture`).
177 Parameter `visitor`: A `NodeVisitor` object, containing a
178 ``visit`` implementation for each `Node` subclass encountered.
180 Return true if we should stop the traversal.
181 """
182 stop = False
183 visitor.document.reporter.debug(
184 'docutils.nodes.Node.walk calling dispatch_visit for %s'
185 % self.__class__.__name__)
186 try:
187 try:
188 visitor.dispatch_visit(self)
189 except (SkipChildren, SkipNode):
190 return stop
191 except SkipDeparture: # not applicable; ignore
192 pass
193 children = self.children
194 try:
195 for child in children[:]:
196 if child.walk(visitor):
197 stop = True
198 break
199 except SkipSiblings:
200 pass
201 except StopTraversal:
202 stop = True
203 return stop
205 def walkabout(self, visitor: NodeVisitor) -> bool:
206 """
207 Perform a tree traversal similarly to `Node.walk()` (which
208 see), except also call the `dispatch_departure()` method
209 before exiting each node.
211 Parameter `visitor`: A `NodeVisitor` object, containing a
212 ``visit`` and ``depart`` implementation for each `Node`
213 subclass encountered.
215 Return true if we should stop the traversal.
216 """
217 call_depart = True
218 stop = False
219 visitor.document.reporter.debug(
220 'docutils.nodes.Node.walkabout calling dispatch_visit for %s'
221 % self.__class__.__name__)
222 try:
223 try:
224 visitor.dispatch_visit(self)
225 except SkipNode:
226 return stop
227 except SkipDeparture:
228 call_depart = False
229 children = self.children
230 try:
231 for child in children[:]:
232 if child.walkabout(visitor):
233 stop = True
234 break
235 except SkipSiblings:
236 pass
237 except SkipChildren:
238 pass
239 except StopTraversal:
240 stop = True
241 if call_depart:
242 visitor.document.reporter.debug(
243 'docutils.nodes.Node.walkabout calling dispatch_departure '
244 'for %s' % self.__class__.__name__)
245 visitor.dispatch_departure(self)
246 return stop
248 def _fast_findall(self, cls: type|tuple[type]) -> Iterator:
249 """Return iterator that only supports instance checks."""
250 if isinstance(self, cls):
251 yield self
252 for child in self.children:
253 yield from child._fast_findall(cls)
255 def _superfast_findall(self) -> Iterator:
256 """Return iterator that doesn't check for a condition."""
257 # This is different from ``iter(self)`` implemented via
258 # __getitem__() and __len__() in the Element subclass,
259 # which yields only the direct children.
260 yield self
261 for child in self.children:
262 yield from child._superfast_findall()
264 def findall(self,
265 condition: type|tuple[type]|Callable[[Node], bool]|None = None,
266 include_self: bool = True,
267 descend: bool = True,
268 siblings: bool = False,
269 ascend: bool = False,
270 ) -> Iterator:
271 """
272 Return an iterator yielding nodes following `self`:
274 * self (if `include_self` is true)
275 * all descendants in tree traversal order (if `descend` is true)
276 * the following siblings (if `siblings` is true) and their
277 descendants (if also `descend` is true)
278 * the following siblings of the parent (if `ascend` is true) and
279 their descendants (if also `descend` is true), and so on.
281 If `condition` is not None, the iterator yields only nodes
282 for which ``condition(node)`` is true.
283 If `condition` is a type (or tuple of types) ``cls``, it is equivalent
284 to a function consisting of ``return isinstance(node, cls)``.
286 If `ascend` is true, assume `siblings` to be true as well.
288 If the tree structure is modified during iteration, the result
289 is undefined.
291 For example, given the following tree::
293 <paragraph>
294 <emphasis> <--- emphasis.traverse() and
295 <strong> <--- strong.traverse() are called.
296 Foo
297 Bar
298 <reference name="Baz" refid="baz">
299 Baz
301 Then tuple(emphasis.traverse()) equals ::
303 (<emphasis>, <strong>, <#text: Foo>, <#text: Bar>)
305 and list(strong.traverse(ascend=True) equals ::
307 [<strong>, <#text: Foo>, <#text: Bar>, <reference>, <#text: Baz>]
308 """
309 if ascend:
310 siblings = True
311 # Check for special argument combinations that allow using an
312 # optimized version of traverse()
313 if include_self and descend and not siblings:
314 if condition is None:
315 yield from self._superfast_findall()
316 return
317 elif isinstance(condition, (type, tuple)):
318 yield from self._fast_findall(condition)
319 return
320 # Check if `condition` is a class (check for TypeType for Python
321 # implementations that use only new-style classes, like PyPy).
322 if isinstance(condition, (type, tuple)):
323 class_or_tuple = condition
325 def condition(node, class_or_tuple=class_or_tuple):
326 return isinstance(node, class_or_tuple)
328 if include_self and (condition is None or condition(self)):
329 yield self
330 if descend and len(self.children):
331 for child in self:
332 yield from child.findall(condition=condition,
333 include_self=True, descend=True,
334 siblings=False, ascend=False)
335 if siblings or ascend:
336 node = self
337 while node.parent:
338 index = node.parent.index(node)
339 # extra check since Text nodes have value-equality
340 while node.parent[index] is not node:
341 index = node.parent.index(node, index + 1)
342 for sibling in node.parent[index+1:]:
343 yield from sibling.findall(
344 condition=condition,
345 include_self=True, descend=descend,
346 siblings=False, ascend=False)
347 if not ascend:
348 break
349 else:
350 node = node.parent
352 def traverse(
353 self,
354 condition: type|tuple[type]|Callable[[Node], bool]|None = None,
355 include_self: bool = True,
356 descend: bool = True,
357 siblings: bool = False,
358 ascend: bool = False,
359 ) -> list:
360 """Return list of nodes following `self`.
362 For looping, Node.findall() is faster and more memory efficient.
363 """
364 # traverse() may be eventually removed:
365 warnings.warn('nodes.Node.traverse() is obsoleted by Node.findall().',
366 DeprecationWarning, stacklevel=2)
367 return list(self.findall(condition, include_self, descend,
368 siblings, ascend))
370 def next_node(
371 self,
372 condition: type|tuple[type]|Callable[[Node], bool]|None = None,
373 include_self: bool = False,
374 descend: bool = True,
375 siblings: bool = False,
376 ascend: bool = False,
377 ) -> Node | None:
378 """
379 Return the first node in the iterator returned by findall(),
380 or None if the iterable is empty.
382 Parameter list is the same as of `findall()`. Note that `include_self`
383 defaults to False, though.
384 """
385 try:
386 return next(self.findall(condition, include_self,
387 descend, siblings, ascend))
388 except StopIteration:
389 return None
391 def validate(self, recursive: bool = True) -> None:
392 """Raise ValidationError if this node is not valid.
394 Override in subclasses that define validity constraints.
395 """
397 def validate_position(self) -> None:
398 """Hook for additional checks of the parent's content model.
400 Raise ValidationError, if `self` is at an invalid position.
402 Override in subclasses with complex validity constraints. See
403 `subtitle.validate_position()` and `transition.validate_position()`.
404 """
407class Text(Node, str): # NoQA: SLOT000 (Node doesn't define __slots__)
408 """
409 Instances are terminal nodes (leaves) containing text only; no child
410 nodes or attributes. Initialize by passing a string to the constructor.
412 Access the raw (null-escaped) text with ``str(<instance>)``
413 and unescaped text with ``<instance>.astext()``.
414 """
416 tagname: Final = '#text'
418 children: Final = ()
419 """Text nodes have no children, and cannot have children."""
421 def __new__(cls, data: str, rawsource: None = None) -> Self:
422 """Assert that `data` is not an array of bytes
423 and warn if the deprecated `rawsource` argument is used.
424 """
425 if isinstance(data, bytes):
426 raise TypeError('expecting str data, not bytes')
427 if rawsource is not None:
428 warnings.warn('nodes.Text: initialization argument "rawsource" '
429 'is ignored and will be removed in Docutils 2.0.',
430 DeprecationWarning, stacklevel=2)
431 return str.__new__(cls, data)
433 def shortrepr(self, maxlen: int = 18) -> str:
434 data = self
435 if len(data) > maxlen:
436 data = data[:maxlen-4] + ' ...'
437 return '<%s: %r>' % (self.tagname, str(data))
439 def __repr__(self) -> str:
440 return self.shortrepr(maxlen=68)
442 def astext(self) -> str:
443 return str(unescape(self))
445 def _dom_node(self, domroot: minidom.Document) -> minidom.Text:
446 return domroot.createTextNode(str(self))
448 def copy(self) -> Self:
449 return self.__class__(str(self))
451 def deepcopy(self) -> Self:
452 return self.copy()
454 def pformat(self, indent: str = ' ', level: int = 0) -> str:
455 try:
456 if self.document.settings.detailed:
457 tag = '%s%s' % (indent*level, '<#text>')
458 lines = (indent*(level+1) + repr(line)
459 for line in self.splitlines(True))
460 return '\n'.join((tag, *lines)) + '\n'
461 except AttributeError:
462 pass
463 indent = indent * level
464 lines = [indent+line for line in self.astext().splitlines()]
465 if not lines:
466 return ''
467 return '\n'.join(lines) + '\n'
469 # rstrip and lstrip are used by substitution definitions where
470 # they are expected to return a Text instance, this was formerly
471 # taken care of by UserString.
473 def rstrip(self, chars: str | None = None) -> Self:
474 return self.__class__(str.rstrip(self, chars))
476 def lstrip(self, chars: str | None = None) -> Self:
477 return self.__class__(str.lstrip(self, chars))
480class Element(Node):
481 """
482 `Element` is the superclass to all specific elements.
484 Elements contain attributes and child nodes.
485 They can be described as a cross between a list and a dictionary.
487 Elements emulate dictionaries for external [#]_ attributes, indexing by
488 attribute name (a string). To set the attribute 'att' to 'value', do::
490 element['att'] = 'value'
492 .. [#] External attributes correspond to the XML element attributes.
493 From its `Node` superclass, Element also inherits "internal"
494 class attributes that are accessed using the standard syntax, e.g.
495 ``element.parent``.
497 There are two special attributes: 'ids' and 'names'. Both are
498 lists of unique identifiers: 'ids' conform to the regular expression
499 ``[a-z](-?[a-z0-9]+)*`` (see the make_id() function for rationale and
500 details). 'names' serve as user-friendly interfaces to IDs; they are
501 case- and whitespace-normalized (see the fully_normalize_name() function).
503 Elements emulate lists for child nodes (element nodes and/or text
504 nodes), indexing by integer. To get the first child node, use::
506 element[0]
508 to iterate over the child nodes (without descending), use::
510 for child in element:
511 ...
513 Elements may be constructed using the ``+=`` operator. To add one new
514 child node to element, do::
516 element += node
518 This is equivalent to ``element.append(node)``.
520 To add a list of multiple child nodes at once, use the same ``+=``
521 operator::
523 element += [node1, node2]
525 This is equivalent to ``element.extend([node1, node2])``.
526 """
528 list_attributes: Final = ('ids', 'classes', 'names', 'dupnames')
529 """Tuple of attributes that are initialized to empty lists.
531 NOTE: Derived classes should update this value when supporting
532 additional list attributes.
533 """
535 valid_attributes: Final = list_attributes + ('source',)
536 """Tuple of attributes that are valid for elements of this class.
538 NOTE: Derived classes should update this value when supporting
539 additional attributes.
540 """
542 common_attributes: Final = valid_attributes
543 """Tuple of `common attributes`__ known to all Doctree Element classes.
545 __ https://docutils.sourceforge.io/docs/ref/doctree.html#common-attributes
546 """
548 known_attributes: Final = common_attributes
549 """Alias for `common_attributes`. Will be removed in Docutils 2.0."""
551 basic_attributes: Final = list_attributes
552 """Common list attributes. Deprecated. Will be removed in Docutils 2.0."""
554 local_attributes: Final = ('backrefs',)
555 """Obsolete. Will be removed in Docutils 2.0."""
557 content_model: ClassVar[_ContentModelTuple] = ()
558 """Python representation of the element's content model (cf. docutils.dtd).
560 A tuple of ``(category, quantifier)`` tuples with
562 :category: class or tuple of classes that are expected at this place(s)
563 in the list of children
564 :quantifier: string representation stating how many elements
565 of `category` are expected. Value is one of:
566 '.' (exactly one), '?' (zero or one),
567 '+' (one or more), '*' (zero or more).
569 NOTE: The default describes the empty element. Derived classes should
570 update this value to match their content model.
572 Provisional.
573 """
575 tagname: str | None = None
576 """The element generic identifier.
578 If None, it is set as an instance attribute to the name of the class.
579 """
581 child_text_separator: Final = '\n\n'
582 """Separator for child nodes, used by `astext()` method."""
584 def __init__(self,
585 rawsource: str = '',
586 *children,
587 **attributes: Any,
588 ) -> None:
589 self.rawsource = rawsource
590 """The raw text from which this element was constructed.
592 For informative and debugging purposes. Don't rely on its value!
594 NOTE: some elements do not set this value (default '').
595 """
596 if isinstance(rawsource, Element):
597 raise TypeError('First argument "rawsource" must be a string.')
599 self.children: list = []
600 """List of child nodes (elements and/or `Text`)."""
602 self.extend(children) # maintain parent info
604 self.attributes: dict[str, Any] = {}
605 """Dictionary of attribute {name: value}."""
607 # Initialize list attributes.
608 for att in self.list_attributes:
609 self.attributes[att] = []
611 for att, value in attributes.items():
612 att = att.lower() # normalize attribute name
613 if att in self.list_attributes:
614 # lists are mutable; make a copy for this node
615 self.attributes[att] = value[:]
616 else:
617 self.attributes[att] = value
619 if self.tagname is None:
620 self.tagname: str = self.__class__.__name__
622 def _dom_node(self, domroot: minidom.Document) -> minidom.Element:
623 element = domroot.createElement(self.tagname)
624 for attribute, value in self.attlist():
625 if isinstance(value, list):
626 value = ' '.join(serial_escape('%s' % (v,)) for v in value)
627 element.setAttribute(attribute, '%s' % value)
628 for child in self.children:
629 element.appendChild(child._dom_node(domroot))
630 return element
632 def __repr__(self) -> str:
633 data = ''
634 for c in self.children:
635 data += c.shortrepr()
636 if len(data) > 60:
637 data = data[:56] + ' ...'
638 break
639 if self['names']:
640 return '<%s "%s": %s>' % (self.tagname,
641 '; '.join(self['names']), data)
642 else:
643 return '<%s: %s>' % (self.tagname, data)
645 def shortrepr(self) -> str:
646 if self['names']:
647 return '<%s "%s"...>' % (self.tagname, '; '.join(self['names']))
648 else:
649 return '<%s...>' % self.tagname
651 def __str__(self) -> str:
652 if self.children:
653 return '%s%s%s' % (self.starttag(),
654 ''.join(str(c) for c in self.children),
655 self.endtag())
656 else:
657 return self.emptytag()
659 def starttag(self, quoteattr: Callable[[str], str] | None = None) -> str:
660 # the optional arg is used by the docutils_xml writer
661 if quoteattr is None:
662 quoteattr = pseudo_quoteattr
663 parts = [self.tagname]
664 for name, value in self.attlist():
665 if value is None: # boolean attribute
666 parts.append('%s="True"' % name)
667 continue
668 if isinstance(value, bool):
669 value = str(int(value))
670 if isinstance(value, list):
671 values = [serial_escape('%s' % (v,)) for v in value]
672 value = ' '.join(values)
673 else:
674 value = str(value)
675 value = quoteattr(value)
676 parts.append('%s=%s' % (name, value))
677 return '<%s>' % ' '.join(parts)
679 def endtag(self) -> str:
680 return '</%s>' % self.tagname
682 def emptytag(self) -> str:
683 attributes = ('%s="%s"' % (n, v) for n, v in self.attlist())
684 return '<%s/>' % ' '.join((self.tagname, *attributes))
686 def __len__(self) -> int:
687 return len(self.children)
689 def __contains__(self, key: str | Node) -> bool:
690 # Test for both, children and attributes with operator ``in``.
691 #
692 # Caution: Document Tree traversal also returns Text nodes where
693 # ``in`` looks for substrings in the text content.
694 if isinstance(key, str):
695 return key in self.attributes
696 return key in self.children
698 def __getitem__(self, key: str | int | slice) -> Any:
699 if isinstance(key, str):
700 return self.attributes[key]
701 elif isinstance(key, int):
702 return self.children[key]
703 elif isinstance(key, slice):
704 assert key.step in (None, 1), 'cannot handle slice with stride'
705 return self.children[key.start:key.stop]
706 else:
707 raise TypeError('element index must be an integer, a slice, or '
708 'an attribute name string')
710 def __setitem__(self, key, item) -> None:
711 if isinstance(key, str):
712 self.attributes[str(key)] = item
713 elif isinstance(key, int):
714 self.setup_child(item)
715 self.children[key] = item
716 elif isinstance(key, slice):
717 assert key.step in (None, 1), 'cannot handle slice with stride'
718 for node in item:
719 self.setup_child(node)
720 self.children[key.start:key.stop] = item
721 else:
722 raise TypeError('element index must be an integer, a slice, or '
723 'an attribute name string')
725 def __delitem__(self, key: str | int | slice) -> None:
726 if isinstance(key, str):
727 del self.attributes[key]
728 elif isinstance(key, int):
729 del self.children[key]
730 elif isinstance(key, slice):
731 assert key.step in (None, 1), 'cannot handle slice with stride'
732 del self.children[key.start:key.stop]
733 else:
734 raise TypeError('element index must be an integer, a simple '
735 'slice, or an attribute name string')
737 def __add__(self, other: list) -> list:
738 return self.children + other
740 def __radd__(self, other: list) -> list:
741 return other + self.children
743 def __iadd__(self, other) -> Self:
744 """Append a node or a list of nodes to `self.children`."""
745 if isinstance(other, Node):
746 self.append(other)
747 elif other is not None:
748 self.extend(other)
749 return self
751 def astext(self) -> str:
752 return self.child_text_separator.join(
753 [child.astext() for child in self.children])
755 def non_default_attributes(self) -> dict[str, Any]:
756 atts = {key: value for key, value in self.attributes.items()
757 if self.is_not_default(key)}
758 return atts
760 def attlist(self) -> list[tuple[str, Any]]:
761 return sorted(self.non_default_attributes().items())
763 def get(self, key: str, failobj: Any | None = None) -> Any:
764 return self.attributes.get(key, failobj)
766 def hasattr(self, attr: str) -> bool:
767 return attr in self.attributes
769 def delattr(self, attr: str) -> None:
770 if attr in self.attributes:
771 del self.attributes[attr]
773 def setdefault(self, key: str, failobj: Any | None = None) -> Any:
774 return self.attributes.setdefault(key, failobj)
776 has_key = hasattr
778 def get_language_code(self, fallback: str = '') -> str:
779 """Return node's language tag.
781 Look iteratively in self and parents for a class argument
782 starting with ``language-`` and return the remainder of it
783 (which should be a `BCP49` language tag) or the `fallback`.
784 """
785 for cls in self.get('classes', []):
786 if cls.startswith('language-'):
787 return cls.removeprefix('language-')
788 try:
789 return self.parent.get_language_code(fallback)
790 except AttributeError:
791 return fallback
793 def append(self, item) -> None:
794 self.setup_child(item)
795 self.children.append(item)
797 def extend(self, item: Iterable) -> None:
798 for node in item:
799 self.append(node)
801 def insert(self, index: SupportsIndex, item) -> None:
802 if isinstance(item, Node):
803 self.setup_child(item)
804 self.children.insert(index, item)
805 elif item is not None:
806 self[index:index] = item
808 def pop(self, i: int = -1):
809 return self.children.pop(i)
811 def remove(self, item) -> None:
812 self.children.remove(item)
814 def index(self, item, start: int = 0, stop: int = sys.maxsize) -> int:
815 return self.children.index(item, start, stop)
817 def previous_sibling(self):
818 """Return preceding sibling node or ``None``."""
819 try:
820 i = self.parent.index(self)
821 except (AttributeError):
822 return None
823 return self.parent[i-1] if i > 0 else None
825 def section_hierarchy(self) -> list[section]:
826 """Return the element's section anchestors.
828 Return a list of all <section> elements that contain `self`
829 (including `self` if it is a <section>) and have a parent node.
831 List item ``[i]`` is the parent <section> of level i+1
832 (1: section, 2: subsection, 3: subsubsection, ...).
833 The length of the list is the element's section level.
835 See `docutils.parsers.rst.states.RSTState.check_subsection()`
836 for a usage example.
838 Provisional. May be changed or removed without warning.
839 """
840 sections = []
841 node = self
842 while node.parent is not None:
843 if isinstance(node, section):
844 sections.append(node)
845 node = node.parent
846 sections.reverse()
847 return sections
849 def is_not_default(self, key: str) -> bool:
850 if self[key] == [] and key in self.list_attributes:
851 return False
852 else:
853 return True
855 def update_basic_atts(self, dict_: Mapping[str, Any] | Element) -> None:
856 """
857 Update basic attributes ('ids', 'names', 'classes',
858 'dupnames', but not 'source') from node or dictionary `dict_`.
860 Provisional.
861 """
862 if isinstance(dict_, Node):
863 dict_ = dict_.attributes
864 for att in self.basic_attributes:
865 self.append_attr_list(att, dict_.get(att, []))
867 def append_attr_list(self, attr: str, values: Iterable[Any]) -> None:
868 """
869 For each element in values, if it does not exist in self[attr], append
870 it.
872 NOTE: Requires self[attr] and values to be sequence type and the
873 former should specifically be a list.
874 """
875 # List Concatenation
876 for value in values:
877 if value not in self[attr]:
878 self[attr].append(value)
880 def coerce_append_attr_list(
881 self, attr: str, value: list[Any] | Any) -> None:
882 """
883 First, convert both self[attr] and value to a non-string sequence
884 type; if either is not already a sequence, convert it to a list of one
885 element. Then call append_attr_list.
887 NOTE: self[attr] and value both must not be None.
888 """
889 # List Concatenation
890 if not isinstance(self.get(attr), list):
891 self[attr] = [self[attr]]
892 if not isinstance(value, list):
893 value = [value]
894 self.append_attr_list(attr, value)
896 def replace_attr(self, attr: str, value: Any, force: bool = True) -> None:
897 """
898 If self[attr] does not exist or force is True or omitted, set
899 self[attr] to value, otherwise do nothing.
900 """
901 # One or the other
902 if force or self.get(attr) is None:
903 self[attr] = value
905 def copy_attr_convert(
906 self, attr: str, value: Any, replace: bool = True) -> None:
907 """
908 If attr is an attribute of self, set self[attr] to
909 [self[attr], value], otherwise set self[attr] to value.
911 NOTE: replace is not used by this function and is kept only for
912 compatibility with the other copy functions.
913 """
914 if self.get(attr) is not value:
915 self.coerce_append_attr_list(attr, value)
917 def copy_attr_coerce(self, attr: str, value: Any, replace: bool) -> None:
918 """
919 If attr is an attribute of self and either self[attr] or value is a
920 list, convert all non-sequence values to a sequence of 1 element and
921 then concatenate the two sequence, setting the result to self[attr].
922 If both self[attr] and value are non-sequences and replace is True or
923 self[attr] is None, replace self[attr] with value. Otherwise, do
924 nothing.
925 """
926 if self.get(attr) is not value:
927 if isinstance(self.get(attr), list) or \
928 isinstance(value, list):
929 self.coerce_append_attr_list(attr, value)
930 else:
931 self.replace_attr(attr, value, replace)
933 def copy_attr_concatenate(
934 self, attr: str, value: Any, replace: bool) -> None:
935 """
936 If attr is an attribute of self and both self[attr] and value are
937 lists, concatenate the two sequences, setting the result to
938 self[attr]. If either self[attr] or value are non-sequences and
939 replace is True or self[attr] is None, replace self[attr] with value.
940 Otherwise, do nothing.
941 """
942 if self.get(attr) is not value:
943 if isinstance(self.get(attr), list) and \
944 isinstance(value, list):
945 self.append_attr_list(attr, value)
946 else:
947 self.replace_attr(attr, value, replace)
949 def copy_attr_consistent(
950 self, attr: str, value: Any, replace: bool) -> None:
951 """
952 If replace is True or self[attr] is None, replace self[attr] with
953 value. Otherwise, do nothing.
954 """
955 if self.get(attr) is not value:
956 self.replace_attr(attr, value, replace)
958 def update_all_atts(self,
959 dict_: Mapping[str, Any] | Element,
960 update_fun: _UpdateFun = copy_attr_consistent,
961 replace: bool = True,
962 and_source: bool = False,
963 ) -> None:
964 """
965 Updates all attributes from node or dictionary `dict_`.
967 Appends the basic attributes ('ids', 'names', 'classes',
968 'dupnames', but not 'source') and then, for all other attributes in
969 dict_, updates the same attribute in self. When attributes with the
970 same identifier appear in both self and dict_, the two values are
971 merged based on the value of update_fun. Generally, when replace is
972 True, the values in self are replaced or merged with the values in
973 dict_; otherwise, the values in self may be preserved or merged. When
974 and_source is True, the 'source' attribute is included in the copy.
976 NOTE: When replace is False, and self contains a 'source' attribute,
977 'source' is not replaced even when dict_ has a 'source'
978 attribute, though it may still be merged into a list depending
979 on the value of update_fun.
980 NOTE: It is easier to call the update-specific methods then to pass
981 the update_fun method to this function.
982 """
983 if isinstance(dict_, Node):
984 dict_ = dict_.attributes
986 # Include the source attribute when copying?
987 if and_source:
988 filter_fun = self.is_not_list_attribute
989 else:
990 filter_fun = self.is_not_known_attribute
992 # Copy the basic attributes
993 self.update_basic_atts(dict_)
995 # Grab other attributes in dict_ not in self except the
996 # (All basic attributes should be copied already)
997 for att in filter(filter_fun, dict_):
998 update_fun(self, att, dict_[att], replace)
1000 def update_all_atts_consistantly(self,
1001 dict_: Mapping[str, Any] | Element,
1002 replace: bool = True,
1003 and_source: bool = False,
1004 ) -> None:
1005 """
1006 Updates all attributes from node or dictionary `dict_`.
1008 Appends the basic attributes ('ids', 'names', 'classes',
1009 'dupnames', but not 'source') and then, for all other attributes in
1010 dict_, updates the same attribute in self. When attributes with the
1011 same identifier appear in both self and dict_ and replace is True, the
1012 values in self are replaced with the values in dict_; otherwise, the
1013 values in self are preserved. When and_source is True, the 'source'
1014 attribute is included in the copy.
1016 NOTE: When replace is False, and self contains a 'source' attribute,
1017 'source' is not replaced even when dict_ has a 'source'
1018 attribute, though it may still be merged into a list depending
1019 on the value of update_fun.
1020 """
1021 self.update_all_atts(dict_, Element.copy_attr_consistent, replace,
1022 and_source)
1024 def update_all_atts_concatenating(self,
1025 dict_: Mapping[str, Any] | Element,
1026 replace: bool = True,
1027 and_source: bool = False,
1028 ) -> None:
1029 """
1030 Updates all attributes from node or dictionary `dict_`.
1032 Appends the basic attributes ('ids', 'names', 'classes',
1033 'dupnames', but not 'source') and then, for all other attributes in
1034 dict_, updates the same attribute in self. When attributes with the
1035 same identifier appear in both self and dict_ whose values aren't each
1036 lists and replace is True, the values in self are replaced with the
1037 values in dict_; if the values from self and dict_ for the given
1038 identifier are both of list type, then the two lists are concatenated
1039 and the result stored in self; otherwise, the values in self are
1040 preserved. When and_source is True, the 'source' attribute is
1041 included in the copy.
1043 NOTE: When replace is False, and self contains a 'source' attribute,
1044 'source' is not replaced even when dict_ has a 'source'
1045 attribute, though it may still be merged into a list depending
1046 on the value of update_fun.
1047 """
1048 self.update_all_atts(dict_, Element.copy_attr_concatenate, replace,
1049 and_source)
1051 def update_all_atts_coercion(self,
1052 dict_: Mapping[str, Any] | Element,
1053 replace: bool = True,
1054 and_source: bool = False,
1055 ) -> None:
1056 """
1057 Updates all attributes from node or dictionary `dict_`.
1059 Appends the basic attributes ('ids', 'names', 'classes',
1060 'dupnames', but not 'source') and then, for all other attributes in
1061 dict_, updates the same attribute in self. When attributes with the
1062 same identifier appear in both self and dict_ whose values are both
1063 not lists and replace is True, the values in self are replaced with
1064 the values in dict_; if either of the values from self and dict_ for
1065 the given identifier are of list type, then first any non-lists are
1066 converted to 1-element lists and then the two lists are concatenated
1067 and the result stored in self; otherwise, the values in self are
1068 preserved. When and_source is True, the 'source' attribute is
1069 included in the copy.
1071 NOTE: When replace is False, and self contains a 'source' attribute,
1072 'source' is not replaced even when dict_ has a 'source'
1073 attribute, though it may still be merged into a list depending
1074 on the value of update_fun.
1075 """
1076 self.update_all_atts(dict_, Element.copy_attr_coerce, replace,
1077 and_source)
1079 def update_all_atts_convert(self,
1080 dict_: Mapping[str, Any] | Element,
1081 and_source: bool = False,
1082 ) -> None:
1083 """
1084 Updates all attributes from node or dictionary `dict_`.
1086 Appends the basic attributes ('ids', 'names', 'classes',
1087 'dupnames', but not 'source') and then, for all other attributes in
1088 dict_, updates the same attribute in self. When attributes with the
1089 same identifier appear in both self and dict_ then first any non-lists
1090 are converted to 1-element lists and then the two lists are
1091 concatenated and the result stored in self; otherwise, the values in
1092 self are preserved. When and_source is True, the 'source' attribute
1093 is included in the copy.
1095 NOTE: When replace is False, and self contains a 'source' attribute,
1096 'source' is not replaced even when dict_ has a 'source'
1097 attribute, though it may still be merged into a list depending
1098 on the value of update_fun.
1099 """
1100 self.update_all_atts(dict_, Element.copy_attr_convert,
1101 and_source=and_source)
1103 def clear(self) -> None:
1104 self.children = []
1106 def replace(self, old, new) -> None:
1107 """Replace one child `Node` with another child or children."""
1108 index = self.index(old)
1109 if isinstance(new, Node):
1110 self.setup_child(new)
1111 self[index] = new
1112 elif new is not None:
1113 self[index:index+1] = new
1115 def replace_self(self, new) -> None:
1116 """
1117 Replace `self` node with `new`, where `new` is a node or a
1118 list of nodes.
1120 Provisional: the handling of node attributes will be revised.
1121 """
1122 update = new
1123 if not isinstance(new, Node):
1124 # `new` is a list; update first child.
1125 try:
1126 update = new[0]
1127 except IndexError:
1128 update = None
1129 if isinstance(update, Element):
1130 update.update_basic_atts(self)
1131 else:
1132 # `update` is a Text node or `new` is an empty list.
1133 # Assert that we aren't losing any attributes.
1134 for att in self.basic_attributes:
1135 assert not self[att], \
1136 'Losing "%s" attribute: %s' % (att, self[att])
1137 self.parent.replace(self, new)
1139 def first_child_matching_class(self,
1140 childclass: type[Element] | type[Text]
1141 | tuple[type[Element] | type[Text], ...],
1142 start: int = 0,
1143 end: int = sys.maxsize,
1144 ) -> int | None:
1145 """
1146 Return the index of the first child whose class exactly matches.
1148 Parameters:
1150 - `childclass`: A `Node` subclass to search for, or a tuple of `Node`
1151 classes. If a tuple, any of the classes may match.
1152 - `start`: Initial index to check.
1153 - `end`: Initial index to *not* check.
1154 """
1155 if not isinstance(childclass, tuple):
1156 childclass = (childclass,)
1157 for index in range(start, min(len(self), end)):
1158 for c in childclass:
1159 if isinstance(self[index], c):
1160 return index
1161 return None
1163 def first_child_not_matching_class(
1164 self,
1165 childclass: type[Element] | type[Text]
1166 | tuple[type[Element] | type[Text], ...],
1167 start: int = 0,
1168 end: int = sys.maxsize,
1169 ) -> int | None:
1170 """
1171 Return the index of the first child whose class does *not* match.
1173 Parameters:
1175 - `childclass`: A `Node` subclass to skip, or a tuple of `Node`
1176 classes. If a tuple, none of the classes may match.
1177 - `start`: Initial index to check.
1178 - `end`: Initial index to *not* check.
1179 """
1180 if not isinstance(childclass, tuple):
1181 childclass = (childclass,)
1182 for index in range(start, min(len(self), end)):
1183 for c in childclass:
1184 if isinstance(self.children[index], c):
1185 break
1186 else:
1187 return index
1188 return None
1190 def pformat(self, indent: str = ' ', level: int = 0) -> str:
1191 tagline = '%s%s\n' % (indent*level, self.starttag())
1192 childreps = (c.pformat(indent, level+1) for c in self.children)
1193 return ''.join((tagline, *childreps))
1195 def copy(self) -> Self:
1196 obj = self.__class__(rawsource=self.rawsource, **self.attributes)
1197 obj._document = self._document
1198 obj.source = self.source
1199 obj.line = self.line
1200 return obj
1202 def deepcopy(self) -> Self:
1203 copy = self.copy()
1204 copy.extend([child.deepcopy() for child in self.children])
1205 return copy
1207 def note_referenced_by(self,
1208 name: str | None = None,
1209 id: str | None = None,
1210 ) -> None:
1211 """Note that this Element has been referenced by its name
1212 `name` or id `id`."""
1213 self.referenced = True
1214 # Element.expect_referenced_by_* dictionaries map names or ids
1215 # that were "propagated" to this element to the elements that
1216 # had these attributes before. Mark them as ``referenced``
1217 # when this node is referenced by the respective names or ids.
1218 if relay := getattr(self, 'expect_referenced_by_name', {}).get(name):
1219 relay.referenced = True
1220 if relay := getattr(self, 'expect_referenced_by_id', {}).get(id):
1221 relay.referenced = True
1223 @classmethod
1224 def is_not_list_attribute(cls, attr: str) -> bool:
1225 """
1226 Returns True if and only if the given attribute is NOT one of the
1227 basic list attributes defined for all Elements.
1228 """
1229 return attr not in cls.list_attributes
1231 @classmethod
1232 def is_not_known_attribute(cls, attr: str) -> bool:
1233 """
1234 Return True if `attr` is NOT defined for all Element instances.
1236 Provisional. May be removed in Docutils 2.0.
1237 """
1238 return attr not in cls.common_attributes
1240 def validate_attributes(self) -> None:
1241 """Normalize and validate element attributes.
1243 Convert string values to expected datatype.
1244 Normalize values.
1246 Raise `ValidationError` for invalid attributes or attribute values.
1248 Provisional.
1249 """
1250 messages = []
1251 for key, value in self.attributes.items():
1252 if key.startswith('internal:'):
1253 continue # see docs/user/config.html#expose-internals
1254 if key not in self.valid_attributes:
1255 va = '", "'.join(self.valid_attributes)
1256 messages.append(f'Attribute "{key}" not one of "{va}".')
1257 continue
1258 try:
1259 self.attributes[key] = ATTRIBUTE_VALIDATORS[key](value)
1260 except (ValueError, TypeError, KeyError) as e:
1261 messages.append(
1262 f'Attribute "{key}" has invalid value "{value}".\n {e}')
1263 if messages:
1264 raise ValidationError(f'Element {self.starttag()} invalid:\n '
1265 + '\n '.join(messages),
1266 problematic_element=self)
1268 def validate_content(self,
1269 model: _ContentModelTuple | None = None,
1270 elements: Sequence | None = None,
1271 ) -> list:
1272 """Test compliance of `elements` with `model`.
1274 :model: content model description, default `self.content_model`,
1275 :elements: list of doctree elements, default `self.children`.
1277 Return list of children that do not fit in the model or raise
1278 `ValidationError` if the content does not comply with the `model`.
1280 Provisional.
1281 """
1282 if model is None:
1283 model = self.content_model
1284 if elements is None:
1285 elements = self.children
1286 ichildren = iter(elements)
1287 child = next(ichildren, None)
1288 for category, quantifier in model:
1289 if not isinstance(child, category):
1290 if quantifier in ('.', '+'):
1291 raise ValidationError(self._report_child(child, category),
1292 problematic_element=child)
1293 else: # quantifier in ('?', '*') -> optional child
1294 continue # try same child with next part of content model
1295 else:
1296 # Check additional placement constraints (if applicable):
1297 child.validate_position()
1298 # advance:
1299 if quantifier in ('.', '?'): # go to next element
1300 child = next(ichildren, None)
1301 else: # if quantifier in ('*', '+'): # pass all matching elements
1302 for child in ichildren:
1303 if not isinstance(child, category):
1304 break
1305 try:
1306 child.validate_position()
1307 except AttributeError:
1308 pass
1309 else:
1310 child = None
1311 return [] if child is None else [child, *ichildren]
1313 def _report_child(self,
1314 child,
1315 category: Element | Iterable[Element],
1316 ) -> str:
1317 # Return a str reporting a missing child or child of wrong category.
1318 try:
1319 _type = category.__name__
1320 except AttributeError:
1321 _type = '> or <'.join(c.__name__ for c in category)
1322 msg = f'Element {self.starttag()} invalid:\n'
1323 if child is None:
1324 return f'{msg} Missing child of type <{_type}>.'
1325 if isinstance(child, Text):
1326 return (f'{msg} Expecting child of type <{_type}>, '
1327 f'not text data "{child.astext()}".')
1328 return (f'{msg} Expecting child of type <{_type}>, '
1329 f'not {child.starttag()}.')
1331 def validate(self, recursive: bool = True) -> None:
1332 """Validate Docutils Document Tree element ("doctree").
1334 Raise ValidationError if there are violations.
1335 If `recursive` is True, validate also the element's descendants.
1337 See `The Docutils Document Tree`__ for details of the
1338 Docutils Document Model.
1340 __ https://docutils.sourceforge.io/docs/ref/doctree.html
1342 Provisional (work in progress).
1343 """
1344 self.validate_attributes()
1346 leftover_childs = self.validate_content()
1347 for child in leftover_childs:
1348 if isinstance(child, Text):
1349 raise ValidationError(f'Element {self.starttag()} invalid:\n'
1350 f' Spurious text: "{child.astext()}".',
1351 problematic_element=self)
1352 else:
1353 raise ValidationError(f'Element {self.starttag()} invalid:\n'
1354 f' Child element {child.starttag()} '
1355 'not allowed at this position.',
1356 problematic_element=child)
1358 if recursive:
1359 for child in self:
1360 child.validate(recursive=recursive)
1363# ====================
1364# Element Categories
1365# ====================
1366#
1367# See https://docutils.sourceforge.io/docs/ref/doctree.html#element-hierarchy.
1369class Root:
1370 """Element at the root of a document tree."""
1373class Structural:
1374 """`Structural elements`__.
1376 __ https://docutils.sourceforge.io/docs/ref/doctree.html
1377 #structural-elements
1378 """
1381class SubStructural:
1382 """`Structural subelements`__ are children of `Structural` elements.
1384 Most Structural elements accept only specific `SubStructural` elements.
1386 __ https://docutils.sourceforge.io/docs/ref/doctree.html
1387 #structural-subelements
1388 """
1391class Bibliographic:
1392 """`Bibliographic Elements`__ (displayed document meta-data).
1394 __ https://docutils.sourceforge.io/docs/ref/doctree.html
1395 #bibliographic-elements
1396 """
1399class Body:
1400 """`Body elements`__.
1402 __ https://docutils.sourceforge.io/docs/ref/doctree.html#body-elements
1403 """
1406class Admonition(Body):
1407 """Admonitions (distinctive and self-contained notices)."""
1408 content_model: Final = ((Body, '+'),) # (%body.elements;)+
1411class Sequential(Body):
1412 """List-like body elements."""
1415class General(Body):
1416 """Miscellaneous body elements."""
1419class Special(Body):
1420 """Special internal body elements."""
1423class Part:
1424 """`Body Subelements`__ always occur within specific parent elements.
1426 __ https://docutils.sourceforge.io/docs/ref/doctree.html#body-subelements
1427 """
1430class Decorative:
1431 """Decorative elements (`header` and `footer`).
1433 Children of `decoration`.
1434 """
1435 content_model: Final = ((Body, '+'),) # (%body.elements;)+
1438class Inline:
1439 """Inline elements contain text data and possibly other inline elements.
1440 """
1443# Orthogonal categories and Mixins
1444# ================================
1446class PreBibliographic:
1447 """Elements which may occur before Bibliographic Elements."""
1450class Invisible(Special, PreBibliographic):
1451 """Internal elements that don't appear in output."""
1454class Labeled:
1455 """Contains a `label` as its first element."""
1458class Resolvable:
1459 resolved: bool = False
1462class BackLinkable:
1463 """Mixin for Elements that accept a "backrefs" attribute."""
1465 list_attributes: Final = Element.list_attributes + ('backrefs',)
1466 valid_attributes: Final = Element.valid_attributes + ('backrefs',)
1468 def add_backref(self: Element, refid: str) -> None:
1469 self['backrefs'].append(refid)
1472class Referential(Resolvable):
1473 """Elements holding a cross-reference (outgoing hyperlink)."""
1476class Targetable(Resolvable):
1477 """Cross-reference targets (incoming hyperlink)."""
1478 referenced: int = 0
1481class Titular:
1482 """Title, sub-title, or informal heading (rubric)."""
1485class TextElement(Element):
1486 """
1487 An element which directly contains text.
1489 Its children are all `Text` or `Inline` subclass nodes. You can
1490 check whether an element's context is inline simply by checking whether
1491 its immediate parent is a `TextElement` instance (including subclasses).
1492 This is handy for nodes like `image` that can appear both inline and as
1493 standalone body elements.
1495 If passing children to `__init__()`, make sure to set `text` to
1496 ``''`` or some other suitable value.
1497 """
1498 content_model: Final = (((Text, Inline), '*'),)
1499 # (#PCDATA | %inline.elements;)*
1501 child_text_separator: Final = ''
1502 """Separator for child nodes, used by `astext()` method."""
1504 def __init__(self,
1505 rawsource: str = '',
1506 text: str = '',
1507 *children,
1508 **attributes: Any,
1509 ) -> None:
1510 if text:
1511 textnode = Text(text)
1512 Element.__init__(self, rawsource, textnode, *children,
1513 **attributes)
1514 else:
1515 Element.__init__(self, rawsource, *children, **attributes)
1518class FixedTextElement(TextElement):
1519 """An element which directly contains preformatted text."""
1521 valid_attributes: Final = Element.valid_attributes + ('xml:space',)
1523 def __init__(self,
1524 rawsource: str = '',
1525 text: str = '',
1526 *children,
1527 **attributes: Any,
1528 ) -> None:
1529 super().__init__(rawsource, text, *children, **attributes)
1530 self.attributes['xml:space'] = 'preserve'
1533class PureTextElement(TextElement):
1534 """An element which only contains text, no children."""
1535 content_model: Final = ((Text, '?'),) # (#PCDATA)
1538# =================================
1539# Concrete Document Tree Elements
1540# =================================
1541#
1542# See https://docutils.sourceforge.io/docs/ref/doctree.html#element-reference
1544# Special purpose elements
1545# ========================
1546#
1547# Body elements for internal use or special requests.
1549class comment(Invisible, FixedTextElement, PureTextElement):
1550 """Author notes, hidden from the output."""
1553class substitution_definition(Invisible, TextElement):
1554 valid_attributes: Final = Element.valid_attributes + ('ltrim', 'rtrim')
1557class target(Invisible, Inline, TextElement, Targetable):
1558 valid_attributes: Final = Element.valid_attributes + (
1559 'anonymous', 'refid', 'refname', 'refuri')
1562class system_message(Special, BackLinkable, PreBibliographic, Element):
1563 """
1564 System message element.
1566 Do not instantiate this class directly; use
1567 ``document.reporter.info/warning/error/severe()`` instead.
1568 """
1569 valid_attributes: Final = BackLinkable.valid_attributes + (
1570 'level', 'line', 'type')
1571 content_model: Final = ((Body, '+'),) # (%body.elements;)+
1573 def __init__(self,
1574 message: str | None = None,
1575 *children,
1576 **attributes: Any,
1577 ) -> None:
1578 rawsource = attributes.pop('rawsource', '')
1579 if message:
1580 p = paragraph('', message)
1581 children = (p,) + children
1582 try:
1583 Element.__init__(self, rawsource, *children, **attributes)
1584 except: # NoQA: E722 (catchall)
1585 print('system_message: children=%r' % (children,))
1586 raise
1588 def astext(self) -> str:
1589 line = self.get('line', '')
1590 return '%s:%s: (%s/%s) %s' % (self['source'], line, self['type'],
1591 self['level'], Element.astext(self))
1594class pending(Invisible, Element):
1595 """
1596 Placeholder for pending operations.
1598 The "pending" element is used to encapsulate a pending operation: the
1599 operation (transform), the point at which to apply it, and any data it
1600 requires. Only the pending operation's location within the document is
1601 stored in the public document tree (by the "pending" object itself); the
1602 operation and its data are stored in the "pending" object's internal
1603 instance attributes.
1605 For example, say you want a table of contents in your reStructuredText
1606 document. The easiest way to specify where to put it is from within the
1607 document, with a directive::
1609 .. contents::
1611 But the "contents" directive can't do its work until the entire document
1612 has been parsed and possibly transformed to some extent. So the directive
1613 code leaves a placeholder behind that will trigger the second phase of its
1614 processing, something like this::
1616 <pending ...public attributes...> + internal attributes
1618 Use `document.note_pending()` so that the
1619 `docutils.transforms.Transformer` stage of processing can run all pending
1620 transforms.
1621 """
1623 def __init__(self,
1624 transform: Transform,
1625 details: Mapping[str, Any] | None = None,
1626 rawsource: str = '',
1627 *children,
1628 **attributes: Any,
1629 ) -> None:
1630 Element.__init__(self, rawsource, *children, **attributes)
1632 self.transform: Transform = transform
1633 """The `docutils.transforms.Transform` class implementing the pending
1634 operation."""
1636 self.details: Mapping[str, Any] = details or {}
1637 """Detail data (dictionary) required by the pending operation."""
1639 def pformat(self, indent: str = ' ', level: int = 0) -> str:
1640 internals = ['.. internal attributes:',
1641 ' .transform: %s.%s' % (self.transform.__module__,
1642 self.transform.__name__),
1643 ' .details:']
1644 details = sorted(self.details.items())
1645 for key, value in details:
1646 if isinstance(value, Node):
1647 internals.append('%7s%s:' % ('', key))
1648 internals.extend(['%9s%s' % ('', line)
1649 for line in value.pformat().splitlines()])
1650 elif (value
1651 and isinstance(value, list)
1652 and isinstance(value[0], Node)):
1653 internals.append('%7s%s:' % ('', key))
1654 for v in value:
1655 internals.extend(['%9s%s' % ('', line)
1656 for line in v.pformat().splitlines()])
1657 else:
1658 internals.append('%7s%s: %r' % ('', key, value))
1659 return (Element.pformat(self, indent, level)
1660 + ''.join((' %s%s\n' % (indent * level, line))
1661 for line in internals))
1663 def copy(self) -> Self:
1664 obj = self.__class__(self.transform, self.details, self.rawsource,
1665 **self.attributes)
1666 obj._document = self._document
1667 obj.source = self.source
1668 obj.line = self.line
1669 return obj
1672class raw(Special, Inline, PreBibliographic,
1673 FixedTextElement, PureTextElement):
1674 """Raw data that is to be passed untouched to the Writer.
1676 Can be used as Body element or Inline element.
1677 """
1678 valid_attributes: Final = Element.valid_attributes + (
1679 'format', 'xml:space')
1682# Decorative Elements
1683# ===================
1685class header(Decorative, Element): pass
1686class footer(Decorative, Element): pass
1689# Structural Subelements
1690# ======================
1692class title(Titular, PreBibliographic, SubStructural, TextElement):
1693 """Title of `document`, `section`, `topic` and generic `admonition`.
1694 """
1695 valid_attributes: Final = Element.valid_attributes + ('auto', 'refid')
1698class subtitle(Titular, PreBibliographic, SubStructural, TextElement):
1699 """Sub-title of `document`, `section` and `sidebar`."""
1701 def validate_position(self) -> None:
1702 """Check position of subtitle: must follow a title."""
1703 if self.parent and self.parent.index(self) == 0:
1704 raise ValidationError(f'Element {self.parent.starttag()} invalid:'
1705 '\n <subtitle> only allowed after <title>.',
1706 problematic_element=self)
1709class meta(PreBibliographic, SubStructural, Element):
1710 """Container for "invisible" bibliographic data, or meta-data."""
1711 valid_attributes: Final = Element.valid_attributes + (
1712 'content', 'dir', 'http-equiv', 'lang', 'media', 'name', 'scheme')
1715class docinfo(SubStructural, Element):
1716 """Container for displayed document meta-data."""
1717 content_model: Final = ((Bibliographic, '+'),)
1718 # (%bibliographic.elements;)+
1721class decoration(PreBibliographic, SubStructural, Element):
1722 """Container for `header` and `footer`."""
1723 content_model: Final = ((header, '?'), # Empty element doesn't make sense,
1724 (footer, '?'), # but is simpler to define.
1725 )
1726 # (header?, footer?)
1728 def get_header(self) -> header:
1729 if not len(self.children) or not isinstance(self.children[0], header):
1730 self.insert(0, header())
1731 return self.children[0]
1733 def get_footer(self) -> footer:
1734 if not len(self.children) or not isinstance(self.children[-1], footer):
1735 self.append(footer())
1736 return self.children[-1]
1739class transition(SubStructural, Element):
1740 """Transitions__ represent "semantic breaks".
1742 __ https://docutils.sourceforge.io/docs/ref/doctree.html#transition
1743 """
1744 # Sibling nodes that are ignored when validating a transition's position
1745 # (titles plus moving and invisible elements except comments):
1746 ignored_siblings = (decoration, meta, pending, substitution_definition,
1747 subtitle, target, title)
1749 def validate_position(self) -> None:
1750 """Check additional constraints on `transition` placement.
1752 A transition may not begin or end section or document text,
1753 nor may two transitions be immediately adjacent.
1754 """
1755 messages = [f'Element {self.parent.starttag()} invalid:']
1756 if isinstance(self.previous_sibling(), transition):
1757 messages.append(
1758 '<transition> may not directly follow another transition.')
1759 i = self.parent.index(self)
1760 prev_siblings = self.parent[:i]
1761 if not [sibling for sibling in prev_siblings
1762 if not isinstance(sibling, self.ignored_siblings)]:
1763 messages.append(
1764 '<transition> may not begin a section or document.')
1765 next_siblings = self.parent[i+1:]
1766 if not [sibling for sibling in next_siblings
1767 if not isinstance(sibling, self.ignored_siblings)]:
1768 messages.append('<transition> may not end a section or document.')
1769 if len(messages) > 1:
1770 raise ValidationError('\n '.join(messages),
1771 problematic_element=self)
1774# Structural Elements
1775# ===================
1777class topic(Structural, Element):
1778 """
1779 Topics__ are non-recursive, mini-sections.
1781 __ https://docutils.sourceforge.io/docs/ref/doctree.html#topic
1782 """
1783 content_model: Final = ((title, '?'), (Body, '+'))
1784 # (title?, (%body.elements;)+)
1787class sidebar(Structural, Element):
1788 """
1789 Sidebars__ are like parallel documents providing related material.
1791 A sidebar is typically offset by a border and "floats" to the side
1792 of the page
1794 __ https://docutils.sourceforge.io/docs/ref/doctree.html#sidebar
1795 """
1796 content_model: Final = ((title, '?'),
1797 (subtitle, '?'),
1798 ((topic, Body), '+'),
1799 )
1800 # ((title, subtitle?)?, (%body.elements; | topic)+)
1801 # "subtitle only after title" is ensured in `subtitle.validate_position()`.
1804class section(Structural, Element):
1805 """Document section__. The main unit of hierarchy.
1807 __ https://docutils.sourceforge.io/docs/ref/doctree.html#section
1808 """
1809 # recursive content model, see below
1812section.content_model = ((title, '.'),
1813 (subtitle, '?'),
1814 ((Body, topic, sidebar, transition), '*'),
1815 ((section, transition), '*'),
1816 )
1817# (title, subtitle?, %structure.model;)
1818# Correct transition placement is ensured in `transition.validate_position()`.
1821# Root Element
1822# ============
1824class document(Root, Element):
1825 """
1826 The document root element.
1828 Do not instantiate this class directly; use
1829 `docutils.utils.new_document()` instead.
1830 """
1831 valid_attributes: Final = Element.valid_attributes + ('title',)
1832 content_model: Final = ((title, '?'),
1833 (subtitle, '?'),
1834 (meta, '*'),
1835 (decoration, '?'),
1836 (docinfo, '?'),
1837 (transition, '?'),
1838 ((Body, topic, sidebar, transition), '*'),
1839 ((section, transition), '*'),
1840 )
1841 # ( (title, subtitle?)?,
1842 # meta*,
1843 # decoration?,
1844 # (docinfo, transition?)?,
1845 # %structure.model; )
1846 # Additional restrictions for `subtitle` and `transition` are tested
1847 # with the respective `validate_position()` methods.
1849 def __init__(self,
1850 settings: Values,
1851 reporter: Reporter,
1852 *args,
1853 **kwargs: Any,
1854 ) -> None:
1855 Element.__init__(self, *args, **kwargs)
1857 self.current_source: StrPath | None = None
1858 """Path to or description of the input source being processed."""
1860 self.current_line: int | None = None
1861 """Line number (1-based) of `current_source`."""
1863 self.settings: Values = settings
1864 """Runtime settings data record."""
1866 self.reporter: Reporter = reporter
1867 """System message generator."""
1869 self.indirect_targets: list[target] = []
1870 """List of indirect target nodes."""
1872 self.substitution_defs: dict[str, substitution_definition] = {}
1873 """Mapping of substitution names to substitution_definition nodes."""
1875 self.substitution_names: dict[str, str] = {}
1876 """Mapping of case-normalized to case-sensitive substitution names."""
1878 self.refnames: dict[str, list[Element]] = {}
1879 """Mapping of names to lists of referencing nodes."""
1881 self.refids: dict[str, list[Element]] = {}
1882 """(Incomplete) Mapping of ids to lists of referencing nodes."""
1884 self.names: dict[str, Element|None] = {}
1885 """Mapping of names to nodes (or ``None`` if name is a duplicate)."""
1887 self.ids: dict[str, Element] = {}
1888 """Mapping of ids to nodes."""
1890 self.nameids: dict[str, str] = {}
1891 """Mapping of names to unique id's."""
1893 self.nametypes: dict[str, bool] = {}
1894 """Mapping of names to hyperlink type. True: explicit, False: implicit.
1895 """
1897 self.footnote_refs: dict[str, list[footnote_reference]] = {}
1898 """Mapping of footnote labels to lists of footnote_reference nodes."""
1900 self.citation_refs: dict[str, list[citation_reference]] = {}
1901 """Mapping of citation labels to lists of citation_reference nodes."""
1903 self.autofootnotes: list[footnote] = []
1904 """List of auto-numbered footnote nodes."""
1906 self.autofootnote_refs: list[footnote_reference] = []
1907 """List of auto-numbered footnote_reference nodes."""
1909 self.symbol_footnotes: list[footnote] = []
1910 """List of symbol footnote nodes."""
1912 self.symbol_footnote_refs: list[footnote_reference] = []
1913 """List of symbol footnote_reference nodes."""
1915 self.footnotes: list[footnote] = []
1916 """List of manually-numbered footnote nodes."""
1918 self.citations: list[citation] = []
1919 """List of citation nodes."""
1921 self.autofootnote_start: int = 1
1922 """Initial auto-numbered footnote number."""
1924 self.symbol_footnote_start: int = 0
1925 """Initial symbol footnote symbol index."""
1927 self.id_counter: Counter[int] = Counter()
1928 """Numbers added to otherwise identical IDs."""
1930 self.parse_messages: list[system_message] = []
1931 """System messages generated while parsing."""
1933 self.transform_messages: list[system_message] = []
1934 """System messages generated while applying transforms."""
1936 import docutils.transforms
1937 self.transformer: Transformer = docutils.transforms.Transformer(self)
1938 """Storage for transforms to be applied to this document."""
1940 self.include_log: list[tuple[StrPath, tuple]] = []
1941 """The current source's parents (to detect inclusion loops)."""
1943 self.decoration: decoration | None = None
1944 """Document's `decoration` node."""
1946 self._document: document = self
1948 def __getstate__(self) -> dict[str, Any]:
1949 """
1950 Return dict with unpicklable references removed.
1951 """
1952 state = self.__dict__.copy()
1953 state['reporter'] = None
1954 state['transformer'] = None
1955 return state
1957 def asdom(self, dom: ModuleType | None = None) -> minidom.Document:
1958 """Return a DOM representation of this document."""
1959 if dom is None:
1960 import xml.dom.minidom as dom
1961 domroot = dom.Document()
1962 domroot.appendChild(self._dom_node(domroot))
1963 return domroot
1965 def set_id(self,
1966 node: Element,
1967 msgnode: Element | None = None,
1968 suggested_prefix: str = '',
1969 ) -> str:
1970 """
1971 Check/set identifiers of element `node`. Return last identifier.
1973 Check `node`s identifiers for duplicates,
1974 create a new identifier if there are no identifiers.
1975 Update `document.ids` and `document.nameids`.
1977 Provisional.
1978 """
1979 if not node['ids']:
1980 node['ids'].append(self.create_id(node, suggested_prefix))
1981 # register and check for duplicates
1982 for id in node['ids']:
1983 self.ids.setdefault(id, node)
1984 if self.ids[id] is not node:
1985 msg = self.reporter.error(f'Duplicate ID: "{id}" used by '
1986 f'{self.ids[id].starttag()} '
1987 f'and {node.starttag()}',
1988 base_node=node)
1989 if msgnode is not None:
1990 msgnode += msg
1991 for name in node['names']:
1992 self.nameids[name] = id
1993 return id
1995 def create_id(self, node: Element, suggested_prefix: str = '') -> str:
1996 # Internal auxiliary method for set_id():
1997 # generate and return a suitable identifier for `node`.
1998 # See also make_id()
1999 id_prefix = self.settings.id_prefix
2000 auto_id_prefix = self.settings.auto_id_prefix
2001 base_id = ''
2002 id = ''
2003 for name in node['names']:
2004 if id_prefix: # allow names starting with numbers
2005 base_id = make_id('x'+name)[1:]
2006 else:
2007 base_id = make_id(name)
2008 # TODO: normalize id-prefix? (would make code simpler)
2009 id = id_prefix + base_id
2010 if base_id and id not in self.ids:
2011 break
2012 else:
2013 if base_id and auto_id_prefix.endswith('%'):
2014 # disambiguate name-derived ID
2015 # TODO: remove second condition after announcing change
2016 prefix = id + '-'
2017 elif (node['dupnames'] and auto_id_prefix.endswith('%')
2018 and make_id(node['dupnames'][0])):
2019 prefix = make_id(node['dupnames'][0]) + '-'
2020 else:
2021 prefix = id_prefix + auto_id_prefix
2022 if prefix.endswith('%'):
2023 prefix = f"""{prefix[:-1]}{suggested_prefix
2024 or make_id(node.tagname)}-"""
2025 while True:
2026 self.id_counter[prefix] += 1
2027 id = f'{prefix}{self.id_counter[prefix]}'
2028 if id not in self.ids:
2029 break
2030 return id
2032 def set_name_id_map(self,
2033 node: Element,
2034 id: str,
2035 msgnode: Element | None = None,
2036 explicit: bool = False,
2037 ) -> None:
2038 """Deprecated. Will be removed in Docutils 2.0."""
2039 warnings.warn('nodes.document.set_name_id_map() will be removed'
2040 ' in Docutils 2.0.', DeprecationWarning, stacklevel=2)
2041 self.note_names(node, msgnode, explicit)
2042 for name in node['names']:
2043 self.nameids[name] = id
2045 def set_duplicate_name(self,
2046 node: Element,
2047 name: str,
2048 msgnode: Element,
2049 explicit: bool,
2050 ) -> None:
2051 """
2052 Handle name conflicts according to the `rST specification`__.
2054 Called by `self.note_names()` when the reference name `name`
2055 of the element `node` is already registered in `self.names`.
2057 `self.names` maps names to elements. The value ``None`` indicates
2058 that the name is a "dupname" (i.e. the document contains two or
2059 more elements with the same name and target type).
2061 `self.nametypes` maps names to booleans representing the target type
2062 (True = "explicit", False = "implicit").
2064 The following state transition table shows how the values
2065 of `self.names` ("name") and `self.nametypes` ("type") items
2066 with key `name` change and which actions are performed.
2068 "Old" is the element with conflicting reference name,
2069 "new" is the element specified by the argument `node`.
2070 The "Input type" is specified by the argument `explicit`.
2072 ======== ==== ======== ==== ======== =============== =======
2073 Input Old State New State Action
2074 -------- -------------- -------------- ------------------------
2075 type name type name type invalidate [#]_ report
2076 ======== ==== ======== ==== ======== =============== =======
2077 explicit old explicit None explicit new,old [#ex]_ WARNING
2078 implicit old explicit old explicit new INFO
2079 explicit old implicit new explicit old INFO
2080 implicit old implicit None implicit new,old [#ex]_ INFO
2081 explicit None explicit None explicit new WARNING
2082 implicit None explicit None explicit new INFO
2083 explicit None implicit new explicit
2084 implicit None implicit None implicit new INFO
2085 ======== ==== ======== ==== ======== =============== =======
2087 .. [#] When "invalidating" an element, `name` is transferred from
2088 the element's "name" attribute to its "dupnames" attribute.
2090 .. [#ex] If both "old" and "new" refer to identical URIs or
2091 reference names, keep the old state and only invalidate "new".
2093 __ https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html
2094 #implicit-hyperlink-targets
2096 Provisional.
2097 """
2098 old_node = self.names[name] # None if name is only dupname
2099 old_explicit = self.nametypes[name]
2100 level = 0 # system message level: 1-info, 2-warning
2102 self.nametypes[name] = old_explicit or explicit
2104 if old_node is not None and (
2105 'refname' in node and node['refname'] == old_node.get('refname')
2106 or 'refuri' in node and node['refuri'] == old_node.get('refuri')
2107 ):
2108 # indirect targets with same reference -> keep old target
2109 level = 1
2110 ref = node.get('refuri') or node.get('refname')
2111 s = f'Duplicate name "{name}" for external target "{ref}".'
2112 dupname(node, name)
2113 elif explicit:
2114 if old_explicit:
2115 level = 2
2116 s = f'Duplicate explicit target name: "{name}".'
2117 dupname(node, name)
2118 if old_node is not None:
2119 dupname(old_node, name)
2120 self.names[name] = None
2121 self.nameids[name] = None
2122 else: # new explicit, old implicit -> override
2123 self.names[name] = node
2124 if old_node is not None:
2125 level = 1
2126 s = f'Target name overrides implicit target name "{name}".'
2127 dupname(old_node, name)
2128 else: # new name is implicit
2129 level = 1
2130 s = f'Duplicate implicit target name: "{name}".'
2131 dupname(node, name)
2132 if old_node is not None and not old_explicit:
2133 dupname(old_node, name)
2134 self.names[name] = None
2135 self.nameids[name] = None
2136 self.set_id(old_node) # set id to get running numbers right
2137 if level:
2138 # don't add backref id for empty targets (not shown in output)
2139 if isinstance(node, target) and not node.children:
2140 backrefs = []
2141 else:
2142 backrefs = [self.set_id(node)]
2143 msg = self.reporter.system_message(level, s, backrefs=backrefs,
2144 base_node=node)
2145 # try appending near to the problem:
2146 if msgnode is not None and 'Body' in repr(msgnode.content_model):
2147 msgnode += msg
2149 def note_names(self,
2150 node: Element,
2151 msgnode: Element|None = None,
2152 explicit: bool = False,
2153 ) -> None:
2154 """
2155 Register the reference names of the element `node`.
2157 Update `self.names` and `self.nametypes`
2158 for each name in the "names" attribute of `node`.
2159 In case of name conflicts, call `self.set_duplicate_name()`.
2160 """
2161 for name in tuple(node['names']):
2162 if name in self.names and self.names[name] != node:
2163 self.set_duplicate_name(node, name, msgnode, explicit)
2164 # attention: modifies node['names']
2165 else:
2166 self.names[name] = node
2167 self.nametypes.setdefault(name, explicit)
2169 def has_name(self, name: str) -> bool:
2170 # TODO: deprecate in Docutils 2.0 (use ``name in document.names``)
2171 return name in self.names
2173 # "note" here is an imperative verb: "take note of".
2174 def note_implicit_target(self, target: Element,
2175 msgnode: Element|None = None) -> None:
2176 self.note_names(target, msgnode, explicit=False)
2177 if getattr(self.settings, "legacy_ids", True):
2178 self.set_id(target, msgnode)
2180 def note_explicit_target(self, target: Element,
2181 msgnode: Element|None = None) -> None:
2182 self.note_names(target, msgnode, explicit=True)
2183 if getattr(self.settings, "legacy_ids", True):
2184 self.set_id(target, msgnode)
2186 def note_refname(self, node: Element) -> None:
2187 self.refnames.setdefault(node['refname'], []).append(node)
2189 def note_refid(self, node: Element) -> None:
2190 self.refids.setdefault(node['refid'], []).append(node)
2192 def note_indirect_target(self, target: target) -> None:
2193 self.indirect_targets.append(target)
2194 if target['names']:
2195 self.note_refname(target)
2197 def note_anonymous_target(self, target: target) -> None:
2198 if getattr(self.settings, "legacy_ids", True):
2199 self.set_id(target)
2201 def note_autofootnote(self, footnote: footnote) -> None:
2202 self.set_id(footnote)
2203 self.autofootnotes.append(footnote)
2205 def note_autofootnote_ref(self, ref: footnote_reference) -> None:
2206 self.set_id(ref)
2207 self.autofootnote_refs.append(ref)
2209 def note_symbol_footnote(self, footnote: footnote) -> None:
2210 self.set_id(footnote)
2211 self.symbol_footnotes.append(footnote)
2213 def note_symbol_footnote_ref(self, ref: footnote_reference) -> None:
2214 self.set_id(ref)
2215 self.symbol_footnote_refs.append(ref)
2217 def note_footnote(self, footnote: footnote) -> None:
2218 self.set_id(footnote)
2219 self.footnotes.append(footnote)
2221 def note_footnote_ref(self, ref: footnote_reference) -> None:
2222 self.set_id(ref)
2223 self.footnote_refs.setdefault(ref['refname'], []).append(ref)
2224 self.note_refname(ref)
2226 def note_citation(self, citation: citation) -> None:
2227 self.citations.append(citation)
2229 def note_citation_ref(self, ref: citation_reference) -> None:
2230 self.set_id(ref)
2231 self.citation_refs.setdefault(ref['refname'], []).append(ref)
2232 self.note_refname(ref)
2234 def note_substitution_def(self,
2235 subdef: substitution_definition,
2236 def_name: str,
2237 msgnode: Element | None = None,
2238 ) -> None:
2239 name = whitespace_normalize_name(def_name)
2240 if name in self.substitution_defs:
2241 msg = self.reporter.error(
2242 'Duplicate substitution definition name: "%s".' % name,
2243 base_node=subdef)
2244 if msgnode is not None:
2245 msgnode += msg
2246 oldnode = self.substitution_defs[name]
2247 dupname(oldnode, name)
2248 # keep only the last definition:
2249 self.substitution_defs[name] = subdef
2250 # case-insensitive mapping:
2251 self.substitution_names[fully_normalize_name(name)] = name
2253 def note_substitution_ref(self,
2254 subref: substitution_reference,
2255 refname: str,
2256 ) -> None:
2257 subref['refname'] = whitespace_normalize_name(refname)
2259 def note_pending(
2260 self, pending: pending, priority: int | None = None) -> None:
2261 self.transformer.add_pending(pending, priority)
2263 def note_parse_message(self, message: system_message) -> None:
2264 self.parse_messages.append(message)
2266 def note_transform_message(self, message: system_message) -> None:
2267 self.transform_messages.append(message)
2269 def note_source(self,
2270 source: StrPath | None,
2271 offset: int | None,
2272 ) -> None:
2273 self.current_source = source and os.fspath(source)
2274 if offset is None:
2275 self.current_line = offset
2276 else:
2277 self.current_line = offset + 1
2279 def copy(self) -> Self:
2280 obj = self.__class__(self.settings, self.reporter,
2281 **self.attributes)
2282 obj.source = self.source
2283 obj.line = self.line
2284 return obj
2286 def get_decoration(self) -> decoration:
2287 if not self.decoration:
2288 self.decoration: decoration = decoration()
2289 index = self.first_child_not_matching_class((Titular, meta))
2290 if index is None:
2291 self.append(self.decoration)
2292 else:
2293 self.insert(index, self.decoration)
2294 return self.decoration
2297# Bibliographic Elements
2298# ======================
2300class author(Bibliographic, TextElement): pass
2301class organization(Bibliographic, TextElement): pass
2302class address(Bibliographic, FixedTextElement): pass
2303class contact(Bibliographic, TextElement): pass
2304class version(Bibliographic, TextElement): pass
2305class revision(Bibliographic, TextElement): pass
2306class status(Bibliographic, TextElement): pass
2307class date(Bibliographic, TextElement): pass
2308class copyright(Bibliographic, TextElement): pass # NoQA: A001 (builtin name)
2311class authors(Bibliographic, Element):
2312 """Container for author information for documents with multiple authors.
2313 """
2314 content_model: Final = ((author, '+'),
2315 (organization, '?'),
2316 (address, '?'),
2317 (contact, '?'),
2318 )
2319 # (author, organization?, address?, contact?)+
2321 def validate_content(self,
2322 model: _ContentModelTuple | None = None,
2323 elements: Sequence | None = None,
2324 ) -> list:
2325 """Repeatedly test for children matching the content model.
2327 Provisional.
2328 """
2329 relics = super().validate_content()
2330 while relics:
2331 relics = super().validate_content(elements=relics)
2332 return relics
2335# Body Elements
2336# =============
2337#
2338# General
2339# -------
2340#
2341# Miscellaneous Body Elements and related Body Subelements (Part)
2343class paragraph(General, TextElement): pass
2344class rubric(Titular, General, TextElement): pass
2347class compound(General, Element):
2348 content_model: Final = ((Body, '+'),) # (%body.elements;)+
2351class container(General, Element):
2352 content_model: Final = ((Body, '+'),) # (%body.elements;)+
2355class attribution(Part, TextElement):
2356 """Visible reference to the source of a `block_quote`."""
2359class block_quote(General, Element):
2360 """An extended quotation, set off from the main text."""
2361 content_model: Final = ((Body, '+'), (attribution, '?'))
2362 # ((%body.elements;)+, attribution?)
2365class reference(General, Inline, Referential, TextElement):
2366 valid_attributes: Final = Element.valid_attributes + (
2367 'anonymous', 'refid', 'refname', 'refuri')
2370# Lists
2371# -----
2372#
2373# Lists (Sequential) and related Body Subelements (Part)
2375class list_item(Part, Element):
2376 content_model: Final = ((Body, '*'),) # (%body.elements;)*
2379class bullet_list(Sequential, Element):
2380 valid_attributes: Final = Element.valid_attributes + ('bullet',)
2381 content_model: Final = ((list_item, '+'),) # (list_item+)
2384class enumerated_list(Sequential, Element):
2385 valid_attributes: Final = Element.valid_attributes + (
2386 'enumtype', 'prefix', 'suffix', 'start')
2387 content_model: Final = ((list_item, '+'),) # (list_item+)
2390class term(Part, TextElement): pass
2391class classifier(Part, TextElement): pass
2394class definition(Part, Element):
2395 """Definition of a `term` in a `definition_list`."""
2396 content_model: Final = ((Body, '+'),) # (%body.elements;)+
2399class definition_list_item(Part, Element):
2400 content_model: Final = ((term, '.'),
2401 ((classifier, term), '*'),
2402 (definition, '.'),
2403 )
2404 # ((term, classifier*)+, definition)
2407class definition_list(Sequential, Element):
2408 """List of terms and their definitions.
2410 Can be used for glossaries or dictionaries, to describe or
2411 classify things, for dialogues, or to itemize subtopics.
2412 """
2413 content_model: Final = ((definition_list_item, '+'),)
2414 # (definition_list_item+)
2417class field_name(Part, TextElement): pass
2420class field_body(Part, Element):
2421 content_model: Final = ((Body, '*'),) # (%body.elements;)*
2424class field(Part, Bibliographic, Element):
2425 content_model: Final = ((field_name, '.'), (field_body, '.'))
2426 # (field_name, field_body)
2429class field_list(Sequential, Element):
2430 """List of label & data pairs.
2432 Typically rendered as a two-column list.
2433 Also used for extension syntax or special processing.
2434 """
2435 content_model: Final = ((field, '+'),) # (field+)
2438class option_string(Part, PureTextElement):
2439 """A literal command-line option. Typically monospaced."""
2442class option_argument(Part, PureTextElement):
2443 """Placeholder text for option arguments."""
2444 valid_attributes: Final = Element.valid_attributes + ('delimiter',)
2446 def astext(self) -> str:
2447 return self.get('delimiter', ' ') + TextElement.astext(self)
2450class option(Part, Element):
2451 """Option element in an `option_list_item`.
2453 Groups an option string with zero or more option argument placeholders.
2454 """
2455 child_text_separator: Final = ''
2456 content_model: Final = ((option_string, '.'), (option_argument, '*'))
2457 # (option_string, option_argument*)
2460class option_group(Part, Element):
2461 """Groups together one or more `option` elements, all synonyms."""
2462 child_text_separator: Final = ', '
2463 content_model: Final = ((option, '+'),) # (option+)
2466class description(Part, Element):
2467 """Describtion of a command-line option."""
2468 content_model: Final = ((Body, '+'),) # (%body.elements;)+
2471class option_list_item(Part, Element):
2472 """Container for a pair of `option_group` and `description` elements.
2473 """
2474 child_text_separator: Final = ' '
2475 content_model: Final = ((option_group, '.'), (description, '.'))
2476 # (option_group, description)
2479class option_list(Sequential, Element):
2480 """Two-column list of command-line options and descriptions."""
2481 content_model: Final = ((option_list_item, '+'),) # (option_list_item+)
2484# Pre-formatted text blocks
2485# -------------------------
2487class literal_block(General, FixedTextElement): pass
2488class doctest_block(General, FixedTextElement): pass
2491class math_block(General, FixedTextElement, PureTextElement):
2492 """Mathematical notation (display formula)."""
2495class line(Part, TextElement):
2496 """Single line of text in a `line_block`."""
2497 indent: str | None = None
2500class line_block(General, Element):
2501 """Sequence of lines and nested line blocks.
2502 """
2503 # recursive content model: (line | line_block)+
2506line_block.content_model = (((line, line_block), '+'),)
2509# Admonitions
2510# -----------
2511# distinctive and self-contained notices
2513class attention(Admonition, Element): pass
2514class caution(Admonition, Element): pass
2515class danger(Admonition, Element): pass
2516class error(Admonition, Element): pass
2517class important(Admonition, Element): pass
2518class note(Admonition, Element): pass
2519class tip(Admonition, Element): pass
2520class hint(Admonition, Element): pass
2521class warning(Admonition, Element): pass
2524class admonition(Admonition, Element):
2525 content_model: Final = ((title, '.'), (Body, '+'))
2526 # (title, (%body.elements;)+)
2529# Footnote and citation
2530# ---------------------
2532class label(Part, PureTextElement):
2533 """Visible identifier for footnotes and citations."""
2536class footnote(General, BackLinkable, Element, Labeled, Targetable):
2537 """Labelled note providing additional context (footnote or endnote)."""
2538 valid_attributes: Final = Element.valid_attributes + ('auto', 'backrefs')
2539 content_model: Final = ((label, '.'), (Body, '+'))
2540 # (label, (%body.elements;)+)
2541 # The label was optional in Docutils < 1.0.
2544class citation(General, BackLinkable, Element, Labeled, Targetable):
2545 content_model: Final = ((label, '.'), (Body, '+'))
2546 # (label, (%body.elements;)+)
2549# Graphical elements
2550# ------------------
2552class image(General, Inline, Element):
2553 """Reference to an image resource.
2555 May be body element or inline element.
2556 """
2557 valid_attributes: Final = Element.valid_attributes + (
2558 'uri', 'alt', 'align', 'height', 'width', 'scale', 'loading')
2560 def astext(self) -> str:
2561 return self.get('alt', '')
2564class caption(Part, TextElement): pass
2567class legend(Part, Element):
2568 """A wrapper for text accompanying a `figure` that is not the caption."""
2569 content_model: Final = ((Body, '+'),) # (%body.elements;)+
2572class figure(General, Element):
2573 """A formal figure, generally an illustration, with a title."""
2574 valid_attributes: Final = Element.valid_attributes + ('align', 'width')
2575 content_model: Final = (((image, reference), '.'),
2576 (caption, '?'),
2577 (legend, '?'),
2578 )
2579 # (image, ((caption, legend?) | legend))
2580 # A caption or legend is required (cf. [bugs: #489]).
2583# Tables
2584# ------
2586class entry(Part, Element):
2587 """An entry in a `row` (a table cell)."""
2588 valid_attributes: Final = Element.valid_attributes + (
2589 'align', 'char', 'charoff', 'colname', 'colsep', 'morecols',
2590 'morerows', 'namest', 'nameend', 'rowsep', 'valign')
2591 content_model: Final = ((Body, '*'),)
2592 # %tbl.entry.mdl -> (%body.elements;)*
2595class row(Part, Element):
2596 """Row of table cells."""
2597 valid_attributes: Final = Element.valid_attributes + ('rowsep', 'valign')
2598 content_model: Final = ((entry, '+'),) # (%tbl.row.mdl;) -> entry+
2601class colspec(Part, Element):
2602 """Specifications for a column in a `tgroup`."""
2603 valid_attributes: Final = Element.valid_attributes + (
2604 'align', 'char', 'charoff', 'colname', 'colnum',
2605 'colsep', 'colwidth', 'rowsep', 'stub')
2607 def propwidth(self) -> int|float:
2608 """Return numerical value of "colwidth__" attribute. Default 1.
2610 Raise ValueError if "colwidth" is zero, negative, or a *fixed value*.
2612 Provisional.
2614 __ https://docutils.sourceforge.io/docs/ref/doctree.html#colwidth
2615 """
2616 # Move current implementation of validate_colwidth() here
2617 # in Docutils 1.0
2618 return validate_colwidth(self.get('colwidth', ''))
2621class thead(Part, Element):
2622 """Row(s) that form the head of a `tgroup`."""
2623 valid_attributes: Final = Element.valid_attributes + ('valign',)
2624 content_model: Final = ((row, '+'),) # (row+)
2627class tbody(Part, Element):
2628 """Body of a `tgroup`."""
2629 valid_attributes: Final = Element.valid_attributes + ('valign',)
2630 content_model: Final = ((row, '+'),) # (row+)
2633class tgroup(Part, Element):
2634 """A portion of a table. Most tables have just one `tgroup`."""
2635 valid_attributes: Final = Element.valid_attributes + (
2636 'align', 'cols', 'colsep', 'rowsep')
2637 content_model: Final = ((colspec, '*'), (thead, '?'), (tbody, '.'))
2638 # (colspec*, thead?, tbody)
2641class table(General, Element):
2642 """A data arrangement with rows and columns."""
2643 valid_attributes: Final = Element.valid_attributes + (
2644 'align', 'colsep', 'frame', 'pgwide', 'rowsep', 'width')
2645 content_model: Final = ((title, '?'), (tgroup, '+'))
2646 # (title?, tgroup+)
2649# Inline Elements
2650# ===============
2652class abbreviation(Inline, TextElement): pass
2653class acronym(Inline, TextElement): pass
2654class emphasis(Inline, TextElement): pass
2655class generated(Inline, TextElement): pass
2656class inline(Inline, TextElement, Targetable): pass
2657class literal(Inline, TextElement): pass
2658class strong(Inline, TextElement): pass
2659class subscript(Inline, TextElement): pass
2660class superscript(Inline, TextElement): pass
2661class title_reference(Inline, TextElement): pass
2664class footnote_reference(Inline, Referential, PureTextElement):
2665 valid_attributes: Final = Element.valid_attributes + (
2666 'auto', 'refid', 'refname')
2669class citation_reference(Inline, Referential, PureTextElement):
2670 valid_attributes: Final = Element.valid_attributes + ('refid', 'refname')
2673class substitution_reference(Inline, TextElement):
2674 valid_attributes: Final = Element.valid_attributes + ('refname',)
2677class math(Inline, PureTextElement):
2678 """Mathematical notation in running text."""
2681class problematic(Inline, TextElement):
2682 valid_attributes: Final = Element.valid_attributes + (
2683 'refid', 'refname', 'refuri')
2686# ========================================
2687# Auxiliary Classes, Functions, and Data
2688# ========================================
2690node_class_names: Sequence[str] = """
2691 Text
2692 abbreviation acronym address admonition attention attribution author
2693 authors
2694 block_quote bullet_list
2695 caption caution citation citation_reference classifier colspec comment
2696 compound contact container copyright
2697 danger date decoration definition definition_list definition_list_item
2698 description docinfo doctest_block document
2699 emphasis entry enumerated_list error
2700 field field_body field_list field_name figure footer
2701 footnote footnote_reference
2702 generated
2703 header hint
2704 image important inline
2705 label legend line line_block list_item literal literal_block
2706 math math_block meta
2707 note
2708 option option_argument option_group option_list option_list_item
2709 option_string organization
2710 paragraph pending problematic
2711 raw reference revision row rubric
2712 section sidebar status strong subscript substitution_definition
2713 substitution_reference subtitle superscript system_message
2714 table target tbody term tgroup thead tip title title_reference topic
2715 transition
2716 version
2717 warning""".split()
2718"""A list of names of all concrete Node subclasses."""
2721class NodeVisitor:
2722 """
2723 "Visitor" pattern [GoF95]_ abstract superclass implementation for
2724 document tree traversals.
2726 Each node class has corresponding methods, doing nothing by
2727 default; override individual methods for specific and useful
2728 behaviour. The `dispatch_visit()` method is called by
2729 `Node.walk()` upon entering a node. `Node.walkabout()` also calls
2730 the `dispatch_departure()` method before exiting a node.
2732 The dispatch methods call "``visit_`` + node class name" or
2733 "``depart_`` + node class name", resp.
2735 This is a base class for visitors whose ``visit_...`` & ``depart_...``
2736 methods must be implemented for *all* compulsory node types encountered
2737 (such as for `docutils.writers.Writer` subclasses).
2738 Unimplemented methods will raise exceptions (except for optional nodes).
2740 For sparse traversals, where only certain node types are of interest, use
2741 subclass `SparseNodeVisitor` instead. When (mostly or entirely) uniform
2742 processing is desired, subclass `GenericNodeVisitor`.
2744 .. [GoF95] Gamma, Helm, Johnson, Vlissides. *Design Patterns: Elements of
2745 Reusable Object-Oriented Software*. Addison-Wesley, Reading, MA, USA,
2746 1995.
2747 """
2749 optional: ClassVar[tuple[str, ...]] = ('meta',)
2750 """
2751 Tuple containing node class names (as strings).
2753 No exception will be raised if writers do not implement visit
2754 or departure functions for these node classes.
2756 Used to ensure transitional compatibility with existing 3rd-party writers.
2757 """
2759 def __init__(self, document: document, /) -> None:
2760 self.document: document = document
2762 def dispatch_visit(self, node) -> None:
2763 """
2764 Call self."``visit_`` + node class name" with `node` as
2765 parameter. If the ``visit_...`` method does not exist, call
2766 self.unknown_visit.
2767 """
2768 node_name = node.__class__.__name__
2769 method = getattr(self, 'visit_' + node_name, self.unknown_visit)
2770 self.document.reporter.debug(
2771 'docutils.nodes.NodeVisitor.dispatch_visit calling %s for %s'
2772 % (method.__name__, node_name))
2773 return method(node)
2775 def dispatch_departure(self, node) -> None:
2776 """
2777 Call self."``depart_`` + node class name" with `node` as
2778 parameter. If the ``depart_...`` method does not exist, call
2779 self.unknown_departure.
2780 """
2781 node_name = node.__class__.__name__
2782 method = getattr(self, 'depart_' + node_name, self.unknown_departure)
2783 self.document.reporter.debug(
2784 'docutils.nodes.NodeVisitor.dispatch_departure calling %s for %s'
2785 % (method.__name__, node_name))
2786 return method(node)
2788 def unknown_visit(self, node) -> None:
2789 """
2790 Called when entering unknown `Node` types.
2792 Raise an exception unless overridden.
2793 """
2794 if (self.document.settings.strict_visitor
2795 or node.__class__.__name__ not in self.optional):
2796 raise NotImplementedError(
2797 '%s visiting unknown node type: %s'
2798 % (self.__class__, node.__class__.__name__))
2800 def unknown_departure(self, node) -> None:
2801 """
2802 Called before exiting unknown `Node` types.
2804 Raise exception unless overridden.
2805 """
2806 if (self.document.settings.strict_visitor
2807 or node.__class__.__name__ not in self.optional):
2808 raise NotImplementedError(
2809 '%s departing unknown node type: %s'
2810 % (self.__class__, node.__class__.__name__))
2813class SparseNodeVisitor(NodeVisitor):
2814 """
2815 Base class for sparse traversals, where only certain node types are of
2816 interest. When ``visit_...`` & ``depart_...`` methods should be
2817 implemented for *all* node types (such as for `docutils.writers.Writer`
2818 subclasses), subclass `NodeVisitor` instead.
2819 """
2822class GenericNodeVisitor(NodeVisitor):
2823 """
2824 Generic "Visitor" abstract superclass, for simple traversals.
2826 Unless overridden, each ``visit_...`` method calls `default_visit()`, and
2827 each ``depart_...`` method (when using `Node.walkabout()`) calls
2828 `default_departure()`. `default_visit()` (and `default_departure()`) must
2829 be overridden in subclasses.
2831 Define fully generic visitors by overriding `default_visit()` (and
2832 `default_departure()`) only. Define semi-generic visitors by overriding
2833 individual ``visit_...()`` (and ``depart_...()``) methods also.
2835 `NodeVisitor.unknown_visit()` (`NodeVisitor.unknown_departure()`) should
2836 be overridden for default behavior.
2837 """
2839 def default_visit(self, node):
2840 """Override for generic, uniform traversals."""
2841 raise NotImplementedError
2843 def default_departure(self, node):
2844 """Override for generic, uniform traversals."""
2845 raise NotImplementedError
2848def _call_default_visit(self: GenericNodeVisitor, node) -> None:
2849 self.default_visit(node)
2852def _call_default_departure(self: GenericNodeVisitor, node) -> None:
2853 self.default_departure(node)
2856def _nop(self: SparseNodeVisitor, node) -> None:
2857 pass
2860def _add_node_class_names(names) -> None:
2861 """Save typing with dynamic assignments:"""
2862 for _name in names:
2863 setattr(GenericNodeVisitor, "visit_" + _name, _call_default_visit)
2864 setattr(GenericNodeVisitor, "depart_" + _name, _call_default_departure)
2865 setattr(SparseNodeVisitor, 'visit_' + _name, _nop)
2866 setattr(SparseNodeVisitor, 'depart_' + _name, _nop)
2869_add_node_class_names(node_class_names)
2872class TreeCopyVisitor(GenericNodeVisitor):
2873 """
2874 Make a complete copy of a tree or branch, including element attributes.
2875 """
2877 def __init__(self, document: document) -> None:
2878 super().__init__(document)
2879 self.parent_stack: list[list] = []
2880 self.parent: list = []
2882 def get_tree_copy(self):
2883 return self.parent[0]
2885 def default_visit(self, node) -> None:
2886 """Copy the current node, and make it the new acting parent."""
2887 newnode = node.copy()
2888 self.parent.append(newnode)
2889 self.parent_stack.append(self.parent)
2890 self.parent = newnode
2892 def default_departure(self, node) -> None:
2893 """Restore the previous acting parent."""
2894 self.parent = self.parent_stack.pop()
2897# Custom Exceptions
2898# =================
2900class ValidationError(ValueError):
2901 """Invalid Docutils Document Tree Element."""
2902 def __init__(self, msg: str, problematic_element: Element = None) -> None:
2903 super().__init__(msg)
2904 self.problematic_element = problematic_element
2907class TreePruningException(Exception):
2908 """
2909 Base class for `NodeVisitor`-related tree pruning exceptions.
2911 Raise subclasses from within ``visit_...`` or ``depart_...`` methods
2912 called from `Node.walk()` and `Node.walkabout()` tree traversals to prune
2913 the tree traversed.
2914 """
2917class SkipChildren(TreePruningException):
2918 """
2919 Do not visit any children of the current node. The current node's
2920 siblings and ``depart_...`` method are not affected.
2921 """
2924class SkipSiblings(TreePruningException):
2925 """
2926 Do not visit any more siblings (to the right) of the current node. The
2927 current node's children and its ``depart_...`` method are not affected.
2928 """
2931class SkipNode(TreePruningException):
2932 """
2933 Do not visit the current node's children, and do not call the current
2934 node's ``depart_...`` method.
2935 """
2938class SkipDeparture(TreePruningException):
2939 """
2940 Do not call the current node's ``depart_...`` method. The current node's
2941 children and siblings are not affected.
2942 """
2945class NodeFound(TreePruningException):
2946 """
2947 Raise to indicate that the target of a search has been found. This
2948 exception must be caught by the client; it is not caught by the traversal
2949 code.
2950 """
2953class StopTraversal(TreePruningException):
2954 """
2955 Stop the traversal altogether. The current node's ``depart_...`` method
2956 is not affected. The parent nodes ``depart_...`` methods are also called
2957 as usual. No other nodes are visited. This is an alternative to
2958 NodeFound that does not cause exception handling to trickle up to the
2959 caller.
2960 """
2963# definition moved here from `utils` to avoid circular import dependency
2964def unescape(text: str,
2965 restore_backslashes: bool = False,
2966 respect_whitespace: bool = False,
2967 ) -> str:
2968 """
2969 Return a string with nulls removed or restored to backslashes.
2970 Backslash-escaped spaces are also removed.
2971 """
2972 # `respect_whitespace` is ignored (since introduction 2016-12-16)
2973 if restore_backslashes:
2974 return text.replace('\x00', '\\')
2975 else:
2976 for sep in ['\x00 ', '\x00\n', '\x00']:
2977 text = ''.join(text.split(sep))
2978 return text
2981def make_id(string: str) -> str:
2982 """
2983 Convert `string` into an identifier and return it.
2985 Docutils identifiers will conform to the regular expression
2986 ``[a-z](-?[a-z0-9]+)*``. For CSS compatibility, identifiers (the "class"
2987 and "id" attributes) should have no underscores, colons, or periods.
2988 Hyphens may be used.
2990 - The `HTML 4.01 spec`_ defines identifiers based on SGML tokens:
2992 ID and NAME tokens must begin with a letter ([A-Za-z]) and may be
2993 followed by any number of letters, digits ([0-9]), hyphens ("-"),
2994 underscores ("_"), colons (":"), and periods (".").
2996 - However the `CSS1 spec`_ defines identifiers based on the "name" token,
2997 a tighter interpretation ("flex" tokenizer notation; "latin1" and
2998 "escape" 8-bit characters have been replaced with entities)::
3000 unicode \\[0-9a-f]{1,4}
3001 latin1 [¡-ÿ]
3002 escape {unicode}|\\[ -~¡-ÿ]
3003 nmchar [-a-z0-9]|{latin1}|{escape}
3004 name {nmchar}+
3006 The CSS1 "nmchar" rule does not include underscores ("_"), colons (":"),
3007 or periods ("."), therefore "class" and "id" attributes should not contain
3008 these characters. They should be replaced with hyphens ("-"). Combined
3009 with HTML's requirements (the first character must be a letter; no
3010 "unicode", "latin1", or "escape" characters), this results in the
3011 ``[a-z](-?[a-z0-9]+)*`` pattern.
3013 .. _HTML 4.01 spec: https://www.w3.org/TR/html401
3014 .. _CSS1 spec: https://www.w3.org/TR/REC-CSS1
3015 """
3016 id = string.lower()
3017 id = id.translate(_non_id_translate_digraphs)
3018 id = id.translate(_non_id_translate)
3019 # get rid of non-ascii characters.
3020 # 'ascii' lowercase to prevent problems with turkish locale.
3021 id = unicodedata.normalize(
3022 'NFKD', id).encode('ascii', 'ignore').decode('ascii')
3023 # shrink runs of whitespace and replace by hyphen
3024 id = _non_id_chars.sub('-', ' '.join(id.split()))
3025 id = _non_id_at_ends.sub('', id)
3026 return str(id)
3029_non_id_chars: re.Pattern[str] = re.compile('[^a-z0-9]+')
3030_non_id_at_ends: re.Pattern[str] = re.compile('^[-0-9]+|-+$')
3031_non_id_translate: dict[int, str] = {
3032 0x00f8: 'o', # o with stroke
3033 0x0111: 'd', # d with stroke
3034 0x0127: 'h', # h with stroke
3035 0x0131: 'i', # dotless i
3036 0x0142: 'l', # l with stroke
3037 0x0167: 't', # t with stroke
3038 0x0180: 'b', # b with stroke
3039 0x0183: 'b', # b with topbar
3040 0x0188: 'c', # c with hook
3041 0x018c: 'd', # d with topbar
3042 0x0192: 'f', # f with hook
3043 0x0199: 'k', # k with hook
3044 0x019a: 'l', # l with bar
3045 0x019e: 'n', # n with long right leg
3046 0x01a5: 'p', # p with hook
3047 0x01ab: 't', # t with palatal hook
3048 0x01ad: 't', # t with hook
3049 0x01b4: 'y', # y with hook
3050 0x01b6: 'z', # z with stroke
3051 0x01e5: 'g', # g with stroke
3052 0x0225: 'z', # z with hook
3053 0x0234: 'l', # l with curl
3054 0x0235: 'n', # n with curl
3055 0x0236: 't', # t with curl
3056 0x0237: 'j', # dotless j
3057 0x023c: 'c', # c with stroke
3058 0x023f: 's', # s with swash tail
3059 0x0240: 'z', # z with swash tail
3060 0x0247: 'e', # e with stroke
3061 0x0249: 'j', # j with stroke
3062 0x024b: 'q', # q with hook tail
3063 0x024d: 'r', # r with stroke
3064 0x024f: 'y', # y with stroke
3065}
3066_non_id_translate_digraphs: dict[int, str] = {
3067 0x00df: 'sz', # ligature sz
3068 0x00e6: 'ae', # ae
3069 0x0153: 'oe', # ligature oe
3070 0x0238: 'db', # db digraph
3071 0x0239: 'qp', # qp digraph
3072}
3075def dupname(node: Element, name: str) -> None:
3076 node['dupnames'].append(name)
3077 node['names'].remove(name)
3078 # Assume that `node` is referenced, even though it isn't;
3079 # we don't want to throw unnecessary system_messages.
3080 node.referenced = True
3083def fully_normalize_name(name: str) -> str:
3084 """Return a case- and whitespace-normalized name."""
3085 return ' '.join(name.lower().split())
3088def whitespace_normalize_name(name: str) -> str:
3089 """Return a whitespace-normalized name."""
3090 return ' '.join(name.split())
3093def serial_escape(value: str) -> str:
3094 """Escape string values that are elements of a list, for serialization."""
3095 return value.replace('\\', r'\\').replace(' ', r'\ ')
3098def split_name_list(s: str) -> list[str]:
3099 r"""Split a string at non-escaped whitespace.
3101 Backslashes escape internal whitespace (cf. `serial_escape()`).
3102 Return list of "names" (after removing escaping backslashes).
3104 >>> split_name_list(r'a\ n\ame two\\ n\\ames'),
3105 ['a name', 'two\\', r'n\ames']
3107 Provisional.
3108 """
3109 s = s.replace('\\', '\x00') # escape with NULL char
3110 s = s.replace('\x00\x00', '\\') # unescape backslashes
3111 s = s.replace('\x00 ', '\x00\x00') # escaped spaces -> NULL NULL
3112 names = s.split(' ')
3113 # restore internal spaces, drop other escaping characters
3114 return [name.replace('\x00\x00', ' ').replace('\x00', '')
3115 for name in names]
3118def pseudo_quoteattr(value: str) -> str:
3119 """Quote attributes for pseudo-xml"""
3120 return '"%s"' % value
3123def parse_measure(measure: str, unit_pattern: str = '[a-zA-Zµ]*|%?'
3124 ) -> tuple[int|float, str]:
3125 """Parse a measure__, return value + unit.
3127 `unit_pattern` is a regular expression describing recognized units.
3128 The default is suited for (but not limited to) CSS3 units and SI units.
3129 It matches runs of ASCII letters or Greek mu, a single percent sign,
3130 or no unit.
3132 __ https://docutils.sourceforge.io/docs/ref/doctree.html#measure
3134 Provisional.
3135 """
3136 match = re.fullmatch(f'(-?[0-9.]+) *({unit_pattern})', measure)
3137 try:
3138 try:
3139 value = int(match.group(1))
3140 except ValueError:
3141 value = float(match.group(1))
3142 unit = match.group(2)
3143 except (AttributeError, ValueError):
3144 raise ValueError(f'"{measure}" is no valid measure.')
3145 return value, unit
3148# Methods to validate `Element attribute`__ values.
3150# Ensure the expected Python `data type`__, normalize, and check for
3151# restrictions.
3152#
3153# The methods can be used to convert `str` values (eg. from an XML
3154# representation) or to validate an existing document tree or node.
3155#
3156# Cf. `Element.validate_attributes()`, `docutils.parsers.docutils_xml`,
3157# and the `attribute_validating_functions` mapping below.
3158#
3159# __ https://docutils.sourceforge.io/docs/ref/doctree.html#attribute-reference
3160# __ https://docutils.sourceforge.io/docs/ref/doctree.html#attribute-types
3162def create_keyword_validator(*keywords: str) -> Callable[[str], str]:
3163 """
3164 Return a function that validates a `str` against given `keywords`.
3166 Provisional.
3167 """
3168 def validate_keywords(value: str) -> str:
3169 if value not in keywords:
3170 allowed = '", \"'.join(keywords)
3171 raise ValueError(f'"{value}" is not one of "{allowed}".')
3172 return value
3173 return validate_keywords
3176def validate_identifier(value: str) -> str:
3177 """
3178 Validate identifier key or class name.
3180 Used in `idref.type`__ and for the tokens in `validate_identifier_list()`.
3182 __ https://docutils.sourceforge.io/docs/ref/doctree.html#idref-type
3184 Provisional.
3185 """
3186 if value != make_id(value):
3187 raise ValueError(f'"{value}" is no valid id or class name.')
3188 return value
3191def validate_identifier_list(value: str | list[str]) -> list[str]:
3192 """
3193 A (space-separated) list of ids or class names.
3195 `value` may be a `list` or a `str` with space separated
3196 ids or class names (cf. `validate_identifier()`).
3198 Used in `classnames.type`__, `ids.type`__, and `idrefs.type`__.
3200 __ https://docutils.sourceforge.io/docs/ref/doctree.html#classnames-type
3201 __ https://docutils.sourceforge.io/docs/ref/doctree.html#ids-type
3202 __ https://docutils.sourceforge.io/docs/ref/doctree.html#idrefs-type
3204 Provisional.
3205 """
3206 if isinstance(value, str):
3207 value = value.split()
3208 for token in value:
3209 validate_identifier(token)
3210 return value
3213def validate_measure(measure: str) -> str:
3214 """
3215 Validate a measure__ (number + optional unit). Return normalized `str`.
3217 See `parse_measure()` for a function returning a "number + unit" tuple.
3219 The unit may be a run of ASCII letters or Greek mu, a single percent sign,
3220 or the empty string. Case is preserved.
3222 Provisional.
3224 __ https://docutils.sourceforge.io/docs/ref/doctree.html#measure
3225 """
3226 value, unit = parse_measure(measure)
3227 return f'{value}{unit}'
3230def validate_colwidth(measure: str|int|float) -> int|float:
3231 """Validate the "colwidth__" attribute.
3233 Provisional:
3234 `measure` must be a `str` and will be returned as normalized `str`
3235 (with unit "*" for proportional values) in Docutils 1.0.
3237 The default unit will change to "pt" in Docutils 2.0.
3239 __ https://docutils.sourceforge.io/docs/ref/doctree.html#colwidth
3240 """
3241 if isinstance(measure, (int, float)):
3242 value = measure
3243 elif measure in ('*', ''): # short for '1*'
3244 value = 1
3245 else:
3246 try:
3247 value, _unit = parse_measure(measure, unit_pattern='[*]?')
3248 except ValueError:
3249 value = -1
3250 if value <= 0:
3251 raise ValueError(f'"{measure}" is no proportional measure.')
3252 return value
3255def validate_NMTOKEN(value: str) -> str:
3256 """
3257 Validate a "name token": a `str` of ASCII letters, digits, and [-._].
3259 Provisional.
3260 """
3261 if not re.fullmatch('[-._A-Za-z0-9]+', value):
3262 raise ValueError(f'"{value}" is no NMTOKEN.')
3263 return value
3266def validate_NMTOKENS(value: str | list[str]) -> list[str]:
3267 """
3268 Validate a list of "name tokens".
3270 Provisional.
3271 """
3272 if isinstance(value, str):
3273 value = value.split()
3274 for token in value:
3275 validate_NMTOKEN(token)
3276 return value
3279def validate_refname_list(value: str | list[str]) -> list[str]:
3280 """
3281 Validate a list of `reference names`__.
3283 Reference names may contain all characters;
3284 whitespace is normalized (cf, `whitespace_normalize_name()`).
3286 `value` may be either a `list` of names or a `str` with
3287 space separated names (with internal spaces backslash escaped
3288 and literal backslashes doubled cf. `serial_escape()`).
3290 Return a list of whitespace-normalized, unescaped reference names.
3292 Provisional.
3294 __ https://docutils.sourceforge.io/docs/ref/doctree.html#reference-name
3295 """
3296 if isinstance(value, str):
3297 value = split_name_list(value)
3298 return [whitespace_normalize_name(name) for name in value]
3301def validate_yesorno(value: str | int | bool) -> bool:
3302 """Validate a `%yesorno`__ (flag) value.
3304 The string literal "0" evaluates to ``False``, all other
3305 values are converterd with `bool()`.
3307 __ https://docutils.sourceforge.io/docs/ref/doctree.html#yesorno
3308 """
3309 if value == "0":
3310 return False
3311 return bool(value)
3314ATTRIBUTE_VALIDATORS: dict[str, Callable[[str], Any]] = {
3315 'alt': str, # CDATA
3316 'align': str,
3317 'anonymous': validate_yesorno,
3318 'auto': str, # CDATA (only '1' or '*' are used in rST)
3319 'backrefs': validate_identifier_list,
3320 'bullet': str, # CDATA (only '-', '+', or '*' are used in rST)
3321 'classes': validate_identifier_list,
3322 'char': str, # from Exchange Table Model (CALS), currently ignored
3323 'charoff': validate_NMTOKEN, # from CALS, currently ignored
3324 'colname': validate_NMTOKEN, # from CALS, currently ignored
3325 'colnum': int, # from CALS, currently ignored
3326 'cols': int, # from CALS: "NMTOKEN, […] must be an integer > 0".
3327 'colsep': validate_yesorno,
3328 'colwidth': validate_colwidth, # see docstring for pending changes
3329 'content': str, # <meta>
3330 'delimiter': str,
3331 'dir': create_keyword_validator('ltr', 'rtl', 'auto'), # <meta>
3332 'dupnames': validate_refname_list,
3333 'enumtype': create_keyword_validator('arabic', 'loweralpha', 'lowerroman',
3334 'upperalpha', 'upperroman'),
3335 'format': str, # CDATA (space separated format names)
3336 'frame': create_keyword_validator('top', 'bottom', 'topbot', 'all',
3337 'sides', 'none'), # from CALS, ignored
3338 'height': validate_measure,
3339 'http-equiv': str, # <meta>
3340 'ids': validate_identifier_list,
3341 'lang': str, # <meta>
3342 'level': int,
3343 'line': int,
3344 'ltrim': validate_yesorno,
3345 'loading': create_keyword_validator('embed', 'link', 'lazy'),
3346 'media': str, # <meta>
3347 'morecols': int,
3348 'morerows': int,
3349 'name': whitespace_normalize_name, # in <reference> (deprecated)
3350 # 'name': node_attributes.validate_NMTOKEN, # in <meta>
3351 'names': validate_refname_list,
3352 'namest': validate_NMTOKEN, # start of span, from CALS, currently ignored
3353 'nameend': validate_NMTOKEN, # end of span, from CALS, currently ignored
3354 'pgwide': validate_yesorno, # from CALS, currently ignored
3355 'prefix': str,
3356 'refid': validate_identifier,
3357 'refname': whitespace_normalize_name,
3358 'refuri': str,
3359 'rowsep': validate_yesorno,
3360 'rtrim': validate_yesorno,
3361 'scale': int,
3362 'scheme': str,
3363 'source': str,
3364 'start': int,
3365 'stub': validate_yesorno,
3366 'suffix': str,
3367 'title': str,
3368 'type': validate_NMTOKEN,
3369 'uri': str,
3370 'valign': create_keyword_validator('top', 'middle', 'bottom'), # from CALS
3371 'width': validate_measure,
3372 'xml:space': create_keyword_validator('default', 'preserve'),
3373 }
3374"""
3375Mapping of `attribute names`__ to validating functions.
3377Provisional.
3379__ https://docutils.sourceforge.io/docs/ref/doctree.html#attribute-reference
3380"""