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
« 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:
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 f"Builder has no config: {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(%r) 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(f"{' ' * 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=f'[{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=f'[{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 = f" ={width}x{height}"
530 if style == " =x":
531 style = ""
532 text = f"![{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=f'{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 = 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
754 def depart_pending_xref(self, node):
755 raise NotImplementedError("Error")
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
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
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
798 def visit_download_reference(self, node):
799 self.log_unknown("download_reference", node)
801 def depart_download_reference(self, node):
802 pass
804 def visit_emphasis(self, node):
805 self.add_text('*')
807 def depart_emphasis(self, node):
808 self.add_text('*')
810 def visit_literal_emphasis(self, node):
811 self.add_text('*')
813 def depart_literal_emphasis(self, node):
814 self.add_text('*')
816 def visit_strong(self, node):
817 self.add_text('**')
819 def depart_strong(self, node):
820 self.add_text('**')
822 def visit_abbreviation(self, node):
823 self.add_text('')
825 def depart_abbreviation(self, node):
826 if node.hasattr('explanation'):
827 self.add_text(f" ({node['explanation']})")
829 def visit_title_reference(self, node):
830 # self.log_unknown("title_reference", node)
831 self.add_text('*')
833 def depart_title_reference(self, node):
834 self.add_text('*')
836 def visit_literal(self, node):
837 self.add_text('``')
839 def depart_literal(self, node):
840 self.add_text('``')
842 def visit_subscript(self, node):
843 self.add_text('_')
845 def depart_subscript(self, node):
846 pass
848 def visit_superscript(self, node):
849 self.add_text('^')
851 def depart_superscript(self, node):
852 pass
854 def visit_footnote_reference(self, node):
855 self.add_text(f'[{node.astext()}]')
856 raise nodes.SkipNode
858 def visit_citation_reference(self, node):
859 self.add_text(f'[{node.astext()}]')
860 raise nodes.SkipNode
862 def visit_Text(self, node):
863 self.add_text(node.astext())
865 def depart_Text(self, node):
866 pass
868 def visit_generated(self, node):
869 # self.log_unknown("generated", node)
870 pass
872 def depart_generated(self, node):
873 pass
875 def visit_inline(self, node):
876 # self.log_unknown("inline", node)
877 pass
879 def depart_inline(self, node):
880 pass
882 def visit_problematic(self, node):
883 self.add_text('>>')
885 def depart_problematic(self, node):
886 self.add_text('<<')
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
894 def visit_comment(self, node):
895 raise nodes.SkipNode
897 def visit_meta(self, node):
898 # only valid for HTML
899 raise nodes.SkipNode
901 def visit_raw(self, node):
902 if 'text' in node.get('format', '').split():
903 self.add_text(node.astext())
904 raise nodes.SkipNode
906 def visit_issue(self, node):
907 self.add_text('(issue *')
908 self.add_text(node['text'])
910 def depart_issue(self, node):
911 self.add_text('*)')
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
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
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
942 def visit_CodeNode(self, node):
943 self.add_text('.. CodeNode.' + self.nl)
945 def depart_CodeNode(self, node):
946 pass
948 def visit_downloadlink_node(self, node):
949 visit_downloadlink_node_md(self, node)
951 def depart_downloadlink_node(self, node):
952 depart_downloadlink_node_md(self, node)
954 def visit_runpythonthis_node(self, node):
955 # for unit test.
956 pass
958 def depart_runpythonthis_node(self, node):
959 # for unit test.
960 pass
962 def visit_inheritance_diagram(self, node):
963 pass
965 def depart_inheritance_diagram(self, node):
966 pass
968 def visit_todo_node(self, node):
969 self.visit_admonition(node)
971 def depart_todo_node(self, node):
972 self.depart_admonition(node)
974 def visit_imgsgnode(self, node):
975 pass
977 def depart_imgsgnode(self, node):
978 pass
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)
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
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")
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
1014 # Function to convert the docname to a markdown file name.
1015 def file_transform(docname):
1016 return docname + self.file_suffix
1018 # Function to convert the docname to a relative URI.
1019 def link_transform(docname):
1020 return docname + self.link_suffix
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
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))
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
1058 def get_target_uri(self, docname, typ=None):
1059 return self.link_transform(docname)
1061 def prepare_writing(self, docnames):
1062 self.writer = MdWriter(self)
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("\\", "/")
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)
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)
1086 def finish(self):
1087 pass
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
1099 output = None
1101 def __init__(self, builder):
1102 writers.Writer.__init__(self)
1103 self.builder = builder
1105 def translate(self):
1106 visitor = self.builder.create_translator(self.document, self.builder)
1107 self.document.walkabout(visitor)
1108 self.output = visitor.body
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')