Skip to content
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
344 lines (289 sloc) 12.4 KB
# -*- coding: utf-8 -*-
@brief Defines a sphinx extension which proposes a new version of ``.. toctree::``
which takes into account titles dynamically added.
import os
import re
from docutils import nodes
from docutils.parsers.rst import Directive, directives
from sphinx.util import logging
from sphinx.errors import NoUri
import sphinx
class tocdelay_node(nodes.paragraph):
defines ``tocdelay`` node
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.
.. tocdelay::
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.
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}
def run(self):
Just add a @see cl tocdelay_node and list the documents to add.
@return of nodes or list of nodes, container
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]
docname = ''
ret = []
# It analyses rule.
rule = self.options.get("rule", "blogpost_node,toctitle,tocid,tocdoc")
spl = rule.split(",")
if len(spl) > 4:
"tocdelay rule is wrong: '{0}' ".format(rule) +
'document %r' % docname, line=self.lineno))
elif len(spl) == 4:
rule = tuple(spl)
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:
# 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
osjoin = os.path.join
keep_list = []
for name in documents:
if name.endswith(">"):
# title <link>
match =
if match:
gr = match.groups()
title = gr[0].strip()
name = gr[1].strip()
"tocdelay: wrong format for '{0}' ".format(name) +
'document %r' % docname, line=self.lineno))
title = None
docname = osjoin(loc, name)
if not docname.endswith(".rst"):
docname += ".rst"
if not os.path.exists(docname):
'tocdelay contains reference to nonexisting '
'document %r' % docname, line=self.lineno))
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))
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'])
return ret
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.
for node in doctree.traverse(tocdelay_node):
node["tdprocessed"] += 1
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 @see cl RunPythonDirective
calls function ``nested_parse_with_titles``. ``.. tocdelay::`` will capture the
new section this function might eventually add to the page.
post_list = list(doctree.traverse(tocdelay_node))
if len(post_list) == 0:
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"]))
if node["tdprocessed"] > 1:
# logger.warning("[tocdelay] already processed: 'tdprocessed'={0} , File '{1}', line {2}".format(
# node["tdprocessed"], node["tddocname"], node["tdlineno"]))
docs = node["tddocuments"]
if len(docs) == 0:
# No document to look at.
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"]"[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:"[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:
if len(diginto) == 0:
"[tocdelay] ERROR (3): No node '{0}' found for '{1}'".format(clname, subname))
# It takes the first one available.
subnode = None
for d in diginto:
if 'tocdoc' in d.attributes and d['tocdoc'].endswith(subname):
subnode = d
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))
rootnode = subnode
if tocid not in rootnode.attributes:
"[tocdelay] ERROR (7): Unable to find 'tocid' in '{0}'".format(rootnode))
if tocdoc not in rootnode.attributes:
"[tocdelay] ERROR (8): Unable to find 'tocdoc' in '{0}'".format(rootnode))
refid = rootnode[tocid]
refdoc = rootnode[tocdoc]
subnode = list(rootnode.traverse(nodes.title))
if not subnode:
"[tocdelay] ERROR (5): Unable to find a title in '{0}'".format(subname))
subnode = subnode[0]
refuri = app.builder.get_relative_uri(nodedocname, refdoc)
"[tocdelay] add link for '{0}' - '{1}' from '{2}'".format(refid, refdoc, nodedocname))
except NoUri:
docn = list(sorted(app.builder.docnames))"[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
def _print_loop_on_children(node, indent="", msg="-"):
logger = logging.getLogger("tocdelay")
if hasattr(node, "children"):
"[tocdelay] '{0}' - {1} - {2}".format(type(node), msg, node))
for child in node.children:"[tocdelay] {0}{1} - '{2}'".format(indent, type(child),
child.astext().replace("\n", " #EOL# ")))
_print_loop_on_children(child, indent + " ")
def visit_tocdelay_node(self, node):
does nothing
_print_loop_on_children(node, msg="visit")
def depart_tocdelay_node(self, node):
does nothing
_print_loop_on_children(node, msg="depart")
def setup(app):
setup for ``tocdelay`` (sphinx)
if hasattr(app, "add_mapping"):
app.add_mapping('tocdelay', 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}
You can’t perform that action at this time.