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

1284 statements  

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. 

5 

6""" 

7Docutils document tree element class library. 

8 

9The relationships and semantics of elements and attributes is documented in 

10`The Docutils Document Tree`__. 

11 

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_. 

16 

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. 

22 

23__ https://docutils.sourceforge.io/docs/ref/doctree.html 

24.. _DTD: https://docutils.sourceforge.io/docs/ref/docutils.dtd 

25""" 

26 

27from __future__ import annotations 

28 

29__docformat__ = 'reStructuredText' 

30 

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() 

39 

40# import docutils.transforms # -> delayed import in document.__init__() 

41 

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 

48 

49 from docutils.utils._typing import TypeAlias 

50 

51 from xml.dom import minidom 

52 

53 from docutils.frontend import Values 

54 from docutils.transforms import Transformer, Transform 

55 from docutils.utils import Reporter 

56 

57 _ContentModelCategory: TypeAlias = tuple['Element' | tuple['Element', ...]] 

58 _ContentModelQuantifier = Literal['.', '?', '+', '*'] 

59 _ContentModelItem: TypeAlias = tuple[_ContentModelCategory, 

60 _ContentModelQuantifier] 

61 _ContentModelTuple: TypeAlias = tuple[_ContentModelItem, ...] 

62 

63 StrPath: TypeAlias = str | os.PathLike[str] 

64 """File system path. No bytes!""" 

65 

66 _UpdateFun: TypeAlias = Callable[[str, Any, bool], None] 

67 

68 

69# ============================== 

70# Functional Node Base Classes 

71# ============================== 

72 

73class Node: 

74 """Abstract base class of nodes in a document tree.""" 

75 

76 parent: Element | None = None 

77 """Back-reference to the Node immediately containing this Node.""" 

78 

79 children: Sequence # defined in subclasses 

80 """List of child nodes (Elements or Text). 

81 

82 Override in subclass instances that are not terminal nodes. 

83 """ 

84 

85 source: StrPath | None = None 

86 """Path or description of the input source which generated this Node.""" 

87 

88 line: int | None = None 

89 """The line number (1-based) of the beginning of this Node in `source`.""" 

90 

91 tagname: str # defined in subclasses 

92 """The element generic identifier.""" 

93 

94 _document: document | None = None 

95 

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 

104 

105 @document.setter 

106 def document(self, value: document) -> None: 

107 self._document = value 

108 

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. 

114 

115 Use `len()` to check node length. 

116 """ 

117 return True 

118 

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) 

127 

128 def pformat(self, indent: str = ' ', level: int = 0) -> str: 

129 """ 

130 Return an indented pseudo-XML representation, for test purposes. 

131 

132 Override in subclasses. 

133 """ 

134 raise NotImplementedError 

135 

136 def copy(self) -> Self: 

137 """Return a copy of self.""" 

138 raise NotImplementedError 

139 

140 def deepcopy(self) -> Self: 

141 """Return a deep copy of self (also copying children).""" 

142 raise NotImplementedError 

143 

144 def astext(self) -> str: 

145 """Return a string representation of this Node.""" 

146 raise NotImplementedError 

147 

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 

156 

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.) 

164 

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. 

170 

171 Within ``visit`` methods (and ``depart`` methods for 

172 `walkabout()`), `TreePruningException` subclasses may be raised 

173 (`SkipChildren`, `SkipSiblings`, `SkipNode`, `SkipDeparture`). 

174 

175 Parameter `visitor`: A `NodeVisitor` object, containing a 

176 ``visit`` implementation for each `Node` subclass encountered. 

177 

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 

202 

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. 

208 

209 Parameter `visitor`: A `NodeVisitor` object, containing a 

210 ``visit`` and ``depart`` implementation for each `Node` 

211 subclass encountered. 

212 

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 

245 

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) 

252 

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() 

261 

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`: 

271 

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. 

278 

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)``. 

283 

284 If `ascend` is true, assume `siblings` to be true as well. 

285 

286 If the tree structure is modified during iteration, the result 

287 is undefined. 

288 

289 For example, given the following tree:: 

290 

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 

298 

299 Then tuple(emphasis.traverse()) equals :: 

300 

301 (<emphasis>, <strong>, <#text: Foo>, <#text: Bar>) 

302 

303 and list(strong.traverse(ascend=True) equals :: 

304 

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 

322 

323 def condition(node, class_or_tuple=class_or_tuple): 

324 return isinstance(node, class_or_tuple) 

325 

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 

349 

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`. 

359 

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)) 

367 

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. 

379 

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 

388 

389 def validate(self, recursive: bool = True) -> None: 

390 """Raise ValidationError if this node is not valid. 

391 

392 Override in subclasses that define validity constraints. 

393 """ 

394 

395 def validate_position(self) -> None: 

396 """Hook for additional checks of the parent's content model. 

397 

398 Raise ValidationError, if `self` is at an invalid position. 

399 

400 Override in subclasses with complex validity constraints. See 

401 `subtitle.validate_position()` and `transition.validate_position()`. 

402 """ 

403 

404 

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. 

409 

410 Access the raw (null-escaped) text with ``str(<instance>)`` 

411 and unescaped text with ``<instance>.astext()``. 

412 """ 

413 

414 tagname: Final = '#text' 

415 

416 children: Final = () 

417 """Text nodes have no children, and cannot have children.""" 

418 

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) 

430 

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)) 

436 

437 def __repr__(self) -> str: 

438 return self.shortrepr(maxlen=68) 

439 

440 def astext(self) -> str: 

441 return str(unescape(self)) 

442 

443 def _dom_node(self, domroot: minidom.Document) -> minidom.Text: 

444 return domroot.createTextNode(str(self)) 

445 

446 def copy(self) -> Self: 

447 return self.__class__(str(self)) 

448 

449 def deepcopy(self) -> Self: 

450 return self.copy() 

451 

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' 

466 

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. 

470 

471 def rstrip(self, chars: str | None = None) -> Self: 

472 return self.__class__(str.rstrip(self, chars)) 

473 

474 def lstrip(self, chars: str | None = None) -> Self: 

475 return self.__class__(str.lstrip(self, chars)) 

476 

477 

478class Element(Node): 

479 """ 

480 `Element` is the superclass to all specific elements. 

481 

482 Elements contain attributes and child nodes. 

483 They can be described as a cross between a list and a dictionary. 

484 

485 Elements emulate dictionaries for external [#]_ attributes, indexing by 

486 attribute name (a string). To set the attribute 'att' to 'value', do:: 

487 

488 element['att'] = 'value' 

489 

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``. 

494 

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). 

500 

501 Elements emulate lists for child nodes (element nodes and/or text 

502 nodes), indexing by integer. To get the first child node, use:: 

503 

504 element[0] 

505 

506 to iterate over the child nodes (without descending), use:: 

507 

508 for child in element: 

509 ... 

510 

511 Elements may be constructed using the ``+=`` operator. To add one new 

512 child node to element, do:: 

513 

514 element += node 

515 

516 This is equivalent to ``element.append(node)``. 

517 

518 To add a list of multiple child nodes at once, use the same ``+=`` 

519 operator:: 

520 

521 element += [node1, node2] 

522 

523 This is equivalent to ``element.extend([node1, node2])``. 

524 """ 

525 

526 list_attributes: Final = ('ids', 'classes', 'names', 'dupnames') 

527 """Tuple of attributes that are initialized to empty lists. 

528 

529 NOTE: Derived classes should update this value when supporting 

530 additional list attributes. 

531 """ 

532 

533 valid_attributes: Final = list_attributes + ('source',) 

534 """Tuple of attributes that are valid for elements of this class. 

535 

536 NOTE: Derived classes should update this value when supporting 

537 additional attributes. 

538 """ 

539 

540 common_attributes: Final = valid_attributes 

541 """Tuple of `common attributes`__ known to all Doctree Element classes. 

542 

543 __ https://docutils.sourceforge.io/docs/ref/doctree.html#common-attributes 

544 """ 

545 

546 known_attributes: Final = common_attributes 

547 """Alias for `common_attributes`. Will be removed in Docutils 2.0.""" 

548 

549 basic_attributes: Final = list_attributes 

550 """Common list attributes. Deprecated. Will be removed in Docutils 2.0.""" 

551 

552 local_attributes: Final = ('backrefs',) 

553 """Obsolete. Will be removed in Docutils 2.0.""" 

554 

555 content_model: ClassVar[_ContentModelTuple] = () 

556 """Python representation of the element's content model (cf. docutils.dtd). 

557 

558 A tuple of ``(category, quantifier)`` tuples with 

559 

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). 

566 

567 NOTE: The default describes the empty element. Derived classes should 

568 update this value to match their content model. 

569 

570 Provisional. 

571 """ 

572 

573 tagname: str | None = None 

574 """The element generic identifier. 

575 

576 If None, it is set as an instance attribute to the name of the class. 

577 """ 

578 

579 child_text_separator: Final = '\n\n' 

580 """Separator for child nodes, used by `astext()` method.""" 

581 

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. 

589 

590 For informative and debugging purposes. Don't rely on its value! 

591 

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.') 

596 

597 self.children: list = [] 

598 """List of child nodes (elements and/or `Text`).""" 

599 

600 self.extend(children) # maintain parent info 

601 

602 self.attributes: dict[str, Any] = {} 

603 """Dictionary of attribute {name: value}.""" 

604 

605 # Initialize list attributes. 

606 for att in self.list_attributes: 

607 self.attributes[att] = [] 

608 

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 

616 

617 if self.tagname is None: 

618 self.tagname: str = self.__class__.__name__ 

619 

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 

629 

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) 

642 

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 

648 

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() 

656 

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) 

676 

677 def endtag(self) -> str: 

678 return '</%s>' % self.tagname 

679 

680 def emptytag(self) -> str: 

681 attributes = ('%s="%s"' % (n, v) for n, v in self.attlist()) 

682 return '<%s/>' % ' '.join((self.tagname, *attributes)) 

683 

684 def __len__(self) -> int: 

685 return len(self.children) 

686 

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 

695 

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') 

707 

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') 

722 

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') 

734 

735 def __add__(self, other: list) -> list: 

736 return self.children + other 

737 

738 def __radd__(self, other: list) -> list: 

739 return other + self.children 

740 

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 

748 

749 def astext(self) -> str: 

750 return self.child_text_separator.join( 

751 [child.astext() for child in self.children]) 

752 

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 

757 

758 def attlist(self) -> list[tuple[str, Any]]: 

759 return sorted(self.non_default_attributes().items()) 

760 

761 def get(self, key: str, failobj: Any | None = None) -> Any: 

762 return self.attributes.get(key, failobj) 

763 

764 def hasattr(self, attr: str) -> bool: 

765 return attr in self.attributes 

766 

767 def delattr(self, attr: str) -> None: 

768 if attr in self.attributes: 

769 del self.attributes[attr] 

770 

771 def setdefault(self, key: str, failobj: Any | None = None) -> Any: 

772 return self.attributes.setdefault(key, failobj) 

773 

774 has_key = hasattr 

775 

776 def get_language_code(self, fallback: str = '') -> str: 

777 """Return node's language tag. 

778 

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 

790 

791 def append(self, item) -> None: 

792 self.setup_child(item) 

793 self.children.append(item) 

794 

795 def extend(self, item: Iterable) -> None: 

796 for node in item: 

797 self.append(node) 

798 

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 

805 

806 def pop(self, i: int = -1): 

807 return self.children.pop(i) 

808 

809 def remove(self, item) -> None: 

810 self.children.remove(item) 

811 

812 def index(self, item, start: int = 0, stop: int = sys.maxsize) -> int: 

813 return self.children.index(item, start, stop) 

814 

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 

822 

823 def section_hierarchy(self) -> list[section]: 

824 """Return the element's section anchestors. 

825 

826 Return a list of all <section> elements that contain `self` 

827 (including `self` if it is a <section>) and have a parent node. 

828 

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. 

832 

833 See `docutils.parsers.rst.states.RSTState.check_subsection()` 

834 for a usage example. 

835 

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 

846 

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 

852 

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_`. 

857 

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, [])) 

864 

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. 

869 

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) 

877 

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. 

884 

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) 

893 

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 

902 

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. 

908 

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) 

914 

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) 

930 

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) 

946 

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) 

955 

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_`. 

964 

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. 

973 

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 

983 

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 

989 

990 # Copy the basic attributes 

991 self.update_basic_atts(dict_) 

992 

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) 

997 

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_`. 

1005 

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. 

1013 

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) 

1021 

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_`. 

1029 

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. 

1040 

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) 

1048 

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_`. 

1056 

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. 

1068 

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) 

1076 

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_`. 

1083 

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. 

1092 

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) 

1100 

1101 def clear(self) -> None: 

1102 self.children = [] 

1103 

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 

1112 

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. 

1117 

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) 

1136 

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. 

1145 

1146 Parameters: 

1147 

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 

1160 

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. 

1170 

1171 Parameters: 

1172 

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 

1187 

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)) 

1192 

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 

1199 

1200 def deepcopy(self) -> Self: 

1201 copy = self.copy() 

1202 copy.extend([child.deepcopy() for child in self.children]) 

1203 return copy 

1204 

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 

1220 

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 

1228 

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. 

1233 

1234 Provisional. May be removed in Docutils 2.0. 

1235 """ 

1236 return attr not in cls.common_attributes 

1237 

1238 def validate_attributes(self) -> None: 

1239 """Normalize and validate element attributes. 

1240 

1241 Convert string values to expected datatype. 

1242 Normalize values. 

1243 

1244 Raise `ValidationError` for invalid attributes or attribute values. 

1245 

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) 

1265 

1266 def validate_content(self, 

1267 model: _ContentModelTuple | None = None, 

1268 elements: Sequence | None = None, 

1269 ) -> list: 

1270 """Test compliance of `elements` with `model`. 

1271 

1272 :model: content model description, default `self.content_model`, 

1273 :elements: list of doctree elements, default `self.children`. 

1274 

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`. 

1277 

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] 

1310 

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()}.') 

1328 

1329 def validate(self, recursive: bool = True) -> None: 

1330 """Validate Docutils Document Tree element ("doctree"). 

1331 

1332 Raise ValidationError if there are violations. 

1333 If `recursive` is True, validate also the element's descendants. 

1334 

1335 See `The Docutils Document Tree`__ for details of the 

1336 Docutils Document Model. 

1337 

1338 __ https://docutils.sourceforge.io/docs/ref/doctree.html 

1339 

1340 Provisional (work in progress). 

1341 """ 

1342 self.validate_attributes() 

1343 

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) 

1355 

1356 if recursive: 

1357 for child in self: 

1358 child.validate(recursive=recursive) 

1359 

1360 

1361# ==================== 

1362# Element Categories 

1363# ==================== 

1364# 

1365# See https://docutils.sourceforge.io/docs/ref/doctree.html#element-hierarchy. 

1366 

1367class Root: 

1368 """Element at the root of a document tree.""" 

1369 

1370 

1371class Structural: 

1372 """`Structural elements`__. 

1373 

1374 __ https://docutils.sourceforge.io/docs/ref/doctree.html 

1375 #structural-elements 

1376 """ 

1377 

1378 

1379class SubStructural: 

1380 """`Structural subelements`__ are children of `Structural` elements. 

1381 

1382 Most Structural elements accept only specific `SubStructural` elements. 

1383 

1384 __ https://docutils.sourceforge.io/docs/ref/doctree.html 

1385 #structural-subelements 

1386 """ 

1387 

1388 

1389class Bibliographic: 

1390 """`Bibliographic Elements`__ (displayed document meta-data). 

1391 

1392 __ https://docutils.sourceforge.io/docs/ref/doctree.html 

1393 #bibliographic-elements 

1394 """ 

1395 

1396 

1397class Body: 

1398 """`Body elements`__. 

1399 

1400 __ https://docutils.sourceforge.io/docs/ref/doctree.html#body-elements 

1401 """ 

1402 

1403 

1404class Admonition(Body): 

1405 """Admonitions (distinctive and self-contained notices).""" 

1406 content_model: Final = ((Body, '+'),) # (%body.elements;)+ 

1407 

1408 

1409class Sequential(Body): 

1410 """List-like body elements.""" 

1411 

1412 

1413class General(Body): 

1414 """Miscellaneous body elements.""" 

1415 

1416 

1417class Special(Body): 

1418 """Special internal body elements.""" 

1419 

1420 

1421class Part: 

1422 """`Body Subelements`__ always occur within specific parent elements. 

1423 

1424 __ https://docutils.sourceforge.io/docs/ref/doctree.html#body-subelements 

1425 """ 

1426 

1427 

1428class Decorative: 

1429 """Decorative elements (`header` and `footer`). 

1430 

1431 Children of `decoration`. 

1432 """ 

1433 content_model: Final = ((Body, '+'),) # (%body.elements;)+ 

1434 

1435 

1436class Inline: 

1437 """Inline elements contain text data and possibly other inline elements. 

1438 """ 

1439 

1440 

1441# Orthogonal categories and Mixins 

1442# ================================ 

1443 

1444class PreBibliographic: 

1445 """Elements which may occur before Bibliographic Elements.""" 

1446 

1447 

1448class Invisible(Special, PreBibliographic): 

1449 """Internal elements that don't appear in output.""" 

1450 

1451 

1452class Labeled: 

1453 """Contains a `label` as its first element.""" 

1454 

1455 

1456class Resolvable: 

1457 resolved: bool = False 

1458 

1459 

1460class BackLinkable: 

1461 """Mixin for Elements that accept a "backrefs" attribute.""" 

1462 

1463 list_attributes: Final = Element.list_attributes + ('backrefs',) 

1464 valid_attributes: Final = Element.valid_attributes + ('backrefs',) 

1465 

1466 def add_backref(self: Element, refid: str) -> None: 

1467 self['backrefs'].append(refid) 

1468 

1469 

1470class Referential(Resolvable): 

1471 """Elements holding a cross-reference (outgoing hyperlink).""" 

1472 

1473 

1474class Targetable(Resolvable): 

1475 """Cross-reference targets (incoming hyperlink).""" 

1476 referenced: int = 0 

1477 

1478 

1479class Titular: 

1480 """Title, sub-title, or informal heading (rubric).""" 

1481 

1482 

1483class TextElement(Element): 

1484 """ 

1485 An element which directly contains text. 

1486 

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. 

1492 

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;)* 

1498 

1499 child_text_separator: Final = '' 

1500 """Separator for child nodes, used by `astext()` method.""" 

1501 

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) 

1514 

1515 

1516class FixedTextElement(TextElement): 

1517 """An element which directly contains preformatted text.""" 

1518 

1519 valid_attributes: Final = Element.valid_attributes + ('xml:space',) 

1520 

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' 

1529 

1530 

1531class PureTextElement(TextElement): 

1532 """An element which only contains text, no children.""" 

1533 content_model: Final = ((Text, '?'),) # (#PCDATA) 

1534 

1535 

1536# ================================= 

1537# Concrete Document Tree Elements 

1538# ================================= 

1539# 

1540# See https://docutils.sourceforge.io/docs/ref/doctree.html#element-reference 

1541 

1542# Special purpose elements 

1543# ======================== 

1544# 

1545# Body elements for internal use or special requests. 

1546 

1547class comment(Invisible, FixedTextElement, PureTextElement): 

1548 """Author notes, hidden from the output.""" 

1549 

1550 

1551class substitution_definition(Invisible, TextElement): 

1552 valid_attributes: Final = Element.valid_attributes + ('ltrim', 'rtrim') 

1553 

1554 

1555class target(Invisible, Inline, TextElement, Targetable): 

1556 valid_attributes: Final = Element.valid_attributes + ( 

1557 'anonymous', 'refid', 'refname', 'refuri') 

1558 

1559 

1560class system_message(Special, BackLinkable, PreBibliographic, Element): 

1561 """ 

1562 System message element. 

1563 

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;)+ 

1570 

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 

1585 

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)) 

1590 

1591 

1592class pending(Invisible, Element): 

1593 """ 

1594 Placeholder for pending operations. 

1595 

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. 

1602 

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:: 

1606 

1607 .. contents:: 

1608 

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:: 

1613 

1614 <pending ...public attributes...> + internal attributes 

1615 

1616 Use `document.note_pending()` so that the 

1617 `docutils.transforms.Transformer` stage of processing can run all pending 

1618 transforms. 

1619 """ 

1620 

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) 

1629 

1630 self.transform: Transform = transform 

1631 """The `docutils.transforms.Transform` class implementing the pending 

1632 operation.""" 

1633 

1634 self.details: Mapping[str, Any] = details or {} 

1635 """Detail data (dictionary) required by the pending operation.""" 

1636 

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)) 

1660 

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 

1668 

1669 

1670class raw(Special, Inline, PreBibliographic, 

1671 FixedTextElement, PureTextElement): 

1672 """Raw data that is to be passed untouched to the Writer. 

1673 

1674 Can be used as Body element or Inline element. 

1675 """ 

1676 valid_attributes: Final = Element.valid_attributes + ( 

1677 'format', 'xml:space') 

1678 

1679 

1680# Decorative Elements 

1681# =================== 

1682 

1683class header(Decorative, Element): pass 

1684class footer(Decorative, Element): pass 

1685 

1686 

1687# Structural Subelements 

1688# ====================== 

1689 

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') 

1694 

1695 

1696class subtitle(Titular, PreBibliographic, SubStructural, TextElement): 

1697 """Sub-title of `document`, `section` and `sidebar`.""" 

1698 

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) 

1705 

1706 

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') 

1711 

1712 

1713class docinfo(SubStructural, Element): 

1714 """Container for displayed document meta-data.""" 

1715 content_model: Final = ((Bibliographic, '+'),) 

1716 # (%bibliographic.elements;)+ 

1717 

1718 

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?) 

1725 

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] 

1730 

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] 

1735 

1736 

1737class transition(SubStructural, Element): 

1738 """Transitions__ represent "semantic breaks". 

1739 

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) 

1746 

1747 def validate_position(self) -> None: 

1748 """Check additional constraints on `transition` placement. 

1749 

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) 

1770 

1771 

1772# Structural Elements 

1773# =================== 

1774 

1775class topic(Structural, Element): 

1776 """ 

1777 Topics__ are non-recursive, mini-sections. 

1778 

1779 __ https://docutils.sourceforge.io/docs/ref/doctree.html#topic 

1780 """ 

1781 content_model: Final = ((title, '?'), (Body, '+')) 

1782 # (title?, (%body.elements;)+) 

1783 

1784 

1785class sidebar(Structural, Element): 

1786 """ 

1787 Sidebars__ are like parallel documents providing related material. 

1788 

1789 A sidebar is typically offset by a border and "floats" to the side 

1790 of the page 

1791 

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()`. 

1800 

1801 

1802class section(Structural, Element): 

1803 """Document section__. The main unit of hierarchy. 

1804 

1805 __ https://docutils.sourceforge.io/docs/ref/doctree.html#section 

1806 """ 

1807 # recursive content model, see below 

1808 

1809 

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()`. 

1817 

1818 

1819# Root Element 

1820# ============ 

1821 

1822class document(Root, Element): 

1823 """ 

1824 The document root element. 

1825 

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. 

1846 

1847 def __init__(self, 

1848 settings: Values, 

1849 reporter: Reporter, 

1850 *args, 

1851 **kwargs: Any, 

1852 ) -> None: 

1853 Element.__init__(self, *args, **kwargs) 

1854 

1855 self.current_source: StrPath | None = None 

1856 """Path to or description of the input source being processed.""" 

1857 

1858 self.current_line: int | None = None 

1859 """Line number (1-based) of `current_source`.""" 

1860 

1861 self.settings: Values = settings 

1862 """Runtime settings data record.""" 

1863 

1864 self.reporter: Reporter = reporter 

1865 """System message generator.""" 

1866 

1867 self.indirect_targets: list[target] = [] 

1868 """List of indirect target nodes.""" 

1869 

1870 self.substitution_defs: dict[str, substitution_definition] = {} 

1871 """Mapping of substitution names to substitution_definition nodes.""" 

1872 

1873 self.substitution_names: dict[str, str] = {} 

1874 """Mapping of case-normalized to case-sensitive substitution names.""" 

1875 

1876 self.refnames: dict[str, list[Element]] = {} 

1877 """Mapping of names to lists of referencing nodes.""" 

1878 

1879 self.refids: dict[str, list[Element]] = {} 

1880 """(Incomplete) Mapping of ids to lists of referencing nodes.""" 

1881 

1882 self.names: dict[str, Element|None] = {} 

1883 """Mapping of names to nodes (or ``None`` if name is a duplicate).""" 

1884 

1885 self.ids: dict[str, Element] = {} 

1886 """Mapping of ids to nodes.""" 

1887 

1888 self.nameids: dict[str, str] = {} 

1889 """Mapping of names to unique id's.""" 

1890 

1891 self.nametypes: dict[str, bool] = {} 

1892 """Mapping of names to hyperlink type. True: explicit, False: implicit. 

1893 """ 

1894 

1895 self.footnote_refs: dict[str, list[footnote_reference]] = {} 

1896 """Mapping of footnote labels to lists of footnote_reference nodes.""" 

1897 

1898 self.citation_refs: dict[str, list[citation_reference]] = {} 

1899 """Mapping of citation labels to lists of citation_reference nodes.""" 

1900 

1901 self.autofootnotes: list[footnote] = [] 

1902 """List of auto-numbered footnote nodes.""" 

1903 

1904 self.autofootnote_refs: list[footnote_reference] = [] 

1905 """List of auto-numbered footnote_reference nodes.""" 

1906 

1907 self.symbol_footnotes: list[footnote] = [] 

1908 """List of symbol footnote nodes.""" 

1909 

1910 self.symbol_footnote_refs: list[footnote_reference] = [] 

1911 """List of symbol footnote_reference nodes.""" 

1912 

1913 self.footnotes: list[footnote] = [] 

1914 """List of manually-numbered footnote nodes.""" 

1915 

1916 self.citations: list[citation] = [] 

1917 """List of citation nodes.""" 

1918 

1919 self.autofootnote_start: int = 1 

1920 """Initial auto-numbered footnote number.""" 

1921 

1922 self.symbol_footnote_start: int = 0 

1923 """Initial symbol footnote symbol index.""" 

1924 

1925 self.id_counter: Counter[int] = Counter() 

1926 """Numbers added to otherwise identical IDs.""" 

1927 

1928 self.parse_messages: list[system_message] = [] 

1929 """System messages generated while parsing.""" 

1930 

1931 self.transform_messages: list[system_message] = [] 

1932 """System messages generated while applying transforms.""" 

1933 

1934 import docutils.transforms 

1935 self.transformer: Transformer = docutils.transforms.Transformer(self) 

1936 """Storage for transforms to be applied to this document.""" 

1937 

1938 self.include_log: list[tuple[StrPath, tuple]] = [] 

1939 """The current source's parents (to detect inclusion loops).""" 

1940 

1941 self.decoration: decoration | None = None 

1942 """Document's `decoration` node.""" 

1943 

1944 self._document: document = self 

1945 

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 

1954 

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 

1962 

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. 

1970 

1971 Check `node`s identifiers for duplicates, 

1972 create a new identifier if there are no identifiers. 

1973 Update `document.ids` and `document.nameids`. 

1974 

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 

1992 

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 

2029 

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 

2042 

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`__. 

2051 

2052 Called by `self.note_names()` when the reference name `name` 

2053 of the element `node` is already registered in `self.names`. 

2054 

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). 

2058 

2059 `self.nametypes` maps names to booleans representing the target type 

2060 (True = "explicit", False = "implicit"). 

2061 

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. 

2065 

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`. 

2069 

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 ======== ==== ======== ==== ======== =============== ======= 

2084 

2085 .. [#] When "invalidating" an element, `name` is transferred from 

2086 the element's "name" attribute to its "dupnames" attribute. 

2087 

2088 .. [#ex] If both "old" and "new" refer to identical URIs or 

2089 reference names, keep the old state and only invalidate "new". 

2090 

2091 __ https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html 

2092 #implicit-hyperlink-targets 

2093 

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 

2099 

2100 self.nametypes[name] = old_explicit or explicit 

2101 

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 

2146 

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`. 

2154 

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) 

2166 

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 

2170 

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) 

2177 

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) 

2183 

2184 def note_refname(self, node: Element) -> None: 

2185 self.refnames.setdefault(node['refname'], []).append(node) 

2186 

2187 def note_refid(self, node: Element) -> None: 

2188 self.refids.setdefault(node['refid'], []).append(node) 

2189 

2190 def note_indirect_target(self, target: target) -> None: 

2191 self.indirect_targets.append(target) 

2192 if target['names']: 

2193 self.note_refname(target) 

2194 

2195 def note_anonymous_target(self, target: target) -> None: 

2196 if getattr(self.settings, "legacy_ids", True): 

2197 self.set_id(target) 

2198 

2199 def note_autofootnote(self, footnote: footnote) -> None: 

2200 self.set_id(footnote) 

2201 self.autofootnotes.append(footnote) 

2202 

2203 def note_autofootnote_ref(self, ref: footnote_reference) -> None: 

2204 self.set_id(ref) 

2205 self.autofootnote_refs.append(ref) 

2206 

2207 def note_symbol_footnote(self, footnote: footnote) -> None: 

2208 self.set_id(footnote) 

2209 self.symbol_footnotes.append(footnote) 

2210 

2211 def note_symbol_footnote_ref(self, ref: footnote_reference) -> None: 

2212 self.set_id(ref) 

2213 self.symbol_footnote_refs.append(ref) 

2214 

2215 def note_footnote(self, footnote: footnote) -> None: 

2216 self.set_id(footnote) 

2217 self.footnotes.append(footnote) 

2218 

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) 

2223 

2224 def note_citation(self, citation: citation) -> None: 

2225 self.citations.append(citation) 

2226 

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) 

2231 

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 

2250 

2251 def note_substitution_ref(self, 

2252 subref: substitution_reference, 

2253 refname: str, 

2254 ) -> None: 

2255 subref['refname'] = whitespace_normalize_name(refname) 

2256 

2257 def note_pending( 

2258 self, pending: pending, priority: int | None = None) -> None: 

2259 self.transformer.add_pending(pending, priority) 

2260 

2261 def note_parse_message(self, message: system_message) -> None: 

2262 self.parse_messages.append(message) 

2263 

2264 def note_transform_message(self, message: system_message) -> None: 

2265 self.transform_messages.append(message) 

2266 

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 

2276 

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 

2283 

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 

2293 

2294 

2295# Bibliographic Elements 

2296# ====================== 

2297 

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) 

2307 

2308 

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?)+ 

2318 

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. 

2324 

2325 Provisional. 

2326 """ 

2327 relics = super().validate_content() 

2328 while relics: 

2329 relics = super().validate_content(elements=relics) 

2330 return relics 

2331 

2332 

2333# Body Elements 

2334# ============= 

2335# 

2336# General 

2337# ------- 

2338# 

2339# Miscellaneous Body Elements and related Body Subelements (Part) 

2340 

2341class paragraph(General, TextElement): pass 

2342class rubric(Titular, General, TextElement): pass 

2343 

2344 

2345class compound(General, Element): 

2346 content_model: Final = ((Body, '+'),) # (%body.elements;)+ 

2347 

2348 

2349class container(General, Element): 

2350 content_model: Final = ((Body, '+'),) # (%body.elements;)+ 

2351 

2352 

2353class attribution(Part, TextElement): 

2354 """Visible reference to the source of a `block_quote`.""" 

2355 

2356 

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?) 

2361 

2362 

2363class reference(General, Inline, Referential, TextElement): 

2364 valid_attributes: Final = Element.valid_attributes + ( 

2365 'anonymous', 'refid', 'refname', 'refuri') 

2366 

2367 

2368# Lists 

2369# ----- 

2370# 

2371# Lists (Sequential) and related Body Subelements (Part) 

2372 

2373class list_item(Part, Element): 

2374 content_model: Final = ((Body, '*'),) # (%body.elements;)* 

2375 

2376 

2377class bullet_list(Sequential, Element): 

2378 valid_attributes: Final = Element.valid_attributes + ('bullet',) 

2379 content_model: Final = ((list_item, '+'),) # (list_item+) 

2380 

2381 

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+) 

2386 

2387 

2388class term(Part, TextElement): pass 

2389class classifier(Part, TextElement): pass 

2390 

2391 

2392class definition(Part, Element): 

2393 """Definition of a `term` in a `definition_list`.""" 

2394 content_model: Final = ((Body, '+'),) # (%body.elements;)+ 

2395 

2396 

2397class definition_list_item(Part, Element): 

2398 content_model: Final = ((term, '.'), 

2399 ((classifier, term), '*'), 

2400 (definition, '.'), 

2401 ) 

2402 # ((term, classifier*)+, definition) 

2403 

2404 

2405class definition_list(Sequential, Element): 

2406 """List of terms and their definitions. 

2407 

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+) 

2413 

2414 

2415class field_name(Part, TextElement): pass 

2416 

2417 

2418class field_body(Part, Element): 

2419 content_model: Final = ((Body, '*'),) # (%body.elements;)* 

2420 

2421 

2422class field(Part, Bibliographic, Element): 

2423 content_model: Final = ((field_name, '.'), (field_body, '.')) 

2424 # (field_name, field_body) 

2425 

2426 

2427class field_list(Sequential, Element): 

2428 """List of label & data pairs. 

2429 

2430 Typically rendered as a two-column list. 

2431 Also used for extension syntax or special processing. 

2432 """ 

2433 content_model: Final = ((field, '+'),) # (field+) 

2434 

2435 

2436class option_string(Part, PureTextElement): 

2437 """A literal command-line option. Typically monospaced.""" 

2438 

2439 

2440class option_argument(Part, PureTextElement): 

2441 """Placeholder text for option arguments.""" 

2442 valid_attributes: Final = Element.valid_attributes + ('delimiter',) 

2443 

2444 def astext(self) -> str: 

2445 return self.get('delimiter', ' ') + TextElement.astext(self) 

2446 

2447 

2448class option(Part, Element): 

2449 """Option element in an `option_list_item`. 

2450 

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*) 

2456 

2457 

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+) 

2462 

2463 

2464class description(Part, Element): 

2465 """Describtion of a command-line option.""" 

2466 content_model: Final = ((Body, '+'),) # (%body.elements;)+ 

2467 

2468 

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) 

2475 

2476 

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+) 

2480 

2481 

2482# Pre-formatted text blocks 

2483# ------------------------- 

2484 

2485class literal_block(General, FixedTextElement): pass 

2486 

2487 

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) 

2495 

2496 

2497class math_block(General, FixedTextElement, PureTextElement): 

2498 """Mathematical notation (display formula).""" 

2499 

2500 

2501class line(Part, TextElement): 

2502 """Single line of text in a `line_block`.""" 

2503 indent: str | None = None 

2504 

2505 

2506class line_block(General, Element): 

2507 """Sequence of lines and nested line blocks. 

2508 """ 

2509 # recursive content model: (line | line_block)+ 

2510 

2511 

2512line_block.content_model = (((line, line_block), '+'),) 

2513 

2514 

2515# Admonitions 

2516# ----------- 

2517# distinctive and self-contained notices 

2518 

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 

2528 

2529 

2530class admonition(Admonition, Element): 

2531 content_model: Final = ((title, '.'), (Body, '+')) 

2532 # (title, (%body.elements;)+) 

2533 

2534 

2535# Footnote and citation 

2536# --------------------- 

2537 

2538class label(Part, PureTextElement): 

2539 """Visible identifier for footnotes and citations.""" 

2540 

2541 

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. 

2548 

2549 

2550class citation(General, BackLinkable, Element, Labeled, Targetable): 

2551 content_model: Final = ((label, '.'), (Body, '+')) 

2552 # (label, (%body.elements;)+) 

2553 

2554 

2555# Graphical elements 

2556# ------------------ 

2557 

2558class image(General, Inline, Element): 

2559 """Reference to an image resource. 

2560 

2561 May be body element or inline element. 

2562 """ 

2563 valid_attributes: Final = Element.valid_attributes + ( 

2564 'uri', 'alt', 'align', 'height', 'width', 'scale', 'loading') 

2565 

2566 def astext(self) -> str: 

2567 return self.get('alt', '') 

2568 

2569 

2570class caption(Part, TextElement): pass 

2571 

2572 

2573class legend(Part, Element): 

2574 """A wrapper for text accompanying a `figure` that is not the caption.""" 

2575 content_model: Final = ((Body, '+'),) # (%body.elements;)+ 

2576 

2577 

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]). 

2587 

2588 

2589# Tables 

2590# ------ 

2591 

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;)* 

2599 

2600 

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+ 

2605 

2606 

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') 

2612 

2613 def propwidth(self) -> int|float: 

2614 """Return numerical value of "colwidth__" attribute. Default 1. 

2615 

2616 Raise ValueError if "colwidth" is zero, negative, or a *fixed value*. 

2617 

2618 Provisional. 

2619 

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 

2635 

2636 

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+) 

2641 

2642 

2643class tbody(Part, Element): 

2644 """Body of a `tgroup`.""" 

2645 valid_attributes: Final = Element.valid_attributes + ('valign',) 

2646 content_model: Final = ((row, '+'),) # (row+) 

2647 

2648 

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) 

2655 

2656 

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+) 

2663 

2664 

2665# Inline Elements 

2666# =============== 

2667 

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 

2678 

2679 

2680class footnote_reference(Inline, Referential, PureTextElement): 

2681 valid_attributes: Final = Element.valid_attributes + ( 

2682 'auto', 'refid', 'refname') 

2683 

2684 

2685class citation_reference(Inline, Referential, PureTextElement): 

2686 valid_attributes: Final = Element.valid_attributes + ('refid', 'refname') 

2687 

2688 

2689class substitution_reference(Inline, TextElement): 

2690 valid_attributes: Final = Element.valid_attributes + ('refname',) 

2691 

2692 

2693class math(Inline, PureTextElement): 

2694 """Mathematical notation in running text.""" 

2695 

2696 

2697class problematic(Inline, TextElement): 

2698 valid_attributes: Final = Element.valid_attributes + ( 

2699 'refid', 'refname', 'refuri') 

2700 

2701 

2702# ======================================== 

2703# Auxiliary Classes, Functions, and Data 

2704# ======================================== 

2705 

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.""" 

2735 

2736 

2737class NodeVisitor: 

2738 """ 

2739 "Visitor" pattern [GoF95]_ abstract superclass implementation for 

2740 document tree traversals. 

2741 

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. 

2747 

2748 The dispatch methods call "``visit_`` + node class name" or 

2749 "``depart_`` + node class name", resp. 

2750 

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). 

2755 

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`. 

2759 

2760 .. [GoF95] Gamma, Helm, Johnson, Vlissides. *Design Patterns: Elements of 

2761 Reusable Object-Oriented Software*. Addison-Wesley, Reading, MA, USA, 

2762 1995. 

2763 """ 

2764 

2765 optional: ClassVar[tuple[str, ...]] = ('meta',) 

2766 """ 

2767 Tuple containing node class names (as strings). 

2768 

2769 No exception will be raised if writers do not implement visit 

2770 or departure functions for these node classes. 

2771 

2772 Used to ensure transitional compatibility with existing 3rd-party writers. 

2773 """ 

2774 

2775 def __init__(self, document: document, /) -> None: 

2776 self.document: document = document 

2777 

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) 

2790 

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) 

2803 

2804 def unknown_visit(self, node) -> None: 

2805 """ 

2806 Called when entering unknown `Node` types. 

2807 

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__)) 

2815 

2816 def unknown_departure(self, node) -> None: 

2817 """ 

2818 Called before exiting unknown `Node` types. 

2819 

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__)) 

2827 

2828 

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 """ 

2836 

2837 

2838class GenericNodeVisitor(NodeVisitor): 

2839 """ 

2840 Generic "Visitor" abstract superclass, for simple traversals. 

2841 

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. 

2846 

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. 

2850 

2851 `NodeVisitor.unknown_visit()` (`NodeVisitor.unknown_departure()`) should 

2852 be overridden for default behavior. 

2853 """ 

2854 

2855 def default_visit(self, node): 

2856 """Override for generic, uniform traversals.""" 

2857 raise NotImplementedError 

2858 

2859 def default_departure(self, node): 

2860 """Override for generic, uniform traversals.""" 

2861 raise NotImplementedError 

2862 

2863 

2864def _call_default_visit(self: GenericNodeVisitor, node) -> None: 

2865 self.default_visit(node) 

2866 

2867 

2868def _call_default_departure(self: GenericNodeVisitor, node) -> None: 

2869 self.default_departure(node) 

2870 

2871 

2872def _nop(self: SparseNodeVisitor, node) -> None: 

2873 pass 

2874 

2875 

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) 

2883 

2884 

2885_add_node_class_names(node_class_names) 

2886 

2887 

2888class TreeCopyVisitor(GenericNodeVisitor): 

2889 """ 

2890 Make a complete copy of a tree or branch, including element attributes. 

2891 """ 

2892 

2893 def __init__(self, document: document) -> None: 

2894 super().__init__(document) 

2895 self.parent_stack: list[list] = [] 

2896 self.parent: list = [] 

2897 

2898 def get_tree_copy(self): 

2899 return self.parent[0] 

2900 

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 

2907 

2908 def default_departure(self, node) -> None: 

2909 """Restore the previous acting parent.""" 

2910 self.parent = self.parent_stack.pop() 

2911 

2912 

2913# Custom Exceptions 

2914# ================= 

2915 

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 

2921 

2922 

2923class TreePruningException(Exception): 

2924 """ 

2925 Base class for `NodeVisitor`-related tree pruning exceptions. 

2926 

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 """ 

2931 

2932 

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 """ 

2938 

2939 

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 """ 

2945 

2946 

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 """ 

2952 

2953 

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 """ 

2959 

2960 

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 """ 

2967 

2968 

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 """ 

2977 

2978 

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 

2995 

2996 

2997def make_id(string: str) -> str: 

2998 """ 

2999 Convert `string` into an identifier and return it. 

3000 

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. 

3005 

3006 - The `HTML 4.01 spec`_ defines identifiers based on SGML tokens: 

3007 

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 ("."). 

3011 

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):: 

3015 

3016 unicode \\[0-9a-f]{1,4} 

3017 latin1 [&iexcl;-&yuml;] 

3018 escape {unicode}|\\[ -~&iexcl;-&yuml;] 

3019 nmchar [-a-z0-9]|{latin1}|{escape} 

3020 name {nmchar}+ 

3021 

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. 

3028 

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) 

3043 

3044 

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} 

3089 

3090 

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 

3097 

3098 

3099def fully_normalize_name(name: str) -> str: 

3100 """Return a case- and whitespace-normalized name.""" 

3101 return ' '.join(name.lower().split()) 

3102 

3103 

3104def whitespace_normalize_name(name: str) -> str: 

3105 """Return a whitespace-normalized name.""" 

3106 return ' '.join(name.split()) 

3107 

3108 

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'\ ') 

3112 

3113 

3114def split_name_list(s: str) -> list[str]: 

3115 r"""Split a string at non-escaped whitespace. 

3116 

3117 Backslashes escape internal whitespace (cf. `serial_escape()`). 

3118 Return list of "names" (after removing escaping backslashes). 

3119 

3120 >>> split_name_list(r'a\ n\ame two\\ n\\ames'), 

3121 ['a name', 'two\\', r'n\ames'] 

3122 

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] 

3132 

3133 

3134def pseudo_quoteattr(value: str) -> str: 

3135 """Quote attributes for pseudo-xml""" 

3136 return '"%s"' % value 

3137 

3138 

3139def parse_measure(measure: str, unit_pattern: str = '[a-zA-Zµ]*|%?' 

3140 ) -> tuple[int|float, str]: 

3141 """Parse a measure__, return value + unit. 

3142 

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. 

3147 

3148 __ https://docutils.sourceforge.io/docs/ref/doctree.html#measure 

3149 

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 

3162 

3163 

3164# Methods to validate `Element attribute`__ values. 

3165 

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 

3177 

3178def create_keyword_validator(*keywords: str) -> Callable[[str], str]: 

3179 """ 

3180 Return a function that validates a `str` against given `keywords`. 

3181 

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 

3190 

3191 

3192def validate_identifier(value: str) -> str: 

3193 """ 

3194 Validate identifier key or class name. 

3195 

3196 Used in `idref.type`__ and for the tokens in `validate_identifier_list()`. 

3197 

3198 __ https://docutils.sourceforge.io/docs/ref/doctree.html#idref-type 

3199 

3200 Provisional. 

3201 """ 

3202 if value != make_id(value): 

3203 raise ValueError(f'"{value}" is no valid id or class name.') 

3204 return value 

3205 

3206 

3207def validate_identifier_list(value: str | list[str]) -> list[str]: 

3208 """ 

3209 A (space-separated) list of ids or class names. 

3210 

3211 `value` may be a `list` or a `str` with space separated 

3212 ids or class names (cf. `validate_identifier()`). 

3213 

3214 Used in `classnames.type`__, `ids.type`__, and `idrefs.type`__. 

3215 

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 

3219 

3220 Provisional. 

3221 """ 

3222 if isinstance(value, str): 

3223 value = value.split() 

3224 for token in value: 

3225 validate_identifier(token) 

3226 return value 

3227 

3228 

3229def validate_measure(measure: str) -> str: 

3230 """ 

3231 Validate a measure__ (number + optional unit).  Return normalized `str`. 

3232 

3233 See `parse_measure()` for a function returning a "number + unit" tuple. 

3234 

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. 

3237 

3238 Provisional. 

3239 

3240 __ https://docutils.sourceforge.io/docs/ref/doctree.html#measure 

3241 """ 

3242 value, unit = parse_measure(measure) 

3243 return f'{value}{unit}' 

3244 

3245 

3246def validate_colwidth(measure: str|int|float) -> str: 

3247 """Validate the "colwidth__" attribute. 

3248 

3249 Provisional: 

3250 Accept fixed length units in Docutils 2.0. 

3251 The default unit will change to "pt" in Docutils 3.0. 

3252 

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}' 

3267 

3268 

3269def validate_NMTOKEN(value: str) -> str: 

3270 """ 

3271 Validate a "name token": a `str` of ASCII letters, digits, and [-._]. 

3272 

3273 Provisional. 

3274 """ 

3275 if not re.fullmatch('[-._A-Za-z0-9]+', value): 

3276 raise ValueError(f'"{value}" is no NMTOKEN.') 

3277 return value 

3278 

3279 

3280def validate_NMTOKENS(value: str | list[str]) -> list[str]: 

3281 """ 

3282 Validate a list of "name tokens". 

3283 

3284 Provisional. 

3285 """ 

3286 if isinstance(value, str): 

3287 value = value.split() 

3288 for token in value: 

3289 validate_NMTOKEN(token) 

3290 return value 

3291 

3292 

3293def validate_refname_list(value: str | list[str]) -> list[str]: 

3294 """ 

3295 Validate a list of `reference names`__. 

3296 

3297 Reference names may contain all characters; 

3298 whitespace is normalized (cf, `whitespace_normalize_name()`). 

3299 

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()`). 

3303 

3304 Return a list of whitespace-normalized, unescaped reference names. 

3305 

3306 Provisional. 

3307 

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] 

3313 

3314 

3315def validate_yesorno(value: str | int | bool) -> bool: 

3316 """Validate a `%yesorno`__ (flag) value. 

3317 

3318 The string literal "0" evaluates to ``False``, all other 

3319 values are converterd with `bool()`. 

3320 

3321 __ https://docutils.sourceforge.io/docs/ref/doctree.html#yesorno 

3322 """ 

3323 if value == "0": 

3324 return False 

3325 return bool(value) 

3326 

3327 

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. 

3390 

3391Provisional. 

3392 

3393__ https://docutils.sourceforge.io/docs/ref/doctree.html#attribute-reference 

3394"""