1# $Id: parts.py 8671 2021-04-07 12:09:51Z milde $ 2# Authors: David Goodger <goodger@python.org>; Ueli Schlaepfer; Dmitry Jemerov 3# Copyright: This module has been placed in the public domain. 4 5""" 6Transforms related to document parts. 7""" 8 9__docformat__ = 'reStructuredText' 10 11 12import re 13import sys 14from docutils import nodes, utils 15from docutils.transforms import TransformError, Transform 16 17 18class SectNum(Transform): 19 20 """ 21 Automatically assigns numbers to the titles of document sections. 22 23 It is possible to limit the maximum section level for which the numbers 24 are added. For those sections that are auto-numbered, the "autonum" 25 attribute is set, informing the contents table generator that a different 26 form of the TOC should be used. 27 """ 28 29 default_priority = 710 30 """Should be applied before `Contents`.""" 31 32 def apply(self): 33 self.maxdepth = self.startnode.details.get('depth', None) 34 self.startvalue = self.startnode.details.get('start', 1) 35 self.prefix = self.startnode.details.get('prefix', '') 36 self.suffix = self.startnode.details.get('suffix', '') 37 self.startnode.parent.remove(self.startnode) 38 if self.document.settings.sectnum_xform: 39 if self.maxdepth is None: 40 self.maxdepth = sys.maxsize 41 self.update_section_numbers(self.document) 42 else: # store details for eventual section numbering by the writer 43 self.document.settings.sectnum_depth = self.maxdepth 44 self.document.settings.sectnum_start = self.startvalue 45 self.document.settings.sectnum_prefix = self.prefix 46 self.document.settings.sectnum_suffix = self.suffix 47 48 def update_section_numbers(self, node, prefix=(), depth=0): 49 depth += 1 50 if prefix: 51 sectnum = 1 52 else: 53 sectnum = self.startvalue 54 for child in node: 55 if isinstance(child, nodes.section): 56 numbers = prefix + (str(sectnum),) 57 title = child[0] 58 # Use for spacing: 59 generated = nodes.generated( 60 '', (self.prefix + '.'.join(numbers) + self.suffix 61 + u'\u00a0' * 3), 62 classes=['sectnum']) 63 title.insert(0, generated) 64 title['auto'] = 1 65 if depth < self.maxdepth: 66 self.update_section_numbers(child, numbers, depth) 67 sectnum += 1 68 69 70class Contents(Transform): 71 72 """ 73 This transform generates a table of contents from the entire document tree 74 or from a single branch. It locates "section" elements and builds them 75 into a nested bullet list, which is placed within a "topic" created by the 76 contents directive. A title is either explicitly specified, taken from 77 the appropriate language module, or omitted (local table of contents). 78 The depth may be specified. Two-way references between the table of 79 contents and section titles are generated (requires Writer support). 80 81 This transform requires a startnode, which contains generation 82 options and provides the location for the generated table of contents (the 83 startnode is replaced by the table of contents "topic"). 84 """ 85 86 default_priority = 720 87 88 def apply(self): 89 # let the writer (or output software) build the contents list? 90 toc_by_writer = getattr(self.document.settings, 'use_latex_toc', False) 91 details = self.startnode.details 92 if 'local' in details: 93 startnode = self.startnode.parent.parent 94 while not (isinstance(startnode, nodes.section) 95 or isinstance(startnode, nodes.document)): 96 # find the ToC root: a direct ancestor of startnode 97 startnode = startnode.parent 98 else: 99 startnode = self.document 100 self.toc_id = self.startnode.parent['ids'][0] 101 if 'backlinks' in details: 102 self.backlinks = details['backlinks'] 103 else: 104 self.backlinks = self.document.settings.toc_backlinks 105 if toc_by_writer: 106 # move customization settings to the parent node 107 self.startnode.parent.attributes.update(details) 108 self.startnode.parent.remove(self.startnode) 109 else: 110 contents = self.build_contents(startnode) 111 if len(contents): 112 self.startnode.replace_self(contents) 113 else: 114 self.startnode.parent.parent.remove(self.startnode.parent) 115 116 def build_contents(self, node, level=0): 117 level += 1 118 sections = [sect for sect in node if isinstance(sect, nodes.section)] 119 entries = [] 120 autonum = 0 121 depth = self.startnode.details.get('depth', sys.maxsize) 122 for section in sections: 123 title = section[0] 124 auto = title.get('auto') # May be set by SectNum. 125 entrytext = self.copy_and_filter(title) 126 reference = nodes.reference('', '', refid=section['ids'][0], 127 *entrytext) 128 ref_id = self.document.set_id(reference, 129 suggested_prefix='toc-entry') 130 entry = nodes.paragraph('', '', reference) 131 item = nodes.list_item('', entry) 132 if ( self.backlinks in ('entry', 'top') 133 and title.next_node(nodes.reference) is None): 134 if self.backlinks == 'entry': 135 title['refid'] = ref_id 136 elif self.backlinks == 'top': 137 title['refid'] = self.toc_id 138 if level < depth: 139 subsects = self.build_contents(section, level) 140 item += subsects 141 entries.append(item) 142 if entries: 143 contents = nodes.bullet_list('', *entries) 144 if auto: 145 contents['classes'].append('auto-toc') 146 return contents 147 else: 148 return [] 149 150 def copy_and_filter(self, node): 151 """Return a copy of a title, with references, images, etc. removed.""" 152 visitor = ContentsFilter(self.document) 153 node.walkabout(visitor) 154 return visitor.get_entry_text() 155 156 157class ContentsFilter(nodes.TreeCopyVisitor): 158 159 def get_entry_text(self): 160 return self.get_tree_copy().children 161 162 def visit_citation_reference(self, node): 163 raise nodes.SkipNode 164 165 def visit_footnote_reference(self, node): 166 raise nodes.SkipNode 167 168 def visit_image(self, node): 169 if node.hasattr('alt'): 170 self.parent.append(nodes.Text(node['alt'])) 171 raise nodes.SkipNode 172 173 def ignore_node_but_process_children(self, node): 174 raise nodes.SkipDeparture 175 176 visit_problematic = ignore_node_but_process_children 177 visit_reference = ignore_node_but_process_children 178 visit_target = ignore_node_but_process_children 179