]>
Commit | Line | Data |
---|---|---|
3313b612 MAL |
1 | #!/usr/bin/env python |
2 | # QAPI texi generator | |
3 | # | |
4 | # This work is licensed under the terms of the GNU LGPL, version 2+. | |
5 | # See the COPYING file in the top-level directory. | |
6 | """This script produces the documentation of a qapi schema in texinfo format""" | |
7 | import re | |
8 | import sys | |
9 | ||
10 | import qapi | |
11 | ||
597494ab | 12 | MSG_FMT = """ |
3313b612 MAL |
13 | @deftypefn {type} {{}} {name} |
14 | ||
15 | {body} | |
16 | ||
17 | @end deftypefn | |
18 | ||
19 | """.format | |
20 | ||
597494ab | 21 | TYPE_FMT = """ |
3313b612 MAL |
22 | @deftp {{{type}}} {name} |
23 | ||
24 | {body} | |
25 | ||
26 | @end deftp | |
27 | ||
28 | """.format | |
29 | ||
30 | EXAMPLE_FMT = """@example | |
31 | {code} | |
32 | @end example | |
33 | """.format | |
34 | ||
35 | ||
36 | def subst_strong(doc): | |
37 | """Replaces *foo* by @strong{foo}""" | |
38 | return re.sub(r'\*([^*\n]+)\*', r'@emph{\1}', doc) | |
39 | ||
40 | ||
41 | def subst_emph(doc): | |
42 | """Replaces _foo_ by @emph{foo}""" | |
43 | return re.sub(r'\b_([^_\n]+)_\b', r' @emph{\1} ', doc) | |
44 | ||
45 | ||
46 | def subst_vars(doc): | |
47 | """Replaces @var by @code{var}""" | |
48 | return re.sub(r'@([\w-]+)', r'@code{\1}', doc) | |
49 | ||
50 | ||
51 | def subst_braces(doc): | |
52 | """Replaces {} with @{ @}""" | |
ef801a9b | 53 | return doc.replace('{', '@{').replace('}', '@}') |
3313b612 MAL |
54 | |
55 | ||
56 | def texi_example(doc): | |
57 | """Format @example""" | |
58 | # TODO: Neglects to escape @ characters. | |
59 | # We should probably escape them in subst_braces(), and rename the | |
60 | # function to subst_special() or subs_texi_special(). If we do that, we | |
61 | # need to delay it until after subst_vars() in texi_format(). | |
62 | doc = subst_braces(doc).strip('\n') | |
63 | return EXAMPLE_FMT(code=doc) | |
64 | ||
65 | ||
66 | def texi_format(doc): | |
67 | """ | |
68 | Format documentation | |
69 | ||
70 | Lines starting with: | |
71 | - |: generates an @example | |
72 | - =: generates @section | |
73 | - ==: generates @subsection | |
74 | - 1. or 1): generates an @enumerate @item | |
75 | - */-: generates an @itemize list | |
76 | """ | |
77 | lines = [] | |
78 | doc = subst_braces(doc) | |
79 | doc = subst_vars(doc) | |
80 | doc = subst_emph(doc) | |
81 | doc = subst_strong(doc) | |
ef801a9b | 82 | inlist = '' |
3313b612 MAL |
83 | lastempty = False |
84 | for line in doc.split('\n'): | |
ef801a9b | 85 | empty = line == '' |
3313b612 MAL |
86 | |
87 | # FIXME: Doing this in a single if / elif chain is | |
88 | # problematic. For instance, a line without markup terminates | |
89 | # a list if it follows a blank line (reaches the final elif), | |
90 | # but a line with some *other* markup, such as a = title | |
91 | # doesn't. | |
92 | # | |
93 | # Make sure to update section "Documentation markup" in | |
94 | # docs/qapi-code-gen.txt when fixing this. | |
ef801a9b | 95 | if line.startswith('| '): |
3313b612 | 96 | line = EXAMPLE_FMT(code=line[2:]) |
ef801a9b MA |
97 | elif line.startswith('= '): |
98 | line = '@section ' + line[2:] | |
99 | elif line.startswith('== '): | |
100 | line = '@subsection ' + line[3:] | |
3313b612 MAL |
101 | elif re.match(r'^([0-9]*\.) ', line): |
102 | if not inlist: | |
ef801a9b MA |
103 | lines.append('@enumerate') |
104 | inlist = 'enumerate' | |
105 | line = line[line.find(' ')+1:] | |
106 | lines.append('@item') | |
3313b612 MAL |
107 | elif re.match(r'^[*-] ', line): |
108 | if not inlist: | |
ef801a9b MA |
109 | lines.append('@itemize %s' % {'*': '@bullet', |
110 | '-': '@minus'}[line[0]]) | |
111 | inlist = 'itemize' | |
112 | lines.append('@item') | |
3313b612 MAL |
113 | line = line[2:] |
114 | elif lastempty and inlist: | |
ef801a9b MA |
115 | lines.append('@end %s\n' % inlist) |
116 | inlist = '' | |
3313b612 MAL |
117 | |
118 | lastempty = empty | |
119 | lines.append(line) | |
120 | ||
121 | if inlist: | |
ef801a9b MA |
122 | lines.append('@end %s\n' % inlist) |
123 | return '\n'.join(lines) | |
3313b612 MAL |
124 | |
125 | ||
aa964b7f MA |
126 | def texi_body(doc): |
127 | """Format the main documentation body""" | |
128 | return texi_format(str(doc.body)) + '\n' | |
129 | ||
130 | ||
131 | def texi_enum_value(value): | |
132 | """Format a table of members item for an enumeration value""" | |
71d918a1 | 133 | return '@item @code{%s}\n' % value.name |
aa964b7f MA |
134 | |
135 | ||
136 | def texi_member(member): | |
137 | """Format a table of members item for an object type member""" | |
691e0313 MA |
138 | typ = member.type.doc_type() |
139 | return '@item @code{%s%s%s}%s\n' % ( | |
140 | member.name, | |
141 | ': ' if typ else '', | |
142 | typ if typ else '', | |
143 | ' (optional)' if member.optional else '') | |
aa964b7f | 144 | |
3313b612 | 145 | |
2c99f5fd | 146 | def texi_members(doc, what, member_func): |
aa964b7f MA |
147 | """Format the table of members""" |
148 | items = '' | |
149 | for section in doc.args.itervalues(): | |
5da19f14 MA |
150 | if section.content: |
151 | desc = str(section) | |
152 | else: | |
153 | desc = 'Not documented' | |
aa964b7f MA |
154 | items += member_func(section.member) + texi_format(desc) + '\n' |
155 | if not items: | |
156 | return '' | |
2a1183ce | 157 | return '\n@b{%s:}\n@table @asis\n%s@end table\n' % (what, items) |
aa964b7f MA |
158 | |
159 | ||
160 | def texi_sections(doc): | |
161 | """Format additional sections following arguments""" | |
162 | body = '' | |
3313b612 MAL |
163 | for section in doc.sections: |
164 | name, doc = (section.name, str(section)) | |
165 | func = texi_format | |
ef801a9b | 166 | if name.startswith('Example'): |
3313b612 MAL |
167 | func = texi_example |
168 | ||
169 | if name: | |
1ede77df | 170 | # prefer @b over @strong, so txt doesn't translate it to *Foo:* |
ef801a9b | 171 | body += '\n\n@b{%s:}\n' % name |
1ede77df MAL |
172 | |
173 | body += func(doc) | |
3313b612 MAL |
174 | return body |
175 | ||
176 | ||
2c99f5fd | 177 | def texi_entity(doc, what, member_func=texi_member): |
aa964b7f | 178 | return (texi_body(doc) |
2c99f5fd | 179 | + texi_members(doc, what, member_func) |
aa964b7f MA |
180 | + texi_sections(doc)) |
181 | ||
182 | ||
183 | class QAPISchemaGenDocVisitor(qapi.QAPISchemaVisitor): | |
184 | def __init__(self): | |
185 | self.out = None | |
186 | self.cur_doc = None | |
187 | ||
188 | def visit_begin(self, schema): | |
189 | self.out = '' | |
190 | ||
191 | def visit_enum_type(self, name, info, values, prefix): | |
192 | doc = self.cur_doc | |
193 | if self.out: | |
194 | self.out += '\n' | |
195 | self.out += TYPE_FMT(type='Enum', | |
196 | name=doc.symbol, | |
2a1183ce | 197 | body=texi_entity(doc, 'Values', |
2c99f5fd | 198 | member_func=texi_enum_value)) |
aa964b7f MA |
199 | |
200 | def visit_object_type(self, name, info, base, members, variants): | |
201 | doc = self.cur_doc | |
202 | if not variants: | |
203 | typ = 'Struct' | |
204 | elif variants._tag_name: # TODO unclean member access | |
205 | typ = 'Flat Union' | |
206 | else: | |
207 | typ = 'Simple Union' | |
208 | if self.out: | |
209 | self.out += '\n' | |
210 | self.out += TYPE_FMT(type=typ, | |
211 | name=doc.symbol, | |
2a1183ce | 212 | body=texi_entity(doc, 'Members')) |
aa964b7f MA |
213 | |
214 | def visit_alternate_type(self, name, info, variants): | |
215 | doc = self.cur_doc | |
216 | if self.out: | |
217 | self.out += '\n' | |
218 | self.out += TYPE_FMT(type='Alternate', | |
219 | name=doc.symbol, | |
2a1183ce | 220 | body=texi_entity(doc, 'Members')) |
aa964b7f MA |
221 | |
222 | def visit_command(self, name, info, arg_type, ret_type, | |
223 | gen, success_response, boxed): | |
224 | doc = self.cur_doc | |
225 | if self.out: | |
226 | self.out += '\n' | |
c2dd311c MA |
227 | if boxed: |
228 | body = texi_body(doc) | |
229 | body += '\n@b{Arguments:} the members of @code{%s}' % arg_type.name | |
230 | body += texi_sections(doc) | |
231 | else: | |
232 | body = texi_entity(doc, 'Arguments') | |
aa964b7f MA |
233 | self.out += MSG_FMT(type='Command', |
234 | name=doc.symbol, | |
c2dd311c | 235 | body=body) |
aa964b7f MA |
236 | |
237 | def visit_event(self, name, info, arg_type, boxed): | |
238 | doc = self.cur_doc | |
239 | if self.out: | |
240 | self.out += '\n' | |
241 | self.out += MSG_FMT(type='Event', | |
242 | name=doc.symbol, | |
2a1183ce | 243 | body=texi_entity(doc, 'Arguments')) |
aa964b7f MA |
244 | |
245 | def symbol(self, doc, entity): | |
246 | self.cur_doc = doc | |
247 | entity.visit(self) | |
248 | self.cur_doc = None | |
249 | ||
250 | def freeform(self, doc): | |
251 | assert not doc.args | |
252 | if self.out: | |
253 | self.out += '\n' | |
254 | self.out += texi_body(doc) + texi_sections(doc) | |
255 | ||
256 | ||
257 | def texi_schema(schema): | |
258 | """Convert QAPI schema documentation to Texinfo""" | |
259 | gen = QAPISchemaGenDocVisitor() | |
260 | gen.visit_begin(schema) | |
261 | for doc in schema.docs: | |
262 | if doc.symbol: | |
263 | gen.symbol(doc, schema.lookup_entity(doc.symbol)) | |
264 | else: | |
265 | gen.freeform(doc) | |
266 | return gen.out | |
3313b612 MAL |
267 | |
268 | ||
269 | def main(argv): | |
270 | """Takes schema argument, prints result to stdout""" | |
271 | if len(argv) != 2: | |
272 | print >>sys.stderr, "%s: need exactly 1 argument: SCHEMA" % argv[0] | |
273 | sys.exit(1) | |
274 | ||
275 | schema = qapi.QAPISchema(argv[1]) | |
bc52d03f MA |
276 | if not qapi.doc_required: |
277 | print >>sys.stderr, ("%s: need pragma 'doc-required' " | |
278 | "to generate documentation" % argv[0]) | |
aa964b7f | 279 | print texi_schema(schema) |
3313b612 MAL |
280 | |
281 | ||
ef801a9b | 282 | if __name__ == '__main__': |
3313b612 | 283 | main(sys.argv) |