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