34f97931cf1b35246323628eceb52bc9abc6dae1
[idea/community.git] / python / helpers / rest_formatter.py
1 import sys
2 import re
3 from docutils.core import publish_string
4 from docutils import nodes
5 from docutils.nodes import Text, field_body, field_name
6 from docutils.writers.html4css1 import HTMLTranslator
7 from epydoc.markup import DocstringLinker
8 from epydoc.markup.restructuredtext import ParsedRstDocstring, _EpydocHTMLTranslator, \
9     _DocumentPseudoWriter, _EpydocReader
10
11
12 class RestHTMLTranslator(_EpydocHTMLTranslator):
13     def visit_field_name(self, node):
14         atts = {}
15         if self.in_docinfo:
16             atts['class'] = 'docinfo-name'
17         else:
18             atts['class'] = 'field-name'
19
20         self.context.append('')
21         atts['align'] = "right"
22         self.body.append(self.starttag(node, 'th', '', **atts))
23
24     def visit_field_body(self, node):
25         self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
26         parent_text = node.parent[0][0].astext()
27         if hasattr(node.parent, "type"):
28             self.body.append("(")
29             self.body.append(self.starttag(node, 'a', '',
30                                            href='psi_element://#typename#' + node.parent.type))
31             self.body.append(node.parent.type)
32             self.body.append("</a>")
33             self.body.append(") ")
34         elif parent_text.startswith("type "):
35             index = parent_text.index("type ")
36             type_string = parent_text[index + 5]
37             self.body.append(self.starttag(node, 'a', '',
38                                            href='psi_element://#typename#' + type_string))
39         elif parent_text.startswith("rtype"):
40             type_string = node.children[0][0].astext()
41             self.body.append(self.starttag(node, 'a', '',
42                                            href='psi_element://#typename#' + type_string))
43
44         self.set_class_on_child(node, 'first', 0)
45         field = node.parent
46         if (self.compact_field_list or
47                 isinstance(field.parent, nodes.docinfo) or
48                     field.parent.index(field) == len(field.parent) - 1):
49             # If we are in a compact list, the docinfo, or if this is
50             # the last field of the field list, do not add vertical
51             # space after last element.
52             self.set_class_on_child(node, 'last', -1)
53
54     def depart_field_body(self, node):
55         if node.parent[0][0].astext().startswith("type "):
56             self.body.append("</a>")
57         HTMLTranslator.depart_field_body(self, node)
58
59     def visit_reference(self, node):
60         atts = {}
61         if 'refuri' in node:
62             atts['href'] = node['refuri']
63             if self.settings.cloak_email_addresses and atts['href'].startswith('mailto:'):
64                 atts['href'] = self.cloak_mailto(atts['href'])
65                 self.in_mailto = True
66                 # atts['class'] += ' external'
67         else:
68             assert 'refid' in node, 'References must have "refuri" or "refid" attribute.'
69             atts['href'] = '#' + node['refid']
70             atts['class'] += ' internal'
71         if not isinstance(node.parent, nodes.TextElement):
72             assert len(node) == 1 and isinstance(node[0], nodes.image)
73             atts['class'] += ' image-reference'
74         self.body.append(self.starttag(node, 'a', '', **atts))
75
76     def starttag(self, node, tagname, suffix='\n', **attributes):
77         attr_dicts = [attributes]
78         if isinstance(node, nodes.Node):
79             attr_dicts.append(node.attributes)
80         if isinstance(node, dict):
81             attr_dicts.append(node)
82         # Munge each attribute dictionary.  Unfortunately, we need to
83         # iterate through attributes one at a time because some
84         # versions of docutils don't case-normalize attributes.
85         for attr_dict in attr_dicts:
86             # For some reason additional classes in bullet list make it render poorly.
87             # Such lists are used to render multiple return values in Numpy docstrings by Napoleon.
88             if tagname == 'ul' and isinstance(node.parent, field_body):
89                 attr_dict.pop('class', None)
90                 attr_dict.pop('classes', None)
91                 continue
92
93             for (key, val) in attr_dict.items():
94                 # Prefix all CSS classes with "rst-"; and prefix all
95                 # names with "rst-" to avoid conflicts.
96                 if key.lower() in ('class', 'id', 'name'):
97                     attr_dict[key] = 'rst-%s' % val
98                 elif key.lower() in ('classes', 'ids', 'names'):
99                     attr_dict[key] = ['rst-%s' % cls for cls in val]
100                 elif key.lower() == 'href':
101                     if attr_dict[key][:1] == '#':
102                         attr_dict[key] = '#rst-%s' % attr_dict[key][1:]
103
104         if tagname == 'th' and isinstance(node, field_name):
105             attributes['valign'] = 'top'
106
107         # For headings, use class="heading"
108         if re.match(r'^h\d+$', tagname):
109             attributes['class'] = ' '.join([attributes.get('class', ''), 'heading']).strip()
110
111         return HTMLTranslator.starttag(self, node, tagname, suffix, **attributes)
112
113     def visit_field_list(self, node):
114         fields = {}
115         for n in node.children:
116             if not n.children:
117                 continue
118             child = n.children[0]
119             rawsource = child.rawsource
120             if rawsource.startswith("param "):
121                 index = rawsource.index("param ")
122                 if not child.children:
123                     continue
124                 # Strip leading escaped asterisks for vararg parameters in Google code style docstrings
125                 trimmed_name = re.sub(r'\\\*', '*', rawsource[index + 6:])
126                 child.children[0] = Text(trimmed_name)
127                 fields[trimmed_name] = n
128             if rawsource == "return":
129                 fields["return"] = n
130
131         for n in node.children:
132             if not n.children:
133                 continue
134             child = n.children[0]
135             rawsource = child.rawsource
136             if rawsource.startswith("type "):
137                 index = rawsource.index("type ")
138                 name = re.sub(r'\\\*', '*', rawsource[index + 5:])
139                 if name in fields:
140                     fields[name].type = n.children[1][0][0]
141                     node.children.remove(n)
142             if rawsource == "rtype":
143                 if "return" in fields:
144                     fields["return"].type = n.children[1][0][0]
145                     node.children.remove(n)
146
147         HTMLTranslator.visit_field_list(self, node)
148
149     def unknown_visit(self, node):
150         """ Ignore unknown nodes """
151
152     def unknown_departure(self, node):
153         """ Ignore unknown nodes """
154
155     def visit_problematic(self, node):
156         """Don't insert hyperlinks to nowhere for e.g. unclosed asterisks."""
157         # Note that children text elements will be visited anyway
158
159     def depart_problematic(self, node):
160         pass
161
162     def visit_block_quote(self, node):
163         self.body.append(self.emptytag(node, "br"))
164
165     def depart_block_quote(self, node):
166         pass
167
168     def visit_literal(self, node):
169         """Process text to prevent tokens from wrapping."""
170         self.body.append(self.starttag(node, 'tt', '', CLASS='docutils literal'))
171         text = node.astext()
172         for token in self.words_and_spaces.findall(text):
173             if token.strip():
174                 self.body.append('<code>%s</code>'
175                                  % self.encode(token))
176             elif token in ('\n', ' '):
177                 # Allow breaks at whitespace:
178                 self.body.append(token)
179             else:
180                 # Protect runs of multiple spaces; the last space can wrap:
181                 self.body.append('&nbsp;' * (len(token) - 1) + ' ')
182         self.body.append('</tt>')
183         raise nodes.SkipNode
184
185
186 class MyParsedRstDocstring(ParsedRstDocstring):
187     def __init__(self, document):
188         ParsedRstDocstring.__init__(self, document)
189
190     def to_html(self, docstring_linker, directory=None,
191                 docindex=None, context=None, **options):
192         visitor = RestHTMLTranslator(self._document, docstring_linker,
193                                      directory, docindex, context)
194         self._document.walkabout(visitor)
195         return ''.join(visitor.body)
196
197
198 def parse_docstring(docstring, errors, **options):
199     writer = _DocumentPseudoWriter()
200     reader = _EpydocReader(errors)  # Outputs errors to the list.
201     publish_string(docstring, writer=writer, reader=reader,
202                    settings_overrides={'report_level': 10000,
203                                        'halt_level': 10000,
204                                        'warning_stream': None})
205     return MyParsedRstDocstring(writer.document)
206
207
208 def main(text=None):
209     try:
210         src = sys.stdin.read() if text is None else text
211
212         errors = []
213
214         class EmptyLinker(DocstringLinker):
215             def translate_indexterm(self, indexterm):
216                 return ""
217
218             def translate_identifier_xref(self, identifier, label=None):
219                 return identifier
220
221         docstring = parse_docstring(src, errors)
222         html = docstring.to_html(EmptyLinker())
223
224         if errors and not html:
225             sys.stderr.write("Error parsing docstring:\n")
226             for error in errors:
227                 sys.stderr.write(str(error) + "\n")
228             sys.exit(1)
229
230         sys.stdout.write(html)
231         sys.stdout.flush()
232     except:
233         exc_type, exc_value, exc_traceback = sys.exc_info()
234         sys.stderr.write("Error calculating docstring: " + str(exc_value))
235         sys.exit(1)
236
237
238 if __name__ == '__main__':
239     main()