Coverage for pyquickhelper/sphinxext/sphinx_tocdelay_extension.py: 63%

178 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-03 02:21 +0200

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 f"tocdelay rule is wrong: '{rule}' " + 

84 f'document {docname!r}', 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 f"tocdelay: wrong format for '{name}' " + 

117 f'document {docname!r}', 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'=%s , File %r, line %s", 

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

197 continue 

198 if node["tdprocessed"] > 1: 

199 continue 

200 

201 docs = node["tddocuments"] 

202 if len(docs) == 0: 

203 # No document to look at. 

204 continue 

205 

206 main_par = nodes.paragraph() 

207 # node += main_par 

208 bullet_list = nodes.bullet_list() 

209 main_par += bullet_list 

210 

211 nodedocname = node["tddocname"] 

212 dirdocname = os.path.dirname(nodedocname) 

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

214 

215 logger.info("[tocdelay] transform_tocdelay %r from %r", 

216 nodedocname, fromdocname) 

217 node["tdprocessed"] += 1 

218 

219 for name, subname, extitle in docs: 

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

221 raise FileNotFoundError( 

222 f"Unable to find document '{subname}'") 

223 

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

225 # as they are not necessarily attached to it. 

226 subname = f"{dirdocname}/{name}" 

227 doc_doctree = env.get_doctree(subname) 

228 if doc_doctree is None: 

229 logger.info("[tocdelay] ERROR (4): No doctree found for %r from %r", 

230 subname, nodedocname) 

231 

232 # It finds a node sharing the same name. 

233 diginto = [] 

234 for n in doc_doctree.traverse(): 

235 if n.__class__.__name__ == clname: 

236 diginto.append(n) 

237 if len(diginto) == 0: 

238 logger.info( 

239 "[tocdelay] ERROR (3): No node %r found for %r", clname, subname) 

240 continue 

241 

242 # It takes the first one available. 

243 subnode = None 

244 for d in diginto: 

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

246 subnode = d 

247 break 

248 if subnode is None: 

249 found = list( 

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

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

252 logger.warning("[tocdelay] ERROR (2): Unable to find node %r in %s [%r]", 

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

254 continue 

255 

256 rootnode = subnode 

257 

258 if tocid not in rootnode.attributes: 

259 logger.warning( 

260 "[tocdelay] ERROR (7): Unable to find 'tocid' in %r", rootnode) 

261 continue 

262 if tocdoc not in rootnode.attributes: 

263 logger.warning( 

264 "[tocdelay] ERROR (8): Unable to find 'tocdoc' in %r", rootnode) 

265 continue 

266 refid = rootnode[tocid] 

267 refdoc = rootnode[tocdoc] 

268 

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

270 if not subnode: 

271 logger.warning( 

272 "[tocdelay] ERROR (5): Unable to find a title in %r", subname) 

273 continue 

274 subnode = subnode[0] 

275 

276 try: 

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

278 logger.info("[tocdelay] add link for %r - %r from %r", 

279 refid, refdoc, nodedocname) 

280 except NoUri: 

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

282 logger.info("[tocdelay] ERROR (9): unable to find a link for %r - %r from %r -- %s - %s", 

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

284 refuri = '' 

285 

286 use_title = extitle or subnode.astext() 

287 par = nodes.paragraph() 

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

289 internal=True, refuri=refuri) 

290 par += ref 

291 bullet = nodes.list_item() 

292 bullet += par 

293 bullet_list += bullet 

294 

295 node.replace_self(main_par) 

296 

297 

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

299 logger = logging.getLogger("tocdelay") 

300 if hasattr(node, "children"): 

301 logger.info("[tocdelay] %r - %s - %s", type(node), msg, node) 

302 for child in node.children: 

303 logger.info("[tocdelay] %s%s - %r", 

304 indent, type(child), child.astext().replace("\n", " #EOL# ")) 

305 _print_loop_on_children(child, indent + " ") 

306 

307 

308def visit_tocdelay_node(self, node): 

309 """ 

310 does nothing 

311 """ 

312 _print_loop_on_children(node, msg="visit") 

313 

314 

315def depart_tocdelay_node(self, node): 

316 """ 

317 does nothing 

318 """ 

319 _print_loop_on_children(node, msg="depart") 

320 

321 

322def setup(app): 

323 """ 

324 setup for ``tocdelay`` (sphinx) 

325 """ 

326 if hasattr(app, "add_mapping"): 

327 app.add_mapping('tocdelay', tocdelay_node) 

328 

329 app.add_node(tocdelay_node, 

330 html=(visit_tocdelay_node, depart_tocdelay_node), 

331 epub=(visit_tocdelay_node, depart_tocdelay_node), 

332 elatex=(visit_tocdelay_node, depart_tocdelay_node), 

333 latex=(visit_tocdelay_node, depart_tocdelay_node), 

334 text=(visit_tocdelay_node, depart_tocdelay_node), 

335 md=(visit_tocdelay_node, depart_tocdelay_node), 

336 rst=(visit_tocdelay_node, depart_tocdelay_node)) 

337 

338 app.add_directive('tocdelay', TocDelayDirective) 

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

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

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