Source code for pyquickhelper.sphinxext.sphinx_tocdelay_extension

# -*- coding: utf-8 -*-
"""
Defines a sphinx extension which proposes a new version of ``.. toctree::``
which takes into account titles dynamically added.


:githublink:`%|py|7`
"""
import os
import re
from docutils import nodes
from docutils.parsers.rst import Directive, directives
from sphinx.util import logging
from sphinx.environment import NoUri
import sphinx


[docs]class tocdelay_node(nodes.paragraph): """ defines ``tocdelay`` node :githublink:`%|py|19` """ pass
[docs]class TocDelayDirective(Directive): """ Defines a :epkg:`sphinx` extension which proposes a new version of ``.. toctree::`` which takes into account titles dynamically added. It only considers one level. Example:: .. tocdelay:: document Directive ``.. toctree::`` only considers titles defined by the user, not titles dynamically created by another directives. .. warning:: It is not recommended to dynamically insert such a directive. It is not recursive. Parameter *rule* implements specific behaviors. It contains the name of the node which holds the document name, the title, the id. In case of the blog, the rule is: ``blogpost_node,toctitle,tocid,tocdoc``. That means the *TocDelayDirective* will look for nodes ``blogpost_node`` and fetch attributes *toctitle*, *tocid*, *tocdoc* to fill the toc contents. No depth is allowed at this point. The previous value is the default value. Option *path* is mostly used to test the directive. :githublink:`%|py|51` """ node_class = tocdelay_node name_sphinx = "tocdelay" has_content = True regex_title = re.compile("(.*) +[<]([/a-z_A-Z0-9-]+)[>]") option_spec = {'rule': directives.unchanged, 'path': directives.unchanged}
[docs] def run(self): """ Just add a :class:`tocdelay_node <pyquickhelper.sphinxext.sphinx_tocdelay_extension.tocdelay_node>` and list the documents to add. :return: of nodes or list of nodes, container :githublink:`%|py|65` """ lineno = self.lineno settings = self.state.document.settings env = settings.env if hasattr(settings, "env") else None docname = None if env is None else env.docname if docname is not None: docname = docname.replace("\\", "/").split("/")[-1] else: docname = '' ret = [] # It analyses rule. rule = self.options.get("rule", "blogpost_node,toctitle,tocid,tocdoc") spl = rule.split(",") if len(spl) > 4: ret.append(self.state.document.reporter.warning( "tocdelay rule is wrong: '{0}' ".format(rule) + 'document %r' % docname, line=self.lineno)) elif len(spl) == 4: rule = tuple(spl) else: defa = ("blogpost_node", "toctitle", "tocid", "tocdoc") rule = tuple(spl) + defa[4 - len(spl):] # It looks for the documents to add. documents = [] for line in self.content: sline = line.strip() if len(sline) > 0: documents.append(sline) # It checks their existence. loc = self.options.get("path", None) if loc is None: loc = os.path.join(env.srcdir, os.path.dirname(env.docname)) osjoin = os.path.join else: osjoin = os.path.join keep_list = [] for name in documents: if name.endswith(">"): # title <link> match = TocDelayDirective.regex_title.search(name) if match: gr = match.groups() title = gr[0].strip() name = gr[1].strip() else: ret.append(self.state.document.reporter.warning( "tocdelay: wrong format for '{0}' ".format(name) + 'document %r' % docname, line=self.lineno)) else: title = None docname = osjoin(loc, name) if not docname.endswith(".rst"): docname += ".rst" if not os.path.exists(docname): ret.append(self.state.document.reporter.warning( 'tocdelay contains reference to nonexisting ' 'document %r' % docname, line=self.lineno)) else: keep_list.append((name, docname, title)) if len(keep_list) == 0: raise ValueError("No found document in '{0}'\nLIST:\n{1}".format( loc, "\n".join(documents))) # It updates internal references in env. entries = [] includefiles = [] for name, docname, title in keep_list: entries.append((None, docname)) includefiles.append(docname) node = tocdelay_node() node['entries'] = entries node['includefiles'] = includefiles node['tdlineno'] = lineno node['tddocname'] = env.docname node['tdfullname'] = docname node["tdprocessed"] = 0 node["tddocuments"] = keep_list node["tdrule"] = rule node["tdloc"] = loc wrappernode = nodes.compound(classes=['toctree-wrapper']) wrappernode.append(node) ret.append(wrappernode) return ret
[docs]def process_tocdelay(app, doctree): """ Collect all *tocdelay* in the environment. Look for the section or document which contain them. Put them into the variable *tocdelay_all_tocdelay* in the config. :githublink:`%|py|164` """ for node in doctree.traverse(tocdelay_node): node["tdprocessed"] += 1
[docs]def transform_tocdelay(app, doctree, fromdocname): """ The function is called by event ``'doctree_resolved'``. It looks for every section in page stored in *tocdelay_all_tocdelay* in the configuration and builds a short table of contents. The instruction ``.. toctree::`` is resolved before every directive in the page is executed, the instruction ``.. tocdelay::`` is resolved after. :param app: Sphinx application :param doctree: doctree :param fromdocname: docname Thiis directive should be used if you need to capture a section which was dynamically added by another one. For example :class:`RunPythonDirective <pyquickhelper.sphinxext.sphinx_runpython_extension.RunPythonDirective>` calls function ``nested_parse_with_titles``. ``.. tocdelay::`` will capture the new section this function might eventually add to the page. :githublink:`%|py|185` """ post_list = list(doctree.traverse(tocdelay_node)) if len(post_list) == 0: return env = app.env logger = logging.getLogger("tocdelay") for node in post_list: if node["tdprocessed"] == 0: logger.warning("[tocdelay] no first loop was ever processed: 'tdprocessed'={0} , File '{1}', line {2}".format( node["tdprocessed"], node["tddocname"], node["tdlineno"])) continue if node["tdprocessed"] > 1: # logger.warning("[tocdelay] already processed: 'tdprocessed'={0} , File '{1}', line {2}".format( # node["tdprocessed"], node["tddocname"], node["tdlineno"])) continue docs = node["tddocuments"] if len(docs) == 0: # No document to look at. continue main_par = nodes.paragraph() # node += main_par bullet_list = nodes.bullet_list() main_par += bullet_list nodedocname = node["tddocname"] dirdocname = os.path.dirname(nodedocname) clname, toctitle, tocid, tocdoc = node["tdrule"] logger.info("[tocdelay] transform_tocdelay '{0}' from '{1}'".format( nodedocname, fromdocname)) node["tdprocessed"] += 1 for name, subname, extitle in docs: if not os.path.exists(subname): raise FileNotFoundError( "Unable to find document '{0}'".format(subname)) # The doctree it needs is not necessarily accessible from the main node # as they are not necessarily attached to it. subname = "{0}/{1}".format(dirdocname, name) doc_doctree = env.get_doctree(subname) if doc_doctree is None: logger.info("[tocdelay] ERROR (4): No doctree found for '{0}' from '{1}'".format( subname, nodedocname)) # It finds a node sharing the same name. diginto = [] for n in doc_doctree.traverse(): if n.__class__.__name__ == clname: diginto.append(n) if len(diginto) == 0: logger.info( "[tocdelay] ERROR (3): No node '{0}' found for '{1}'".format(clname, subname)) continue # It takes the first one available. subnode = None for d in diginto: if 'tocdoc' in d.attributes and d['tocdoc'].endswith(subname): subnode = d break if subnode is None: found = list( sorted(set(map(lambda x: x.__class__.__name__, diginto)))) ext = diginto[0].attributes if len(diginto) > 0 else "" logger.warning("[tocdelay] ERROR (2): Unable to find node '{0}' in {1} [{2}]".format( subname, ", ".join(map(str, found)), ext)) continue rootnode = subnode if tocid not in rootnode.attributes: logger.warning( "[tocdelay] ERROR (7): Unable to find 'tocid' in '{0}'".format(rootnode)) continue if tocdoc not in rootnode.attributes: logger.warning( "[tocdelay] ERROR (8): Unable to find 'tocdoc' in '{0}'".format(rootnode)) continue refid = rootnode[tocid] refdoc = rootnode[tocdoc] subnode = list(rootnode.traverse(nodes.title)) if not subnode: logger.warning( "[tocdelay] ERROR (5): Unable to find a title in '{0}'".format(subname)) continue subnode = subnode[0] try: refuri = app.builder.get_relative_uri(nodedocname, refdoc) logger.info( "[tocdelay] add link for '{0}' - '{1}' from '{2}'".format(refid, refdoc, nodedocname)) except NoUri: docn = list(sorted(app.builder.docnames)) logger.info("[tocdelay] ERROR (9): unable to find a link for '{0}' - '{1}' from '{2}` -- {3} - {4}".format( refid, refdoc, nodedocname, type(app.builder), docn)) refuri = '' use_title = extitle or subnode.astext() par = nodes.paragraph() ref = nodes.reference(refid=refid, reftitle=use_title, text=use_title, internal=True, refuri=refuri) par += ref bullet = nodes.list_item() bullet += par bullet_list += bullet node.replace_self(main_par)
[docs]def _print_loop_on_children(node, indent="", msg="-"): logger = logging.getLogger("tocdelay") if hasattr(node, "children"): logger.info( "[tocdelay] '{0}' - {1} - {2}".format(type(node), msg, node)) for child in node.children: logger.info("[tocdelay] {0}{1} - '{2}'".format(indent, type(child), child.astext().replace("\n", " #EOL# "))) _print_loop_on_children(child, indent + " ")
[docs]def visit_tocdelay_node(self, node): """ does nothing :githublink:`%|py|314` """ _print_loop_on_children(node, msg="visit")
[docs]def depart_tocdelay_node(self, node): """ does nothing :githublink:`%|py|321` """ _print_loop_on_children(node, msg="depart")
[docs]def setup(app): """ setup for ``tocdelay`` (sphinx) :githublink:`%|py|328` """ if hasattr(app, "add_mapping"): app.add_mapping('tocdelay', tocdelay_node) app.add_node(tocdelay_node, html=(visit_tocdelay_node, depart_tocdelay_node), epub=(visit_tocdelay_node, depart_tocdelay_node), elatex=(visit_tocdelay_node, depart_tocdelay_node), latex=(visit_tocdelay_node, depart_tocdelay_node), text=(visit_tocdelay_node, depart_tocdelay_node), md=(visit_tocdelay_node, depart_tocdelay_node), rst=(visit_tocdelay_node, depart_tocdelay_node)) app.add_directive('tocdelay', TocDelayDirective) app.connect('doctree-read', process_tocdelay) app.connect('doctree-resolved', transform_tocdelay) return {'version': sphinx.__display_version__, 'parallel_read_safe': True}