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 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 = "{0}:{1}".format(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 = [".. _{0}:".format(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('indexmathe-u-%s' % 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 Exception(
137 "Unable to interpret format '{0}'.".format(number_format)) from e
139 # title
140 title = self.options.get('title', "").strip()
141 if len(title) > 0:
142 title = "{0} {1} : {2}".format(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 Exception(
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, "1", "", None, "None")
263 if env is not None:
264 targetid = 'indexmathelist-%s' % env.new_serialno('indexmathelist')
265 targetnode = nodes.target('', '', ids=[targetid])
266 n = mathdeflist('')
267 n["mathtag"] = tag
268 n["mathcontents"] = contents
269 n['docname'] = env.docname if env else "none"
270 return [targetnode, n]
272 n = mathdeflist('')
273 n["mathtag"] = tag
274 n["mathcontents"] = contents
275 n['docname'] = env.docname if env else "none"
276 return [n]
279def process_mathdef_nodes(app, doctree, fromdocname):
280 """
281 process_mathdef_nodes
282 """
283 if not app.config['mathdef_include_mathsext']:
284 for node in doctree.traverse(mathdef_node):
285 node.parent.remove(node)
287 # Replace all mathdeflist nodes with a list of the collected mathsext.
288 # Augment each mathdef with a backlink to the original location.
289 env = app.builder.env
290 if hasattr(env, "settings") and hasattr(env.settings, "language_code"):
291 lang = env.settings.language_code
292 else:
293 lang = "en"
295 orig_entry = TITLES[lang]["original entry"]
296 mathmes = TITLES[lang]["mathmes"]
298 if not hasattr(env, 'mathdef_all_mathsext'):
299 env.mathdef_all_mathsext = []
301 for ilist, node in enumerate(doctree.traverse(mathdeflist)):
302 if 'ids' in node:
303 node['ids'] = []
304 if not app.config['mathdef_include_mathsext']:
305 node.replace_self([])
306 continue
308 nbmath = 0
309 content = []
310 mathtag = node["mathtag"]
311 add_contents = node["mathcontents"]
312 mathdocname = node["docname"]
314 if add_contents:
315 bullets = nodes.enumerated_list()
316 content.append(bullets)
318 double_list = [(info.get('mathtitle', ''), info)
319 for info in env.mathdef_all_mathsext]
320 double_list.sort(key=lambda x: x[:1])
321 for n, mathdef_info_ in enumerate(double_list):
322 mathdef_info = mathdef_info_[1]
323 if mathdef_info["mathtag"] != mathtag:
324 continue
326 nbmath += 1
327 para = nodes.paragraph(classes=['mathdef-source'])
328 if app.config['mathdef_link_only']:
329 description = _('<<%s>>' % orig_entry)
330 else:
331 description = (
332 _(mathmes) %
333 (orig_entry, os.path.split(mathdef_info['source'])[-1],
334 mathdef_info['lineno'])
335 )
336 desc1 = description[:description.find('<<')]
337 desc2 = description[description.find('>>') + 2:]
338 para += nodes.Text(desc1, desc1)
340 # Create a reference
341 newnode = nodes.reference('', '', internal=True)
342 innernode = nodes.emphasis('', _(orig_entry))
343 try:
344 newnode['refuri'] = app.builder.get_relative_uri(
345 fromdocname, mathdef_info['docname'])
346 try:
347 newnode['refuri'] += '#' + mathdef_info['target']['refid']
348 except Exception as e: # pragma: no cover
349 raise KeyError("refid in not present in '{0}'".format(
350 mathdef_info['target'])) from e
351 except NoUri: # pragma: no cover
352 # ignore if no URI can be determined, e.g. for LaTeX output
353 pass
354 newnode.append(innernode)
355 para += newnode
356 para += nodes.Text(desc2, desc2)
358 # (Recursively) resolve references in the mathdef content
359 mathdef_entry = mathdef_info['mathdef']
360 idss = ["index-mathdef-%d-%d" % (ilist, n)]
361 # Insert into the mathreflist
362 if add_contents:
363 title = mathdef_info['mathtitle']
364 item = nodes.list_item()
365 p = nodes.paragraph()
366 item += p
367 newnode = nodes.reference('', '', internal=True)
368 innernode = nodes.paragraph(text=title)
369 try:
370 newnode['refuri'] = app.builder.get_relative_uri(
371 fromdocname, mathdocname)
372 newnode['refuri'] += '#' + idss[0]
373 except NoUri: # pragma: no cover
374 # ignore if no URI can be determined, e.g. for LaTeX output
375 pass
376 newnode.append(innernode)
377 p += newnode
378 bullets += item
380 mathdef_entry["ids"] = idss
382 if not hasattr(mathdef_entry, "settings"):
383 mathdef_entry.settings = Values()
384 mathdef_entry.settings.env = env
385 # If an exception happens here, see blog 2017-05-21 from the
386 # documentation.
387 env.resolve_references(mathdef_entry, mathdef_info['docname'],
388 app.builder)
390 # Insert into the mathdeflist
391 content.append(mathdef_entry)
392 content.append(para)
394 node.replace_self(content)
397def purge_mathsext(app, env, docname):
398 """
399 purge_mathsext
400 """
401 if not hasattr(env, 'mathdef_all_mathsext'):
402 return
403 env.mathdef_all_mathsext = [mathdef for mathdef in env.mathdef_all_mathsext
404 if mathdef['docname'] != docname]
407def merge_mathdef(app, env, docnames, other):
408 """
409 merge_mathdef
410 """
411 if not hasattr(other, 'mathdef_all_mathsext'):
412 return
413 if not hasattr(env, 'mathdef_all_mathsext'):
414 env.mathdef_all_mathsext = []
415 env.mathdef_all_mathsext.extend(other.mathdef_all_mathsext)
418def visit_mathdef_node(self, node):
419 """
420 visit_mathdef_node
421 """
422 self.visit_admonition(node)
425def depart_mathdef_node(self, node):
426 """
427 depart_mathdef_node,
428 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
429 """
430 self.depart_admonition(node)
433def visit_mathdeflist_node(self, node):
434 """
435 visit_mathdeflist_node
436 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
437 """
438 self.visit_admonition(node)
441def depart_mathdeflist_node(self, node):
442 """
443 depart_mathdef_node
444 """
445 self.depart_admonition(node)
448def setup(app):
449 """
450 setup for ``mathdef`` (sphinx)
451 """
452 if hasattr(app, "add_mapping"):
453 app.add_mapping('mathdef', mathdef_node)
454 app.add_mapping('mathdeflist', mathdeflist)
456 app.add_config_value('mathdef_include_mathsext', True, 'html')
457 app.add_config_value('mathdef_link_only', True, 'html')
458 app.add_config_value('mathdef_link_number',
459 "{first_letter}{number}", 'html')
461 app.add_node(mathdeflist,
462 html=(visit_mathdeflist_node, depart_mathdeflist_node),
463 epub=(visit_mathdeflist_node, depart_mathdeflist_node),
464 elatex=(visit_mathdeflist_node, depart_mathdeflist_node),
465 latex=(visit_mathdeflist_node, depart_mathdeflist_node),
466 text=(visit_mathdeflist_node, depart_mathdeflist_node),
467 md=(visit_mathdeflist_node, depart_mathdeflist_node),
468 rst=(visit_mathdeflist_node, depart_mathdeflist_node))
469 app.add_node(mathdef_node,
470 html=(visit_mathdef_node, depart_mathdef_node),
471 epub=(visit_mathdef_node, depart_mathdef_node),
472 elatex=(visit_mathdef_node, depart_mathdef_node),
473 latex=(visit_mathdef_node, depart_mathdef_node),
474 text=(visit_mathdef_node, depart_mathdef_node),
475 md=(visit_mathdef_node, depart_mathdef_node),
476 rst=(visit_mathdef_node, depart_mathdef_node))
478 app.add_directive('mathdef', MathDef)
479 app.add_directive('mathdeflist', MathDefList)
480 app.connect('doctree-read', process_mathdefs)
481 app.connect('doctree-resolved', process_mathdef_nodes)
482 app.connect('env-purge-doc', purge_mathsext)
483 app.connect('env-merge-info', merge_mathdef)
484 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}