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

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

1272 statements  

1# $Id: nodes.py 10351 2026-06-11 21:51:21Z 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) -> bool: 

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

691 if isinstance(key, str): 

692 return key in self.attributes 

693 return key in self.children 

694 

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

696 if isinstance(key, str): 

697 return self.attributes[key] 

698 elif isinstance(key, int): 

699 return self.children[key] 

700 elif isinstance(key, slice): 

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

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

703 else: 

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

705 'an attribute name string') 

706 

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

708 if isinstance(key, str): 

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

710 elif isinstance(key, int): 

711 self.setup_child(item) 

712 self.children[key] = item 

713 elif isinstance(key, slice): 

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

715 for node in item: 

716 self.setup_child(node) 

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

718 else: 

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

720 'an attribute name string') 

721 

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

723 if isinstance(key, str): 

724 del self.attributes[key] 

725 elif isinstance(key, int): 

726 del self.children[key] 

727 elif isinstance(key, slice): 

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

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

730 else: 

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

732 'slice, or an attribute name string') 

733 

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

735 return self.children + other 

736 

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

738 return other + self.children 

739 

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

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

742 if isinstance(other, Node): 

743 self.append(other) 

744 elif other is not None: 

745 self.extend(other) 

746 return self 

747 

748 def astext(self) -> str: 

749 return self.child_text_separator.join( 

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

751 

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

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

754 if self.is_not_default(key)} 

755 return atts 

756 

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

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

759 

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

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

762 

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

764 return attr in self.attributes 

765 

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

767 if attr in self.attributes: 

768 del self.attributes[attr] 

769 

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

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

772 

773 has_key = hasattr 

774 

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

776 """Return node's language tag. 

777 

778 Look iteratively in self and parents for a class argument 

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

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

781 """ 

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

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

784 return cls.removeprefix('language-') 

785 try: 

786 return self.parent.get_language_code(fallback) 

787 except AttributeError: 

788 return fallback 

789 

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

791 self.setup_child(item) 

792 self.children.append(item) 

793 

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

795 for node in item: 

796 self.append(node) 

797 

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

799 if isinstance(item, Node): 

800 self.setup_child(item) 

801 self.children.insert(index, item) 

802 elif item is not None: 

803 self[index:index] = item 

804 

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

806 return self.children.pop(i) 

807 

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

809 self.children.remove(item) 

810 

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

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

813 

814 def previous_sibling(self): 

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

816 try: 

817 i = self.parent.index(self) 

818 except (AttributeError): 

819 return None 

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

821 

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

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

824 

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

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

827 

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

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

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

831 

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

833 for a usage example. 

834 

835 Provisional. May be changed or removed without warning. 

836 """ 

837 sections = [] 

838 node = self 

839 while node.parent is not None: 

840 if isinstance(node, section): 

841 sections.append(node) 

842 node = node.parent 

843 sections.reverse() 

844 return sections 

845 

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

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

848 return False 

849 else: 

850 return True 

851 

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

853 """ 

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

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

856 

857 Provisional. 

858 """ 

859 if isinstance(dict_, Node): 

860 dict_ = dict_.attributes 

861 for att in self.basic_attributes: 

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

863 

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

865 """ 

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

867 it. 

868 

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

870 former should specifically be a list. 

871 """ 

872 # List Concatenation 

873 for value in values: 

874 if value not in self[attr]: 

875 self[attr].append(value) 

876 

877 def coerce_append_attr_list( 

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

879 """ 

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

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

882 element. Then call append_attr_list. 

883 

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

885 """ 

886 # List Concatenation 

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

888 self[attr] = [self[attr]] 

889 if not isinstance(value, list): 

890 value = [value] 

891 self.append_attr_list(attr, value) 

892 

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

894 """ 

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

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

897 """ 

898 # One or the other 

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

900 self[attr] = value 

901 

902 def copy_attr_convert( 

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

904 """ 

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

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

907 

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

909 compatibility with the other copy functions. 

910 """ 

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

912 self.coerce_append_attr_list(attr, value) 

913 

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

915 """ 

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

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

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

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

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

921 nothing. 

922 """ 

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

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

925 isinstance(value, list): 

926 self.coerce_append_attr_list(attr, value) 

927 else: 

928 self.replace_attr(attr, value, replace) 

929 

930 def copy_attr_concatenate( 

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

932 """ 

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

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

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

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

937 Otherwise, do nothing. 

938 """ 

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

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

941 isinstance(value, list): 

942 self.append_attr_list(attr, value) 

943 else: 

944 self.replace_attr(attr, value, replace) 

945 

946 def copy_attr_consistent( 

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

948 """ 

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

950 value. Otherwise, do nothing. 

951 """ 

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

953 self.replace_attr(attr, value, replace) 

954 

955 def update_all_atts(self, 

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

957 update_fun: _UpdateFun = copy_attr_consistent, 

958 replace: bool = True, 

959 and_source: bool = False, 

960 ) -> None: 

961 """ 

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

963 

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

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

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

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

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

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

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

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

972 

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

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

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

976 on the value of update_fun. 

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

978 the update_fun method to this function. 

979 """ 

980 if isinstance(dict_, Node): 

981 dict_ = dict_.attributes 

982 

983 # Include the source attribute when copying? 

984 if and_source: 

985 filter_fun = self.is_not_list_attribute 

986 else: 

987 filter_fun = self.is_not_known_attribute 

988 

989 # Copy the basic attributes 

990 self.update_basic_atts(dict_) 

991 

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

993 # (All basic attributes should be copied already) 

994 for att in filter(filter_fun, dict_): 

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

996 

997 def update_all_atts_consistantly(self, 

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

999 replace: bool = True, 

1000 and_source: bool = False, 

1001 ) -> None: 

1002 """ 

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

1004 

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

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

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

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

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

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

1011 attribute is included in the copy. 

1012 

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

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

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

1016 on the value of update_fun. 

1017 """ 

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

1019 and_source) 

1020 

1021 def update_all_atts_concatenating(self, 

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

1023 replace: bool = True, 

1024 and_source: bool = False, 

1025 ) -> None: 

1026 """ 

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

1028 

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

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

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

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

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

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

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

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

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

1038 included in the copy. 

1039 

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

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

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

1043 on the value of update_fun. 

1044 """ 

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

1046 and_source) 

1047 

1048 def update_all_atts_coercion(self, 

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

1050 replace: bool = True, 

1051 and_source: bool = False, 

1052 ) -> None: 

1053 """ 

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

1055 

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

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

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

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

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

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

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

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

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

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

1066 included in the copy. 

1067 

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

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

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

1071 on the value of update_fun. 

1072 """ 

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

1074 and_source) 

1075 

1076 def update_all_atts_convert(self, 

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

1078 and_source: bool = False, 

1079 ) -> None: 

1080 """ 

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

1082 

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

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

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

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

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

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

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

1090 is included in the copy. 

1091 

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

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

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

1095 on the value of update_fun. 

1096 """ 

1097 self.update_all_atts(dict_, Element.copy_attr_convert, 

1098 and_source=and_source) 

1099 

1100 def clear(self) -> None: 

1101 self.children = [] 

1102 

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

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

1105 index = self.index(old) 

1106 if isinstance(new, Node): 

1107 self.setup_child(new) 

1108 self[index] = new 

1109 elif new is not None: 

1110 self[index:index+1] = new 

1111 

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

1113 """ 

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

1115 list of nodes. 

1116 

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

1118 """ 

1119 update = new 

1120 if not isinstance(new, Node): 

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

1122 try: 

1123 update = new[0] 

1124 except IndexError: 

1125 update = None 

1126 if isinstance(update, Element): 

1127 update.update_basic_atts(self) 

1128 else: 

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

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

1131 for att in self.basic_attributes: 

1132 assert not self[att], \ 

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

1134 self.parent.replace(self, new) 

1135 

1136 def first_child_matching_class(self, 

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

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

1139 start: int = 0, 

1140 end: int = sys.maxsize, 

1141 ) -> int | None: 

1142 """ 

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

1144 

1145 Parameters: 

1146 

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

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

1149 - `start`: Initial index to check. 

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

1151 """ 

1152 if not isinstance(childclass, tuple): 

1153 childclass = (childclass,) 

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

1155 for c in childclass: 

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

1157 return index 

1158 return None 

1159 

1160 def first_child_not_matching_class( 

1161 self, 

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

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

1164 start: int = 0, 

1165 end: int = sys.maxsize, 

1166 ) -> int | None: 

1167 """ 

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

1169 

1170 Parameters: 

1171 

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

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

1174 - `start`: Initial index to check. 

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

1176 """ 

1177 if not isinstance(childclass, tuple): 

1178 childclass = (childclass,) 

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

1180 for c in childclass: 

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

1182 break 

1183 else: 

1184 return index 

1185 return None 

1186 

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

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

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

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

1191 

1192 def copy(self) -> Self: 

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

1194 obj._document = self._document 

1195 obj.source = self.source 

1196 obj.line = self.line 

1197 return obj 

1198 

1199 def deepcopy(self) -> Self: 

1200 copy = self.copy() 

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

1202 return copy 

1203 

1204 def note_referenced_by(self, 

1205 name: str | None = None, 

1206 id: str | None = None, 

1207 ) -> None: 

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

1209 `name` or id `id`.""" 

1210 self.referenced = True 

1211 # Element.expect_referenced_by_* dictionaries map names or ids 

1212 # to nodes whose ``referenced`` attribute is set to true as 

1213 # soon as this node is referenced by the given name or id. 

1214 # Needed for target propagation. 

1215 by_name = getattr(self, 'expect_referenced_by_name', {}).get(name) 

1216 by_id = getattr(self, 'expect_referenced_by_id', {}).get(id) 

1217 if by_name: 

1218 assert name is not None 

1219 by_name.referenced = True 

1220 if by_id: 

1221 assert id is not None 

1222 by_id.referenced = True 

1223 

1224 @classmethod 

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

1226 """ 

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

1228 basic list attributes defined for all Elements. 

1229 """ 

1230 return attr not in cls.list_attributes 

1231 

1232 @classmethod 

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

1234 """ 

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

1236 

1237 Provisional. May be removed in Docutils 2.0. 

1238 """ 

1239 return attr not in cls.common_attributes 

1240 

1241 def validate_attributes(self) -> None: 

1242 """Normalize and validate element attributes. 

1243 

1244 Convert string values to expected datatype. 

1245 Normalize values. 

1246 

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

1248 

1249 Provisional. 

1250 """ 

1251 messages = [] 

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

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

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

1255 if key not in self.valid_attributes: 

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

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

1258 continue 

1259 try: 

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

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

1262 messages.append( 

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

1264 if messages: 

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

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

1267 problematic_element=self) 

1268 

1269 def validate_content(self, 

1270 model: _ContentModelTuple | None = None, 

1271 elements: Sequence | None = None, 

1272 ) -> list: 

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

1274 

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

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

1277 

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

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

1280 

1281 Provisional. 

1282 """ 

1283 if model is None: 

1284 model = self.content_model 

1285 if elements is None: 

1286 elements = self.children 

1287 ichildren = iter(elements) 

1288 child = next(ichildren, None) 

1289 for category, quantifier in model: 

1290 if not isinstance(child, category): 

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

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

1293 problematic_element=child) 

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

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

1296 else: 

1297 # Check additional placement constraints (if applicable): 

1298 child.validate_position() 

1299 # advance: 

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

1301 child = next(ichildren, None) 

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

1303 for child in ichildren: 

1304 if not isinstance(child, category): 

1305 break 

1306 try: 

1307 child.validate_position() 

1308 except AttributeError: 

1309 pass 

1310 else: 

1311 child = None 

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

1313 

1314 def _report_child(self, 

1315 child, 

1316 category: Element | Iterable[Element], 

1317 ) -> str: 

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

1319 try: 

1320 _type = category.__name__ 

1321 except AttributeError: 

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

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

1324 if child is None: 

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

1326 if isinstance(child, Text): 

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

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

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

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

1331 

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

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

1334 

1335 Raise ValidationError if there are violations. 

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

1337 

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

1339 Docutils Document Model. 

1340 

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

1342 

1343 Provisional (work in progress). 

1344 """ 

1345 self.validate_attributes() 

1346 

1347 leftover_childs = self.validate_content() 

1348 for child in leftover_childs: 

1349 if isinstance(child, Text): 

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

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

1352 problematic_element=self) 

1353 else: 

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

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

1356 'not allowed at this position.', 

1357 problematic_element=child) 

1358 

1359 if recursive: 

1360 for child in self: 

1361 child.validate(recursive=recursive) 

1362 

1363 

1364# ==================== 

1365# Element Categories 

1366# ==================== 

1367# 

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

1369 

1370class Root: 

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

1372 

1373 

1374class Structural: 

1375 """`Structural elements`__. 

1376 

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

1378 #structural-elements 

1379 """ 

1380 

1381 

1382class SubStructural: 

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

1384 

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

1386 

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

1388 #structural-subelements 

1389 """ 

1390 

1391 

1392class Bibliographic: 

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

1394 

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

1396 #bibliographic-elements 

1397 """ 

1398 

1399 

1400class Body: 

1401 """`Body elements`__. 

1402 

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

1404 """ 

1405 

1406 

1407class Admonition(Body): 

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

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

1410 

1411 

1412class Sequential(Body): 

1413 """List-like body elements.""" 

1414 

1415 

1416class General(Body): 

1417 """Miscellaneous body elements.""" 

1418 

1419 

1420class Special(Body): 

1421 """Special internal body elements.""" 

1422 

1423 

1424class Part: 

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

1426 

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

1428 """ 

1429 

1430 

1431class Decorative: 

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

1433 

1434 Children of `decoration`. 

1435 """ 

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

1437 

1438 

1439class Inline: 

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

1441 """ 

1442 

1443 

1444# Orthogonal categories and Mixins 

1445# ================================ 

1446 

1447class PreBibliographic: 

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

1449 

1450 

1451class Invisible(Special, PreBibliographic): 

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

1453 

1454 

1455class Labeled: 

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

1457 

1458 

1459class Resolvable: 

1460 resolved: bool = False 

1461 

1462 

1463class BackLinkable: 

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

1465 

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

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

1468 

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

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

1471 

1472 

1473class Referential(Resolvable): 

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

1475 

1476 

1477class Targetable(Resolvable): 

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

1479 referenced: int = 0 

1480 

1481 

1482class Titular: 

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

1484 

1485 

1486class TextElement(Element): 

1487 """ 

1488 An element which directly contains text. 

1489 

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

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

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

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

1494 standalone body elements. 

1495 

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

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

1498 """ 

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

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

1501 

1502 child_text_separator: Final = '' 

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

1504 

1505 def __init__(self, 

1506 rawsource: str = '', 

1507 text: str = '', 

1508 *children, 

1509 **attributes: Any, 

1510 ) -> None: 

1511 if text: 

1512 textnode = Text(text) 

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

1514 **attributes) 

1515 else: 

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

1517 

1518 

1519class FixedTextElement(TextElement): 

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

1521 

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

1523 

1524 def __init__(self, 

1525 rawsource: str = '', 

1526 text: str = '', 

1527 *children, 

1528 **attributes: Any, 

1529 ) -> None: 

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

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

1532 

1533 

1534class PureTextElement(TextElement): 

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

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

1537 

1538 

1539# ================================= 

1540# Concrete Document Tree Elements 

1541# ================================= 

1542# 

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

1544 

1545# Special purpose elements 

1546# ======================== 

1547# 

1548# Body elements for internal use or special requests. 

1549 

1550class comment(Invisible, FixedTextElement, PureTextElement): 

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

1552 

1553 

1554class substitution_definition(Invisible, TextElement): 

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

1556 

1557 

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

1559 valid_attributes: Final = Element.valid_attributes + ( 

1560 'anonymous', 'refid', 'refname', 'refuri') 

1561 

1562 

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

1564 """ 

1565 System message element. 

1566 

1567 Do not instantiate this class directly; use 

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

1569 """ 

1570 valid_attributes: Final = BackLinkable.valid_attributes + ( 

1571 'level', 'line', 'type') 

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

1573 

1574 def __init__(self, 

1575 message: str | None = None, 

1576 *children, 

1577 **attributes: Any, 

1578 ) -> None: 

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

1580 if message: 

1581 p = paragraph('', message) 

1582 children = (p,) + children 

1583 try: 

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

1585 except: # NoQA: E722 (catchall) 

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

1587 raise 

1588 

1589 def astext(self) -> str: 

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

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

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

1593 

1594 

1595class pending(Invisible, Element): 

1596 """ 

1597 Placeholder for pending operations. 

1598 

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

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

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

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

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

1604 instance attributes. 

1605 

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

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

1608 document, with a directive:: 

1609 

1610 .. contents:: 

1611 

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

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

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

1615 processing, something like this:: 

1616 

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

1618 

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

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

1621 transforms. 

1622 """ 

1623 

1624 def __init__(self, 

1625 transform: Transform, 

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

1627 rawsource: str = '', 

1628 *children, 

1629 **attributes: Any, 

1630 ) -> None: 

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

1632 

1633 self.transform: Transform = transform 

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

1635 operation.""" 

1636 

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

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

1639 

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

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

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

1643 self.transform.__name__), 

1644 ' .details:'] 

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

1646 for key, value in details: 

1647 if isinstance(value, Node): 

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

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

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

1651 elif (value 

1652 and isinstance(value, list) 

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

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

1655 for v in value: 

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

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

1658 else: 

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

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

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

1662 for line in internals)) 

1663 

1664 def copy(self) -> Self: 

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

1666 **self.attributes) 

1667 obj._document = self._document 

1668 obj.source = self.source 

1669 obj.line = self.line 

1670 return obj 

1671 

1672 

1673class raw(Special, Inline, PreBibliographic, 

1674 FixedTextElement, PureTextElement): 

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

1676 

1677 Can be used as Body element or Inline element. 

1678 """ 

1679 valid_attributes: Final = Element.valid_attributes + ( 

1680 'format', 'xml:space') 

1681 

1682 

1683# Decorative Elements 

1684# =================== 

1685 

1686class header(Decorative, Element): pass 

1687class footer(Decorative, Element): pass 

1688 

1689 

1690# Structural Subelements 

1691# ====================== 

1692 

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

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

1695 """ 

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

1697 

1698 

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

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

1701 

1702 def validate_position(self) -> None: 

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

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

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

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

1707 problematic_element=self) 

1708 

1709 

1710class meta(PreBibliographic, SubStructural, Element): 

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

1712 valid_attributes: Final = Element.valid_attributes + ( 

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

1714 

1715 

1716class docinfo(SubStructural, Element): 

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

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

1719 # (%bibliographic.elements;)+ 

1720 

1721 

1722class decoration(PreBibliographic, SubStructural, Element): 

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

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

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

1726 ) 

1727 # (header?, footer?) 

1728 

1729 def get_header(self) -> header: 

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

1731 self.insert(0, header()) 

1732 return self.children[0] 

1733 

1734 def get_footer(self) -> footer: 

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

1736 self.append(footer()) 

1737 return self.children[-1] 

1738 

1739 

1740class transition(SubStructural, Element): 

1741 """Transitions__ represent "semantic breaks". 

1742 

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

1744 """ 

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

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

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

1748 subtitle, target, title) 

1749 

1750 def validate_position(self) -> None: 

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

1752 

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

1754 nor may two transitions be immediately adjacent. 

1755 """ 

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

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

1758 messages.append( 

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

1760 i = self.parent.index(self) 

1761 prev_siblings = self.parent[:i] 

1762 if not [sibling for sibling in prev_siblings 

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

1764 messages.append( 

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

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

1767 if not [sibling for sibling in next_siblings 

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

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

1770 if len(messages) > 1: 

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

1772 problematic_element=self) 

1773 

1774 

1775# Structural Elements 

1776# =================== 

1777 

1778class topic(Structural, Element): 

1779 """ 

1780 Topics__ are non-recursive, mini-sections. 

1781 

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

1783 """ 

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

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

1786 

1787 

1788class sidebar(Structural, Element): 

1789 """ 

1790 Sidebars__ are like parallel documents providing related material. 

1791 

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

1793 of the page 

1794 

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

1796 """ 

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

1798 (subtitle, '?'), 

1799 ((topic, Body), '+'), 

1800 ) 

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

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

1803 

1804 

1805class section(Structural, Element): 

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

1807 

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

1809 """ 

1810 # recursive content model, see below 

1811 

1812 

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

1814 (subtitle, '?'), 

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

1816 ((section, transition), '*'), 

1817 ) 

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

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

1820 

1821 

1822# Root Element 

1823# ============ 

1824 

1825class document(Root, Element): 

1826 """ 

1827 The document root element. 

1828 

1829 Do not instantiate this class directly; use 

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

1831 """ 

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

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

1834 (subtitle, '?'), 

1835 (meta, '*'), 

1836 (decoration, '?'), 

1837 (docinfo, '?'), 

1838 (transition, '?'), 

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

1840 ((section, transition), '*'), 

1841 ) 

1842 # ( (title, subtitle?)?, 

1843 # meta*, 

1844 # decoration?, 

1845 # (docinfo, transition?)?, 

1846 # %structure.model; ) 

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

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

1849 

1850 def __init__(self, 

1851 settings: Values, 

1852 reporter: Reporter, 

1853 *args, 

1854 **kwargs: Any, 

1855 ) -> None: 

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

1857 

1858 self.current_source: StrPath | None = None 

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

1860 

1861 self.current_line: int | None = None 

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

1863 

1864 self.settings: Values = settings 

1865 """Runtime settings data record.""" 

1866 

1867 self.reporter: Reporter = reporter 

1868 """System message generator.""" 

1869 

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

1871 """List of indirect target nodes.""" 

1872 

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

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

1875 

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

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

1878 

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

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

1881 

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

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

1884 

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

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

1887 

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

1889 """Mapping of ids to nodes.""" 

1890 

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

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

1893 

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

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

1896 """ 

1897 

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

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

1900 

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

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

1903 

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

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

1906 

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

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

1909 

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

1911 """List of symbol footnote nodes.""" 

1912 

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

1914 """List of symbol footnote_reference nodes.""" 

1915 

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

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

1918 

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

1920 """List of citation nodes.""" 

1921 

1922 self.autofootnote_start: int = 1 

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

1924 

1925 self.symbol_footnote_start: int = 0 

1926 """Initial symbol footnote symbol index.""" 

1927 

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

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

1930 

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

1932 """System messages generated while parsing.""" 

1933 

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

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

1936 

1937 import docutils.transforms 

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

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

1940 

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

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

1943 

1944 self.decoration: decoration | None = None 

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

1946 

1947 self._document: document = self 

1948 

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

1950 """ 

1951 Return dict with unpicklable references removed. 

1952 """ 

1953 state = self.__dict__.copy() 

1954 state['reporter'] = None 

1955 state['transformer'] = None 

1956 return state 

1957 

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

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

1960 if dom is None: 

1961 import xml.dom.minidom as dom 

1962 domroot = dom.Document() 

1963 domroot.appendChild(self._dom_node(domroot)) 

1964 return domroot 

1965 

1966 def set_id(self, 

1967 node: Element, 

1968 msgnode: Element | None = None, 

1969 suggested_prefix: str = '', 

1970 ) -> str: 

1971 """ 

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

1973 

1974 Check `node`s identifiers for duplicates, 

1975 create a new identifier if there are none. 

1976 Update `document.ids` and `document.nameids`. 

1977 

1978 Provisional. 

1979 """ 

1980 if not node['ids']: 

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

1982 # register and check for duplicates 

1983 for id in node['ids']: 

1984 self.ids.setdefault(id, node) 

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

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

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

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

1989 base_node=node) 

1990 if msgnode is not None: 

1991 msgnode += msg 

1992 for name in node['names']: 

1993 self.nameids[name] = id 

1994 return id 

1995 

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

1997 # Internal auxiliary method for set_id(): 

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

1999 # See also make_id() 

2000 id_prefix = self.settings.id_prefix 

2001 auto_id_prefix = self.settings.auto_id_prefix 

2002 base_id = '' 

2003 id = '' 

2004 for name in node['names']: 

2005 if id_prefix: # allow names starting with numbers 

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

2007 else: 

2008 base_id = make_id(name) 

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

2010 id = id_prefix + base_id 

2011 if base_id and id not in self.ids: 

2012 break 

2013 else: 

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

2015 # disambiguate name-derived ID 

2016 # TODO: remove second condition after announcing change 

2017 prefix = id + '-' 

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

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

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

2021 else: 

2022 prefix = id_prefix + auto_id_prefix 

2023 if prefix.endswith('%'): 

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

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

2026 while True: 

2027 self.id_counter[prefix] += 1 

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

2029 if id not in self.ids: 

2030 break 

2031 return id 

2032 

2033 def set_name_id_map(self, 

2034 node: Element, 

2035 id: str, 

2036 msgnode: Element | None = None, 

2037 explicit: bool = False, 

2038 ) -> None: 

2039 """Deprecated. Will be removed in Docutils 1.0.""" 

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

2041 ' in Docutils 1.0.', DeprecationWarning, stacklevel=2) 

2042 self.note_names(node, msgnode, explicit) 

2043 for name in node['names']: 

2044 self.nameids[name] = id 

2045 

2046 def set_duplicate_name(self, 

2047 node: Element, 

2048 name: str, 

2049 msgnode: Element, 

2050 explicit: bool, 

2051 ) -> None: 

2052 """ 

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

2054 

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

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

2057 

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

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

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

2061 

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

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

2064 

2065 The following state transition table shows how the values 

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

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

2068 

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

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

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

2072 

2073 ======== ==== ======== ==== ======== =============== ======= 

2074 Input Old State New State Action 

2075 -------- -------------- -------------- ------------------------ 

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

2077 ======== ==== ======== ==== ======== =============== ======= 

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

2079 implicit old explicit old explicit new INFO 

2080 explicit old implicit new explicit old INFO 

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

2082 explicit None explicit None explicit new WARNING 

2083 implicit None explicit None explicit new INFO 

2084 explicit None implicit new explicit 

2085 implicit None implicit None implicit new INFO 

2086 ======== ==== ======== ==== ======== =============== ======= 

2087 

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

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

2090 

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

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

2093 

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

2095 #implicit-hyperlink-targets 

2096 

2097 Provisional. 

2098 """ 

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

2100 old_explicit = self.nametypes[name] 

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

2102 

2103 self.nametypes[name] = old_explicit or explicit 

2104 

2105 if old_node is not None and ( 

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

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

2108 ): 

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

2110 level = 1 

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

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

2113 dupname(node, name) 

2114 elif explicit: 

2115 if old_explicit: 

2116 level = 2 

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

2118 dupname(node, name) 

2119 if old_node is not None: 

2120 dupname(old_node, name) 

2121 self.names[name] = None 

2122 self.nameids[name] = None 

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

2124 self.names[name] = node 

2125 if old_node is not None: 

2126 level = 1 

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

2128 dupname(old_node, name) 

2129 else: # new name is implicit 

2130 level = 1 

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

2132 dupname(node, name) 

2133 if old_node is not None and not old_explicit: 

2134 dupname(old_node, name) 

2135 self.names[name] = None 

2136 self.nameids[name] = None 

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

2138 if level: 

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

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

2141 backrefs = [] 

2142 else: 

2143 backrefs = [self.set_id(node)] 

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

2145 base_node=node) 

2146 # try appending near to the problem: 

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

2148 msgnode += msg 

2149 

2150 def note_names(self, 

2151 node: Element, 

2152 msgnode: Element|None = None, 

2153 explicit: bool = False, 

2154 ) -> None: 

2155 """ 

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

2157 

2158 Update `self.names` and `self.nametypes` 

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

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

2161 """ 

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

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

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

2165 # attention: modifies node['names'] 

2166 else: 

2167 self.names[name] = node 

2168 self.nametypes.setdefault(name, explicit) 

2169 

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

2171 # TODO: deprecate? (use ``name in document.names``) 

2172 return name in self.names 

2173 

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

2175 def note_implicit_target(self, target: Element, 

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

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

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

2179 self.set_id(target, msgnode) 

2180 

2181 def note_explicit_target(self, target: Element, 

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

2183 self.note_names(target, msgnode, explicit=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 self.set_id(target) 

2199 

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

2201 self.set_id(footnote) 

2202 self.autofootnotes.append(footnote) 

2203 

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

2205 self.set_id(ref) 

2206 self.autofootnote_refs.append(ref) 

2207 

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

2209 self.set_id(footnote) 

2210 self.symbol_footnotes.append(footnote) 

2211 

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

2213 self.set_id(ref) 

2214 self.symbol_footnote_refs.append(ref) 

2215 

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

2217 self.set_id(footnote) 

2218 self.footnotes.append(footnote) 

2219 

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

2221 self.set_id(ref) 

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

2223 self.note_refname(ref) 

2224 

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

2226 self.citations.append(citation) 

2227 

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

2229 self.set_id(ref) 

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

2231 self.note_refname(ref) 

2232 

2233 def note_substitution_def(self, 

2234 subdef: substitution_definition, 

2235 def_name: str, 

2236 msgnode: Element | None = None, 

2237 ) -> None: 

2238 name = whitespace_normalize_name(def_name) 

2239 if name in self.substitution_defs: 

2240 msg = self.reporter.error( 

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

2242 base_node=subdef) 

2243 if msgnode is not None: 

2244 msgnode += msg 

2245 oldnode = self.substitution_defs[name] 

2246 dupname(oldnode, name) 

2247 # keep only the last definition: 

2248 self.substitution_defs[name] = subdef 

2249 # case-insensitive mapping: 

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

2251 

2252 def note_substitution_ref(self, 

2253 subref: substitution_reference, 

2254 refname: str, 

2255 ) -> None: 

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

2257 

2258 def note_pending( 

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

2260 self.transformer.add_pending(pending, priority) 

2261 

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

2263 self.parse_messages.append(message) 

2264 

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

2266 self.transform_messages.append(message) 

2267 

2268 def note_source(self, 

2269 source: StrPath | None, 

2270 offset: int | None, 

2271 ) -> None: 

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

2273 if offset is None: 

2274 self.current_line = offset 

2275 else: 

2276 self.current_line = offset + 1 

2277 

2278 def copy(self) -> Self: 

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

2280 **self.attributes) 

2281 obj.source = self.source 

2282 obj.line = self.line 

2283 return obj 

2284 

2285 def get_decoration(self) -> decoration: 

2286 if not self.decoration: 

2287 self.decoration: decoration = decoration() 

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

2289 if index is None: 

2290 self.append(self.decoration) 

2291 else: 

2292 self.insert(index, self.decoration) 

2293 return self.decoration 

2294 

2295 

2296# Bibliographic Elements 

2297# ====================== 

2298 

2299class author(Bibliographic, TextElement): pass 

2300class organization(Bibliographic, TextElement): pass 

2301class address(Bibliographic, FixedTextElement): pass 

2302class contact(Bibliographic, TextElement): pass 

2303class version(Bibliographic, TextElement): pass 

2304class revision(Bibliographic, TextElement): pass 

2305class status(Bibliographic, TextElement): pass 

2306class date(Bibliographic, TextElement): pass 

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

2308 

2309 

2310class authors(Bibliographic, Element): 

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

2312 """ 

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

2314 (organization, '?'), 

2315 (address, '?'), 

2316 (contact, '?'), 

2317 ) 

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

2319 

2320 def validate_content(self, 

2321 model: _ContentModelTuple | None = None, 

2322 elements: Sequence | None = None, 

2323 ) -> list: 

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

2325 

2326 Provisional. 

2327 """ 

2328 relics = super().validate_content() 

2329 while relics: 

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

2331 return relics 

2332 

2333 

2334# Body Elements 

2335# ============= 

2336# 

2337# General 

2338# ------- 

2339# 

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

2341 

2342class paragraph(General, TextElement): pass 

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

2344 

2345 

2346class compound(General, Element): 

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

2348 

2349 

2350class container(General, Element): 

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

2352 

2353 

2354class attribution(Part, TextElement): 

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

2356 

2357 

2358class block_quote(General, Element): 

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

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

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

2362 

2363 

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

2365 valid_attributes: Final = Element.valid_attributes + ( 

2366 'anonymous', 'refid', 'refname', 'refuri') 

2367 

2368 

2369# Lists 

2370# ----- 

2371# 

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

2373 

2374class list_item(Part, Element): 

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

2376 

2377 

2378class bullet_list(Sequential, Element): 

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

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

2381 

2382 

2383class enumerated_list(Sequential, Element): 

2384 valid_attributes: Final = Element.valid_attributes + ( 

2385 'enumtype', 'prefix', 'suffix', 'start') 

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

2387 

2388 

2389class term(Part, TextElement): pass 

2390class classifier(Part, TextElement): pass 

2391 

2392 

2393class definition(Part, Element): 

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

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

2396 

2397 

2398class definition_list_item(Part, Element): 

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

2400 ((classifier, term), '*'), 

2401 (definition, '.'), 

2402 ) 

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

2404 

2405 

2406class definition_list(Sequential, Element): 

2407 """List of terms and their definitions. 

2408 

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

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

2411 """ 

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

2413 # (definition_list_item+) 

2414 

2415 

2416class field_name(Part, TextElement): pass 

2417 

2418 

2419class field_body(Part, Element): 

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

2421 

2422 

2423class field(Part, Bibliographic, Element): 

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

2425 # (field_name, field_body) 

2426 

2427 

2428class field_list(Sequential, Element): 

2429 """List of label & data pairs. 

2430 

2431 Typically rendered as a two-column list. 

2432 Also used for extension syntax or special processing. 

2433 """ 

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

2435 

2436 

2437class option_string(Part, PureTextElement): 

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

2439 

2440 

2441class option_argument(Part, PureTextElement): 

2442 """Placeholder text for option arguments.""" 

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

2444 

2445 def astext(self) -> str: 

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

2447 

2448 

2449class option(Part, Element): 

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

2451 

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

2453 """ 

2454 child_text_separator: Final = '' 

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

2456 # (option_string, option_argument*) 

2457 

2458 

2459class option_group(Part, Element): 

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

2461 child_text_separator: Final = ', ' 

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

2463 

2464 

2465class description(Part, Element): 

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

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

2468 

2469 

2470class option_list_item(Part, Element): 

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

2472 """ 

2473 child_text_separator: Final = ' ' 

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

2475 # (option_group, description) 

2476 

2477 

2478class option_list(Sequential, Element): 

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

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

2481 

2482 

2483# Pre-formatted text blocks 

2484# ------------------------- 

2485 

2486class literal_block(General, FixedTextElement): pass 

2487class doctest_block(General, FixedTextElement): pass 

2488 

2489 

2490class math_block(General, FixedTextElement, PureTextElement): 

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

2492 

2493 

2494class line(Part, TextElement): 

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

2496 indent: str | None = None 

2497 

2498 

2499class line_block(General, Element): 

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

2501 """ 

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

2503 

2504 

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

2506 

2507 

2508# Admonitions 

2509# ----------- 

2510# distinctive and self-contained notices 

2511 

2512class attention(Admonition, Element): pass 

2513class caution(Admonition, Element): pass 

2514class danger(Admonition, Element): pass 

2515class error(Admonition, Element): pass 

2516class important(Admonition, Element): pass 

2517class note(Admonition, Element): pass 

2518class tip(Admonition, Element): pass 

2519class hint(Admonition, Element): pass 

2520class warning(Admonition, Element): pass 

2521 

2522 

2523class admonition(Admonition, Element): 

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

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

2526 

2527 

2528# Footnote and citation 

2529# --------------------- 

2530 

2531class label(Part, PureTextElement): 

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

2533 

2534 

2535class footnote(General, BackLinkable, Element, Labeled, Targetable): 

2536 """Labelled note providing additional context (footnote or endnote).""" 

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

2538 content_model: Final = ((label, '?'), (Body, '+')) 

2539 # (label?, (%body.elements;)+) 

2540 # The label will become required in Docutils 1.0. 

2541 

2542 

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

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

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

2546 

2547 

2548# Graphical elements 

2549# ------------------ 

2550 

2551class image(General, Inline, Element): 

2552 """Reference to an image resource. 

2553 

2554 May be body element or inline element. 

2555 """ 

2556 valid_attributes: Final = Element.valid_attributes + ( 

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

2558 

2559 def astext(self) -> str: 

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

2561 

2562 

2563class caption(Part, TextElement): pass 

2564 

2565 

2566class legend(Part, Element): 

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

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

2569 

2570 

2571class figure(General, Element): 

2572 """A formal figure, generally an illustration, with a title.""" 

2573 valid_attributes: Final = Element.valid_attributes + ('align', 'width') 

2574 content_model: Final = (((image, reference), '.'), 

2575 (caption, '?'), 

2576 (legend, '?'), 

2577 ) 

2578 # (image, ((caption, legend?) | legend)) 

2579 # TODO: According to the DTD, a caption or legend is required 

2580 # but rST allows "bare" figures which are formatted differently from 

2581 # images (floating in LaTeX, nested in a <figure> in HTML). [bugs: #489] 

2582 

2583 

2584# Tables 

2585# ------ 

2586 

2587class entry(Part, Element): 

2588 """An entry in a `row` (a table cell).""" 

2589 valid_attributes: Final = Element.valid_attributes + ( 

2590 'align', 'char', 'charoff', 'colname', 'colsep', 'morecols', 

2591 'morerows', 'namest', 'nameend', 'rowsep', 'valign') 

2592 content_model: Final = ((Body, '*'),) 

2593 # %tbl.entry.mdl -> (%body.elements;)* 

2594 

2595 

2596class row(Part, Element): 

2597 """Row of table cells.""" 

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

2599 content_model: Final = ((entry, '+'),) # (%tbl.row.mdl;) -> entry+ 

2600 

2601 

2602class colspec(Part, Element): 

2603 """Specifications for a column in a `tgroup`.""" 

2604 valid_attributes: Final = Element.valid_attributes + ( 

2605 'align', 'char', 'charoff', 'colname', 'colnum', 

2606 'colsep', 'colwidth', 'rowsep', 'stub') 

2607 

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

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

2610 

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

2612 

2613 Provisional. 

2614 

2615 __ https://docutils.sourceforge.io/docs/ref/doctree.html#colwidth 

2616 """ 

2617 # Move current implementation of validate_colwidth() here 

2618 # in Docutils 1.0 

2619 return validate_colwidth(self.get('colwidth', '')) 

2620 

2621 

2622class thead(Part, Element): 

2623 """Row(s) that form the head of a `tgroup`.""" 

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

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

2626 

2627 

2628class tbody(Part, Element): 

2629 """Body of a `tgroup`.""" 

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

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

2632 

2633 

2634class tgroup(Part, Element): 

2635 """A portion of a table. Most tables have just one `tgroup`.""" 

2636 valid_attributes: Final = Element.valid_attributes + ( 

2637 'align', 'cols', 'colsep', 'rowsep') 

2638 content_model: Final = ((colspec, '*'), (thead, '?'), (tbody, '.')) 

2639 # (colspec*, thead?, tbody) 

2640 

2641 

2642class table(General, Element): 

2643 """A data arrangement with rows and columns.""" 

2644 valid_attributes: Final = Element.valid_attributes + ( 

2645 'align', 'colsep', 'frame', 'pgwide', 'rowsep', 'width') 

2646 content_model: Final = ((title, '?'), (tgroup, '+')) 

2647 # (title?, tgroup+) 

2648 

2649 

2650# Inline Elements 

2651# =============== 

2652 

2653class abbreviation(Inline, TextElement): pass 

2654class acronym(Inline, TextElement): pass 

2655class emphasis(Inline, TextElement): pass 

2656class generated(Inline, TextElement): pass 

2657class inline(Inline, TextElement): pass 

2658class literal(Inline, TextElement): pass 

2659class strong(Inline, TextElement): pass 

2660class subscript(Inline, TextElement): pass 

2661class superscript(Inline, TextElement): pass 

2662class title_reference(Inline, TextElement): pass 

2663 

2664 

2665class footnote_reference(Inline, Referential, PureTextElement): 

2666 valid_attributes: Final = Element.valid_attributes + ( 

2667 'auto', 'refid', 'refname') 

2668 

2669 

2670class citation_reference(Inline, Referential, PureTextElement): 

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

2672 

2673 

2674class substitution_reference(Inline, TextElement): 

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

2676 

2677 

2678class math(Inline, PureTextElement): 

2679 """Mathematical notation in running text.""" 

2680 

2681 

2682class problematic(Inline, TextElement): 

2683 valid_attributes: Final = Element.valid_attributes + ( 

2684 'refid', 'refname', 'refuri') 

2685 

2686 

2687# ======================================== 

2688# Auxiliary Classes, Functions, and Data 

2689# ======================================== 

2690 

2691node_class_names: Sequence[str] = """ 

2692 Text 

2693 abbreviation acronym address admonition attention attribution author 

2694 authors 

2695 block_quote bullet_list 

2696 caption caution citation citation_reference classifier colspec comment 

2697 compound contact container copyright 

2698 danger date decoration definition definition_list definition_list_item 

2699 description docinfo doctest_block document 

2700 emphasis entry enumerated_list error 

2701 field field_body field_list field_name figure footer 

2702 footnote footnote_reference 

2703 generated 

2704 header hint 

2705 image important inline 

2706 label legend line line_block list_item literal literal_block 

2707 math math_block meta 

2708 note 

2709 option option_argument option_group option_list option_list_item 

2710 option_string organization 

2711 paragraph pending problematic 

2712 raw reference revision row rubric 

2713 section sidebar status strong subscript substitution_definition 

2714 substitution_reference subtitle superscript system_message 

2715 table target tbody term tgroup thead tip title title_reference topic 

2716 transition 

2717 version 

2718 warning""".split() 

2719"""A list of names of all concrete Node subclasses.""" 

2720 

2721 

2722class NodeVisitor: 

2723 """ 

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

2725 document tree traversals. 

2726 

2727 Each node class has corresponding methods, doing nothing by 

2728 default; override individual methods for specific and useful 

2729 behaviour. The `dispatch_visit()` method is called by 

2730 `Node.walk()` upon entering a node. `Node.walkabout()` also calls 

2731 the `dispatch_departure()` method before exiting a node. 

2732 

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

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

2735 

2736 This is a base class for visitors whose ``visit_...`` & ``depart_...`` 

2737 methods must be implemented for *all* compulsory node types encountered 

2738 (such as for `docutils.writers.Writer` subclasses). 

2739 Unimplemented methods will raise exceptions (except for optional nodes). 

2740 

2741 For sparse traversals, where only certain node types are of interest, use 

2742 subclass `SparseNodeVisitor` instead. When (mostly or entirely) uniform 

2743 processing is desired, subclass `GenericNodeVisitor`. 

2744 

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

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

2747 1995. 

2748 """ 

2749 

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

2751 """ 

2752 Tuple containing node class names (as strings). 

2753 

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

2755 or departure functions for these node classes. 

2756 

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

2758 """ 

2759 

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

2761 self.document: document = document 

2762 

2763 def dispatch_visit(self, node) -> None: 

2764 """ 

2765 Call self."``visit_`` + node class name" with `node` as 

2766 parameter. If the ``visit_...`` method does not exist, call 

2767 self.unknown_visit. 

2768 """ 

2769 node_name = node.__class__.__name__ 

2770 method = getattr(self, 'visit_' + node_name, self.unknown_visit) 

2771 self.document.reporter.debug( 

2772 'docutils.nodes.NodeVisitor.dispatch_visit calling %s for %s' 

2773 % (method.__name__, node_name)) 

2774 return method(node) 

2775 

2776 def dispatch_departure(self, node) -> None: 

2777 """ 

2778 Call self."``depart_`` + node class name" with `node` as 

2779 parameter. If the ``depart_...`` method does not exist, call 

2780 self.unknown_departure. 

2781 """ 

2782 node_name = node.__class__.__name__ 

2783 method = getattr(self, 'depart_' + node_name, self.unknown_departure) 

2784 self.document.reporter.debug( 

2785 'docutils.nodes.NodeVisitor.dispatch_departure calling %s for %s' 

2786 % (method.__name__, node_name)) 

2787 return method(node) 

2788 

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

2790 """ 

2791 Called when entering unknown `Node` types. 

2792 

2793 Raise an exception unless overridden. 

2794 """ 

2795 if (self.document.settings.strict_visitor 

2796 or node.__class__.__name__ not in self.optional): 

2797 raise NotImplementedError( 

2798 '%s visiting unknown node type: %s' 

2799 % (self.__class__, node.__class__.__name__)) 

2800 

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

2802 """ 

2803 Called before exiting unknown `Node` types. 

2804 

2805 Raise exception unless overridden. 

2806 """ 

2807 if (self.document.settings.strict_visitor 

2808 or node.__class__.__name__ not in self.optional): 

2809 raise NotImplementedError( 

2810 '%s departing unknown node type: %s' 

2811 % (self.__class__, node.__class__.__name__)) 

2812 

2813 

2814class SparseNodeVisitor(NodeVisitor): 

2815 """ 

2816 Base class for sparse traversals, where only certain node types are of 

2817 interest. When ``visit_...`` & ``depart_...`` methods should be 

2818 implemented for *all* node types (such as for `docutils.writers.Writer` 

2819 subclasses), subclass `NodeVisitor` instead. 

2820 """ 

2821 

2822 

2823class GenericNodeVisitor(NodeVisitor): 

2824 """ 

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

2826 

2827 Unless overridden, each ``visit_...`` method calls `default_visit()`, and 

2828 each ``depart_...`` method (when using `Node.walkabout()`) calls 

2829 `default_departure()`. `default_visit()` (and `default_departure()`) must 

2830 be overridden in subclasses. 

2831 

2832 Define fully generic visitors by overriding `default_visit()` (and 

2833 `default_departure()`) only. Define semi-generic visitors by overriding 

2834 individual ``visit_...()`` (and ``depart_...()``) methods also. 

2835 

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

2837 be overridden for default behavior. 

2838 """ 

2839 

2840 def default_visit(self, node): 

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

2842 raise NotImplementedError 

2843 

2844 def default_departure(self, node): 

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

2846 raise NotImplementedError 

2847 

2848 

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

2850 self.default_visit(node) 

2851 

2852 

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

2854 self.default_departure(node) 

2855 

2856 

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

2858 pass 

2859 

2860 

2861def _add_node_class_names(names) -> None: 

2862 """Save typing with dynamic assignments:""" 

2863 for _name in names: 

2864 setattr(GenericNodeVisitor, "visit_" + _name, _call_default_visit) 

2865 setattr(GenericNodeVisitor, "depart_" + _name, _call_default_departure) 

2866 setattr(SparseNodeVisitor, 'visit_' + _name, _nop) 

2867 setattr(SparseNodeVisitor, 'depart_' + _name, _nop) 

2868 

2869 

2870_add_node_class_names(node_class_names) 

2871 

2872 

2873class TreeCopyVisitor(GenericNodeVisitor): 

2874 """ 

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

2876 """ 

2877 

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

2879 super().__init__(document) 

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

2881 self.parent: list = [] 

2882 

2883 def get_tree_copy(self): 

2884 return self.parent[0] 

2885 

2886 def default_visit(self, node) -> None: 

2887 """Copy the current node, and make it the new acting parent.""" 

2888 newnode = node.copy() 

2889 self.parent.append(newnode) 

2890 self.parent_stack.append(self.parent) 

2891 self.parent = newnode 

2892 

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

2894 """Restore the previous acting parent.""" 

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

2896 

2897 

2898# Custom Exceptions 

2899# ================= 

2900 

2901class ValidationError(ValueError): 

2902 """Invalid Docutils Document Tree Element.""" 

2903 def __init__(self, msg: str, problematic_element: Element = None) -> None: 

2904 super().__init__(msg) 

2905 self.problematic_element = problematic_element 

2906 

2907 

2908class TreePruningException(Exception): 

2909 """ 

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

2911 

2912 Raise subclasses from within ``visit_...`` or ``depart_...`` methods 

2913 called from `Node.walk()` and `Node.walkabout()` tree traversals to prune 

2914 the tree traversed. 

2915 """ 

2916 

2917 

2918class SkipChildren(TreePruningException): 

2919 """ 

2920 Do not visit any children of the current node. The current node's 

2921 siblings and ``depart_...`` method are not affected. 

2922 """ 

2923 

2924 

2925class SkipSiblings(TreePruningException): 

2926 """ 

2927 Do not visit any more siblings (to the right) of the current node. The 

2928 current node's children and its ``depart_...`` method are not affected. 

2929 """ 

2930 

2931 

2932class SkipNode(TreePruningException): 

2933 """ 

2934 Do not visit the current node's children, and do not call the current 

2935 node's ``depart_...`` method. 

2936 """ 

2937 

2938 

2939class SkipDeparture(TreePruningException): 

2940 """ 

2941 Do not call the current node's ``depart_...`` method. The current node's 

2942 children and siblings are not affected. 

2943 """ 

2944 

2945 

2946class NodeFound(TreePruningException): 

2947 """ 

2948 Raise to indicate that the target of a search has been found. This 

2949 exception must be caught by the client; it is not caught by the traversal 

2950 code. 

2951 """ 

2952 

2953 

2954class StopTraversal(TreePruningException): 

2955 """ 

2956 Stop the traversal altogether. The current node's ``depart_...`` method 

2957 is not affected. The parent nodes ``depart_...`` methods are also called 

2958 as usual. No other nodes are visited. This is an alternative to 

2959 NodeFound that does not cause exception handling to trickle up to the 

2960 caller. 

2961 """ 

2962 

2963 

2964# definition moved here from `utils` to avoid circular import dependency 

2965def unescape(text: str, 

2966 restore_backslashes: bool = False, 

2967 respect_whitespace: bool = False, 

2968 ) -> str: 

2969 """ 

2970 Return a string with nulls removed or restored to backslashes. 

2971 Backslash-escaped spaces are also removed. 

2972 """ 

2973 # `respect_whitespace` is ignored (since introduction 2016-12-16) 

2974 if restore_backslashes: 

2975 return text.replace('\x00', '\\') 

2976 else: 

2977 for sep in ['\x00 ', '\x00\n', '\x00']: 

2978 text = ''.join(text.split(sep)) 

2979 return text 

2980 

2981 

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

2983 """ 

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

2985 

2986 Docutils identifiers will conform to the regular expression 

2987 ``[a-z](-?[a-z0-9]+)*``. For CSS compatibility, identifiers (the "class" 

2988 and "id" attributes) should have no underscores, colons, or periods. 

2989 Hyphens may be used. 

2990 

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

2992 

2993 ID and NAME tokens must begin with a letter ([A-Za-z]) and may be 

2994 followed by any number of letters, digits ([0-9]), hyphens ("-"), 

2995 underscores ("_"), colons (":"), and periods ("."). 

2996 

2997 - However the `CSS1 spec`_ defines identifiers based on the "name" token, 

2998 a tighter interpretation ("flex" tokenizer notation; "latin1" and 

2999 "escape" 8-bit characters have been replaced with entities):: 

3000 

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

3002 latin1 [&iexcl;-&yuml;] 

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

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

3005 name {nmchar}+ 

3006 

3007 The CSS1 "nmchar" rule does not include underscores ("_"), colons (":"), 

3008 or periods ("."), therefore "class" and "id" attributes should not contain 

3009 these characters. They should be replaced with hyphens ("-"). Combined 

3010 with HTML's requirements (the first character must be a letter; no 

3011 "unicode", "latin1", or "escape" characters), this results in the 

3012 ``[a-z](-?[a-z0-9]+)*`` pattern. 

3013 

3014 .. _HTML 4.01 spec: https://www.w3.org/TR/html401 

3015 .. _CSS1 spec: https://www.w3.org/TR/REC-CSS1 

3016 """ 

3017 id = string.lower() 

3018 id = id.translate(_non_id_translate_digraphs) 

3019 id = id.translate(_non_id_translate) 

3020 # get rid of non-ascii characters. 

3021 # 'ascii' lowercase to prevent problems with turkish locale. 

3022 id = unicodedata.normalize( 

3023 'NFKD', id).encode('ascii', 'ignore').decode('ascii') 

3024 # shrink runs of whitespace and replace by hyphen 

3025 id = _non_id_chars.sub('-', ' '.join(id.split())) 

3026 id = _non_id_at_ends.sub('', id) 

3027 return str(id) 

3028 

3029 

3030_non_id_chars: re.Pattern[str] = re.compile('[^a-z0-9]+') 

3031_non_id_at_ends: re.Pattern[str] = re.compile('^[-0-9]+|-+$') 

3032_non_id_translate: dict[int, str] = { 

3033 0x00f8: 'o', # o with stroke 

3034 0x0111: 'd', # d with stroke 

3035 0x0127: 'h', # h with stroke 

3036 0x0131: 'i', # dotless i 

3037 0x0142: 'l', # l with stroke 

3038 0x0167: 't', # t with stroke 

3039 0x0180: 'b', # b with stroke 

3040 0x0183: 'b', # b with topbar 

3041 0x0188: 'c', # c with hook 

3042 0x018c: 'd', # d with topbar 

3043 0x0192: 'f', # f with hook 

3044 0x0199: 'k', # k with hook 

3045 0x019a: 'l', # l with bar 

3046 0x019e: 'n', # n with long right leg 

3047 0x01a5: 'p', # p with hook 

3048 0x01ab: 't', # t with palatal hook 

3049 0x01ad: 't', # t with hook 

3050 0x01b4: 'y', # y with hook 

3051 0x01b6: 'z', # z with stroke 

3052 0x01e5: 'g', # g with stroke 

3053 0x0225: 'z', # z with hook 

3054 0x0234: 'l', # l with curl 

3055 0x0235: 'n', # n with curl 

3056 0x0236: 't', # t with curl 

3057 0x0237: 'j', # dotless j 

3058 0x023c: 'c', # c with stroke 

3059 0x023f: 's', # s with swash tail 

3060 0x0240: 'z', # z with swash tail 

3061 0x0247: 'e', # e with stroke 

3062 0x0249: 'j', # j with stroke 

3063 0x024b: 'q', # q with hook tail 

3064 0x024d: 'r', # r with stroke 

3065 0x024f: 'y', # y with stroke 

3066} 

3067_non_id_translate_digraphs: dict[int, str] = { 

3068 0x00df: 'sz', # ligature sz 

3069 0x00e6: 'ae', # ae 

3070 0x0153: 'oe', # ligature oe 

3071 0x0238: 'db', # db digraph 

3072 0x0239: 'qp', # qp digraph 

3073} 

3074 

3075 

3076def dupname(node: Element, name: str) -> None: 

3077 node['dupnames'].append(name) 

3078 node['names'].remove(name) 

3079 # Assume that `node` is referenced, even though it isn't; 

3080 # we don't want to throw unnecessary system_messages. 

3081 node.referenced = True 

3082 

3083 

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

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

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

3087 

3088 

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

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

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

3092 

3093 

3094def serial_escape(value: str) -> str: 

3095 """Escape string values that are elements of a list, for serialization.""" 

3096 return value.replace('\\', r'\\').replace(' ', r'\ ') 

3097 

3098 

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

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

3101 

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

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

3104 

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

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

3107 

3108 Provisional. 

3109 """ 

3110 s = s.replace('\\', '\x00') # escape with NULL char 

3111 s = s.replace('\x00\x00', '\\') # unescape backslashes 

3112 s = s.replace('\x00 ', '\x00\x00') # escaped spaces -> NULL NULL 

3113 names = s.split(' ') 

3114 # restore internal spaces, drop other escaping characters 

3115 return [name.replace('\x00\x00', ' ').replace('\x00', '') 

3116 for name in names] 

3117 

3118 

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

3120 """Quote attributes for pseudo-xml""" 

3121 return '"%s"' % value 

3122 

3123 

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

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

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

3127 

3128 `unit_pattern` is a regular expression describing recognized units. 

3129 The default is suited for (but not limited to) CSS3 units and SI units. 

3130 It matches runs of ASCII letters or Greek mu, a single percent sign, 

3131 or no unit. 

3132 

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

3134 

3135 Provisional. 

3136 """ 

3137 match = re.fullmatch(f'(-?[0-9.]+) *({unit_pattern})', measure) 

3138 try: 

3139 try: 

3140 value = int(match.group(1)) 

3141 except ValueError: 

3142 value = float(match.group(1)) 

3143 unit = match.group(2) 

3144 except (AttributeError, ValueError): 

3145 raise ValueError(f'"{measure}" is no valid measure.') 

3146 return value, unit 

3147 

3148 

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

3150 

3151# Ensure the expected Python `data type`__, normalize, and check for 

3152# restrictions. 

3153# 

3154# The methods can be used to convert `str` values (eg. from an XML 

3155# representation) or to validate an existing document tree or node. 

3156# 

3157# Cf. `Element.validate_attributes()`, `docutils.parsers.docutils_xml`, 

3158# and the `attribute_validating_functions` mapping below. 

3159# 

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

3161# __ https://docutils.sourceforge.io/docs/ref/doctree.html#attribute-types 

3162 

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

3164 """ 

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

3166 

3167 Provisional. 

3168 """ 

3169 def validate_keywords(value: str) -> str: 

3170 if value not in keywords: 

3171 allowed = '", \"'.join(keywords) 

3172 raise ValueError(f'"{value}" is not one of "{allowed}".') 

3173 return value 

3174 return validate_keywords 

3175 

3176 

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

3178 """ 

3179 Validate identifier key or class name. 

3180 

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

3182 

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

3184 

3185 Provisional. 

3186 """ 

3187 if value != make_id(value): 

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

3189 return value 

3190 

3191 

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

3193 """ 

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

3195 

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

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

3198 

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

3200 

3201 __ https://docutils.sourceforge.io/docs/ref/doctree.html#classnames-type 

3202 __ https://docutils.sourceforge.io/docs/ref/doctree.html#ids-type 

3203 __ https://docutils.sourceforge.io/docs/ref/doctree.html#idrefs-type 

3204 

3205 Provisional. 

3206 """ 

3207 if isinstance(value, str): 

3208 value = value.split() 

3209 for token in value: 

3210 validate_identifier(token) 

3211 return value 

3212 

3213 

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

3215 """ 

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

3217 

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

3219 

3220 The unit may be a run of ASCII letters or Greek mu, a single percent sign, 

3221 or the empty string. Case is preserved. 

3222 

3223 Provisional. 

3224 

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

3226 """ 

3227 value, unit = parse_measure(measure) 

3228 return f'{value}{unit}' 

3229 

3230 

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

3232 """Validate the "colwidth__" attribute. 

3233 

3234 Provisional: 

3235 `measure` must be a `str` and will be returned as normalized `str` 

3236 (with unit "*" for proportional values) in Docutils 1.0. 

3237 

3238 The default unit will change to "pt" in Docutils 2.0. 

3239 

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

3241 """ 

3242 if isinstance(measure, (int, float)): 

3243 value = measure 

3244 elif measure in ('*', ''): # short for '1*' 

3245 value = 1 

3246 else: 

3247 try: 

3248 value, _unit = parse_measure(measure, unit_pattern='[*]?') 

3249 except ValueError: 

3250 value = -1 

3251 if value <= 0: 

3252 raise ValueError(f'"{measure}" is no proportional measure.') 

3253 return value 

3254 

3255 

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

3257 """ 

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

3259 

3260 Provisional. 

3261 """ 

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

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

3264 return value 

3265 

3266 

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

3268 """ 

3269 Validate a list of "name tokens". 

3270 

3271 Provisional. 

3272 """ 

3273 if isinstance(value, str): 

3274 value = value.split() 

3275 for token in value: 

3276 validate_NMTOKEN(token) 

3277 return value 

3278 

3279 

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

3281 """ 

3282 Validate a list of `reference names`__. 

3283 

3284 Reference names may contain all characters; 

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

3286 

3287 `value` may be either a `list` of names or a `str` with 

3288 space separated names (with internal spaces backslash escaped 

3289 and literal backslashes doubled cf. `serial_escape()`). 

3290 

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

3292 

3293 Provisional. 

3294 

3295 __ https://docutils.sourceforge.io/docs/ref/doctree.html#reference-name 

3296 """ 

3297 if isinstance(value, str): 

3298 value = split_name_list(value) 

3299 return [whitespace_normalize_name(name) for name in value] 

3300 

3301 

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

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

3304 

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

3306 values are converterd with `bool()`. 

3307 

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

3309 """ 

3310 if value == "0": 

3311 return False 

3312 return bool(value) 

3313 

3314 

3315ATTRIBUTE_VALIDATORS: dict[str, Callable[[str], Any]] = { 

3316 'alt': str, # CDATA 

3317 'align': str, 

3318 'anonymous': validate_yesorno, 

3319 'auto': str, # CDATA (only '1' or '*' are used in rST) 

3320 'backrefs': validate_identifier_list, 

3321 'bullet': str, # CDATA (only '-', '+', or '*' are used in rST) 

3322 'classes': validate_identifier_list, 

3323 'char': str, # from Exchange Table Model (CALS), currently ignored 

3324 'charoff': validate_NMTOKEN, # from CALS, currently ignored 

3325 'colname': validate_NMTOKEN, # from CALS, currently ignored 

3326 'colnum': int, # from CALS, currently ignored 

3327 'cols': int, # from CALS: "NMTOKEN, […] must be an integer > 0". 

3328 'colsep': validate_yesorno, 

3329 'colwidth': validate_colwidth, # see docstring for pending changes 

3330 'content': str, # <meta> 

3331 'delimiter': str, 

3332 'dir': create_keyword_validator('ltr', 'rtl', 'auto'), # <meta> 

3333 'dupnames': validate_refname_list, 

3334 'enumtype': create_keyword_validator('arabic', 'loweralpha', 'lowerroman', 

3335 'upperalpha', 'upperroman'), 

3336 'format': str, # CDATA (space separated format names) 

3337 'frame': create_keyword_validator('top', 'bottom', 'topbot', 'all', 

3338 'sides', 'none'), # from CALS, ignored 

3339 'height': validate_measure, 

3340 'http-equiv': str, # <meta> 

3341 'ids': validate_identifier_list, 

3342 'lang': str, # <meta> 

3343 'level': int, 

3344 'line': int, 

3345 'ltrim': validate_yesorno, 

3346 'loading': create_keyword_validator('embed', 'link', 'lazy'), 

3347 'media': str, # <meta> 

3348 'morecols': int, 

3349 'morerows': int, 

3350 'name': whitespace_normalize_name, # in <reference> (deprecated) 

3351 # 'name': node_attributes.validate_NMTOKEN, # in <meta> 

3352 'names': validate_refname_list, 

3353 'namest': validate_NMTOKEN, # start of span, from CALS, currently ignored 

3354 'nameend': validate_NMTOKEN, # end of span, from CALS, currently ignored 

3355 'pgwide': validate_yesorno, # from CALS, currently ignored 

3356 'prefix': str, 

3357 'refid': validate_identifier, 

3358 'refname': whitespace_normalize_name, 

3359 'refuri': str, 

3360 'rowsep': validate_yesorno, 

3361 'rtrim': validate_yesorno, 

3362 'scale': int, 

3363 'scheme': str, 

3364 'source': str, 

3365 'start': int, 

3366 'stub': validate_yesorno, 

3367 'suffix': str, 

3368 'title': str, 

3369 'type': validate_NMTOKEN, 

3370 'uri': str, 

3371 'valign': create_keyword_validator('top', 'middle', 'bottom'), # from CALS 

3372 'width': validate_measure, 

3373 'xml:space': create_keyword_validator('default', 'preserve'), 

3374 } 

3375""" 

3376Mapping of `attribute names`__ to validating functions. 

3377 

3378Provisional. 

3379 

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

3381"""