Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Defines a sphinx extension to output the documentation in :epkg:`RST`.
5It is inspired from `restbuilder
6<https://github.com/sphinx-contrib/legacy>`_.
7I replicate its license here:
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, _
47try:
48 from sphinx.domains.changeset import versionlabels
49except ImportError: # pragma: no cover
50 from sphinx.locale import versionlabels
51from sphinx.writers.text import TextTranslator, MAXWIDTH, STDINDENT
52from .sphinx_bigger_extension import visit_bigger_node_rst, depart_bigger_node_rst
53from .sphinx_gitlog_extension import visit_gitlog_node_rst, depart_gitlog_node_rst
54from .sphinx_collapse_extension import visit_collapse_node_rst, depart_collapse_node_rst
55from .sphinx_gdot_extension import visit_gdot_node_rst, depart_gdot_node_rst
56from .sphinx_quote_extension import visit_quote_node_rst, depart_quote_node_rst
57from .sphinx_sharenet_extension import visit_sharenet_node_rst, depart_sharenet_node_rst
58from .sphinx_downloadlink_extension import visit_downloadlink_node_rst, depart_downloadlink_node_rst
59from ._sphinx_common_builder import CommonSphinxWriterHelpers
62class RstTranslator(TextTranslator, CommonSphinxWriterHelpers):
63 """
64 Defines a :epkg:`RST` translator.
65 """
66 sectionchars = '*=-~"+`'
68 def __init__(self, document, builder):
69 if not hasattr(builder, "config"):
70 raise TypeError("Builder has no config: {}".format(type(builder)))
71 TextTranslator.__init__(self, document, builder)
73 newlines = builder.config.text_newlines
74 if newlines == 'windows':
75 self.nl = '\r\n'
76 elif newlines == 'native':
77 self.nl = os.linesep
78 else:
79 self.nl = '\n'
80 self.sectionchars = builder.config.text_sectionchars
81 self.states = [[]]
82 self.stateindent = [0]
83 self.list_counter = []
84 self.sectionlevel = 0
85 self._table = None
86 if self.builder.config.rst_indent:
87 self.indent = self.builder.config.rst_indent
88 else:
89 self.indent = STDINDENT
90 self.wrapper = textwrap.TextWrapper(
91 width=STDINDENT, break_long_words=False, break_on_hyphens=False)
93 def log_unknown(self, type, node):
94 logger = logging.getLogger("RstBuilder")
95 logger.warning("[rst] %s(%s) unsupported formatting" % (type, node))
97 def wrap(self, text, width=STDINDENT):
98 self.wrapper.width = width
99 return self.wrapper.wrap(text)
101 def add_text(self, text, indent=-1):
102 self.states[-1].append((indent, text))
104 def new_state(self, indent=STDINDENT):
105 self.states.append([])
106 self.stateindent.append(indent)
108 def end_state(self, wrap=True, end=[''], first=None):
109 content = self.states.pop()
110 maxindent = sum(self.stateindent)
111 indent = self.stateindent.pop()
112 result = []
113 toformat = []
115 def do_format():
116 if not toformat:
117 return
118 if wrap:
119 res = self.wrap(''.join(toformat), width=MAXWIDTH - maxindent)
120 else:
121 res = ''.join(toformat).splitlines()
122 if end:
123 res += end
124 result.append((indent, res))
126 for itemindent, item in content:
127 if itemindent == -1:
128 toformat.append(item)
129 else:
130 do_format()
131 result.append((indent + itemindent, item))
132 toformat = []
134 do_format()
136 if first is not None and result:
137 itemindent, item = result[0]
138 if item:
139 result.insert(0, (itemindent - indent, [first + item[0]]))
140 result[1] = (itemindent, item[1:])
142 self.states[-1].extend(result)
144 def visit_document(self, node):
145 self.new_state(0)
147 def depart_document(self, node):
148 self.end_state()
149 self.body = self.nl.join(line and (' ' * indent + line)
150 for indent, lines in self.states[0]
151 for line in lines)
153 def visit_highlightlang(self, node):
154 raise nodes.SkipNode
156 def visit_section(self, node):
157 self._title_char = self.sectionchars[self.sectionlevel]
158 self.sectionlevel += 1
160 def depart_section(self, node):
161 self.sectionlevel -= 1
163 def visit_topic(self, node):
164 self.new_state(0)
166 def depart_topic(self, node):
167 self.end_state()
169 visit_sidebar = visit_topic
170 depart_sidebar = depart_topic
172 def visit_rubric(self, node):
173 self.new_state(0)
174 self.add_text('-[ ')
176 def depart_rubric(self, node):
177 self.add_text(' ]-')
178 self.end_state()
180 def visit_compound(self, node):
181 # self.log_unknown("compount", node)
182 pass
184 def depart_compound(self, node):
185 pass
187 def visit_glossary(self, node):
188 # self.log_unknown("glossary", node)
189 pass
191 def depart_glossary(self, node):
192 pass
194 def visit_title(self, node):
195 if isinstance(node.parent, nodes.Admonition):
196 self.add_text(node.astext() + ': ')
197 raise nodes.SkipNode
198 self.new_state(0)
200 def depart_title(self, node):
201 if isinstance(node.parent, nodes.section):
202 char = self._title_char
203 else:
204 char = '^'
205 text = ''.join(x[1] for x in self.states.pop() if x[0] == -1)
206 self.stateindent.pop()
207 self.states[-1].append((0, ['', text, '%s' % (char * len(text)), '']))
209 def visit_subtitle(self, node):
210 # self.log_unknown("subtitle", node)
211 pass
213 def depart_subtitle(self, node):
214 pass
216 def visit_attribution(self, node):
217 self.add_text('-- ')
219 def depart_attribution(self, node):
220 pass
222 def visit_desc(self, node):
223 self.new_state(0)
225 def depart_desc(self, node):
226 self.end_state()
228 def visit_desc_signature(self, node):
229 if node.parent['objtype'] in ('class', 'exception', 'method', 'function'):
230 self.add_text('**')
231 else:
232 self.add_text('``')
234 def depart_desc_signature(self, node):
235 if node.parent['objtype'] in ('class', 'exception', 'method', 'function'):
236 self.add_text('**')
237 else:
238 self.add_text('``')
240 def visit_desc_name(self, node):
241 # self.log_unknown("desc_name", node)
242 pass
244 def depart_desc_name(self, node):
245 pass
247 def visit_desc_addname(self, node):
248 # self.log_unknown("desc_addname", node)
249 pass
251 def depart_desc_addname(self, node):
252 pass
254 def visit_desc_type(self, node):
255 # self.log_unknown("desc_type", node)
256 pass
258 def depart_desc_type(self, node):
259 pass
261 def visit_desc_returns(self, node):
262 self.add_text(' -> ')
264 def depart_desc_returns(self, node):
265 pass
267 def visit_desc_parameterlist(self, node):
268 self.add_text('(')
269 self.first_param = 1
271 def depart_desc_parameterlist(self, node):
272 self.add_text(')')
274 def visit_desc_parameter(self, node):
275 if not self.first_param:
276 self.add_text(', ')
277 else:
278 self.first_param = 0
279 self.add_text(node.astext())
280 raise nodes.SkipNode
282 def visit_desc_optional(self, node):
283 self.add_text('[')
285 def depart_desc_optional(self, node):
286 self.add_text(']')
288 def visit_desc_annotation(self, node):
289 content = node.astext()
290 if len(content) > MAXWIDTH: # pragma: no cover
291 h = int(MAXWIDTH / 3)
292 content = content[:h] + " ... " + content[-h:]
293 self.add_text(content)
294 raise nodes.SkipNode
296 def depart_desc_annotation(self, node):
297 pass
299 def visit_refcount(self, node):
300 pass
302 def depart_refcount(self, node):
303 pass
305 def visit_desc_content(self, node):
306 self.new_state(self.indent)
308 def depart_desc_content(self, node):
309 self.end_state()
311 def visit_figure(self, node):
312 self.new_state(self.indent)
314 def depart_figure(self, node):
315 self.end_state()
317 def visit_caption(self, node):
318 # self.log_unknown("caption", node)
319 pass
321 def depart_caption(self, node):
322 pass
324 def visit_productionlist(self, node):
325 self.new_state(self.indent)
326 names = []
327 for production in node:
328 names.append(production['tokenname'])
329 maxlen = max(len(name) for name in names)
330 for production in node:
331 if production['tokenname']:
332 self.add_text(production['tokenname'].ljust(maxlen) + ' ::=')
333 lastname = production['tokenname']
334 else:
335 self.add_text('%s ' % (' ' * len(lastname)))
336 self.add_text(production.astext() + self.nl)
337 self.end_state(wrap=False)
338 raise nodes.SkipNode
340 def visit_seealso(self, node):
341 self.new_state(self.indent)
343 def depart_seealso(self, node):
344 self.end_state(first='')
346 def visit_footnote(self, node):
347 self._footnote = node.children[0].astext().strip()
348 self.new_state(len(self._footnote) + self.indent)
350 def depart_footnote(self, node):
351 self.end_state(first='[%s] ' % self._footnote)
353 def visit_citation(self, node):
354 if len(node) and isinstance(node[0], nodes.label):
355 self._citlabel = node[0].astext()
356 else:
357 self._citlabel = ''
358 self.new_state(len(self._citlabel) + self.indent)
360 def depart_citation(self, node):
361 self.end_state(first='[%s] ' % self._citlabel)
363 def visit_label(self, node):
364 raise nodes.SkipNode
366 def visit_option_list(self, node):
367 # self.log_unknown("option_list", node)
368 pass
370 def depart_option_list(self, node):
371 pass
373 def visit_option_list_item(self, node):
374 self.new_state(0)
376 def depart_option_list_item(self, node):
377 self.end_state()
379 def visit_option_group(self, node):
380 self._firstoption = True
382 def depart_option_group(self, node):
383 self.add_text(' ')
385 def visit_option(self, node):
386 if self._firstoption:
387 self._firstoption = False
388 else:
389 self.add_text(', ')
391 def depart_option(self, node):
392 pass
394 def visit_option_string(self, node):
395 # self.log_unknown("option_string", node)
396 pass
398 def depart_option_string(self, node):
399 pass
401 def visit_option_argument(self, node):
402 self.add_text(node['delimiter'])
404 def depart_option_argument(self, node):
405 pass
407 def visit_description(self, node):
408 # self.log_unknown("description", node)
409 pass
411 def depart_description(self, node):
412 pass
414 def visit_tabular_col_spec(self, node):
415 raise nodes.SkipNode
417 def visit_colspec(self, node):
418 self._table[0].append(node['colwidth'])
419 raise nodes.SkipNode
421 def visit_tgroup(self, node):
422 # self.log_unknown("tgroup", node)
423 pass
425 def depart_tgroup(self, node):
426 pass
428 def visit_thead(self, node):
429 # self.log_unknown("thead", node)
430 pass
432 def depart_thead(self, node):
433 pass
435 def visit_tbody(self, node):
436 self._table.append('sep')
438 def depart_tbody(self, node):
439 pass
441 def visit_row(self, node):
442 self._table.append([])
444 def depart_row(self, node):
445 pass
447 def visit_entry(self, node):
448 if hasattr(node, 'morerows') or hasattr(node, 'morecols'):
449 raise NotImplementedError('Column or row spanning cells are '
450 'not implemented.')
451 self.new_state(0)
453 def depart_entry(self, node):
454 text = self.nl.join(self.nl.join(x[1]) for x in self.states.pop())
455 self.stateindent.pop()
456 self._table[-1].append(text)
458 def visit_table(self, node):
459 if self._table:
460 raise NotImplementedError('Nested tables are not supported.')
461 self.new_state(0)
462 self._table = [[]]
464 def depart_table(self, node):
465 lines = self._table[1:]
466 fmted_rows = []
467 colwidths = self._table[0]
468 realwidths = list(map(lambda x: x if isinstance(x, int) else 1,
469 colwidths[:]))
470 separator = 0
471 # don't allow paragraphs in table cells for now
472 for line in lines:
473 if line == 'sep':
474 separator = len(fmted_rows)
475 else:
476 cells = []
477 for i, cell in enumerate(line):
478 try:
479 par = self.wrap(cell, width=int(colwidths[i]))
480 except (IndexError, ValueError):
481 par = self.wrap(cell)
482 if par:
483 maxwidth = max(map(len, par))
484 else:
485 maxwidth = 0
486 if i >= len(realwidths):
487 realwidths.append(maxwidth)
488 elif isinstance(realwidths[i], str):
489 realwidths[i] = maxwidth
490 else:
491 realwidths[i] = max(realwidths[i], maxwidth)
492 cells.append(par)
493 fmted_rows.append(cells)
494 self._table = None
496 def writesep(char='-'):
497 out = ['+']
498 for width in realwidths:
499 out.append(char * (width + 2))
500 out.append('+')
501 self.add_text(''.join(out) + self.nl)
503 def writerow(row):
504 lines = zip(*row)
505 for line in lines:
506 out = ['|']
507 for i, cell in enumerate(line):
508 if cell:
509 out.append(' ' + cell.ljust(realwidths[i] + 1))
510 else:
511 out.append(' ' * (realwidths[i] + 2))
512 out.append('|')
513 self.add_text(''.join(out) + self.nl)
515 for i, row in enumerate(fmted_rows):
516 if separator and i == separator:
517 writesep('=')
518 else:
519 writesep('-')
520 writerow(row)
521 writesep('-')
522 self._table = None
523 self.end_state(wrap=False)
525 def visit_acks(self, node):
526 self.new_state(0)
527 self.add_text(', '.join(n.astext()
528 for n in node.children[0].children) + '.')
529 self.end_state()
530 raise nodes.SkipNode
532 def visit_simpleimage(self, node):
533 self.visit_image(node)
535 def depart_simpleimage(self, node):
536 self.depart_image(node)
538 def visit_image(self, node):
539 self.new_state(0)
540 atts = self.base_visit_image(node, self.builder.rst_image_dest)
541 self.add_text('.. image:: {0}'.format(atts['src']))
542 for att_name in 'width', 'height', 'alt', 'download':
543 if att_name in node.attributes and node.get(att_name) != 'auto':
544 self.new_state(4)
545 self.add_text(":{0}: {1}".format(att_name, node[att_name]))
546 self.end_state(wrap=False, end=None)
548 def depart_image(self, node):
549 self.end_state(wrap=False, end=None)
551 def visit_transition(self, node):
552 indent = sum(self.stateindent)
553 self.new_state(0)
554 self.add_text('=' * (MAXWIDTH - indent))
555 self.end_state()
556 raise nodes.SkipNode
558 def visit_bullet_list(self, node):
559 self.list_counter.append(-1)
561 def depart_bullet_list(self, node):
562 self.list_counter.pop()
564 def visit_enumerated_list(self, node):
565 self.list_counter.append(0)
567 def depart_enumerated_list(self, node):
568 self.list_counter.pop()
570 def visit_definition_list(self, node):
571 self.list_counter.append(-2)
573 def depart_definition_list(self, node):
574 self.list_counter.pop()
576 def visit_list_item(self, node):
577 if self.list_counter[-1] == -1:
578 # bullet list
579 self.new_state(self.indent)
580 elif self.list_counter[-1] == -2:
581 # definition list
582 pass
583 else:
584 # enumerated list
585 self.list_counter[-1] += 1
586 self.new_state(len(str(self.list_counter[-1])) + self.indent)
588 def depart_list_item(self, node):
589 if self.list_counter[-1] == -1:
590 self.end_state(first='* ', end=None)
591 elif self.list_counter[-1] == -2:
592 pass
593 else:
594 self.end_state(first='%s. ' % self.list_counter[-1], end=None)
596 def visit_definition_list_item(self, node):
597 self._li_has_classifier = len(node) >= 2 and \
598 isinstance(node[1], nodes.classifier)
600 def depart_definition_list_item(self, node):
601 pass
603 def visit_term(self, node):
604 self.new_state(0)
606 def depart_term(self, node):
607 if not self._li_has_classifier:
608 self.end_state(end=None)
610 def visit_termsep(self, node):
611 self.add_text(', ')
612 raise nodes.SkipNode
614 def visit_classifier(self, node):
615 self.add_text(' : ')
617 def depart_classifier(self, node):
618 self.end_state(end=None)
620 def visit_definition(self, node):
621 self.new_state(self.indent)
623 def depart_definition(self, node):
624 self.end_state()
626 def visit_field_list(self, node):
627 # self.log_unknown("field_list", node)
628 pass
630 def depart_field_list(self, node):
631 pass
633 def visit_field(self, node):
634 self.new_state(0)
636 def depart_field(self, node):
637 self.end_state(end=None)
639 def visit_field_name(self, node):
640 self.add_text(':')
642 def depart_field_name(self, node):
643 self.add_text(':')
644 content = node.astext()
645 self.add_text((16 - len(content)) * ' ')
647 def visit_field_body(self, node):
648 self.new_state(self.indent)
650 def depart_field_body(self, node):
651 self.end_state()
653 def visit_centered(self, node):
654 pass
656 def depart_centered(self, node):
657 pass
659 def visit_hlist(self, node):
660 # self.log_unknown("hlist", node)
661 pass
663 def depart_hlist(self, node):
664 pass
666 def visit_hlistcol(self, node):
667 # self.log_unknown("hlistcol", node)
668 pass
670 def depart_hlistcol(self, node):
671 pass
673 def visit_admonition(self, node):
674 self.new_state(0)
676 def depart_admonition(self, node):
677 self.end_state()
679 def _visit_admonition(self, node):
680 self.new_state(self.indent)
682 def _make_depart_admonition(name):
683 def depart_admonition(self, node):
684 self.end_state(first=admonitionlabels[name] + ': ')
685 return depart_admonition
687 visit_attention = _visit_admonition
688 depart_attention = _make_depart_admonition('attention')
689 visit_caution = _visit_admonition
690 depart_caution = _make_depart_admonition('caution')
691 visit_danger = _visit_admonition
692 depart_danger = _make_depart_admonition('danger')
693 visit_error = _visit_admonition
694 depart_error = _make_depart_admonition('error')
695 visit_hint = _visit_admonition
696 depart_hint = _make_depart_admonition('hint')
697 visit_important = _visit_admonition
698 depart_important = _make_depart_admonition('important')
699 visit_note = _visit_admonition
700 depart_note = _make_depart_admonition('note')
701 visit_tip = _visit_admonition
702 depart_tip = _make_depart_admonition('tip')
703 visit_warning = _visit_admonition
704 depart_warning = _make_depart_admonition('warning')
706 def visit_versionmodified(self, node):
707 self.new_state(0)
708 if node.children:
709 self.add_text(versionlabels[node['type']] % node['version'] + ': ')
710 else:
711 self.add_text(versionlabels[node['type']] % node['version'] + '.')
713 def depart_versionmodified(self, node):
714 self.end_state()
716 def visit_literal_block(self, node):
717 if 'language' in node.attributes:
718 self.add_text(".. code-block:: {0}".format(node["language"]))
719 if 'linenos' in node.attributes:
720 self.new_state(4)
721 self.add_text(":linenos:")
722 self.end_state(wrap=False)
723 else:
724 self.add_text("::")
725 self.new_state(self.indent)
727 def depart_literal_block(self, node):
728 self.end_state(wrap=False)
730 def visit_doctest_block(self, node):
731 self.new_state(0)
733 def depart_doctest_block(self, node):
734 self.end_state(wrap=False)
736 def visit_line_block(self, node):
737 self.new_state(0)
739 def depart_line_block(self, node):
740 self.end_state(wrap=False)
742 def visit_line(self, node):
743 # self.log_unknown("line", node)
744 pass
746 def depart_line(self, node):
747 pass
749 def visit_block_quote(self, node):
750 self.add_text('..')
751 self.new_state(self.indent)
753 def depart_block_quote(self, node):
754 self.end_state()
756 def visit_compact_paragraph(self, node):
757 pass
759 def depart_compact_paragraph(self, node):
760 pass
762 def visit_paragraph(self, node):
763 if not isinstance(node.parent, nodes.Admonition) or \
764 isinstance(node.parent, addnodes.seealso):
765 self.new_state(0)
767 def depart_paragraph(self, node):
768 if not isinstance(node.parent, nodes.Admonition) or \
769 isinstance(node.parent, addnodes.seealso):
770 self.end_state()
772 def visit_target(self, node):
773 if 'refid' in node:
774 self.new_state(0)
775 self.add_text('.. _' + node['refid'] + ':' + self.nl)
777 def depart_target(self, node):
778 if 'refid' in node:
779 self.end_state(wrap=False)
781 def visit_index(self, node):
782 raise nodes.SkipNode
784 def visit_substitution_definition(self, node):
785 raise nodes.SkipNode
787 def visit_pending_xref(self, node):
788 if node.get('refexplicit'):
789 text = ':py:%s:`%s <%s>`' % (
790 node['reftype'], node.astext(), node['reftarget'])
791 else:
792 text = ':py:%s:`%s`' % (node['reftype'], node['reftarget'])
793 self.add_text(text)
794 raise nodes.SkipNode
796 def depart_pending_xref(self, node):
797 raise NotImplementedError("Error")
799 def visit_reference(self, node):
800 """
801 Runs upon entering a reference.
802 Because this class inherits from the TextTranslator class,
803 regularly defined links, such as::
805 `Some Text`_
807 .. _Some Text: http://www.some_url.com
809 were being written as plaintext. This included internal
810 references defined in the standard rst way, such as::
812 `Some Reference`
814 .. _Some Reference:
816 Some Title
817 ----------
819 To resolve this, if ``refuri`` is not included in the node (an
820 internal, non-Sphinx-defined internal uri, the reference is
821 left unchanged.
823 If ``internal`` is not in the node (as for an external,
824 non-Sphinx URI, the reference is rewritten as an inline link,
825 e.g.::
827 Some Text <http://www.some_url.com>`_
829 If ``reftitle`` is in the node (as in a Sphinx-generated
830 reference), the node is converted to an inline link.
832 Finally, all other links are also converted to an inline link
833 format.
834 """
835 def clean_refuri(uri):
836 ext = os.path.splitext(uri)[-1]
837 link = uri if ext != '.rst' else uri[:-4]
838 return link
840 if 'refuri' not in node:
841 if 'name' in node.attributes:
842 self.add_text('`%s`_' % node['name'])
843 elif 'refid' in node and node['refid']:
844 self.add_text(':ref:`%s`' % node['refid'])
845 else:
846 self.log_unknown(type(node), node)
847 elif 'internal' not in node and 'name' in node.attributes:
848 self.add_text('`%s <%s>`_' %
849 (node['name'], clean_refuri(node['refuri'])))
850 elif 'internal' not in node and 'names' in node.attributes:
851 anchor = node['names'][0] if len(
852 node['names']) > 0 else node['refuri']
853 self.add_text('`%s <%s>`_' %
854 (anchor, clean_refuri(node['refuri'])))
855 elif 'reftitle' in node:
856 # Include node as text, rather than with markup.
857 # reST seems unable to parse a construct like ` ``literal`` <url>`_
858 # Hence it reverts to the more simple `literal <url>`_
859 name = node['name'] if 'name' in node else node.astext()
860 self.add_text('`%s <%s>`_' %
861 (name, clean_refuri(node['refuri'])))
862 # self.end_state(wrap=False)
863 else:
864 name = node['name'] if 'name' in node else node.astext()
865 self.add_text('`%s <%s>`_' % (name, node['refuri']))
866 if 'internal' in node:
867 raise nodes.SkipNode
869 def depart_reference(self, node):
870 if 'refuri' not in node:
871 pass # Don't add these anchors
872 elif 'internal' not in node:
873 # Don't add external links (they are automatically added by the reST spec)
874 pass
875 elif 'reftitle' in node:
876 pass
878 def visit_download_reference(self, node):
879 self.log_unknown("download_reference", node)
881 def depart_download_reference(self, node):
882 pass
884 def visit_emphasis(self, node):
885 self.add_text('*')
887 def depart_emphasis(self, node):
888 self.add_text('*')
890 def visit_literal_emphasis(self, node):
891 self.add_text('*')
893 def depart_literal_emphasis(self, node):
894 self.add_text('*')
896 def visit_strong(self, node):
897 self.add_text('**')
899 def depart_strong(self, node):
900 self.add_text('**')
902 def visit_abbreviation(self, node):
903 self.add_text('')
905 def depart_abbreviation(self, node):
906 if node.hasattr('explanation'):
907 self.add_text(' (%s)' % node['explanation'])
909 def visit_title_reference(self, node):
910 # self.log_unknown("title_reference", node)
911 self.add_text('*')
913 def depart_title_reference(self, node):
914 self.add_text('*')
916 def visit_literal(self, node):
917 self.add_text('``')
919 def depart_literal(self, node):
920 self.add_text('``')
922 def visit_subscript(self, node):
923 self.add_text('_')
925 def depart_subscript(self, node):
926 pass
928 def visit_superscript(self, node):
929 self.add_text('^')
931 def depart_superscript(self, node):
932 pass
934 def visit_footnote_reference(self, node):
935 self.add_text('[%s]' % node.astext())
936 raise nodes.SkipNode
938 def visit_citation_reference(self, node):
939 self.add_text('[%s]' % node.astext())
940 raise nodes.SkipNode
942 def visit_Text(self, node):
943 self.add_text(node.astext())
945 def depart_Text(self, node):
946 pass
948 def visit_generated(self, node):
949 # self.log_unknown("generated", node)
950 pass
952 def depart_generated(self, node):
953 pass
955 def visit_inline(self, node):
956 # self.log_unknown("inline", node)
957 pass
959 def depart_inline(self, node):
960 pass
962 def visit_problematic(self, node):
963 self.add_text('>>')
965 def depart_problematic(self, node):
966 self.add_text('<<')
968 def visit_system_message(self, node):
969 self.new_state(0)
970 self.add_text('<SYSTEM MESSAGE: %s>' % node.astext())
971 self.end_state()
972 raise nodes.SkipNode
974 def visit_comment(self, node):
975 raise nodes.SkipNode
977 def visit_meta(self, node):
978 # only valid for HTML
979 raise nodes.SkipNode
981 def visit_raw(self, node):
982 if 'text' in node.get('format', '').split():
983 self.add_text(node.astext())
984 raise nodes.SkipNode
986 def visit_bigger_node(self, node):
987 visit_bigger_node_rst(self, node)
989 def depart_bigger_node(self, node):
990 depart_bigger_node_rst(self, node)
992 def visit_gitlog_node(self, node):
993 visit_gitlog_node_rst(self, node)
995 def depart_gitlog_node(self, node):
996 depart_gitlog_node_rst(self, node)
998 def visit_collapse_node(self, node):
999 visit_collapse_node_rst(self, node)
1001 def depart_collapse_node(self, node):
1002 depart_collapse_node_rst(self, node)
1004 def visit_gdot_node(self, node):
1005 visit_gdot_node_rst(self, node)
1007 def depart_gdot_node(self, node):
1008 depart_gdot_node_rst(self, node)
1010 def visit_quote_node(self, node):
1011 visit_quote_node_rst(self, node)
1013 def depart_quote_node(self, node):
1014 depart_quote_node_rst(self, node)
1016 def visit_issue(self, node):
1017 self.add_text(':issue:`')
1018 self.add_text(node['text'])
1020 def depart_issue(self, node):
1021 self.add_text('`')
1023 def eval_expr(self, expr):
1024 md = False
1025 rst = True
1026 html = False
1027 latex = False
1028 if not(rst or html or latex or md):
1029 raise ValueError("One of them should be True") # pragma: no cover
1030 try:
1031 ev = eval(expr)
1032 except Exception as e: # pragma: no cover
1033 raise ValueError(
1034 "Unable to interpret expression '{0}'".format(expr))
1035 return ev
1037 def visit_only(self, node):
1038 ev = self.eval_expr(node.attributes['expr'])
1039 if ev:
1040 pass
1041 else:
1042 raise nodes.SkipNode
1044 def depart_only(self, node):
1045 ev = self.eval_expr(node.attributes['expr'])
1046 if ev:
1047 pass
1048 else:
1049 # The program should not necessarily be here.
1050 pass
1052 def visit_CodeNode(self, node):
1053 self.add_text('.. CodeNode.' + self.nl)
1055 def depart_CodeNode(self, node):
1056 pass
1058 def visit_sharenet_node(self, node):
1059 visit_sharenet_node_rst(self, node)
1061 def depart_sharenet_node(self, node):
1062 depart_sharenet_node_rst(self, node)
1064 def visit_downloadlink_node(self, node):
1065 visit_downloadlink_node_rst(self, node)
1067 def depart_downloadlink_node(self, node):
1068 depart_downloadlink_node_rst(self, node)
1070 def visit_runpythonthis_node(self, node):
1071 # for unit test.
1072 pass
1074 def depart_runpythonthis_node(self, node):
1075 # for unit test.
1076 pass
1078 def visit_inheritance_diagram(self, node):
1079 self.new_state(0)
1080 self.add_text('.. inheritance_diagram:: {0}'.format(node['content']))
1082 def depart_inheritance_diagram(self, node):
1083 self.end_state(wrap=False, end=['\n'])
1085 def visit_todo_node(self, node):
1086 self.visit_admonition(node)
1088 def depart_todo_node(self, node):
1089 self.depart_admonition(node)
1091 def visit_imgsgnode(self, node):
1092 self.add_text('.. imgsgnode(visit).')
1094 def depart_imgsgnode(self, node):
1095 self.add_text('.. imgsgnode(depart).')
1097 def unknown_visit(self, node):
1098 classname = node.__class__.__name__
1099 if classname in {'JupyterKernelNode', 'JupyterCellNode',
1100 'JupyterWidgetViewNode', 'JupyterWidgetStateNode',
1101 'ThebeSourceNode', 'ThebeOutputNode',
1102 'ThebeButtonNode',
1103 }:
1104 # due to jupyter_sphinx
1105 return
1106 logger = logging.getLogger("RstBuilder")
1107 logger.warning("[rst] unknown visit node: '{0}' - '{1}'".format(
1108 node.__class__.__name__, node))
1110 def unknown_departure(self, node):
1111 classname = node.__class__.__name__
1112 if classname in {'JupyterKernelNode', 'JupyterCellNode',
1113 'JupyterWidgetViewNode', 'JupyterWidgetStateNode',
1114 'ThebeSourceNode', 'ThebeOutputNode',
1115 'ThebeButtonNode',
1116 }:
1117 # due to jupyter_sphinx
1118 return
1119 logger = logging.getLogger("RstBuilder")
1120 logger.warning("[rst] unknown depart node: '{0}' - '{1}'".format(
1121 node.__class__.__name__, node))
1124class _BodyPlaceholder:
1125 def __init__(self, builder):
1126 self.lines = []
1127 self.logger = logging.getLogger("RstBuilder")
1129 def append(self, element):
1130 if isinstance(element, str):
1131 el = element.replace("\n", " ")
1132 if len(el) > 50:
1133 el = el[:50] + "..."
1134 self.logger.warning(
1135 "[rst] body.append was called with string %r." % el)
1136 else:
1137 self.logger.warning(
1138 "[rst] body.append was called with type %r." % type(element))
1139 self.lines.append(element)
1142class RstBuilder(Builder):
1143 """
1144 Defines a :epkg:`RST` builder.
1145 """
1146 name = 'rst'
1147 format = 'rst'
1148 file_suffix = '.rst'
1149 link_suffix = None # defaults to file_suffix
1150 default_translator_class = RstTranslator
1152 def __init__(self, *args, **kwargs):
1153 """
1154 Constructor, add a logger.
1155 """
1156 Builder.__init__(self, *args, **kwargs)
1157 self.logger = logging.getLogger("RstBuilder")
1158 # Should not be populated, it may be due to a function
1159 # implemented for HTML but used for RST.
1160 self.body = _BodyPlaceholder(self)
1162 def init(self):
1163 """
1164 Load necessary templates and perform initialization.
1165 """
1166 if self.config.rst_file_suffix is not None:
1167 self.file_suffix = self.config.rst_file_suffix
1168 if self.config.rst_link_suffix is not None:
1169 self.link_suffix = self.config.rst_link_suffix
1170 if self.link_suffix is None:
1171 self.link_suffix = self.file_suffix
1173 # Function to convert the docname to a reST file name.
1174 def file_transform(docname):
1175 return docname + self.file_suffix
1177 # Function to convert the docname to a relative URI.
1178 def link_transform(docname):
1179 return docname + self.link_suffix
1181 if self.config.rst_file_transform is not None:
1182 self.file_transform = self.config.rst_file_transform
1183 else:
1184 self.file_transform = file_transform
1185 if self.config.rst_link_transform is not None:
1186 self.link_transform = self.config.rst_link_transform
1187 else:
1188 self.link_transform = link_transform
1189 self.rst_image_dest = self.config.rst_image_dest
1191 def get_outdated_docs(self):
1192 """
1193 Return an iterable of input files that are outdated.
1194 This method is taken from ``TextBuilder.get_outdated_docs()``
1195 with minor changes to support ``(confval, rst_file_transform))``.
1196 """
1197 for docname in self.env.found_docs:
1198 if docname not in self.env.all_docs:
1199 yield docname
1200 continue
1201 sourcename = path.join(self.env.srcdir, docname +
1202 self.file_suffix)
1203 targetname = path.join(self.outdir, self.file_transform(docname))
1205 try:
1206 targetmtime = path.getmtime(targetname)
1207 except Exception:
1208 targetmtime = 0
1209 try:
1210 srcmtime = path.getmtime(sourcename)
1211 if srcmtime > targetmtime:
1212 yield docname
1213 except EnvironmentError:
1214 # source doesn't exist anymore
1215 pass
1217 def get_target_uri(self, docname, typ=None):
1218 return self.link_transform(docname)
1220 def prepare_writing(self, docnames):
1221 self.writer = RstWriter(self)
1223 def get_outfilename(self, pagename):
1224 """
1225 Overwrites *get_target_uri* to control file names.
1226 """
1227 return "{0}/{1}.rst".format(self.outdir, pagename).replace("\\", "/")
1229 def write_doc(self, docname, doctree):
1230 destination = StringOutput(encoding='utf-8')
1231 self.current_docname = docname
1232 self.writer.write(doctree, destination)
1233 ctx = None
1234 self.handle_page(docname, ctx, event_arg=doctree)
1236 def handle_page(self, pagename, addctx, templatename=None,
1237 outfilename=None, event_arg=None):
1238 if templatename is not None:
1239 raise NotImplementedError("templatename must be None.")
1240 outfilename = self.get_outfilename(pagename)
1241 ensuredir(path.dirname(outfilename))
1242 with open(outfilename, 'w', encoding='utf-8') as f:
1243 f.write(self.writer.output)
1245 def finish(self):
1246 pass
1249class RstWriter(writers.Writer):
1250 """
1251 Defines a :epkg:`RST` writer.
1252 """
1253 supported = ('text',)
1254 settings_spec = ('No options here.', '', ())
1255 settings_defaults = {}
1256 translator_class = RstTranslator
1258 output = None
1260 def __init__(self, builder):
1261 writers.Writer.__init__(self)
1262 self.builder = builder
1264 def translate(self):
1265 visitor = self.builder.create_translator(self.document, self.builder)
1266 self.document.walkabout(visitor)
1267 self.output = visitor.body
1270def setup(app):
1271 """
1272 Initializes the :epkg:`RST` builder.
1273 """
1274 app.add_builder(RstBuilder)
1275 app.add_config_value('rst_file_suffix', ".rst", 'env')
1276 app.add_config_value('rst_link_suffix', None, 'env')
1277 app.add_config_value('rst_file_transform', None, 'env')
1278 app.add_config_value('rst_link_transform', None, 'env')
1279 app.add_config_value('rst_indent', STDINDENT, 'env')
1280 app.add_config_value('rst_image_dest', None, 'env')