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 Defines a sphinx extension which proposes a new version of ``.. toctree::`` 

5which takes into account titles dynamically added. 

6""" 

7import os 

8import re 

9from docutils import nodes 

10from docutils.parsers.rst import Directive, directives 

11from sphinx.util import logging 

12from sphinx.errors import NoUri 

13import sphinx 

14 

15 

16class tocdelay_node(nodes.paragraph): 

17 """ 

18 defines ``tocdelay`` node 

19 """ 

20 pass 

21 

22 

23class TocDelayDirective(Directive): 

24 """ 

25 Defines a :epkg:`sphinx` extension which proposes a new version of ``.. toctree::`` 

26 which takes into account titles dynamically added. It only considers 

27 one level. 

28 

29 Example:: 

30 

31 .. tocdelay:: 

32 

33 document 

34 

35 Directive ``.. toctree::`` only considers titles defined by the user, 

36 not titles dynamically created by another directives. 

37 

38 .. warning:: It is not recommended to dynamically insert 

39 such a directive. It is not recursive. 

40 

41 Parameter *rule* implements specific behaviors. 

42 It contains the name of the node which holds 

43 the document name, the title, the id. In case of the blog, 

44 the rule is: ``blogpost_node,toctitle,tocid,tocdoc``. 

45 That means the *TocDelayDirective* will look for nodes 

46 ``blogpost_node`` and fetch attributes 

47 *toctitle*, *tocid*, *tocdoc* to fill the toc contents. 

48 No depth is allowed at this point. 

49 The previous value is the default value. 

50 Option *path* is mostly used to test the directive. 

51 """ 

52 

53 node_class = tocdelay_node 

54 name_sphinx = "tocdelay" 

55 has_content = True 

56 regex_title = re.compile("(.*) +[<]([/a-z_A-Z0-9-]+)[>]") 

57 option_spec = {'rule': directives.unchanged, 

58 'path': directives.unchanged} 

59 

60 def run(self): 

61 """ 

62 Just add a @see cl tocdelay_node and list the documents to add. 

63 

64 @return of nodes or list of nodes, container 

65 """ 

66 lineno = self.lineno 

67 

68 settings = self.state.document.settings 

69 env = settings.env if hasattr(settings, "env") else None 

70 docname = None if env is None else env.docname 

71 if docname is not None: 

72 docname = docname.replace("\\", "/").split("/")[-1] 

73 else: 

74 docname = '' 

75 

76 ret = [] 

77 

78 # It analyses rule. 

79 rule = self.options.get("rule", "blogpost_node,toctitle,tocid,tocdoc") 

80 spl = rule.split(",") 

81 if len(spl) > 4: 

82 ret.append(self.state.document.reporter.warning( 

83 "tocdelay rule is wrong: '{0}' ".format(rule) + 

84 'document %r' % docname, line=self.lineno)) 

85 elif len(spl) == 4: 

86 rule = tuple(spl) 

87 else: 

88 defa = ("blogpost_node", "toctitle", "tocid", "tocdoc") 

89 rule = tuple(spl) + defa[4 - len(spl):] 

90 

91 # It looks for the documents to add. 

92 documents = [] 

93 for line in self.content: 

94 sline = line.strip() 

95 if len(sline) > 0: 

96 documents.append(sline) 

97 

98 # It checks their existence. 

99 loc = self.options.get("path", None) 

100 if loc is None: 

101 loc = os.path.join(env.srcdir, os.path.dirname(env.docname)) 

102 osjoin = os.path.join 

103 else: 

104 osjoin = os.path.join 

105 keep_list = [] 

106 for name in documents: 

107 if name.endswith(">"): 

108 # title <link> 

109 match = TocDelayDirective.regex_title.search(name) 

110 if match: 

111 gr = match.groups() 

112 title = gr[0].strip() 

113 name = gr[1].strip() 

114 else: 

115 ret.append(self.state.document.reporter.warning( 

116 "tocdelay: wrong format for '{0}' ".format(name) + 

117 'document %r' % docname, line=self.lineno)) 

118 else: 

119 title = None 

120 

121 docname = osjoin(loc, name) 

122 if not docname.endswith(".rst"): 

123 docname += ".rst" 

124 if not os.path.exists(docname): 

125 ret.append(self.state.document.reporter.warning( 

126 'tocdelay contains reference to nonexisting ' 

127 'document %r' % docname, line=self.lineno)) 

128 else: 

129 keep_list.append((name, docname, title)) 

130 

131 if len(keep_list) == 0: 

132 raise ValueError("No found document in '{0}'\nLIST:\n{1}".format( 

133 loc, "\n".join(documents))) 

134 

135 # It updates internal references in env. 

136 entries = [] 

137 includefiles = [] 

138 for name, docname, title in keep_list: 

139 entries.append((None, docname)) 

140 includefiles.append(docname) 

141 

142 node = tocdelay_node() 

143 node['entries'] = entries 

144 node['includefiles'] = includefiles 

145 node['tdlineno'] = lineno 

146 node['tddocname'] = env.docname 

147 node['tdfullname'] = docname 

148 node["tdprocessed"] = 0 

149 node["tddocuments"] = keep_list 

150 node["tdrule"] = rule 

151 node["tdloc"] = loc 

152 

153 wrappernode = nodes.compound(classes=['toctree-wrapper']) 

154 wrappernode.append(node) 

155 ret.append(wrappernode) 

156 return ret 

157 

158 

159def process_tocdelay(app, doctree): 

160 """ 

161 Collect all *tocdelay* in the environment. 

162 Look for the section or document which contain them. 

163 Put them into the variable *tocdelay_all_tocdelay* in the config. 

164 """ 

165 for node in doctree.traverse(tocdelay_node): 

166 node["tdprocessed"] += 1 

167 

168 

169def transform_tocdelay(app, doctree, fromdocname): 

170 """ 

171 The function is called by event ``'doctree_resolved'``. It looks for 

172 every section in page stored in *tocdelay_all_tocdelay* 

173 in the configuration and builds a short table of contents. 

174 The instruction ``.. toctree::`` is resolved before every directive in 

175 the page is executed, the instruction ``.. tocdelay::`` is resolved after. 

176 

177 @param app Sphinx application 

178 @param doctree doctree 

179 @param fromdocname docname 

180 

181 Thiis directive should be used if you need to capture a section 

182 which was dynamically added by another one. For example @see cl RunPythonDirective 

183 calls function ``nested_parse_with_titles``. ``.. tocdelay::`` will capture the 

184 new section this function might eventually add to the page. 

185 """ 

186 post_list = list(doctree.traverse(tocdelay_node)) 

187 if len(post_list) == 0: 

188 return 

189 

190 env = app.env 

191 logger = logging.getLogger("tocdelay") 

192 

193 for node in post_list: 

194 if node["tdprocessed"] == 0: 

195 logger.warning("[tocdelay] no first loop was ever processed: 'tdprocessed'={0} , File '{1}', line {2}".format( 

196 node["tdprocessed"], node["tddocname"], node["tdlineno"])) 

197 continue 

198 if node["tdprocessed"] > 1: 

199 # logger.warning("[tocdelay] already processed: 'tdprocessed'={0} , File '{1}', line {2}".format( 

200 # node["tdprocessed"], node["tddocname"], node["tdlineno"])) 

201 continue 

202 

203 docs = node["tddocuments"] 

204 if len(docs) == 0: 

205 # No document to look at. 

206 continue 

207 

208 main_par = nodes.paragraph() 

209 # node += main_par 

210 bullet_list = nodes.bullet_list() 

211 main_par += bullet_list 

212 

213 nodedocname = node["tddocname"] 

214 dirdocname = os.path.dirname(nodedocname) 

215 clname, toctitle, tocid, tocdoc = node["tdrule"] 

216 

217 logger.info("[tocdelay] transform_tocdelay '{0}' from '{1}'".format( 

218 nodedocname, fromdocname)) 

219 node["tdprocessed"] += 1 

220 

221 for name, subname, extitle in docs: 

222 if not os.path.exists(subname): 

223 raise FileNotFoundError( 

224 "Unable to find document '{0}'".format(subname)) 

225 

226 # The doctree it needs is not necessarily accessible from the main node 

227 # as they are not necessarily attached to it. 

228 subname = "{0}/{1}".format(dirdocname, name) 

229 doc_doctree = env.get_doctree(subname) 

230 if doc_doctree is None: 

231 logger.info("[tocdelay] ERROR (4): No doctree found for '{0}' from '{1}'".format( 

232 subname, nodedocname)) 

233 

234 # It finds a node sharing the same name. 

235 diginto = [] 

236 for n in doc_doctree.traverse(): 

237 if n.__class__.__name__ == clname: 

238 diginto.append(n) 

239 if len(diginto) == 0: 

240 logger.info( 

241 "[tocdelay] ERROR (3): No node '{0}' found for '{1}'".format(clname, subname)) 

242 continue 

243 

244 # It takes the first one available. 

245 subnode = None 

246 for d in diginto: 

247 if 'tocdoc' in d.attributes and d['tocdoc'].endswith(subname): 

248 subnode = d 

249 break 

250 if subnode is None: 

251 found = list( 

252 sorted(set(map(lambda x: x.__class__.__name__, diginto)))) 

253 ext = diginto[0].attributes if len(diginto) > 0 else "" 

254 logger.warning("[tocdelay] ERROR (2): Unable to find node '{0}' in {1} [{2}]".format( 

255 subname, ", ".join(map(str, found)), ext)) 

256 continue 

257 

258 rootnode = subnode 

259 

260 if tocid not in rootnode.attributes: 

261 logger.warning( 

262 "[tocdelay] ERROR (7): Unable to find 'tocid' in '{0}'".format(rootnode)) 

263 continue 

264 if tocdoc not in rootnode.attributes: 

265 logger.warning( 

266 "[tocdelay] ERROR (8): Unable to find 'tocdoc' in '{0}'".format(rootnode)) 

267 continue 

268 refid = rootnode[tocid] 

269 refdoc = rootnode[tocdoc] 

270 

271 subnode = list(rootnode.traverse(nodes.title)) 

272 if not subnode: 

273 logger.warning( 

274 "[tocdelay] ERROR (5): Unable to find a title in '{0}'".format(subname)) 

275 continue 

276 subnode = subnode[0] 

277 

278 try: 

279 refuri = app.builder.get_relative_uri(nodedocname, refdoc) 

280 logger.info( 

281 "[tocdelay] add link for '{0}' - '{1}' from '{2}'".format(refid, refdoc, nodedocname)) 

282 except NoUri: 

283 docn = list(sorted(app.builder.docnames)) 

284 logger.info("[tocdelay] ERROR (9): unable to find a link for '{0}' - '{1}' from '{2}` -- {3} - {4}".format( 

285 refid, refdoc, nodedocname, type(app.builder), docn)) 

286 refuri = '' 

287 

288 use_title = extitle or subnode.astext() 

289 par = nodes.paragraph() 

290 ref = nodes.reference(refid=refid, reftitle=use_title, text=use_title, 

291 internal=True, refuri=refuri) 

292 par += ref 

293 bullet = nodes.list_item() 

294 bullet += par 

295 bullet_list += bullet 

296 

297 node.replace_self(main_par) 

298 

299 

300def _print_loop_on_children(node, indent="", msg="-"): 

301 logger = logging.getLogger("tocdelay") 

302 if hasattr(node, "children"): 

303 logger.info( 

304 "[tocdelay] '{0}' - {1} - {2}".format(type(node), msg, node)) 

305 for child in node.children: 

306 logger.info("[tocdelay] {0}{1} - '{2}'".format(indent, type(child), 

307 child.astext().replace("\n", " #EOL# "))) 

308 _print_loop_on_children(child, indent + " ") 

309 

310 

311def visit_tocdelay_node(self, node): 

312 """ 

313 does nothing 

314 """ 

315 _print_loop_on_children(node, msg="visit") 

316 

317 

318def depart_tocdelay_node(self, node): 

319 """ 

320 does nothing 

321 """ 

322 _print_loop_on_children(node, msg="depart") 

323 

324 

325def setup(app): 

326 """ 

327 setup for ``tocdelay`` (sphinx) 

328 """ 

329 if hasattr(app, "add_mapping"): 

330 app.add_mapping('tocdelay', tocdelay_node) 

331 

332 app.add_node(tocdelay_node, 

333 html=(visit_tocdelay_node, depart_tocdelay_node), 

334 epub=(visit_tocdelay_node, depart_tocdelay_node), 

335 elatex=(visit_tocdelay_node, depart_tocdelay_node), 

336 latex=(visit_tocdelay_node, depart_tocdelay_node), 

337 text=(visit_tocdelay_node, depart_tocdelay_node), 

338 md=(visit_tocdelay_node, depart_tocdelay_node), 

339 rst=(visit_tocdelay_node, depart_tocdelay_node)) 

340 

341 app.add_directive('tocdelay', TocDelayDirective) 

342 app.connect('doctree-read', process_tocdelay) 

343 app.connect('doctree-resolved', transform_tocdelay) 

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