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