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 &nbsp; 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