Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Overwrites `toctree <https://www.sphinx-doc.org/en/master/markup/toctree.html#directive-toctree>`_
5directive to get catch exceptions when a document is processed inline.
6"""
7from docutils.parsers.rst import directives
8from docutils import nodes
9import sphinx
10from sphinx.directives.other import TocTree, int_or_nothing
11from sphinx.util.nodes import explicit_title_re, set_source_info
12from sphinx import addnodes
13from sphinx.util import url_re, docname_join
14from sphinx.environment.collectors.toctree import TocTreeCollector
15from sphinx.util import logging
16from sphinx.transforms import SphinxContentsFilter
17from sphinx.environment.adapters.toctree import TocTree as AdaptersTocTree
18from sphinx.util.matching import patfilter
19from ..texthelper import compare_module_version
22class CustomTocTree(TocTree):
23 """
24 Overwrites `toctree
25 <https://www.sphinx-doc.org/en/master/markup/toctree.html#directive-toctree>`_.
26 The code is located at
27 `sphinx/directives/other.py
28 <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/directives/other.py#L38>`_.
29 """
31 has_content = True
32 required_arguments = 0
33 optional_arguments = 0
34 final_argument_whitespace = False
35 option_spec = {
36 'maxdepth': int,
37 'name': directives.unchanged,
38 'caption': directives.unchanged_required,
39 'glob': directives.flag,
40 'hidden': directives.flag,
41 'includehidden': directives.flag,
42 'numbered': int_or_nothing,
43 'titlesonly': directives.flag,
44 'reversed': directives.flag,
45 }
47 def run(self):
48 env = self.state.document.settings.env
49 suffixes = env.config.source_suffix
50 glob = 'glob' in self.options
52 ret = []
53 # (title, ref) pairs, where ref may be a document, or an external link,
54 # and title may be None if the document's title is to be used
55 entries = []
56 includefiles = []
57 all_docnames = env.found_docs.copy()
58 # don't add the currently visited file in catch-all patterns
59 try:
60 all_docnames.remove(env.docname)
61 except KeyError:
62 if env.docname == "<<string>>":
63 # This comes from rst2html.
64 pass
65 else:
66 logger = logging.getLogger("CustomTocTreeCollector")
67 logger.warning(
68 "[CustomTocTreeCollector] unable to remove document '{0}' from {1}".format(
69 env.docname, ", ".join(all_docnames)))
71 for entry in self.content:
72 if not entry:
73 continue
74 if glob and ('*' in entry or '?' in entry or '[' in entry):
75 patname = docname_join(env.docname, entry)
76 docnames = sorted(patfilter(all_docnames, patname))
77 for docname in docnames:
78 all_docnames.remove(docname) # don't include it again
79 entries.append((None, docname))
80 includefiles.append(docname)
81 if not docnames:
82 ret.append(self.state.document.reporter.warning(
83 '[CustomTocTree] glob pattern %r didn\'t match any documents'
84 % entry, line=self.lineno))
85 else:
86 # look for explicit titles ("Some Title <document>")
87 m = explicit_title_re.match(entry)
88 if m:
89 ref = m.group(2)
90 title = m.group(1)
91 docname = ref
92 else:
93 ref = docname = entry
94 title = None
95 # remove suffixes (backwards compatibility)
96 for suffix in suffixes:
97 if docname.endswith(suffix):
98 docname = docname[:-len(suffix)]
99 break
100 # absolutize filenames
101 docname = docname_join(env.docname, docname)
102 if url_re.match(ref) or ref == 'self':
103 entries.append((title, ref))
104 elif docname not in env.found_docs:
105 ret.append(self.state.document.reporter.warning(
106 '[CustomTocTree] contains reference to nonexisting '
107 'document %r' % docname, line=self.lineno))
108 env.note_reread()
109 else:
110 all_docnames.discard(docname)
111 entries.append((title, docname))
112 includefiles.append(docname)
113 subnode = addnodes.toctree()
114 subnode['parent'] = env.docname
115 # entries contains all entries (self references, external links etc.)
116 if 'reversed' in self.options:
117 entries.reverse()
118 subnode['entries'] = entries
119 # includefiles only entries that are documents
120 subnode['includefiles'] = includefiles
121 subnode['maxdepth'] = self.options.get('maxdepth', -1)
122 subnode['caption'] = self.options.get('caption')
123 subnode['glob'] = glob
124 subnode['hidden'] = 'hidden' in self.options
125 subnode['includehidden'] = 'includehidden' in self.options
126 subnode['numbered'] = self.options.get('numbered', 0)
127 subnode['titlesonly'] = 'titlesonly' in self.options
128 set_source_info(self, subnode)
129 wrappernode = nodes.compound(classes=['toctree-wrapper'])
130 wrappernode.append(subnode)
131 self.add_name(wrappernode)
132 ret.append(wrappernode)
133 return ret
136class CustomTocTreeCollector(TocTreeCollector):
137 """
138 Overwrites `TocTreeCollector <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/environment/collectors/toctree.py>`_.
139 """
141 # def __init__(self, *p, **kw):
142 # TocTreeCollector.__init__(self, *p, **kw)
143 # assert self.listener_ids is None
145 def enable(self, app):
146 # It needs to disable TocTreeCollector.
147 app.disconnect_env_collector("TocTreeCollector", exc=False)
148 assert self.listener_ids is None
149 self.listener_ids = {
150 'doctree-read': app.connect('doctree-read', self.process_doc),
151 'env-merge-info': app.connect('env-merge-info', self.merge_other),
152 'env-purge-doc': app.connect('env-purge-doc', self.clear_doc),
153 'env-get-updated': app.connect('env-get-updated', self.get_updated_docs),
154 'env-get-outdated': app.connect('env-get-outdated', self.get_outdated_docs),
155 }
157 def process_doc(self, app, doctree):
158 """Build a TOC from the doctree and store it in the inventory."""
159 docname = app.env.docname
160 numentries = [0] # nonlocal again...
162 def traverse_in_section(node, cls):
163 """Like traverse(), but stay within the same section."""
164 result = []
165 if isinstance(node, cls):
166 result.append(node)
167 for child in node.children:
168 if isinstance(child, nodes.section):
169 continue
170 result.extend(traverse_in_section(child, cls))
171 return result
173 def build_toc(node, depth=1):
174 """Builds toc."""
175 entries = []
176 for sectionnode in node:
177 # find all toctree nodes in this section and add them
178 # to the toc (just copying the toctree node which is then
179 # resolved in self.get_and_resolve_doctree)
180 if isinstance(sectionnode, addnodes.only):
181 onlynode = addnodes.only(expr=sectionnode['expr'])
182 blist = build_toc(sectionnode, depth)
183 if blist:
184 onlynode += blist.children
185 entries.append(onlynode)
186 continue
187 if not isinstance(sectionnode, nodes.section):
188 for toctreenode in traverse_in_section(sectionnode,
189 addnodes.toctree):
190 item = toctreenode.copy()
191 entries.append(item)
192 # important: do the inventory stuff
193 CustomAdaptersTocTree(app.env).note(
194 docname, toctreenode)
195 continue
196 title = sectionnode[0]
197 # copy the contents of the section title, but without references
198 # and unnecessary stuff
199 visitor = SphinxContentsFilter(doctree)
200 title.walkabout(visitor)
201 nodetext = visitor.get_entry_text()
203 if not numentries[0]:
204 # for the very first toc entry, don't add an anchor
205 # as it is the file's title anyway
206 anchorname = ''
207 else:
208 if len(sectionnode['ids']) == 0:
209 an = "unkown-anchor"
210 logger = logging.getLogger("CustomTocTreeCollector")
211 logger.warning(
212 "[CustomTocTreeCollector] no id for node '{0}'".format(sectionnode))
213 else:
214 an = sectionnode['ids'][0]
215 anchorname = '#' + an
217 numentries[0] += 1
218 # make these nodes:
219 # list_item -> compact_paragraph -> reference
220 reference = nodes.reference(
221 '', '', internal=True, refuri=docname,
222 anchorname=anchorname, *nodetext)
223 para = addnodes.compact_paragraph('', '', reference)
224 item = nodes.list_item('', para)
225 sub_item = build_toc(sectionnode, depth + 1)
226 item += sub_item
227 entries.append(item)
228 if entries:
229 return nodes.bullet_list('', *entries)
230 return []
231 toc = build_toc(doctree)
232 if toc:
233 app.env.tocs[docname] = toc
234 else:
235 app.env.tocs[docname] = nodes.bullet_list('')
236 app.env.toc_num_entries[docname] = numentries[0]
239class CustomAdaptersTocTree(AdaptersTocTree):
240 ":epkg:`Sphinx` directive"
241 pass
244def setup(app):
245 """
246 Setup for ``toctree`` and ``toctree2`` (sphinx).
247 """
248 app.add_directive('toctree2', CustomTocTree)
249 directives.register_directive('toctree2', CustomTocTree)
251 if hasattr(app, 'disconnect_env_collector'):
252 # If it can disable the previous TocTreeCollector,
253 # it connects a new collector to the app,
254 # it disables the previous one.
255 directives.register_directive('toctree', CustomTocTree)
256 if compare_module_version(sphinx.__version__, '1.8') < 0:
257 app.add_directive('toctree', CustomTocTree)
258 else:
259 app.add_directive('toctree', CustomTocTree, override=True)
260 app.add_env_collector(CustomTocTreeCollector)
262 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}