]>
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 | ||
12 | COMMAND_FMT = """ | |
13 | @deftypefn {type} {{}} {name} | |
14 | ||
15 | {body} | |
16 | ||
17 | @end deftypefn | |
18 | ||
19 | """.format | |
20 | ||
21 | ENUM_FMT = """ | |
22 | @deftp Enum {name} | |
23 | ||
24 | {body} | |
25 | ||
26 | @end deftp | |
27 | ||
28 | """.format | |
29 | ||
30 | STRUCT_FMT = """ | |
31 | @deftp {{{type}}} {name} | |
32 | ||
33 | {body} | |
34 | ||
35 | @end deftp | |
36 | ||
37 | """.format | |
38 | ||
39 | EXAMPLE_FMT = """@example | |
40 | {code} | |
41 | @end example | |
42 | """.format | |
43 | ||
44 | ||
45 | def subst_strong(doc): | |
46 | """Replaces *foo* by @strong{foo}""" | |
47 | return re.sub(r'\*([^*\n]+)\*', r'@emph{\1}', doc) | |
48 | ||
49 | ||
50 | def subst_emph(doc): | |
51 | """Replaces _foo_ by @emph{foo}""" | |
52 | return re.sub(r'\b_([^_\n]+)_\b', r' @emph{\1} ', doc) | |
53 | ||
54 | ||
55 | def subst_vars(doc): | |
56 | """Replaces @var by @code{var}""" | |
57 | return re.sub(r'@([\w-]+)', r'@code{\1}', doc) | |
58 | ||
59 | ||
60 | def subst_braces(doc): | |
61 | """Replaces {} with @{ @}""" | |
62 | return doc.replace("{", "@{").replace("}", "@}") | |
63 | ||
64 | ||
65 | def texi_example(doc): | |
66 | """Format @example""" | |
67 | # TODO: Neglects to escape @ characters. | |
68 | # We should probably escape them in subst_braces(), and rename the | |
69 | # function to subst_special() or subs_texi_special(). If we do that, we | |
70 | # need to delay it until after subst_vars() in texi_format(). | |
71 | doc = subst_braces(doc).strip('\n') | |
72 | return EXAMPLE_FMT(code=doc) | |
73 | ||
74 | ||
75 | def texi_format(doc): | |
76 | """ | |
77 | Format documentation | |
78 | ||
79 | Lines starting with: | |
80 | - |: generates an @example | |
81 | - =: generates @section | |
82 | - ==: generates @subsection | |
83 | - 1. or 1): generates an @enumerate @item | |
84 | - */-: generates an @itemize list | |
85 | """ | |
86 | lines = [] | |
87 | doc = subst_braces(doc) | |
88 | doc = subst_vars(doc) | |
89 | doc = subst_emph(doc) | |
90 | doc = subst_strong(doc) | |
91 | inlist = "" | |
92 | lastempty = False | |
93 | for line in doc.split('\n'): | |
94 | empty = line == "" | |
95 | ||
96 | # FIXME: Doing this in a single if / elif chain is | |
97 | # problematic. For instance, a line without markup terminates | |
98 | # a list if it follows a blank line (reaches the final elif), | |
99 | # but a line with some *other* markup, such as a = title | |
100 | # doesn't. | |
101 | # | |
102 | # Make sure to update section "Documentation markup" in | |
103 | # docs/qapi-code-gen.txt when fixing this. | |
104 | if line.startswith("| "): | |
105 | line = EXAMPLE_FMT(code=line[2:]) | |
106 | elif line.startswith("= "): | |
107 | line = "@section " + line[2:] | |
108 | elif line.startswith("== "): | |
109 | line = "@subsection " + line[3:] | |
110 | elif re.match(r'^([0-9]*\.) ', line): | |
111 | if not inlist: | |
112 | lines.append("@enumerate") | |
113 | inlist = "enumerate" | |
114 | line = line[line.find(" ")+1:] | |
115 | lines.append("@item") | |
116 | elif re.match(r'^[*-] ', line): | |
117 | if not inlist: | |
118 | lines.append("@itemize %s" % {'*': "@bullet", | |
119 | '-': "@minus"}[line[0]]) | |
120 | inlist = "itemize" | |
121 | lines.append("@item") | |
122 | line = line[2:] | |
123 | elif lastempty and inlist: | |
124 | lines.append("@end %s\n" % inlist) | |
125 | inlist = "" | |
126 | ||
127 | lastempty = empty | |
128 | lines.append(line) | |
129 | ||
130 | if inlist: | |
131 | lines.append("@end %s\n" % inlist) | |
132 | return "\n".join(lines) | |
133 | ||
134 | ||
135 | def texi_body(doc): | |
136 | """ | |
137 | Format the body of a symbol documentation: | |
138 | - main body | |
139 | - table of arguments | |
140 | - followed by "Returns/Notes/Since/Example" sections | |
141 | """ | |
142 | body = texi_format(str(doc.body)) + "\n" | |
143 | if doc.args: | |
144 | body += "@table @asis\n" | |
145 | for arg, section in doc.args.iteritems(): | |
146 | desc = str(section) | |
147 | opt = '' | |
148 | if "#optional" in desc: | |
149 | desc = desc.replace("#optional", "") | |
150 | opt = ' (optional)' | |
151 | body += "@item @code{'%s'}%s\n%s\n" % (arg, opt, | |
152 | texi_format(desc)) | |
153 | body += "@end table\n" | |
154 | ||
155 | for section in doc.sections: | |
156 | name, doc = (section.name, str(section)) | |
157 | func = texi_format | |
158 | if name.startswith("Example"): | |
159 | func = texi_example | |
160 | ||
161 | if name: | |
162 | # FIXME the indentation produced by @quotation in .txt and | |
163 | # .html output is confusing | |
164 | body += "\n@quotation %s\n%s\n@end quotation" % \ | |
165 | (name, func(doc)) | |
166 | else: | |
167 | body += func(doc) | |
168 | ||
169 | return body | |
170 | ||
171 | ||
172 | def texi_alternate(expr, doc): | |
173 | """Format an alternate to texi""" | |
174 | body = texi_body(doc) | |
175 | return STRUCT_FMT(type="Alternate", | |
176 | name=doc.symbol, | |
177 | body=body) | |
178 | ||
179 | ||
180 | def texi_union(expr, doc): | |
181 | """Format a union to texi""" | |
182 | discriminator = expr.get("discriminator") | |
183 | if discriminator: | |
184 | union = "Flat Union" | |
185 | else: | |
186 | union = "Simple Union" | |
187 | ||
188 | body = texi_body(doc) | |
189 | return STRUCT_FMT(type=union, | |
190 | name=doc.symbol, | |
191 | body=body) | |
192 | ||
193 | ||
194 | def texi_enum(expr, doc): | |
195 | """Format an enum to texi""" | |
196 | for i in expr['data']: | |
197 | if i not in doc.args: | |
198 | doc.args[i] = '' | |
199 | body = texi_body(doc) | |
200 | return ENUM_FMT(name=doc.symbol, | |
201 | body=body) | |
202 | ||
203 | ||
204 | def texi_struct(expr, doc): | |
205 | """Format a struct to texi""" | |
206 | body = texi_body(doc) | |
207 | return STRUCT_FMT(type="Struct", | |
208 | name=doc.symbol, | |
209 | body=body) | |
210 | ||
211 | ||
212 | def texi_command(expr, doc): | |
213 | """Format a command to texi""" | |
214 | body = texi_body(doc) | |
215 | return COMMAND_FMT(type="Command", | |
216 | name=doc.symbol, | |
217 | body=body) | |
218 | ||
219 | ||
220 | def texi_event(expr, doc): | |
221 | """Format an event to texi""" | |
222 | body = texi_body(doc) | |
223 | return COMMAND_FMT(type="Event", | |
224 | name=doc.symbol, | |
225 | body=body) | |
226 | ||
227 | ||
228 | def texi_expr(expr, doc): | |
229 | """Format an expr to texi""" | |
230 | (kind, _) = expr.items()[0] | |
231 | ||
232 | fmt = {"command": texi_command, | |
233 | "struct": texi_struct, | |
234 | "enum": texi_enum, | |
235 | "union": texi_union, | |
236 | "alternate": texi_alternate, | |
237 | "event": texi_event}[kind] | |
238 | ||
239 | return fmt(expr, doc) | |
240 | ||
241 | ||
242 | def texi(docs): | |
243 | """Convert QAPI schema expressions to texi documentation""" | |
244 | res = [] | |
245 | for doc in docs: | |
246 | expr = doc.expr | |
247 | if not expr: | |
248 | res.append(texi_body(doc)) | |
249 | continue | |
250 | try: | |
251 | doc = texi_expr(expr, doc) | |
252 | res.append(doc) | |
253 | except: | |
254 | print >>sys.stderr, "error at @%s" % doc.info | |
255 | raise | |
256 | ||
257 | return '\n'.join(res) | |
258 | ||
259 | ||
260 | def main(argv): | |
261 | """Takes schema argument, prints result to stdout""" | |
262 | if len(argv) != 2: | |
263 | print >>sys.stderr, "%s: need exactly 1 argument: SCHEMA" % argv[0] | |
264 | sys.exit(1) | |
265 | ||
266 | schema = qapi.QAPISchema(argv[1]) | |
267 | print texi(schema.docs) | |
268 | ||
269 | ||
270 | if __name__ == "__main__": | |
271 | main(sys.argv) |