Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/docutils/nodes.py: 58%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1270 statements  

1# $Id: nodes.py 10391 2026-07-22 20:59:24Z milde $ 

2# Author: David Goodger <goodger@python.org> 

3# Maintainer: docutils-develop@lists.sourceforge.net 

4# Copyright: This module has been placed in the public domain. 

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.Document | minidom.Element | minidom.Text: 

122 # TODO: minidom.Document is only returned by document.asdom() 

123 # (which overwrites this base-class implementation) 

124 """Return a DOM **fragment** representation of this Node.""" 

125 if dom is None: 

126 import xml.dom.minidom as dom 

127 domroot = dom.Document() 

128 return self._dom_node(domroot) 

129 

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

131 """ 

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

133 

134 Override in subclasses. 

135 """ 

136 raise NotImplementedError 

137 

138 def copy(self) -> Self: 

139 """Return a copy of self.""" 

140 raise NotImplementedError 

141 

142 def deepcopy(self) -> Self: 

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

144 raise NotImplementedError 

145 

146 def astext(self) -> str: 

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

148 raise NotImplementedError 

149 

150 def setup_child(self, child) -> None: 

151 child.parent = self 

152 if self.document: 

153 child.document = self.document 

154 if child.source is None: 

155 child.source = self.document.current_source 

156 if child.line is None: 

157 child.line = self.document.current_line 

158 

159 def walk(self, visitor: NodeVisitor) -> bool: 

160 """ 

161 Traverse a tree of `Node` objects, calling the 

162 `dispatch_visit()` method of `visitor` when entering each 

163 node. (The `walkabout()` method is similar, except it also 

164 calls the `dispatch_departure()` method before exiting each 

165 node.) 

166 

167 This tree traversal supports limited in-place tree 

168 modifications. Replacing one node with one or more nodes is 

169 OK, as is removing an element. However, if the node removed 

170 or replaced occurs after the current node, the old node will 

171 still be traversed, and any new nodes will not. 

172 

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

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

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

176 

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

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

179 

180 Return true if we should stop the traversal. 

181 """ 

182 stop = False 

183 visitor.document.reporter.debug( 

184 'docutils.nodes.Node.walk calling dispatch_visit for %s' 

185 % self.__class__.__name__) 

186 try: 

187 try: 

188 visitor.dispatch_visit(self) 

189 except (SkipChildren, SkipNode): 

190 return stop 

191 except SkipDeparture: # not applicable; ignore 

192 pass 

193 children = self.children 

194 try: 

195 for child in children[:]: 

196 if child.walk(visitor): 

197 stop = True 

198 break 

199 except SkipSiblings: 

200 pass 

201 except StopTraversal: 

202 stop = True 

203 return stop 

204 

205 def walkabout(self, visitor: NodeVisitor) -> bool: 

206 """ 

207 Perform a tree traversal similarly to `Node.walk()` (which 

208 see), except also call the `dispatch_departure()` method 

209 before exiting each node. 

210 

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

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

213 subclass encountered. 

214 

215 Return true if we should stop the traversal. 

216 """ 

217 call_depart = True 

218 stop = False 

219 visitor.document.reporter.debug( 

220 'docutils.nodes.Node.walkabout calling dispatch_visit for %s' 

221 % self.__class__.__name__) 

222 try: 

223 try: 

224 visitor.dispatch_visit(self) 

225 except SkipNode: 

226 return stop 

227 except SkipDeparture: 

228 call_depart = False 

229 children = self.children 

230 try: 

231 for child in children[:]: 

232 if child.walkabout(visitor): 

233 stop = True 

234 break 

235 except SkipSiblings: 

236 pass 

237 except SkipChildren: 

238 pass 

239 except StopTraversal: 

240 stop = True 

241 if call_depart: 

242 visitor.document.reporter.debug( 

243 'docutils.nodes.Node.walkabout calling dispatch_departure ' 

244 'for %s' % self.__class__.__name__) 

245 visitor.dispatch_departure(self) 

246 return stop 

247 

248 def _fast_findall(self, cls: type|tuple[type]) -> Iterator: 

249 """Return iterator that only supports instance checks.""" 

250 if isinstance(self, cls): 

251 yield self 

252 for child in self.children: 

253 yield from child._fast_findall(cls) 

254 

255 def _superfast_findall(self) -> Iterator: 

256 """Return iterator that doesn't check for a condition.""" 

257 # This is different from ``iter(self)`` implemented via 

258 # __getitem__() and __len__() in the Element subclass, 

259 # which yields only the direct children. 

260 yield self 

261 for child in self.children: 

262 yield from child._superfast_findall() 

263 

264 def findall(self, 

265 condition: type|tuple[type]|Callable[[Node], bool]|None = None, 

266 include_self: bool = True, 

267 descend: bool = True, 

268 siblings: bool = False, 

269 ascend: bool = False, 

270 ) -> Iterator: 

271 """ 

272 Return an iterator yielding nodes following `self`: 

273 

274 * self (if `include_self` is true) 

275 * all descendants in tree traversal order (if `descend` is true) 

276 * the following siblings (if `siblings` is true) and their 

277 descendants (if also `descend` is true) 

278 * the following siblings of the parent (if `ascend` is true) and 

279 their descendants (if also `descend` is true), and so on. 

280 

281 If `condition` is not None, the iterator yields only nodes 

282 for which ``condition(node)`` is true. 

283 If `condition` is a type (or tuple of types) ``cls``, it is equivalent 

284 to a function consisting of ``return isinstance(node, cls)``. 

285 

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

287 

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

289 is undefined. 

290 

291 For example, given the following tree:: 

292 

293 <paragraph> 

294 <emphasis> <--- emphasis.traverse() and 

295 <strong> <--- strong.traverse() are called. 

296 Foo 

297 Bar 

298 <reference name="Baz" refid="baz"> 

299 Baz 

300 

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

302 

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

304 

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

306 

307 [<strong>, <#text: Foo>, <#text: Bar>, <reference>, <#text: Baz>] 

308 """ 

309 if ascend: 

310 siblings = True 

311 # Check for special argument combinations that allow using an 

312 # optimized version of traverse() 

313 if include_self and descend and not siblings: 

314 if condition is None: 

315 yield from self._superfast_findall() 

316 return 

317 elif isinstance(condition, (type, tuple)): 

318 yield from self._fast_findall(condition) 

319 return 

320 # Check if `condition` is a class (check for TypeType for Python 

321 # implementations that use only new-style classes, like PyPy). 

322 if isinstance(condition, (type, tuple)): 

323 class_or_tuple = condition 

324 

325 def condition(node, class_or_tuple=class_or_tuple): 

326 return isinstance(node, class_or_tuple) 

327 

328 if include_self and (condition is None or condition(self)): 

329 yield self 

330 if descend and len(self.children): 

331 for child in self: 

332 yield from child.findall(condition=condition, 

333 include_self=True, descend=True, 

334 siblings=False, ascend=False) 

335 if siblings or ascend: 

336 node = self 

337 while node.parent: 

338 index = node.parent.index(node) 

339 # extra check since Text nodes have value-equality 

340 while node.parent[index] is not node: 

341 index = node.parent.index(node, index + 1) 

342 for sibling in node.parent[index+1:]: 

343 yield from sibling.findall( 

344 condition=condition, 

345 include_self=True, descend=descend, 

346 siblings=False, ascend=False) 

347 if not ascend: 

348 break 

349 else: 

350 node = node.parent 

351 

352 def traverse( 

353 self, 

354 condition: type|tuple[type]|Callable[[Node], bool]|None = None, 

355 include_self: bool = True, 

356 descend: bool = True, 

357 siblings: bool = False, 

358 ascend: bool = False, 

359 ) -> list: 

360 """Return list of nodes following `self`. 

361 

362 For looping, Node.findall() is faster and more memory efficient. 

363 """ 

364 # traverse() may be eventually removed: 

365 warnings.warn('nodes.Node.traverse() is obsoleted by Node.findall().', 

366 DeprecationWarning, stacklevel=2) 

367 return list(self.findall(condition, include_self, descend, 

368 siblings, ascend)) 

369 

370 def next_node( 

371 self, 

372 condition: type|tuple[type]|Callable[[Node], bool]|None = None, 

373 include_self: bool = False, 

374 descend: bool = True, 

375 siblings: bool = False, 

376 ascend: bool = False, 

377 ) -> Node | None: 

378 """ 

379 Return the first node in the iterator returned by findall(), 

380 or None if the iterable is empty. 

381 

382 Parameter list is the same as of `findall()`. Note that `include_self` 

383 defaults to False, though. 

384 """ 

385 try: 

386 return next(self.findall(condition, include_self, 

387 descend, siblings, ascend)) 

388 except StopIteration: 

389 return None 

390 

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

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

393 

394 Override in subclasses that define validity constraints. 

395 """ 

396 

397 def validate_position(self) -> None: 

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

399 

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

401 

402 Override in subclasses with complex validity constraints. See 

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

404 """ 

405 

406 

407class Text(Node, str): # NoQA: SLOT000 (Node doesn't define __slots__) 

408 """ 

409 Instances are terminal nodes (leaves) containing text only; no child 

410 nodes or attributes. Initialize by passing a string to the constructor. 

411 

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

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

414 """ 

415 

416 tagname: Final = '#text' 

417 

418 children: Final = () 

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

420 

421 def __new__(cls, data: str, rawsource: None = None) -> Self: 

422 """Assert that `data` is not an array of bytes 

423 and warn if the deprecated `rawsource` argument is used. 

424 """ 

425 if isinstance(data, bytes): 

426 raise TypeError('expecting str data, not bytes') 

427 if rawsource is not None: 

428 warnings.warn('nodes.Text: initialization argument "rawsource" ' 

429 'is ignored and will be removed in Docutils 2.0.', 

430 DeprecationWarning, stacklevel=2) 

431 return str.__new__(cls, data) 

432 

433 def shortrepr(self, maxlen: int = 18) -> str: 

434 data = self 

435 if len(data) > maxlen: 

436 data = data[:maxlen-4] + ' ...' 

437 return '<%s: %r>' % (self.tagname, str(data)) 

438 

439 def __repr__(self) -> str: 

440 return self.shortrepr(maxlen=68) 

441 

442 def astext(self) -> str: 

443 return str(unescape(self)) 

444 

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

446 return domroot.createTextNode(str(self)) 

447 

448 def copy(self) -> Self: 

449 return self.__class__(str(self)) 

450 

451 def deepcopy(self) -> Self: 

452 return self.copy() 

453 

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

455 try: 

456 if self.document.settings.detailed: 

457 tag = '%s%s' % (indent*level, '<#text>') 

458 lines = (indent*(level+1) + repr(line) 

459 for line in self.splitlines(True)) 

460 return '\n'.join((tag, *lines)) + '\n' 

461 except AttributeError: 

462 pass 

463 indent = indent * level 

464 lines = [indent+line for line in self.astext().splitlines()] 

465 if not lines: 

466 return '' 

467 return '\n'.join(lines) + '\n' 

468 

469 # rstrip and lstrip are used by substitution definitions where 

470 # they are expected to return a Text instance, this was formerly 

471 # taken care of by UserString. 

472 

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

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

475 

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

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

478 

479 

480class Element(Node): 

481 """ 

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

483 

484 Elements contain attributes and child nodes. 

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

486 

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

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

489 

490 element['att'] = 'value' 

491 

492 .. [#] External attributes correspond to the XML element attributes. 

493 From its `Node` superclass, Element also inherits "internal" 

494 class attributes that are accessed using the standard syntax, e.g. 

495 ``element.parent``. 

496 

497 There are two special attributes: 'ids' and 'names'. Both are 

498 lists of unique identifiers: 'ids' conform to the regular expression 

499 ``[a-z](-?[a-z0-9]+)*`` (see the make_id() function for rationale and 

500 details). 'names' serve as user-friendly interfaces to IDs; they are 

501 case- and whitespace-normalized (see the fully_normalize_name() function). 

502 

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

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

505 

506 element[0] 

507 

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

509 

510 for child in element: 

511 ... 

512 

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

514 child node to element, do:: 

515 

516 element += node 

517 

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

519 

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

521 operator:: 

522 

523 element += [node1, node2] 

524 

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

526 """ 

527 

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

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

530 

531 NOTE: Derived classes should update this value when supporting 

532 additional list attributes. 

533 """ 

534 

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

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

537 

538 NOTE: Derived classes should update this value when supporting 

539 additional attributes. 

540 """ 

541 

542 common_attributes: Final = valid_attributes 

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

544 

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

546 """ 

547 

548 known_attributes: Final = common_attributes 

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

550 

551 basic_attributes: Final = list_attributes 

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

553 

554 local_attributes: Final = ('backrefs',) 

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

556 

557 content_model: ClassVar[_ContentModelTuple] = () 

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

559 

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

561 

562 :category: class or tuple of classes that are expected at this place(s) 

563 in the list of children 

564 :quantifier: string representation stating how many elements 

565 of `category` are expected. Value is one of: 

566 '.' (exactly one), '?' (zero or one), 

567 '+' (one or more), '*' (zero or more). 

568 

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

570 update this value to match their content model. 

571 

572 Provisional. 

573 """ 

574 

575 tagname: str | None = None 

576 """The element generic identifier. 

577 

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

579 """ 

580 

581 child_text_separator: Final = '\n\n' 

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

583 

584 def __init__(self, 

585 rawsource: str = '', 

586 *children, 

587 **attributes: Any, 

588 ) -> None: 

589 self.rawsource = rawsource 

590 """The raw text from which this element was constructed. 

591 

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

593 

594 NOTE: some elements do not set this value (default ''). 

595 """ 

596 if isinstance(rawsource, Element): 

597 raise TypeError('First argument "rawsource" must be a string.') 

598 

599 self.children: list = [] 

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

601 

602 self.extend(children) # maintain parent info 

603 

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

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

606 

607 # Initialize list attributes. 

608 for att in self.list_attributes: 

609 self.attributes[att] = [] 

610 

611 for att, value in attributes.items(): 

612 att = att.lower() # normalize attribute name 

613 if att in self.list_attributes: 

614 # lists are mutable; make a copy for this node 

615 self.attributes[att] = value[:] 

616 else: 

617 self.attributes[att] = value 

618 

619 if self.tagname is None: 

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

621 

622 def _dom_node(self, domroot: minidom.Document) -> minidom.Element: 

623 element = domroot.createElement(self.tagname) 

624 for attribute, value in self.attlist(): 

625 if isinstance(value, list): 

626 value = ' '.join(serial_escape('%s' % (v,)) for v in value) 

627 element.setAttribute(attribute, '%s' % value) 

628 for child in self.children: 

629 element.appendChild(child._dom_node(domroot)) 

630 return element 

631 

632 def __repr__(self) -> str: 

633 data = '' 

634 for c in self.children: 

635 data += c.shortrepr() 

636 if len(data) > 60: 

637 data = data[:56] + ' ...' 

638 break 

639 if self['names']: 

640 return '<%s "%s": %s>' % (self.tagname, 

641 '; '.join(self['names']), data) 

642 else: 

643 return '<%s: %s>' % (self.tagname, data) 

644 

645 def shortrepr(self) -> str: 

646 if self['names']: 

647 return '<%s "%s"...>' % (self.tagname, '; '.join(self['names'])) 

648 else: 

649 return '<%s...>' % self.tagname 

650 

651 def __str__(self) -> str: 

652 if self.children: 

653 return '%s%s%s' % (self.starttag(), 

654 ''.join(str(c) for c in self.children), 

655 self.endtag()) 

656 else: 

657 return self.emptytag() 

658 

659 def starttag(self, quoteattr: Callable[[str], str] | None = None) -> str: 

660 # the optional arg is used by the docutils_xml writer 

661 if quoteattr is None: 

662 quoteattr = pseudo_quoteattr 

663 parts = [self.tagname] 

664 for name, value in self.attlist(): 

665 if value is None: # boolean attribute 

666 parts.append('%s="True"' % name) 

667 continue 

668 if isinstance(value, bool): 

669 value = str(int(value)) 

670 if isinstance(value, list): 

671 values = [serial_escape('%s' % (v,)) for v in value] 

672 value = ' '.join(values) 

673 else: 

674 value = str(value) 

675 value = quoteattr(value) 

676 parts.append('%s=%s' % (name, value)) 

677 return '<%s>' % ' '.join(parts) 

678 

679 def endtag(self) -> str: 

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

681 

682 def emptytag(self) -> str: 

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

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

685 

686 def __len__(self) -> int: 

687 return len(self.children) 

688 

689 def __contains__(self, key: str | Node) -> bool: 

690 # Test for both, children and attributes with operator ``in``. 

691 # 

692 # Caution: Document Tree traversal also returns Text nodes where 

693 # ``in`` looks for substrings in the text content. 

694 if isinstance(key, str): 

695 return key in self.attributes 

696 return key in self.children 

697 

698 def __getitem__(self, key: str | int | slice) -> Any: 

699 if isinstance(key, str): 

700 return self.attributes[key] 

701 elif isinstance(key, int): 

702 return self.children[key] 

703 elif isinstance(key, slice): 

704 assert key.step in (None, 1), 'cannot handle slice with stride' 

705 return self.children[key.start:key.stop] 

706 else: 

707 raise TypeError('element index must be an integer, a slice, or ' 

708 'an attribute name string') 

709 

710 def __setitem__(self, key, item) -> None: 

711 if isinstance(key, str): 

712 self.attributes[str(key)] = item 

713 elif isinstance(key, int): 

714 self.setup_child(item) 

715 self.children[key] = item 

716 elif isinstance(key, slice): 

717 assert key.step in (None, 1), 'cannot handle slice with stride' 

718 for node in item: 

719 self.setup_child(node) 

720 self.children[key.start:key.stop] = item 

721 else: 

722 raise TypeError('element index must be an integer, a slice, or ' 

723 'an attribute name string') 

724 

725 def __delitem__(self, key: str | int | slice) -> None: 

726 if isinstance(key, str): 

727 del self.attributes[key] 

728 elif isinstance(key, int): 

729 del self.children[key] 

730 elif isinstance(key, slice): 

731 assert key.step in (None, 1), 'cannot handle slice with stride' 

732 del self.children[key.start:key.stop] 

733 else: 

734 raise TypeError('element index must be an integer, a simple ' 

735 'slice, or an attribute name string') 

736 

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

738 return self.children + other 

739 

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

741 return other + self.children 

742 

743 def __iadd__(self, other) -> Self: 

744 """Append a node or a list of nodes to `self.children`.""" 

745 if isinstance(other, Node): 

746 self.append(other) 

747 elif other is not None: 

748 self.extend(other) 

749 return self 

750 

751 def astext(self) -> str: 

752 return self.child_text_separator.join( 

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

754 

755 def non_default_attributes(self) -> dict[str, Any]: 

756 atts = {key: value for key, value in self.attributes.items() 

757 if self.is_not_default(key)} 

758 return atts 

759 

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

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

762 

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

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

765 

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

767 return attr in self.attributes 

768 

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

770 if attr in self.attributes: 

771 del self.attributes[attr] 

772 

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

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

775 

776 has_key = hasattr 

777 

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

779 """Return node's language tag. 

780 

781 Look iteratively in self and parents for a class argument 

782 starting with ``language-`` and return the remainder of it 

783 (which should be a `BCP49` language tag) or the `fallback`. 

784 """ 

785 for cls in self.get('classes', []): 

786 if cls.startswith('language-'): 

787 return cls.removeprefix('language-') 

788 try: 

789 return self.parent.get_language_code(fallback) 

790 except AttributeError: 

791 return fallback 

792 

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

794 self.setup_child(item) 

795 self.children.append(item) 

796 

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

798 for node in item: 

799 self.append(node) 

800 

801 def insert(self, index: SupportsIndex, item) -> None: 

802 if isinstance(item, Node): 

803 self.setup_child(item) 

804 self.children.insert(index, item) 

805 elif item is not None: 

806 self[index:index] = item 

807 

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

809 return self.children.pop(i) 

810 

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

812 self.children.remove(item) 

813 

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

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

816 

817 def previous_sibling(self): 

818 """Return preceding sibling node or ``None``.""" 

819 try: 

820 i = self.parent.index(self) 

821 except (AttributeError): 

822 return None 

823 return self.parent[i-1] if i > 0 else None 

824 

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

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

827 

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

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

830 

831 List item ``[i]`` is the parent <section> of level i+1 

832 (1: section, 2: subsection, 3: subsubsection, ...). 

833 The length of the list is the element's section level. 

834 

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

836 for a usage example. 

837 

838 Provisional. May be changed or removed without warning. 

839 """ 

840 sections = [] 

841 node = self 

842 while node.parent is not None: 

843 if isinstance(node, section): 

844 sections.append(node) 

845 node = node.parent 

846 sections.reverse() 

847 return sections 

848 

849 def is_not_default(self, key: str) -> bool: 

850 if self[key] == [] and key in self.list_attributes: 

851 return False 

852 else: 

853 return True 

854 

855 def update_basic_atts(self, dict_: Mapping[str, Any] | Element) -> None: 

856 """ 

857 Update basic attributes ('ids', 'names', 'classes', 

858 'dupnames', but not 'source') from node or dictionary `dict_`. 

859 

860 Provisional. 

861 """ 

862 if isinstance(dict_, Node): 

863 dict_ = dict_.attributes 

864 for att in self.basic_attributes: 

865 self.append_attr_list(att, dict_.get(att, [])) 

866 

867 def append_attr_list(self, attr: str, values: Iterable[Any]) -> None: 

868 """ 

869 For each element in values, if it does not exist in self[attr], append 

870 it. 

871 

872 NOTE: Requires self[attr] and values to be sequence type and the 

873 former should specifically be a list. 

874 """ 

875 # List Concatenation 

876 for value in values: 

877 if value not in self[attr]: 

878 self[attr].append(value) 

879 

880 def coerce_append_attr_list( 

881 self, attr: str, value: list[Any] | Any) -> None: 

882 """ 

883 First, convert both self[attr] and value to a non-string sequence 

884 type; if either is not already a sequence, convert it to a list of one 

885 element. Then call append_attr_list. 

886 

887 NOTE: self[attr] and value both must not be None. 

888 """ 

889 # List Concatenation 

890 if not isinstance(self.get(attr), list): 

891 self[attr] = [self[attr]] 

892 if not isinstance(value, list): 

893 value = [value] 

894 self.append_attr_list(attr, value) 

895 

896 def replace_attr(self, attr: str, value: Any, force: bool = True) -> None: 

897 """ 

898 If self[attr] does not exist or force is True or omitted, set 

899 self[attr] to value, otherwise do nothing. 

900 """ 

901 # One or the other 

902 if force or self.get(attr) is None: 

903 self[attr] = value 

904 

905 def copy_attr_convert( 

906 self, attr: str, value: Any, replace: bool = True) -> None: 

907 """ 

908 If attr is an attribute of self, set self[attr] to 

909 [self[attr], value], otherwise set self[attr] to value. 

910 

911 NOTE: replace is not used by this function and is kept only for 

912 compatibility with the other copy functions. 

913 """ 

914 if self.get(attr) is not value: 

915 self.coerce_append_attr_list(attr, value) 

916 

917 def copy_attr_coerce(self, attr: str, value: Any, replace: bool) -> None: 

918 """ 

919 If attr is an attribute of self and either self[attr] or value is a 

920 list, convert all non-sequence values to a sequence of 1 element and 

921 then concatenate the two sequence, setting the result to self[attr]. 

922 If both self[attr] and value are non-sequences and replace is True or 

923 self[attr] is None, replace self[attr] with value. Otherwise, do 

924 nothing. 

925 """ 

926 if self.get(attr) is not value: 

927 if isinstance(self.get(attr), list) or \ 

928 isinstance(value, list): 

929 self.coerce_append_attr_list(attr, value) 

930 else: 

931 self.replace_attr(attr, value, replace) 

932 

933 def copy_attr_concatenate( 

934 self, attr: str, value: Any, replace: bool) -> None: 

935 """ 

936 If attr is an attribute of self and both self[attr] and value are 

937 lists, concatenate the two sequences, setting the result to 

938 self[attr]. If either self[attr] or value are non-sequences and 

939 replace is True or self[attr] is None, replace self[attr] with value. 

940 Otherwise, do nothing. 

941 """ 

942 if self.get(attr) is not value: 

943 if isinstance(self.get(attr), list) and \ 

944 isinstance(value, list): 

945 self.append_attr_list(attr, value) 

946 else: 

947 self.replace_attr(attr, value, replace) 

948 

949 def copy_attr_consistent( 

950 self, attr: str, value: Any, replace: bool) -> None: 

951 """ 

952 If replace is True or self[attr] is None, replace self[attr] with 

953 value. Otherwise, do nothing. 

954 """ 

955 if self.get(attr) is not value: 

956 self.replace_attr(attr, value, replace) 

957 

958 def update_all_atts(self, 

959 dict_: Mapping[str, Any] | Element, 

960 update_fun: _UpdateFun = copy_attr_consistent, 

961 replace: bool = True, 

962 and_source: bool = False, 

963 ) -> None: 

964 """ 

965 Updates all attributes from node or dictionary `dict_`. 

966 

967 Appends the basic attributes ('ids', 'names', 'classes', 

968 'dupnames', but not 'source') and then, for all other attributes in 

969 dict_, updates the same attribute in self. When attributes with the 

970 same identifier appear in both self and dict_, the two values are 

971 merged based on the value of update_fun. Generally, when replace is 

972 True, the values in self are replaced or merged with the values in 

973 dict_; otherwise, the values in self may be preserved or merged. When 

974 and_source is True, the 'source' attribute is included in the copy. 

975 

976 NOTE: When replace is False, and self contains a 'source' attribute, 

977 'source' is not replaced even when dict_ has a 'source' 

978 attribute, though it may still be merged into a list depending 

979 on the value of update_fun. 

980 NOTE: It is easier to call the update-specific methods then to pass 

981 the update_fun method to this function. 

982 """ 

983 if isinstance(dict_, Node): 

984 dict_ = dict_.attributes 

985 

986 # Include the source attribute when copying? 

987 if and_source: 

988 filter_fun = self.is_not_list_attribute 

989 else: 

990 filter_fun = self.is_not_known_attribute 

991 

992 # Copy the basic attributes 

993 self.update_basic_atts(dict_) 

994 

995 # Grab other attributes in dict_ not in self except the 

996 # (All basic attributes should be copied already) 

997 for att in filter(filter_fun, dict_): 

998 update_fun(self, att, dict_[att], replace) 

999 

1000 def update_all_atts_consistantly(self, 

1001 dict_: Mapping[str, Any] | Element, 

1002 replace: bool = True, 

1003 and_source: bool = False, 

1004 ) -> None: 

1005 """ 

1006 Updates all attributes from node or dictionary `dict_`. 

1007 

1008 Appends the basic attributes ('ids', 'names', 'classes', 

1009 'dupnames', but not 'source') and then, for all other attributes in 

1010 dict_, updates the same attribute in self. When attributes with the 

1011 same identifier appear in both self and dict_ and replace is True, the 

1012 values in self are replaced with the values in dict_; otherwise, the 

1013 values in self are preserved. When and_source is True, the 'source' 

1014 attribute is included in the copy. 

1015 

1016 NOTE: When replace is False, and self contains a 'source' attribute, 

1017 'source' is not replaced even when dict_ has a 'source' 

1018 attribute, though it may still be merged into a list depending 

1019 on the value of update_fun. 

1020 """ 

1021 self.update_all_atts(dict_, Element.copy_attr_consistent, replace, 

1022 and_source) 

1023 

1024 def update_all_atts_concatenating(self, 

1025 dict_: Mapping[str, Any] | Element, 

1026 replace: bool = True, 

1027 and_source: bool = False, 

1028 ) -> None: 

1029 """ 

1030 Updates all attributes from node or dictionary `dict_`. 

1031 

1032 Appends the basic attributes ('ids', 'names', 'classes', 

1033 'dupnames', but not 'source') and then, for all other attributes in 

1034 dict_, updates the same attribute in self. When attributes with the 

1035 same identifier appear in both self and dict_ whose values aren't each 

1036 lists and replace is True, the values in self are replaced with the 

1037 values in dict_; if the values from self and dict_ for the given 

1038 identifier are both of list type, then the two lists are concatenated 

1039 and the result stored in self; otherwise, the values in self are 

1040 preserved. When and_source is True, the 'source' attribute is 

1041 included in the copy. 

1042 

1043 NOTE: When replace is False, and self contains a 'source' attribute, 

1044 'source' is not replaced even when dict_ has a 'source' 

1045 attribute, though it may still be merged into a list depending 

1046 on the value of update_fun. 

1047 """ 

1048 self.update_all_atts(dict_, Element.copy_attr_concatenate, replace, 

1049 and_source) 

1050 

1051 def update_all_atts_coercion(self, 

1052 dict_: Mapping[str, Any] | Element, 

1053 replace: bool = True, 

1054 and_source: bool = False, 

1055 ) -> None: 

1056 """ 

1057 Updates all attributes from node or dictionary `dict_`. 

1058 

1059 Appends the basic attributes ('ids', 'names', 'classes', 

1060 'dupnames', but not 'source') and then, for all other attributes in 

1061 dict_, updates the same attribute in self. When attributes with the 

1062 same identifier appear in both self and dict_ whose values are both 

1063 not lists and replace is True, the values in self are replaced with 

1064 the values in dict_; if either of the values from self and dict_ for 

1065 the given identifier are of list type, then first any non-lists are 

1066 converted to 1-element lists and then the two lists are concatenated 

1067 and the result stored in self; otherwise, the values in self are 

1068 preserved. When and_source is True, the 'source' attribute is 

1069 included in the copy. 

1070 

1071 NOTE: When replace is False, and self contains a 'source' attribute, 

1072 'source' is not replaced even when dict_ has a 'source' 

1073 attribute, though it may still be merged into a list depending 

1074 on the value of update_fun. 

1075 """ 

1076 self.update_all_atts(dict_, Element.copy_attr_coerce, replace, 

1077 and_source) 

1078 

1079 def update_all_atts_convert(self, 

1080 dict_: Mapping[str, Any] | Element, 

1081 and_source: bool = False, 

1082 ) -> None: 

1083 """ 

1084 Updates all attributes from node or dictionary `dict_`. 

1085 

1086 Appends the basic attributes ('ids', 'names', 'classes', 

1087 'dupnames', but not 'source') and then, for all other attributes in 

1088 dict_, updates the same attribute in self. When attributes with the 

1089 same identifier appear in both self and dict_ then first any non-lists 

1090 are converted to 1-element lists and then the two lists are 

1091 concatenated and the result stored in self; otherwise, the values in 

1092 self are preserved. When and_source is True, the 'source' attribute 

1093 is included in the copy. 

1094 

1095 NOTE: When replace is False, and self contains a 'source' attribute, 

1096 'source' is not replaced even when dict_ has a 'source' 

1097 attribute, though it may still be merged into a list depending 

1098 on the value of update_fun. 

1099 """ 

1100 self.update_all_atts(dict_, Element.copy_attr_convert, 

1101 and_source=and_source) 

1102 

1103 def clear(self) -> None: 

1104 self.children = [] 

1105 

1106 def replace(self, old, new) -> None: 

1107 """Replace one child `Node` with another child or children.""" 

1108 index = self.index(old) 

1109 if isinstance(new, Node): 

1110 self.setup_child(new) 

1111 self[index] = new 

1112 elif new is not None: 

1113 self[index:index+1] = new 

1114 

1115 def replace_self(self, new) -> None: 

1116 """ 

1117 Replace `self` node with `new`, where `new` is a node or a 

1118 list of nodes. 

1119 

1120 Provisional: the handling of node attributes will be revised. 

1121 """ 

1122 update = new 

1123 if not isinstance(new, Node): 

1124 # `new` is a list; update first child. 

1125 try: 

1126 update = new[0] 

1127 except IndexError: 

1128 update = None 

1129 if isinstance(update, Element): 

1130 update.update_basic_atts(self) 

1131 else: 

1132 # `update` is a Text node or `new` is an empty list. 

1133 # Assert that we aren't losing any attributes. 

1134 for att in self.basic_attributes: 

1135 assert not self[att], \ 

1136 'Losing "%s" attribute: %s' % (att, self[att]) 

1137 self.parent.replace(self, new) 

1138 

1139 def first_child_matching_class(self, 

1140 childclass: type[Element] | type[Text] 

1141 | tuple[type[Element] | type[Text], ...], 

1142 start: int = 0, 

1143 end: int = sys.maxsize, 

1144 ) -> int | None: 

1145 """ 

1146 Return the index of the first child whose class exactly matches. 

1147 

1148 Parameters: 

1149 

1150 - `childclass`: A `Node` subclass to search for, or a tuple of `Node` 

1151 classes. If a tuple, any of the classes may match. 

1152 - `start`: Initial index to check. 

1153 - `end`: Initial index to *not* check. 

1154 """ 

1155 if not isinstance(childclass, tuple): 

1156 childclass = (childclass,) 

1157 for index in range(start, min(len(self), end)): 

1158 for c in childclass: 

1159 if isinstance(self[index], c): 

1160 return index 

1161 return None 

1162 

1163 def first_child_not_matching_class( 

1164 self, 

1165 childclass: type[Element] | type[Text] 

1166 | tuple[type[Element] | type[Text], ...], 

1167 start: int = 0, 

1168 end: int = sys.maxsize, 

1169 ) -> int | None: 

1170 """ 

1171 Return the index of the first child whose class does *not* match. 

1172 

1173 Parameters: 

1174 

1175 - `childclass`: A `Node` subclass to skip, or a tuple of `Node` 

1176 classes. If a tuple, none of the classes may match. 

1177 - `start`: Initial index to check. 

1178 - `end`: Initial index to *not* check. 

1179 """ 

1180 if not isinstance(childclass, tuple): 

1181 childclass = (childclass,) 

1182 for index in range(start, min(len(self), end)): 

1183 for c in childclass: 

1184 if isinstance(self.children[index], c): 

1185 break 

1186 else: 

1187 return index 

1188 return None 

1189 

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

1191 tagline = '%s%s\n' % (indent*level, self.starttag()) 

1192 childreps = (c.pformat(indent, level+1) for c in self.children) 

1193 return ''.join((tagline, *childreps)) 

1194 

1195 def copy(self) -> Self: 

1196 obj = self.__class__(rawsource=self.rawsource, **self.attributes) 

1197 obj._document = self._document 

1198 obj.source = self.source 

1199 obj.line = self.line 

1200 return obj 

1201 

1202 def deepcopy(self) -> Self: 

1203 copy = self.copy() 

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

1205 return copy 

1206 

1207 def note_referenced_by(self, 

1208 name: str | None = None, 

1209 id: str | None = None, 

1210 ) -> None: 

1211 """Note that this Element has been referenced by its name 

1212 `name` or id `id`.""" 

1213 self.referenced = True 

1214 # Element.expect_referenced_by_* dictionaries map names or ids 

1215 # that were "propagated" to this element to the elements that 

1216 # had these attributes before. Mark them as ``referenced`` 

1217 # when this node is referenced by the respective names or ids. 

1218 if relay := getattr(self, 'expect_referenced_by_name', {}).get(name): 

1219 relay.referenced = True 

1220 if relay := getattr(self, 'expect_referenced_by_id', {}).get(id): 

1221 relay.referenced = True 

1222 

1223 @classmethod 

1224 def is_not_list_attribute(cls, attr: str) -> bool: 

1225 """ 

1226 Returns True if and only if the given attribute is NOT one of the 

1227 basic list attributes defined for all Elements. 

1228 """ 

1229 return attr not in cls.list_attributes 

1230 

1231 @classmethod 

1232 def is_not_known_attribute(cls, attr: str) -> bool: 

1233 """ 

1234 Return True if `attr` is NOT defined for all Element instances. 

1235 

1236 Provisional. May be removed in Docutils 2.0. 

1237 """ 

1238 return attr not in cls.common_attributes 

1239 

1240 def validate_attributes(self) -> None: 

1241 """Normalize and validate element attributes. 

1242 

1243 Convert string values to expected datatype. 

1244 Normalize values. 

1245 

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

1247 

1248 Provisional. 

1249 """ 

1250 messages = [] 

1251 for key, value in self.attributes.items(): 

1252 if key.startswith('internal:'): 

1253 continue # see docs/user/config.html#expose-internals 

1254 if key not in self.valid_attributes: 

1255 va = '", "'.join(self.valid_attributes) 

1256 messages.append(f'Attribute "{key}" not one of "{va}".') 

1257 continue 

1258 try: 

1259 self.attributes[key] = ATTRIBUTE_VALIDATORS[key](value) 

1260 except (ValueError, TypeError, KeyError) as e: 

1261 messages.append( 

1262 f'Attribute "{key}" has invalid value "{value}".\n {e}') 

1263 if messages: 

1264 raise ValidationError(f'Element {self.starttag()} invalid:\n ' 

1265 + '\n '.join(messages), 

1266 problematic_element=self) 

1267 

1268 def validate_content(self, 

1269 model: _ContentModelTuple | None = None, 

1270 elements: Sequence | None = None, 

1271 ) -> list: 

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

1273 

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

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

1276 

1277 Return list of children that do not fit in the model or raise 

1278 `ValidationError` if the content does not comply with the `model`. 

1279 

1280 Provisional. 

1281 """ 

1282 if model is None: 

1283 model = self.content_model 

1284 if elements is None: 

1285 elements = self.children 

1286 ichildren = iter(elements) 

1287 child = next(ichildren, None) 

1288 for category, quantifier in model: 

1289 if not isinstance(child, category): 

1290 if quantifier in ('.', '+'): 

1291 raise ValidationError(self._report_child(child, category), 

1292 problematic_element=child) 

1293 else: # quantifier in ('?', '*') -> optional child 

1294 continue # try same child with next part of content model 

1295 else: 

1296 # Check additional placement constraints (if applicable): 

1297 child.validate_position() 

1298 # advance: 

1299 if quantifier in ('.', '?'): # go to next element 

1300 child = next(ichildren, None) 

1301 else: # if quantifier in ('*', '+'): # pass all matching elements 

1302 for child in ichildren: 

1303 if not isinstance(child, category): 

1304 break 

1305 try: 

1306 child.validate_position() 

1307 except AttributeError: 

1308 pass 

1309 else: 

1310 child = None 

1311 return [] if child is None else [child, *ichildren] 

1312 

1313 def _report_child(self, 

1314 child, 

1315 category: Element | Iterable[Element], 

1316 ) -> str: 

1317 # Return a str reporting a missing child or child of wrong category. 

1318 try: 

1319 _type = category.__name__ 

1320 except AttributeError: 

1321 _type = '> or <'.join(c.__name__ for c in category) 

1322 msg = f'Element {self.starttag()} invalid:\n' 

1323 if child is None: 

1324 return f'{msg} Missing child of type <{_type}>.' 

1325 if isinstance(child, Text): 

1326 return (f'{msg} Expecting child of type <{_type}>, ' 

1327 f'not text data "{child.astext()}".') 

1328 return (f'{msg} Expecting child of type <{_type}>, ' 

1329 f'not {child.starttag()}.') 

1330 

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

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

1333 

1334 Raise ValidationError if there are violations. 

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

1336 

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

1338 Docutils Document Model. 

1339 

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

1341 

1342 Provisional (work in progress). 

1343 """ 

1344 self.validate_attributes() 

1345 

1346 leftover_childs = self.validate_content() 

1347 for child in leftover_childs: 

1348 if isinstance(child, Text): 

1349 raise ValidationError(f'Element {self.starttag()} invalid:\n' 

1350 f' Spurious text: "{child.astext()}".', 

1351 problematic_element=self) 

1352 else: 

1353 raise ValidationError(f'Element {self.starttag()} invalid:\n' 

1354 f' Child element {child.starttag()} ' 

1355 'not allowed at this position.', 

1356 problematic_element=child) 

1357 

1358 if recursive: 

1359 for child in self: 

1360 child.validate(recursive=recursive) 

1361 

1362 

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

1364# Element Categories 

1365# ==================== 

1366# 

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

1368 

1369class Root: 

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

1371 

1372 

1373class Structural: 

1374 """`Structural elements`__. 

1375 

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

1377 #structural-elements 

1378 """ 

1379 

1380 

1381class SubStructural: 

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

1383 

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

1385 

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

1387 #structural-subelements 

1388 """ 

1389 

1390 

1391class Bibliographic: 

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

1393 

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

1395 #bibliographic-elements 

1396 """ 

1397 

1398 

1399class Body: 

1400 """`Body elements`__. 

1401 

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

1403 """ 

1404 

1405 

1406class Admonition(Body): 

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

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

1409 

1410 

1411class Sequential(Body): 

1412 """List-like body elements.""" 

1413 

1414 

1415class General(Body): 

1416 """Miscellaneous body elements.""" 

1417 

1418 

1419class Special(Body): 

1420 """Special internal body elements.""" 

1421 

1422 

1423class Part: 

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

1425 

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

1427 """ 

1428 

1429 

1430class Decorative: 

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

1432 

1433 Children of `decoration`. 

1434 """ 

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

1436 

1437 

1438class Inline: 

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

1440 """ 

1441 

1442 

1443# Orthogonal categories and Mixins 

1444# ================================ 

1445 

1446class PreBibliographic: 

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

1448 

1449 

1450class Invisible(Special, PreBibliographic): 

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

1452 

1453 

1454class Labeled: 

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

1456 

1457 

1458class Resolvable: 

1459 resolved: bool = False 

1460 

1461 

1462class BackLinkable: 

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

1464 

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

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

1467 

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

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

1470 

1471 

1472class Referential(Resolvable): 

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

1474 

1475 

1476class Targetable(Resolvable): 

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

1478 referenced: int = 0 

1479 

1480 

1481class Titular: 

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

1483 

1484 

1485class TextElement(Element): 

1486 """ 

1487 An element which directly contains text. 

1488 

1489 Its children are all `Text` or `Inline` subclass nodes. You can 

1490 check whether an element's context is inline simply by checking whether 

1491 its immediate parent is a `TextElement` instance (including subclasses). 

1492 This is handy for nodes like `image` that can appear both inline and as 

1493 standalone body elements. 

1494 

1495 If passing children to `__init__()`, make sure to set `text` to 

1496 ``''`` or some other suitable value. 

1497 """ 

1498 content_model: Final = (((Text, Inline), '*'),) 

1499 # (#PCDATA | %inline.elements;)* 

1500 

1501 child_text_separator: Final = '' 

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

1503 

1504 def __init__(self, 

1505 rawsource: str = '', 

1506 text: str = '', 

1507 *children, 

1508 **attributes: Any, 

1509 ) -> None: 

1510 if text: 

1511 textnode = Text(text) 

1512 Element.__init__(self, rawsource, textnode, *children, 

1513 **attributes) 

1514 else: 

1515 Element.__init__(self, rawsource, *children, **attributes) 

1516 

1517 

1518class FixedTextElement(TextElement): 

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

1520 

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

1522 

1523 def __init__(self, 

1524 rawsource: str = '', 

1525 text: str = '', 

1526 *children, 

1527 **attributes: Any, 

1528 ) -> None: 

1529 super().__init__(rawsource, text, *children, **attributes) 

1530 self.attributes['xml:space'] = 'preserve' 

1531 

1532 

1533class PureTextElement(TextElement): 

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

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

1536 

1537 

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

1539# Concrete Document Tree Elements 

1540# ================================= 

1541# 

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

1543 

1544# Special purpose elements 

1545# ======================== 

1546# 

1547# Body elements for internal use or special requests. 

1548 

1549class comment(Invisible, FixedTextElement, PureTextElement): 

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

1551 

1552 

1553class substitution_definition(Invisible, TextElement): 

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

1555 

1556 

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

1558 valid_attributes: Final = Element.valid_attributes + ( 

1559 'anonymous', 'refid', 'refname', 'refuri') 

1560 

1561 

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

1563 """ 

1564 System message element. 

1565 

1566 Do not instantiate this class directly; use 

1567 ``document.reporter.info/warning/error/severe()`` instead. 

1568 """ 

1569 valid_attributes: Final = BackLinkable.valid_attributes + ( 

1570 'level', 'line', 'type') 

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

1572 

1573 def __init__(self, 

1574 message: str | None = None, 

1575 *children, 

1576 **attributes: Any, 

1577 ) -> None: 

1578 rawsource = attributes.pop('rawsource', '') 

1579 if message: 

1580 p = paragraph('', message) 

1581 children = (p,) + children 

1582 try: 

1583 Element.__init__(self, rawsource, *children, **attributes) 

1584 except: # NoQA: E722 (catchall) 

1585 print('system_message: children=%r' % (children,)) 

1586 raise 

1587 

1588 def astext(self) -> str: 

1589 line = self.get('line', '') 

1590 return '%s:%s: (%s/%s) %s' % (self['source'], line, self['type'], 

1591 self['level'], Element.astext(self)) 

1592 

1593 

1594class pending(Invisible, Element): 

1595 """ 

1596 Placeholder for pending operations. 

1597 

1598 The "pending" element is used to encapsulate a pending operation: the 

1599 operation (transform), the point at which to apply it, and any data it 

1600 requires. Only the pending operation's location within the document is 

1601 stored in the public document tree (by the "pending" object itself); the 

1602 operation and its data are stored in the "pending" object's internal 

1603 instance attributes. 

1604 

1605 For example, say you want a table of contents in your reStructuredText 

1606 document. The easiest way to specify where to put it is from within the 

1607 document, with a directive:: 

1608 

1609 .. contents:: 

1610 

1611 But the "contents" directive can't do its work until the entire document 

1612 has been parsed and possibly transformed to some extent. So the directive 

1613 code leaves a placeholder behind that will trigger the second phase of its 

1614 processing, something like this:: 

1615 

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

1617 

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

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

1620 transforms. 

1621 """ 

1622 

1623 def __init__(self, 

1624 transform: Transform, 

1625 details: Mapping[str, Any] | None = None, 

1626 rawsource: str = '', 

1627 *children, 

1628 **attributes: Any, 

1629 ) -> None: 

1630 Element.__init__(self, rawsource, *children, **attributes) 

1631 

1632 self.transform: Transform = transform 

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

1634 operation.""" 

1635 

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

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

1638 

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

1640 internals = ['.. internal attributes:', 

1641 ' .transform: %s.%s' % (self.transform.__module__, 

1642 self.transform.__name__), 

1643 ' .details:'] 

1644 details = sorted(self.details.items()) 

1645 for key, value in details: 

1646 if isinstance(value, Node): 

1647 internals.append('%7s%s:' % ('', key)) 

1648 internals.extend(['%9s%s' % ('', line) 

1649 for line in value.pformat().splitlines()]) 

1650 elif (value 

1651 and isinstance(value, list) 

1652 and isinstance(value[0], Node)): 

1653 internals.append('%7s%s:' % ('', key)) 

1654 for v in value: 

1655 internals.extend(['%9s%s' % ('', line) 

1656 for line in v.pformat().splitlines()]) 

1657 else: 

1658 internals.append('%7s%s: %r' % ('', key, value)) 

1659 return (Element.pformat(self, indent, level) 

1660 + ''.join((' %s%s\n' % (indent * level, line)) 

1661 for line in internals)) 

1662 

1663 def copy(self) -> Self: 

1664 obj = self.__class__(self.transform, self.details, self.rawsource, 

1665 **self.attributes) 

1666 obj._document = self._document 

1667 obj.source = self.source 

1668 obj.line = self.line 

1669 return obj 

1670 

1671 

1672class raw(Special, Inline, PreBibliographic, 

1673 FixedTextElement, PureTextElement): 

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

1675 

1676 Can be used as Body element or Inline element. 

1677 """ 

1678 valid_attributes: Final = Element.valid_attributes + ( 

1679 'format', 'xml:space') 

1680 

1681 

1682# Decorative Elements 

1683# =================== 

1684 

1685class header(Decorative, Element): pass 

1686class footer(Decorative, Element): pass 

1687 

1688 

1689# Structural Subelements 

1690# ====================== 

1691 

1692class title(Titular, PreBibliographic, SubStructural, TextElement): 

1693 """Title of `document`, `section`, `topic` and generic `admonition`. 

1694 """ 

1695 valid_attributes: Final = Element.valid_attributes + ('auto', 'refid') 

1696 

1697 

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

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

1700 

1701 def validate_position(self) -> None: 

1702 """Check position of subtitle: must follow a title.""" 

1703 if self.parent and self.parent.index(self) == 0: 

1704 raise ValidationError(f'Element {self.parent.starttag()} invalid:' 

1705 '\n <subtitle> only allowed after <title>.', 

1706 problematic_element=self) 

1707 

1708 

1709class meta(PreBibliographic, SubStructural, Element): 

1710 """Container for "invisible" bibliographic data, or meta-data.""" 

1711 valid_attributes: Final = Element.valid_attributes + ( 

1712 'content', 'dir', 'http-equiv', 'lang', 'media', 'name', 'scheme') 

1713 

1714 

1715class docinfo(SubStructural, Element): 

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

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

1718 # (%bibliographic.elements;)+ 

1719 

1720 

1721class decoration(PreBibliographic, SubStructural, Element): 

1722 """Container for `header` and `footer`.""" 

1723 content_model: Final = ((header, '?'), # Empty element doesn't make sense, 

1724 (footer, '?'), # but is simpler to define. 

1725 ) 

1726 # (header?, footer?) 

1727 

1728 def get_header(self) -> header: 

1729 if not len(self.children) or not isinstance(self.children[0], header): 

1730 self.insert(0, header()) 

1731 return self.children[0] 

1732 

1733 def get_footer(self) -> footer: 

1734 if not len(self.children) or not isinstance(self.children[-1], footer): 

1735 self.append(footer()) 

1736 return self.children[-1] 

1737 

1738 

1739class transition(SubStructural, Element): 

1740 """Transitions__ represent "semantic breaks". 

1741 

1742 __ https://docutils.sourceforge.io/docs/ref/doctree.html#transition 

1743 """ 

1744 # Sibling nodes that are ignored when validating a transition's position 

1745 # (titles plus moving and invisible elements except comments): 

1746 ignored_siblings = (decoration, meta, pending, substitution_definition, 

1747 subtitle, target, title) 

1748 

1749 def validate_position(self) -> None: 

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

1751 

1752 A transition may not begin or end section or document text, 

1753 nor may two transitions be immediately adjacent. 

1754 """ 

1755 messages = [f'Element {self.parent.starttag()} invalid:'] 

1756 if isinstance(self.previous_sibling(), transition): 

1757 messages.append( 

1758 '<transition> may not directly follow another transition.') 

1759 i = self.parent.index(self) 

1760 prev_siblings = self.parent[:i] 

1761 if not [sibling for sibling in prev_siblings 

1762 if not isinstance(sibling, self.ignored_siblings)]: 

1763 messages.append( 

1764 '<transition> may not begin a section or document.') 

1765 next_siblings = self.parent[i+1:] 

1766 if not [sibling for sibling in next_siblings 

1767 if not isinstance(sibling, self.ignored_siblings)]: 

1768 messages.append('<transition> may not end a section or document.') 

1769 if len(messages) > 1: 

1770 raise ValidationError('\n '.join(messages), 

1771 problematic_element=self) 

1772 

1773 

1774# Structural Elements 

1775# =================== 

1776 

1777class topic(Structural, Element): 

1778 """ 

1779 Topics__ are non-recursive, mini-sections. 

1780 

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

1782 """ 

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

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

1785 

1786 

1787class sidebar(Structural, Element): 

1788 """ 

1789 Sidebars__ are like parallel documents providing related material. 

1790 

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

1792 of the page 

1793 

1794 __ https://docutils.sourceforge.io/docs/ref/doctree.html#sidebar 

1795 """ 

1796 content_model: Final = ((title, '?'), 

1797 (subtitle, '?'), 

1798 ((topic, Body), '+'), 

1799 ) 

1800 # ((title, subtitle?)?, (%body.elements; | topic)+) 

1801 # "subtitle only after title" is ensured in `subtitle.validate_position()`. 

1802 

1803 

1804class section(Structural, Element): 

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

1806 

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

1808 """ 

1809 # recursive content model, see below 

1810 

1811 

1812section.content_model = ((title, '.'), 

1813 (subtitle, '?'), 

1814 ((Body, topic, sidebar, transition), '*'), 

1815 ((section, transition), '*'), 

1816 ) 

1817# (title, subtitle?, %structure.model;) 

1818# Correct transition placement is ensured in `transition.validate_position()`. 

1819 

1820 

1821# Root Element 

1822# ============ 

1823 

1824class document(Root, Element): 

1825 """ 

1826 The document root element. 

1827 

1828 Do not instantiate this class directly; use 

1829 `docutils.utils.new_document()` instead. 

1830 """ 

1831 valid_attributes: Final = Element.valid_attributes + ('title',) 

1832 content_model: Final = ((title, '?'), 

1833 (subtitle, '?'), 

1834 (meta, '*'), 

1835 (decoration, '?'), 

1836 (docinfo, '?'), 

1837 (transition, '?'), 

1838 ((Body, topic, sidebar, transition), '*'), 

1839 ((section, transition), '*'), 

1840 ) 

1841 # ( (title, subtitle?)?, 

1842 # meta*, 

1843 # decoration?, 

1844 # (docinfo, transition?)?, 

1845 # %structure.model; ) 

1846 # Additional restrictions for `subtitle` and `transition` are tested 

1847 # with the respective `validate_position()` methods. 

1848 

1849 def __init__(self, 

1850 settings: Values, 

1851 reporter: Reporter, 

1852 *args, 

1853 **kwargs: Any, 

1854 ) -> None: 

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

1856 

1857 self.current_source: StrPath | None = None 

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

1859 

1860 self.current_line: int | None = None 

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

1862 

1863 self.settings: Values = settings 

1864 """Runtime settings data record.""" 

1865 

1866 self.reporter: Reporter = reporter 

1867 """System message generator.""" 

1868 

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

1870 """List of indirect target nodes.""" 

1871 

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

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

1874 

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

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

1877 

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

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

1880 

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

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

1883 

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

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

1886 

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

1888 """Mapping of ids to nodes.""" 

1889 

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

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

1892 

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

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

1895 """ 

1896 

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

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

1899 

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

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

1902 

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

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

1905 

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

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

1908 

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

1910 """List of symbol footnote nodes.""" 

1911 

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

1913 """List of symbol footnote_reference nodes.""" 

1914 

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

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

1917 

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

1919 """List of citation nodes.""" 

1920 

1921 self.autofootnote_start: int = 1 

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

1923 

1924 self.symbol_footnote_start: int = 0 

1925 """Initial symbol footnote symbol index.""" 

1926 

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

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

1929 

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

1931 """System messages generated while parsing.""" 

1932 

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

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

1935 

1936 import docutils.transforms 

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

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

1939 

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

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

1942 

1943 self.decoration: decoration | None = None 

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

1945 

1946 self._document: document = self 

1947 

1948 def __getstate__(self) -> dict[str, Any]: 

1949 """ 

1950 Return dict with unpicklable references removed. 

1951 """ 

1952 state = self.__dict__.copy() 

1953 state['reporter'] = None 

1954 state['transformer'] = None 

1955 return state 

1956 

1957 def asdom(self, dom: ModuleType | None = None) -> minidom.Document: 

1958 """Return a DOM representation of this document.""" 

1959 if dom is None: 

1960 import xml.dom.minidom as dom 

1961 domroot = dom.Document() 

1962 domroot.appendChild(self._dom_node(domroot)) 

1963 return domroot 

1964 

1965 def set_id(self, 

1966 node: Element, 

1967 msgnode: Element | None = None, 

1968 suggested_prefix: str = '', 

1969 ) -> str: 

1970 """ 

1971 Check/set identifiers of element `node`. Return last identifier. 

1972 

1973 Check `node`s identifiers for duplicates, 

1974 create a new identifier if there are no identifiers. 

1975 Update `document.ids` and `document.nameids`. 

1976 

1977 Provisional. 

1978 """ 

1979 if not node['ids']: 

1980 node['ids'].append(self.create_id(node, suggested_prefix)) 

1981 # register and check for duplicates 

1982 for id in node['ids']: 

1983 self.ids.setdefault(id, node) 

1984 if self.ids[id] is not node: 

1985 msg = self.reporter.error(f'Duplicate ID: "{id}" used by ' 

1986 f'{self.ids[id].starttag()} ' 

1987 f'and {node.starttag()}', 

1988 base_node=node) 

1989 if msgnode is not None: 

1990 msgnode += msg 

1991 for name in node['names']: 

1992 self.nameids[name] = id 

1993 return id 

1994 

1995 def create_id(self, node: Element, suggested_prefix: str = '') -> str: 

1996 # Internal auxiliary method for set_id(): 

1997 # generate and return a suitable identifier for `node`. 

1998 # See also make_id() 

1999 id_prefix = self.settings.id_prefix 

2000 auto_id_prefix = self.settings.auto_id_prefix 

2001 base_id = '' 

2002 id = '' 

2003 for name in node['names']: 

2004 if id_prefix: # allow names starting with numbers 

2005 base_id = make_id('x'+name)[1:] 

2006 else: 

2007 base_id = make_id(name) 

2008 # TODO: normalize id-prefix? (would make code simpler) 

2009 id = id_prefix + base_id 

2010 if base_id and id not in self.ids: 

2011 break 

2012 else: 

2013 if base_id and auto_id_prefix.endswith('%'): 

2014 # disambiguate name-derived ID 

2015 # TODO: remove second condition after announcing change 

2016 prefix = id + '-' 

2017 elif (node['dupnames'] and auto_id_prefix.endswith('%') 

2018 and make_id(node['dupnames'][0])): 

2019 prefix = make_id(node['dupnames'][0]) + '-' 

2020 else: 

2021 prefix = id_prefix + auto_id_prefix 

2022 if prefix.endswith('%'): 

2023 prefix = f"""{prefix[:-1]}{suggested_prefix 

2024 or make_id(node.tagname)}-""" 

2025 while True: 

2026 self.id_counter[prefix] += 1 

2027 id = f'{prefix}{self.id_counter[prefix]}' 

2028 if id not in self.ids: 

2029 break 

2030 return id 

2031 

2032 def set_name_id_map(self, 

2033 node: Element, 

2034 id: str, 

2035 msgnode: Element | None = None, 

2036 explicit: bool = False, 

2037 ) -> None: 

2038 """Deprecated. Will be removed in Docutils 2.0.""" 

2039 warnings.warn('nodes.document.set_name_id_map() will be removed' 

2040 ' in Docutils 2.0.', DeprecationWarning, stacklevel=2) 

2041 self.note_names(node, msgnode, explicit) 

2042 for name in node['names']: 

2043 self.nameids[name] = id 

2044 

2045 def set_duplicate_name(self, 

2046 node: Element, 

2047 name: str, 

2048 msgnode: Element, 

2049 explicit: bool, 

2050 ) -> None: 

2051 """ 

2052 Handle name conflicts according to the `rST specification`__. 

2053 

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

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

2056 

2057 `self.names` maps names to elements. The value ``None`` indicates 

2058 that the name is a "dupname" (i.e. the document contains two or 

2059 more elements with the same name and target type). 

2060 

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

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

2063 

2064 The following state transition table shows how the values 

2065 of `self.names` ("name") and `self.nametypes` ("type") items 

2066 with key `name` change and which actions are performed. 

2067 

2068 "Old" is the element with conflicting reference name, 

2069 "new" is the element specified by the argument `node`. 

2070 The "Input type" is specified by the argument `explicit`. 

2071 

2072 ======== ==== ======== ==== ======== =============== ======= 

2073 Input Old State New State Action 

2074 -------- -------------- -------------- ------------------------ 

2075 type name type name type invalidate [#]_ report 

2076 ======== ==== ======== ==== ======== =============== ======= 

2077 explicit old explicit None explicit new,old [#ex]_ WARNING 

2078 implicit old explicit old explicit new INFO 

2079 explicit old implicit new explicit old INFO 

2080 implicit old implicit None implicit new,old [#ex]_ INFO 

2081 explicit None explicit None explicit new WARNING 

2082 implicit None explicit None explicit new INFO 

2083 explicit None implicit new explicit 

2084 implicit None implicit None implicit new INFO 

2085 ======== ==== ======== ==== ======== =============== ======= 

2086 

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

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

2089 

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

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

2092 

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

2094 #implicit-hyperlink-targets 

2095 

2096 Provisional. 

2097 """ 

2098 old_node = self.names[name] # None if name is only dupname 

2099 old_explicit = self.nametypes[name] 

2100 level = 0 # system message level: 1-info, 2-warning 

2101 

2102 self.nametypes[name] = old_explicit or explicit 

2103 

2104 if old_node is not None and ( 

2105 'refname' in node and node['refname'] == old_node.get('refname') 

2106 or 'refuri' in node and node['refuri'] == old_node.get('refuri') 

2107 ): 

2108 # indirect targets with same reference -> keep old target 

2109 level = 1 

2110 ref = node.get('refuri') or node.get('refname') 

2111 s = f'Duplicate name "{name}" for external target "{ref}".' 

2112 dupname(node, name) 

2113 elif explicit: 

2114 if old_explicit: 

2115 level = 2 

2116 s = f'Duplicate explicit target name: "{name}".' 

2117 dupname(node, name) 

2118 if old_node is not None: 

2119 dupname(old_node, name) 

2120 self.names[name] = None 

2121 self.nameids[name] = None 

2122 else: # new explicit, old implicit -> override 

2123 self.names[name] = node 

2124 if old_node is not None: 

2125 level = 1 

2126 s = f'Target name overrides implicit target name "{name}".' 

2127 dupname(old_node, name) 

2128 else: # new name is implicit 

2129 level = 1 

2130 s = f'Duplicate implicit target name: "{name}".' 

2131 dupname(node, name) 

2132 if old_node is not None and not old_explicit: 

2133 dupname(old_node, name) 

2134 self.names[name] = None 

2135 self.nameids[name] = None 

2136 self.set_id(old_node) # set id to get running numbers right 

2137 if level: 

2138 # don't add backref id for empty targets (not shown in output) 

2139 if isinstance(node, target) and not node.children: 

2140 backrefs = [] 

2141 else: 

2142 backrefs = [self.set_id(node)] 

2143 msg = self.reporter.system_message(level, s, backrefs=backrefs, 

2144 base_node=node) 

2145 # try appending near to the problem: 

2146 if msgnode is not None and 'Body' in repr(msgnode.content_model): 

2147 msgnode += msg 

2148 

2149 def note_names(self, 

2150 node: Element, 

2151 msgnode: Element|None = None, 

2152 explicit: bool = False, 

2153 ) -> None: 

2154 """ 

2155 Register the reference names of the element `node`. 

2156 

2157 Update `self.names` and `self.nametypes` 

2158 for each name in the "names" attribute of `node`. 

2159 In case of name conflicts, call `self.set_duplicate_name()`. 

2160 """ 

2161 for name in tuple(node['names']): 

2162 if name in self.names and self.names[name] != node: 

2163 self.set_duplicate_name(node, name, msgnode, explicit) 

2164 # attention: modifies node['names'] 

2165 else: 

2166 self.names[name] = node 

2167 self.nametypes.setdefault(name, explicit) 

2168 

2169 def has_name(self, name: str) -> bool: 

2170 # TODO: deprecate in Docutils 2.0 (use ``name in document.names``) 

2171 return name in self.names 

2172 

2173 # "note" here is an imperative verb: "take note of". 

2174 def note_implicit_target(self, target: Element, 

2175 msgnode: Element|None = None) -> None: 

2176 self.note_names(target, msgnode, explicit=False) 

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

2178 self.set_id(target, msgnode) 

2179 

2180 def note_explicit_target(self, target: Element, 

2181 msgnode: Element|None = None) -> None: 

2182 self.note_names(target, msgnode, explicit=True) 

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

2184 self.set_id(target, msgnode) 

2185 

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

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

2188 

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

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

2191 

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

2193 self.indirect_targets.append(target) 

2194 if target['names']: 

2195 self.note_refname(target) 

2196 

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

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

2199 self.set_id(target) 

2200 

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

2202 self.set_id(footnote) 

2203 self.autofootnotes.append(footnote) 

2204 

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

2206 self.set_id(ref) 

2207 self.autofootnote_refs.append(ref) 

2208 

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

2210 self.set_id(footnote) 

2211 self.symbol_footnotes.append(footnote) 

2212 

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

2214 self.set_id(ref) 

2215 self.symbol_footnote_refs.append(ref) 

2216 

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

2218 self.set_id(footnote) 

2219 self.footnotes.append(footnote) 

2220 

2221 def note_footnote_ref(self, ref: footnote_reference) -> None: 

2222 self.set_id(ref) 

2223 self.footnote_refs.setdefault(ref['refname'], []).append(ref) 

2224 self.note_refname(ref) 

2225 

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

2227 self.citations.append(citation) 

2228 

2229 def note_citation_ref(self, ref: citation_reference) -> None: 

2230 self.set_id(ref) 

2231 self.citation_refs.setdefault(ref['refname'], []).append(ref) 

2232 self.note_refname(ref) 

2233 

2234 def note_substitution_def(self, 

2235 subdef: substitution_definition, 

2236 def_name: str, 

2237 msgnode: Element | None = None, 

2238 ) -> None: 

2239 name = whitespace_normalize_name(def_name) 

2240 if name in self.substitution_defs: 

2241 msg = self.reporter.error( 

2242 'Duplicate substitution definition name: "%s".' % name, 

2243 base_node=subdef) 

2244 if msgnode is not None: 

2245 msgnode += msg 

2246 oldnode = self.substitution_defs[name] 

2247 dupname(oldnode, name) 

2248 # keep only the last definition: 

2249 self.substitution_defs[name] = subdef 

2250 # case-insensitive mapping: 

2251 self.substitution_names[fully_normalize_name(name)] = name 

2252 

2253 def note_substitution_ref(self, 

2254 subref: substitution_reference, 

2255 refname: str, 

2256 ) -> None: 

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

2258 

2259 def note_pending( 

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

2261 self.transformer.add_pending(pending, priority) 

2262 

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

2264 self.parse_messages.append(message) 

2265 

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

2267 self.transform_messages.append(message) 

2268 

2269 def note_source(self, 

2270 source: StrPath | None, 

2271 offset: int | None, 

2272 ) -> None: 

2273 self.current_source = source and os.fspath(source) 

2274 if offset is None: 

2275 self.current_line = offset 

2276 else: 

2277 self.current_line = offset + 1 

2278 

2279 def copy(self) -> Self: 

2280 obj = self.__class__(self.settings, self.reporter, 

2281 **self.attributes) 

2282 obj.source = self.source 

2283 obj.line = self.line 

2284 return obj 

2285 

2286 def get_decoration(self) -> decoration: 

2287 if not self.decoration: 

2288 self.decoration: decoration = decoration() 

2289 index = self.first_child_not_matching_class((Titular, meta)) 

2290 if index is None: 

2291 self.append(self.decoration) 

2292 else: 

2293 self.insert(index, self.decoration) 

2294 return self.decoration 

2295 

2296 

2297# Bibliographic Elements 

2298# ====================== 

2299 

2300class author(Bibliographic, TextElement): pass 

2301class organization(Bibliographic, TextElement): pass 

2302class address(Bibliographic, FixedTextElement): pass 

2303class contact(Bibliographic, TextElement): pass 

2304class version(Bibliographic, TextElement): pass 

2305class revision(Bibliographic, TextElement): pass 

2306class status(Bibliographic, TextElement): pass 

2307class date(Bibliographic, TextElement): pass 

2308class copyright(Bibliographic, TextElement): pass # NoQA: A001 (builtin name) 

2309 

2310 

2311class authors(Bibliographic, Element): 

2312 """Container for author information for documents with multiple authors. 

2313 """ 

2314 content_model: Final = ((author, '+'), 

2315 (organization, '?'), 

2316 (address, '?'), 

2317 (contact, '?'), 

2318 ) 

2319 # (author, organization?, address?, contact?)+ 

2320 

2321 def validate_content(self, 

2322 model: _ContentModelTuple | None = None, 

2323 elements: Sequence | None = None, 

2324 ) -> list: 

2325 """Repeatedly test for children matching the content model. 

2326 

2327 Provisional. 

2328 """ 

2329 relics = super().validate_content() 

2330 while relics: 

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

2332 return relics 

2333 

2334 

2335# Body Elements 

2336# ============= 

2337# 

2338# General 

2339# ------- 

2340# 

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

2342 

2343class paragraph(General, TextElement): pass 

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

2345 

2346 

2347class compound(General, Element): 

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

2349 

2350 

2351class container(General, Element): 

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

2353 

2354 

2355class attribution(Part, TextElement): 

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

2357 

2358 

2359class block_quote(General, Element): 

2360 """An extended quotation, set off from the main text.""" 

2361 content_model: Final = ((Body, '+'), (attribution, '?')) 

2362 # ((%body.elements;)+, attribution?) 

2363 

2364 

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

2366 valid_attributes: Final = Element.valid_attributes + ( 

2367 'anonymous', 'refid', 'refname', 'refuri') 

2368 

2369 

2370# Lists 

2371# ----- 

2372# 

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

2374 

2375class list_item(Part, Element): 

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

2377 

2378 

2379class bullet_list(Sequential, Element): 

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

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

2382 

2383 

2384class enumerated_list(Sequential, Element): 

2385 valid_attributes: Final = Element.valid_attributes + ( 

2386 'enumtype', 'prefix', 'suffix', 'start') 

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

2388 

2389 

2390class term(Part, TextElement): pass 

2391class classifier(Part, TextElement): pass 

2392 

2393 

2394class definition(Part, Element): 

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

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

2397 

2398 

2399class definition_list_item(Part, Element): 

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

2401 ((classifier, term), '*'), 

2402 (definition, '.'), 

2403 ) 

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

2405 

2406 

2407class definition_list(Sequential, Element): 

2408 """List of terms and their definitions. 

2409 

2410 Can be used for glossaries or dictionaries, to describe or 

2411 classify things, for dialogues, or to itemize subtopics. 

2412 """ 

2413 content_model: Final = ((definition_list_item, '+'),) 

2414 # (definition_list_item+) 

2415 

2416 

2417class field_name(Part, TextElement): pass 

2418 

2419 

2420class field_body(Part, Element): 

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

2422 

2423 

2424class field(Part, Bibliographic, Element): 

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

2426 # (field_name, field_body) 

2427 

2428 

2429class field_list(Sequential, Element): 

2430 """List of label & data pairs. 

2431 

2432 Typically rendered as a two-column list. 

2433 Also used for extension syntax or special processing. 

2434 """ 

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

2436 

2437 

2438class option_string(Part, PureTextElement): 

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

2440 

2441 

2442class option_argument(Part, PureTextElement): 

2443 """Placeholder text for option arguments.""" 

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

2445 

2446 def astext(self) -> str: 

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

2448 

2449 

2450class option(Part, Element): 

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

2452 

2453 Groups an option string with zero or more option argument placeholders. 

2454 """ 

2455 child_text_separator: Final = '' 

2456 content_model: Final = ((option_string, '.'), (option_argument, '*')) 

2457 # (option_string, option_argument*) 

2458 

2459 

2460class option_group(Part, Element): 

2461 """Groups together one or more `option` elements, all synonyms.""" 

2462 child_text_separator: Final = ', ' 

2463 content_model: Final = ((option, '+'),) # (option+) 

2464 

2465 

2466class description(Part, Element): 

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

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

2469 

2470 

2471class option_list_item(Part, Element): 

2472 """Container for a pair of `option_group` and `description` elements. 

2473 """ 

2474 child_text_separator: Final = ' ' 

2475 content_model: Final = ((option_group, '.'), (description, '.')) 

2476 # (option_group, description) 

2477 

2478 

2479class option_list(Sequential, Element): 

2480 """Two-column list of command-line options and descriptions.""" 

2481 content_model: Final = ((option_list_item, '+'),) # (option_list_item+) 

2482 

2483 

2484# Pre-formatted text blocks 

2485# ------------------------- 

2486 

2487class literal_block(General, FixedTextElement): pass 

2488class doctest_block(General, FixedTextElement): pass 

2489 

2490 

2491class math_block(General, FixedTextElement, PureTextElement): 

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

2493 

2494 

2495class line(Part, TextElement): 

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

2497 indent: str | None = None 

2498 

2499 

2500class line_block(General, Element): 

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

2502 """ 

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

2504 

2505 

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

2507 

2508 

2509# Admonitions 

2510# ----------- 

2511# distinctive and self-contained notices 

2512 

2513class attention(Admonition, Element): pass 

2514class caution(Admonition, Element): pass 

2515class danger(Admonition, Element): pass 

2516class error(Admonition, Element): pass 

2517class important(Admonition, Element): pass 

2518class note(Admonition, Element): pass 

2519class tip(Admonition, Element): pass 

2520class hint(Admonition, Element): pass 

2521class warning(Admonition, Element): pass 

2522 

2523 

2524class admonition(Admonition, Element): 

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

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

2527 

2528 

2529# Footnote and citation 

2530# --------------------- 

2531 

2532class label(Part, PureTextElement): 

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

2534 

2535 

2536class footnote(General, BackLinkable, Element, Labeled, Targetable): 

2537 """Labelled note providing additional context (footnote or endnote).""" 

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

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

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

2541 # The label was optional in Docutils < 1.0. 

2542 

2543 

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

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

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

2547 

2548 

2549# Graphical elements 

2550# ------------------ 

2551 

2552class image(General, Inline, Element): 

2553 """Reference to an image resource. 

2554 

2555 May be body element or inline element. 

2556 """ 

2557 valid_attributes: Final = Element.valid_attributes + ( 

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

2559 

2560 def astext(self) -> str: 

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

2562 

2563 

2564class caption(Part, TextElement): pass 

2565 

2566 

2567class legend(Part, Element): 

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

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

2570 

2571 

2572class figure(General, Element): 

2573 """A formal figure, generally an illustration, with a title.""" 

2574 valid_attributes: Final = Element.valid_attributes + ('align', 'width') 

2575 content_model: Final = (((image, reference), '.'), 

2576 (caption, '?'), 

2577 (legend, '?'), 

2578 ) 

2579 # (image, ((caption, legend?) | legend)) 

2580 # A caption or legend is required (cf. [bugs: #489]). 

2581 

2582 

2583# Tables 

2584# ------ 

2585 

2586class entry(Part, Element): 

2587 """An entry in a `row` (a table cell).""" 

2588 valid_attributes: Final = Element.valid_attributes + ( 

2589 'align', 'char', 'charoff', 'colname', 'colsep', 'morecols', 

2590 'morerows', 'namest', 'nameend', 'rowsep', 'valign') 

2591 content_model: Final = ((Body, '*'),) 

2592 # %tbl.entry.mdl -> (%body.elements;)* 

2593 

2594 

2595class row(Part, Element): 

2596 """Row of table cells.""" 

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

2598 content_model: Final = ((entry, '+'),) # (%tbl.row.mdl;) -> entry+ 

2599 

2600 

2601class colspec(Part, Element): 

2602 """Specifications for a column in a `tgroup`.""" 

2603 valid_attributes: Final = Element.valid_attributes + ( 

2604 'align', 'char', 'charoff', 'colname', 'colnum', 

2605 'colsep', 'colwidth', 'rowsep', 'stub') 

2606 

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

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

2609 

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

2611 

2612 Provisional. 

2613 

2614 __ https://docutils.sourceforge.io/docs/ref/doctree.html#colwidth 

2615 """ 

2616 # Move current implementation of validate_colwidth() here 

2617 # in Docutils 1.0 

2618 return validate_colwidth(self.get('colwidth', '')) 

2619 

2620 

2621class thead(Part, Element): 

2622 """Row(s) that form the head of a `tgroup`.""" 

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

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

2625 

2626 

2627class tbody(Part, Element): 

2628 """Body of a `tgroup`.""" 

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

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

2631 

2632 

2633class tgroup(Part, Element): 

2634 """A portion of a table. Most tables have just one `tgroup`.""" 

2635 valid_attributes: Final = Element.valid_attributes + ( 

2636 'align', 'cols', 'colsep', 'rowsep') 

2637 content_model: Final = ((colspec, '*'), (thead, '?'), (tbody, '.')) 

2638 # (colspec*, thead?, tbody) 

2639 

2640 

2641class table(General, Element): 

2642 """A data arrangement with rows and columns.""" 

2643 valid_attributes: Final = Element.valid_attributes + ( 

2644 'align', 'colsep', 'frame', 'pgwide', 'rowsep', 'width') 

2645 content_model: Final = ((title, '?'), (tgroup, '+')) 

2646 # (title?, tgroup+) 

2647 

2648 

2649# Inline Elements 

2650# =============== 

2651 

2652class abbreviation(Inline, TextElement): pass 

2653class acronym(Inline, TextElement): pass 

2654class emphasis(Inline, TextElement): pass 

2655class generated(Inline, TextElement): pass 

2656class inline(Inline, TextElement, Targetable): pass 

2657class literal(Inline, TextElement): pass 

2658class strong(Inline, TextElement): pass 

2659class subscript(Inline, TextElement): pass 

2660class superscript(Inline, TextElement): pass 

2661class title_reference(Inline, TextElement): pass 

2662 

2663 

2664class footnote_reference(Inline, Referential, PureTextElement): 

2665 valid_attributes: Final = Element.valid_attributes + ( 

2666 'auto', 'refid', 'refname') 

2667 

2668 

2669class citation_reference(Inline, Referential, PureTextElement): 

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

2671 

2672 

2673class substitution_reference(Inline, TextElement): 

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

2675 

2676 

2677class math(Inline, PureTextElement): 

2678 """Mathematical notation in running text.""" 

2679 

2680 

2681class problematic(Inline, TextElement): 

2682 valid_attributes: Final = Element.valid_attributes + ( 

2683 'refid', 'refname', 'refuri') 

2684 

2685 

2686# ======================================== 

2687# Auxiliary Classes, Functions, and Data 

2688# ======================================== 

2689 

2690node_class_names: Sequence[str] = """ 

2691 Text 

2692 abbreviation acronym address admonition attention attribution author 

2693 authors 

2694 block_quote bullet_list 

2695 caption caution citation citation_reference classifier colspec comment 

2696 compound contact container copyright 

2697 danger date decoration definition definition_list definition_list_item 

2698 description docinfo doctest_block document 

2699 emphasis entry enumerated_list error 

2700 field field_body field_list field_name figure footer 

2701 footnote footnote_reference 

2702 generated 

2703 header hint 

2704 image important inline 

2705 label legend line line_block list_item literal literal_block 

2706 math math_block meta 

2707 note 

2708 option option_argument option_group option_list option_list_item 

2709 option_string organization 

2710 paragraph pending problematic 

2711 raw reference revision row rubric 

2712 section sidebar status strong subscript substitution_definition 

2713 substitution_reference subtitle superscript system_message 

2714 table target tbody term tgroup thead tip title title_reference topic 

2715 transition 

2716 version 

2717 warning""".split() 

2718"""A list of names of all concrete Node subclasses.""" 

2719 

2720 

2721class NodeVisitor: 

2722 """ 

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

2724 document tree traversals. 

2725 

2726 Each node class has corresponding methods, doing nothing by 

2727 default; override individual methods for specific and useful 

2728 behaviour. The `dispatch_visit()` method is called by 

2729 `Node.walk()` upon entering a node. `Node.walkabout()` also calls 

2730 the `dispatch_departure()` method before exiting a node. 

2731 

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

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

2734 

2735 This is a base class for visitors whose ``visit_...`` & ``depart_...`` 

2736 methods must be implemented for *all* compulsory node types encountered 

2737 (such as for `docutils.writers.Writer` subclasses). 

2738 Unimplemented methods will raise exceptions (except for optional nodes). 

2739 

2740 For sparse traversals, where only certain node types are of interest, use 

2741 subclass `SparseNodeVisitor` instead. When (mostly or entirely) uniform 

2742 processing is desired, subclass `GenericNodeVisitor`. 

2743 

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

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

2746 1995. 

2747 """ 

2748 

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

2750 """ 

2751 Tuple containing node class names (as strings). 

2752 

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

2754 or departure functions for these node classes. 

2755 

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

2757 """ 

2758 

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

2760 self.document: document = document 

2761 

2762 def dispatch_visit(self, node) -> None: 

2763 """ 

2764 Call self."``visit_`` + node class name" with `node` as 

2765 parameter. If the ``visit_...`` method does not exist, call 

2766 self.unknown_visit. 

2767 """ 

2768 node_name = node.__class__.__name__ 

2769 method = getattr(self, 'visit_' + node_name, self.unknown_visit) 

2770 self.document.reporter.debug( 

2771 'docutils.nodes.NodeVisitor.dispatch_visit calling %s for %s' 

2772 % (method.__name__, node_name)) 

2773 return method(node) 

2774 

2775 def dispatch_departure(self, node) -> None: 

2776 """ 

2777 Call self."``depart_`` + node class name" with `node` as 

2778 parameter. If the ``depart_...`` method does not exist, call 

2779 self.unknown_departure. 

2780 """ 

2781 node_name = node.__class__.__name__ 

2782 method = getattr(self, 'depart_' + node_name, self.unknown_departure) 

2783 self.document.reporter.debug( 

2784 'docutils.nodes.NodeVisitor.dispatch_departure calling %s for %s' 

2785 % (method.__name__, node_name)) 

2786 return method(node) 

2787 

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

2789 """ 

2790 Called when entering unknown `Node` types. 

2791 

2792 Raise an exception unless overridden. 

2793 """ 

2794 if (self.document.settings.strict_visitor 

2795 or node.__class__.__name__ not in self.optional): 

2796 raise NotImplementedError( 

2797 '%s visiting unknown node type: %s' 

2798 % (self.__class__, node.__class__.__name__)) 

2799 

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

2801 """ 

2802 Called before exiting unknown `Node` types. 

2803 

2804 Raise exception unless overridden. 

2805 """ 

2806 if (self.document.settings.strict_visitor 

2807 or node.__class__.__name__ not in self.optional): 

2808 raise NotImplementedError( 

2809 '%s departing unknown node type: %s' 

2810 % (self.__class__, node.__class__.__name__)) 

2811 

2812 

2813class SparseNodeVisitor(NodeVisitor): 

2814 """ 

2815 Base class for sparse traversals, where only certain node types are of 

2816 interest. When ``visit_...`` & ``depart_...`` methods should be 

2817 implemented for *all* node types (such as for `docutils.writers.Writer` 

2818 subclasses), subclass `NodeVisitor` instead. 

2819 """ 

2820 

2821 

2822class GenericNodeVisitor(NodeVisitor): 

2823 """ 

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

2825 

2826 Unless overridden, each ``visit_...`` method calls `default_visit()`, and 

2827 each ``depart_...`` method (when using `Node.walkabout()`) calls 

2828 `default_departure()`. `default_visit()` (and `default_departure()`) must 

2829 be overridden in subclasses. 

2830 

2831 Define fully generic visitors by overriding `default_visit()` (and 

2832 `default_departure()`) only. Define semi-generic visitors by overriding 

2833 individual ``visit_...()`` (and ``depart_...()``) methods also. 

2834 

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

2836 be overridden for default behavior. 

2837 """ 

2838 

2839 def default_visit(self, node): 

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

2841 raise NotImplementedError 

2842 

2843 def default_departure(self, node): 

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

2845 raise NotImplementedError 

2846 

2847 

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

2849 self.default_visit(node) 

2850 

2851 

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

2853 self.default_departure(node) 

2854 

2855 

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

2857 pass 

2858 

2859 

2860def _add_node_class_names(names) -> None: 

2861 """Save typing with dynamic assignments:""" 

2862 for _name in names: 

2863 setattr(GenericNodeVisitor, "visit_" + _name, _call_default_visit) 

2864 setattr(GenericNodeVisitor, "depart_" + _name, _call_default_departure) 

2865 setattr(SparseNodeVisitor, 'visit_' + _name, _nop) 

2866 setattr(SparseNodeVisitor, 'depart_' + _name, _nop) 

2867 

2868 

2869_add_node_class_names(node_class_names) 

2870 

2871 

2872class TreeCopyVisitor(GenericNodeVisitor): 

2873 """ 

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

2875 """ 

2876 

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

2878 super().__init__(document) 

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

2880 self.parent: list = [] 

2881 

2882 def get_tree_copy(self): 

2883 return self.parent[0] 

2884 

2885 def default_visit(self, node) -> None: 

2886 """Copy the current node, and make it the new acting parent.""" 

2887 newnode = node.copy() 

2888 self.parent.append(newnode) 

2889 self.parent_stack.append(self.parent) 

2890 self.parent = newnode 

2891 

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

2893 """Restore the previous acting parent.""" 

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

2895 

2896 

2897# Custom Exceptions 

2898# ================= 

2899 

2900class ValidationError(ValueError): 

2901 """Invalid Docutils Document Tree Element.""" 

2902 def __init__(self, msg: str, problematic_element: Element = None) -> None: 

2903 super().__init__(msg) 

2904 self.problematic_element = problematic_element 

2905 

2906 

2907class TreePruningException(Exception): 

2908 """ 

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

2910 

2911 Raise subclasses from within ``visit_...`` or ``depart_...`` methods 

2912 called from `Node.walk()` and `Node.walkabout()` tree traversals to prune 

2913 the tree traversed. 

2914 """ 

2915 

2916 

2917class SkipChildren(TreePruningException): 

2918 """ 

2919 Do not visit any children of the current node. The current node's 

2920 siblings and ``depart_...`` method are not affected. 

2921 """ 

2922 

2923 

2924class SkipSiblings(TreePruningException): 

2925 """ 

2926 Do not visit any more siblings (to the right) of the current node. The 

2927 current node's children and its ``depart_...`` method are not affected. 

2928 """ 

2929 

2930 

2931class SkipNode(TreePruningException): 

2932 """ 

2933 Do not visit the current node's children, and do not call the current 

2934 node's ``depart_...`` method. 

2935 """ 

2936 

2937 

2938class SkipDeparture(TreePruningException): 

2939 """ 

2940 Do not call the current node's ``depart_...`` method. The current node's 

2941 children and siblings are not affected. 

2942 """ 

2943 

2944 

2945class NodeFound(TreePruningException): 

2946 """ 

2947 Raise to indicate that the target of a search has been found. This 

2948 exception must be caught by the client; it is not caught by the traversal 

2949 code. 

2950 """ 

2951 

2952 

2953class StopTraversal(TreePruningException): 

2954 """ 

2955 Stop the traversal altogether. The current node's ``depart_...`` method 

2956 is not affected. The parent nodes ``depart_...`` methods are also called 

2957 as usual. No other nodes are visited. This is an alternative to 

2958 NodeFound that does not cause exception handling to trickle up to the 

2959 caller. 

2960 """ 

2961 

2962 

2963# definition moved here from `utils` to avoid circular import dependency 

2964def unescape(text: str, 

2965 restore_backslashes: bool = False, 

2966 respect_whitespace: bool = False, 

2967 ) -> str: 

2968 """ 

2969 Return a string with nulls removed or restored to backslashes. 

2970 Backslash-escaped spaces are also removed. 

2971 """ 

2972 # `respect_whitespace` is ignored (since introduction 2016-12-16) 

2973 if restore_backslashes: 

2974 return text.replace('\x00', '\\') 

2975 else: 

2976 for sep in ['\x00 ', '\x00\n', '\x00']: 

2977 text = ''.join(text.split(sep)) 

2978 return text 

2979 

2980 

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

2982 """ 

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

2984 

2985 Docutils identifiers will conform to the regular expression 

2986 ``[a-z](-?[a-z0-9]+)*``. For CSS compatibility, identifiers (the "class" 

2987 and "id" attributes) should have no underscores, colons, or periods. 

2988 Hyphens may be used. 

2989 

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

2991 

2992 ID and NAME tokens must begin with a letter ([A-Za-z]) and may be 

2993 followed by any number of letters, digits ([0-9]), hyphens ("-"), 

2994 underscores ("_"), colons (":"), and periods ("."). 

2995 

2996 - However the `CSS1 spec`_ defines identifiers based on the "name" token, 

2997 a tighter interpretation ("flex" tokenizer notation; "latin1" and 

2998 "escape" 8-bit characters have been replaced with entities):: 

2999 

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

3001 latin1 [&iexcl;-&yuml;] 

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

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

3004 name {nmchar}+ 

3005 

3006 The CSS1 "nmchar" rule does not include underscores ("_"), colons (":"), 

3007 or periods ("."), therefore "class" and "id" attributes should not contain 

3008 these characters. They should be replaced with hyphens ("-"). Combined 

3009 with HTML's requirements (the first character must be a letter; no 

3010 "unicode", "latin1", or "escape" characters), this results in the 

3011 ``[a-z](-?[a-z0-9]+)*`` pattern. 

3012 

3013 .. _HTML 4.01 spec: https://www.w3.org/TR/html401 

3014 .. _CSS1 spec: https://www.w3.org/TR/REC-CSS1 

3015 """ 

3016 id = string.lower() 

3017 id = id.translate(_non_id_translate_digraphs) 

3018 id = id.translate(_non_id_translate) 

3019 # get rid of non-ascii characters. 

3020 # 'ascii' lowercase to prevent problems with turkish locale. 

3021 id = unicodedata.normalize( 

3022 'NFKD', id).encode('ascii', 'ignore').decode('ascii') 

3023 # shrink runs of whitespace and replace by hyphen 

3024 id = _non_id_chars.sub('-', ' '.join(id.split())) 

3025 id = _non_id_at_ends.sub('', id) 

3026 return str(id) 

3027 

3028 

3029_non_id_chars: re.Pattern[str] = re.compile('[^a-z0-9]+') 

3030_non_id_at_ends: re.Pattern[str] = re.compile('^[-0-9]+|-+$') 

3031_non_id_translate: dict[int, str] = { 

3032 0x00f8: 'o', # o with stroke 

3033 0x0111: 'd', # d with stroke 

3034 0x0127: 'h', # h with stroke 

3035 0x0131: 'i', # dotless i 

3036 0x0142: 'l', # l with stroke 

3037 0x0167: 't', # t with stroke 

3038 0x0180: 'b', # b with stroke 

3039 0x0183: 'b', # b with topbar 

3040 0x0188: 'c', # c with hook 

3041 0x018c: 'd', # d with topbar 

3042 0x0192: 'f', # f with hook 

3043 0x0199: 'k', # k with hook 

3044 0x019a: 'l', # l with bar 

3045 0x019e: 'n', # n with long right leg 

3046 0x01a5: 'p', # p with hook 

3047 0x01ab: 't', # t with palatal hook 

3048 0x01ad: 't', # t with hook 

3049 0x01b4: 'y', # y with hook 

3050 0x01b6: 'z', # z with stroke 

3051 0x01e5: 'g', # g with stroke 

3052 0x0225: 'z', # z with hook 

3053 0x0234: 'l', # l with curl 

3054 0x0235: 'n', # n with curl 

3055 0x0236: 't', # t with curl 

3056 0x0237: 'j', # dotless j 

3057 0x023c: 'c', # c with stroke 

3058 0x023f: 's', # s with swash tail 

3059 0x0240: 'z', # z with swash tail 

3060 0x0247: 'e', # e with stroke 

3061 0x0249: 'j', # j with stroke 

3062 0x024b: 'q', # q with hook tail 

3063 0x024d: 'r', # r with stroke 

3064 0x024f: 'y', # y with stroke 

3065} 

3066_non_id_translate_digraphs: dict[int, str] = { 

3067 0x00df: 'sz', # ligature sz 

3068 0x00e6: 'ae', # ae 

3069 0x0153: 'oe', # ligature oe 

3070 0x0238: 'db', # db digraph 

3071 0x0239: 'qp', # qp digraph 

3072} 

3073 

3074 

3075def dupname(node: Element, name: str) -> None: 

3076 node['dupnames'].append(name) 

3077 node['names'].remove(name) 

3078 # Assume that `node` is referenced, even though it isn't; 

3079 # we don't want to throw unnecessary system_messages. 

3080 node.referenced = True 

3081 

3082 

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

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

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

3086 

3087 

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

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

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

3091 

3092 

3093def serial_escape(value: str) -> str: 

3094 """Escape string values that are elements of a list, for serialization.""" 

3095 return value.replace('\\', r'\\').replace(' ', r'\ ') 

3096 

3097 

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

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

3100 

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

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

3103 

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

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

3106 

3107 Provisional. 

3108 """ 

3109 s = s.replace('\\', '\x00') # escape with NULL char 

3110 s = s.replace('\x00\x00', '\\') # unescape backslashes 

3111 s = s.replace('\x00 ', '\x00\x00') # escaped spaces -> NULL NULL 

3112 names = s.split(' ') 

3113 # restore internal spaces, drop other escaping characters 

3114 return [name.replace('\x00\x00', ' ').replace('\x00', '') 

3115 for name in names] 

3116 

3117 

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

3119 """Quote attributes for pseudo-xml""" 

3120 return '"%s"' % value 

3121 

3122 

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

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

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

3126 

3127 `unit_pattern` is a regular expression describing recognized units. 

3128 The default is suited for (but not limited to) CSS3 units and SI units. 

3129 It matches runs of ASCII letters or Greek mu, a single percent sign, 

3130 or no unit. 

3131 

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

3133 

3134 Provisional. 

3135 """ 

3136 match = re.fullmatch(f'(-?[0-9.]+) *({unit_pattern})', measure) 

3137 try: 

3138 try: 

3139 value = int(match.group(1)) 

3140 except ValueError: 

3141 value = float(match.group(1)) 

3142 unit = match.group(2) 

3143 except (AttributeError, ValueError): 

3144 raise ValueError(f'"{measure}" is no valid measure.') 

3145 return value, unit 

3146 

3147 

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

3149 

3150# Ensure the expected Python `data type`__, normalize, and check for 

3151# restrictions. 

3152# 

3153# The methods can be used to convert `str` values (eg. from an XML 

3154# representation) or to validate an existing document tree or node. 

3155# 

3156# Cf. `Element.validate_attributes()`, `docutils.parsers.docutils_xml`, 

3157# and the `attribute_validating_functions` mapping below. 

3158# 

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

3160# __ https://docutils.sourceforge.io/docs/ref/doctree.html#attribute-types 

3161 

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

3163 """ 

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

3165 

3166 Provisional. 

3167 """ 

3168 def validate_keywords(value: str) -> str: 

3169 if value not in keywords: 

3170 allowed = '", \"'.join(keywords) 

3171 raise ValueError(f'"{value}" is not one of "{allowed}".') 

3172 return value 

3173 return validate_keywords 

3174 

3175 

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

3177 """ 

3178 Validate identifier key or class name. 

3179 

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

3181 

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

3183 

3184 Provisional. 

3185 """ 

3186 if value != make_id(value): 

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

3188 return value 

3189 

3190 

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

3192 """ 

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

3194 

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

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

3197 

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

3199 

3200 __ https://docutils.sourceforge.io/docs/ref/doctree.html#classnames-type 

3201 __ https://docutils.sourceforge.io/docs/ref/doctree.html#ids-type 

3202 __ https://docutils.sourceforge.io/docs/ref/doctree.html#idrefs-type 

3203 

3204 Provisional. 

3205 """ 

3206 if isinstance(value, str): 

3207 value = value.split() 

3208 for token in value: 

3209 validate_identifier(token) 

3210 return value 

3211 

3212 

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

3214 """ 

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

3216 

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

3218 

3219 The unit may be a run of ASCII letters or Greek mu, a single percent sign, 

3220 or the empty string. Case is preserved. 

3221 

3222 Provisional. 

3223 

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

3225 """ 

3226 value, unit = parse_measure(measure) 

3227 return f'{value}{unit}' 

3228 

3229 

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

3231 """Validate the "colwidth__" attribute. 

3232 

3233 Provisional: 

3234 `measure` must be a `str` and will be returned as normalized `str` 

3235 (with unit "*" for proportional values) in Docutils 1.0. 

3236 

3237 The default unit will change to "pt" in Docutils 2.0. 

3238 

3239 __ https://docutils.sourceforge.io/docs/ref/doctree.html#colwidth 

3240 """ 

3241 if isinstance(measure, (int, float)): 

3242 value = measure 

3243 elif measure in ('*', ''): # short for '1*' 

3244 value = 1 

3245 else: 

3246 try: 

3247 value, _unit = parse_measure(measure, unit_pattern='[*]?') 

3248 except ValueError: 

3249 value = -1 

3250 if value <= 0: 

3251 raise ValueError(f'"{measure}" is no proportional measure.') 

3252 return value 

3253 

3254 

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

3256 """ 

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

3258 

3259 Provisional. 

3260 """ 

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

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

3263 return value 

3264 

3265 

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

3267 """ 

3268 Validate a list of "name tokens". 

3269 

3270 Provisional. 

3271 """ 

3272 if isinstance(value, str): 

3273 value = value.split() 

3274 for token in value: 

3275 validate_NMTOKEN(token) 

3276 return value 

3277 

3278 

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

3280 """ 

3281 Validate a list of `reference names`__. 

3282 

3283 Reference names may contain all characters; 

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

3285 

3286 `value` may be either a `list` of names or a `str` with 

3287 space separated names (with internal spaces backslash escaped 

3288 and literal backslashes doubled cf. `serial_escape()`). 

3289 

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

3291 

3292 Provisional. 

3293 

3294 __ https://docutils.sourceforge.io/docs/ref/doctree.html#reference-name 

3295 """ 

3296 if isinstance(value, str): 

3297 value = split_name_list(value) 

3298 return [whitespace_normalize_name(name) for name in value] 

3299 

3300 

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

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

3303 

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

3305 values are converterd with `bool()`. 

3306 

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

3308 """ 

3309 if value == "0": 

3310 return False 

3311 return bool(value) 

3312 

3313 

3314ATTRIBUTE_VALIDATORS: dict[str, Callable[[str], Any]] = { 

3315 'alt': str, # CDATA 

3316 'align': str, 

3317 'anonymous': validate_yesorno, 

3318 'auto': str, # CDATA (only '1' or '*' are used in rST) 

3319 'backrefs': validate_identifier_list, 

3320 'bullet': str, # CDATA (only '-', '+', or '*' are used in rST) 

3321 'classes': validate_identifier_list, 

3322 'char': str, # from Exchange Table Model (CALS), currently ignored 

3323 'charoff': validate_NMTOKEN, # from CALS, currently ignored 

3324 'colname': validate_NMTOKEN, # from CALS, currently ignored 

3325 'colnum': int, # from CALS, currently ignored 

3326 'cols': int, # from CALS: "NMTOKEN, […] must be an integer > 0". 

3327 'colsep': validate_yesorno, 

3328 'colwidth': validate_colwidth, # see docstring for pending changes 

3329 'content': str, # <meta> 

3330 'delimiter': str, 

3331 'dir': create_keyword_validator('ltr', 'rtl', 'auto'), # <meta> 

3332 'dupnames': validate_refname_list, 

3333 'enumtype': create_keyword_validator('arabic', 'loweralpha', 'lowerroman', 

3334 'upperalpha', 'upperroman'), 

3335 'format': str, # CDATA (space separated format names) 

3336 'frame': create_keyword_validator('top', 'bottom', 'topbot', 'all', 

3337 'sides', 'none'), # from CALS, ignored 

3338 'height': validate_measure, 

3339 'http-equiv': str, # <meta> 

3340 'ids': validate_identifier_list, 

3341 'lang': str, # <meta> 

3342 'level': int, 

3343 'line': int, 

3344 'ltrim': validate_yesorno, 

3345 'loading': create_keyword_validator('embed', 'link', 'lazy'), 

3346 'media': str, # <meta> 

3347 'morecols': int, 

3348 'morerows': int, 

3349 'name': whitespace_normalize_name, # in <reference> (deprecated) 

3350 # 'name': node_attributes.validate_NMTOKEN, # in <meta> 

3351 'names': validate_refname_list, 

3352 'namest': validate_NMTOKEN, # start of span, from CALS, currently ignored 

3353 'nameend': validate_NMTOKEN, # end of span, from CALS, currently ignored 

3354 'pgwide': validate_yesorno, # from CALS, currently ignored 

3355 'prefix': str, 

3356 'refid': validate_identifier, 

3357 'refname': whitespace_normalize_name, 

3358 'refuri': str, 

3359 'rowsep': validate_yesorno, 

3360 'rtrim': validate_yesorno, 

3361 'scale': int, 

3362 'scheme': str, 

3363 'source': str, 

3364 'start': int, 

3365 'stub': validate_yesorno, 

3366 'suffix': str, 

3367 'title': str, 

3368 'type': validate_NMTOKEN, 

3369 'uri': str, 

3370 'valign': create_keyword_validator('top', 'middle', 'bottom'), # from CALS 

3371 'width': validate_measure, 

3372 'xml:space': create_keyword_validator('default', 'preserve'), 

3373 } 

3374""" 

3375Mapping of `attribute names`__ to validating functions. 

3376 

3377Provisional. 

3378 

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

3380"""