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 to keep track of blocs such as examples, FAQ, ...
5"""
6import os
7from docutils import nodes
8from docutils.parsers.rst import directives
10import sphinx
11from sphinx.locale import _
12try:
13 from sphinx.errors import NoUri
14except ImportError:
15 from sphinx.environment import NoUri
16from docutils.parsers.rst import Directive
17from docutils.parsers.rst.directives.admonitions import BaseAdmonition
18from docutils.statemachine import StringList
19from docutils.frontend import Values
20from sphinx.util.nodes import set_source_info, process_index_entry
21from sphinx import addnodes
22from ..texthelper.texts_language import TITLES
23from .sphinx_ext_helper import info_blocref
26class blocref_node(nodes.admonition):
27 """
28 Defines ``blocref`` node.
29 """
30 pass
33class blocreflist(nodes.General, nodes.Element):
34 """
35 defines ``blocreflist`` node
36 """
37 pass
40class BlocRef(BaseAdmonition):
41 """
42 A ``blocref`` entry, displayed in the form of an admonition.
43 It takes the following options:
45 * *title*: a title for the bloc
46 * *tag*: a tag to have several categories of blocs
47 * *lid* or *label*: a label to refer to
48 * *index*: to add an entry to the index (comma separated)
50 Example::
52 .. blocref::
53 :title: example of a blocref
54 :tag: example
55 :lid: id-you-can-choose
57 An example of code::
59 print("mignon")
61 Which renders as:
63 .. blocref::
64 :title: example of a blocref
65 :tag: dummy_example
66 :lid: id-you-can-choose
68 An example of code::
70 print("mignon")
72 All blocs can be displayed in another page by using ``blocreflist``::
74 .. blocreflist::
75 :tag: dummy_example
76 :sort: title
78 Only examples tagged as ``dummy_example`` will be inserted here.
79 The option ``sort`` sorts items by *title*, *number*, *file*.
80 You also link to it by typing ``:ref:'anchor <id-you-can-choose>'`` which gives
81 something like :ref:`link_to_blocref <id-you-can-choose>`. The link must receive a name.
83 .. blocreflist::
84 :tag: dummy_example
85 :sort: title
87 This directive is used to highlight a bloc about
88 anything @see cl BlocRef, a question @see cl FaqRef,
89 a magic command @see cl NbRef, an example @see cl ExRef.
90 It supports option *index* in most of the extensions
91 so that the documentation can refer to it.
92 """
94 node_class = blocref_node
95 name_sphinx = "blocref"
96 has_content = True
97 required_arguments = 0
98 optional_arguments = 0
99 final_argument_whitespace = False
100 option_spec = {
101 'class': directives.class_option,
102 'title': directives.unchanged,
103 'tag': directives.unchanged,
104 'lid': directives.unchanged,
105 'label': directives.unchanged,
106 'index': directives.unchanged,
107 }
109 def _update_title(self, title, tag, lid):
110 """
111 Updates the title for the bloc itself.
112 """
113 return title
115 def run(self):
116 """
117 Builds a node @see cl blocref_node.
118 """
119 return self.private_run()
121 def private_run(self, add_container=False):
122 """
123 Builds a node @see cl blocref_node.
125 @param add_container add a container node and return as a second result
126 @return list of nodes or list of nodes, container
127 """
128 name_desc = self.__class__.name_sphinx
129 lineno = self.lineno
131 settings = self.state.document.settings
132 env = settings.env if hasattr(settings, "env") else None
133 docname = None if env is None else env.docname
134 if docname is not None:
135 docname = docname.replace("\\", "/").split("/")[-1]
136 legend = "{0}:{1}".format(docname, lineno)
137 else:
138 legend = ''
140 if not self.options.get('class'):
141 self.options['class'] = ['admonition-%s' % name_desc]
143 # body
144 (blocref,) = super(BlocRef, self).run()
145 if isinstance(blocref, nodes.system_message):
146 return [blocref]
148 # add a label
149 lid = self.options.get('lid', self.options.get('label', None))
150 if lid:
151 container = nodes.container()
152 tnl = [".. _{0}:".format(lid), ""]
153 content = StringList(tnl)
154 self.state.nested_parse(content, self.content_offset, container)
155 else:
156 container = None
158 # mid
159 breftag = self.options.get('tag', '').strip()
160 if len(breftag) == 0:
161 raise ValueError("tag is empty") # pragma: no cover
162 if env is not None:
163 mid = int(env.new_serialno('index%s-%s' %
164 (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 Exception(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 = '%s_all_%ss' % (bloc_name, bloc_name)
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 = '%s_all_%ss' % (class_name, class_name)
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 = '%s_include_%ss' % (class_name, class_name)
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=['%s-source' % class_name])
428 # Create a target?
429 int_ids = ['index%s-%s' % (blocref_info['target']['refid'],
430 env.new_serialno(blocref_info['target']['refid']))]
431 int_targetnode = nodes.target(
432 blocref_info['breftitle'], '', ids=int_ids)
433 para += int_targetnode
435 # rest of the content
436 if app.config['%s_link_only' % class_name]:
437 description = _('<<%s>>' % orig_entry)
438 else:
439 description = (
440 _(brefmes) %
441 (orig_entry, os.path.split(blocref_info['source'])[-1],
442 blocref_info['lineno']))
443 desc1 = description[:description.find('<<')]
444 desc2 = description[description.find('>>') + 2:]
445 para += nodes.Text(desc1, desc1)
447 # Create a reference
448 newnode = nodes.reference('', '', internal=True)
449 newnode['name'] = _(orig_entry)
450 try:
451 newnode['refuri'] = app.builder.get_relative_uri(
452 fromdocname, blocref_info['docname'])
453 if blocref_info['target'] is None:
454 raise NoUri # pragma: no cover
455 try:
456 newnode['refuri'] += '#' + blocref_info['target']['refid']
457 except Exception as e: # pragma: no cover
458 raise KeyError("refid in not present in '{0}'".format(
459 blocref_info['target'])) from e
460 except NoUri: # pragma: no cover
461 # ignore if no URI can be determined, e.g. for LaTeX output
462 pass
464 newnode.append(nodes.Text(newnode['name']))
466 # para is duplicate of the content of the bloc
467 para += newnode
468 para += nodes.Text(desc2, desc2)
470 blocref_entry = blocref_info['blocref']
471 idss = ["index-%s-%d-%d" % (class_name, ilist, n)]
473 # Inserts into the blocreflist
474 # in the list of links at the beginning of the page.
475 if add_contents:
476 title = blocref_info['breftitle']
477 item = nodes.list_item()
478 p = nodes.paragraph()
479 item += p
480 newnode = nodes.reference('', title, internal=True)
481 try:
482 newnode['refuri'] = app.builder.get_relative_uri(
483 fromdocname, brefdocname)
484 newnode['refuri'] += '#' + idss[0]
485 except NoUri: # pragma: no cover
486 # ignore if no URI can be determined, e.g. for LaTeX output
487 pass
488 p += newnode
489 bullets += item
491 # Adds the content.
492 blocref_entry["ids"] = idss
493 if not hasattr(blocref_entry, "settings"):
494 blocref_entry.settings = Values()
495 blocref_entry.settings.env = env
496 # If an exception happens here, see blog 2017-05-21 from the
497 # documentation.
498 env.resolve_references(blocref_entry, blocref_info[
499 'docname'], app.builder)
500 content.append(blocref_entry)
501 content.append(para)
503 node.replace_self(content)
506def purge_blocrefs(app, env, docname):
507 """
508 purge_blocrefs
509 """
510 if not hasattr(env, 'blocref_all_blocrefs'):
511 return
512 env.blocref_all_blocrefs = [blocref for blocref in env.blocref_all_blocrefs
513 if blocref['docname'] != docname]
516def merge_blocref(app, env, docnames, other):
517 """
518 merge_blocref
519 """
520 if not hasattr(other, 'blocref_all_blocrefs'):
521 return
522 if not hasattr(env, 'blocref_all_blocrefs'):
523 env.blocref_all_blocrefs = []
524 env.blocref_all_blocrefs.extend(other.blocref_all_blocrefs)
527def visit_blocref_node(self, node):
528 """
529 visit_blocref_node
530 """
531 self.visit_admonition(node)
534def depart_blocref_node(self, node):
535 """
536 depart_blocref_node,
537 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
538 """
539 self.depart_admonition(node)
542def visit_blocreflist_node(self, node):
543 """
544 visit_blocreflist_node
545 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
546 """
547 self.visit_admonition(node)
550def depart_blocreflist_node(self, node):
551 """
552 depart_blocref_node
553 """
554 self.depart_admonition(node)
557def setup(app):
558 """
559 setup for ``blocref`` (sphinx)
560 """
561 if hasattr(app, "add_mapping"):
562 app.add_mapping('blocref', blocref_node)
563 app.add_mapping('blocreflist', blocreflist)
565 app.add_config_value('blocref_include_blocrefs', True, 'html')
566 app.add_config_value('blocref_link_only', False, 'html')
568 app.add_node(blocreflist,
569 html=(visit_blocreflist_node, depart_blocreflist_node),
570 epub=(visit_blocreflist_node, depart_blocreflist_node),
571 latex=(visit_blocreflist_node, depart_blocreflist_node),
572 elatex=(visit_blocreflist_node, depart_blocreflist_node),
573 text=(visit_blocreflist_node, depart_blocreflist_node),
574 md=(visit_blocreflist_node, depart_blocreflist_node),
575 rst=(visit_blocreflist_node, depart_blocreflist_node))
576 app.add_node(blocref_node,
577 html=(visit_blocref_node, depart_blocref_node),
578 epub=(visit_blocref_node, depart_blocref_node),
579 elatex=(visit_blocref_node, depart_blocref_node),
580 latex=(visit_blocref_node, depart_blocref_node),
581 text=(visit_blocref_node, depart_blocref_node),
582 md=(visit_blocref_node, depart_blocref_node),
583 rst=(visit_blocref_node, depart_blocref_node))
585 app.add_directive('blocref', BlocRef)
586 app.add_directive('blocreflist', BlocRefList)
587 app.connect('doctree-read', process_blocrefs)
588 app.connect('doctree-resolved', process_blocref_nodes)
589 app.connect('env-purge-doc', purge_blocrefs)
590 app.connect('env-merge-info', merge_blocref)
591 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}