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 :epkg:`sphinx` extension which proposes a new version of ``.. contents::``
5which takes into account titles dynamically added.
6"""
7from docutils import nodes
8from docutils.parsers.rst import directives
9from sphinx.util import logging
11import sphinx
12from sphinx.util.logging import getLogger
13from docutils.parsers.rst import Directive
14from .sphinx_ext_helper import traverse, NodeLeave, WrappedNode
17class postcontents_node(nodes.paragraph):
18 """
19 defines ``postcontents`` node
20 """
21 pass
24class PostContentsDirective(Directive):
25 """
26 Defines a sphinx extension which proposes a new version of ``.. contents::``
27 which takes into account titles dynamically added.
29 Example::
31 .. postcontents::
33 title 1
34 =======
36 .. runpython::
37 :rst:
39 print("title 2")
40 print("=======")
42 Which renders as:
44 .. contents::
45 :local:
47 title 1
48 =======
50 title 2
51 =======
53 Directive ``.. contents::`` only considers titles defined by the user,
54 not titles dynamically created by another directives.
56 .. warning:: It is not recommended to dynamically insert
57 such a directive. It is not recursive.
58 """
60 node_class = postcontents_node
61 name_sphinx = "postcontents"
62 has_content = True
63 option_spec = {'depth': directives.unchanged,
64 'local': directives.unchanged}
66 def run(self):
67 """
68 Just add a @see cl postcontents_node.
70 @return list of nodes or list of nodes, container
71 """
72 lineno = self.lineno
74 settings = self.state.document.settings
75 env = settings.env if hasattr(settings, "env") else None
76 docname = None if env is None else env.docname
77 if docname is not None:
78 docname = docname.replace("\\", "/").split("/")[-1]
79 else:
80 docname = '' # pragma: no cover
82 node = postcontents_node()
83 node['pclineno'] = lineno
84 node['pcdocname'] = docname
85 node["pcprocessed"] = 0
86 node["depth"] = self.options.get("depth", "*")
87 node["local"] = self.options.get("local", None)
88 return [node]
91def process_postcontents(app, doctree):
92 """
93 Collect all *postcontents* in the environment.
94 Look for the section or document which contain them.
95 Put them into the variable *postcontents_all_postcontents* in the config.
96 """
97 logger = getLogger('postcontents')
98 env = app.builder.env
99 attr = 'postcontents_all_postcontents'
100 if not hasattr(env, attr):
101 setattr(env, attr, [])
102 attr_list = getattr(env, attr)
103 for node in doctree.traverse(postcontents_node):
104 # It looks for a section or document which contains the directive.
105 parent = node
106 while not isinstance(parent, (nodes.document, nodes.section)):
107 parent = node.parent
108 node["node_section"] = WrappedNode(parent)
109 node["pcprocessed"] += 1
110 node["processed"] = 1
111 attr_list.append(node)
112 logger.info("[postcontents] in '{}.rst' line={} found:{}".format(
113 node['pcdocname'], node['pclineno'], node['pcprocessed']))
114 _modify_postcontents(node, "postcontentsP")
117def _modify_postcontents(node, event):
118 node["transformed"] = 1
119 logger = getLogger('postcontents')
120 logger.info("[{}] in '{}.rst' line={} found:{}".format(
121 event, node['pcdocname'], node['pclineno'], node['pcprocessed']))
122 parent = node["node_section"]
123 sections = []
124 main_par = nodes.paragraph()
125 node += main_par
126 roots = [main_par]
127 # depth = int(node["depth"]) if node["depth"] != '*' else 20
128 memo = {}
129 level = 0
131 for _, subnode in traverse(parent):
132 if isinstance(subnode, nodes.section):
133 if len(subnode["ids"]) == 0:
134 subnode["ids"].append("postid-{}".format(id(subnode)))
135 nid = subnode["ids"][0]
136 if nid in memo:
137 raise KeyError( # pragma: no cover
138 "node was already added '{0}'".format(nid))
139 logger.info("[{}] {}section id '{}'".format(
140 event, " " * level, nid))
141 level += 1
142 memo[nid] = subnode
143 bli = nodes.bullet_list()
144 roots[-1] += bli
145 roots.append(bli)
146 sections.append(subnode)
147 elif isinstance(subnode, nodes.title):
148 logger.info("[{}] {}title '{}'".format(
149 event, " " * level, subnode.astext()))
150 par = nodes.paragraph()
151 ref = nodes.reference(refid=sections[-1]["ids"][0],
152 reftitle=subnode.astext(),
153 text=subnode.astext())
154 par += ref
155 bullet = nodes.list_item()
156 bullet += par
157 roots[-1] += bullet
158 elif isinstance(subnode, NodeLeave):
159 parent = subnode.parent
160 if isinstance(parent, nodes.section):
161 ids = None if len(parent["ids"]) == 0 else parent["ids"][0]
162 if ids in memo:
163 level -= 1
164 logger.info("[{}] {}end of section '{}'".format(
165 event, " " * level, parent["ids"]))
166 sections.pop()
167 roots.pop()
170def transform_postcontents(app, doctree, fromdocname):
171 """
172 The function is called by event ``'doctree_resolved'``. It looks for
173 every section in page stored in *postcontents_all_postcontents*
174 in the configuration and builds a short table of contents.
175 The instruction ``.. contents::`` is resolved before every directive in
176 the page is executed, the instruction ``.. postcontents::`` is resolved after.
178 @param app Sphinx application
179 @param doctree doctree
180 @param fromdocname docname
182 Thiis directive should be used if you need to capture a section
183 which was dynamically added by another one. For example @see cl RunPythonDirective
184 calls function ``nested_parse_with_titles``. ``.. postcontents::`` will capture the
185 new section this function might eventually add to the page.
186 For some reason, this function does not seem to be able to change
187 the doctree (any creation of nodes is not taken into account).
188 """
189 logger = logging.getLogger("postcontents")
191 # check this is something to process
192 env = app.builder.env
193 attr_name = 'postcontents_all_postcontents'
194 if not hasattr(env, attr_name):
195 setattr(env, attr_name, [])
196 post_list = getattr(env, attr_name)
197 if len(post_list) == 0:
198 # No postcontents found.
199 return
201 for node in post_list:
202 if node["pcprocessed"] != 1:
203 logger.warning("[postcontents] no first loop was ever processed: 'pcprocessed'={0} , File '{1}', line {2}".format(
204 node["pcprocessed"], node["pcdocname"], node["pclineno"]))
205 continue
206 if len(node.children) > 0:
207 # already processed
208 continue
210 _modify_postcontents(node, "postcontentsT")
213def visit_postcontents_node(self, node):
214 """
215 does nothing
216 """
217 pass
220def depart_postcontents_node(self, node):
221 """
222 does nothing
223 """
224 pass
227def setup(app):
228 """
229 setup for ``postcontents`` (sphinx)
230 """
231 if hasattr(app, "add_mapping"):
232 app.add_mapping('postcontents', postcontents_node)
234 app.add_node(postcontents_node,
235 html=(visit_postcontents_node, depart_postcontents_node),
236 epub=(visit_postcontents_node, depart_postcontents_node),
237 elatex=(visit_postcontents_node, depart_postcontents_node),
238 latex=(visit_postcontents_node, depart_postcontents_node),
239 text=(visit_postcontents_node, depart_postcontents_node),
240 md=(visit_postcontents_node, depart_postcontents_node),
241 rst=(visit_postcontents_node, depart_postcontents_node))
243 app.add_directive('postcontents', PostContentsDirective)
244 app.connect('doctree-read', process_postcontents)
245 app.connect('doctree-resolved', transform_postcontents)
246 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}