Hide keyboard shortcuts

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 

20 

21 

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 """ 

30 

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 } 

46 

47 def run(self): 

48 env = self.state.document.settings.env 

49 suffixes = env.config.source_suffix 

50 glob = 'glob' in self.options 

51 

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))) 

70 

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 

134 

135 

136class CustomTocTreeCollector(TocTreeCollector): 

137 """ 

138 Overwrites `TocTreeCollector <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/environment/collectors/toctree.py>`_. 

139 """ 

140 

141 # def __init__(self, *p, **kw): 

142 # TocTreeCollector.__init__(self, *p, **kw) 

143 # assert self.listener_ids is None 

144 

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 } 

156 

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... 

161 

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 

172 

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() 

202 

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 

216 

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] 

237 

238 

239class CustomAdaptersTocTree(AdaptersTocTree): 

240 ":epkg:`Sphinx` directive" 

241 pass 

242 

243 

244def setup(app): 

245 """ 

246 Setup for ``toctree`` and ``toctree2`` (sphinx). 

247 """ 

248 app.add_directive('toctree2', CustomTocTree) 

249 directives.register_directive('toctree2', CustomTocTree) 

250 

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) 

261 

262 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}