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
13 class RestHTMLTranslator(_EpydocHTMLTranslator):
14 def visit_field_name(self, node):
17 atts['class'] = 'docinfo-name'
19 atts['class'] = 'field-name'
21 self.context.append('')
22 atts['align'] = "right"
23 self.body.append(self.starttag(node, 'th', '', **atts))
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"):
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))
45 self.set_class_on_child(node, 'first', 0)
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)
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)
60 def visit_reference(self, 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'])
67 # atts['class'] += ' external'
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))
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)
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:]
105 if tagname == 'th' and isinstance(node, field_name):
106 attributes['valign'] = 'top'
108 # Render rubric start as HTML header
109 if tagname == 'p' and isinstance(node, rubric):
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)
117 def visit_field_list(self, node):
119 for n in node.children:
122 child = n.children[0]
123 rawsource = child.rawsource
124 if rawsource.startswith("param "):
125 index = rawsource.index("param ")
126 if not child.children:
128 # Strip leading escaped asterisks for vararg parameters in Google code style docstrings
129 trimmed_name = re.sub(r'\\\*', '*', rawsource[index + 6:])
130 child.children[0] = Text(trimmed_name)
131 fields[trimmed_name] = n
132 if rawsource == "return":
135 for n in node.children:
138 child = n.children[0]
139 rawsource = child.rawsource
140 if rawsource.startswith("type "):
141 index = rawsource.index("type ")
142 name = re.sub(r'\\\*', '*', rawsource[index + 5:])
144 fields[name].type = n.children[1][0][0]
145 node.children.remove(n)
146 if rawsource == "rtype":
147 if "return" in fields:
148 fields["return"].type = n.children[1][0][0]
149 node.children.remove(n)
151 HTMLTranslator.visit_field_list(self, node)
153 def unknown_visit(self, node):
154 """ Ignore unknown nodes """
156 def unknown_departure(self, node):
157 """ Ignore unknown nodes """
159 def visit_problematic(self, node):
160 """Don't insert hyperlinks to nowhere for e.g. unclosed asterisks."""
161 # Note that children text elements will be visited anyway
163 def depart_problematic(self, node):
166 def visit_block_quote(self, node):
167 self.body.append(self.emptytag(node, "br"))
169 def depart_block_quote(self, node):
172 def visit_literal(self, node):
173 """Process text to prevent tokens from wrapping."""
174 self.body.append(self.starttag(node, 'tt', '', CLASS='docutils literal'))
176 for token in self.words_and_spaces.findall(text):
178 self.body.append('<code>%s</code>'
179 % self.encode(token))
180 elif token in ('\n', ' '):
181 # Allow breaks at whitespace:
182 self.body.append(token)
184 # Protect runs of multiple spaces; the last space can wrap:
185 self.body.append(' ' * (len(token) - 1) + ' ')
186 self.body.append('</tt>')
190 class MyParsedRstDocstring(ParsedRstDocstring):
191 def __init__(self, document):
192 ParsedRstDocstring.__init__(self, document)
194 def to_html(self, docstring_linker, directory=None,
195 docindex=None, context=None, **options):
196 visitor = RestHTMLTranslator(self._document, docstring_linker,
197 directory, docindex, context)
198 self._document.walkabout(visitor)
199 return ''.join(visitor.body)
202 def parse_docstring(docstring, errors, **options):
203 writer = _DocumentPseudoWriter()
204 reader = _EpydocReader(errors) # Outputs errors to the list.
205 publish_string(docstring, writer=writer, reader=reader,
206 settings_overrides={'report_level': 10000,
208 'warning_stream': None})
209 return MyParsedRstDocstring(writer.document)
214 src = sys.stdin.read() if text is None else text
218 class EmptyLinker(DocstringLinker):
219 def translate_indexterm(self, indexterm):
222 def translate_identifier_xref(self, identifier, label=None):
225 docstring = parse_docstring(src, errors)
226 html = docstring.to_html(EmptyLinker())
228 if errors and not html:
229 sys.stderr.write("Error parsing docstring:\n")
231 sys.stderr.write(str(error) + "\n")
234 sys.stdout.write(html)
237 exc_type, exc_value, exc_traceback = sys.exc_info()
238 sys.stderr.write("Error calculating docstring: " + str(exc_value))
242 if __name__ == '__main__':