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 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("Builder has no config: {}".format(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, '%s' % (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('%s ' % (' ' * 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='[%s] ' % 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='[%s] ' % 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('.. image:: {0}'.format(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(":{0}: {1}".format(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='%s. ' % 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(".. code-block:: {0}".format(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 = ':py:%s:`%s <%s>`' % ( 

790 node['reftype'], node.astext(), node['reftarget']) 

791 else: 

792 text = ':py:%s:`%s`' % (node['reftype'], node['reftarget']) 

793 self.add_text(text) 

794 raise nodes.SkipNode 

795 

796 def depart_pending_xref(self, node): 

797 raise NotImplementedError("Error") 

798 

799 def visit_reference(self, node): 

800 """ 

801 Runs upon entering a reference. 

802 Because this class inherits from the TextTranslator class, 

803 regularly defined links, such as:: 

804 

805 `Some Text`_ 

806 

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

808 

809 were being written as plaintext. This included internal 

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

811 

812 `Some Reference` 

813 

814 .. _Some Reference: 

815 

816 Some Title 

817 ---------- 

818 

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

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

821 left unchanged. 

822 

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

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

825 e.g.:: 

826 

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

828 

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

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

831 

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

833 format. 

834 """ 

835 def clean_refuri(uri): 

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

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

838 return link 

839 

840 if 'refuri' not in node: 

841 if 'name' in node.attributes: 

842 self.add_text('`%s`_' % node['name']) 

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

844 self.add_text(':ref:`%s`' % node['refid']) 

845 else: 

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

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

848 self.add_text('`%s <%s>`_' % 

849 (node['name'], clean_refuri(node['refuri']))) 

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

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

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

853 self.add_text('`%s <%s>`_' % 

854 (anchor, clean_refuri(node['refuri']))) 

855 elif 'reftitle' in node: 

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

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

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

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

860 self.add_text('`%s <%s>`_' % 

861 (name, clean_refuri(node['refuri']))) 

862 # self.end_state(wrap=False) 

863 else: 

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

865 self.add_text('`%s <%s>`_' % (name, node['refuri'])) 

866 if 'internal' in node: 

867 raise nodes.SkipNode 

868 

869 def depart_reference(self, node): 

870 if 'refuri' not in node: 

871 pass # Don't add these anchors 

872 elif 'internal' not in node: 

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

874 pass 

875 elif 'reftitle' in node: 

876 pass 

877 

878 def visit_download_reference(self, node): 

879 self.log_unknown("download_reference", node) 

880 

881 def depart_download_reference(self, node): 

882 pass 

883 

884 def visit_emphasis(self, node): 

885 self.add_text('*') 

886 

887 def depart_emphasis(self, node): 

888 self.add_text('*') 

889 

890 def visit_literal_emphasis(self, node): 

891 self.add_text('*') 

892 

893 def depart_literal_emphasis(self, node): 

894 self.add_text('*') 

895 

896 def visit_strong(self, node): 

897 self.add_text('**') 

898 

899 def depart_strong(self, node): 

900 self.add_text('**') 

901 

902 def visit_abbreviation(self, node): 

903 self.add_text('') 

904 

905 def depart_abbreviation(self, node): 

906 if node.hasattr('explanation'): 

907 self.add_text(' (%s)' % node['explanation']) 

908 

909 def visit_title_reference(self, node): 

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

911 self.add_text('*') 

912 

913 def depart_title_reference(self, node): 

914 self.add_text('*') 

915 

916 def visit_literal(self, node): 

917 self.add_text('``') 

918 

919 def depart_literal(self, node): 

920 self.add_text('``') 

921 

922 def visit_subscript(self, node): 

923 self.add_text('_') 

924 

925 def depart_subscript(self, node): 

926 pass 

927 

928 def visit_superscript(self, node): 

929 self.add_text('^') 

930 

931 def depart_superscript(self, node): 

932 pass 

933 

934 def visit_footnote_reference(self, node): 

935 self.add_text('[%s]' % node.astext()) 

936 raise nodes.SkipNode 

937 

938 def visit_citation_reference(self, node): 

939 self.add_text('[%s]' % node.astext()) 

940 raise nodes.SkipNode 

941 

942 def visit_Text(self, node): 

943 self.add_text(node.astext()) 

944 

945 def depart_Text(self, node): 

946 pass 

947 

948 def visit_generated(self, node): 

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

950 pass 

951 

952 def depart_generated(self, node): 

953 pass 

954 

955 def visit_inline(self, node): 

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

957 pass 

958 

959 def depart_inline(self, node): 

960 pass 

961 

962 def visit_problematic(self, node): 

963 self.add_text('>>') 

964 

965 def depart_problematic(self, node): 

966 self.add_text('<<') 

967 

968 def visit_system_message(self, node): 

969 self.new_state(0) 

970 self.add_text('<SYSTEM MESSAGE: %s>' % node.astext()) 

971 self.end_state() 

972 raise nodes.SkipNode 

973 

974 def visit_comment(self, node): 

975 raise nodes.SkipNode 

976 

977 def visit_meta(self, node): 

978 # only valid for HTML 

979 raise nodes.SkipNode 

980 

981 def visit_raw(self, node): 

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

983 self.add_text(node.astext()) 

984 raise nodes.SkipNode 

985 

986 def visit_bigger_node(self, node): 

987 visit_bigger_node_rst(self, node) 

988 

989 def depart_bigger_node(self, node): 

990 depart_bigger_node_rst(self, node) 

991 

992 def visit_gitlog_node(self, node): 

993 visit_gitlog_node_rst(self, node) 

994 

995 def depart_gitlog_node(self, node): 

996 depart_gitlog_node_rst(self, node) 

997 

998 def visit_collapse_node(self, node): 

999 visit_collapse_node_rst(self, node) 

1000 

1001 def depart_collapse_node(self, node): 

1002 depart_collapse_node_rst(self, node) 

1003 

1004 def visit_gdot_node(self, node): 

1005 visit_gdot_node_rst(self, node) 

1006 

1007 def depart_gdot_node(self, node): 

1008 depart_gdot_node_rst(self, node) 

1009 

1010 def visit_quote_node(self, node): 

1011 visit_quote_node_rst(self, node) 

1012 

1013 def depart_quote_node(self, node): 

1014 depart_quote_node_rst(self, node) 

1015 

1016 def visit_issue(self, node): 

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

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

1019 

1020 def depart_issue(self, node): 

1021 self.add_text('`') 

1022 

1023 def eval_expr(self, expr): 

1024 md = False 

1025 rst = True 

1026 html = False 

1027 latex = False 

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

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

1030 try: 

1031 ev = eval(expr) 

1032 except Exception as e: # pragma: no cover 

1033 raise ValueError( 

1034 "Unable to interpret expression '{0}'".format(expr)) 

1035 return ev 

1036 

1037 def visit_only(self, node): 

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

1039 if ev: 

1040 pass 

1041 else: 

1042 raise nodes.SkipNode 

1043 

1044 def depart_only(self, node): 

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

1046 if ev: 

1047 pass 

1048 else: 

1049 # The program should not necessarily be here. 

1050 pass 

1051 

1052 def visit_CodeNode(self, node): 

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

1054 

1055 def depart_CodeNode(self, node): 

1056 pass 

1057 

1058 def visit_sharenet_node(self, node): 

1059 visit_sharenet_node_rst(self, node) 

1060 

1061 def depart_sharenet_node(self, node): 

1062 depart_sharenet_node_rst(self, node) 

1063 

1064 def visit_downloadlink_node(self, node): 

1065 visit_downloadlink_node_rst(self, node) 

1066 

1067 def depart_downloadlink_node(self, node): 

1068 depart_downloadlink_node_rst(self, node) 

1069 

1070 def visit_runpythonthis_node(self, node): 

1071 # for unit test. 

1072 pass 

1073 

1074 def depart_runpythonthis_node(self, node): 

1075 # for unit test. 

1076 pass 

1077 

1078 def visit_inheritance_diagram(self, node): 

1079 self.new_state(0) 

1080 self.add_text('.. inheritance_diagram:: {0}'.format(node['content'])) 

1081 

1082 def depart_inheritance_diagram(self, node): 

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

1084 

1085 def visit_todo_node(self, node): 

1086 self.visit_admonition(node) 

1087 

1088 def depart_todo_node(self, node): 

1089 self.depart_admonition(node) 

1090 

1091 def visit_imgsgnode(self, node): 

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

1093 

1094 def depart_imgsgnode(self, node): 

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

1096 

1097 def unknown_visit(self, node): 

1098 classname = node.__class__.__name__ 

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

1100 'JupyterWidgetViewNode', 'JupyterWidgetStateNode', 

1101 'ThebeSourceNode', 'ThebeOutputNode', 

1102 'ThebeButtonNode', 

1103 }: 

1104 # due to jupyter_sphinx 

1105 return 

1106 logger = logging.getLogger("RstBuilder") 

1107 logger.warning("[rst] unknown visit node: '{0}' - '{1}'".format( 

1108 node.__class__.__name__, node)) 

1109 

1110 def unknown_departure(self, node): 

1111 classname = node.__class__.__name__ 

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

1113 'JupyterWidgetViewNode', 'JupyterWidgetStateNode', 

1114 'ThebeSourceNode', 'ThebeOutputNode', 

1115 'ThebeButtonNode', 

1116 }: 

1117 # due to jupyter_sphinx 

1118 return 

1119 logger = logging.getLogger("RstBuilder") 

1120 logger.warning("[rst] unknown depart node: '{0}' - '{1}'".format( 

1121 node.__class__.__name__, node)) 

1122 

1123 

1124class _BodyPlaceholder: 

1125 def __init__(self, builder): 

1126 self.lines = [] 

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

1128 

1129 def append(self, element): 

1130 if isinstance(element, str): 

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

1132 if len(el) > 50: 

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

1134 self.logger.warning( 

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

1136 else: 

1137 self.logger.warning( 

1138 "[rst] body.append was called with type %r." % type(element)) 

1139 self.lines.append(element) 

1140 

1141 

1142class RstBuilder(Builder): 

1143 """ 

1144 Defines a :epkg:`RST` builder. 

1145 """ 

1146 name = 'rst' 

1147 format = 'rst' 

1148 file_suffix = '.rst' 

1149 link_suffix = None # defaults to file_suffix 

1150 default_translator_class = RstTranslator 

1151 

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

1153 """ 

1154 Constructor, add a logger. 

1155 """ 

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

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

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

1159 # implemented for HTML but used for RST. 

1160 self.body = _BodyPlaceholder(self) 

1161 

1162 def init(self): 

1163 """ 

1164 Load necessary templates and perform initialization. 

1165 """ 

1166 if self.config.rst_file_suffix is not None: 

1167 self.file_suffix = self.config.rst_file_suffix 

1168 if self.config.rst_link_suffix is not None: 

1169 self.link_suffix = self.config.rst_link_suffix 

1170 if self.link_suffix is None: 

1171 self.link_suffix = self.file_suffix 

1172 

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

1174 def file_transform(docname): 

1175 return docname + self.file_suffix 

1176 

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

1178 def link_transform(docname): 

1179 return docname + self.link_suffix 

1180 

1181 if self.config.rst_file_transform is not None: 

1182 self.file_transform = self.config.rst_file_transform 

1183 else: 

1184 self.file_transform = file_transform 

1185 if self.config.rst_link_transform is not None: 

1186 self.link_transform = self.config.rst_link_transform 

1187 else: 

1188 self.link_transform = link_transform 

1189 self.rst_image_dest = self.config.rst_image_dest 

1190 

1191 def get_outdated_docs(self): 

1192 """ 

1193 Return an iterable of input files that are outdated. 

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

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

1196 """ 

1197 for docname in self.env.found_docs: 

1198 if docname not in self.env.all_docs: 

1199 yield docname 

1200 continue 

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

1202 self.file_suffix) 

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

1204 

1205 try: 

1206 targetmtime = path.getmtime(targetname) 

1207 except Exception: 

1208 targetmtime = 0 

1209 try: 

1210 srcmtime = path.getmtime(sourcename) 

1211 if srcmtime > targetmtime: 

1212 yield docname 

1213 except EnvironmentError: 

1214 # source doesn't exist anymore 

1215 pass 

1216 

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

1218 return self.link_transform(docname) 

1219 

1220 def prepare_writing(self, docnames): 

1221 self.writer = RstWriter(self) 

1222 

1223 def get_outfilename(self, pagename): 

1224 """ 

1225 Overwrites *get_target_uri* to control file names. 

1226 """ 

1227 return "{0}/{1}.rst".format(self.outdir, pagename).replace("\\", "/") 

1228 

1229 def write_doc(self, docname, doctree): 

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

1231 self.current_docname = docname 

1232 self.writer.write(doctree, destination) 

1233 ctx = None 

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

1235 

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

1237 outfilename=None, event_arg=None): 

1238 if templatename is not None: 

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

1240 outfilename = self.get_outfilename(pagename) 

1241 ensuredir(path.dirname(outfilename)) 

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

1243 f.write(self.writer.output) 

1244 

1245 def finish(self): 

1246 pass 

1247 

1248 

1249class RstWriter(writers.Writer): 

1250 """ 

1251 Defines a :epkg:`RST` writer. 

1252 """ 

1253 supported = ('text',) 

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

1255 settings_defaults = {} 

1256 translator_class = RstTranslator 

1257 

1258 output = None 

1259 

1260 def __init__(self, builder): 

1261 writers.Writer.__init__(self) 

1262 self.builder = builder 

1263 

1264 def translate(self): 

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

1266 self.document.walkabout(visitor) 

1267 self.output = visitor.body 

1268 

1269 

1270def setup(app): 

1271 """ 

1272 Initializes the :epkg:`RST` builder. 

1273 """ 

1274 app.add_builder(RstBuilder) 

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

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

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

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

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

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