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

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 = f"{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 = [f".. _{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(f'indexmathe-u-{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 RuntimeError( 

137 f"Unable to interpret format '{number_format}'.") from e 

138 

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 

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

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, 

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] 

272 

273 n = mathdeflist('') 

274 n["mathtag"] = tag 

275 n["mathcontents"] = contents 

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

277 return [n] 

278 

279 

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) 

287 

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" 

295 

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

297 mathmes = TITLES[lang]["mathmes"] 

298 

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

300 env.mathdef_all_mathsext = [] 

301 

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 

308 

309 nbmath = 0 

310 content = [] 

311 mathtag = node["mathtag"] 

312 add_contents = node["mathcontents"] 

313 mathdocname = node["docname"] 

314 

315 if add_contents: 

316 bullets = nodes.enumerated_list() 

317 content.append(bullets) 

318 

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 

326 

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) 

340 

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) 

358 

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 

380 

381 mathdef_entry["ids"] = idss 

382 

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) 

390 

391 # Insert into the mathdeflist 

392 content.append(mathdef_entry) 

393 content.append(para) 

394 

395 node.replace_self(content) 

396 

397 

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] 

406 

407 

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) 

417 

418 

419def visit_mathdef_node(self, node): 

420 """ 

421 visit_mathdef_node 

422 """ 

423 self.visit_admonition(node) 

424 

425 

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) 

432 

433 

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) 

440 

441 

442def depart_mathdeflist_node(self, node): 

443 """ 

444 depart_mathdef_node 

445 """ 

446 self.depart_admonition(node) 

447 

448 

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) 

456 

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

461 

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

478 

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}