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:`Markdown` 

5or *MD*. It 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, versionlabels, _ 

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

48from ._sphinx_common_builder import CommonSphinxWriterHelpers 

49from .sphinx_downloadlink_extension import visit_downloadlink_node_md, depart_downloadlink_node_md 

50 

51 

52class MdTranslator(TextTranslator, CommonSphinxWriterHelpers): 

53 """ 

54 Defines a :epkg:`MD` translator. 

55 """ 

56 

57 def __init__(self, document, builder): 

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

59 raise TypeError( # pragma: no cover 

60 "Builder has no config: {}".format(type(builder))) 

61 TextTranslator.__init__(self, document, builder) 

62 

63 newlines = builder.config.text_newlines 

64 if newlines == 'windows': 

65 self.nl = '\r\n' 

66 elif newlines == 'native': 

67 self.nl = os.linesep 

68 else: 

69 self.nl = '\n' 

70 self.sectionchars = builder.config.text_sectionchars 

71 self.states = [[]] 

72 self.stateindent = [0] 

73 self.list_counter = [] 

74 self.sectionlevel = 0 

75 self._table = [] 

76 if self.builder.config.md_indent: 

77 self.indent = self.builder.config.md_indent 

78 else: 

79 self.indent = STDINDENT 

80 self.wrapper = textwrap.TextWrapper( 

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

82 

83 def log_unknown(self, type, node): 

84 logger = logging.getLogger("MdBuilder") 

85 logger.warning("%s(%s) unsupported formatting" % (type, node)) 

86 

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

88 self.wrapper.width = width 

89 return self.wrapper.wrap(text) 

90 

91 def add_text(self, text): 

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

93 

94 def new_state(self, indent=STDINDENT): 

95 self.states.append([]) 

96 self.stateindent.append(indent) 

97 

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

99 content = self.states.pop() 

100 maxindent = sum(self.stateindent) 

101 indent = self.stateindent.pop() 

102 result = [] 

103 toformat = [] 

104 

105 def do_format(): 

106 if not toformat: 

107 return 

108 if wrap: 

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

110 else: 

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

112 if end: 

113 res += end 

114 result.append((indent, res)) 

115 

116 for itemindent, item in content: 

117 if itemindent == -1: 

118 toformat.append(item) 

119 else: 

120 do_format() 

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

122 toformat = [] 

123 

124 do_format() 

125 

126 if first is not None and result: 

127 itemindent, item = result[0] 

128 if item: 

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

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

131 

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

133 

134 def visit_document(self, node): 

135 self.new_state(0) 

136 

137 def depart_document(self, node): 

138 self.end_state() 

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

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

141 for line in lines) 

142 

143 def visit_highlightlang(self, node): 

144 raise nodes.SkipNode 

145 

146 def visit_section(self, node): 

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

148 self.sectionlevel += 1 

149 

150 def depart_section(self, node): 

151 self.sectionlevel -= 1 

152 

153 def visit_topic(self, node): 

154 self.new_state(0) 

155 

156 def depart_topic(self, node): 

157 self.end_state() 

158 

159 visit_sidebar = visit_topic 

160 depart_sidebar = depart_topic 

161 

162 def visit_rubric(self, node): 

163 self.new_state(0) 

164 self.add_text('-[ ') 

165 

166 def depart_rubric(self, node): 

167 self.add_text(' ]-') 

168 self.end_state() 

169 

170 def visit_compound(self, node): 

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

172 pass 

173 

174 def depart_compound(self, node): 

175 pass 

176 

177 def visit_glossary(self, node): 

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

179 pass 

180 

181 def depart_glossary(self, node): 

182 pass 

183 

184 def visit_title(self, node): 

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

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

187 raise nodes.SkipNode 

188 self.new_state(0) 

189 

190 def depart_title(self, node): 

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

192 prefix = "#" * self.sectionlevel 

193 else: 

194 prefix = "#" * 6 

195 text = prefix + ' ' + ''.join(x[1] 

196 for x in self.states.pop() if x[0] == -1) 

197 self.stateindent.pop() 

198 self.states[-1].append((0, ['', text, ''])) 

199 

200 def visit_subtitle(self, node): 

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

202 pass 

203 

204 def depart_subtitle(self, node): 

205 pass 

206 

207 def visit_attribution(self, node): 

208 self.add_text('-- ') 

209 

210 def depart_attribution(self, node): 

211 pass 

212 

213 def visit_desc(self, node): 

214 self.new_state(0) 

215 

216 def depart_desc(self, node): 

217 self.end_state() 

218 

219 def visit_desc_signature(self, node): 

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

221 self.add_text('**') 

222 else: 

223 self.add_text('``') 

224 

225 def depart_desc_signature(self, node): 

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

227 self.add_text('**') 

228 else: 

229 self.add_text('``') 

230 

231 def visit_desc_name(self, node): 

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

233 pass 

234 

235 def depart_desc_name(self, node): 

236 pass 

237 

238 def visit_desc_addname(self, node): 

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

240 pass 

241 

242 def depart_desc_addname(self, node): 

243 pass 

244 

245 def visit_desc_type(self, node): 

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

247 pass 

248 

249 def depart_desc_type(self, node): 

250 pass 

251 

252 def visit_desc_returns(self, node): 

253 self.add_text(' -> ') 

254 

255 def depart_desc_returns(self, node): 

256 pass 

257 

258 def visit_desc_parameterlist(self, node): 

259 self.add_text('(') 

260 self.first_param = 1 

261 

262 def depart_desc_parameterlist(self, node): 

263 self.add_text(')') 

264 

265 def visit_desc_parameter(self, node): 

266 if not self.first_param: 

267 self.add_text(', ') 

268 else: 

269 self.first_param = 0 

270 self.add_text(node.astext()) 

271 raise nodes.SkipNode 

272 

273 def visit_desc_optional(self, node): 

274 self.add_text('[') 

275 

276 def depart_desc_optional(self, node): 

277 self.add_text(']') 

278 

279 def visit_desc_annotation(self, node): 

280 content = node.astext() 

281 if len(content) > MAXWIDTH: 

282 h = int(MAXWIDTH / 3) 

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

284 self.add_text(content) 

285 raise nodes.SkipNode 

286 

287 def depart_desc_annotation(self, node): 

288 pass 

289 

290 def visit_refcount(self, node): 

291 pass 

292 

293 def depart_refcount(self, node): 

294 pass 

295 

296 def visit_desc_content(self, node): 

297 self.new_state(self.indent) 

298 

299 def depart_desc_content(self, node): 

300 self.end_state() 

301 

302 def visit_figure(self, node): 

303 self.new_state(self.indent) 

304 

305 def depart_figure(self, node): 

306 self.end_state() 

307 

308 def visit_caption(self, node): 

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

310 pass 

311 

312 def depart_caption(self, node): 

313 pass 

314 

315 def visit_productionlist(self, node): 

316 self.new_state(self.indent) 

317 names = [] 

318 for production in node: 

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

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

321 for production in node: 

322 if production['tokenname']: 

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

324 lastname = production['tokenname'] 

325 else: 

326 self.add_text('%s ' % (' ' * len(lastname))) 

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

328 self.end_state(wrap=False) 

329 raise nodes.SkipNode 

330 

331 def visit_seealso(self, node): 

332 self.new_state(self.indent) 

333 

334 def depart_seealso(self, node): 

335 self.end_state(first='') 

336 

337 def visit_footnote(self, node): 

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

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

340 

341 def depart_footnote(self, node): 

342 self.end_state(first='[%s] ' % self._footnote) 

343 

344 def visit_citation(self, node): 

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

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

347 else: 

348 self._citlabel = '' 

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

350 

351 def depart_citation(self, node): 

352 self.end_state(first='[%s] ' % self._citlabel) 

353 

354 def visit_label(self, node): 

355 raise nodes.SkipNode 

356 

357 def visit_option_list(self, node): 

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

359 pass 

360 

361 def depart_option_list(self, node): 

362 pass 

363 

364 def visit_option_list_item(self, node): 

365 self.new_state(0) 

366 

367 def depart_option_list_item(self, node): 

368 self.end_state() 

369 

370 def visit_option_group(self, node): 

371 self._firstoption = True 

372 

373 def depart_option_group(self, node): 

374 self.add_text(' ') 

375 

376 def visit_option(self, node): 

377 if self._firstoption: 

378 self._firstoption = False 

379 else: 

380 self.add_text(', ') 

381 

382 def depart_option(self, node): 

383 pass 

384 

385 def visit_option_string(self, node): 

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

387 pass 

388 

389 def depart_option_string(self, node): 

390 pass 

391 

392 def visit_option_argument(self, node): 

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

394 

395 def depart_option_argument(self, node): 

396 pass 

397 

398 def visit_description(self, node): 

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

400 pass 

401 

402 def depart_description(self, node): 

403 pass 

404 

405 def visit_tabular_col_spec(self, node): 

406 raise nodes.SkipNode 

407 

408 def visit_colspec(self, node): 

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

410 raise nodes.SkipNode 

411 

412 def visit_tgroup(self, node): 

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

414 pass 

415 

416 def depart_tgroup(self, node): 

417 pass 

418 

419 def visit_thead(self, node): 

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

421 pass 

422 

423 def depart_thead(self, node): 

424 pass 

425 

426 def visit_tbody(self, node): 

427 self._table.append('sep') 

428 

429 def depart_tbody(self, node): 

430 pass 

431 

432 def visit_row(self, node): 

433 self._table.append([]) 

434 

435 def depart_row(self, node): 

436 pass 

437 

438 def visit_entry(self, node): 

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

440 raise NotImplementedError( # pragma: no cover 

441 'Column or row spanning cells are not implemented.') 

442 self.new_state(0) 

443 

444 def depart_entry(self, node): 

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

446 self.stateindent.pop() 

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

448 

449 def visit_table(self, node): 

450 if self._table: 

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

452 self.new_state(0) 

453 self._table = [[]] 

454 

455 def depart_table(self, node): 

456 lines = self._table[1:] 

457 fmted_rows = [] 

458 colwidths = self._table[0] 

459 realwidths = colwidths[:] 

460 separator = 0 

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

462 for line in lines: 

463 if line == 'sep': 

464 separator = len(fmted_rows) 

465 else: 

466 cells = [] 

467 for i, cell in enumerate(line): 

468 try: 

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

470 except (IndexError, ValueError): 

471 par = self.wrap(cell) 

472 if par: 

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

474 else: 

475 maxwidth = 0 

476 if i >= len(realwidths): 

477 realwidths.append(maxwidth) 

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

479 realwidths[i] = maxwidth 

480 else: 

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

482 cells.append(par) 

483 fmted_rows.append(cells) 

484 

485 def writesep(char='-'): 

486 out = [] 

487 for width in realwidths: 

488 out.append('---') 

489 self.add_text(' | '.join(out) + self.nl) 

490 

491 def writerow(row): 

492 lines = zip(*row) 

493 for line in lines: 

494 out = [] 

495 for i, cell in enumerate(line): 

496 if cell: 

497 out.append(cell) 

498 else: 

499 out.append('') 

500 self.add_text(' | '.join(out) + self.nl) 

501 

502 for i, row in enumerate(fmted_rows): 

503 if separator and i == separator: 

504 writesep('-') 

505 writerow(row) 

506 self._table = [] 

507 self.end_state(wrap=False) 

508 

509 def visit_acks(self, node): 

510 self.new_state(0) 

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

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

513 self.end_state() 

514 raise nodes.SkipNode 

515 

516 def visit_simpleimage(self, node): 

517 self.visit_image(node) 

518 

519 def depart_simpleimage(self, node): 

520 self.depart_image(node) 

521 

522 def visit_image(self, node): 

523 self.new_state(0) 

524 atts = self.base_visit_image(node, self.builder.md_image_dest) 

525 alt = atts.get("alt", "") 

526 uri = atts.get('uri', atts['src']) 

527 width = atts.get('width', '').replace('px', '').replace("auto", "") 

528 height = atts.get('height', '').replace('px', '').replace("auto", "") 

529 style = " ={0}x{1}".format(width, height) 

530 if style == " =x": 

531 style = "" 

532 text = "![{0}]({1}{2})".format(alt, uri, style) 

533 self.add_text(text) 

534 

535 def depart_image(self, node): 

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

537 

538 def visit_transition(self, node): 

539 indent = sum(self.stateindent) 

540 self.new_state(0) 

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

542 self.end_state() 

543 raise nodes.SkipNode 

544 

545 def visit_bullet_list(self, node): 

546 self.list_counter.append(-1) 

547 

548 def depart_bullet_list(self, node): 

549 self.list_counter.pop() 

550 

551 def visit_enumerated_list(self, node): 

552 self.list_counter.append(0) 

553 

554 def depart_enumerated_list(self, node): 

555 self.list_counter.pop() 

556 

557 def visit_definition_list(self, node): 

558 self.list_counter.append(-2) 

559 

560 def depart_definition_list(self, node): 

561 self.list_counter.pop() 

562 

563 def visit_list_item(self, node): 

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

565 # bullet list 

566 self.new_state(self.indent) 

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

568 # definition list 

569 pass 

570 else: 

571 # enumerated list 

572 self.list_counter[-1] += 1 

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

574 

575 def depart_list_item(self, node): 

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

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

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

579 pass 

580 else: 

581 self.end_state(first='%s. ' % self.list_counter[-1], end=None) 

582 

583 def visit_definition_list_item(self, node): 

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

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

586 

587 def depart_definition_list_item(self, node): 

588 pass 

589 

590 def visit_term(self, node): 

591 self.new_state(0) 

592 

593 def depart_term(self, node): 

594 if not self._li_has_classifier: 

595 self.end_state(end=None) 

596 

597 def visit_termsep(self, node): 

598 self.add_text(', ') 

599 raise nodes.SkipNode 

600 

601 def visit_classifier(self, node): 

602 self.add_text(' : ') 

603 

604 def depart_classifier(self, node): 

605 self.end_state(end=None) 

606 

607 def visit_definition(self, node): 

608 self.new_state(self.indent) 

609 

610 def depart_definition(self, node): 

611 self.end_state() 

612 

613 def visit_field_list(self, node): 

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

615 pass 

616 

617 def depart_field_list(self, node): 

618 pass 

619 

620 def visit_field(self, node): 

621 self.new_state(0) 

622 

623 def depart_field(self, node): 

624 self.end_state(end=None) 

625 

626 def visit_field_name(self, node): 

627 self.add_text(':') 

628 

629 def depart_field_name(self, node): 

630 self.add_text(':') 

631 content = node.astext() 

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

633 

634 def visit_field_body(self, node): 

635 self.new_state(self.indent) 

636 

637 def depart_field_body(self, node): 

638 self.end_state() 

639 

640 def visit_centered(self, node): 

641 pass 

642 

643 def depart_centered(self, node): 

644 pass 

645 

646 def visit_hlist(self, node): 

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

648 pass 

649 

650 def depart_hlist(self, node): 

651 pass 

652 

653 def visit_hlistcol(self, node): 

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

655 pass 

656 

657 def depart_hlistcol(self, node): 

658 pass 

659 

660 def visit_admonition(self, node): 

661 self.new_state(0) 

662 

663 def depart_admonition(self, node): 

664 self.end_state() 

665 

666 def _visit_admonition(self, node): 

667 self.new_state(self.indent) 

668 

669 def _make_depart_admonition(name): 

670 def depart_admonition(self, node): 

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

672 return depart_admonition 

673 

674 visit_attention = _visit_admonition 

675 depart_attention = _make_depart_admonition('attention') 

676 visit_caution = _visit_admonition 

677 depart_caution = _make_depart_admonition('caution') 

678 visit_danger = _visit_admonition 

679 depart_danger = _make_depart_admonition('danger') 

680 visit_error = _visit_admonition 

681 depart_error = _make_depart_admonition('error') 

682 visit_hint = _visit_admonition 

683 depart_hint = _make_depart_admonition('hint') 

684 visit_important = _visit_admonition 

685 depart_important = _make_depart_admonition('important') 

686 visit_note = _visit_admonition 

687 depart_note = _make_depart_admonition('note') 

688 visit_tip = _visit_admonition 

689 depart_tip = _make_depart_admonition('tip') 

690 visit_warning = _visit_admonition 

691 depart_warning = _make_depart_admonition('warning') 

692 

693 def visit_literal_block(self, node): 

694 self.add_text("```") 

695 self.new_state(0) 

696 

697 def depart_literal_block(self, node): 

698 self.add_text(self.nl) 

699 self.add_text('```') 

700 self.end_state(wrap=False) 

701 

702 def visit_doctest_block(self, node): 

703 self.new_state(0) 

704 

705 def depart_doctest_block(self, node): 

706 self.end_state(wrap=False) 

707 

708 def visit_line_block(self, node): 

709 self.new_state(0) 

710 

711 def depart_line_block(self, node): 

712 self.end_state(wrap=False) 

713 

714 def visit_line(self, node): 

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

716 pass 

717 

718 def depart_line(self, node): 

719 pass 

720 

721 def visit_compact_paragraph(self, node): 

722 pass 

723 

724 def depart_compact_paragraph(self, node): 

725 pass 

726 

727 def visit_paragraph(self, node): 

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

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

730 self.new_state(0) 

731 

732 def depart_paragraph(self, node): 

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

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

735 self.end_state() 

736 

737 def visit_target(self, node): 

738 raise nodes.SkipNode 

739 

740 def visit_index(self, node): 

741 raise nodes.SkipNode 

742 

743 def visit_substitution_definition(self, node): 

744 raise nodes.SkipNode 

745 

746 def visit_pending_xref(self, node): 

747 if node.get('refexplicit'): 

748 text = '[%s](%s.md#%s)' % ( 

749 node.astext(), node['refdoc'], node['reftarget']) 

750 else: 

751 text = '[%s](%s.md#%s)' % ( 

752 node['reftarget'], node['refdoc'], node['reftarget']) 

753 self.add_text(text) 

754 raise nodes.SkipNode 

755 

756 def depart_pending_xref(self, node): 

757 raise NotImplementedError("Error") 

758 

759 def visit_reference(self, node): 

760 def clean_refuri(uri): 

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

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

763 return link 

764 

765 if 'refuri' not in node: 

766 if 'name' in node.attributes: 

767 self.add_text('[!%s]' % node['name']) 

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

769 self.add_text('[!%s]' % node['refid']) 

770 else: 

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

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

773 self.add_text('[%s](%s)' % 

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

775 raise nodes.SkipNode 

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

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

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

779 self.add_text('[%s](%s)' % 

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

781 raise nodes.SkipNode 

782 elif 'reftitle' in node: 

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

784 self.add_text('[%s](%s)' % 

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

786 raise nodes.SkipNode 

787 else: 

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

789 self.add_text('[%s](%s)' % (name, node['refuri'])) 

790 raise nodes.SkipNode 

791 if 'internal' in node: 

792 raise nodes.SkipNode 

793 

794 def depart_reference(self, node): 

795 if 'refuri' not in node: 

796 pass # Don't add these anchors 

797 elif 'internal' not in node: 

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

799 pass 

800 elif 'reftitle' in node: 

801 pass 

802 

803 def visit_download_reference(self, node): 

804 self.log_unknown("download_reference", node) 

805 

806 def depart_download_reference(self, node): 

807 pass 

808 

809 def visit_emphasis(self, node): 

810 self.add_text('*') 

811 

812 def depart_emphasis(self, node): 

813 self.add_text('*') 

814 

815 def visit_literal_emphasis(self, node): 

816 self.add_text('*') 

817 

818 def depart_literal_emphasis(self, node): 

819 self.add_text('*') 

820 

821 def visit_strong(self, node): 

822 self.add_text('**') 

823 

824 def depart_strong(self, node): 

825 self.add_text('**') 

826 

827 def visit_abbreviation(self, node): 

828 self.add_text('') 

829 

830 def depart_abbreviation(self, node): 

831 if node.hasattr('explanation'): 

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

833 

834 def visit_title_reference(self, node): 

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

836 self.add_text('*') 

837 

838 def depart_title_reference(self, node): 

839 self.add_text('*') 

840 

841 def visit_literal(self, node): 

842 self.add_text('``') 

843 

844 def depart_literal(self, node): 

845 self.add_text('``') 

846 

847 def visit_subscript(self, node): 

848 self.add_text('_') 

849 

850 def depart_subscript(self, node): 

851 pass 

852 

853 def visit_superscript(self, node): 

854 self.add_text('^') 

855 

856 def depart_superscript(self, node): 

857 pass 

858 

859 def visit_footnote_reference(self, node): 

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

861 raise nodes.SkipNode 

862 

863 def visit_citation_reference(self, node): 

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

865 raise nodes.SkipNode 

866 

867 def visit_Text(self, node): 

868 self.add_text(node.astext()) 

869 

870 def depart_Text(self, node): 

871 pass 

872 

873 def visit_generated(self, node): 

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

875 pass 

876 

877 def depart_generated(self, node): 

878 pass 

879 

880 def visit_inline(self, node): 

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

882 pass 

883 

884 def depart_inline(self, node): 

885 pass 

886 

887 def visit_problematic(self, node): 

888 self.add_text('>>') 

889 

890 def depart_problematic(self, node): 

891 self.add_text('<<') 

892 

893 def visit_system_message(self, node): 

894 self.new_state(0) 

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

896 self.end_state() 

897 raise nodes.SkipNode 

898 

899 def visit_comment(self, node): 

900 raise nodes.SkipNode 

901 

902 def visit_meta(self, node): 

903 # only valid for HTML 

904 raise nodes.SkipNode 

905 

906 def visit_raw(self, node): 

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

908 self.add_text(node.astext()) 

909 raise nodes.SkipNode 

910 

911 def visit_issue(self, node): 

912 self.add_text('(issue *') 

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

914 

915 def depart_issue(self, node): 

916 self.add_text('*)') 

917 

918 def eval_expr(self, expr): 

919 md = True 

920 rst = False 

921 html = False 

922 latex = False 

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

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

925 try: 

926 ev = eval(expr) 

927 except Exception as e: # pragma: no cover 

928 raise ValueError( 

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

930 return ev 

931 

932 def visit_only(self, node): 

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

934 if ev: 

935 pass 

936 else: 

937 raise nodes.SkipNode 

938 

939 def depart_only(self, node): 

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

941 if ev: 

942 pass 

943 else: 

944 # The program should not necessarily be here. 

945 pass 

946 

947 def visit_CodeNode(self, node): 

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

949 

950 def depart_CodeNode(self, node): 

951 pass 

952 

953 def visit_downloadlink_node(self, node): 

954 visit_downloadlink_node_md(self, node) 

955 

956 def depart_downloadlink_node(self, node): 

957 depart_downloadlink_node_md(self, node) 

958 

959 def visit_runpythonthis_node(self, node): 

960 # for unit test. 

961 pass 

962 

963 def depart_runpythonthis_node(self, node): 

964 # for unit test. 

965 pass 

966 

967 def visit_inheritance_diagram(self, node): 

968 pass 

969 

970 def depart_inheritance_diagram(self, node): 

971 pass 

972 

973 def visit_todo_node(self, node): 

974 self.visit_admonition(node) 

975 

976 def depart_todo_node(self, node): 

977 self.depart_admonition(node) 

978 

979 def visit_imgsgnode(self, node): 

980 pass 

981 

982 def depart_imgsgnode(self, node): 

983 pass 

984 

985 def unknown_visit(self, node): 

986 logger = logging.getLogger("MdBuilder") 

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

988 node.__class__.__name__, node)) 

989 

990 

991class MdBuilder(Builder): 

992 """ 

993 Defines a :epkg:`MD` builder. 

994 """ 

995 name = 'md' 

996 format = 'md' 

997 file_suffix = '.md' 

998 link_suffix = None # defaults to file_suffix 

999 default_translator_class = MdTranslator 

1000 

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

1002 """ 

1003 Constructor, add a logger. 

1004 """ 

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

1006 self.logger = logging.getLogger("MdBuilder") 

1007 

1008 def init(self): 

1009 """ 

1010 Load necessary templates and perform initialization. 

1011 """ 

1012 if self.config.md_file_suffix is not None: 

1013 self.file_suffix = self.config.md_file_suffix 

1014 if self.config.md_link_suffix is not None: 

1015 self.link_suffix = self.config.md_link_suffix 

1016 if self.link_suffix is None: 

1017 self.link_suffix = self.file_suffix 

1018 

1019 # Function to convert the docname to a markdown file name. 

1020 def file_transform(docname): 

1021 return docname + self.file_suffix 

1022 

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

1024 def link_transform(docname): 

1025 return docname + self.link_suffix 

1026 

1027 if self.config.md_file_transform is not None: 

1028 self.file_transform = self.config.md_file_transform 

1029 else: 

1030 self.file_transform = file_transform 

1031 if self.config.md_link_transform is not None: 

1032 self.link_transform = self.config.md_link_transform 

1033 else: 

1034 self.link_transform = link_transform 

1035 self.md_image_dest = self.config.md_image_dest 

1036 

1037 def get_outdated_docs(self): # pragma: no cover 

1038 """ 

1039 Return an iterable of input files that are outdated. 

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

1041 with minor changes to support ``(confval, md_file_transform))``. 

1042 """ 

1043 for docname in self.env.found_docs: 

1044 if docname not in self.env.all_docs: 

1045 yield docname 

1046 continue 

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

1048 self.file_suffix) 

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

1050 

1051 try: 

1052 targetmtime = path.getmtime(targetname) 

1053 except Exception: 

1054 targetmtime = 0 

1055 try: 

1056 srcmtime = path.getmtime(sourcename) 

1057 if srcmtime > targetmtime: 

1058 yield docname 

1059 except EnvironmentError: 

1060 # source doesn't exist anymore 

1061 pass 

1062 

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

1064 return self.link_transform(docname) 

1065 

1066 def prepare_writing(self, docnames): 

1067 self.writer = MdWriter(self) 

1068 

1069 def get_outfilename(self, pagename): # pragma: no cover 

1070 """ 

1071 Overwrite *get_target_uri* to control file names. 

1072 """ 

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

1074 

1075 def write_doc(self, docname, doctree): 

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

1077 self.current_docname = docname 

1078 self.writer.write(doctree, destination) 

1079 ctx = None 

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

1081 

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

1083 outfilename=None, event_arg=None): # pragma: no cover 

1084 if templatename is not None: 

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

1086 outfilename = self.get_outfilename(pagename) 

1087 ensuredir(path.dirname(outfilename)) 

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

1089 f.write(self.writer.output) 

1090 

1091 def finish(self): 

1092 pass 

1093 

1094 

1095class MdWriter(writers.Writer): 

1096 """ 

1097 Defines a :epkg:`MD` writer. 

1098 """ 

1099 supported = ('text',) 

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

1101 settings_defaults = {} 

1102 translator_class = MdTranslator 

1103 

1104 output = None 

1105 

1106 def __init__(self, builder): 

1107 writers.Writer.__init__(self) 

1108 self.builder = builder 

1109 

1110 def translate(self): 

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

1112 self.document.walkabout(visitor) 

1113 self.output = visitor.body 

1114 

1115 

1116def setup(app): 

1117 """ 

1118 Initializes the :epkg:`MD` builder. 

1119 """ 

1120 app.add_builder(MdBuilder) 

1121 app.add_config_value('md_file_suffix', ".md", 'env') 

1122 app.add_config_value('md_link_suffix', None, 'env') 

1123 app.add_config_value('md_file_transform', None, 'env') 

1124 app.add_config_value('md_link_transform', None, 'env') 

1125 app.add_config_value('md_indent', STDINDENT, 'env') 

1126 app.add_config_value('md_image_dest', None, 'env')