Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/docutils/nodes.py: 57%
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 10405 2026-09-26 17:50:44Z 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.Element | minidom.Text:
122 """Return a DOM **fragment** representation of this Node."""
123 if dom is None:
124 import xml.dom.minidom as dom
125 domroot = dom.Document()
126 return self._dom_node(domroot)
128 def pformat(self, indent: str = ' ', level: int = 0) -> str:
129 """
130 Return an indented pseudo-XML representation, for test purposes.
132 Override in subclasses.
133 """
134 raise NotImplementedError
136 def copy(self) -> Self:
137 """Return a copy of self."""
138 raise NotImplementedError
140 def deepcopy(self) -> Self:
141 """Return a deep copy of self (also copying children)."""
142 raise NotImplementedError
144 def astext(self) -> str:
145 """Return a string representation of this Node."""
146 raise NotImplementedError
148 def setup_child(self, child) -> None:
149 child.parent = self
150 if self.document:
151 child.document = self.document
152 if child.source is None:
153 child.source = self.document.current_source
154 if child.line is None:
155 child.line = self.document.current_line
157 def walk(self, visitor: NodeVisitor) -> bool:
158 """
159 Traverse a tree of `Node` objects, calling the
160 `dispatch_visit()` method of `visitor` when entering each
161 node. (The `walkabout()` method is similar, except it also
162 calls the `dispatch_departure()` method before exiting each
163 node.)
165 This tree traversal supports limited in-place tree
166 modifications. Replacing one node with one or more nodes is
167 OK, as is removing an element. However, if the node removed
168 or replaced occurs after the current node, the old node will
169 still be traversed, and any new nodes will not.
171 Within ``visit`` methods (and ``depart`` methods for
172 `walkabout()`), `TreePruningException` subclasses may be raised
173 (`SkipChildren`, `SkipSiblings`, `SkipNode`, `SkipDeparture`).
175 Parameter `visitor`: A `NodeVisitor` object, containing a
176 ``visit`` implementation for each `Node` subclass encountered.
178 Return true if we should stop the traversal.
179 """
180 stop = False
181 visitor.document.reporter.debug(
182 'docutils.nodes.Node.walk calling dispatch_visit for %s'
183 % self.__class__.__name__)
184 try:
185 try:
186 visitor.dispatch_visit(self)
187 except (SkipChildren, SkipNode):
188 return stop
189 except SkipDeparture: # not applicable; ignore
190 pass
191 children = self.children
192 try:
193 for child in children[:]:
194 if child.walk(visitor):
195 stop = True
196 break
197 except SkipSiblings:
198 pass
199 except StopTraversal:
200 stop = True
201 return stop
203 def walkabout(self, visitor: NodeVisitor) -> bool:
204 """
205 Perform a tree traversal similarly to `Node.walk()` (which
206 see), except also call the `dispatch_departure()` method
207 before exiting each node.
209 Parameter `visitor`: A `NodeVisitor` object, containing a
210 ``visit`` and ``depart`` implementation for each `Node`
211 subclass encountered.
213 Return true if we should stop the traversal.
214 """
215 call_depart = True
216 stop = False
217 visitor.document.reporter.debug(
218 'docutils.nodes.Node.walkabout calling dispatch_visit for %s'
219 % self.__class__.__name__)
220 try:
221 try:
222 visitor.dispatch_visit(self)
223 except SkipNode:
224 return stop
225 except SkipDeparture:
226 call_depart = False
227 children = self.children
228 try:
229 for child in children[:]:
230 if child.walkabout(visitor):
231 stop = True
232 break
233 except SkipSiblings:
234 pass
235 except SkipChildren:
236 pass
237 except StopTraversal:
238 stop = True
239 if call_depart:
240 visitor.document.reporter.debug(
241 'docutils.nodes.Node.walkabout calling dispatch_departure '
242 'for %s' % self.__class__.__name__)
243 visitor.dispatch_departure(self)
244 return stop
246 def _fast_findall(self, cls: type|tuple[type]) -> Iterator:
247 """Return iterator that only supports instance checks."""
248 if isinstance(self, cls):
249 yield self
250 for child in self.children:
251 yield from child._fast_findall(cls)
253 def _superfast_findall(self) -> Iterator:
254 """Return iterator that doesn't check for a condition."""
255 # This is different from ``iter(self)`` implemented via
256 # __getitem__() and __len__() in the Element subclass,
257 # which yields only the direct children.
258 yield self
259 for child in self.children:
260 yield from child._superfast_findall()
262 def findall(self,
263 condition: type|tuple[type]|Callable[[Node], bool]|None = None,
264 include_self: bool = True,
265 descend: bool = True,
266 siblings: bool = False,
267 ascend: bool = False,
268 ) -> Iterator:
269 """
270 Return an iterator yielding nodes following `self`:
272 * self (if `include_self` is true)
273 * all descendants in tree traversal order (if `descend` is true)
274 * the following siblings (if `siblings` is true) and their
275 descendants (if also `descend` is true)
276 * the following siblings of the parent (if `ascend` is true) and
277 their descendants (if also `descend` is true), and so on.
279 If `condition` is not None, the iterator yields only nodes
280 for which ``condition(node)`` is true.
281 If `condition` is a type (or tuple of types) ``cls``, it is equivalent
282 to a function consisting of ``return isinstance(node, cls)``.
284 If `ascend` is true, assume `siblings` to be true as well.
286 If the tree structure is modified during iteration, the result
287 is undefined.
289 For example, given the following tree::
291 <paragraph>
292 <emphasis> <--- emphasis.traverse() and
293 <strong> <--- strong.traverse() are called.
294 Foo
295 Bar
296 <reference name="Baz" refid="baz">
297 Baz
299 Then tuple(emphasis.traverse()) equals ::
301 (<emphasis>, <strong>, <#text: Foo>, <#text: Bar>)
303 and list(strong.traverse(ascend=True) equals ::
305 [<strong>, <#text: Foo>, <#text: Bar>, <reference>, <#text: Baz>]
306 """
307 if ascend:
308 siblings = True
309 # Check for special argument combinations that allow using an
310 # optimized version of traverse()
311 if include_self and descend and not siblings:
312 if condition is None:
313 yield from self._superfast_findall()
314 return
315 elif isinstance(condition, (type, tuple)):
316 yield from self._fast_findall(condition)
317 return
318 # Check if `condition` is a class (check for TypeType for Python
319 # implementations that use only new-style classes, like PyPy).
320 if isinstance(condition, (type, tuple)):
321 class_or_tuple = condition
323 def condition(node, class_or_tuple=class_or_tuple):
324 return isinstance(node, class_or_tuple)
326 if include_self and (condition is None or condition(self)):
327 yield self
328 if descend and len(self.children):
329 for child in self:
330 yield from child.findall(condition=condition,
331 include_self=True, descend=True,
332 siblings=False, ascend=False)
333 if siblings or ascend:
334 node = self
335 while node.parent:
336 index = node.parent.index(node)
337 # extra check since Text nodes have value-equality
338 while node.parent[index] is not node:
339 index = node.parent.index(node, index + 1)
340 for sibling in node.parent[index+1:]:
341 yield from sibling.findall(
342 condition=condition,
343 include_self=True, descend=descend,
344 siblings=False, ascend=False)
345 if not ascend:
346 break
347 else:
348 node = node.parent
350 def traverse(
351 self,
352 condition: type|tuple[type]|Callable[[Node], bool]|None = None,
353 include_self: bool = True,
354 descend: bool = True,
355 siblings: bool = False,
356 ascend: bool = False,
357 ) -> list:
358 """Return list of nodes following `self`.
360 For looping, Node.findall() is faster and more memory efficient.
361 """
362 # traverse() may be eventually removed:
363 warnings.warn('nodes.Node.traverse() is obsoleted by Node.findall().',
364 DeprecationWarning, stacklevel=2)
365 return list(self.findall(condition, include_self, descend,
366 siblings, ascend))
368 def next_node(
369 self,
370 condition: type|tuple[type]|Callable[[Node], bool]|None = None,
371 include_self: bool = False,
372 descend: bool = True,
373 siblings: bool = False,
374 ascend: bool = False,
375 ) -> Node | None:
376 """
377 Return the first node in the iterator returned by findall(),
378 or None if the iterable is empty.
380 Parameter list is the same as of `findall()`. Note that `include_self`
381 defaults to False, though.
382 """
383 try:
384 return next(self.findall(condition, include_self,
385 descend, siblings, ascend))
386 except StopIteration:
387 return None
389 def validate(self, recursive: bool = True) -> None:
390 """Raise ValidationError if this node is not valid.
392 Override in subclasses that define validity constraints.
393 """
395 def validate_position(self) -> None:
396 """Hook for additional checks of the parent's content model.
398 Raise ValidationError, if `self` is at an invalid position.
400 Override in subclasses with complex validity constraints. See
401 `subtitle.validate_position()` and `transition.validate_position()`.
402 """
405class Text(Node, str): # NoQA: SLOT000 (Node doesn't define __slots__)
406 """
407 Instances are terminal nodes (leaves) containing text only; no child
408 nodes or attributes. Initialize by passing a string to the constructor.
410 Access the raw (null-escaped) text with ``str(<instance>)``
411 and unescaped text with ``<instance>.astext()``.
412 """
414 tagname: Final = '#text'
416 children: Final = ()
417 """Text nodes have no children, and cannot have children."""
419 def __new__(cls, data: str, rawsource: None = None) -> Self:
420 """Assert that `data` is not an array of bytes
421 and warn if the deprecated `rawsource` argument is used.
422 """
423 if isinstance(data, bytes):
424 raise TypeError('expecting str data, not bytes')
425 if rawsource is not None:
426 warnings.warn('nodes.Text: initialization argument "rawsource" '
427 'is ignored and will be removed in Docutils 2.0.',
428 DeprecationWarning, stacklevel=2)
429 return str.__new__(cls, data)
431 def shortrepr(self, maxlen: int = 18) -> str:
432 data = self
433 if len(data) > maxlen:
434 data = data[:maxlen-4] + ' ...'
435 return '<%s: %r>' % (self.tagname, str(data))
437 def __repr__(self) -> str:
438 return self.shortrepr(maxlen=68)
440 def astext(self) -> str:
441 return str(unescape(self))
443 def _dom_node(self, domroot: minidom.Document) -> minidom.Text:
444 return domroot.createTextNode(str(self))
446 def copy(self) -> Self:
447 return self.__class__(str(self))
449 def deepcopy(self) -> Self:
450 return self.copy()
452 def pformat(self, indent: str = ' ', level: int = 0) -> str:
453 try:
454 if self.document.settings.detailed:
455 tag = '%s%s' % (indent*level, '<#text>')
456 lines = (indent*(level+1) + repr(line)
457 for line in self.splitlines(True))
458 return '\n'.join((tag, *lines)) + '\n'
459 except AttributeError:
460 pass
461 indent = indent * level
462 lines = [indent+line for line in self.astext().splitlines()]
463 if not lines:
464 return ''
465 return '\n'.join(lines) + '\n'
467 # rstrip and lstrip are used by substitution definitions where
468 # they are expected to return a Text instance, this was formerly
469 # taken care of by UserString.
471 def rstrip(self, chars: str | None = None) -> Self:
472 return self.__class__(str.rstrip(self, chars))
474 def lstrip(self, chars: str | None = None) -> Self:
475 return self.__class__(str.lstrip(self, chars))
478class Element(Node):
479 """
480 `Element` is the superclass to all specific elements.
482 Elements contain attributes and child nodes.
483 They can be described as a cross between a list and a dictionary.
485 Elements emulate dictionaries for external [#]_ attributes, indexing by
486 attribute name (a string). To set the attribute 'att' to 'value', do::
488 element['att'] = 'value'
490 .. [#] External attributes correspond to the XML element attributes.
491 From its `Node` superclass, Element also inherits "internal"
492 class attributes that are accessed using the standard syntax, e.g.
493 ``element.parent``.
495 There are two special attributes: 'ids' and 'names'. Both are
496 lists of unique identifiers: 'ids' conform to the regular expression
497 ``[a-z](-?[a-z0-9]+)*`` (see the make_id() function for rationale and
498 details). 'names' serve as user-friendly interfaces to IDs; they are
499 case- and whitespace-normalized (see the fully_normalize_name() function).
501 Elements emulate lists for child nodes (element nodes and/or text
502 nodes), indexing by integer. To get the first child node, use::
504 element[0]
506 to iterate over the child nodes (without descending), use::
508 for child in element:
509 ...
511 Elements may be constructed using the ``+=`` operator. To add one new
512 child node to element, do::
514 element += node
516 This is equivalent to ``element.append(node)``.
518 To add a list of multiple child nodes at once, use the same ``+=``
519 operator::
521 element += [node1, node2]
523 This is equivalent to ``element.extend([node1, node2])``.
524 """
526 list_attributes: Final = ('ids', 'classes', 'names', 'dupnames')
527 """Tuple of attributes that are initialized to empty lists.
529 NOTE: Derived classes should update this value when supporting
530 additional list attributes.
531 """
533 valid_attributes: Final = list_attributes + ('source',)
534 """Tuple of attributes that are valid for elements of this class.
536 NOTE: Derived classes should update this value when supporting
537 additional attributes.
538 """
540 common_attributes: Final = valid_attributes
541 """Tuple of `common attributes`__ known to all Doctree Element classes.
543 __ https://docutils.sourceforge.io/docs/ref/doctree.html#common-attributes
544 """
546 known_attributes: Final = common_attributes
547 """Alias for `common_attributes`. Will be removed in Docutils 2.0."""
549 basic_attributes: Final = list_attributes
550 """Common list attributes. Deprecated. Will be removed in Docutils 2.0."""
552 local_attributes: Final = ('backrefs',)
553 """Obsolete. Will be removed in Docutils 2.0."""
555 content_model: ClassVar[_ContentModelTuple] = ()
556 """Python representation of the element's content model (cf. docutils.dtd).
558 A tuple of ``(category, quantifier)`` tuples with
560 :category: class or tuple of classes that are expected at this place(s)
561 in the list of children
562 :quantifier: string representation stating how many elements
563 of `category` are expected. Value is one of:
564 '.' (exactly one), '?' (zero or one),
565 '+' (one or more), '*' (zero or more).
567 NOTE: The default describes the empty element. Derived classes should
568 update this value to match their content model.
570 Provisional.
571 """
573 tagname: str | None = None
574 """The element generic identifier.
576 If None, it is set as an instance attribute to the name of the class.
577 """
579 child_text_separator: Final = '\n\n'
580 """Separator for child nodes, used by `astext()` method."""
582 def __init__(self,
583 rawsource: str = '',
584 *children,
585 **attributes: Any,
586 ) -> None:
587 self.rawsource = rawsource
588 """The raw text from which this element was constructed.
590 For informative and debugging purposes. Don't rely on its value!
592 NOTE: some elements do not set this value (default '').
593 """
594 if isinstance(rawsource, Element):
595 raise TypeError('First argument "rawsource" must be a string.')
597 self.children: list = []
598 """List of child nodes (elements and/or `Text`)."""
600 self.extend(children) # maintain parent info
602 self.attributes: dict[str, Any] = {}
603 """Dictionary of attribute {name: value}."""
605 # Initialize list attributes.
606 for att in self.list_attributes:
607 self.attributes[att] = []
609 for att, value in attributes.items():
610 att = att.lower() # normalize attribute name
611 if att in self.list_attributes:
612 # lists are mutable; make a copy for this node
613 self.attributes[att] = value[:]
614 else:
615 self.attributes[att] = value
617 if self.tagname is None:
618 self.tagname: str = self.__class__.__name__
620 def _dom_node(self, domroot: minidom.Document) -> minidom.Element:
621 element = domroot.createElement(self.tagname)
622 for attribute, value in self.attlist():
623 if isinstance(value, list):
624 value = ' '.join(serial_escape('%s' % (v,)) for v in value)
625 element.setAttribute(attribute, '%s' % value)
626 for child in self.children:
627 element.appendChild(child._dom_node(domroot))
628 return element
630 def __repr__(self) -> str:
631 data = ''
632 for c in self.children:
633 data += c.shortrepr()
634 if len(data) > 60:
635 data = data[:56] + ' ...'
636 break
637 if self['names']:
638 return '<%s "%s": %s>' % (self.tagname,
639 '; '.join(self['names']), data)
640 else:
641 return '<%s: %s>' % (self.tagname, data)
643 def shortrepr(self) -> str:
644 if self['names']:
645 return '<%s "%s"...>' % (self.tagname, '; '.join(self['names']))
646 else:
647 return '<%s...>' % self.tagname
649 def __str__(self) -> str:
650 if self.children:
651 return '%s%s%s' % (self.starttag(),
652 ''.join(str(c) for c in self.children),
653 self.endtag())
654 else:
655 return self.emptytag()
657 def starttag(self, quoteattr: Callable[[str], str] | None = None) -> str:
658 # the optional arg is used by the docutils_xml writer
659 if quoteattr is None:
660 quoteattr = pseudo_quoteattr
661 parts = [self.tagname]
662 for name, value in self.attlist():
663 if value is None: # boolean attribute
664 parts.append('%s="True"' % name)
665 continue
666 if isinstance(value, bool):
667 value = str(int(value))
668 if isinstance(value, list):
669 values = [serial_escape('%s' % (v,)) for v in value]
670 value = ' '.join(values)
671 else:
672 value = str(value)
673 value = quoteattr(value)
674 parts.append('%s=%s' % (name, value))
675 return '<%s>' % ' '.join(parts)
677 def endtag(self) -> str:
678 return '</%s>' % self.tagname
680 def emptytag(self) -> str:
681 attributes = ('%s="%s"' % (n, v) for n, v in self.attlist())
682 return '<%s/>' % ' '.join((self.tagname, *attributes))
684 def __len__(self) -> int:
685 return len(self.children)
687 def __contains__(self, key: str | Node) -> bool:
688 # Test for both, children and attributes with operator ``in``.
689 #
690 # Caution: Document Tree traversal also returns Text nodes where
691 # ``in`` looks for substrings in the text content.
692 if isinstance(key, str):
693 return key in self.attributes
694 return key in self.children
696 def __getitem__(self, key: str | int | slice) -> Any:
697 if isinstance(key, str):
698 return self.attributes[key]
699 elif isinstance(key, int):
700 return self.children[key]
701 elif isinstance(key, slice):
702 assert key.step in (None, 1), 'cannot handle slice with stride'
703 return self.children[key.start:key.stop]
704 else:
705 raise TypeError('element index must be an integer, a slice, or '
706 'an attribute name string')
708 def __setitem__(self, key, item) -> None:
709 if isinstance(key, str):
710 self.attributes[str(key)] = item
711 elif isinstance(key, int):
712 self.setup_child(item)
713 self.children[key] = item
714 elif isinstance(key, slice):
715 assert key.step in (None, 1), 'cannot handle slice with stride'
716 for node in item:
717 self.setup_child(node)
718 self.children[key.start:key.stop] = item
719 else:
720 raise TypeError('element index must be an integer, a slice, or '
721 'an attribute name string')
723 def __delitem__(self, key: str | int | slice) -> None:
724 if isinstance(key, str):
725 del self.attributes[key]
726 elif isinstance(key, int):
727 del self.children[key]
728 elif isinstance(key, slice):
729 assert key.step in (None, 1), 'cannot handle slice with stride'
730 del self.children[key.start:key.stop]
731 else:
732 raise TypeError('element index must be an integer, a simple '
733 'slice, or an attribute name string')
735 def __add__(self, other: list) -> list:
736 return self.children + other
738 def __radd__(self, other: list) -> list:
739 return other + self.children
741 def __iadd__(self, other) -> Self:
742 """Append a node or a list of nodes to `self.children`."""
743 if isinstance(other, Node):
744 self.append(other)
745 elif other is not None:
746 self.extend(other)
747 return self
749 def astext(self) -> str:
750 return self.child_text_separator.join(
751 [child.astext() for child in self.children])
753 def non_default_attributes(self) -> dict[str, Any]:
754 atts = {key: value for key, value in self.attributes.items()
755 if self.is_not_default(key)}
756 return atts
758 def attlist(self) -> list[tuple[str, Any]]:
759 return sorted(self.non_default_attributes().items())
761 def get(self, key: str, failobj: Any | None = None) -> Any:
762 return self.attributes.get(key, failobj)
764 def hasattr(self, attr: str) -> bool:
765 return attr in self.attributes
767 def delattr(self, attr: str) -> None:
768 if attr in self.attributes:
769 del self.attributes[attr]
771 def setdefault(self, key: str, failobj: Any | None = None) -> Any:
772 return self.attributes.setdefault(key, failobj)
774 has_key = hasattr
776 def get_language_code(self, fallback: str = '') -> str:
777 """Return node's language tag.
779 Look iteratively in self and parents for a class argument
780 starting with ``language-`` and return the remainder of it
781 (which should be a `BCP49` language tag) or the `fallback`.
782 """
783 for cls in self.get('classes', []):
784 if cls.startswith('language-'):
785 return cls.removeprefix('language-')
786 try:
787 return self.parent.get_language_code(fallback)
788 except AttributeError:
789 return fallback
791 def append(self, item) -> None:
792 self.setup_child(item)
793 self.children.append(item)
795 def extend(self, item: Iterable) -> None:
796 for node in item:
797 self.append(node)
799 def insert(self, index: SupportsIndex, item) -> None:
800 if isinstance(item, Node):
801 self.setup_child(item)
802 self.children.insert(index, item)
803 elif item is not None:
804 self[index:index] = item
806 def pop(self, i: int = -1):
807 return self.children.pop(i)
809 def remove(self, item) -> None:
810 self.children.remove(item)
812 def index(self, item, start: int = 0, stop: int = sys.maxsize) -> int:
813 return self.children.index(item, start, stop)
815 def previous_sibling(self):
816 """Return preceding sibling node or ``None``."""
817 try:
818 i = self.parent.index(self)
819 except (AttributeError):
820 return None
821 return self.parent[i-1] if i > 0 else None
823 def section_hierarchy(self) -> list[section]:
824 """Return the element's section anchestors.
826 Return a list of all <section> elements that contain `self`
827 (including `self` if it is a <section>) and have a parent node.
829 List item ``[i]`` is the parent <section> of level i+1
830 (1: section, 2: subsection, 3: subsubsection, ...).
831 The length of the list is the element's section level.
833 See `docutils.parsers.rst.states.RSTState.check_subsection()`
834 for a usage example.
836 Provisional. May be changed or removed without warning.
837 """
838 sections = []
839 node = self
840 while node.parent is not None:
841 if isinstance(node, section):
842 sections.append(node)
843 node = node.parent
844 sections.reverse()
845 return sections
847 def is_not_default(self, key: str) -> bool:
848 if self[key] == [] and key in self.list_attributes:
849 return False
850 else:
851 return True
853 def update_basic_atts(self, dict_: Mapping[str, Any] | Element) -> None:
854 """
855 Update basic attributes ('ids', 'names', 'classes',
856 'dupnames', but not 'source') from node or dictionary `dict_`.
858 Provisional.
859 """
860 if isinstance(dict_, Node):
861 dict_ = dict_.attributes
862 for att in self.basic_attributes:
863 self.append_attr_list(att, dict_.get(att, []))
865 def append_attr_list(self, attr: str, values: Iterable[Any]) -> None:
866 """
867 For each element in values, if it does not exist in self[attr], append
868 it.
870 NOTE: Requires self[attr] and values to be sequence type and the
871 former should specifically be a list.
872 """
873 # List Concatenation
874 for value in values:
875 if value not in self[attr]:
876 self[attr].append(value)
878 def coerce_append_attr_list(
879 self, attr: str, value: list[Any] | Any) -> None:
880 """
881 First, convert both self[attr] and value to a non-string sequence
882 type; if either is not already a sequence, convert it to a list of one
883 element. Then call append_attr_list.
885 NOTE: self[attr] and value both must not be None.
886 """
887 # List Concatenation
888 if not isinstance(self.get(attr), list):
889 self[attr] = [self[attr]]
890 if not isinstance(value, list):
891 value = [value]
892 self.append_attr_list(attr, value)
894 def replace_attr(self, attr: str, value: Any, force: bool = True) -> None:
895 """
896 If self[attr] does not exist or force is True or omitted, set
897 self[attr] to value, otherwise do nothing.
898 """
899 # One or the other
900 if force or self.get(attr) is None:
901 self[attr] = value
903 def copy_attr_convert(
904 self, attr: str, value: Any, replace: bool = True) -> None:
905 """
906 If attr is an attribute of self, set self[attr] to
907 [self[attr], value], otherwise set self[attr] to value.
909 NOTE: replace is not used by this function and is kept only for
910 compatibility with the other copy functions.
911 """
912 if self.get(attr) is not value:
913 self.coerce_append_attr_list(attr, value)
915 def copy_attr_coerce(self, attr: str, value: Any, replace: bool) -> None:
916 """
917 If attr is an attribute of self and either self[attr] or value is a
918 list, convert all non-sequence values to a sequence of 1 element and
919 then concatenate the two sequence, setting the result to self[attr].
920 If both self[attr] and value are non-sequences and replace is True or
921 self[attr] is None, replace self[attr] with value. Otherwise, do
922 nothing.
923 """
924 if self.get(attr) is not value:
925 if isinstance(self.get(attr), list) or \
926 isinstance(value, list):
927 self.coerce_append_attr_list(attr, value)
928 else:
929 self.replace_attr(attr, value, replace)
931 def copy_attr_concatenate(
932 self, attr: str, value: Any, replace: bool) -> None:
933 """
934 If attr is an attribute of self and both self[attr] and value are
935 lists, concatenate the two sequences, setting the result to
936 self[attr]. If either self[attr] or value are non-sequences and
937 replace is True or self[attr] is None, replace self[attr] with value.
938 Otherwise, do nothing.
939 """
940 if self.get(attr) is not value:
941 if isinstance(self.get(attr), list) and \
942 isinstance(value, list):
943 self.append_attr_list(attr, value)
944 else:
945 self.replace_attr(attr, value, replace)
947 def copy_attr_consistent(
948 self, attr: str, value: Any, replace: bool) -> None:
949 """
950 If replace is True or self[attr] is None, replace self[attr] with
951 value. Otherwise, do nothing.
952 """
953 if self.get(attr) is not value:
954 self.replace_attr(attr, value, replace)
956 def update_all_atts(self,
957 dict_: Mapping[str, Any] | Element,
958 update_fun: _UpdateFun = copy_attr_consistent,
959 replace: bool = True,
960 and_source: bool = False,
961 ) -> None:
962 """
963 Updates all attributes from node or dictionary `dict_`.
965 Appends the basic attributes ('ids', 'names', 'classes',
966 'dupnames', but not 'source') and then, for all other attributes in
967 dict_, updates the same attribute in self. When attributes with the
968 same identifier appear in both self and dict_, the two values are
969 merged based on the value of update_fun. Generally, when replace is
970 True, the values in self are replaced or merged with the values in
971 dict_; otherwise, the values in self may be preserved or merged. When
972 and_source is True, the 'source' attribute is included in the copy.
974 NOTE: When replace is False, and self contains a 'source' attribute,
975 'source' is not replaced even when dict_ has a 'source'
976 attribute, though it may still be merged into a list depending
977 on the value of update_fun.
978 NOTE: It is easier to call the update-specific methods then to pass
979 the update_fun method to this function.
980 """
981 if isinstance(dict_, Node):
982 dict_ = dict_.attributes
984 # Include the source attribute when copying?
985 if and_source:
986 filter_fun = self.is_not_list_attribute
987 else:
988 filter_fun = self.is_not_known_attribute
990 # Copy the basic attributes
991 self.update_basic_atts(dict_)
993 # Grab other attributes in dict_ not in self except the
994 # (All basic attributes should be copied already)
995 for att in filter(filter_fun, dict_):
996 update_fun(self, att, dict_[att], replace)
998 def update_all_atts_consistantly(self,
999 dict_: Mapping[str, Any] | Element,
1000 replace: bool = True,
1001 and_source: bool = False,
1002 ) -> None:
1003 """
1004 Updates all attributes from node or dictionary `dict_`.
1006 Appends the basic attributes ('ids', 'names', 'classes',
1007 'dupnames', but not 'source') and then, for all other attributes in
1008 dict_, updates the same attribute in self. When attributes with the
1009 same identifier appear in both self and dict_ and replace is True, the
1010 values in self are replaced with the values in dict_; otherwise, the
1011 values in self are preserved. When and_source is True, the 'source'
1012 attribute is included in the copy.
1014 NOTE: When replace is False, and self contains a 'source' attribute,
1015 'source' is not replaced even when dict_ has a 'source'
1016 attribute, though it may still be merged into a list depending
1017 on the value of update_fun.
1018 """
1019 self.update_all_atts(dict_, Element.copy_attr_consistent, replace,
1020 and_source)
1022 def update_all_atts_concatenating(self,
1023 dict_: Mapping[str, Any] | Element,
1024 replace: bool = True,
1025 and_source: bool = False,
1026 ) -> None:
1027 """
1028 Updates all attributes from node or dictionary `dict_`.
1030 Appends the basic attributes ('ids', 'names', 'classes',
1031 'dupnames', but not 'source') and then, for all other attributes in
1032 dict_, updates the same attribute in self. When attributes with the
1033 same identifier appear in both self and dict_ whose values aren't each
1034 lists and replace is True, the values in self are replaced with the
1035 values in dict_; if the values from self and dict_ for the given
1036 identifier are both of list type, then the two lists are concatenated
1037 and the result stored in self; otherwise, the values in self are
1038 preserved. When and_source is True, the 'source' attribute is
1039 included in the copy.
1041 NOTE: When replace is False, and self contains a 'source' attribute,
1042 'source' is not replaced even when dict_ has a 'source'
1043 attribute, though it may still be merged into a list depending
1044 on the value of update_fun.
1045 """
1046 self.update_all_atts(dict_, Element.copy_attr_concatenate, replace,
1047 and_source)
1049 def update_all_atts_coercion(self,
1050 dict_: Mapping[str, Any] | Element,
1051 replace: bool = True,
1052 and_source: bool = False,
1053 ) -> None:
1054 """
1055 Updates all attributes from node or dictionary `dict_`.
1057 Appends the basic attributes ('ids', 'names', 'classes',
1058 'dupnames', but not 'source') and then, for all other attributes in
1059 dict_, updates the same attribute in self. When attributes with the
1060 same identifier appear in both self and dict_ whose values are both
1061 not lists and replace is True, the values in self are replaced with
1062 the values in dict_; if either of the values from self and dict_ for
1063 the given identifier are of list type, then first any non-lists are
1064 converted to 1-element lists and then the two lists are concatenated
1065 and the result stored in self; otherwise, the values in self are
1066 preserved. When and_source is True, the 'source' attribute is
1067 included in the copy.
1069 NOTE: When replace is False, and self contains a 'source' attribute,
1070 'source' is not replaced even when dict_ has a 'source'
1071 attribute, though it may still be merged into a list depending
1072 on the value of update_fun.
1073 """
1074 self.update_all_atts(dict_, Element.copy_attr_coerce, replace,
1075 and_source)
1077 def update_all_atts_convert(self,
1078 dict_: Mapping[str, Any] | Element,
1079 and_source: bool = False,
1080 ) -> None:
1081 """
1082 Updates all attributes from node or dictionary `dict_`.
1084 Appends the basic attributes ('ids', 'names', 'classes',
1085 'dupnames', but not 'source') and then, for all other attributes in
1086 dict_, updates the same attribute in self. When attributes with the
1087 same identifier appear in both self and dict_ then first any non-lists
1088 are converted to 1-element lists and then the two lists are
1089 concatenated and the result stored in self; otherwise, the values in
1090 self are preserved. When and_source is True, the 'source' attribute
1091 is included in the copy.
1093 NOTE: When replace is False, and self contains a 'source' attribute,
1094 'source' is not replaced even when dict_ has a 'source'
1095 attribute, though it may still be merged into a list depending
1096 on the value of update_fun.
1097 """
1098 self.update_all_atts(dict_, Element.copy_attr_convert,
1099 and_source=and_source)
1101 def clear(self) -> None:
1102 self.children = []
1104 def replace(self, old, new) -> None:
1105 """Replace one child `Node` with another child or children."""
1106 index = self.index(old)
1107 if isinstance(new, Node):
1108 self.setup_child(new)
1109 self[index] = new
1110 elif new is not None:
1111 self[index:index+1] = new
1113 def replace_self(self, new) -> None:
1114 """
1115 Replace `self` node with `new`, where `new` is a node or a
1116 list of nodes.
1118 Provisional: the handling of node attributes will be revised.
1119 """
1120 update = new
1121 if not isinstance(new, Node):
1122 # `new` is a list; update first child.
1123 try:
1124 update = new[0]
1125 except IndexError:
1126 update = None
1127 if isinstance(update, Element):
1128 update.update_basic_atts(self)
1129 else:
1130 # `update` is a Text node or `new` is an empty list.
1131 # Assert that we aren't losing any attributes.
1132 for att in self.basic_attributes:
1133 assert not self[att], \
1134 'Losing "%s" attribute: %s' % (att, self[att])
1135 self.parent.replace(self, new)
1137 def first_child_matching_class(self,
1138 childclass: type[Element] | type[Text]
1139 | tuple[type[Element] | type[Text], ...],
1140 start: int = 0,
1141 end: int = sys.maxsize,
1142 ) -> int | None:
1143 """
1144 Return the index of the first child whose class exactly matches.
1146 Parameters:
1148 - `childclass`: A `Node` subclass to search for, or a tuple of `Node`
1149 classes. If a tuple, any of the classes may match.
1150 - `start`: Initial index to check.
1151 - `end`: Initial index to *not* check.
1152 """
1153 if not isinstance(childclass, tuple):
1154 childclass = (childclass,)
1155 for index in range(start, min(len(self), end)):
1156 for c in childclass:
1157 if isinstance(self[index], c):
1158 return index
1159 return None
1161 def first_child_not_matching_class(
1162 self,
1163 childclass: type[Element] | type[Text]
1164 | tuple[type[Element] | type[Text], ...],
1165 start: int = 0,
1166 end: int = sys.maxsize,
1167 ) -> int | None:
1168 """
1169 Return the index of the first child whose class does *not* match.
1171 Parameters:
1173 - `childclass`: A `Node` subclass to skip, or a tuple of `Node`
1174 classes. If a tuple, none of the classes may match.
1175 - `start`: Initial index to check.
1176 - `end`: Initial index to *not* check.
1177 """
1178 if not isinstance(childclass, tuple):
1179 childclass = (childclass,)
1180 for index in range(start, min(len(self), end)):
1181 for c in childclass:
1182 if isinstance(self.children[index], c):
1183 break
1184 else:
1185 return index
1186 return None
1188 def pformat(self, indent: str = ' ', level: int = 0) -> str:
1189 tagline = '%s%s\n' % (indent*level, self.starttag())
1190 childreps = (c.pformat(indent, level+1) for c in self.children)
1191 return ''.join((tagline, *childreps))
1193 def copy(self) -> Self:
1194 obj = self.__class__(rawsource=self.rawsource, **self.attributes)
1195 obj._document = self._document
1196 obj.source = self.source
1197 obj.line = self.line
1198 return obj
1200 def deepcopy(self) -> Self:
1201 copy = self.copy()
1202 copy.extend([child.deepcopy() for child in self.children])
1203 return copy
1205 def note_referenced_by(self,
1206 name: str | None = None,
1207 id: str | None = None,
1208 ) -> None:
1209 """Note that this Element has been referenced by its name
1210 `name` or id `id`."""
1211 self.referenced = True
1212 # Element.expect_referenced_by_* dictionaries map names or ids
1213 # that were "propagated" to this element to the elements that
1214 # had these attributes before. Mark them as ``referenced``
1215 # when this node is referenced by the respective names or ids.
1216 if relay := getattr(self, 'expect_referenced_by_name', {}).get(name):
1217 relay.referenced = True
1218 if relay := getattr(self, 'expect_referenced_by_id', {}).get(id):
1219 relay.referenced = True
1221 @classmethod
1222 def is_not_list_attribute(cls, attr: str) -> bool:
1223 """
1224 Returns True if and only if the given attribute is NOT one of the
1225 basic list attributes defined for all Elements.
1226 """
1227 return attr not in cls.list_attributes
1229 @classmethod
1230 def is_not_known_attribute(cls, attr: str) -> bool:
1231 """
1232 Return True if `attr` is NOT defined for all Element instances.
1234 Provisional. May be removed in Docutils 2.0.
1235 """
1236 return attr not in cls.common_attributes
1238 def validate_attributes(self) -> None:
1239 """Normalize and validate element attributes.
1241 Convert string values to expected datatype.
1242 Normalize values.
1244 Raise `ValidationError` for invalid attributes or attribute values.
1246 Provisional.
1247 """
1248 messages = []
1249 for key, value in self.attributes.items():
1250 if key.startswith('internal:'):
1251 continue # see docs/user/config.html#expose-internals
1252 if key not in self.valid_attributes:
1253 va = '", "'.join(self.valid_attributes)
1254 messages.append(f'Attribute "{key}" not one of "{va}".')
1255 continue
1256 try:
1257 self.attributes[key] = ATTRIBUTE_VALIDATORS[key](value)
1258 except (ValueError, TypeError, KeyError) as e:
1259 messages.append(
1260 f'Attribute "{key}" has invalid value "{value}".\n {e}')
1261 if messages:
1262 raise ValidationError(f'Element {self.starttag()} invalid:\n '
1263 + '\n '.join(messages),
1264 problematic_element=self)
1266 def validate_content(self,
1267 model: _ContentModelTuple | None = None,
1268 elements: Sequence | None = None,
1269 ) -> list:
1270 """Test compliance of `elements` with `model`.
1272 :model: content model description, default `self.content_model`,
1273 :elements: list of doctree elements, default `self.children`.
1275 Return list of children that do not fit in the model or raise
1276 `ValidationError` if the content does not comply with the `model`.
1278 Provisional.
1279 """
1280 if model is None:
1281 model = self.content_model
1282 if elements is None:
1283 elements = self.children
1284 ichildren = iter(elements)
1285 child = next(ichildren, None)
1286 for category, quantifier in model:
1287 if not isinstance(child, category):
1288 if quantifier in ('.', '+'):
1289 raise ValidationError(self._report_child(child, category),
1290 problematic_element=child)
1291 else: # quantifier in ('?', '*') -> optional child
1292 continue # try same child with next part of content model
1293 else:
1294 # Check additional placement constraints (if applicable):
1295 child.validate_position()
1296 # advance:
1297 if quantifier in ('.', '?'): # go to next element
1298 child = next(ichildren, None)
1299 else: # if quantifier in ('*', '+'): # pass all matching elements
1300 for child in ichildren:
1301 if not isinstance(child, category):
1302 break
1303 try:
1304 child.validate_position()
1305 except AttributeError:
1306 pass
1307 else:
1308 child = None
1309 return [] if child is None else [child, *ichildren]
1311 def _report_child(self,
1312 child,
1313 category: Element | Iterable[Element],
1314 ) -> str:
1315 # Return a str reporting a missing child or child of wrong category.
1316 try:
1317 _type = category.__name__
1318 except AttributeError:
1319 _type = '> or <'.join(c.__name__ for c in category)
1320 msg = f'Element {self.starttag()} invalid:\n'
1321 if child is None:
1322 return f'{msg} Missing child of type <{_type}>.'
1323 if isinstance(child, Text):
1324 return (f'{msg} Expecting child of type <{_type}>, '
1325 f'not text data "{child.astext()}".')
1326 return (f'{msg} Expecting child of type <{_type}>, '
1327 f'not {child.starttag()}.')
1329 def validate(self, recursive: bool = True) -> None:
1330 """Validate Docutils Document Tree element ("doctree").
1332 Raise ValidationError if there are violations.
1333 If `recursive` is True, validate also the element's descendants.
1335 See `The Docutils Document Tree`__ for details of the
1336 Docutils Document Model.
1338 __ https://docutils.sourceforge.io/docs/ref/doctree.html
1340 Provisional (work in progress).
1341 """
1342 self.validate_attributes()
1344 leftover_childs = self.validate_content()
1345 for child in leftover_childs:
1346 if isinstance(child, Text):
1347 raise ValidationError(f'Element {self.starttag()} invalid:\n'
1348 f' Spurious text: "{child.astext()}".',
1349 problematic_element=self)
1350 else:
1351 raise ValidationError(f'Element {self.starttag()} invalid:\n'
1352 f' Child element {child.starttag()} '
1353 'not allowed at this position.',
1354 problematic_element=child)
1356 if recursive:
1357 for child in self:
1358 child.validate(recursive=recursive)
1361# ====================
1362# Element Categories
1363# ====================
1364#
1365# See https://docutils.sourceforge.io/docs/ref/doctree.html#element-hierarchy.
1367class Root:
1368 """Element at the root of a document tree."""
1371class Structural:
1372 """`Structural elements`__.
1374 __ https://docutils.sourceforge.io/docs/ref/doctree.html
1375 #structural-elements
1376 """
1379class SubStructural:
1380 """`Structural subelements`__ are children of `Structural` elements.
1382 Most Structural elements accept only specific `SubStructural` elements.
1384 __ https://docutils.sourceforge.io/docs/ref/doctree.html
1385 #structural-subelements
1386 """
1389class Bibliographic:
1390 """`Bibliographic Elements`__ (displayed document meta-data).
1392 __ https://docutils.sourceforge.io/docs/ref/doctree.html
1393 #bibliographic-elements
1394 """
1397class Body:
1398 """`Body elements`__.
1400 __ https://docutils.sourceforge.io/docs/ref/doctree.html#body-elements
1401 """
1404class Admonition(Body):
1405 """Admonitions (distinctive and self-contained notices)."""
1406 content_model: Final = ((Body, '+'),) # (%body.elements;)+
1409class Sequential(Body):
1410 """List-like body elements."""
1413class General(Body):
1414 """Miscellaneous body elements."""
1417class Special(Body):
1418 """Special internal body elements."""
1421class Part:
1422 """`Body Subelements`__ always occur within specific parent elements.
1424 __ https://docutils.sourceforge.io/docs/ref/doctree.html#body-subelements
1425 """
1428class Decorative:
1429 """Decorative elements (`header` and `footer`).
1431 Children of `decoration`.
1432 """
1433 content_model: Final = ((Body, '+'),) # (%body.elements;)+
1436class Inline:
1437 """Inline elements contain text data and possibly other inline elements.
1438 """
1441# Orthogonal categories and Mixins
1442# ================================
1444class PreBibliographic:
1445 """Elements which may occur before Bibliographic Elements."""
1448class Invisible(Special, PreBibliographic):
1449 """Internal elements that don't appear in output."""
1452class Labeled:
1453 """Contains a `label` as its first element."""
1456class Resolvable:
1457 resolved: bool = False
1460class BackLinkable:
1461 """Mixin for Elements that accept a "backrefs" attribute."""
1463 list_attributes: Final = Element.list_attributes + ('backrefs',)
1464 valid_attributes: Final = Element.valid_attributes + ('backrefs',)
1466 def add_backref(self: Element, refid: str) -> None:
1467 self['backrefs'].append(refid)
1470class Referential(Resolvable):
1471 """Elements holding a cross-reference (outgoing hyperlink)."""
1474class Targetable(Resolvable):
1475 """Cross-reference targets (incoming hyperlink)."""
1476 referenced: int = 0
1479class Titular:
1480 """Title, sub-title, or informal heading (rubric)."""
1483class TextElement(Element):
1484 """
1485 An element which directly contains text.
1487 Its children are all `Text` or `Inline` subclass nodes. You can
1488 check whether an element's context is inline simply by checking whether
1489 its immediate parent is a `TextElement` instance (including subclasses).
1490 This is handy for nodes like `image` that can appear both inline and as
1491 standalone body elements.
1493 If passing children to `__init__()`, make sure to set `text` to
1494 ``''`` or some other suitable value.
1495 """
1496 content_model: Final = (((Text, Inline), '*'),)
1497 # (#PCDATA | %inline.elements;)*
1499 child_text_separator: Final = ''
1500 """Separator for child nodes, used by `astext()` method."""
1502 def __init__(self,
1503 rawsource: str = '',
1504 text: str = '',
1505 *children,
1506 **attributes: Any,
1507 ) -> None:
1508 if text:
1509 textnode = Text(text)
1510 Element.__init__(self, rawsource, textnode, *children,
1511 **attributes)
1512 else:
1513 Element.__init__(self, rawsource, *children, **attributes)
1516class FixedTextElement(TextElement):
1517 """An element which directly contains preformatted text."""
1519 valid_attributes: Final = Element.valid_attributes + ('xml:space',)
1521 def __init__(self,
1522 rawsource: str = '',
1523 text: str = '',
1524 *children,
1525 **attributes: Any,
1526 ) -> None:
1527 super().__init__(rawsource, text, *children, **attributes)
1528 self.attributes['xml:space'] = 'preserve'
1531class PureTextElement(TextElement):
1532 """An element which only contains text, no children."""
1533 content_model: Final = ((Text, '?'),) # (#PCDATA)
1536# =================================
1537# Concrete Document Tree Elements
1538# =================================
1539#
1540# See https://docutils.sourceforge.io/docs/ref/doctree.html#element-reference
1542# Special purpose elements
1543# ========================
1544#
1545# Body elements for internal use or special requests.
1547class comment(Invisible, FixedTextElement, PureTextElement):
1548 """Author notes, hidden from the output."""
1551class substitution_definition(Invisible, TextElement):
1552 valid_attributes: Final = Element.valid_attributes + ('ltrim', 'rtrim')
1555class target(Invisible, Inline, TextElement, Targetable):
1556 valid_attributes: Final = Element.valid_attributes + (
1557 'anonymous', 'refid', 'refname', 'refuri')
1560class system_message(Special, BackLinkable, PreBibliographic, Element):
1561 """
1562 System message element.
1564 Do not instantiate this class directly; use
1565 ``document.reporter.info/warning/error/severe()`` instead.
1566 """
1567 valid_attributes: Final = BackLinkable.valid_attributes + (
1568 'level', 'line', 'type')
1569 content_model: Final = ((Body, '+'),) # (%body.elements;)+
1571 def __init__(self,
1572 message: str | None = None,
1573 *children,
1574 **attributes: Any,
1575 ) -> None:
1576 rawsource = attributes.pop('rawsource', '')
1577 if message:
1578 p = paragraph('', message)
1579 children = (p,) + children
1580 try:
1581 Element.__init__(self, rawsource, *children, **attributes)
1582 except: # NoQA: E722 (catchall)
1583 print('system_message: children=%r' % (children,))
1584 raise
1586 def astext(self) -> str:
1587 line = self.get('line', '')
1588 return '%s:%s: (%s/%s) %s' % (self['source'], line, self['type'],
1589 self['level'], Element.astext(self))
1592class pending(Invisible, Element):
1593 """
1594 Placeholder for pending operations.
1596 The "pending" element is used to encapsulate a pending operation: the
1597 operation (transform), the point at which to apply it, and any data it
1598 requires. Only the pending operation's location within the document is
1599 stored in the public document tree (by the "pending" object itself); the
1600 operation and its data are stored in the "pending" object's internal
1601 instance attributes.
1603 For example, say you want a table of contents in your reStructuredText
1604 document. The easiest way to specify where to put it is from within the
1605 document, with a directive::
1607 .. contents::
1609 But the "contents" directive can't do its work until the entire document
1610 has been parsed and possibly transformed to some extent. So the directive
1611 code leaves a placeholder behind that will trigger the second phase of its
1612 processing, something like this::
1614 <pending ...public attributes...> + internal attributes
1616 Use `document.note_pending()` so that the
1617 `docutils.transforms.Transformer` stage of processing can run all pending
1618 transforms.
1619 """
1621 def __init__(self,
1622 transform: Transform,
1623 details: Mapping[str, Any] | None = None,
1624 rawsource: str = '',
1625 *children,
1626 **attributes: Any,
1627 ) -> None:
1628 Element.__init__(self, rawsource, *children, **attributes)
1630 self.transform: Transform = transform
1631 """The `docutils.transforms.Transform` class implementing the pending
1632 operation."""
1634 self.details: Mapping[str, Any] = details or {}
1635 """Detail data (dictionary) required by the pending operation."""
1637 def pformat(self, indent: str = ' ', level: int = 0) -> str:
1638 internals = ['.. internal attributes:',
1639 ' .transform: %s.%s' % (self.transform.__module__,
1640 self.transform.__name__),
1641 ' .details:']
1642 details = sorted(self.details.items())
1643 for key, value in details:
1644 if isinstance(value, Node):
1645 internals.append('%7s%s:' % ('', key))
1646 internals.extend(['%9s%s' % ('', line)
1647 for line in value.pformat().splitlines()])
1648 elif (value
1649 and isinstance(value, list)
1650 and isinstance(value[0], Node)):
1651 internals.append('%7s%s:' % ('', key))
1652 for v in value:
1653 internals.extend(['%9s%s' % ('', line)
1654 for line in v.pformat().splitlines()])
1655 else:
1656 internals.append('%7s%s: %r' % ('', key, value))
1657 return (Element.pformat(self, indent, level)
1658 + ''.join((' %s%s\n' % (indent * level, line))
1659 for line in internals))
1661 def copy(self) -> Self:
1662 obj = self.__class__(self.transform, self.details, self.rawsource,
1663 **self.attributes)
1664 obj._document = self._document
1665 obj.source = self.source
1666 obj.line = self.line
1667 return obj
1670class raw(Special, Inline, PreBibliographic,
1671 FixedTextElement, PureTextElement):
1672 """Raw data that is to be passed untouched to the Writer.
1674 Can be used as Body element or Inline element.
1675 """
1676 valid_attributes: Final = Element.valid_attributes + (
1677 'format', 'xml:space')
1680# Decorative Elements
1681# ===================
1683class header(Decorative, Element): pass
1684class footer(Decorative, Element): pass
1687# Structural Subelements
1688# ======================
1690class title(Titular, PreBibliographic, SubStructural, TextElement):
1691 """Title of `document`, `section`, `topic` and generic `admonition`.
1692 """
1693 valid_attributes: Final = Element.valid_attributes + ('auto', 'refid')
1696class subtitle(Titular, PreBibliographic, SubStructural, TextElement):
1697 """Sub-title of `document`, `section` and `sidebar`."""
1699 def validate_position(self) -> None:
1700 """Check position of subtitle: must follow a title."""
1701 if self.parent and self.parent.index(self) == 0:
1702 raise ValidationError(f'Element {self.parent.starttag()} invalid:'
1703 '\n <subtitle> only allowed after <title>.',
1704 problematic_element=self)
1707class meta(PreBibliographic, SubStructural, Element):
1708 """Container for "invisible" bibliographic data, or meta-data."""
1709 valid_attributes: Final = Element.valid_attributes + (
1710 'content', 'dir', 'http-equiv', 'lang', 'media', 'name', 'scheme')
1713class docinfo(SubStructural, Element):
1714 """Container for displayed document meta-data."""
1715 content_model: Final = ((Bibliographic, '+'),)
1716 # (%bibliographic.elements;)+
1719class decoration(PreBibliographic, SubStructural, Element):
1720 """Container for `header` and `footer`."""
1721 content_model: Final = ((header, '?'), # Empty element doesn't make sense,
1722 (footer, '?'), # but is simpler to define.
1723 )
1724 # (header?, footer?)
1726 def get_header(self) -> header:
1727 if not len(self.children) or not isinstance(self.children[0], header):
1728 self.insert(0, header())
1729 return self.children[0]
1731 def get_footer(self) -> footer:
1732 if not len(self.children) or not isinstance(self.children[-1], footer):
1733 self.append(footer())
1734 return self.children[-1]
1737class transition(SubStructural, Element):
1738 """Transitions__ represent "semantic breaks".
1740 __ https://docutils.sourceforge.io/docs/ref/doctree.html#transition
1741 """
1742 # Sibling nodes that are ignored when validating a transition's position
1743 # (titles plus moving and invisible elements except comments):
1744 ignored_siblings = (decoration, meta, pending, substitution_definition,
1745 subtitle, target, title)
1747 def validate_position(self) -> None:
1748 """Check additional constraints on `transition` placement.
1750 A transition may not begin or end section or document text,
1751 nor may two transitions be immediately adjacent.
1752 """
1753 messages = [f'Element {self.parent.starttag()} invalid:']
1754 if isinstance(self.previous_sibling(), transition):
1755 messages.append(
1756 '<transition> may not directly follow another transition.')
1757 i = self.parent.index(self)
1758 prev_siblings = self.parent[:i]
1759 if not [sibling for sibling in prev_siblings
1760 if not isinstance(sibling, self.ignored_siblings)]:
1761 messages.append(
1762 '<transition> may not begin a section or document.')
1763 next_siblings = self.parent[i+1:]
1764 if not [sibling for sibling in next_siblings
1765 if not isinstance(sibling, self.ignored_siblings)]:
1766 messages.append('<transition> may not end a section or document.')
1767 if len(messages) > 1:
1768 raise ValidationError('\n '.join(messages),
1769 problematic_element=self)
1772# Structural Elements
1773# ===================
1775class topic(Structural, Element):
1776 """
1777 Topics__ are non-recursive, mini-sections.
1779 __ https://docutils.sourceforge.io/docs/ref/doctree.html#topic
1780 """
1781 content_model: Final = ((title, '?'), (Body, '+'))
1782 # (title?, (%body.elements;)+)
1785class sidebar(Structural, Element):
1786 """
1787 Sidebars__ are like parallel documents providing related material.
1789 A sidebar is typically offset by a border and "floats" to the side
1790 of the page
1792 __ https://docutils.sourceforge.io/docs/ref/doctree.html#sidebar
1793 """
1794 content_model: Final = ((title, '?'),
1795 (subtitle, '?'),
1796 ((topic, Body), '+'),
1797 )
1798 # ((title, subtitle?)?, (%body.elements; | topic)+)
1799 # "subtitle only after title" is ensured in `subtitle.validate_position()`.
1802class section(Structural, Element):
1803 """Document section__. The main unit of hierarchy.
1805 __ https://docutils.sourceforge.io/docs/ref/doctree.html#section
1806 """
1807 # recursive content model, see below
1810section.content_model = ((title, '.'),
1811 (subtitle, '?'),
1812 ((Body, topic, sidebar, transition), '*'),
1813 ((section, transition), '*'),
1814 )
1815# (title, subtitle?, %structure.model;)
1816# Correct transition placement is ensured in `transition.validate_position()`.
1819# Root Element
1820# ============
1822class document(Root, Element):
1823 """
1824 The document root element.
1826 Do not instantiate this class directly; use
1827 `docutils.utils.new_document()` instead.
1828 """
1829 valid_attributes: Final = Element.valid_attributes + ('title',)
1830 content_model: Final = ((title, '?'),
1831 (subtitle, '?'),
1832 (meta, '*'),
1833 (decoration, '?'),
1834 (docinfo, '?'),
1835 (transition, '?'),
1836 ((Body, topic, sidebar, transition), '*'),
1837 ((section, transition), '*'),
1838 )
1839 # ( (title, subtitle?)?,
1840 # meta*,
1841 # decoration?,
1842 # (docinfo, transition?)?,
1843 # %structure.model; )
1844 # Additional restrictions for `subtitle` and `transition` are tested
1845 # with the respective `validate_position()` methods.
1847 def __init__(self,
1848 settings: Values,
1849 reporter: Reporter,
1850 *args,
1851 **kwargs: Any,
1852 ) -> None:
1853 Element.__init__(self, *args, **kwargs)
1855 self.current_source: StrPath | None = None
1856 """Path to or description of the input source being processed."""
1858 self.current_line: int | None = None
1859 """Line number (1-based) of `current_source`."""
1861 self.settings: Values = settings
1862 """Runtime settings data record."""
1864 self.reporter: Reporter = reporter
1865 """System message generator."""
1867 self.indirect_targets: list[target] = []
1868 """List of indirect target nodes."""
1870 self.substitution_defs: dict[str, substitution_definition] = {}
1871 """Mapping of substitution names to substitution_definition nodes."""
1873 self.substitution_names: dict[str, str] = {}
1874 """Mapping of case-normalized to case-sensitive substitution names."""
1876 self.refnames: dict[str, list[Element]] = {}
1877 """Mapping of names to lists of referencing nodes."""
1879 self.refids: dict[str, list[Element]] = {}
1880 """(Incomplete) Mapping of ids to lists of referencing nodes."""
1882 self.names: dict[str, Element|None] = {}
1883 """Mapping of names to nodes (or ``None`` if name is a duplicate)."""
1885 self.ids: dict[str, Element] = {}
1886 """Mapping of ids to nodes."""
1888 self.nameids: dict[str, str] = {}
1889 """Mapping of names to unique id's."""
1891 self.nametypes: dict[str, bool] = {}
1892 """Mapping of names to hyperlink type. True: explicit, False: implicit.
1893 """
1895 self.footnote_refs: dict[str, list[footnote_reference]] = {}
1896 """Mapping of footnote labels to lists of footnote_reference nodes."""
1898 self.citation_refs: dict[str, list[citation_reference]] = {}
1899 """Mapping of citation labels to lists of citation_reference nodes."""
1901 self.autofootnotes: list[footnote] = []
1902 """List of auto-numbered footnote nodes."""
1904 self.autofootnote_refs: list[footnote_reference] = []
1905 """List of auto-numbered footnote_reference nodes."""
1907 self.symbol_footnotes: list[footnote] = []
1908 """List of symbol footnote nodes."""
1910 self.symbol_footnote_refs: list[footnote_reference] = []
1911 """List of symbol footnote_reference nodes."""
1913 self.footnotes: list[footnote] = []
1914 """List of manually-numbered footnote nodes."""
1916 self.citations: list[citation] = []
1917 """List of citation nodes."""
1919 self.autofootnote_start: int = 1
1920 """Initial auto-numbered footnote number."""
1922 self.symbol_footnote_start: int = 0
1923 """Initial symbol footnote symbol index."""
1925 self.id_counter: Counter[int] = Counter()
1926 """Numbers added to otherwise identical IDs."""
1928 self.parse_messages: list[system_message] = []
1929 """System messages generated while parsing."""
1931 self.transform_messages: list[system_message] = []
1932 """System messages generated while applying transforms."""
1934 import docutils.transforms
1935 self.transformer: Transformer = docutils.transforms.Transformer(self)
1936 """Storage for transforms to be applied to this document."""
1938 self.include_log: list[tuple[StrPath, tuple]] = []
1939 """The current source's parents (to detect inclusion loops)."""
1941 self.decoration: decoration | None = None
1942 """Document's `decoration` node."""
1944 self._document: document = self
1946 def __getstate__(self) -> dict[str, Any]:
1947 """
1948 Return dict with unpicklable references removed.
1949 """
1950 state = self.__dict__.copy()
1951 state['reporter'] = None
1952 state['transformer'] = None
1953 return state
1955 def asdom(self, dom: ModuleType | None = None) -> minidom.Document:
1956 """Return a DOM representation of this document."""
1957 if dom is None:
1958 import xml.dom.minidom as dom
1959 domroot = dom.Document()
1960 domroot.appendChild(self._dom_node(domroot))
1961 return domroot
1963 def set_id(self,
1964 node: Element,
1965 msgnode: Element | None = None,
1966 suggested_prefix: str = '',
1967 ) -> str:
1968 """
1969 Check/set identifiers of element `node`. Return last identifier.
1971 Check `node`s identifiers for duplicates,
1972 create a new identifier if there are no identifiers.
1973 Update `document.ids` and `document.nameids`.
1975 Provisional.
1976 """
1977 if not node['ids']:
1978 node['ids'].append(self.create_id(node, suggested_prefix))
1979 # register and check for duplicates
1980 for id in node['ids']:
1981 self.ids.setdefault(id, node)
1982 if self.ids[id] is not node:
1983 msg = self.reporter.error(f'Duplicate ID: "{id}" used by '
1984 f'{self.ids[id].starttag()} '
1985 f'and {node.starttag()}',
1986 base_node=node)
1987 if msgnode is not None:
1988 msgnode += msg
1989 for name in node['names']:
1990 self.nameids[name] = id
1991 return id
1993 def create_id(self, node: Element, suggested_prefix: str = '') -> str:
1994 # Internal auxiliary method for set_id():
1995 # generate and return a suitable identifier for `node`.
1996 # See also make_id()
1997 id_prefix = self.settings.id_prefix
1998 auto_id_prefix = self.settings.auto_id_prefix
1999 base_id = ''
2000 id = ''
2001 for name in node['names']:
2002 if id_prefix: # allow names starting with numbers
2003 base_id = make_id('x'+name)[1:]
2004 else:
2005 base_id = make_id(name)
2006 # TODO: normalize id-prefix? (would make code simpler)
2007 id = id_prefix + base_id
2008 if base_id and id not in self.ids:
2009 break
2010 else:
2011 if base_id and auto_id_prefix.endswith('%'):
2012 # disambiguate name-derived ID
2013 # TODO: remove second condition after announcing change
2014 prefix = id + '-'
2015 elif (node['dupnames'] and auto_id_prefix.endswith('%')
2016 and make_id(node['dupnames'][0])):
2017 prefix = make_id(node['dupnames'][0]) + '-'
2018 else:
2019 prefix = id_prefix + auto_id_prefix
2020 if prefix.endswith('%'):
2021 prefix = f"""{prefix[:-1]}{suggested_prefix
2022 or make_id(node.tagname)}-"""
2023 while True:
2024 self.id_counter[prefix] += 1
2025 id = f'{prefix}{self.id_counter[prefix]}'
2026 if id not in self.ids:
2027 break
2028 return id
2030 def set_name_id_map(self,
2031 node: Element,
2032 id: str,
2033 msgnode: Element | None = None,
2034 explicit: bool = False,
2035 ) -> None:
2036 """Deprecated. Will be removed in Docutils 2.0."""
2037 warnings.warn('nodes.document.set_name_id_map() will be removed'
2038 ' in Docutils 2.0.', DeprecationWarning, stacklevel=2)
2039 self.note_names(node, msgnode, explicit)
2040 for name in node['names']:
2041 self.nameids[name] = id
2043 def set_duplicate_name(self,
2044 node: Element,
2045 name: str,
2046 msgnode: Element,
2047 explicit: bool,
2048 ) -> None:
2049 """
2050 Handle name conflicts according to the `rST specification`__.
2052 Called by `self.note_names()` when the reference name `name`
2053 of the element `node` is already registered in `self.names`.
2055 `self.names` maps names to elements. The value ``None`` indicates
2056 that the name is a "dupname" (i.e. the document contains two or
2057 more elements with the same name and target type).
2059 `self.nametypes` maps names to booleans representing the target type
2060 (True = "explicit", False = "implicit").
2062 The following state transition table shows how the values
2063 of `self.names` ("name") and `self.nametypes` ("type") items
2064 with key `name` change and which actions are performed.
2066 "Old" is the element with conflicting reference name,
2067 "new" is the element specified by the argument `node`.
2068 The "Input type" is specified by the argument `explicit`.
2070 ======== ==== ======== ==== ======== =============== =======
2071 Input Old State New State Action
2072 -------- -------------- -------------- ------------------------
2073 type name type name type invalidate [#]_ report
2074 ======== ==== ======== ==== ======== =============== =======
2075 explicit old explicit None explicit new,old [#ex]_ WARNING
2076 implicit old explicit old explicit new INFO
2077 explicit old implicit new explicit old INFO
2078 implicit old implicit None implicit new,old [#ex]_ INFO
2079 explicit None explicit None explicit new WARNING
2080 implicit None explicit None explicit new INFO
2081 explicit None implicit new explicit
2082 implicit None implicit None implicit new INFO
2083 ======== ==== ======== ==== ======== =============== =======
2085 .. [#] When "invalidating" an element, `name` is transferred from
2086 the element's "name" attribute to its "dupnames" attribute.
2088 .. [#ex] If both "old" and "new" refer to identical URIs or
2089 reference names, keep the old state and only invalidate "new".
2091 __ https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html
2092 #implicit-hyperlink-targets
2094 Provisional.
2095 """
2096 old_node = self.names[name] # None if name is only dupname
2097 old_explicit = self.nametypes[name]
2098 level = 0 # system message level: 1-info, 2-warning
2100 self.nametypes[name] = old_explicit or explicit
2102 if old_node is not None and (
2103 'refname' in node and node['refname'] == old_node.get('refname')
2104 or 'refuri' in node and node['refuri'] == old_node.get('refuri')
2105 ):
2106 # indirect targets with same reference -> keep old target
2107 level = 1
2108 ref = node.get('refuri') or node.get('refname')
2109 s = f'Duplicate name "{name}" for external target "{ref}".'
2110 dupname(node, name)
2111 elif explicit:
2112 if old_explicit:
2113 level = 2
2114 s = f'Duplicate explicit target name: "{name}".'
2115 dupname(node, name)
2116 if old_node is not None:
2117 dupname(old_node, name)
2118 self.names[name] = None
2119 self.nameids[name] = None
2120 else: # new explicit, old implicit -> override
2121 self.names[name] = node
2122 if old_node is not None:
2123 level = 1
2124 s = f'Target name overrides implicit target name "{name}".'
2125 dupname(old_node, name)
2126 else: # new name is implicit
2127 level = 1
2128 s = f'Duplicate implicit target name: "{name}".'
2129 dupname(node, name)
2130 if old_node is not None and not old_explicit:
2131 dupname(old_node, name)
2132 self.names[name] = None
2133 self.nameids[name] = None
2134 self.set_id(old_node) # set id to get running numbers right
2135 if level:
2136 # don't add backref id for empty targets (not shown in output)
2137 if isinstance(node, target) and not node.children:
2138 backrefs = []
2139 else:
2140 backrefs = [self.set_id(node)]
2141 msg = self.reporter.system_message(level, s, backrefs=backrefs,
2142 base_node=node)
2143 # try appending near to the problem:
2144 if msgnode is not None and 'Body' in repr(msgnode.content_model):
2145 msgnode += msg
2147 def note_names(self,
2148 node: Element,
2149 msgnode: Element|None = None,
2150 explicit: bool = False,
2151 ) -> None:
2152 """
2153 Register the reference names of the element `node`.
2155 Update `self.names` and `self.nametypes`
2156 for each name in the "names" attribute of `node`.
2157 In case of name conflicts, call `self.set_duplicate_name()`.
2158 """
2159 for name in tuple(node['names']):
2160 if name in self.names and self.names[name] != node:
2161 self.set_duplicate_name(node, name, msgnode, explicit)
2162 # attention: modifies node['names']
2163 else:
2164 self.names[name] = node
2165 self.nametypes.setdefault(name, explicit)
2167 def has_name(self, name: str) -> bool:
2168 # TODO: deprecate in Docutils 2.0 (use ``name in document.names``)
2169 return name in self.names
2171 # "note" here is an imperative verb: "take note of".
2172 def note_implicit_target(self, target: Element,
2173 msgnode: Element|None = None) -> None:
2174 self.note_names(target, msgnode, explicit=False)
2175 if getattr(self.settings, "legacy_ids", True):
2176 self.set_id(target, msgnode)
2178 def note_explicit_target(self, target: Element,
2179 msgnode: Element|None = None) -> None:
2180 self.note_names(target, msgnode, explicit=True)
2181 if getattr(self.settings, "legacy_ids", True):
2182 self.set_id(target, msgnode)
2184 def note_refname(self, node: Element) -> None:
2185 self.refnames.setdefault(node['refname'], []).append(node)
2187 def note_refid(self, node: Element) -> None:
2188 self.refids.setdefault(node['refid'], []).append(node)
2190 def note_indirect_target(self, target: target) -> None:
2191 self.indirect_targets.append(target)
2192 if target['names']:
2193 self.note_refname(target)
2195 def note_anonymous_target(self, target: target) -> None:
2196 if getattr(self.settings, "legacy_ids", True):
2197 self.set_id(target)
2199 def note_autofootnote(self, footnote: footnote) -> None:
2200 self.set_id(footnote)
2201 self.autofootnotes.append(footnote)
2203 def note_autofootnote_ref(self, ref: footnote_reference) -> None:
2204 self.set_id(ref)
2205 self.autofootnote_refs.append(ref)
2207 def note_symbol_footnote(self, footnote: footnote) -> None:
2208 self.set_id(footnote)
2209 self.symbol_footnotes.append(footnote)
2211 def note_symbol_footnote_ref(self, ref: footnote_reference) -> None:
2212 self.set_id(ref)
2213 self.symbol_footnote_refs.append(ref)
2215 def note_footnote(self, footnote: footnote) -> None:
2216 self.set_id(footnote)
2217 self.footnotes.append(footnote)
2219 def note_footnote_ref(self, ref: footnote_reference) -> None:
2220 self.set_id(ref)
2221 self.footnote_refs.setdefault(ref['refname'], []).append(ref)
2222 self.note_refname(ref)
2224 def note_citation(self, citation: citation) -> None:
2225 self.citations.append(citation)
2227 def note_citation_ref(self, ref: citation_reference) -> None:
2228 self.set_id(ref)
2229 self.citation_refs.setdefault(ref['refname'], []).append(ref)
2230 self.note_refname(ref)
2232 def note_substitution_def(self,
2233 subdef: substitution_definition,
2234 def_name: str,
2235 msgnode: Element | None = None,
2236 ) -> None:
2237 name = whitespace_normalize_name(def_name)
2238 if name in self.substitution_defs:
2239 msg = self.reporter.error(
2240 'Duplicate substitution definition name: "%s".' % name,
2241 base_node=subdef)
2242 if msgnode is not None:
2243 msgnode += msg
2244 oldnode = self.substitution_defs[name]
2245 dupname(oldnode, name)
2246 # keep only the last definition:
2247 self.substitution_defs[name] = subdef
2248 # case-insensitive mapping:
2249 self.substitution_names[fully_normalize_name(name)] = name
2251 def note_substitution_ref(self,
2252 subref: substitution_reference,
2253 refname: str,
2254 ) -> None:
2255 subref['refname'] = whitespace_normalize_name(refname)
2257 def note_pending(
2258 self, pending: pending, priority: int | None = None) -> None:
2259 self.transformer.add_pending(pending, priority)
2261 def note_parse_message(self, message: system_message) -> None:
2262 self.parse_messages.append(message)
2264 def note_transform_message(self, message: system_message) -> None:
2265 self.transform_messages.append(message)
2267 def note_source(self,
2268 source: StrPath | None,
2269 offset: int | None,
2270 ) -> None:
2271 self.current_source = source and os.fspath(source)
2272 if offset is None:
2273 self.current_line = offset
2274 else:
2275 self.current_line = offset + 1
2277 def copy(self) -> Self:
2278 obj = self.__class__(self.settings, self.reporter,
2279 **self.attributes)
2280 obj.source = self.source
2281 obj.line = self.line
2282 return obj
2284 def get_decoration(self) -> decoration:
2285 if not self.decoration:
2286 self.decoration: decoration = decoration()
2287 index = self.first_child_not_matching_class((Titular, meta))
2288 if index is None:
2289 self.append(self.decoration)
2290 else:
2291 self.insert(index, self.decoration)
2292 return self.decoration
2295# Bibliographic Elements
2296# ======================
2298class author(Bibliographic, TextElement): pass
2299class organization(Bibliographic, TextElement): pass
2300class address(Bibliographic, FixedTextElement): pass
2301class contact(Bibliographic, TextElement): pass
2302class version(Bibliographic, TextElement): pass
2303class revision(Bibliographic, TextElement): pass
2304class status(Bibliographic, TextElement): pass
2305class date(Bibliographic, TextElement): pass
2306class copyright(Bibliographic, TextElement): pass # NoQA: A001 (builtin name)
2309class authors(Bibliographic, Element):
2310 """Container for author information for documents with multiple authors.
2311 """
2312 content_model: Final = ((author, '+'),
2313 (organization, '?'),
2314 (address, '?'),
2315 (contact, '?'),
2316 )
2317 # (author, organization?, address?, contact?)+
2319 def validate_content(self,
2320 model: _ContentModelTuple | None = None,
2321 elements: Sequence | None = None,
2322 ) -> list:
2323 """Repeatedly test for children matching the content model.
2325 Provisional.
2326 """
2327 relics = super().validate_content()
2328 while relics:
2329 relics = super().validate_content(elements=relics)
2330 return relics
2333# Body Elements
2334# =============
2335#
2336# General
2337# -------
2338#
2339# Miscellaneous Body Elements and related Body Subelements (Part)
2341class paragraph(General, TextElement): pass
2342class rubric(Titular, General, TextElement): pass
2345class compound(General, Element):
2346 content_model: Final = ((Body, '+'),) # (%body.elements;)+
2349class container(General, Element):
2350 content_model: Final = ((Body, '+'),) # (%body.elements;)+
2353class attribution(Part, TextElement):
2354 """Visible reference to the source of a `block_quote`."""
2357class block_quote(General, Element):
2358 """An extended quotation, set off from the main text."""
2359 content_model: Final = ((Body, '+'), (attribution, '?'))
2360 # ((%body.elements;)+, attribution?)
2363class reference(General, Inline, Referential, TextElement):
2364 valid_attributes: Final = Element.valid_attributes + (
2365 'anonymous', 'refid', 'refname', 'refuri')
2368# Lists
2369# -----
2370#
2371# Lists (Sequential) and related Body Subelements (Part)
2373class list_item(Part, Element):
2374 content_model: Final = ((Body, '*'),) # (%body.elements;)*
2377class bullet_list(Sequential, Element):
2378 valid_attributes: Final = Element.valid_attributes + ('bullet',)
2379 content_model: Final = ((list_item, '+'),) # (list_item+)
2382class enumerated_list(Sequential, Element):
2383 valid_attributes: Final = Element.valid_attributes + (
2384 'enumtype', 'prefix', 'suffix', 'start')
2385 content_model: Final = ((list_item, '+'),) # (list_item+)
2388class term(Part, TextElement): pass
2389class classifier(Part, TextElement): pass
2392class definition(Part, Element):
2393 """Definition of a `term` in a `definition_list`."""
2394 content_model: Final = ((Body, '+'),) # (%body.elements;)+
2397class definition_list_item(Part, Element):
2398 content_model: Final = ((term, '.'),
2399 ((classifier, term), '*'),
2400 (definition, '.'),
2401 )
2402 # ((term, classifier*)+, definition)
2405class definition_list(Sequential, Element):
2406 """List of terms and their definitions.
2408 Can be used for glossaries or dictionaries, to describe or
2409 classify things, for dialogues, or to itemize subtopics.
2410 """
2411 content_model: Final = ((definition_list_item, '+'),)
2412 # (definition_list_item+)
2415class field_name(Part, TextElement): pass
2418class field_body(Part, Element):
2419 content_model: Final = ((Body, '*'),) # (%body.elements;)*
2422class field(Part, Bibliographic, Element):
2423 content_model: Final = ((field_name, '.'), (field_body, '.'))
2424 # (field_name, field_body)
2427class field_list(Sequential, Element):
2428 """List of label & data pairs.
2430 Typically rendered as a two-column list.
2431 Also used for extension syntax or special processing.
2432 """
2433 content_model: Final = ((field, '+'),) # (field+)
2436class option_string(Part, PureTextElement):
2437 """A literal command-line option. Typically monospaced."""
2440class option_argument(Part, PureTextElement):
2441 """Placeholder text for option arguments."""
2442 valid_attributes: Final = Element.valid_attributes + ('delimiter',)
2444 def astext(self) -> str:
2445 return self.get('delimiter', ' ') + TextElement.astext(self)
2448class option(Part, Element):
2449 """Option element in an `option_list_item`.
2451 Groups an option string with zero or more option argument placeholders.
2452 """
2453 child_text_separator: Final = ''
2454 content_model: Final = ((option_string, '.'), (option_argument, '*'))
2455 # (option_string, option_argument*)
2458class option_group(Part, Element):
2459 """Groups together one or more `option` elements, all synonyms."""
2460 child_text_separator: Final = ', '
2461 content_model: Final = ((option, '+'),) # (option+)
2464class description(Part, Element):
2465 """Describtion of a command-line option."""
2466 content_model: Final = ((Body, '+'),) # (%body.elements;)+
2469class option_list_item(Part, Element):
2470 """Container for a pair of `option_group` and `description` elements.
2471 """
2472 child_text_separator: Final = ' '
2473 content_model: Final = ((option_group, '.'), (description, '.'))
2474 # (option_group, description)
2477class option_list(Sequential, Element):
2478 """Two-column list of command-line options and descriptions."""
2479 content_model: Final = ((option_list_item, '+'),) # (option_list_item+)
2482# Pre-formatted text blocks
2483# -------------------------
2485class literal_block(General, FixedTextElement): pass
2488class doctest_block(General, FixedTextElement):
2489 def __init__(self, rawsource='', text='', *children, **attributes):
2490 warnings.warn('nodes.doctest_block is deprecated '
2491 'and will be removed in Docutils 3.0.\n'
2492 ' Hint: Use nodes.literal_block with classes arguments.',
2493 PendingDeprecationWarning, stacklevel=2)
2494 super().__init__(rawsource, text, *children, **attributes)
2497class math_block(General, FixedTextElement, PureTextElement):
2498 """Mathematical notation (display formula)."""
2501class line(Part, TextElement):
2502 """Single line of text in a `line_block`."""
2503 indent: str | None = None
2506class line_block(General, Element):
2507 """Sequence of lines and nested line blocks.
2508 """
2509 # recursive content model: (line | line_block)+
2512line_block.content_model = (((line, line_block), '+'),)
2515# Admonitions
2516# -----------
2517# distinctive and self-contained notices
2519class attention(Admonition, Element): pass
2520class caution(Admonition, Element): pass
2521class danger(Admonition, Element): pass
2522class error(Admonition, Element): pass
2523class important(Admonition, Element): pass
2524class note(Admonition, Element): pass
2525class tip(Admonition, Element): pass
2526class hint(Admonition, Element): pass
2527class warning(Admonition, Element): pass
2530class admonition(Admonition, Element):
2531 content_model: Final = ((title, '.'), (Body, '+'))
2532 # (title, (%body.elements;)+)
2535# Footnote and citation
2536# ---------------------
2538class label(Part, PureTextElement):
2539 """Visible identifier for footnotes and citations."""
2542class footnote(General, BackLinkable, Element, Labeled, Targetable):
2543 """Labelled note providing additional context (footnote or endnote)."""
2544 valid_attributes: Final = Element.valid_attributes + ('auto', 'backrefs')
2545 content_model: Final = ((label, '.'), (Body, '+'))
2546 # (label, (%body.elements;)+)
2547 # The label was optional in Docutils < 1.0.
2550class citation(General, BackLinkable, Element, Labeled, Targetable):
2551 content_model: Final = ((label, '.'), (Body, '+'))
2552 # (label, (%body.elements;)+)
2555# Graphical elements
2556# ------------------
2558class image(General, Inline, Element):
2559 """Reference to an image resource.
2561 May be body element or inline element.
2562 """
2563 valid_attributes: Final = Element.valid_attributes + (
2564 'uri', 'alt', 'align', 'height', 'width', 'scale', 'loading')
2566 def astext(self) -> str:
2567 return self.get('alt', '')
2570class caption(Part, TextElement): pass
2573class legend(Part, Element):
2574 """A wrapper for text accompanying a `figure` that is not the caption."""
2575 content_model: Final = ((Body, '+'),) # (%body.elements;)+
2578class figure(General, Element):
2579 """A formal figure, generally an illustration, with a title."""
2580 valid_attributes: Final = Element.valid_attributes + ('align', 'width')
2581 content_model: Final = (((image, reference), '.'),
2582 (caption, '?'),
2583 (legend, '?'),
2584 )
2585 # (image, ((caption, legend?) | legend))
2586 # A caption or legend is required (cf. [bugs: #489]).
2589# Tables
2590# ------
2592class entry(Part, Element):
2593 """An entry in a `row` (a table cell)."""
2594 valid_attributes: Final = Element.valid_attributes + (
2595 'align', 'char', 'charoff', 'colname', 'colsep', 'morecols',
2596 'morerows', 'namest', 'nameend', 'rowsep', 'valign')
2597 content_model: Final = ((Body, '*'),)
2598 # %tbl.entry.mdl -> (%body.elements;)*
2601class row(Part, Element):
2602 """Row of table cells."""
2603 valid_attributes: Final = Element.valid_attributes + ('rowsep', 'valign')
2604 content_model: Final = ((entry, '+'),) # (%tbl.row.mdl;) -> entry+
2607class colspec(Part, Element):
2608 """Specifications for a column in a `tgroup`."""
2609 valid_attributes: Final = Element.valid_attributes + (
2610 'align', 'char', 'charoff', 'colname', 'colnum',
2611 'colsep', 'colwidth', 'rowsep', 'stub')
2613 def propwidth(self) -> int|float:
2614 """Return numerical value of "colwidth__" attribute. Default 1.
2616 Raise ValueError if "colwidth" is zero, negative, or a *fixed value*.
2618 Provisional.
2620 __ https://docutils.sourceforge.io/docs/ref/doctree.html#colwidth
2621 """
2622 measure = self.get('colwidth', '')
2623 if isinstance(measure, (int, float)):
2624 value = measure
2625 elif measure in ('*', ''): # short for '1*'
2626 value = 1
2627 else:
2628 try:
2629 value, _unit = parse_measure(measure, unit_pattern='[*]?')
2630 except ValueError:
2631 value = -1
2632 if value <= 0:
2633 raise ValueError(f'"{measure}" is no proportional measure.')
2634 return value
2637class thead(Part, Element):
2638 """Row(s) that form the head of a `tgroup`."""
2639 valid_attributes: Final = Element.valid_attributes + ('valign',)
2640 content_model: Final = ((row, '+'),) # (row+)
2643class tbody(Part, Element):
2644 """Body of a `tgroup`."""
2645 valid_attributes: Final = Element.valid_attributes + ('valign',)
2646 content_model: Final = ((row, '+'),) # (row+)
2649class tgroup(Part, Element):
2650 """A portion of a table. Most tables have just one `tgroup`."""
2651 valid_attributes: Final = Element.valid_attributes + (
2652 'align', 'cols', 'colsep', 'rowsep')
2653 content_model: Final = ((colspec, '*'), (thead, '?'), (tbody, '.'))
2654 # (colspec*, thead?, tbody)
2657class table(General, Element):
2658 """A data arrangement with rows and columns."""
2659 valid_attributes: Final = Element.valid_attributes + (
2660 'align', 'colsep', 'frame', 'pgwide', 'rowsep', 'width')
2661 content_model: Final = ((title, '?'), (tgroup, '+'))
2662 # (title?, tgroup+)
2665# Inline Elements
2666# ===============
2668class abbreviation(Inline, TextElement): pass
2669class acronym(Inline, TextElement): pass
2670class emphasis(Inline, TextElement): pass
2671class generated(Inline, TextElement): pass
2672class inline(Inline, TextElement, Targetable): pass
2673class literal(Inline, TextElement): pass
2674class strong(Inline, TextElement): pass
2675class subscript(Inline, TextElement): pass
2676class superscript(Inline, TextElement): pass
2677class title_reference(Inline, TextElement): pass
2680class footnote_reference(Inline, Referential, PureTextElement):
2681 valid_attributes: Final = Element.valid_attributes + (
2682 'auto', 'refid', 'refname')
2685class citation_reference(Inline, Referential, PureTextElement):
2686 valid_attributes: Final = Element.valid_attributes + ('refid', 'refname')
2689class substitution_reference(Inline, TextElement):
2690 valid_attributes: Final = Element.valid_attributes + ('refname',)
2693class math(Inline, PureTextElement):
2694 """Mathematical notation in running text."""
2697class problematic(Inline, TextElement):
2698 valid_attributes: Final = Element.valid_attributes + (
2699 'refid', 'refname', 'refuri')
2702# ========================================
2703# Auxiliary Classes, Functions, and Data
2704# ========================================
2706node_class_names: Sequence[str] = """
2707 Text
2708 abbreviation acronym address admonition attention attribution author
2709 authors
2710 block_quote bullet_list
2711 caption caution citation citation_reference classifier colspec comment
2712 compound contact container copyright
2713 danger date decoration definition definition_list definition_list_item
2714 description docinfo doctest_block document
2715 emphasis entry enumerated_list error
2716 field field_body field_list field_name figure footer
2717 footnote footnote_reference
2718 generated
2719 header hint
2720 image important inline
2721 label legend line line_block list_item literal literal_block
2722 math math_block meta
2723 note
2724 option option_argument option_group option_list option_list_item
2725 option_string organization
2726 paragraph pending problematic
2727 raw reference revision row rubric
2728 section sidebar status strong subscript substitution_definition
2729 substitution_reference subtitle superscript system_message
2730 table target tbody term tgroup thead tip title title_reference topic
2731 transition
2732 version
2733 warning""".split()
2734"""A list of names of all concrete Node subclasses."""
2737class NodeVisitor:
2738 """
2739 "Visitor" pattern [GoF95]_ abstract superclass implementation for
2740 document tree traversals.
2742 Each node class has corresponding methods, doing nothing by
2743 default; override individual methods for specific and useful
2744 behaviour. The `dispatch_visit()` method is called by
2745 `Node.walk()` upon entering a node. `Node.walkabout()` also calls
2746 the `dispatch_departure()` method before exiting a node.
2748 The dispatch methods call "``visit_`` + node class name" or
2749 "``depart_`` + node class name", resp.
2751 This is a base class for visitors whose ``visit_...`` & ``depart_...``
2752 methods must be implemented for *all* compulsory node types encountered
2753 (such as for `docutils.writers.Writer` subclasses).
2754 Unimplemented methods will raise exceptions (except for optional nodes).
2756 For sparse traversals, where only certain node types are of interest, use
2757 subclass `SparseNodeVisitor` instead. When (mostly or entirely) uniform
2758 processing is desired, subclass `GenericNodeVisitor`.
2760 .. [GoF95] Gamma, Helm, Johnson, Vlissides. *Design Patterns: Elements of
2761 Reusable Object-Oriented Software*. Addison-Wesley, Reading, MA, USA,
2762 1995.
2763 """
2765 optional: ClassVar[tuple[str, ...]] = ('meta',)
2766 """
2767 Tuple containing node class names (as strings).
2769 No exception will be raised if writers do not implement visit
2770 or departure functions for these node classes.
2772 Used to ensure transitional compatibility with existing 3rd-party writers.
2773 """
2775 def __init__(self, document: document, /) -> None:
2776 self.document: document = document
2778 def dispatch_visit(self, node) -> None:
2779 """
2780 Call self."``visit_`` + node class name" with `node` as
2781 parameter. If the ``visit_...`` method does not exist, call
2782 self.unknown_visit.
2783 """
2784 node_name = node.__class__.__name__
2785 method = getattr(self, 'visit_' + node_name, self.unknown_visit)
2786 self.document.reporter.debug(
2787 'docutils.nodes.NodeVisitor.dispatch_visit calling %s for %s'
2788 % (method.__name__, node_name))
2789 return method(node)
2791 def dispatch_departure(self, node) -> None:
2792 """
2793 Call self."``depart_`` + node class name" with `node` as
2794 parameter. If the ``depart_...`` method does not exist, call
2795 self.unknown_departure.
2796 """
2797 node_name = node.__class__.__name__
2798 method = getattr(self, 'depart_' + node_name, self.unknown_departure)
2799 self.document.reporter.debug(
2800 'docutils.nodes.NodeVisitor.dispatch_departure calling %s for %s'
2801 % (method.__name__, node_name))
2802 return method(node)
2804 def unknown_visit(self, node) -> None:
2805 """
2806 Called when entering unknown `Node` types.
2808 Raise an exception unless overridden.
2809 """
2810 if (self.document.settings.strict_visitor
2811 or node.__class__.__name__ not in self.optional):
2812 raise NotImplementedError(
2813 '%s visiting unknown node type: %s'
2814 % (self.__class__, node.__class__.__name__))
2816 def unknown_departure(self, node) -> None:
2817 """
2818 Called before exiting unknown `Node` types.
2820 Raise exception unless overridden.
2821 """
2822 if (self.document.settings.strict_visitor
2823 or node.__class__.__name__ not in self.optional):
2824 raise NotImplementedError(
2825 '%s departing unknown node type: %s'
2826 % (self.__class__, node.__class__.__name__))
2829class SparseNodeVisitor(NodeVisitor):
2830 """
2831 Base class for sparse traversals, where only certain node types are of
2832 interest. When ``visit_...`` & ``depart_...`` methods should be
2833 implemented for *all* node types (such as for `docutils.writers.Writer`
2834 subclasses), subclass `NodeVisitor` instead.
2835 """
2838class GenericNodeVisitor(NodeVisitor):
2839 """
2840 Generic "Visitor" abstract superclass, for simple traversals.
2842 Unless overridden, each ``visit_...`` method calls `default_visit()`, and
2843 each ``depart_...`` method (when using `Node.walkabout()`) calls
2844 `default_departure()`. `default_visit()` (and `default_departure()`) must
2845 be overridden in subclasses.
2847 Define fully generic visitors by overriding `default_visit()` (and
2848 `default_departure()`) only. Define semi-generic visitors by overriding
2849 individual ``visit_...()`` (and ``depart_...()``) methods also.
2851 `NodeVisitor.unknown_visit()` (`NodeVisitor.unknown_departure()`) should
2852 be overridden for default behavior.
2853 """
2855 def default_visit(self, node):
2856 """Override for generic, uniform traversals."""
2857 raise NotImplementedError
2859 def default_departure(self, node):
2860 """Override for generic, uniform traversals."""
2861 raise NotImplementedError
2864def _call_default_visit(self: GenericNodeVisitor, node) -> None:
2865 self.default_visit(node)
2868def _call_default_departure(self: GenericNodeVisitor, node) -> None:
2869 self.default_departure(node)
2872def _nop(self: SparseNodeVisitor, node) -> None:
2873 pass
2876def _add_node_class_names(names) -> None:
2877 """Save typing with dynamic assignments:"""
2878 for _name in names:
2879 setattr(GenericNodeVisitor, "visit_" + _name, _call_default_visit)
2880 setattr(GenericNodeVisitor, "depart_" + _name, _call_default_departure)
2881 setattr(SparseNodeVisitor, 'visit_' + _name, _nop)
2882 setattr(SparseNodeVisitor, 'depart_' + _name, _nop)
2885_add_node_class_names(node_class_names)
2888class TreeCopyVisitor(GenericNodeVisitor):
2889 """
2890 Make a complete copy of a tree or branch, including element attributes.
2891 """
2893 def __init__(self, document: document) -> None:
2894 super().__init__(document)
2895 self.parent_stack: list[list] = []
2896 self.parent: list = []
2898 def get_tree_copy(self):
2899 return self.parent[0]
2901 def default_visit(self, node) -> None:
2902 """Copy the current node, and make it the new acting parent."""
2903 newnode = node.copy()
2904 self.parent.append(newnode)
2905 self.parent_stack.append(self.parent)
2906 self.parent = newnode
2908 def default_departure(self, node) -> None:
2909 """Restore the previous acting parent."""
2910 self.parent = self.parent_stack.pop()
2913# Custom Exceptions
2914# =================
2916class ValidationError(ValueError):
2917 """Invalid Docutils Document Tree Element."""
2918 def __init__(self, msg: str, problematic_element: Element = None) -> None:
2919 super().__init__(msg)
2920 self.problematic_element = problematic_element
2923class TreePruningException(Exception):
2924 """
2925 Base class for `NodeVisitor`-related tree pruning exceptions.
2927 Raise subclasses from within ``visit_...`` or ``depart_...`` methods
2928 called from `Node.walk()` and `Node.walkabout()` tree traversals to prune
2929 the tree traversed.
2930 """
2933class SkipChildren(TreePruningException):
2934 """
2935 Do not visit any children of the current node. The current node's
2936 siblings and ``depart_...`` method are not affected.
2937 """
2940class SkipSiblings(TreePruningException):
2941 """
2942 Do not visit any more siblings (to the right) of the current node. The
2943 current node's children and its ``depart_...`` method are not affected.
2944 """
2947class SkipNode(TreePruningException):
2948 """
2949 Do not visit the current node's children, and do not call the current
2950 node's ``depart_...`` method.
2951 """
2954class SkipDeparture(TreePruningException):
2955 """
2956 Do not call the current node's ``depart_...`` method. The current node's
2957 children and siblings are not affected.
2958 """
2961class NodeFound(TreePruningException):
2962 """
2963 Raise to indicate that the target of a search has been found. This
2964 exception must be caught by the client; it is not caught by the traversal
2965 code.
2966 """
2969class StopTraversal(TreePruningException):
2970 """
2971 Stop the traversal altogether. The current node's ``depart_...`` method
2972 is not affected. The parent nodes ``depart_...`` methods are also called
2973 as usual. No other nodes are visited. This is an alternative to
2974 NodeFound that does not cause exception handling to trickle up to the
2975 caller.
2976 """
2979# definition moved here from `utils` to avoid circular import dependency
2980def unescape(text: str,
2981 restore_backslashes: bool = False,
2982 respect_whitespace: bool = False,
2983 ) -> str:
2984 """
2985 Return a string with nulls removed or restored to backslashes.
2986 Backslash-escaped spaces are also removed.
2987 """
2988 # `respect_whitespace` is ignored (since introduction 2016-12-16)
2989 if restore_backslashes:
2990 return text.replace('\x00', '\\')
2991 else:
2992 for sep in ['\x00 ', '\x00\n', '\x00']:
2993 text = ''.join(text.split(sep))
2994 return text
2997def make_id(string: str) -> str:
2998 """
2999 Convert `string` into an identifier and return it.
3001 Docutils identifiers will conform to the regular expression
3002 ``[a-z](-?[a-z0-9]+)*``. For CSS compatibility, identifiers (the "class"
3003 and "id" attributes) should have no underscores, colons, or periods.
3004 Hyphens may be used.
3006 - The `HTML 4.01 spec`_ defines identifiers based on SGML tokens:
3008 ID and NAME tokens must begin with a letter ([A-Za-z]) and may be
3009 followed by any number of letters, digits ([0-9]), hyphens ("-"),
3010 underscores ("_"), colons (":"), and periods (".").
3012 - However the `CSS1 spec`_ defines identifiers based on the "name" token,
3013 a tighter interpretation ("flex" tokenizer notation; "latin1" and
3014 "escape" 8-bit characters have been replaced with entities)::
3016 unicode \\[0-9a-f]{1,4}
3017 latin1 [¡-ÿ]
3018 escape {unicode}|\\[ -~¡-ÿ]
3019 nmchar [-a-z0-9]|{latin1}|{escape}
3020 name {nmchar}+
3022 The CSS1 "nmchar" rule does not include underscores ("_"), colons (":"),
3023 or periods ("."), therefore "class" and "id" attributes should not contain
3024 these characters. They should be replaced with hyphens ("-"). Combined
3025 with HTML's requirements (the first character must be a letter; no
3026 "unicode", "latin1", or "escape" characters), this results in the
3027 ``[a-z](-?[a-z0-9]+)*`` pattern.
3029 .. _HTML 4.01 spec: https://www.w3.org/TR/html401
3030 .. _CSS1 spec: https://www.w3.org/TR/REC-CSS1
3031 """
3032 id = string.lower()
3033 id = id.translate(_non_id_translate_digraphs)
3034 id = id.translate(_non_id_translate)
3035 # get rid of non-ascii characters.
3036 # 'ascii' lowercase to prevent problems with turkish locale.
3037 id = unicodedata.normalize(
3038 'NFKD', id).encode('ascii', 'ignore').decode('ascii')
3039 # shrink runs of whitespace and replace by hyphen
3040 id = _non_id_chars.sub('-', ' '.join(id.split()))
3041 id = _non_id_at_ends.sub('', id)
3042 return str(id)
3045_non_id_chars: re.Pattern[str] = re.compile('[^a-z0-9]+')
3046_non_id_at_ends: re.Pattern[str] = re.compile('^[-0-9]+|-+$')
3047_non_id_translate: dict[int, str] = {
3048 0x00f8: 'o', # o with stroke
3049 0x0111: 'd', # d with stroke
3050 0x0127: 'h', # h with stroke
3051 0x0131: 'i', # dotless i
3052 0x0142: 'l', # l with stroke
3053 0x0167: 't', # t with stroke
3054 0x0180: 'b', # b with stroke
3055 0x0183: 'b', # b with topbar
3056 0x0188: 'c', # c with hook
3057 0x018c: 'd', # d with topbar
3058 0x0192: 'f', # f with hook
3059 0x0199: 'k', # k with hook
3060 0x019a: 'l', # l with bar
3061 0x019e: 'n', # n with long right leg
3062 0x01a5: 'p', # p with hook
3063 0x01ab: 't', # t with palatal hook
3064 0x01ad: 't', # t with hook
3065 0x01b4: 'y', # y with hook
3066 0x01b6: 'z', # z with stroke
3067 0x01e5: 'g', # g with stroke
3068 0x0225: 'z', # z with hook
3069 0x0234: 'l', # l with curl
3070 0x0235: 'n', # n with curl
3071 0x0236: 't', # t with curl
3072 0x0237: 'j', # dotless j
3073 0x023c: 'c', # c with stroke
3074 0x023f: 's', # s with swash tail
3075 0x0240: 'z', # z with swash tail
3076 0x0247: 'e', # e with stroke
3077 0x0249: 'j', # j with stroke
3078 0x024b: 'q', # q with hook tail
3079 0x024d: 'r', # r with stroke
3080 0x024f: 'y', # y with stroke
3081}
3082_non_id_translate_digraphs: dict[int, str] = {
3083 0x00df: 'sz', # ligature sz
3084 0x00e6: 'ae', # ae
3085 0x0153: 'oe', # ligature oe
3086 0x0238: 'db', # db digraph
3087 0x0239: 'qp', # qp digraph
3088}
3091def dupname(node: Element, name: str) -> None:
3092 node['dupnames'].append(name)
3093 node['names'].remove(name)
3094 # Assume that `node` is referenced, even though it isn't;
3095 # we don't want to throw unnecessary system_messages.
3096 node.referenced = True
3099def fully_normalize_name(name: str) -> str:
3100 """Return a case- and whitespace-normalized name."""
3101 return ' '.join(name.lower().split())
3104def whitespace_normalize_name(name: str) -> str:
3105 """Return a whitespace-normalized name."""
3106 return ' '.join(name.split())
3109def serial_escape(value: str) -> str:
3110 """Escape string values that are elements of a list, for serialization."""
3111 return value.replace('\\', r'\\').replace(' ', r'\ ')
3114def split_name_list(s: str) -> list[str]:
3115 r"""Split a string at non-escaped whitespace.
3117 Backslashes escape internal whitespace (cf. `serial_escape()`).
3118 Return list of "names" (after removing escaping backslashes).
3120 >>> split_name_list(r'a\ n\ame two\\ n\\ames'),
3121 ['a name', 'two\\', r'n\ames']
3123 Provisional.
3124 """
3125 s = s.replace('\\', '\x00') # escape with NULL char
3126 s = s.replace('\x00\x00', '\\') # unescape backslashes
3127 s = s.replace('\x00 ', '\x00\x00') # escaped spaces -> NULL NULL
3128 names = s.split(' ')
3129 # restore internal spaces, drop other escaping characters
3130 return [name.replace('\x00\x00', ' ').replace('\x00', '')
3131 for name in names]
3134def pseudo_quoteattr(value: str) -> str:
3135 """Quote attributes for pseudo-xml"""
3136 return '"%s"' % value
3139def parse_measure(measure: str, unit_pattern: str = '[a-zA-Zµ]*|%?'
3140 ) -> tuple[int|float, str]:
3141 """Parse a measure__, return value + unit.
3143 `unit_pattern` is a regular expression describing recognized units.
3144 The default is suited for (but not limited to) CSS3 units and SI units.
3145 It matches runs of ASCII letters or Greek mu, a single percent sign,
3146 or no unit.
3148 __ https://docutils.sourceforge.io/docs/ref/doctree.html#measure
3150 Provisional.
3151 """
3152 match = re.fullmatch(f'(-?[0-9.]+) *({unit_pattern})', measure)
3153 try:
3154 try:
3155 value = int(match.group(1))
3156 except ValueError:
3157 value = float(match.group(1))
3158 unit = match.group(2)
3159 except (AttributeError, ValueError):
3160 raise ValueError(f'"{measure}" is no valid measure.')
3161 return value, unit
3164# Methods to validate `Element attribute`__ values.
3166# Ensure the expected Python `data type`__, normalize, and check for
3167# restrictions.
3168#
3169# The methods can be used to convert `str` values (eg. from an XML
3170# representation) or to validate an existing document tree or node.
3171#
3172# Cf. `Element.validate_attributes()`, `docutils.parsers.docutils_xml`,
3173# and the `attribute_validating_functions` mapping below.
3174#
3175# __ https://docutils.sourceforge.io/docs/ref/doctree.html#attribute-reference
3176# __ https://docutils.sourceforge.io/docs/ref/doctree.html#attribute-types
3178def create_keyword_validator(*keywords: str) -> Callable[[str], str]:
3179 """
3180 Return a function that validates a `str` against given `keywords`.
3182 Provisional.
3183 """
3184 def validate_keywords(value: str) -> str:
3185 if value not in keywords:
3186 allowed = '", \"'.join(keywords)
3187 raise ValueError(f'"{value}" is not one of "{allowed}".')
3188 return value
3189 return validate_keywords
3192def validate_identifier(value: str) -> str:
3193 """
3194 Validate identifier key or class name.
3196 Used in `idref.type`__ and for the tokens in `validate_identifier_list()`.
3198 __ https://docutils.sourceforge.io/docs/ref/doctree.html#idref-type
3200 Provisional.
3201 """
3202 if value != make_id(value):
3203 raise ValueError(f'"{value}" is no valid id or class name.')
3204 return value
3207def validate_identifier_list(value: str | list[str]) -> list[str]:
3208 """
3209 A (space-separated) list of ids or class names.
3211 `value` may be a `list` or a `str` with space separated
3212 ids or class names (cf. `validate_identifier()`).
3214 Used in `classnames.type`__, `ids.type`__, and `idrefs.type`__.
3216 __ https://docutils.sourceforge.io/docs/ref/doctree.html#classnames-type
3217 __ https://docutils.sourceforge.io/docs/ref/doctree.html#ids-type
3218 __ https://docutils.sourceforge.io/docs/ref/doctree.html#idrefs-type
3220 Provisional.
3221 """
3222 if isinstance(value, str):
3223 value = value.split()
3224 for token in value:
3225 validate_identifier(token)
3226 return value
3229def validate_measure(measure: str) -> str:
3230 """
3231 Validate a measure__ (number + optional unit). Return normalized `str`.
3233 See `parse_measure()` for a function returning a "number + unit" tuple.
3235 The unit may be a run of ASCII letters or Greek mu, a single percent sign,
3236 or the empty string. Case is preserved.
3238 Provisional.
3240 __ https://docutils.sourceforge.io/docs/ref/doctree.html#measure
3241 """
3242 value, unit = parse_measure(measure)
3243 return f'{value}{unit}'
3246def validate_colwidth(measure: str|int|float) -> str:
3247 """Validate the "colwidth__" attribute.
3249 Provisional:
3250 Accept fixed length units in Docutils 2.0.
3251 The default unit will change to "pt" in Docutils 3.0.
3253 __ https://docutils.sourceforge.io/docs/ref/doctree.html#colwidth
3254 """
3255 if isinstance(measure, (int, float)):
3256 value, unit = measure, ''
3257 elif measure in ('*', ''): # short for '1*'
3258 value, unit = 1, ''
3259 else:
3260 try:
3261 value, unit = parse_measure(measure, unit_pattern='[*]?')
3262 except ValueError:
3263 value = -1
3264 if value <= 0:
3265 raise ValueError(f'"{measure}" is no proportional measure.')
3266 return f'{value}{unit}'
3269def validate_NMTOKEN(value: str) -> str:
3270 """
3271 Validate a "name token": a `str` of ASCII letters, digits, and [-._].
3273 Provisional.
3274 """
3275 if not re.fullmatch('[-._A-Za-z0-9]+', value):
3276 raise ValueError(f'"{value}" is no NMTOKEN.')
3277 return value
3280def validate_NMTOKENS(value: str | list[str]) -> list[str]:
3281 """
3282 Validate a list of "name tokens".
3284 Provisional.
3285 """
3286 if isinstance(value, str):
3287 value = value.split()
3288 for token in value:
3289 validate_NMTOKEN(token)
3290 return value
3293def validate_refname_list(value: str | list[str]) -> list[str]:
3294 """
3295 Validate a list of `reference names`__.
3297 Reference names may contain all characters;
3298 whitespace is normalized (cf, `whitespace_normalize_name()`).
3300 `value` may be either a `list` of names or a `str` with
3301 space separated names (with internal spaces backslash escaped
3302 and literal backslashes doubled cf. `serial_escape()`).
3304 Return a list of whitespace-normalized, unescaped reference names.
3306 Provisional.
3308 __ https://docutils.sourceforge.io/docs/ref/doctree.html#reference-name
3309 """
3310 if isinstance(value, str):
3311 value = split_name_list(value)
3312 return [whitespace_normalize_name(name) for name in value]
3315def validate_yesorno(value: str | int | bool) -> bool:
3316 """Validate a `%yesorno`__ (flag) value.
3318 The string literal "0" evaluates to ``False``, all other
3319 values are converterd with `bool()`.
3321 __ https://docutils.sourceforge.io/docs/ref/doctree.html#yesorno
3322 """
3323 if value == "0":
3324 return False
3325 return bool(value)
3328ATTRIBUTE_VALIDATORS: dict[str, Callable[[str], Any]] = {
3329 'alt': str, # CDATA
3330 'align': str,
3331 'anonymous': validate_yesorno,
3332 'auto': str, # CDATA (only '1' or '*' are used in rST)
3333 'backrefs': validate_identifier_list,
3334 'bullet': str, # CDATA (only '-', '+', or '*' are used in rST)
3335 'classes': validate_identifier_list,
3336 'char': str, # from Exchange Table Model (CALS), currently ignored
3337 'charoff': validate_NMTOKEN, # from CALS, currently ignored
3338 'colname': validate_NMTOKEN, # from CALS, currently ignored
3339 'colnum': int, # from CALS, currently ignored
3340 'cols': int, # from CALS: "NMTOKEN, […] must be an integer > 0".
3341 'colsep': validate_yesorno,
3342 'colwidth': validate_colwidth, # see docstring for pending changes
3343 'content': str, # <meta>
3344 'delimiter': str,
3345 'dir': create_keyword_validator('ltr', 'rtl', 'auto'), # <meta>
3346 'dupnames': validate_refname_list,
3347 'enumtype': create_keyword_validator('arabic', 'loweralpha', 'lowerroman',
3348 'upperalpha', 'upperroman'),
3349 'format': str, # CDATA (space separated format names)
3350 'frame': create_keyword_validator('top', 'bottom', 'topbot', 'all',
3351 'sides', 'none'), # from CALS, ignored
3352 'height': validate_measure,
3353 'http-equiv': str, # <meta>
3354 'ids': validate_identifier_list,
3355 'lang': str, # <meta>
3356 'level': int,
3357 'line': int,
3358 'ltrim': validate_yesorno,
3359 'loading': create_keyword_validator('embed', 'link', 'lazy'),
3360 'media': str, # <meta>
3361 'morecols': int,
3362 'morerows': int,
3363 'name': whitespace_normalize_name, # in <reference> (deprecated)
3364 # 'name': node_attributes.validate_NMTOKEN, # in <meta>
3365 'names': validate_refname_list,
3366 'namest': validate_NMTOKEN, # start of span, from CALS, currently ignored
3367 'nameend': validate_NMTOKEN, # end of span, from CALS, currently ignored
3368 'pgwide': validate_yesorno, # from CALS, currently ignored
3369 'prefix': str,
3370 'refid': validate_identifier,
3371 'refname': whitespace_normalize_name,
3372 'refuri': str,
3373 'rowsep': validate_yesorno,
3374 'rtrim': validate_yesorno,
3375 'scale': int,
3376 'scheme': str,
3377 'source': str,
3378 'start': int,
3379 'stub': validate_yesorno,
3380 'suffix': str,
3381 'title': str,
3382 'type': validate_NMTOKEN,
3383 'uri': str,
3384 'valign': create_keyword_validator('top', 'middle', 'bottom'), # from CALS
3385 'width': validate_measure,
3386 'xml:space': create_keyword_validator('default', 'preserve'),
3387 }
3388"""
3389Mapping of `attribute names`__ to validating functions.
3391Provisional.
3393__ https://docutils.sourceforge.io/docs/ref/doctree.html#attribute-reference
3394"""