Hide keyboard shortcuts

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 

12 

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 

25 

26 

27class mathdef_node(nodes.admonition): 

28 """ 

29 Defines ``mathdef`` node. 

30 """ 

31 pass 

32 

33 

34class mathdeflist(nodes.General, nodes.Element): 

35 """ 

36 Defines ``mathdeflist`` node. 

37 """ 

38 pass 

39 

40 

41class MathDef(BaseAdmonition): 

42 """ 

43 A ``mathdef`` entry, displayed in the form of an admonition. 

44 It takes the following options: 

45 

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) 

50 

51 Example:: 

52 

53 .. mathdef:: 

54 :title: title 

55 :tag: definition or theorem or ... 

56 :lid: id (used for further reference) 

57 

58 Description of the math 

59 """ 

60 

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 } 

74 

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 

82 

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 = '' 

91 

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") 

101 

102 if not self.options.get('class'): 

103 self.options['class'] = ['admonition-mathdef'] 

104 

105 # body 

106 (mathdef,) = super(MathDef, self).run() 

107 if isinstance(mathdef, nodes.system_message): 

108 return [mathdef] 

109 

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 

119 

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 

128 

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 

138 

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 

145 

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) 

158 

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 

172 

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 

192 

193 return [a for a in [indexnode, targetnode, mathdef] if a is not None] 

194 

195 

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 }) 

228 

229 

230class MathDefList(Directive): 

231 """ 

232 A list of all mathdef entries, for a specific tag. 

233 

234 * tag: a tag to have several categories of mathdef 

235 * contents: add a bullet list with links to added blocs 

236 

237 Example:: 

238 

239 .. mathdeflist:: 

240 :tag: issue 

241 :contents: 

242 """ 

243 

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 } 

252 

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] 

271 

272 n = mathdeflist('') 

273 n["mathtag"] = tag 

274 n["mathcontents"] = contents 

275 n['docname'] = env.docname if env else "none" 

276 return [n] 

277 

278 

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) 

286 

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" 

294 

295 orig_entry = TITLES[lang]["original entry"] 

296 mathmes = TITLES[lang]["mathmes"] 

297 

298 if not hasattr(env, 'mathdef_all_mathsext'): 

299 env.mathdef_all_mathsext = [] 

300 

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 

307 

308 nbmath = 0 

309 content = [] 

310 mathtag = node["mathtag"] 

311 add_contents = node["mathcontents"] 

312 mathdocname = node["docname"] 

313 

314 if add_contents: 

315 bullets = nodes.enumerated_list() 

316 content.append(bullets) 

317 

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 

325 

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) 

339 

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) 

357 

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 

379 

380 mathdef_entry["ids"] = idss 

381 

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) 

389 

390 # Insert into the mathdeflist 

391 content.append(mathdef_entry) 

392 content.append(para) 

393 

394 node.replace_self(content) 

395 

396 

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] 

405 

406 

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) 

416 

417 

418def visit_mathdef_node(self, node): 

419 """ 

420 visit_mathdef_node 

421 """ 

422 self.visit_admonition(node) 

423 

424 

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) 

431 

432 

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) 

439 

440 

441def depart_mathdeflist_node(self, node): 

442 """ 

443 depart_mathdef_node 

444 """ 

445 self.depart_admonition(node) 

446 

447 

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) 

455 

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') 

460 

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)) 

477 

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}