# -*- coding: utf-8 -*-
"""
Defines a :epkg:`sphinx` extension which proposes a new version of ``.. contents::``
which takes into account titles dynamically added.
:githublink:`%|py|7`
"""
from docutils import nodes
from docutils.parsers.rst import directives
from sphinx.util import logging
import sphinx
from sphinx.util.logging import getLogger
from docutils.parsers.rst import Directive
from .sphinx_ext_helper import traverse, NodeLeave, WrappedNode
[docs]class postcontents_node(nodes.paragraph):
"""
defines ``postcontents`` node
:githublink:`%|py|20`
"""
pass
[docs]class PostContentsDirective(Directive):
"""
Defines a sphinx extension which proposes a new version of ``.. contents::``
which takes into account titles dynamically added.
Example::
.. postcontents::
title 1
=======
.. runpython::
:rst:
print("title 2")
print("=======")
Which renders as:
.. contents::
:local:
title 1
=======
title 2
=======
Directive ``.. contents::`` 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.
:githublink:`%|py|58`
"""
node_class = postcontents_node
name_sphinx = "postcontents"
has_content = True
option_spec = {'depth': directives.unchanged,
'local': directives.unchanged}
[docs] def run(self):
"""
Just add a :class:`postcontents_node <pyquickhelper.sphinxext.sphinx_postcontents_extension.postcontents_node>`.
:return: list of nodes or list of nodes, container
:githublink:`%|py|71`
"""
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 = '' # pragma: no cover
node = postcontents_node()
node['pclineno'] = lineno
node['pcdocname'] = docname
node["pcprocessed"] = 0
node["depth"] = self.options.get("depth", "*")
node["local"] = self.options.get("local", None)
return [node]
[docs]def process_postcontents(app, doctree):
"""
Collect all *postcontents* in the environment.
Look for the section or document which contain them.
Put them into the variable *postcontents_all_postcontents* in the config.
:githublink:`%|py|96`
"""
logger = getLogger('postcontents')
env = app.builder.env
attr = 'postcontents_all_postcontents'
if not hasattr(env, attr):
setattr(env, attr, [])
attr_list = getattr(env, attr)
for node in doctree.traverse(postcontents_node):
# It looks for a section or document which contains the directive.
parent = node
while not isinstance(parent, (nodes.document, nodes.section)):
parent = node.parent
node["node_section"] = WrappedNode(parent)
node["pcprocessed"] += 1
node["processed"] = 1
attr_list.append(node)
logger.info("[postcontents] in '{}.rst' line={} found:{}".format(
node['pcdocname'], node['pclineno'], node['pcprocessed']))
_modify_postcontents(node, "postcontentsP")
[docs]def _modify_postcontents(node, event):
node["transformed"] = 1
logger = getLogger('postcontents')
logger.info("[{}] in '{}.rst' line={} found:{}".format(
event, node['pcdocname'], node['pclineno'], node['pcprocessed']))
parent = node["node_section"]
sections = []
main_par = nodes.paragraph()
node += main_par
roots = [main_par]
# depth = int(node["depth"]) if node["depth"] != '*' else 20
memo = {}
level = 0
for _, subnode in traverse(parent):
if isinstance(subnode, nodes.section):
if len(subnode["ids"]) == 0:
subnode["ids"].append("postid-{}".format(id(subnode)))
nid = subnode["ids"][0]
if nid in memo:
raise KeyError( # pragma: no cover
"node was already added '{0}'".format(nid))
logger.info("[{}] {}section id '{}'".format(
event, " " * level, nid))
level += 1
memo[nid] = subnode
bli = nodes.bullet_list()
roots[-1] += bli
roots.append(bli)
sections.append(subnode)
elif isinstance(subnode, nodes.title):
logger.info("[{}] {}title '{}'".format(
event, " " * level, subnode.astext()))
par = nodes.paragraph()
ref = nodes.reference(refid=sections[-1]["ids"][0],
reftitle=subnode.astext(),
text=subnode.astext())
par += ref
bullet = nodes.list_item()
bullet += par
roots[-1] += bullet
elif isinstance(subnode, NodeLeave):
parent = subnode.parent
if isinstance(parent, nodes.section):
ids = None if len(parent["ids"]) == 0 else parent["ids"][0]
if ids in memo:
level -= 1
logger.info("[{}] {}end of section '{}'".format(
event, " " * level, parent["ids"]))
sections.pop()
roots.pop()
[docs]def transform_postcontents(app, doctree, fromdocname):
"""
The function is called by event ``'doctree_resolved'``. It looks for
every section in page stored in *postcontents_all_postcontents*
in the configuration and builds a short table of contents.
The instruction ``.. contents::`` is resolved before every directive in
the page is executed, the instruction ``.. postcontents::`` 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``. ``.. postcontents::`` will capture the
new section this function might eventually add to the page.
For some reason, this function does not seem to be able to change
the doctree (any creation of nodes is not taken into account).
:githublink:`%|py|188`
"""
logger = logging.getLogger("postcontents")
# check this is something to process
env = app.builder.env
attr_name = 'postcontents_all_postcontents'
if not hasattr(env, attr_name):
setattr(env, attr_name, [])
post_list = getattr(env, attr_name)
if len(post_list) == 0:
# No postcontents found.
return
for node in post_list:
if node["pcprocessed"] != 1:
logger.warning("[postcontents] no first loop was ever processed: 'pcprocessed'={0} , File '{1}', line {2}".format(
node["pcprocessed"], node["pcdocname"], node["pclineno"]))
continue
if len(node.children) > 0:
# already processed
continue
_modify_postcontents(node, "postcontentsT")
[docs]def visit_postcontents_node(self, node):
"""
does nothing
:githublink:`%|py|216`
"""
pass
[docs]def depart_postcontents_node(self, node):
"""
does nothing
:githublink:`%|py|223`
"""
pass
[docs]def setup(app):
"""
setup for ``postcontents`` (sphinx)
:githublink:`%|py|230`
"""
if hasattr(app, "add_mapping"):
app.add_mapping('postcontents', postcontents_node)
app.add_node(postcontents_node,
html=(visit_postcontents_node, depart_postcontents_node),
epub=(visit_postcontents_node, depart_postcontents_node),
elatex=(visit_postcontents_node, depart_postcontents_node),
latex=(visit_postcontents_node, depart_postcontents_node),
text=(visit_postcontents_node, depart_postcontents_node),
md=(visit_postcontents_node, depart_postcontents_node),
rst=(visit_postcontents_node, depart_postcontents_node))
app.add_directive('postcontents', PostContentsDirective)
app.connect('doctree-read', process_postcontents)
app.connect('doctree-resolved', transform_postcontents)
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}