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