Coverage for pyquickhelper/sphinxext/sphinx_blocref_extension.py: 87%

290 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 keep track of blocs such as examples, FAQ, ... 

5""" 

6import os 

7import logging 

8from docutils import nodes 

9from docutils.parsers.rst import directives 

10 

11import sphinx 

12from sphinx.locale import _ 

13try: 

14 from sphinx.errors import NoUri 

15except ImportError: 

16 from sphinx.environment import NoUri 

17from docutils.parsers.rst import Directive 

18from docutils.parsers.rst.directives.admonitions import BaseAdmonition 

19from docutils.statemachine import StringList 

20from docutils.frontend import Values 

21from sphinx.util.nodes import set_source_info, process_index_entry 

22from sphinx import addnodes 

23from ..texthelper.texts_language import TITLES 

24from .sphinx_ext_helper import info_blocref 

25 

26 

27class blocref_node(nodes.admonition): 

28 """ 

29 Defines ``blocref`` node. 

30 """ 

31 pass 

32 

33 

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

35 """ 

36 defines ``blocreflist`` node 

37 """ 

38 pass 

39 

40 

41class BlocRef(BaseAdmonition): 

42 """ 

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

44 It takes the following options: 

45 

46 * *title*: a title for the bloc 

47 * *tag*: a tag to have several categories of blocs 

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 .. blocref:: 

54 :title: example of a blocref 

55 :tag: example 

56 :lid: id-you-can-choose 

57 

58 An example of code:: 

59 

60 print("mignon") 

61 

62 Which renders as: 

63 

64 .. blocref:: 

65 :title: example of a blocref 

66 :tag: dummy_example 

67 :lid: id-you-can-choose 

68 

69 An example of code:: 

70 

71 print("mignon") 

72 

73 All blocs can be displayed in another page by using ``blocreflist``:: 

74 

75 .. blocreflist:: 

76 :tag: dummy_example 

77 :sort: title 

78 

79 Only examples tagged as ``dummy_example`` will be inserted here. 

80 The option ``sort`` sorts items by *title*, *number*, *file*. 

81 You also link to it by typing ``:ref:'anchor <id-you-can-choose>'`` which gives 

82 something like :ref:`link_to_blocref <id-you-can-choose>`. The link must receive a name. 

83 

84 .. blocreflist:: 

85 :tag: dummy_example 

86 :sort: title 

87 

88 This directive is used to highlight a bloc about 

89 anything @see cl BlocRef, a question @see cl FaqRef, 

90 a magic command @see cl NbRef, an example @see cl ExRef. 

91 It supports option *index* in most of the extensions 

92 so that the documentation can refer to it. 

93 """ 

94 

95 node_class = blocref_node 

96 name_sphinx = "blocref" 

97 has_content = True 

98 required_arguments = 0 

99 optional_arguments = 0 

100 final_argument_whitespace = False 

101 option_spec = { 

102 'class': directives.class_option, 

103 'title': directives.unchanged, 

104 'tag': directives.unchanged, 

105 'lid': directives.unchanged, 

106 'label': directives.unchanged, 

107 'index': directives.unchanged, 

108 } 

109 

110 def _update_title(self, title, tag, lid): 

111 """ 

112 Updates the title for the bloc itself. 

113 """ 

114 return title 

115 

116 def run(self): 

117 """ 

118 Builds a node @see cl blocref_node. 

119 """ 

120 return self.private_run() 

121 

122 def private_run(self, add_container=False): 

123 """ 

124 Builds a node @see cl blocref_node. 

125 

126 @param add_container add a container node and return as a second result 

127 @return list of nodes or list of nodes, container 

128 """ 

129 name_desc = self.__class__.name_sphinx 

130 lineno = self.lineno 

131 

132 settings = self.state.document.settings 

133 env = settings.env if hasattr(settings, "env") else None 

134 docname = None if env is None else env.docname 

135 if docname is not None: 

136 docname = docname.replace("\\", "/").split("/")[-1] 

137 legend = f"{docname}:{lineno}" 

138 else: 

139 legend = '' 

140 

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

142 self.options['class'] = [f'admonition-{name_desc}'] 

143 

144 # body 

145 (blocref,) = super(BlocRef, self).run() 

146 if isinstance(blocref, nodes.system_message): 

147 return [blocref] 

148 

149 # add a label 

150 lid = self.options.get('lid', self.options.get('label', None)) 

151 if lid: 

152 container = nodes.container() 

153 tnl = [f".. _{lid}:", ""] 

154 content = StringList(tnl) 

155 self.state.nested_parse(content, self.content_offset, container) 

156 else: 

157 container = None 

158 

159 # mid 

160 breftag = self.options.get('tag', '').strip() 

161 if len(breftag) == 0: 

162 raise ValueError("tag is empty") # pragma: no cover 

163 if env is not None: 

164 mid = int(env.new_serialno(f'index{name_desc}-{breftag}')) + 1 

165 else: 

166 mid = -1 

167 

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) 

173 

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) 

182 

183 if add_container: 

184 ret_container = nodes.container() 

185 blocref += ret_container 

186 

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) 

194 

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 RuntimeError(mes.format(docname, lineno, 

207 title, breftag, targetid)) from e 

208 

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 

223 

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 

228 

229 

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) 

238 

239 

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 = f'{bloc_name}_all_{bloc_name}s' 

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

278 

279 

280class BlocRefList(Directive): 

281 """ 

282 A list of all blocref entries, for a specific tag. 

283 

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 

287 

288 Example:: 

289 

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 } 

305 

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] 

331 

332 

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) 

340 

341 

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, 

346 

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 

355 

356 # check this is something to process 

357 env = app.builder.env 

358 attr_name = f'{class_name}_all_{class_name}s' 

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 

364 

365 # content 

366 incconf = f'{class_name}_include_{class_name}s' 

367 if app.config[incconf] and not app.config[incconf]: 

368 for node in doctree.traverse(class_node): 

369 node.parent.remove(node) 

370 

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" 

382 

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

384 brefmes = TITLES[lang][entry_name] 

385 

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 

392 

393 nbbref = 0 

394 content = [] 

395 breftag = node["breftag"] 

396 brefsort = node["brefsort"] 

397 add_contents = node["brefcontents"] 

398 brefdocname = node["docname"] 

399 

400 if add_contents: 

401 bullets = nodes.enumerated_list() 

402 content.append(bullets) 

403 

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

419 

420 # printing 

421 for n, blocref_info_ in enumerate(double_list): 

422 blocref_info = blocref_info_[1] 

423 

424 nbbref += 1 

425 

426 para = nodes.paragraph(classes=[f'{class_name}-source']) 

427 

428 # Create a target? 

429 try: 

430 targ = blocref_info['target'] 

431 except KeyError as e: 

432 logger = logging.getLogger("blocref") 

433 logger.warning( 

434 "Unable to find key 'target' in %r (e=%r)", blocref_info, e) 

435 continue 

436 try: 

437 targ_refid = blocref_info['target']['refid'] 

438 except KeyError as e: 

439 logger = logging.getLogger("blocref") 

440 logger.warning( 

441 "Unable to find key 'refid' in %r (e=%r)", targ, e) 

442 continue 

443 int_ids = [f'index{targ_refid}-{env.new_serialno(targ_refid)}'] 

444 int_targetnode = nodes.target( 

445 blocref_info['breftitle'], '', ids=int_ids) 

446 para += int_targetnode 

447 

448 # rest of the content 

449 if app.config[f'{class_name}_link_only']: 

450 description = _(f'<<{orig_entry}>>') 

451 else: 

452 description = ( 

453 _(brefmes) % 

454 (orig_entry, os.path.split(blocref_info['source'])[-1], 

455 blocref_info['lineno'])) 

456 desc1 = description[:description.find('<<')] 

457 desc2 = description[description.find('>>') + 2:] 

458 para += nodes.Text(desc1, desc1) 

459 

460 # Create a reference 

461 newnode = nodes.reference('', '', internal=True) 

462 newnode['name'] = _(orig_entry) 

463 try: 

464 newnode['refuri'] = app.builder.get_relative_uri( 

465 fromdocname, blocref_info['docname']) 

466 if blocref_info['target'] is None: 

467 raise NoUri # pragma: no cover 

468 try: 

469 newnode['refuri'] += '#' + blocref_info['target']['refid'] 

470 except Exception as e: # pragma: no cover 

471 raise KeyError("refid in not present in '{0}'".format( 

472 blocref_info['target'])) from e 

473 except NoUri: # pragma: no cover 

474 # ignore if no URI can be determined, e.g. for LaTeX output 

475 pass 

476 

477 newnode.append(nodes.Text(newnode['name'])) 

478 

479 # para is duplicate of the content of the bloc 

480 para += newnode 

481 para += nodes.Text(desc2, desc2) 

482 

483 blocref_entry = blocref_info['blocref'] 

484 idss = ["index-%s-%d-%d" % (class_name, ilist, n)] 

485 

486 # Inserts into the blocreflist 

487 # in the list of links at the beginning of the page. 

488 if add_contents: 

489 title = blocref_info['breftitle'] 

490 item = nodes.list_item() 

491 p = nodes.paragraph() 

492 item += p 

493 newnode = nodes.reference('', title, internal=True) 

494 try: 

495 newnode['refuri'] = app.builder.get_relative_uri( 

496 fromdocname, brefdocname) 

497 newnode['refuri'] += '#' + idss[0] 

498 except NoUri: # pragma: no cover 

499 # ignore if no URI can be determined, e.g. for LaTeX output 

500 pass 

501 p += newnode 

502 bullets += item 

503 

504 # Adds the content. 

505 blocref_entry["ids"] = idss 

506 if not hasattr(blocref_entry, "settings"): 

507 blocref_entry.settings = Values() 

508 blocref_entry.settings.env = env 

509 # If an exception happens here, see blog 2017-05-21 from the 

510 # documentation. 

511 env.resolve_references(blocref_entry, blocref_info[ 

512 'docname'], app.builder) 

513 content.append(blocref_entry) 

514 content.append(para) 

515 

516 node.replace_self(content) 

517 

518 

519def purge_blocrefs(app, env, docname): 

520 """ 

521 purge_blocrefs 

522 """ 

523 if not hasattr(env, 'blocref_all_blocrefs'): 

524 return 

525 env.blocref_all_blocrefs = [blocref for blocref in env.blocref_all_blocrefs 

526 if blocref['docname'] != docname] 

527 

528 

529def merge_blocref(app, env, docnames, other): 

530 """ 

531 merge_blocref 

532 """ 

533 if not hasattr(other, 'blocref_all_blocrefs'): 

534 return 

535 if not hasattr(env, 'blocref_all_blocrefs'): 

536 env.blocref_all_blocrefs = [] 

537 env.blocref_all_blocrefs.extend(other.blocref_all_blocrefs) 

538 

539 

540def visit_blocref_node(self, node): 

541 """ 

542 visit_blocref_node 

543 """ 

544 self.visit_admonition(node) 

545 

546 

547def depart_blocref_node(self, node): 

548 """ 

549 depart_blocref_node, 

550 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py 

551 """ 

552 self.depart_admonition(node) 

553 

554 

555def visit_blocreflist_node(self, node): 

556 """ 

557 visit_blocreflist_node 

558 see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py 

559 """ 

560 self.visit_admonition(node) 

561 

562 

563def depart_blocreflist_node(self, node): 

564 """ 

565 depart_blocref_node 

566 """ 

567 self.depart_admonition(node) 

568 

569 

570def setup(app): 

571 """ 

572 setup for ``blocref`` (sphinx) 

573 """ 

574 if hasattr(app, "add_mapping"): 

575 app.add_mapping('blocref', blocref_node) 

576 app.add_mapping('blocreflist', blocreflist) 

577 

578 app.add_config_value('blocref_include_blocrefs', True, 'html') 

579 app.add_config_value('blocref_link_only', False, 'html') 

580 

581 app.add_node(blocreflist, 

582 html=(visit_blocreflist_node, depart_blocreflist_node), 

583 epub=(visit_blocreflist_node, depart_blocreflist_node), 

584 latex=(visit_blocreflist_node, depart_blocreflist_node), 

585 elatex=(visit_blocreflist_node, depart_blocreflist_node), 

586 text=(visit_blocreflist_node, depart_blocreflist_node), 

587 md=(visit_blocreflist_node, depart_blocreflist_node), 

588 rst=(visit_blocreflist_node, depart_blocreflist_node)) 

589 app.add_node(blocref_node, 

590 html=(visit_blocref_node, depart_blocref_node), 

591 epub=(visit_blocref_node, depart_blocref_node), 

592 elatex=(visit_blocref_node, depart_blocref_node), 

593 latex=(visit_blocref_node, depart_blocref_node), 

594 text=(visit_blocref_node, depart_blocref_node), 

595 md=(visit_blocref_node, depart_blocref_node), 

596 rst=(visit_blocref_node, depart_blocref_node)) 

597 

598 app.add_directive('blocref', BlocRef) 

599 app.add_directive('blocreflist', BlocRefList) 

600 app.connect('doctree-read', process_blocrefs) 

601 app.connect('doctree-resolved', process_blocref_nodes) 

602 app.connect('env-purge-doc', purge_blocrefs) 

603 app.connect('env-merge-info', merge_blocref) 

604 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}