Coverage for pyquickhelper/sphinxext/sphinx_rst_builder.py: 92%
819 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:`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(f"Builder has no config: {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, f'{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(f"{' ' * 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=f'[{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=f'[{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(f".. image:: {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(f":{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=f'{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(f".. code-block:: {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 = f":py:{node['reftype']}:`{node.astext()} <{node['reftarget']}>`"
790 else:
791 text = f":py:{node['reftype']}:`{node['reftarget']}`"
792 self.add_text(text)
793 raise nodes.SkipNode
795 def depart_pending_xref(self, node):
796 raise NotImplementedError("Error")
798 def visit_reference(self, node):
799 """
800 Runs upon entering a reference.
801 Because this class inherits from the TextTranslator class,
802 regularly defined links, such as::
804 `Some Text`_
806 .. _Some Text: http://www.some_url.com
808 were being written as plaintext. This included internal
809 references defined in the standard rst way, such as::
811 `Some Reference`
813 .. _Some Reference:
815 Some Title
816 ----------
818 To resolve this, if ``refuri`` is not included in the node (an
819 internal, non-Sphinx-defined internal uri, the reference is
820 left unchanged.
822 If ``internal`` is not in the node (as for an external,
823 non-Sphinx URI, the reference is rewritten as an inline link,
824 e.g.::
826 Some Text <http://www.some_url.com>`_
828 If ``reftitle`` is in the node (as in a Sphinx-generated
829 reference), the node is converted to an inline link.
831 Finally, all other links are also converted to an inline link
832 format.
833 """
834 def clean_refuri(uri):
835 ext = os.path.splitext(uri)[-1]
836 link = uri if ext != '.rst' else uri[:-4]
837 return link
839 if 'refuri' not in node:
840 if 'name' in node.attributes:
841 self.add_text(f"`{node['name']}`_")
842 elif 'refid' in node and node['refid']:
843 self.add_text(f":ref:`{node['refid']}`")
844 else:
845 self.log_unknown(type(node), node)
846 elif 'internal' not in node and 'name' in node.attributes:
847 self.add_text(
848 f"`{node['name']} <{clean_refuri(node['refuri'])}>`_")
849 elif 'internal' not in node and 'names' in node.attributes:
850 anchor = node['names'][0] if len(
851 node['names']) > 0 else node['refuri']
852 self.add_text(f"`{anchor} <{clean_refuri(node['refuri'])}>`_")
853 elif 'reftitle' in node:
854 # Include node as text, rather than with markup.
855 # reST seems unable to parse a construct like ` ``literal`` <url>`_
856 # Hence it reverts to the more simple `literal <url>`_
857 name = node['name'] if 'name' in node else node.astext()
858 self.add_text(f"`{name} <{clean_refuri(node['refuri'])}>`_")
859 # self.end_state(wrap=False)
860 else:
861 name = node['name'] if 'name' in node else node.astext()
862 self.add_text(f"`{name} <{node['refuri']}>`_")
863 if 'internal' in node:
864 raise nodes.SkipNode
866 def depart_reference(self, node):
867 if 'refuri' not in node:
868 pass # Don't add these anchors
869 elif 'internal' not in node:
870 # Don't add external links (they are automatically added by the reST spec)
871 pass
872 elif 'reftitle' in node:
873 pass
875 def visit_download_reference(self, node):
876 self.log_unknown("download_reference", node)
878 def depart_download_reference(self, node):
879 pass
881 def visit_emphasis(self, node):
882 self.add_text('*')
884 def depart_emphasis(self, node):
885 self.add_text('*')
887 def visit_literal_emphasis(self, node):
888 self.add_text('*')
890 def depart_literal_emphasis(self, node):
891 self.add_text('*')
893 def visit_strong(self, node):
894 self.add_text('**')
896 def depart_strong(self, node):
897 self.add_text('**')
899 def visit_abbreviation(self, node):
900 self.add_text('')
902 def depart_abbreviation(self, node):
903 if node.hasattr('explanation'):
904 self.add_text(f" ({node['explanation']})")
906 def visit_title_reference(self, node):
907 # self.log_unknown("title_reference", node)
908 self.add_text('*')
910 def depart_title_reference(self, node):
911 self.add_text('*')
913 def visit_literal(self, node):
914 self.add_text('``')
916 def depart_literal(self, node):
917 self.add_text('``')
919 def visit_subscript(self, node):
920 self.add_text('_')
922 def depart_subscript(self, node):
923 pass
925 def visit_superscript(self, node):
926 self.add_text('^')
928 def depart_superscript(self, node):
929 pass
931 def visit_footnote_reference(self, node):
932 self.add_text(f'[{node.astext()}]')
933 raise nodes.SkipNode
935 def visit_citation_reference(self, node):
936 self.add_text(f'[{node.astext()}]')
937 raise nodes.SkipNode
939 def visit_Text(self, node):
940 self.add_text(node.astext())
942 def depart_Text(self, node):
943 pass
945 def visit_generated(self, node):
946 # self.log_unknown("generated", node)
947 pass
949 def depart_generated(self, node):
950 pass
952 def visit_inline(self, node):
953 # self.log_unknown("inline", node)
954 pass
956 def depart_inline(self, node):
957 pass
959 def visit_problematic(self, node):
960 self.add_text('>>')
962 def depart_problematic(self, node):
963 self.add_text('<<')
965 def visit_system_message(self, node):
966 self.new_state(0)
967 self.add_text(f'<SYSTEM MESSAGE: {node.astext()}>')
968 self.end_state()
969 raise nodes.SkipNode
971 def visit_comment(self, node):
972 raise nodes.SkipNode
974 def visit_meta(self, node):
975 # only valid for HTML
976 raise nodes.SkipNode
978 def visit_raw(self, node):
979 if 'text' in node.get('format', '').split():
980 self.add_text(node.astext())
981 raise nodes.SkipNode
983 def visit_bigger_node(self, node):
984 visit_bigger_node_rst(self, node)
986 def depart_bigger_node(self, node):
987 depart_bigger_node_rst(self, node)
989 def visit_gitlog_node(self, node):
990 visit_gitlog_node_rst(self, node)
992 def depart_gitlog_node(self, node):
993 depart_gitlog_node_rst(self, node)
995 def visit_collapse_node(self, node):
996 visit_collapse_node_rst(self, node)
998 def depart_collapse_node(self, node):
999 depart_collapse_node_rst(self, node)
1001 def visit_gdot_node(self, node):
1002 visit_gdot_node_rst(self, node)
1004 def depart_gdot_node(self, node):
1005 depart_gdot_node_rst(self, node)
1007 def visit_quote_node(self, node):
1008 visit_quote_node_rst(self, node)
1010 def depart_quote_node(self, node):
1011 depart_quote_node_rst(self, node)
1013 def visit_issue(self, node):
1014 self.add_text(':issue:`')
1015 self.add_text(node['text'])
1017 def depart_issue(self, node):
1018 self.add_text('`')
1020 def eval_expr(self, expr):
1021 md = False
1022 rst = True
1023 html = False
1024 latex = False
1025 if not (rst or html or latex or md):
1026 raise ValueError("One of them should be True") # pragma: no cover
1027 try:
1028 ev = eval(expr)
1029 except Exception as e: # pragma: no cover
1030 raise ValueError(
1031 f"Unable to interpret expression '{expr}'")
1032 return ev
1034 def visit_only(self, node):
1035 ev = self.eval_expr(node.attributes['expr'])
1036 if ev:
1037 pass
1038 else:
1039 raise nodes.SkipNode
1041 def depart_only(self, node):
1042 ev = self.eval_expr(node.attributes['expr'])
1043 if ev:
1044 pass
1045 else:
1046 # The program should not necessarily be here.
1047 pass
1049 def visit_CodeNode(self, node):
1050 self.add_text('.. CodeNode.' + self.nl)
1052 def depart_CodeNode(self, node):
1053 pass
1055 def visit_sharenet_node(self, node):
1056 visit_sharenet_node_rst(self, node)
1058 def depart_sharenet_node(self, node):
1059 depart_sharenet_node_rst(self, node)
1061 def visit_downloadlink_node(self, node):
1062 visit_downloadlink_node_rst(self, node)
1064 def depart_downloadlink_node(self, node):
1065 depart_downloadlink_node_rst(self, node)
1067 def visit_runpythonthis_node(self, node):
1068 # for unit test.
1069 pass
1071 def depart_runpythonthis_node(self, node):
1072 # for unit test.
1073 pass
1075 def visit_inheritance_diagram(self, node):
1076 self.new_state(0)
1077 self.add_text(f".. inheritance_diagram:: {node['content']}")
1079 def depart_inheritance_diagram(self, node):
1080 self.end_state(wrap=False, end=['\n'])
1082 def visit_todo_node(self, node):
1083 self.visit_admonition(node)
1085 def depart_todo_node(self, node):
1086 self.depart_admonition(node)
1088 def visit_imgsgnode(self, node):
1089 self.add_text('.. imgsgnode(visit).')
1091 def depart_imgsgnode(self, node):
1092 self.add_text('.. imgsgnode(depart).')
1094 def unknown_visit(self, node):
1095 classname = node.__class__.__name__
1096 if classname in {'JupyterKernelNode', 'JupyterCellNode',
1097 'JupyterWidgetViewNode', 'JupyterWidgetStateNode',
1098 'ThebeSourceNode', 'ThebeOutputNode',
1099 'ThebeButtonNode',
1100 }:
1101 # due to jupyter_sphinx
1102 return
1103 logger = logging.getLogger("RstBuilder")
1104 logger.warning("[rst] unknown visit node: '%r - %r",
1105 node.__class__.__name__, node)
1107 def unknown_departure(self, node):
1108 classname = node.__class__.__name__
1109 if classname in {'JupyterKernelNode', 'JupyterCellNode',
1110 'JupyterWidgetViewNode', 'JupyterWidgetStateNode',
1111 'ThebeSourceNode', 'ThebeOutputNode',
1112 'ThebeButtonNode',
1113 }:
1114 # due to jupyter_sphinx
1115 return
1116 logger = logging.getLogger("RstBuilder")
1117 logger.warning("[rst] unknown depart node: %r - %r",
1118 node.__class__.__name__, node)
1121class _BodyPlaceholder:
1122 def __init__(self, builder):
1123 self.lines = []
1124 self.logger = logging.getLogger("RstBuilder")
1126 def append(self, element):
1127 if isinstance(element, str):
1128 el = element.replace("\n", " ")
1129 if len(el) > 50:
1130 el = el[:50] + "..."
1131 self.logger.warning(
1132 "[rst] body.append was called with string %r", el)
1133 else:
1134 self.logger.warning(
1135 "[rst] body.append was called with type %", type(element))
1136 self.lines.append(element)
1139class RstBuilder(Builder):
1140 """
1141 Defines a :epkg:`RST` builder.
1142 """
1143 name = 'rst'
1144 format = 'rst'
1145 file_suffix = '.rst'
1146 link_suffix = None # defaults to file_suffix
1147 default_translator_class = RstTranslator
1149 def __init__(self, *args, **kwargs):
1150 """
1151 Constructor, add a logger.
1152 """
1153 Builder.__init__(self, *args, **kwargs)
1154 self.logger = logging.getLogger("RstBuilder")
1155 # Should not be populated, it may be due to a function
1156 # implemented for HTML but used for RST.
1157 self.body = _BodyPlaceholder(self)
1159 def init(self):
1160 """
1161 Load necessary templates and perform initialization.
1162 """
1163 if self.config.rst_file_suffix is not None:
1164 self.file_suffix = self.config.rst_file_suffix
1165 if self.config.rst_link_suffix is not None:
1166 self.link_suffix = self.config.rst_link_suffix
1167 if self.link_suffix is None:
1168 self.link_suffix = self.file_suffix
1170 # Function to convert the docname to a reST file name.
1171 def file_transform(docname):
1172 return docname + self.file_suffix
1174 # Function to convert the docname to a relative URI.
1175 def link_transform(docname):
1176 return docname + self.link_suffix
1178 if self.config.rst_file_transform is not None:
1179 self.file_transform = self.config.rst_file_transform
1180 else:
1181 self.file_transform = file_transform
1182 if self.config.rst_link_transform is not None:
1183 self.link_transform = self.config.rst_link_transform
1184 else:
1185 self.link_transform = link_transform
1186 self.rst_image_dest = self.config.rst_image_dest
1188 def get_outdated_docs(self):
1189 """
1190 Return an iterable of input files that are outdated.
1191 This method is taken from ``TextBuilder.get_outdated_docs()``
1192 with minor changes to support ``(confval, rst_file_transform))``.
1193 """
1194 for docname in self.env.found_docs:
1195 if docname not in self.env.all_docs:
1196 yield docname
1197 continue
1198 sourcename = path.join(self.env.srcdir, docname +
1199 self.file_suffix)
1200 targetname = path.join(self.outdir, self.file_transform(docname))
1202 try:
1203 targetmtime = path.getmtime(targetname)
1204 except Exception:
1205 targetmtime = 0
1206 try:
1207 srcmtime = path.getmtime(sourcename)
1208 if srcmtime > targetmtime:
1209 yield docname
1210 except EnvironmentError:
1211 # source doesn't exist anymore
1212 pass
1214 def get_target_uri(self, docname, typ=None):
1215 return self.link_transform(docname)
1217 def prepare_writing(self, docnames):
1218 self.writer = RstWriter(self)
1220 def get_outfilename(self, pagename):
1221 """
1222 Overwrites *get_target_uri* to control file names.
1223 """
1224 return f"{self.outdir}/{pagename}.rst".replace("\\", "/")
1226 def write_doc(self, docname, doctree):
1227 destination = StringOutput(encoding='utf-8')
1228 self.current_docname = docname
1229 self.writer.write(doctree, destination)
1230 ctx = None
1231 self.handle_page(docname, ctx, event_arg=doctree)
1233 def handle_page(self, pagename, addctx, templatename=None,
1234 outfilename=None, event_arg=None):
1235 if templatename is not None:
1236 raise NotImplementedError("templatename must be None.")
1237 outfilename = self.get_outfilename(pagename)
1238 ensuredir(path.dirname(outfilename))
1239 with open(outfilename, 'w', encoding='utf-8') as f:
1240 f.write(self.writer.output)
1242 def finish(self):
1243 pass
1246class RstWriter(writers.Writer):
1247 """
1248 Defines a :epkg:`RST` writer.
1249 """
1250 supported = ('text',)
1251 settings_spec = ('No options here.', '', ())
1252 settings_defaults = {}
1253 translator_class = RstTranslator
1255 output = None
1257 def __init__(self, builder):
1258 writers.Writer.__init__(self)
1259 self.builder = builder
1261 def translate(self):
1262 visitor = self.builder.create_translator(self.document, self.builder)
1263 self.document.walkabout(visitor)
1264 self.output = visitor.body
1267def setup(app):
1268 """
1269 Initializes the :epkg:`RST` builder.
1270 """
1271 app.add_builder(RstBuilder)
1272 app.add_config_value('rst_file_suffix', ".rst", 'env')
1273 app.add_config_value('rst_link_suffix', None, 'env')
1274 app.add_config_value('rst_file_transform', None, 'env')
1275 app.add_config_value('rst_link_transform', None, 'env')
1276 app.add_config_value('rst_indent', STDINDENT, 'env')
1277 app.add_config_value('rst_image_dest', None, 'env')