Coverage for pyquickhelper/sphinxext/sphinx_rst_builder.py: 92%

819 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 sphinx extension to output the documentation in :epkg:`RST`. 

5It is inspired from `restbuilder 

6<https://github.com/sphinx-contrib/legacy>`_. 

7I replicate its license here: 

8 

9:: 

10 

11 Copyright (c) 2012-2013 by Freek Dijkstra <software@macfreek.nl>. 

12 Some rights reserved. 

13 

14 Redistribution and use in source and binary forms, with or without 

15 modification, are permitted provided that the following conditions are 

16 met: 

17 

18 * Redistributions of source code must retain the above copyright 

19 notice, this list of conditions and the following disclaimer. 

20 

21 * Redistributions in binary form must reproduce the above copyright 

22 notice, this list of conditions and the following disclaimer in the 

23 documentation and/or other materials provided with the distribution. 

24 

25 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 

26 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 

27 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 

28 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 

29 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 

30 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 

31 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 

32 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 

33 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 

34 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 

35 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

36""" 

37import os 

38import textwrap 

39from os import path 

40from sphinx.util import logging 

41from docutils.io import StringOutput 

42from sphinx.builders import Builder 

43from sphinx.util.osutil import ensuredir 

44from docutils import nodes, writers 

45from sphinx import addnodes 

46from sphinx.locale import admonitionlabels, _ 

47try: 

48 from sphinx.domains.changeset import versionlabels 

49except ImportError: # pragma: no cover 

50 from sphinx.locale import versionlabels 

51from sphinx.writers.text import TextTranslator, MAXWIDTH, STDINDENT 

52from .sphinx_bigger_extension import visit_bigger_node_rst, depart_bigger_node_rst 

53from .sphinx_gitlog_extension import visit_gitlog_node_rst, depart_gitlog_node_rst 

54from .sphinx_collapse_extension import visit_collapse_node_rst, depart_collapse_node_rst 

55from .sphinx_gdot_extension import visit_gdot_node_rst, depart_gdot_node_rst 

56from .sphinx_quote_extension import visit_quote_node_rst, depart_quote_node_rst 

57from .sphinx_sharenet_extension import visit_sharenet_node_rst, depart_sharenet_node_rst 

58from .sphinx_downloadlink_extension import visit_downloadlink_node_rst, depart_downloadlink_node_rst 

59from ._sphinx_common_builder import CommonSphinxWriterHelpers 

60 

61 

62class RstTranslator(TextTranslator, CommonSphinxWriterHelpers): 

63 """ 

64 Defines a :epkg:`RST` translator. 

65 """ 

66 sectionchars = '*=-~"+`' 

67 

68 def __init__(self, document, builder): 

69 if not hasattr(builder, "config"): 

70 raise TypeError(f"Builder has no config: {type(builder)}") 

71 TextTranslator.__init__(self, document, builder) 

72 

73 newlines = builder.config.text_newlines 

74 if newlines == 'windows': 

75 self.nl = '\r\n' 

76 elif newlines == 'native': 

77 self.nl = os.linesep 

78 else: 

79 self.nl = '\n' 

80 self.sectionchars = builder.config.text_sectionchars 

81 self.states = [[]] 

82 self.stateindent = [0] 

83 self.list_counter = [] 

84 self.sectionlevel = 0 

85 self._table = None 

86 if self.builder.config.rst_indent: 

87 self.indent = self.builder.config.rst_indent 

88 else: 

89 self.indent = STDINDENT 

90 self.wrapper = textwrap.TextWrapper( 

91 width=STDINDENT, break_long_words=False, break_on_hyphens=False) 

92 

93 def log_unknown(self, type, node): 

94 logger = logging.getLogger("RstBuilder") 

95 logger.warning("[rst] %s(%s) unsupported formatting", type, node) 

96 

97 def wrap(self, text, width=STDINDENT): 

98 self.wrapper.width = width 

99 return self.wrapper.wrap(text) 

100 

101 def add_text(self, text, indent=-1): 

102 self.states[-1].append((indent, text)) 

103 

104 def new_state(self, indent=STDINDENT): 

105 self.states.append([]) 

106 self.stateindent.append(indent) 

107 

108 def end_state(self, wrap=True, end=[''], first=None): 

109 content = self.states.pop() 

110 maxindent = sum(self.stateindent) 

111 indent = self.stateindent.pop() 

112 result = [] 

113 toformat = [] 

114 

115 def do_format(): 

116 if not toformat: 

117 return 

118 if wrap: 

119 res = self.wrap(''.join(toformat), width=MAXWIDTH - maxindent) 

120 else: 

121 res = ''.join(toformat).splitlines() 

122 if end: 

123 res += end 

124 result.append((indent, res)) 

125 

126 for itemindent, item in content: 

127 if itemindent == -1: 

128 toformat.append(item) 

129 else: 

130 do_format() 

131 result.append((indent + itemindent, item)) 

132 toformat = [] 

133 

134 do_format() 

135 

136 if first is not None and result: 

137 itemindent, item = result[0] 

138 if item: 

139 result.insert(0, (itemindent - indent, [first + item[0]])) 

140 result[1] = (itemindent, item[1:]) 

141 

142 self.states[-1].extend(result) 

143 

144 def visit_document(self, node): 

145 self.new_state(0) 

146 

147 def depart_document(self, node): 

148 self.end_state() 

149 self.body = self.nl.join(line and (' ' * indent + line) 

150 for indent, lines in self.states[0] 

151 for line in lines) 

152 

153 def visit_highlightlang(self, node): 

154 raise nodes.SkipNode 

155 

156 def visit_section(self, node): 

157 self._title_char = self.sectionchars[self.sectionlevel] 

158 self.sectionlevel += 1 

159 

160 def depart_section(self, node): 

161 self.sectionlevel -= 1 

162 

163 def visit_topic(self, node): 

164 self.new_state(0) 

165 

166 def depart_topic(self, node): 

167 self.end_state() 

168 

169 visit_sidebar = visit_topic 

170 depart_sidebar = depart_topic 

171 

172 def visit_rubric(self, node): 

173 self.new_state(0) 

174 self.add_text('-[ ') 

175 

176 def depart_rubric(self, node): 

177 self.add_text(' ]-') 

178 self.end_state() 

179 

180 def visit_compound(self, node): 

181 # self.log_unknown("compount", node) 

182 pass 

183 

184 def depart_compound(self, node): 

185 pass 

186 

187 def visit_glossary(self, node): 

188 # self.log_unknown("glossary", node) 

189 pass 

190 

191 def depart_glossary(self, node): 

192 pass 

193 

194 def visit_title(self, node): 

195 if isinstance(node.parent, nodes.Admonition): 

196 self.add_text(node.astext() + ': ') 

197 raise nodes.SkipNode 

198 self.new_state(0) 

199 

200 def depart_title(self, node): 

201 if isinstance(node.parent, nodes.section): 

202 char = self._title_char 

203 else: 

204 char = '^' 

205 text = ''.join(x[1] for x in self.states.pop() if x[0] == -1) 

206 self.stateindent.pop() 

207 self.states[-1].append((0, ['', text, f'{char * len(text)}', ''])) 

208 

209 def visit_subtitle(self, node): 

210 # self.log_unknown("subtitle", node) 

211 pass 

212 

213 def depart_subtitle(self, node): 

214 pass 

215 

216 def visit_attribution(self, node): 

217 self.add_text('-- ') 

218 

219 def depart_attribution(self, node): 

220 pass 

221 

222 def visit_desc(self, node): 

223 self.new_state(0) 

224 

225 def depart_desc(self, node): 

226 self.end_state() 

227 

228 def visit_desc_signature(self, node): 

229 if node.parent['objtype'] in ('class', 'exception', 'method', 'function'): 

230 self.add_text('**') 

231 else: 

232 self.add_text('``') 

233 

234 def depart_desc_signature(self, node): 

235 if node.parent['objtype'] in ('class', 'exception', 'method', 'function'): 

236 self.add_text('**') 

237 else: 

238 self.add_text('``') 

239 

240 def visit_desc_name(self, node): 

241 # self.log_unknown("desc_name", node) 

242 pass 

243 

244 def depart_desc_name(self, node): 

245 pass 

246 

247 def visit_desc_addname(self, node): 

248 # self.log_unknown("desc_addname", node) 

249 pass 

250 

251 def depart_desc_addname(self, node): 

252 pass 

253 

254 def visit_desc_type(self, node): 

255 # self.log_unknown("desc_type", node) 

256 pass 

257 

258 def depart_desc_type(self, node): 

259 pass 

260 

261 def visit_desc_returns(self, node): 

262 self.add_text(' -> ') 

263 

264 def depart_desc_returns(self, node): 

265 pass 

266 

267 def visit_desc_parameterlist(self, node): 

268 self.add_text('(') 

269 self.first_param = 1 

270 

271 def depart_desc_parameterlist(self, node): 

272 self.add_text(')') 

273 

274 def visit_desc_parameter(self, node): 

275 if not self.first_param: 

276 self.add_text(', ') 

277 else: 

278 self.first_param = 0 

279 self.add_text(node.astext()) 

280 raise nodes.SkipNode 

281 

282 def visit_desc_optional(self, node): 

283 self.add_text('[') 

284 

285 def depart_desc_optional(self, node): 

286 self.add_text(']') 

287 

288 def visit_desc_annotation(self, node): 

289 content = node.astext() 

290 if len(content) > MAXWIDTH: # pragma: no cover 

291 h = int(MAXWIDTH / 3) 

292 content = content[:h] + " ... " + content[-h:] 

293 self.add_text(content) 

294 raise nodes.SkipNode 

295 

296 def depart_desc_annotation(self, node): 

297 pass 

298 

299 def visit_refcount(self, node): 

300 pass 

301 

302 def depart_refcount(self, node): 

303 pass 

304 

305 def visit_desc_content(self, node): 

306 self.new_state(self.indent) 

307 

308 def depart_desc_content(self, node): 

309 self.end_state() 

310 

311 def visit_figure(self, node): 

312 self.new_state(self.indent) 

313 

314 def depart_figure(self, node): 

315 self.end_state() 

316 

317 def visit_caption(self, node): 

318 # self.log_unknown("caption", node) 

319 pass 

320 

321 def depart_caption(self, node): 

322 pass 

323 

324 def visit_productionlist(self, node): 

325 self.new_state(self.indent) 

326 names = [] 

327 for production in node: 

328 names.append(production['tokenname']) 

329 maxlen = max(len(name) for name in names) 

330 for production in node: 

331 if production['tokenname']: 

332 self.add_text(production['tokenname'].ljust(maxlen) + ' ::=') 

333 lastname = production['tokenname'] 

334 else: 

335 self.add_text(f"{' ' * len(lastname)} ") 

336 self.add_text(production.astext() + self.nl) 

337 self.end_state(wrap=False) 

338 raise nodes.SkipNode 

339 

340 def visit_seealso(self, node): 

341 self.new_state(self.indent) 

342 

343 def depart_seealso(self, node): 

344 self.end_state(first='') 

345 

346 def visit_footnote(self, node): 

347 self._footnote = node.children[0].astext().strip() 

348 self.new_state(len(self._footnote) + self.indent) 

349 

350 def depart_footnote(self, node): 

351 self.end_state(first=f'[{self._footnote}] ') 

352 

353 def visit_citation(self, node): 

354 if len(node) and isinstance(node[0], nodes.label): 

355 self._citlabel = node[0].astext() 

356 else: 

357 self._citlabel = '' 

358 self.new_state(len(self._citlabel) + self.indent) 

359 

360 def depart_citation(self, node): 

361 self.end_state(first=f'[{self._citlabel}] ') 

362 

363 def visit_label(self, node): 

364 raise nodes.SkipNode 

365 

366 def visit_option_list(self, node): 

367 # self.log_unknown("option_list", node) 

368 pass 

369 

370 def depart_option_list(self, node): 

371 pass 

372 

373 def visit_option_list_item(self, node): 

374 self.new_state(0) 

375 

376 def depart_option_list_item(self, node): 

377 self.end_state() 

378 

379 def visit_option_group(self, node): 

380 self._firstoption = True 

381 

382 def depart_option_group(self, node): 

383 self.add_text(' ') 

384 

385 def visit_option(self, node): 

386 if self._firstoption: 

387 self._firstoption = False 

388 else: 

389 self.add_text(', ') 

390 

391 def depart_option(self, node): 

392 pass 

393 

394 def visit_option_string(self, node): 

395 # self.log_unknown("option_string", node) 

396 pass 

397 

398 def depart_option_string(self, node): 

399 pass 

400 

401 def visit_option_argument(self, node): 

402 self.add_text(node['delimiter']) 

403 

404 def depart_option_argument(self, node): 

405 pass 

406 

407 def visit_description(self, node): 

408 # self.log_unknown("description", node) 

409 pass 

410 

411 def depart_description(self, node): 

412 pass 

413 

414 def visit_tabular_col_spec(self, node): 

415 raise nodes.SkipNode 

416 

417 def visit_colspec(self, node): 

418 self._table[0].append(node['colwidth']) 

419 raise nodes.SkipNode 

420 

421 def visit_tgroup(self, node): 

422 # self.log_unknown("tgroup", node) 

423 pass 

424 

425 def depart_tgroup(self, node): 

426 pass 

427 

428 def visit_thead(self, node): 

429 # self.log_unknown("thead", node) 

430 pass 

431 

432 def depart_thead(self, node): 

433 pass 

434 

435 def visit_tbody(self, node): 

436 self._table.append('sep') 

437 

438 def depart_tbody(self, node): 

439 pass 

440 

441 def visit_row(self, node): 

442 self._table.append([]) 

443 

444 def depart_row(self, node): 

445 pass 

446 

447 def visit_entry(self, node): 

448 if hasattr(node, 'morerows') or hasattr(node, 'morecols'): 

449 raise NotImplementedError('Column or row spanning cells are ' 

450 'not implemented.') 

451 self.new_state(0) 

452 

453 def depart_entry(self, node): 

454 text = self.nl.join(self.nl.join(x[1]) for x in self.states.pop()) 

455 self.stateindent.pop() 

456 self._table[-1].append(text) 

457 

458 def visit_table(self, node): 

459 if self._table: 

460 raise NotImplementedError('Nested tables are not supported.') 

461 self.new_state(0) 

462 self._table = [[]] 

463 

464 def depart_table(self, node): 

465 lines = self._table[1:] 

466 fmted_rows = [] 

467 colwidths = self._table[0] 

468 realwidths = list(map(lambda x: x if isinstance(x, int) else 1, 

469 colwidths[:])) 

470 separator = 0 

471 # don't allow paragraphs in table cells for now 

472 for line in lines: 

473 if line == 'sep': 

474 separator = len(fmted_rows) 

475 else: 

476 cells = [] 

477 for i, cell in enumerate(line): 

478 try: 

479 par = self.wrap(cell, width=int(colwidths[i])) 

480 except (IndexError, ValueError): 

481 par = self.wrap(cell) 

482 if par: 

483 maxwidth = max(map(len, par)) 

484 else: 

485 maxwidth = 0 

486 if i >= len(realwidths): 

487 realwidths.append(maxwidth) 

488 elif isinstance(realwidths[i], str): 

489 realwidths[i] = maxwidth 

490 else: 

491 realwidths[i] = max(realwidths[i], maxwidth) 

492 cells.append(par) 

493 fmted_rows.append(cells) 

494 self._table = None 

495 

496 def writesep(char='-'): 

497 out = ['+'] 

498 for width in realwidths: 

499 out.append(char * (width + 2)) 

500 out.append('+') 

501 self.add_text(''.join(out) + self.nl) 

502 

503 def writerow(row): 

504 lines = zip(*row) 

505 for line in lines: 

506 out = ['|'] 

507 for i, cell in enumerate(line): 

508 if cell: 

509 out.append(' ' + cell.ljust(realwidths[i] + 1)) 

510 else: 

511 out.append(' ' * (realwidths[i] + 2)) 

512 out.append('|') 

513 self.add_text(''.join(out) + self.nl) 

514 

515 for i, row in enumerate(fmted_rows): 

516 if separator and i == separator: 

517 writesep('=') 

518 else: 

519 writesep('-') 

520 writerow(row) 

521 writesep('-') 

522 self._table = None 

523 self.end_state(wrap=False) 

524 

525 def visit_acks(self, node): 

526 self.new_state(0) 

527 self.add_text(', '.join(n.astext() 

528 for n in node.children[0].children) + '.') 

529 self.end_state() 

530 raise nodes.SkipNode 

531 

532 def visit_simpleimage(self, node): 

533 self.visit_image(node) 

534 

535 def depart_simpleimage(self, node): 

536 self.depart_image(node) 

537 

538 def visit_image(self, node): 

539 self.new_state(0) 

540 atts = self.base_visit_image(node, self.builder.rst_image_dest) 

541 self.add_text(f".. image:: {atts['src']}") 

542 for att_name in 'width', 'height', 'alt', 'download': 

543 if att_name in node.attributes and node.get(att_name) != 'auto': 

544 self.new_state(4) 

545 self.add_text(f":{att_name}: {node[att_name]}") 

546 self.end_state(wrap=False, end=None) 

547 

548 def depart_image(self, node): 

549 self.end_state(wrap=False, end=None) 

550 

551 def visit_transition(self, node): 

552 indent = sum(self.stateindent) 

553 self.new_state(0) 

554 self.add_text('=' * (MAXWIDTH - indent)) 

555 self.end_state() 

556 raise nodes.SkipNode 

557 

558 def visit_bullet_list(self, node): 

559 self.list_counter.append(-1) 

560 

561 def depart_bullet_list(self, node): 

562 self.list_counter.pop() 

563 

564 def visit_enumerated_list(self, node): 

565 self.list_counter.append(0) 

566 

567 def depart_enumerated_list(self, node): 

568 self.list_counter.pop() 

569 

570 def visit_definition_list(self, node): 

571 self.list_counter.append(-2) 

572 

573 def depart_definition_list(self, node): 

574 self.list_counter.pop() 

575 

576 def visit_list_item(self, node): 

577 if self.list_counter[-1] == -1: 

578 # bullet list 

579 self.new_state(self.indent) 

580 elif self.list_counter[-1] == -2: 

581 # definition list 

582 pass 

583 else: 

584 # enumerated list 

585 self.list_counter[-1] += 1 

586 self.new_state(len(str(self.list_counter[-1])) + self.indent) 

587 

588 def depart_list_item(self, node): 

589 if self.list_counter[-1] == -1: 

590 self.end_state(first='* ', end=None) 

591 elif self.list_counter[-1] == -2: 

592 pass 

593 else: 

594 self.end_state(first=f'{self.list_counter[-1]}. ', end=None) 

595 

596 def visit_definition_list_item(self, node): 

597 self._li_has_classifier = len(node) >= 2 and \ 

598 isinstance(node[1], nodes.classifier) 

599 

600 def depart_definition_list_item(self, node): 

601 pass 

602 

603 def visit_term(self, node): 

604 self.new_state(0) 

605 

606 def depart_term(self, node): 

607 if not self._li_has_classifier: 

608 self.end_state(end=None) 

609 

610 def visit_termsep(self, node): 

611 self.add_text(', ') 

612 raise nodes.SkipNode 

613 

614 def visit_classifier(self, node): 

615 self.add_text(' : ') 

616 

617 def depart_classifier(self, node): 

618 self.end_state(end=None) 

619 

620 def visit_definition(self, node): 

621 self.new_state(self.indent) 

622 

623 def depart_definition(self, node): 

624 self.end_state() 

625 

626 def visit_field_list(self, node): 

627 # self.log_unknown("field_list", node) 

628 pass 

629 

630 def depart_field_list(self, node): 

631 pass 

632 

633 def visit_field(self, node): 

634 self.new_state(0) 

635 

636 def depart_field(self, node): 

637 self.end_state(end=None) 

638 

639 def visit_field_name(self, node): 

640 self.add_text(':') 

641 

642 def depart_field_name(self, node): 

643 self.add_text(':') 

644 content = node.astext() 

645 self.add_text((16 - len(content)) * ' ') 

646 

647 def visit_field_body(self, node): 

648 self.new_state(self.indent) 

649 

650 def depart_field_body(self, node): 

651 self.end_state() 

652 

653 def visit_centered(self, node): 

654 pass 

655 

656 def depart_centered(self, node): 

657 pass 

658 

659 def visit_hlist(self, node): 

660 # self.log_unknown("hlist", node) 

661 pass 

662 

663 def depart_hlist(self, node): 

664 pass 

665 

666 def visit_hlistcol(self, node): 

667 # self.log_unknown("hlistcol", node) 

668 pass 

669 

670 def depart_hlistcol(self, node): 

671 pass 

672 

673 def visit_admonition(self, node): 

674 self.new_state(0) 

675 

676 def depart_admonition(self, node): 

677 self.end_state() 

678 

679 def _visit_admonition(self, node): 

680 self.new_state(self.indent) 

681 

682 def _make_depart_admonition(name): 

683 def depart_admonition(self, node): 

684 self.end_state(first=admonitionlabels[name] + ': ') 

685 return depart_admonition 

686 

687 visit_attention = _visit_admonition 

688 depart_attention = _make_depart_admonition('attention') 

689 visit_caution = _visit_admonition 

690 depart_caution = _make_depart_admonition('caution') 

691 visit_danger = _visit_admonition 

692 depart_danger = _make_depart_admonition('danger') 

693 visit_error = _visit_admonition 

694 depart_error = _make_depart_admonition('error') 

695 visit_hint = _visit_admonition 

696 depart_hint = _make_depart_admonition('hint') 

697 visit_important = _visit_admonition 

698 depart_important = _make_depart_admonition('important') 

699 visit_note = _visit_admonition 

700 depart_note = _make_depart_admonition('note') 

701 visit_tip = _visit_admonition 

702 depart_tip = _make_depart_admonition('tip') 

703 visit_warning = _visit_admonition 

704 depart_warning = _make_depart_admonition('warning') 

705 

706 def visit_versionmodified(self, node): 

707 self.new_state(0) 

708 if node.children: 

709 self.add_text(versionlabels[node['type']] % node['version'] + ': ') 

710 else: 

711 self.add_text(versionlabels[node['type']] % node['version'] + '.') 

712 

713 def depart_versionmodified(self, node): 

714 self.end_state() 

715 

716 def visit_literal_block(self, node): 

717 if 'language' in node.attributes: 

718 self.add_text(f".. code-block:: {node['language']}") 

719 if 'linenos' in node.attributes: 

720 self.new_state(4) 

721 self.add_text(":linenos:") 

722 self.end_state(wrap=False) 

723 else: 

724 self.add_text("::") 

725 self.new_state(self.indent) 

726 

727 def depart_literal_block(self, node): 

728 self.end_state(wrap=False) 

729 

730 def visit_doctest_block(self, node): 

731 self.new_state(0) 

732 

733 def depart_doctest_block(self, node): 

734 self.end_state(wrap=False) 

735 

736 def visit_line_block(self, node): 

737 self.new_state(0) 

738 

739 def depart_line_block(self, node): 

740 self.end_state(wrap=False) 

741 

742 def visit_line(self, node): 

743 # self.log_unknown("line", node) 

744 pass 

745 

746 def depart_line(self, node): 

747 pass 

748 

749 def visit_block_quote(self, node): 

750 self.add_text('..') 

751 self.new_state(self.indent) 

752 

753 def depart_block_quote(self, node): 

754 self.end_state() 

755 

756 def visit_compact_paragraph(self, node): 

757 pass 

758 

759 def depart_compact_paragraph(self, node): 

760 pass 

761 

762 def visit_paragraph(self, node): 

763 if not isinstance(node.parent, nodes.Admonition) or \ 

764 isinstance(node.parent, addnodes.seealso): 

765 self.new_state(0) 

766 

767 def depart_paragraph(self, node): 

768 if not isinstance(node.parent, nodes.Admonition) or \ 

769 isinstance(node.parent, addnodes.seealso): 

770 self.end_state() 

771 

772 def visit_target(self, node): 

773 if 'refid' in node: 

774 self.new_state(0) 

775 self.add_text('.. _' + node['refid'] + ':' + self.nl) 

776 

777 def depart_target(self, node): 

778 if 'refid' in node: 

779 self.end_state(wrap=False) 

780 

781 def visit_index(self, node): 

782 raise nodes.SkipNode 

783 

784 def visit_substitution_definition(self, node): 

785 raise nodes.SkipNode 

786 

787 def visit_pending_xref(self, node): 

788 if node.get('refexplicit'): 

789 text = f":py:{node['reftype']}:`{node.astext()} <{node['reftarget']}>`" 

790 else: 

791 text = f":py:{node['reftype']}:`{node['reftarget']}`" 

792 self.add_text(text) 

793 raise nodes.SkipNode 

794 

795 def depart_pending_xref(self, node): 

796 raise NotImplementedError("Error") 

797 

798 def visit_reference(self, node): 

799 """ 

800 Runs upon entering a reference. 

801 Because this class inherits from the TextTranslator class, 

802 regularly defined links, such as:: 

803 

804 `Some Text`_ 

805 

806 .. _Some Text: http://www.some_url.com 

807 

808 were being written as plaintext. This included internal 

809 references defined in the standard rst way, such as:: 

810 

811 `Some Reference` 

812 

813 .. _Some Reference: 

814 

815 Some Title 

816 ---------- 

817 

818 To resolve this, if ``refuri`` is not included in the node (an 

819 internal, non-Sphinx-defined internal uri, the reference is 

820 left unchanged. 

821 

822 If ``internal`` is not in the node (as for an external, 

823 non-Sphinx URI, the reference is rewritten as an inline link, 

824 e.g.:: 

825 

826 Some Text <http://www.some_url.com>`_ 

827 

828 If ``reftitle`` is in the node (as in a Sphinx-generated 

829 reference), the node is converted to an inline link. 

830 

831 Finally, all other links are also converted to an inline link 

832 format. 

833 """ 

834 def clean_refuri(uri): 

835 ext = os.path.splitext(uri)[-1] 

836 link = uri if ext != '.rst' else uri[:-4] 

837 return link 

838 

839 if 'refuri' not in node: 

840 if 'name' in node.attributes: 

841 self.add_text(f"`{node['name']}`_") 

842 elif 'refid' in node and node['refid']: 

843 self.add_text(f":ref:`{node['refid']}`") 

844 else: 

845 self.log_unknown(type(node), node) 

846 elif 'internal' not in node and 'name' in node.attributes: 

847 self.add_text( 

848 f"`{node['name']} <{clean_refuri(node['refuri'])}>`_") 

849 elif 'internal' not in node and 'names' in node.attributes: 

850 anchor = node['names'][0] if len( 

851 node['names']) > 0 else node['refuri'] 

852 self.add_text(f"`{anchor} <{clean_refuri(node['refuri'])}>`_") 

853 elif 'reftitle' in node: 

854 # Include node as text, rather than with markup. 

855 # reST seems unable to parse a construct like ` ``literal`` <url>`_ 

856 # Hence it reverts to the more simple `literal <url>`_ 

857 name = node['name'] if 'name' in node else node.astext() 

858 self.add_text(f"`{name} <{clean_refuri(node['refuri'])}>`_") 

859 # self.end_state(wrap=False) 

860 else: 

861 name = node['name'] if 'name' in node else node.astext() 

862 self.add_text(f"`{name} <{node['refuri']}>`_") 

863 if 'internal' in node: 

864 raise nodes.SkipNode 

865 

866 def depart_reference(self, node): 

867 if 'refuri' not in node: 

868 pass # Don't add these anchors 

869 elif 'internal' not in node: 

870 # Don't add external links (they are automatically added by the reST spec) 

871 pass 

872 elif 'reftitle' in node: 

873 pass 

874 

875 def visit_download_reference(self, node): 

876 self.log_unknown("download_reference", node) 

877 

878 def depart_download_reference(self, node): 

879 pass 

880 

881 def visit_emphasis(self, node): 

882 self.add_text('*') 

883 

884 def depart_emphasis(self, node): 

885 self.add_text('*') 

886 

887 def visit_literal_emphasis(self, node): 

888 self.add_text('*') 

889 

890 def depart_literal_emphasis(self, node): 

891 self.add_text('*') 

892 

893 def visit_strong(self, node): 

894 self.add_text('**') 

895 

896 def depart_strong(self, node): 

897 self.add_text('**') 

898 

899 def visit_abbreviation(self, node): 

900 self.add_text('') 

901 

902 def depart_abbreviation(self, node): 

903 if node.hasattr('explanation'): 

904 self.add_text(f" ({node['explanation']})") 

905 

906 def visit_title_reference(self, node): 

907 # self.log_unknown("title_reference", node) 

908 self.add_text('*') 

909 

910 def depart_title_reference(self, node): 

911 self.add_text('*') 

912 

913 def visit_literal(self, node): 

914 self.add_text('``') 

915 

916 def depart_literal(self, node): 

917 self.add_text('``') 

918 

919 def visit_subscript(self, node): 

920 self.add_text('_') 

921 

922 def depart_subscript(self, node): 

923 pass 

924 

925 def visit_superscript(self, node): 

926 self.add_text('^') 

927 

928 def depart_superscript(self, node): 

929 pass 

930 

931 def visit_footnote_reference(self, node): 

932 self.add_text(f'[{node.astext()}]') 

933 raise nodes.SkipNode 

934 

935 def visit_citation_reference(self, node): 

936 self.add_text(f'[{node.astext()}]') 

937 raise nodes.SkipNode 

938 

939 def visit_Text(self, node): 

940 self.add_text(node.astext()) 

941 

942 def depart_Text(self, node): 

943 pass 

944 

945 def visit_generated(self, node): 

946 # self.log_unknown("generated", node) 

947 pass 

948 

949 def depart_generated(self, node): 

950 pass 

951 

952 def visit_inline(self, node): 

953 # self.log_unknown("inline", node) 

954 pass 

955 

956 def depart_inline(self, node): 

957 pass 

958 

959 def visit_problematic(self, node): 

960 self.add_text('>>') 

961 

962 def depart_problematic(self, node): 

963 self.add_text('<<') 

964 

965 def visit_system_message(self, node): 

966 self.new_state(0) 

967 self.add_text(f'<SYSTEM MESSAGE: {node.astext()}>') 

968 self.end_state() 

969 raise nodes.SkipNode 

970 

971 def visit_comment(self, node): 

972 raise nodes.SkipNode 

973 

974 def visit_meta(self, node): 

975 # only valid for HTML 

976 raise nodes.SkipNode 

977 

978 def visit_raw(self, node): 

979 if 'text' in node.get('format', '').split(): 

980 self.add_text(node.astext()) 

981 raise nodes.SkipNode 

982 

983 def visit_bigger_node(self, node): 

984 visit_bigger_node_rst(self, node) 

985 

986 def depart_bigger_node(self, node): 

987 depart_bigger_node_rst(self, node) 

988 

989 def visit_gitlog_node(self, node): 

990 visit_gitlog_node_rst(self, node) 

991 

992 def depart_gitlog_node(self, node): 

993 depart_gitlog_node_rst(self, node) 

994 

995 def visit_collapse_node(self, node): 

996 visit_collapse_node_rst(self, node) 

997 

998 def depart_collapse_node(self, node): 

999 depart_collapse_node_rst(self, node) 

1000 

1001 def visit_gdot_node(self, node): 

1002 visit_gdot_node_rst(self, node) 

1003 

1004 def depart_gdot_node(self, node): 

1005 depart_gdot_node_rst(self, node) 

1006 

1007 def visit_quote_node(self, node): 

1008 visit_quote_node_rst(self, node) 

1009 

1010 def depart_quote_node(self, node): 

1011 depart_quote_node_rst(self, node) 

1012 

1013 def visit_issue(self, node): 

1014 self.add_text(':issue:`') 

1015 self.add_text(node['text']) 

1016 

1017 def depart_issue(self, node): 

1018 self.add_text('`') 

1019 

1020 def eval_expr(self, expr): 

1021 md = False 

1022 rst = True 

1023 html = False 

1024 latex = False 

1025 if not (rst or html or latex or md): 

1026 raise ValueError("One of them should be True") # pragma: no cover 

1027 try: 

1028 ev = eval(expr) 

1029 except Exception as e: # pragma: no cover 

1030 raise ValueError( 

1031 f"Unable to interpret expression '{expr}'") 

1032 return ev 

1033 

1034 def visit_only(self, node): 

1035 ev = self.eval_expr(node.attributes['expr']) 

1036 if ev: 

1037 pass 

1038 else: 

1039 raise nodes.SkipNode 

1040 

1041 def depart_only(self, node): 

1042 ev = self.eval_expr(node.attributes['expr']) 

1043 if ev: 

1044 pass 

1045 else: 

1046 # The program should not necessarily be here. 

1047 pass 

1048 

1049 def visit_CodeNode(self, node): 

1050 self.add_text('.. CodeNode.' + self.nl) 

1051 

1052 def depart_CodeNode(self, node): 

1053 pass 

1054 

1055 def visit_sharenet_node(self, node): 

1056 visit_sharenet_node_rst(self, node) 

1057 

1058 def depart_sharenet_node(self, node): 

1059 depart_sharenet_node_rst(self, node) 

1060 

1061 def visit_downloadlink_node(self, node): 

1062 visit_downloadlink_node_rst(self, node) 

1063 

1064 def depart_downloadlink_node(self, node): 

1065 depart_downloadlink_node_rst(self, node) 

1066 

1067 def visit_runpythonthis_node(self, node): 

1068 # for unit test. 

1069 pass 

1070 

1071 def depart_runpythonthis_node(self, node): 

1072 # for unit test. 

1073 pass 

1074 

1075 def visit_inheritance_diagram(self, node): 

1076 self.new_state(0) 

1077 self.add_text(f".. inheritance_diagram:: {node['content']}") 

1078 

1079 def depart_inheritance_diagram(self, node): 

1080 self.end_state(wrap=False, end=['\n']) 

1081 

1082 def visit_todo_node(self, node): 

1083 self.visit_admonition(node) 

1084 

1085 def depart_todo_node(self, node): 

1086 self.depart_admonition(node) 

1087 

1088 def visit_imgsgnode(self, node): 

1089 self.add_text('.. imgsgnode(visit).') 

1090 

1091 def depart_imgsgnode(self, node): 

1092 self.add_text('.. imgsgnode(depart).') 

1093 

1094 def unknown_visit(self, node): 

1095 classname = node.__class__.__name__ 

1096 if classname in {'JupyterKernelNode', 'JupyterCellNode', 

1097 'JupyterWidgetViewNode', 'JupyterWidgetStateNode', 

1098 'ThebeSourceNode', 'ThebeOutputNode', 

1099 'ThebeButtonNode', 

1100 }: 

1101 # due to jupyter_sphinx 

1102 return 

1103 logger = logging.getLogger("RstBuilder") 

1104 logger.warning("[rst] unknown visit node: '%r - %r", 

1105 node.__class__.__name__, node) 

1106 

1107 def unknown_departure(self, node): 

1108 classname = node.__class__.__name__ 

1109 if classname in {'JupyterKernelNode', 'JupyterCellNode', 

1110 'JupyterWidgetViewNode', 'JupyterWidgetStateNode', 

1111 'ThebeSourceNode', 'ThebeOutputNode', 

1112 'ThebeButtonNode', 

1113 }: 

1114 # due to jupyter_sphinx 

1115 return 

1116 logger = logging.getLogger("RstBuilder") 

1117 logger.warning("[rst] unknown depart node: %r - %r", 

1118 node.__class__.__name__, node) 

1119 

1120 

1121class _BodyPlaceholder: 

1122 def __init__(self, builder): 

1123 self.lines = [] 

1124 self.logger = logging.getLogger("RstBuilder") 

1125 

1126 def append(self, element): 

1127 if isinstance(element, str): 

1128 el = element.replace("\n", " ") 

1129 if len(el) > 50: 

1130 el = el[:50] + "..." 

1131 self.logger.warning( 

1132 "[rst] body.append was called with string %r", el) 

1133 else: 

1134 self.logger.warning( 

1135 "[rst] body.append was called with type %", type(element)) 

1136 self.lines.append(element) 

1137 

1138 

1139class RstBuilder(Builder): 

1140 """ 

1141 Defines a :epkg:`RST` builder. 

1142 """ 

1143 name = 'rst' 

1144 format = 'rst' 

1145 file_suffix = '.rst' 

1146 link_suffix = None # defaults to file_suffix 

1147 default_translator_class = RstTranslator 

1148 

1149 def __init__(self, *args, **kwargs): 

1150 """ 

1151 Constructor, add a logger. 

1152 """ 

1153 Builder.__init__(self, *args, **kwargs) 

1154 self.logger = logging.getLogger("RstBuilder") 

1155 # Should not be populated, it may be due to a function 

1156 # implemented for HTML but used for RST. 

1157 self.body = _BodyPlaceholder(self) 

1158 

1159 def init(self): 

1160 """ 

1161 Load necessary templates and perform initialization. 

1162 """ 

1163 if self.config.rst_file_suffix is not None: 

1164 self.file_suffix = self.config.rst_file_suffix 

1165 if self.config.rst_link_suffix is not None: 

1166 self.link_suffix = self.config.rst_link_suffix 

1167 if self.link_suffix is None: 

1168 self.link_suffix = self.file_suffix 

1169 

1170 # Function to convert the docname to a reST file name. 

1171 def file_transform(docname): 

1172 return docname + self.file_suffix 

1173 

1174 # Function to convert the docname to a relative URI. 

1175 def link_transform(docname): 

1176 return docname + self.link_suffix 

1177 

1178 if self.config.rst_file_transform is not None: 

1179 self.file_transform = self.config.rst_file_transform 

1180 else: 

1181 self.file_transform = file_transform 

1182 if self.config.rst_link_transform is not None: 

1183 self.link_transform = self.config.rst_link_transform 

1184 else: 

1185 self.link_transform = link_transform 

1186 self.rst_image_dest = self.config.rst_image_dest 

1187 

1188 def get_outdated_docs(self): 

1189 """ 

1190 Return an iterable of input files that are outdated. 

1191 This method is taken from ``TextBuilder.get_outdated_docs()`` 

1192 with minor changes to support ``(confval, rst_file_transform))``. 

1193 """ 

1194 for docname in self.env.found_docs: 

1195 if docname not in self.env.all_docs: 

1196 yield docname 

1197 continue 

1198 sourcename = path.join(self.env.srcdir, docname + 

1199 self.file_suffix) 

1200 targetname = path.join(self.outdir, self.file_transform(docname)) 

1201 

1202 try: 

1203 targetmtime = path.getmtime(targetname) 

1204 except Exception: 

1205 targetmtime = 0 

1206 try: 

1207 srcmtime = path.getmtime(sourcename) 

1208 if srcmtime > targetmtime: 

1209 yield docname 

1210 except EnvironmentError: 

1211 # source doesn't exist anymore 

1212 pass 

1213 

1214 def get_target_uri(self, docname, typ=None): 

1215 return self.link_transform(docname) 

1216 

1217 def prepare_writing(self, docnames): 

1218 self.writer = RstWriter(self) 

1219 

1220 def get_outfilename(self, pagename): 

1221 """ 

1222 Overwrites *get_target_uri* to control file names. 

1223 """ 

1224 return f"{self.outdir}/{pagename}.rst".replace("\\", "/") 

1225 

1226 def write_doc(self, docname, doctree): 

1227 destination = StringOutput(encoding='utf-8') 

1228 self.current_docname = docname 

1229 self.writer.write(doctree, destination) 

1230 ctx = None 

1231 self.handle_page(docname, ctx, event_arg=doctree) 

1232 

1233 def handle_page(self, pagename, addctx, templatename=None, 

1234 outfilename=None, event_arg=None): 

1235 if templatename is not None: 

1236 raise NotImplementedError("templatename must be None.") 

1237 outfilename = self.get_outfilename(pagename) 

1238 ensuredir(path.dirname(outfilename)) 

1239 with open(outfilename, 'w', encoding='utf-8') as f: 

1240 f.write(self.writer.output) 

1241 

1242 def finish(self): 

1243 pass 

1244 

1245 

1246class RstWriter(writers.Writer): 

1247 """ 

1248 Defines a :epkg:`RST` writer. 

1249 """ 

1250 supported = ('text',) 

1251 settings_spec = ('No options here.', '', ()) 

1252 settings_defaults = {} 

1253 translator_class = RstTranslator 

1254 

1255 output = None 

1256 

1257 def __init__(self, builder): 

1258 writers.Writer.__init__(self) 

1259 self.builder = builder 

1260 

1261 def translate(self): 

1262 visitor = self.builder.create_translator(self.document, self.builder) 

1263 self.document.walkabout(visitor) 

1264 self.output = visitor.body 

1265 

1266 

1267def setup(app): 

1268 """ 

1269 Initializes the :epkg:`RST` builder. 

1270 """ 

1271 app.add_builder(RstBuilder) 

1272 app.add_config_value('rst_file_suffix', ".rst", 'env') 

1273 app.add_config_value('rst_link_suffix', None, 'env') 

1274 app.add_config_value('rst_file_transform', None, 'env') 

1275 app.add_config_value('rst_link_transform', None, 'env') 

1276 app.add_config_value('rst_indent', STDINDENT, 'env') 

1277 app.add_config_value('rst_image_dest', None, 'env')