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
« 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
16class tocdelay_node(nodes.paragraph):
17 """
18 defines ``tocdelay`` node
19 """
20 pass
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.
29 Example::
31 .. tocdelay::
33 document
35 Directive ``.. toctree::`` only considers titles defined by the user,
36 not titles dynamically created by another directives.
38 .. warning:: It is not recommended to dynamically insert
39 such a directive. It is not recursive.
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 """
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}
60 def run(self):
61 """
62 Just add a @see cl tocdelay_node and list the documents to add.
64 @return of nodes or list of nodes, container
65 """
66 lineno = self.lineno
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 = ''
76 ret = []
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):]
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)
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
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))
131 if len(keep_list) == 0:
132 raise ValueError("No found document in '{0}'\nLIST:\n{1}".format(
133 loc, "\n".join(documents)))
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)
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
153 wrappernode = nodes.compound(classes=['toctree-wrapper'])
154 wrappernode.append(node)
155 ret.append(wrappernode)
156 return ret
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
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.
177 @param app Sphinx application
178 @param doctree doctree
179 @param fromdocname docname
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
190 env = app.env
191 logger = logging.getLogger("tocdelay")
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
201 docs = node["tddocuments"]
202 if len(docs) == 0:
203 # No document to look at.
204 continue
206 main_par = nodes.paragraph()
207 # node += main_par
208 bullet_list = nodes.bullet_list()
209 main_par += bullet_list
211 nodedocname = node["tddocname"]
212 dirdocname = os.path.dirname(nodedocname)
213 clname, toctitle, tocid, tocdoc = node["tdrule"]
215 logger.info("[tocdelay] transform_tocdelay %r from %r",
216 nodedocname, fromdocname)
217 node["tdprocessed"] += 1
219 for name, subname, extitle in docs:
220 if not os.path.exists(subname):
221 raise FileNotFoundError(
222 f"Unable to find document '{subname}'")
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)
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
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
256 rootnode = subnode
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]
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]
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 = ''
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
295 node.replace_self(main_par)
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 + " ")
308def visit_tocdelay_node(self, node):
309 """
310 does nothing
311 """
312 _print_loop_on_children(node, msg="visit")
315def depart_tocdelay_node(self, node):
316 """
317 does nothing
318 """
319 _print_loop_on_children(node, msg="depart")
322def setup(app):
323 """
324 setup for ``tocdelay`` (sphinx)
325 """
326 if hasattr(app, "add_mapping"):
327 app.add_mapping('tocdelay', tocdelay_node)
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))
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}