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
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 "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):]
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 "tocdelay: wrong format for '{0}' ".format(name) +
117 'document %r' % docname, 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'={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
203 docs = node["tddocuments"]
204 if len(docs) == 0:
205 # No document to look at.
206 continue
208 main_par = nodes.paragraph()
209 # node += main_par
210 bullet_list = nodes.bullet_list()
211 main_par += bullet_list
213 nodedocname = node["tddocname"]
214 dirdocname = os.path.dirname(nodedocname)
215 clname, toctitle, tocid, tocdoc = node["tdrule"]
217 logger.info("[tocdelay] transform_tocdelay '{0}' from '{1}'".format(
218 nodedocname, fromdocname))
219 node["tdprocessed"] += 1
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))
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))
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
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
258 rootnode = subnode
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]
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]
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 = ''
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
297 node.replace_self(main_par)
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 + " ")
311def visit_tocdelay_node(self, node):
312 """
313 does nothing
314 """
315 _print_loop_on_children(node, msg="visit")
318def depart_tocdelay_node(self, node):
319 """
320 does nothing
321 """
322 _print_loop_on_children(node, msg="depart")
325def setup(app):
326 """
327 setup for ``tocdelay`` (sphinx)
328 """
329 if hasattr(app, "add_mapping"):
330 app.add_mapping('tocdelay', tocdelay_node)
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))
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}