Coverage for pyquickhelper/sphinxext/sphinx_postcontents_extension.py: 96%
115 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
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(f"postid-{id(subnode)}")
135 nid = subnode["ids"][0]
136 if nid in memo:
137 raise KeyError( # pragma: no cover
138 f"node was already added '{nid}'")
139 logger.info("[%r] %ssection id %r", event, ' ' * level, nid)
140 level += 1
141 memo[nid] = subnode
142 bli = nodes.bullet_list()
143 roots[-1] += bli
144 roots.append(bli)
145 sections.append(subnode)
146 elif isinstance(subnode, nodes.title):
147 logger.info("[%r] %stitle %r", event,
148 ' ' * level, subnode.astext())
149 par = nodes.paragraph()
150 ref = nodes.reference(refid=sections[-1]["ids"][0],
151 reftitle=subnode.astext(),
152 text=subnode.astext())
153 par += ref
154 bullet = nodes.list_item()
155 bullet += par
156 roots[-1] += bullet
157 elif isinstance(subnode, NodeLeave):
158 parent = subnode.parent
159 if isinstance(parent, nodes.section):
160 ids = None if len(parent["ids"]) == 0 else parent["ids"][0]
161 if ids in memo:
162 level -= 1
163 logger.info("[%r] %send of section %r",
164 event, " " * level, parent["ids"])
165 sections.pop()
166 roots.pop()
169def transform_postcontents(app, doctree, fromdocname):
170 """
171 The function is called by event ``'doctree_resolved'``. It looks for
172 every section in page stored in *postcontents_all_postcontents*
173 in the configuration and builds a short table of contents.
174 The instruction ``.. contents::`` is resolved before every directive in
175 the page is executed, the instruction ``.. postcontents::`` 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``. ``.. postcontents::`` will capture the
184 new section this function might eventually add to the page.
185 For some reason, this function does not seem to be able to change
186 the doctree (any creation of nodes is not taken into account).
187 """
188 logger = logging.getLogger("postcontents")
190 # check this is something to process
191 env = app.builder.env
192 attr_name = 'postcontents_all_postcontents'
193 if not hasattr(env, attr_name):
194 setattr(env, attr_name, [])
195 post_list = getattr(env, attr_name)
196 if len(post_list) == 0:
197 # No postcontents found.
198 return
200 for node in post_list:
201 if node["pcprocessed"] != 1:
202 logger.warning("[postcontents] no first loop was ever processed: 'pcprocessed'=%s , File %r, line %r",
203 node["pcprocessed"], node["pcdocname"], node["pclineno"])
204 continue
205 if len(node.children) > 0:
206 # already processed
207 continue
209 _modify_postcontents(node, "postcontentsT")
212def visit_postcontents_node(self, node):
213 """
214 does nothing
215 """
216 pass
219def depart_postcontents_node(self, node):
220 """
221 does nothing
222 """
223 pass
226def setup(app):
227 """
228 setup for ``postcontents`` (sphinx)
229 """
230 if hasattr(app, "add_mapping"):
231 app.add_mapping('postcontents', postcontents_node)
233 app.add_node(postcontents_node,
234 html=(visit_postcontents_node, depart_postcontents_node),
235 epub=(visit_postcontents_node, depart_postcontents_node),
236 elatex=(visit_postcontents_node, depart_postcontents_node),
237 latex=(visit_postcontents_node, depart_postcontents_node),
238 text=(visit_postcontents_node, depart_postcontents_node),
239 md=(visit_postcontents_node, depart_postcontents_node),
240 rst=(visit_postcontents_node, depart_postcontents_node))
242 app.add_directive('postcontents', PostContentsDirective)
243 app.connect('doctree-read', process_postcontents)
244 app.connect('doctree-resolved', transform_postcontents)
245 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}