]>
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: | |
1ede77df MAL |
162 | # prefer @b over @strong, so txt doesn't translate it to *Foo:* |
163 | body += "\n\n@b{%s:}\n" % name | |
164 | ||
165 | body += func(doc) | |
3313b612 MAL |
166 | |
167 | return body | |
168 | ||
169 | ||
170 | def texi_alternate(expr, doc): | |
171 | """Format an alternate to texi""" | |
172 | body = texi_body(doc) | |
173 | return STRUCT_FMT(type="Alternate", | |
174 | name=doc.symbol, | |
175 | body=body) | |
176 | ||
177 | ||
178 | def texi_union(expr, doc): | |
179 | """Format a union to texi""" | |
180 | discriminator = expr.get("discriminator") | |
181 | if discriminator: | |
182 | union = "Flat Union" | |
183 | else: | |
184 | union = "Simple Union" | |
185 | ||
186 | body = texi_body(doc) | |
187 | return STRUCT_FMT(type=union, | |
188 | name=doc.symbol, | |
189 | body=body) | |
190 | ||
191 | ||
192 | def texi_enum(expr, doc): | |
193 | """Format an enum to texi""" | |
194 | for i in expr['data']: | |
195 | if i not in doc.args: | |
196 | doc.args[i] = '' | |
197 | body = texi_body(doc) | |
198 | return ENUM_FMT(name=doc.symbol, | |
199 | body=body) | |
200 | ||
201 | ||
202 | def texi_struct(expr, doc): | |
203 | """Format a struct to texi""" | |
204 | body = texi_body(doc) | |
205 | return STRUCT_FMT(type="Struct", | |
206 | name=doc.symbol, | |
207 | body=body) | |
208 | ||
209 | ||
210 | def texi_command(expr, doc): | |
211 | """Format a command to texi""" | |
212 | body = texi_body(doc) | |
213 | return COMMAND_FMT(type="Command", | |
214 | name=doc.symbol, | |
215 | body=body) | |
216 | ||
217 | ||
218 | def texi_event(expr, doc): | |
219 | """Format an event to texi""" | |
220 | body = texi_body(doc) | |
221 | return COMMAND_FMT(type="Event", | |
222 | name=doc.symbol, | |
223 | body=body) | |
224 | ||
225 | ||
226 | def texi_expr(expr, doc): | |
227 | """Format an expr to texi""" | |
228 | (kind, _) = expr.items()[0] | |
229 | ||
230 | fmt = {"command": texi_command, | |
231 | "struct": texi_struct, | |
232 | "enum": texi_enum, | |
233 | "union": texi_union, | |
234 | "alternate": texi_alternate, | |
235 | "event": texi_event}[kind] | |
236 | ||
237 | return fmt(expr, doc) | |
238 | ||
239 | ||
240 | def texi(docs): | |
241 | """Convert QAPI schema expressions to texi documentation""" | |
242 | res = [] | |
243 | for doc in docs: | |
244 | expr = doc.expr | |
245 | if not expr: | |
246 | res.append(texi_body(doc)) | |
247 | continue | |
248 | try: | |
249 | doc = texi_expr(expr, doc) | |
250 | res.append(doc) | |
251 | except: | |
252 | print >>sys.stderr, "error at @%s" % doc.info | |
253 | raise | |
254 | ||
255 | return '\n'.join(res) | |
256 | ||
257 | ||
258 | def main(argv): | |
259 | """Takes schema argument, prints result to stdout""" | |
260 | if len(argv) != 2: | |
261 | print >>sys.stderr, "%s: need exactly 1 argument: SCHEMA" % argv[0] | |
262 | sys.exit(1) | |
263 | ||
264 | schema = qapi.QAPISchema(argv[1]) | |
265 | print texi(schema.docs) | |
266 | ||
267 | ||
268 | if __name__ == "__main__": | |
269 | main(sys.argv) |