Coverage for pyquickhelper/sphinxext/sphinx_md_builder.py: 91%

723 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:`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 f"Builder has no config: {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(%r) 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(f"{' ' * 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=f'[{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=f'[{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 = f" ={width}x{height}" 

530 if style == " =x": 

531 style = "" 

532 text = f"![{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=f'{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 = f"[{node.astext()}]({node['refdoc']}.md#{node['reftarget']})" 

749 else: 

750 text = f"[{node['reftarget']}]({node['refdoc']}.md#{node['reftarget']})" 

751 self.add_text(text) 

752 raise nodes.SkipNode 

753 

754 def depart_pending_xref(self, node): 

755 raise NotImplementedError("Error") 

756 

757 def visit_reference(self, node): 

758 def clean_refuri(uri): 

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

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

761 return link 

762 

763 if 'refuri' not in node: 

764 if 'name' in node.attributes: 

765 self.add_text(f"[!{node['name']}]") 

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

767 self.add_text(f"[!{node['refid']}]") 

768 else: 

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

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

771 self.add_text(f"[{node['name']}]({clean_refuri(node['refuri'])})") 

772 raise nodes.SkipNode 

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

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

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

776 self.add_text(f"[{anchor}]({clean_refuri(node['refuri'])})") 

777 raise nodes.SkipNode 

778 elif 'reftitle' in node: 

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

780 self.add_text(f"[{name}]({clean_refuri(node['refuri'])})") 

781 raise nodes.SkipNode 

782 else: 

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

784 self.add_text(f"[{name}]({node['refuri']})") 

785 raise nodes.SkipNode 

786 if 'internal' in node: 

787 raise nodes.SkipNode 

788 

789 def depart_reference(self, node): 

790 if 'refuri' not in node: 

791 pass # Don't add these anchors 

792 elif 'internal' not in node: 

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

794 pass 

795 elif 'reftitle' in node: 

796 pass 

797 

798 def visit_download_reference(self, node): 

799 self.log_unknown("download_reference", node) 

800 

801 def depart_download_reference(self, node): 

802 pass 

803 

804 def visit_emphasis(self, node): 

805 self.add_text('*') 

806 

807 def depart_emphasis(self, node): 

808 self.add_text('*') 

809 

810 def visit_literal_emphasis(self, node): 

811 self.add_text('*') 

812 

813 def depart_literal_emphasis(self, node): 

814 self.add_text('*') 

815 

816 def visit_strong(self, node): 

817 self.add_text('**') 

818 

819 def depart_strong(self, node): 

820 self.add_text('**') 

821 

822 def visit_abbreviation(self, node): 

823 self.add_text('') 

824 

825 def depart_abbreviation(self, node): 

826 if node.hasattr('explanation'): 

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

828 

829 def visit_title_reference(self, node): 

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

831 self.add_text('*') 

832 

833 def depart_title_reference(self, node): 

834 self.add_text('*') 

835 

836 def visit_literal(self, node): 

837 self.add_text('``') 

838 

839 def depart_literal(self, node): 

840 self.add_text('``') 

841 

842 def visit_subscript(self, node): 

843 self.add_text('_') 

844 

845 def depart_subscript(self, node): 

846 pass 

847 

848 def visit_superscript(self, node): 

849 self.add_text('^') 

850 

851 def depart_superscript(self, node): 

852 pass 

853 

854 def visit_footnote_reference(self, node): 

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

856 raise nodes.SkipNode 

857 

858 def visit_citation_reference(self, node): 

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

860 raise nodes.SkipNode 

861 

862 def visit_Text(self, node): 

863 self.add_text(node.astext()) 

864 

865 def depart_Text(self, node): 

866 pass 

867 

868 def visit_generated(self, node): 

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

870 pass 

871 

872 def depart_generated(self, node): 

873 pass 

874 

875 def visit_inline(self, node): 

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

877 pass 

878 

879 def depart_inline(self, node): 

880 pass 

881 

882 def visit_problematic(self, node): 

883 self.add_text('>>') 

884 

885 def depart_problematic(self, node): 

886 self.add_text('<<') 

887 

888 def visit_system_message(self, node): 

889 self.new_state(0) 

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

891 self.end_state() 

892 raise nodes.SkipNode 

893 

894 def visit_comment(self, node): 

895 raise nodes.SkipNode 

896 

897 def visit_meta(self, node): 

898 # only valid for HTML 

899 raise nodes.SkipNode 

900 

901 def visit_raw(self, node): 

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

903 self.add_text(node.astext()) 

904 raise nodes.SkipNode 

905 

906 def visit_issue(self, node): 

907 self.add_text('(issue *') 

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

909 

910 def depart_issue(self, node): 

911 self.add_text('*)') 

912 

913 def eval_expr(self, expr): 

914 md = True 

915 rst = False 

916 html = False 

917 latex = False 

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

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

920 try: 

921 ev = eval(expr) 

922 except Exception as e: # pragma: no cover 

923 raise ValueError( 

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

925 return ev 

926 

927 def visit_only(self, node): 

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

929 if ev: 

930 pass 

931 else: 

932 raise nodes.SkipNode 

933 

934 def depart_only(self, node): 

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

936 if ev: 

937 pass 

938 else: 

939 # The program should not necessarily be here. 

940 pass 

941 

942 def visit_CodeNode(self, node): 

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

944 

945 def depart_CodeNode(self, node): 

946 pass 

947 

948 def visit_downloadlink_node(self, node): 

949 visit_downloadlink_node_md(self, node) 

950 

951 def depart_downloadlink_node(self, node): 

952 depart_downloadlink_node_md(self, node) 

953 

954 def visit_runpythonthis_node(self, node): 

955 # for unit test. 

956 pass 

957 

958 def depart_runpythonthis_node(self, node): 

959 # for unit test. 

960 pass 

961 

962 def visit_inheritance_diagram(self, node): 

963 pass 

964 

965 def depart_inheritance_diagram(self, node): 

966 pass 

967 

968 def visit_todo_node(self, node): 

969 self.visit_admonition(node) 

970 

971 def depart_todo_node(self, node): 

972 self.depart_admonition(node) 

973 

974 def visit_imgsgnode(self, node): 

975 pass 

976 

977 def depart_imgsgnode(self, node): 

978 pass 

979 

980 def unknown_visit(self, node): 

981 logger = logging.getLogger("MdBuilder") 

982 logger.warning("[md] unknown visit node: %r - %r", 

983 node.__class__.__name__, node) 

984 

985 

986class MdBuilder(Builder): 

987 """ 

988 Defines a :epkg:`MD` builder. 

989 """ 

990 name = 'md' 

991 format = 'md' 

992 file_suffix = '.md' 

993 link_suffix = None # defaults to file_suffix 

994 default_translator_class = MdTranslator 

995 

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

997 """ 

998 Constructor, add a logger. 

999 """ 

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

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

1002 

1003 def init(self): 

1004 """ 

1005 Load necessary templates and perform initialization. 

1006 """ 

1007 if self.config.md_file_suffix is not None: 

1008 self.file_suffix = self.config.md_file_suffix 

1009 if self.config.md_link_suffix is not None: 

1010 self.link_suffix = self.config.md_link_suffix 

1011 if self.link_suffix is None: 

1012 self.link_suffix = self.file_suffix 

1013 

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

1015 def file_transform(docname): 

1016 return docname + self.file_suffix 

1017 

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

1019 def link_transform(docname): 

1020 return docname + self.link_suffix 

1021 

1022 if self.config.md_file_transform is not None: 

1023 self.file_transform = self.config.md_file_transform 

1024 else: 

1025 self.file_transform = file_transform 

1026 if self.config.md_link_transform is not None: 

1027 self.link_transform = self.config.md_link_transform 

1028 else: 

1029 self.link_transform = link_transform 

1030 self.md_image_dest = self.config.md_image_dest 

1031 

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

1033 """ 

1034 Return an iterable of input files that are outdated. 

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

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

1037 """ 

1038 for docname in self.env.found_docs: 

1039 if docname not in self.env.all_docs: 

1040 yield docname 

1041 continue 

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

1043 self.file_suffix) 

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

1045 

1046 try: 

1047 targetmtime = path.getmtime(targetname) 

1048 except Exception: 

1049 targetmtime = 0 

1050 try: 

1051 srcmtime = path.getmtime(sourcename) 

1052 if srcmtime > targetmtime: 

1053 yield docname 

1054 except EnvironmentError: 

1055 # source doesn't exist anymore 

1056 pass 

1057 

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

1059 return self.link_transform(docname) 

1060 

1061 def prepare_writing(self, docnames): 

1062 self.writer = MdWriter(self) 

1063 

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

1065 """ 

1066 Overwrite *get_target_uri* to control file names. 

1067 """ 

1068 return f"{self.outdir}/{pagename}.md".replace("\\", "/") 

1069 

1070 def write_doc(self, docname, doctree): 

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

1072 self.current_docname = docname 

1073 self.writer.write(doctree, destination) 

1074 ctx = None 

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

1076 

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

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

1079 if templatename is not None: 

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

1081 outfilename = self.get_outfilename(pagename) 

1082 ensuredir(path.dirname(outfilename)) 

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

1084 f.write(self.writer.output) 

1085 

1086 def finish(self): 

1087 pass 

1088 

1089 

1090class MdWriter(writers.Writer): 

1091 """ 

1092 Defines a :epkg:`MD` writer. 

1093 """ 

1094 supported = ('text',) 

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

1096 settings_defaults = {} 

1097 translator_class = MdTranslator 

1098 

1099 output = None 

1100 

1101 def __init__(self, builder): 

1102 writers.Writer.__init__(self) 

1103 self.builder = builder 

1104 

1105 def translate(self): 

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

1107 self.document.walkabout(visitor) 

1108 self.output = visitor.body 

1109 

1110 

1111def setup(app): 

1112 """ 

1113 Initializes the :epkg:`MD` builder. 

1114 """ 

1115 app.add_builder(MdBuilder) 

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

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

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

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

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

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