Coverage for pyquickhelper/sphinxext/sphinx_mathdef_extension.py: 89%
241 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 give a title to a mathematical
5definition, theorem...
6Inspired from `math.py <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/ext/math.py>`_.
7"""
8import os
9from docutils import nodes
10from docutils.parsers.rst import directives
11from docutils.frontend import Values
13import sphinx
14from sphinx.locale import _
15try:
16 from sphinx.errors import NoUri
17except ImportError: # pragma: no cover
18 from sphinx.environment import NoUri
19from docutils.parsers.rst import Directive
20from docutils.parsers.rst.directives.admonitions import BaseAdmonition
21from docutils.statemachine import StringList
22from sphinx.util.nodes import set_source_info, process_index_entry
23from sphinx import addnodes
24from ..texthelper.texts_language import TITLES
27class mathdef_node(nodes.admonition):
28 """
29 Defines ``mathdef`` node.
30 """
31 pass
34class mathdeflist(nodes.General, nodes.Element):
35 """
36 Defines ``mathdeflist`` node.
37 """
38 pass
41class MathDef(BaseAdmonition):
42 """
43 A ``mathdef`` entry, displayed in the form of an admonition.
44 It takes the following options:
46 * *title*: a title for the math
47 * *tag*: a tag to have several categories of math
48 * *lid* or *label*: a label to refer to
49 * *index*: to add an entry to the index (comma separated)
51 Example::
53 .. mathdef::
54 :title: title
55 :tag: definition or theorem or ...
56 :lid: id (used for further reference)
58 Description of the math
59 """
61 node_class = mathdef_node
62 has_content = True
63 required_arguments = 0
64 optional_arguments = 0
65 final_argument_whitespace = False
66 option_spec = {
67 'class': directives.class_option,
68 'title': directives.unchanged,
69 'tag': directives.unchanged,
70 'lid': directives.unchanged,
71 'label': directives.unchanged,
72 'index': directives.unchanged,
73 }
75 def run(self):
76 """
77 Builds the mathdef text.
78 """
79 # sett = self.state.document.settings
80 # language_code = sett.language_code
81 lineno = self.lineno
83 env = self.state.document.settings.env if hasattr(
84 self.state.document.settings, "env") else None
85 docname = None if env is None else env.docname
86 if docname is not None:
87 docname = docname.replace("\\", "/").split("/")[-1]
88 legend = f"{docname}:{lineno}"
89 else:
90 legend = ''
92 if hasattr(env, "settings") and hasattr(env.settings, "mathdef_link_number"):
93 number_format = env.settings.mathdef_link_number
94 elif hasattr(self.state.document.settings, "mathdef_link_number"):
95 number_format = self.state.document.settings.mathdef_link_number
96 elif hasattr(env, "config") and hasattr(env.config, "mathdef_link_number"):
97 number_format = env.config.mathdef_link_number
98 else:
99 raise ValueError( # pragma: no cover
100 "mathdef_link_number is not defined in the configuration")
102 if not self.options.get('class'):
103 self.options['class'] = ['admonition-mathdef']
105 # body
106 (mathdef,) = super(MathDef, self).run()
107 if isinstance(mathdef, nodes.system_message):
108 return [mathdef]
110 # add a label
111 lid = self.options.get('lid', self.options.get('label', None))
112 if lid:
113 container = nodes.container()
114 tnl = [f".. _{lid}:", ""]
115 content = StringList(tnl)
116 self.state.nested_parse(content, self.content_offset, container)
117 else:
118 container = None
120 # mid
121 mathtag = self.options.get('tag', '').strip()
122 if len(mathtag) == 0:
123 raise ValueError("tag is empty") # pragma: no cover
124 if env is not None:
125 mid = int(env.new_serialno(f'indexmathe-u-{mathtag}')) + 1
126 else:
127 mid = -1
129 # id of the section
130 first_letter = mathtag[0].upper()
131 number = mid
132 try:
133 label_number = number_format.format(
134 number=number, first_letter=first_letter)
135 except ValueError as e: # pragma: no cover
136 raise RuntimeError(
137 f"Unable to interpret format '{number_format}'.") from e
139 # title
140 title = self.options.get('title', "").strip()
141 if len(title) > 0:
142 title = f"{mathtag} {label_number} : {title}"
143 else:
144 raise ValueError("title is empty") # pragma: no cover
146 # main node
147 ttitle = title
148 title = nodes.title(text=_(title))
149 if container is not None:
150 mathdef.insert(0, title)
151 mathdef.insert(0, container)
152 else:
153 mathdef.insert(0, title)
154 mathdef['mathtag'] = mathtag
155 mathdef['mathmid'] = mid
156 mathdef['mathtitle'] = ttitle
157 set_source_info(self, mathdef)
159 if env is not None:
160 targetid = 'indexmathe-%s%s' % (mathtag,
161 env.new_serialno('indexmathe%s' % mathtag))
162 ids = [targetid]
163 targetnode = nodes.target(legend, '', ids=ids[0])
164 set_source_info(self, targetnode)
165 try:
166 self.state.add_target(targetid, '', targetnode, lineno)
167 except Exception as e: # pragma: no cover
168 raise RuntimeError(
169 "Issue in\n File '{0}', line {1}\ntid={2}\ntnode={3}".format(
170 None if env is None else env.docname, lineno,
171 targetid, targetnode)) from e
173 # index node
174 index = self.options.get('index', None)
175 imposed = ",".join(a for a in [mathtag, ttitle] if a)
176 if index is None or len(index.strip()) == 0:
177 index = imposed
178 else:
179 index += "," + imposed
180 if index is not None:
181 indexnode = addnodes.index()
182 indexnode['entries'] = ne = []
183 indexnode['inline'] = False
184 set_source_info(self, indexnode)
185 for entry in index.split(","):
186 ne.extend(process_index_entry(entry, targetid))
187 else:
188 indexnode = None
189 else:
190 targetnode = None
191 indexnode = None
193 return [a for a in [indexnode, targetnode, mathdef] if a is not None]
196def process_mathdefs(app, doctree):
197 """
198 collect all mathdefs in the environment
199 this is not done in the directive itself because it some transformations
200 must have already been run, e.g. substitutions
201 """
202 env = app.builder.env
203 if not hasattr(env, 'mathdef_all_mathsext'):
204 env.mathdef_all_mathsext = []
205 for node in doctree.traverse(mathdef_node):
206 try:
207 targetnode = node.parent[node.parent.index(node) - 1]
208 if not isinstance(targetnode, nodes.target):
209 raise IndexError # pragma: no cover
210 except IndexError: # pragma: no cover
211 targetnode = None
212 newnode = node.deepcopy()
213 mathtag = newnode['mathtag']
214 mathtitle = newnode['mathtitle']
215 mathmid = newnode['mathmid']
216 del newnode['ids']
217 del newnode['mathtag']
218 env.mathdef_all_mathsext.append({
219 'docname': env.docname,
220 'source': node.source or env.doc2path(env.docname),
221 'lineno': node.line,
222 'mathdef': newnode,
223 'target': targetnode,
224 'mathtag': mathtag,
225 'mathtitle': mathtitle,
226 'mathmid': mathmid,
227 })
230class MathDefList(Directive):
231 """
232 A list of all mathdef entries, for a specific tag.
234 * tag: a tag to have several categories of mathdef
235 * contents: add a bullet list with links to added blocs
237 Example::
239 .. mathdeflist::
240 :tag: issue
241 :contents:
242 """
244 has_content = False
245 required_arguments = 0
246 optional_arguments = 0
247 final_argument_whitespace = False
248 option_spec = {
249 'tag': directives.unchanged,
250 'contents': directives.unchanged,
251 }
253 def run(self):
254 """
255 Simply insert an empty mathdeflist node which will be replaced later
256 when process_mathdef_nodes is called
257 """
258 env = self.state.document.settings.env if hasattr(
259 self.state.document.settings, "env") else None
260 tag = self.options.get('tag', '').strip()
261 contents = self.options.get(
262 'contents', False) in (True, "True", "true", 1,
263 "1", "", None, "None")
264 if env is not None:
265 targetid = f"indexmathelist-{env.new_serialno('indexmathelist')}"
266 targetnode = nodes.target('', '', ids=[targetid])
267 n = mathdeflist('')
268 n["mathtag"] = tag
269 n["mathcontents"] = contents
270 n['docname'] = env.docname if env else "none"
271 return [targetnode, n]
273 n = mathdeflist('')
274 n["mathtag"] = tag
275 n["mathcontents"] = contents
276 n['docname'] = env.docname if env else "none"
277 return [n]
280def process_mathdef_nodes(app, doctree, fromdocname):
281 """
282 process_mathdef_nodes
283 """
284 if not app.config['mathdef_include_mathsext']:
285 for node in doctree.traverse(mathdef_node):
286 node.parent.remove(node)
288 # Replace all mathdeflist nodes with a list of the collected mathsext.
289 # Augment each mathdef with a backlink to the original location.
290 env = app.builder.env
291 if hasattr(env, "settings") and hasattr(env.settings, "language_code"):
292 lang = env.settings.language_code
293 else:
294 lang = "en"
296 orig_entry = TITLES[lang]["original entry"]
297 mathmes = TITLES[lang]["mathmes"]
299 if not hasattr(env, 'mathdef_all_mathsext'):
300 env.mathdef_all_mathsext = []
302 for ilist, node in enumerate(doctree.traverse(mathdeflist)):
303 if 'ids' in node:
304 node['ids'] = []
305 if not app.config['mathdef_include_mathsext']:
306 node.replace_self([])
307 continue
309 nbmath = 0
310 content = []
311 mathtag = node["mathtag"]
312 add_contents = node["mathcontents"]
313 mathdocname = node["docname"]
315 if add_contents:
316 bullets = nodes.enumerated_list()
317 content.append(bullets)
319 double_list = [(info.get('mathtitle', ''), info)
320 for info in env.mathdef_all_mathsext]
321 double_list.sort(key=lambda x: x[:1])
322 for n, mathdef_info_ in enumerate(double_list):
323 mathdef_info = mathdef_info_[1]
324 if mathdef_info["mathtag"] != mathtag:
325 continue
327 nbmath += 1
328 para = nodes.paragraph(classes=['mathdef-source'])
329 if app.config['mathdef_link_only']:
330 description = _(f'<<{orig_entry}>>')
331 else:
332 description = (
333 _(mathmes) %
334 (orig_entry, os.path.split(mathdef_info['source'])[-1],
335 mathdef_info['lineno'])
336 )
337 desc1 = description[:description.find('<<')]
338 desc2 = description[description.find('>>') + 2:]
339 para += nodes.Text(desc1, desc1)
341 # Create a reference
342 newnode = nodes.reference('', '', internal=True)
343 innernode = nodes.emphasis('', _(orig_entry))
344 try:
345 newnode['refuri'] = app.builder.get_relative_uri(
346 fromdocname, mathdef_info['docname'])
347 try:
348 newnode['refuri'] += '#' + mathdef_info['target']['refid']
349 except Exception as e: # pragma: no cover
350 raise KeyError("refid in not present in '{0}'".format(
351 mathdef_info['target'])) from e
352 except NoUri: # pragma: no cover
353 # ignore if no URI can be determined, e.g. for LaTeX output
354 pass
355 newnode.append(innernode)
356 para += newnode
357 para += nodes.Text(desc2, desc2)
359 # (Recursively) resolve references in the mathdef content
360 mathdef_entry = mathdef_info['mathdef']
361 idss = ["index-mathdef-%d-%d" % (ilist, n)]
362 # Insert into the mathreflist
363 if add_contents:
364 title = mathdef_info['mathtitle']
365 item = nodes.list_item()
366 p = nodes.paragraph()
367 item += p
368 newnode = nodes.reference('', '', internal=True)
369 innernode = nodes.paragraph(text=title)
370 try:
371 newnode['refuri'] = app.builder.get_relative_uri(
372 fromdocname, mathdocname)
373 newnode['refuri'] += '#' + idss[0]
374 except NoUri: # pragma: no cover
375 # ignore if no URI can be determined, e.g. for LaTeX output
376 pass
377 newnode.append(innernode)
378 p += newnode
379 bullets += item
381 mathdef_entry["ids"] = idss
383 if not hasattr(mathdef_entry, "settings"):
384 mathdef_entry.settings = Values()
385 mathdef_entry.settings.env = env
386 # If an exception happens here, see blog 2017-05-21 from the
387 # documentation.
388 env.resolve_references(mathdef_entry, mathdef_info['docname'],
389 app.builder)
391 # Insert into the mathdeflist
392 content.append(mathdef_entry)
393 content.append(para)
395 node.replace_self(content)
398def purge_mathsext(app, env, docname):
399 """
400 purge_mathsext
401 """
402 if not hasattr(env, 'mathdef_all_mathsext'):
403 return
404 env.mathdef_all_mathsext = [mathdef for mathdef in env.mathdef_all_mathsext
405 if mathdef['docname'] != docname]
408def merge_mathdef(app, env, docnames, other):
409 """
410 merge_mathdef
411 """
412 if not hasattr(other, 'mathdef_all_mathsext'):
413 return
414 if not hasattr(env, 'mathdef_all_mathsext'):
415 env.mathdef_all_mathsext = []
416 env.mathdef_all_mathsext.extend(other.mathdef_all_mathsext)
419def visit_mathdef_node(self, node):
420 """
421 visit_mathdef_node
422 """
423 self.visit_admonition(node)
426def depart_mathdef_node(self, node):
427 """
428 depart_mathdef_node,
429 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
430 """
431 self.depart_admonition(node)
434def visit_mathdeflist_node(self, node):
435 """
436 visit_mathdeflist_node
437 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
438 """
439 self.visit_admonition(node)
442def depart_mathdeflist_node(self, node):
443 """
444 depart_mathdef_node
445 """
446 self.depart_admonition(node)
449def setup(app):
450 """
451 setup for ``mathdef`` (sphinx)
452 """
453 if hasattr(app, "add_mapping"):
454 app.add_mapping('mathdef', mathdef_node)
455 app.add_mapping('mathdeflist', mathdeflist)
457 app.add_config_value('mathdef_include_mathsext', True, 'html')
458 app.add_config_value('mathdef_link_only', True, 'html')
459 app.add_config_value('mathdef_link_number',
460 "{first_letter}{number}", 'html')
462 app.add_node(mathdeflist,
463 html=(visit_mathdeflist_node, depart_mathdeflist_node),
464 epub=(visit_mathdeflist_node, depart_mathdeflist_node),
465 elatex=(visit_mathdeflist_node, depart_mathdeflist_node),
466 latex=(visit_mathdeflist_node, depart_mathdeflist_node),
467 text=(visit_mathdeflist_node, depart_mathdeflist_node),
468 md=(visit_mathdeflist_node, depart_mathdeflist_node),
469 rst=(visit_mathdeflist_node, depart_mathdeflist_node))
470 app.add_node(mathdef_node,
471 html=(visit_mathdef_node, depart_mathdef_node),
472 epub=(visit_mathdef_node, depart_mathdef_node),
473 elatex=(visit_mathdef_node, depart_mathdef_node),
474 latex=(visit_mathdef_node, depart_mathdef_node),
475 text=(visit_mathdef_node, depart_mathdef_node),
476 md=(visit_mathdef_node, depart_mathdef_node),
477 rst=(visit_mathdef_node, depart_mathdef_node))
479 app.add_directive('mathdef', MathDef)
480 app.add_directive('mathdeflist', MathDefList)
481 app.connect('doctree-read', process_mathdefs)
482 app.connect('doctree-resolved', process_mathdef_nodes)
483 app.connect('env-purge-doc', purge_mathsext)
484 app.connect('env-merge-info', merge_mathdef)
485 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}