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:
9::
11 Copyright (c) 2012-2013 by Freek Dijkstra <software@macfreek.nl>.
12 Some rights reserved.
14 Redistribution and use in source and binary forms, with or without
15 modification, are permitted provided that the following conditions are
16 met:
18 * Redistributions of source code must retain the above copyright
19 notice, this list of conditions and the following disclaimer.
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.
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
52class MdTranslator(TextTranslator, CommonSphinxWriterHelpers):
53 """
54 Defines a :epkg:`MD` translator.
55 """
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)
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)
83 def log_unknown(self, type, node):
84 logger = logging.getLogger("MdBuilder")
85 logger.warning("%s(%s) unsupported formatting" % (type, node))
87 def wrap(self, text, width=STDINDENT):
88 self.wrapper.width = width
89 return self.wrapper.wrap(text)
91 def add_text(self, text):
92 self.states[-1].append((-1, text))
94 def new_state(self, indent=STDINDENT):
95 self.states.append([])
96 self.stateindent.append(indent)
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 = []
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))
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 = []
124 do_format()
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:])
132 self.states[-1].extend(result)
134 def visit_document(self, node):
135 self.new_state(0)
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)
143 def visit_highlightlang(self, node):
144 raise nodes.SkipNode
146 def visit_section(self, node):
147 self._title_char = self.sectionchars[self.sectionlevel]
148 self.sectionlevel += 1
150 def depart_section(self, node):
151 self.sectionlevel -= 1
153 def visit_topic(self, node):
154 self.new_state(0)
156 def depart_topic(self, node):
157 self.end_state()
159 visit_sidebar = visit_topic
160 depart_sidebar = depart_topic
162 def visit_rubric(self, node):
163 self.new_state(0)
164 self.add_text('-[ ')
166 def depart_rubric(self, node):
167 self.add_text(' ]-')
168 self.end_state()
170 def visit_compound(self, node):
171 # self.log_unknown("compount", node)
172 pass
174 def depart_compound(self, node):
175 pass
177 def visit_glossary(self, node):
178 # self.log_unknown("glossary", node)
179 pass
181 def depart_glossary(self, node):
182 pass
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)
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, '']))
200 def visit_subtitle(self, node):
201 # self.log_unknown("subtitle", node)
202 pass
204 def depart_subtitle(self, node):
205 pass
207 def visit_attribution(self, node):
208 self.add_text('-- ')
210 def depart_attribution(self, node):
211 pass
213 def visit_desc(self, node):
214 self.new_state(0)
216 def depart_desc(self, node):
217 self.end_state()
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('``')
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('``')
231 def visit_desc_name(self, node):
232 # self.log_unknown("desc_name", node)
233 pass
235 def depart_desc_name(self, node):
236 pass
238 def visit_desc_addname(self, node):
239 # self.log_unknown("desc_addname", node)
240 pass
242 def depart_desc_addname(self, node):
243 pass
245 def visit_desc_type(self, node):
246 # self.log_unknown("desc_type", node)
247 pass
249 def depart_desc_type(self, node):
250 pass
252 def visit_desc_returns(self, node):
253 self.add_text(' -> ')
255 def depart_desc_returns(self, node):
256 pass
258 def visit_desc_parameterlist(self, node):
259 self.add_text('(')
260 self.first_param = 1
262 def depart_desc_parameterlist(self, node):
263 self.add_text(')')
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
273 def visit_desc_optional(self, node):
274 self.add_text('[')
276 def depart_desc_optional(self, node):
277 self.add_text(']')
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
287 def depart_desc_annotation(self, node):
288 pass
290 def visit_refcount(self, node):
291 pass
293 def depart_refcount(self, node):
294 pass
296 def visit_desc_content(self, node):
297 self.new_state(self.indent)
299 def depart_desc_content(self, node):
300 self.end_state()
302 def visit_figure(self, node):
303 self.new_state(self.indent)
305 def depart_figure(self, node):
306 self.end_state()
308 def visit_caption(self, node):
309 # self.log_unknown("caption", node)
310 pass
312 def depart_caption(self, node):
313 pass
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
331 def visit_seealso(self, node):
332 self.new_state(self.indent)
334 def depart_seealso(self, node):
335 self.end_state(first='')
337 def visit_footnote(self, node):
338 self._footnote = node.children[0].astext().strip()
339 self.new_state(len(self._footnote) + self.indent)
341 def depart_footnote(self, node):
342 self.end_state(first='[%s] ' % self._footnote)
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)
351 def depart_citation(self, node):
352 self.end_state(first='[%s] ' % self._citlabel)
354 def visit_label(self, node):
355 raise nodes.SkipNode
357 def visit_option_list(self, node):
358 # self.log_unknown("option_list", node)
359 pass
361 def depart_option_list(self, node):
362 pass
364 def visit_option_list_item(self, node):
365 self.new_state(0)
367 def depart_option_list_item(self, node):
368 self.end_state()
370 def visit_option_group(self, node):
371 self._firstoption = True
373 def depart_option_group(self, node):
374 self.add_text(' ')
376 def visit_option(self, node):
377 if self._firstoption:
378 self._firstoption = False
379 else:
380 self.add_text(', ')
382 def depart_option(self, node):
383 pass
385 def visit_option_string(self, node):
386 # self.log_unknown("option_string", node)
387 pass
389 def depart_option_string(self, node):
390 pass
392 def visit_option_argument(self, node):
393 self.add_text(node['delimiter'])
395 def depart_option_argument(self, node):
396 pass
398 def visit_description(self, node):
399 # self.log_unknown("description", node)
400 pass
402 def depart_description(self, node):
403 pass
405 def visit_tabular_col_spec(self, node):
406 raise nodes.SkipNode
408 def visit_colspec(self, node):
409 self._table[0].append(node['colwidth'])
410 raise nodes.SkipNode
412 def visit_tgroup(self, node):
413 # self.log_unknown("tgroup", node)
414 pass
416 def depart_tgroup(self, node):
417 pass
419 def visit_thead(self, node):
420 # self.log_unknown("thead", node)
421 pass
423 def depart_thead(self, node):
424 pass
426 def visit_tbody(self, node):
427 self._table.append('sep')
429 def depart_tbody(self, node):
430 pass
432 def visit_row(self, node):
433 self._table.append([])
435 def depart_row(self, node):
436 pass
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)
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)
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 = [[]]
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)
485 def writesep(char='-'):
486 out = []
487 for width in realwidths:
488 out.append('---')
489 self.add_text(' | '.join(out) + self.nl)
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)
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)
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
516 def visit_simpleimage(self, node):
517 self.visit_image(node)
519 def depart_simpleimage(self, node):
520 self.depart_image(node)
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)
535 def depart_image(self, node):
536 self.end_state(wrap=False, end=None)
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
545 def visit_bullet_list(self, node):
546 self.list_counter.append(-1)
548 def depart_bullet_list(self, node):
549 self.list_counter.pop()
551 def visit_enumerated_list(self, node):
552 self.list_counter.append(0)
554 def depart_enumerated_list(self, node):
555 self.list_counter.pop()
557 def visit_definition_list(self, node):
558 self.list_counter.append(-2)
560 def depart_definition_list(self, node):
561 self.list_counter.pop()
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)
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)
583 def visit_definition_list_item(self, node):
584 self._li_has_classifier = len(node) >= 2 and \
585 isinstance(node[1], nodes.classifier)
587 def depart_definition_list_item(self, node):
588 pass
590 def visit_term(self, node):
591 self.new_state(0)
593 def depart_term(self, node):
594 if not self._li_has_classifier:
595 self.end_state(end=None)
597 def visit_termsep(self, node):
598 self.add_text(', ')
599 raise nodes.SkipNode
601 def visit_classifier(self, node):
602 self.add_text(' : ')
604 def depart_classifier(self, node):
605 self.end_state(end=None)
607 def visit_definition(self, node):
608 self.new_state(self.indent)
610 def depart_definition(self, node):
611 self.end_state()
613 def visit_field_list(self, node):
614 # self.log_unknown("field_list", node)
615 pass
617 def depart_field_list(self, node):
618 pass
620 def visit_field(self, node):
621 self.new_state(0)
623 def depart_field(self, node):
624 self.end_state(end=None)
626 def visit_field_name(self, node):
627 self.add_text(':')
629 def depart_field_name(self, node):
630 self.add_text(':')
631 content = node.astext()
632 self.add_text((16 - len(content)) * ' ')
634 def visit_field_body(self, node):
635 self.new_state(self.indent)
637 def depart_field_body(self, node):
638 self.end_state()
640 def visit_centered(self, node):
641 pass
643 def depart_centered(self, node):
644 pass
646 def visit_hlist(self, node):
647 # self.log_unknown("hlist", node)
648 pass
650 def depart_hlist(self, node):
651 pass
653 def visit_hlistcol(self, node):
654 # self.log_unknown("hlistcol", node)
655 pass
657 def depart_hlistcol(self, node):
658 pass
660 def visit_admonition(self, node):
661 self.new_state(0)
663 def depart_admonition(self, node):
664 self.end_state()
666 def _visit_admonition(self, node):
667 self.new_state(self.indent)
669 def _make_depart_admonition(name):
670 def depart_admonition(self, node):
671 self.end_state(first=admonitionlabels[name] + ': ')
672 return depart_admonition
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')
693 def visit_literal_block(self, node):
694 self.add_text("```")
695 self.new_state(0)
697 def depart_literal_block(self, node):
698 self.add_text(self.nl)
699 self.add_text('```')
700 self.end_state(wrap=False)
702 def visit_doctest_block(self, node):
703 self.new_state(0)
705 def depart_doctest_block(self, node):
706 self.end_state(wrap=False)
708 def visit_line_block(self, node):
709 self.new_state(0)
711 def depart_line_block(self, node):
712 self.end_state(wrap=False)
714 def visit_line(self, node):
715 # self.log_unknown("line", node)
716 pass
718 def depart_line(self, node):
719 pass
721 def visit_compact_paragraph(self, node):
722 pass
724 def depart_compact_paragraph(self, node):
725 pass
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)
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()
737 def visit_target(self, node):
738 raise nodes.SkipNode
740 def visit_index(self, node):
741 raise nodes.SkipNode
743 def visit_substitution_definition(self, node):
744 raise nodes.SkipNode
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
756 def depart_pending_xref(self, node):
757 raise NotImplementedError("Error")
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
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
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
803 def visit_download_reference(self, node):
804 self.log_unknown("download_reference", node)
806 def depart_download_reference(self, node):
807 pass
809 def visit_emphasis(self, node):
810 self.add_text('*')
812 def depart_emphasis(self, node):
813 self.add_text('*')
815 def visit_literal_emphasis(self, node):
816 self.add_text('*')
818 def depart_literal_emphasis(self, node):
819 self.add_text('*')
821 def visit_strong(self, node):
822 self.add_text('**')
824 def depart_strong(self, node):
825 self.add_text('**')
827 def visit_abbreviation(self, node):
828 self.add_text('')
830 def depart_abbreviation(self, node):
831 if node.hasattr('explanation'):
832 self.add_text(' (%s)' % node['explanation'])
834 def visit_title_reference(self, node):
835 # self.log_unknown("title_reference", node)
836 self.add_text('*')
838 def depart_title_reference(self, node):
839 self.add_text('*')
841 def visit_literal(self, node):
842 self.add_text('``')
844 def depart_literal(self, node):
845 self.add_text('``')
847 def visit_subscript(self, node):
848 self.add_text('_')
850 def depart_subscript(self, node):
851 pass
853 def visit_superscript(self, node):
854 self.add_text('^')
856 def depart_superscript(self, node):
857 pass
859 def visit_footnote_reference(self, node):
860 self.add_text('[%s]' % node.astext())
861 raise nodes.SkipNode
863 def visit_citation_reference(self, node):
864 self.add_text('[%s]' % node.astext())
865 raise nodes.SkipNode
867 def visit_Text(self, node):
868 self.add_text(node.astext())
870 def depart_Text(self, node):
871 pass
873 def visit_generated(self, node):
874 # self.log_unknown("generated", node)
875 pass
877 def depart_generated(self, node):
878 pass
880 def visit_inline(self, node):
881 # self.log_unknown("inline", node)
882 pass
884 def depart_inline(self, node):
885 pass
887 def visit_problematic(self, node):
888 self.add_text('>>')
890 def depart_problematic(self, node):
891 self.add_text('<<')
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
899 def visit_comment(self, node):
900 raise nodes.SkipNode
902 def visit_meta(self, node):
903 # only valid for HTML
904 raise nodes.SkipNode
906 def visit_raw(self, node):
907 if 'text' in node.get('format', '').split():
908 self.add_text(node.astext())
909 raise nodes.SkipNode
911 def visit_issue(self, node):
912 self.add_text('(issue *')
913 self.add_text(node['text'])
915 def depart_issue(self, node):
916 self.add_text('*)')
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
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
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
947 def visit_CodeNode(self, node):
948 self.add_text('.. CodeNode.' + self.nl)
950 def depart_CodeNode(self, node):
951 pass
953 def visit_downloadlink_node(self, node):
954 visit_downloadlink_node_md(self, node)
956 def depart_downloadlink_node(self, node):
957 depart_downloadlink_node_md(self, node)
959 def visit_runpythonthis_node(self, node):
960 # for unit test.
961 pass
963 def depart_runpythonthis_node(self, node):
964 # for unit test.
965 pass
967 def visit_inheritance_diagram(self, node):
968 pass
970 def depart_inheritance_diagram(self, node):
971 pass
973 def visit_todo_node(self, node):
974 self.visit_admonition(node)
976 def depart_todo_node(self, node):
977 self.depart_admonition(node)
979 def visit_imgsgnode(self, node):
980 pass
982 def depart_imgsgnode(self, node):
983 pass
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))
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
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")
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
1019 # Function to convert the docname to a markdown file name.
1020 def file_transform(docname):
1021 return docname + self.file_suffix
1023 # Function to convert the docname to a relative URI.
1024 def link_transform(docname):
1025 return docname + self.link_suffix
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
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))
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
1063 def get_target_uri(self, docname, typ=None):
1064 return self.link_transform(docname)
1066 def prepare_writing(self, docnames):
1067 self.writer = MdWriter(self)
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("\\", "/")
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)
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)
1091 def finish(self):
1092 pass
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
1104 output = None
1106 def __init__(self, builder):
1107 writers.Writer.__init__(self)
1108 self.builder = builder
1110 def translate(self):
1111 visitor = self.builder.create_translator(self.document, self.builder)
1112 self.document.walkabout(visitor)
1113 self.output = visitor.body
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')