Coverage for pyquickhelper/sphinxext/sphinx_blocref_extension.py: 87%
290 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 to keep track of blocs such as examples, FAQ, ...
5"""
6import os
7import logging
8from docutils import nodes
9from docutils.parsers.rst import directives
11import sphinx
12from sphinx.locale import _
13try:
14 from sphinx.errors import NoUri
15except ImportError:
16 from sphinx.environment import NoUri
17from docutils.parsers.rst import Directive
18from docutils.parsers.rst.directives.admonitions import BaseAdmonition
19from docutils.statemachine import StringList
20from docutils.frontend import Values
21from sphinx.util.nodes import set_source_info, process_index_entry
22from sphinx import addnodes
23from ..texthelper.texts_language import TITLES
24from .sphinx_ext_helper import info_blocref
27class blocref_node(nodes.admonition):
28 """
29 Defines ``blocref`` node.
30 """
31 pass
34class blocreflist(nodes.General, nodes.Element):
35 """
36 defines ``blocreflist`` node
37 """
38 pass
41class BlocRef(BaseAdmonition):
42 """
43 A ``blocref`` entry, displayed in the form of an admonition.
44 It takes the following options:
46 * *title*: a title for the bloc
47 * *tag*: a tag to have several categories of blocs
48 * *lid* or *label*: a label to refer to
49 * *index*: to add an entry to the index (comma separated)
51 Example::
53 .. blocref::
54 :title: example of a blocref
55 :tag: example
56 :lid: id-you-can-choose
58 An example of code::
60 print("mignon")
62 Which renders as:
64 .. blocref::
65 :title: example of a blocref
66 :tag: dummy_example
67 :lid: id-you-can-choose
69 An example of code::
71 print("mignon")
73 All blocs can be displayed in another page by using ``blocreflist``::
75 .. blocreflist::
76 :tag: dummy_example
77 :sort: title
79 Only examples tagged as ``dummy_example`` will be inserted here.
80 The option ``sort`` sorts items by *title*, *number*, *file*.
81 You also link to it by typing ``:ref:'anchor <id-you-can-choose>'`` which gives
82 something like :ref:`link_to_blocref <id-you-can-choose>`. The link must receive a name.
84 .. blocreflist::
85 :tag: dummy_example
86 :sort: title
88 This directive is used to highlight a bloc about
89 anything @see cl BlocRef, a question @see cl FaqRef,
90 a magic command @see cl NbRef, an example @see cl ExRef.
91 It supports option *index* in most of the extensions
92 so that the documentation can refer to it.
93 """
95 node_class = blocref_node
96 name_sphinx = "blocref"
97 has_content = True
98 required_arguments = 0
99 optional_arguments = 0
100 final_argument_whitespace = False
101 option_spec = {
102 'class': directives.class_option,
103 'title': directives.unchanged,
104 'tag': directives.unchanged,
105 'lid': directives.unchanged,
106 'label': directives.unchanged,
107 'index': directives.unchanged,
108 }
110 def _update_title(self, title, tag, lid):
111 """
112 Updates the title for the bloc itself.
113 """
114 return title
116 def run(self):
117 """
118 Builds a node @see cl blocref_node.
119 """
120 return self.private_run()
122 def private_run(self, add_container=False):
123 """
124 Builds a node @see cl blocref_node.
126 @param add_container add a container node and return as a second result
127 @return list of nodes or list of nodes, container
128 """
129 name_desc = self.__class__.name_sphinx
130 lineno = self.lineno
132 settings = self.state.document.settings
133 env = settings.env if hasattr(settings, "env") else None
134 docname = None if env is None else env.docname
135 if docname is not None:
136 docname = docname.replace("\\", "/").split("/")[-1]
137 legend = f"{docname}:{lineno}"
138 else:
139 legend = ''
141 if not self.options.get('class'):
142 self.options['class'] = [f'admonition-{name_desc}']
144 # body
145 (blocref,) = super(BlocRef, self).run()
146 if isinstance(blocref, nodes.system_message):
147 return [blocref]
149 # add a label
150 lid = self.options.get('lid', self.options.get('label', None))
151 if lid:
152 container = nodes.container()
153 tnl = [f".. _{lid}:", ""]
154 content = StringList(tnl)
155 self.state.nested_parse(content, self.content_offset, container)
156 else:
157 container = None
159 # mid
160 breftag = self.options.get('tag', '').strip()
161 if len(breftag) == 0:
162 raise ValueError("tag is empty") # pragma: no cover
163 if env is not None:
164 mid = int(env.new_serialno(f'index{name_desc}-{breftag}')) + 1
165 else:
166 mid = -1
168 # title
169 titleo = self.options.get('title', "").strip()
170 if len(titleo) == 0:
171 raise ValueError("title is empty") # pragma: no cover
172 title = self._update_title(titleo, breftag, mid)
174 # main node
175 ttitle = title
176 title = nodes.title(text=_(title))
177 if container is not None:
178 blocref.insert(0, title)
179 blocref.insert(0, container)
180 else:
181 blocref.insert(0, title)
183 if add_container:
184 ret_container = nodes.container()
185 blocref += ret_container
187 blocref['breftag'] = breftag
188 blocref['brefmid'] = mid
189 blocref['breftitle'] = ttitle
190 blocref['breftitleo'] = titleo
191 blocref['brefline'] = lineno
192 blocref['breffile'] = docname
193 set_source_info(self, blocref)
195 if env is not None:
196 targetid = 'index%s-%s%s' % (name_desc, breftag,
197 env.new_serialno('index%s%s' % (name_desc, breftag)))
198 blocref["breftargetid"] = targetid
199 ids = [targetid]
200 targetnode = nodes.target(legend, '', ids=ids)
201 set_source_info(self, targetnode)
202 try:
203 self.state.add_target(targetid, '', targetnode, lineno)
204 except Exception as e: # pragma: no cover
205 mes = "Issue in \n File '{0}', line {1}\ntitle={2}\ntag={3}\ntargetid={4}"
206 raise RuntimeError(mes.format(docname, lineno,
207 title, breftag, targetid)) from e
209 # index node
210 index = self.options.get('index', None)
211 if index is not None:
212 indexnode = addnodes.index()
213 indexnode['entries'] = ne = []
214 indexnode['inline'] = False
215 set_source_info(self, indexnode)
216 for entry in index.split(","):
217 ne.extend(process_index_entry(entry, targetid))
218 else:
219 indexnode = None
220 else:
221 targetnode = None
222 indexnode = None
224 res = [a for a in [indexnode, targetnode, blocref] if a is not None]
225 if add_container:
226 return res, ret_container
227 return res
230def process_blocrefs(app, doctree):
231 """
232 collect all blocrefs in the environment
233 this is not done in the directive itself because it some transformations
234 must have already been run, e.g. substitutions
235 """
236 process_blocrefs_generic(
237 app, doctree, bloc_name="blocref", class_node=blocref_node)
240def process_blocrefs_generic(app, doctree, bloc_name, class_node):
241 """
242 collect all blocrefs in the environment
243 this is not done in the directive itself because it some transformations
244 must have already been run, e.g. substitutions
245 """
246 env = app.builder.env
247 attr = f'{bloc_name}_all_{bloc_name}s'
248 if not hasattr(env, attr):
249 setattr(env, attr, [])
250 attr_list = getattr(env, attr)
251 for node in doctree.traverse(class_node):
252 try:
253 targetnode = node.parent[node.parent.index(node) - 1]
254 if not isinstance(targetnode, nodes.target):
255 raise IndexError # pragma: no cover
256 except IndexError: # pragma: no cover
257 targetnode = None
258 newnode = node.deepcopy()
259 breftag = newnode['breftag']
260 breftitle = newnode['breftitle']
261 brefmid = newnode['brefmid']
262 brefline = newnode['brefline']
263 breffile = newnode['breffile']
264 del newnode['ids']
265 del newnode['breftag']
266 attr_list.append({
267 'docname': env.docname,
268 'source': node.source or env.doc2path(env.docname),
269 'lineno': node.line,
270 'blocref': newnode,
271 'target': targetnode,
272 'breftag': breftag,
273 'breftitle': breftitle,
274 'brefmid': brefmid,
275 'brefline': brefline,
276 'breffile': breffile,
277 })
280class BlocRefList(Directive):
281 """
282 A list of all blocref entries, for a specific tag.
284 * tag: a tag to filter bloc having this tag
285 * sort: a way to sort the blocs based on the title, file, number, default: *title*
286 * contents: add a bullet list with links to added blocs
288 Example::
290 .. blocreflist::
291 :tag: issue
292 :contents:
293 """
294 name_sphinx = "blocreflist"
295 node_class = blocreflist
296 has_content = False
297 required_arguments = 0
298 optional_arguments = 0
299 final_argument_whitespace = False
300 option_spec = {
301 'tag': directives.unchanged,
302 'sort': directives.unchanged,
303 'contents': directives.unchanged,
304 }
306 def run(self):
307 """
308 Simply insert an empty blocreflist node which will be replaced later
309 when process_blocref_nodes is called
310 """
311 name_desc = self.__class__.name_sphinx
312 settings = self.state.document.settings
313 env = settings.env if hasattr(settings, "env") else None
314 docname = None if env is None else env.docname
315 tag = self.options.get('tag', '').strip()
316 n = self.__class__.node_class('')
317 n["breftag"] = tag
318 n["brefsort"] = self.options.get('sort', 'title').strip()
319 n["brefsection"] = self.options.get(
320 'section', True) in (True, "True", "true", 1, "1")
321 n["brefcontents"] = self.options.get(
322 'contents', False) in (True, "True", "true", 1, "1", "", None, "None")
323 n['docname'] = docname
324 if env is not None:
325 targetid = 'index%slist-%s' % (name_desc,
326 env.new_serialno('index%slist' % name_desc))
327 targetnode = nodes.target('', '', ids=[targetid])
328 return [targetnode, n]
329 else:
330 return [n]
333def process_blocref_nodes(app, doctree, fromdocname):
334 """
335 process_blocref_nodes
336 """
337 process_blocref_nodes_generic(app, doctree, fromdocname, class_name='blocref',
338 entry_name="brefmes", class_node=blocref_node,
339 class_node_list=blocreflist)
342def process_blocref_nodes_generic(app, doctree, fromdocname, class_name,
343 entry_name, class_node, class_node_list):
344 """
345 process_blocref_nodes and other kinds of nodes,
347 If the configuration file specifies a variable ``blocref_include_blocrefs`` equals to False,
348 all nodes are removed.
349 """
350 # logging
351 cont = info_blocref(app, doctree, fromdocname, class_name,
352 entry_name, class_node, class_node_list)
353 if not cont:
354 return
356 # check this is something to process
357 env = app.builder.env
358 attr_name = f'{class_name}_all_{class_name}s'
359 if not hasattr(env, attr_name):
360 setattr(env, attr_name, [])
361 bloc_list_env = getattr(env, attr_name)
362 if len(bloc_list_env) == 0:
363 return
365 # content
366 incconf = f'{class_name}_include_{class_name}s'
367 if app.config[incconf] and not app.config[incconf]:
368 for node in doctree.traverse(class_node):
369 node.parent.remove(node)
371 # Replace all blocreflist nodes with a list of the collected blocrefs.
372 # Augment each blocref with a backlink to the original location.
373 if hasattr(env, "settings"):
374 settings = env.settings
375 if hasattr(settings, "language_code"):
376 lang = env.settings.language_code
377 else:
378 lang = "en"
379 else:
380 settings = None
381 lang = "en"
383 orig_entry = TITLES[lang]["original entry"]
384 brefmes = TITLES[lang][entry_name]
386 for ilist, node in enumerate(doctree.traverse(class_node_list)):
387 if 'ids' in node:
388 node['ids'] = []
389 if not app.config[incconf]:
390 node.replace_self([])
391 continue
393 nbbref = 0
394 content = []
395 breftag = node["breftag"]
396 brefsort = node["brefsort"]
397 add_contents = node["brefcontents"]
398 brefdocname = node["docname"]
400 if add_contents:
401 bullets = nodes.enumerated_list()
402 content.append(bullets)
404 # sorting
405 if brefsort == 'title':
406 double_list = [(info.get('breftitle', ''), info)
407 for info in bloc_list_env if info['breftag'] == breftag]
408 double_list.sort(key=lambda x: x[:1])
409 elif brefsort == 'file':
410 double_list = [((info.get('breffile', ''), info.get('brefline', '')), info)
411 for info in bloc_list_env if info['breftag'] == breftag]
412 double_list.sort(key=lambda x: x[:1])
413 elif brefsort == 'number':
414 double_list = [(info.get('brefmid', ''), info)
415 for info in bloc_list_env if info['breftag'] == breftag]
416 double_list.sort(key=lambda x: x[:1])
417 else:
418 raise ValueError("sort option should be file, number, title")
420 # printing
421 for n, blocref_info_ in enumerate(double_list):
422 blocref_info = blocref_info_[1]
424 nbbref += 1
426 para = nodes.paragraph(classes=[f'{class_name}-source'])
428 # Create a target?
429 try:
430 targ = blocref_info['target']
431 except KeyError as e:
432 logger = logging.getLogger("blocref")
433 logger.warning(
434 "Unable to find key 'target' in %r (e=%r)", blocref_info, e)
435 continue
436 try:
437 targ_refid = blocref_info['target']['refid']
438 except KeyError as e:
439 logger = logging.getLogger("blocref")
440 logger.warning(
441 "Unable to find key 'refid' in %r (e=%r)", targ, e)
442 continue
443 int_ids = [f'index{targ_refid}-{env.new_serialno(targ_refid)}']
444 int_targetnode = nodes.target(
445 blocref_info['breftitle'], '', ids=int_ids)
446 para += int_targetnode
448 # rest of the content
449 if app.config[f'{class_name}_link_only']:
450 description = _(f'<<{orig_entry}>>')
451 else:
452 description = (
453 _(brefmes) %
454 (orig_entry, os.path.split(blocref_info['source'])[-1],
455 blocref_info['lineno']))
456 desc1 = description[:description.find('<<')]
457 desc2 = description[description.find('>>') + 2:]
458 para += nodes.Text(desc1, desc1)
460 # Create a reference
461 newnode = nodes.reference('', '', internal=True)
462 newnode['name'] = _(orig_entry)
463 try:
464 newnode['refuri'] = app.builder.get_relative_uri(
465 fromdocname, blocref_info['docname'])
466 if blocref_info['target'] is None:
467 raise NoUri # pragma: no cover
468 try:
469 newnode['refuri'] += '#' + blocref_info['target']['refid']
470 except Exception as e: # pragma: no cover
471 raise KeyError("refid in not present in '{0}'".format(
472 blocref_info['target'])) from e
473 except NoUri: # pragma: no cover
474 # ignore if no URI can be determined, e.g. for LaTeX output
475 pass
477 newnode.append(nodes.Text(newnode['name']))
479 # para is duplicate of the content of the bloc
480 para += newnode
481 para += nodes.Text(desc2, desc2)
483 blocref_entry = blocref_info['blocref']
484 idss = ["index-%s-%d-%d" % (class_name, ilist, n)]
486 # Inserts into the blocreflist
487 # in the list of links at the beginning of the page.
488 if add_contents:
489 title = blocref_info['breftitle']
490 item = nodes.list_item()
491 p = nodes.paragraph()
492 item += p
493 newnode = nodes.reference('', title, internal=True)
494 try:
495 newnode['refuri'] = app.builder.get_relative_uri(
496 fromdocname, brefdocname)
497 newnode['refuri'] += '#' + idss[0]
498 except NoUri: # pragma: no cover
499 # ignore if no URI can be determined, e.g. for LaTeX output
500 pass
501 p += newnode
502 bullets += item
504 # Adds the content.
505 blocref_entry["ids"] = idss
506 if not hasattr(blocref_entry, "settings"):
507 blocref_entry.settings = Values()
508 blocref_entry.settings.env = env
509 # If an exception happens here, see blog 2017-05-21 from the
510 # documentation.
511 env.resolve_references(blocref_entry, blocref_info[
512 'docname'], app.builder)
513 content.append(blocref_entry)
514 content.append(para)
516 node.replace_self(content)
519def purge_blocrefs(app, env, docname):
520 """
521 purge_blocrefs
522 """
523 if not hasattr(env, 'blocref_all_blocrefs'):
524 return
525 env.blocref_all_blocrefs = [blocref for blocref in env.blocref_all_blocrefs
526 if blocref['docname'] != docname]
529def merge_blocref(app, env, docnames, other):
530 """
531 merge_blocref
532 """
533 if not hasattr(other, 'blocref_all_blocrefs'):
534 return
535 if not hasattr(env, 'blocref_all_blocrefs'):
536 env.blocref_all_blocrefs = []
537 env.blocref_all_blocrefs.extend(other.blocref_all_blocrefs)
540def visit_blocref_node(self, node):
541 """
542 visit_blocref_node
543 """
544 self.visit_admonition(node)
547def depart_blocref_node(self, node):
548 """
549 depart_blocref_node,
550 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
551 """
552 self.depart_admonition(node)
555def visit_blocreflist_node(self, node):
556 """
557 visit_blocreflist_node
558 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
559 """
560 self.visit_admonition(node)
563def depart_blocreflist_node(self, node):
564 """
565 depart_blocref_node
566 """
567 self.depart_admonition(node)
570def setup(app):
571 """
572 setup for ``blocref`` (sphinx)
573 """
574 if hasattr(app, "add_mapping"):
575 app.add_mapping('blocref', blocref_node)
576 app.add_mapping('blocreflist', blocreflist)
578 app.add_config_value('blocref_include_blocrefs', True, 'html')
579 app.add_config_value('blocref_link_only', False, 'html')
581 app.add_node(blocreflist,
582 html=(visit_blocreflist_node, depart_blocreflist_node),
583 epub=(visit_blocreflist_node, depart_blocreflist_node),
584 latex=(visit_blocreflist_node, depart_blocreflist_node),
585 elatex=(visit_blocreflist_node, depart_blocreflist_node),
586 text=(visit_blocreflist_node, depart_blocreflist_node),
587 md=(visit_blocreflist_node, depart_blocreflist_node),
588 rst=(visit_blocreflist_node, depart_blocreflist_node))
589 app.add_node(blocref_node,
590 html=(visit_blocref_node, depart_blocref_node),
591 epub=(visit_blocref_node, depart_blocref_node),
592 elatex=(visit_blocref_node, depart_blocref_node),
593 latex=(visit_blocref_node, depart_blocref_node),
594 text=(visit_blocref_node, depart_blocref_node),
595 md=(visit_blocref_node, depart_blocref_node),
596 rst=(visit_blocref_node, depart_blocref_node))
598 app.add_directive('blocref', BlocRef)
599 app.add_directive('blocreflist', BlocRefList)
600 app.connect('doctree-read', process_blocrefs)
601 app.connect('doctree-resolved', process_blocref_nodes)
602 app.connect('env-purge-doc', purge_blocrefs)
603 app.connect('env-merge-info', merge_blocref)
604 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}