]>
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 @{ @}""" | |
53 | return doc.replace("{", "@{").replace("}", "@}") | |
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) | |
82 | inlist = "" | |
83 | lastempty = False | |
84 | for line in doc.split('\n'): | |
85 | empty = line == "" | |
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. | |
95 | if line.startswith("| "): | |
96 | line = EXAMPLE_FMT(code=line[2:]) | |
97 | elif line.startswith("= "): | |
98 | line = "@section " + line[2:] | |
99 | elif line.startswith("== "): | |
100 | line = "@subsection " + line[3:] | |
101 | elif re.match(r'^([0-9]*\.) ', line): | |
102 | if not inlist: | |
103 | lines.append("@enumerate") | |
104 | inlist = "enumerate" | |
105 | line = line[line.find(" ")+1:] | |
106 | lines.append("@item") | |
107 | elif re.match(r'^[*-] ', line): | |
108 | if not inlist: | |
109 | lines.append("@itemize %s" % {'*': "@bullet", | |
110 | '-': "@minus"}[line[0]]) | |
111 | inlist = "itemize" | |
112 | lines.append("@item") | |
113 | line = line[2:] | |
114 | elif lastempty and inlist: | |
115 | lines.append("@end %s\n" % inlist) | |
116 | inlist = "" | |
117 | ||
118 | lastempty = empty | |
119 | lines.append(line) | |
120 | ||
121 | if inlist: | |
122 | lines.append("@end %s\n" % inlist) | |
123 | return "\n".join(lines) | |
124 | ||
125 | ||
126 | def texi_body(doc): | |
127 | """ | |
128 | Format the body of a symbol documentation: | |
129 | - main body | |
130 | - table of arguments | |
131 | - followed by "Returns/Notes/Since/Example" sections | |
132 | """ | |
133 | body = texi_format(str(doc.body)) + "\n" | |
134 | if doc.args: | |
135 | body += "@table @asis\n" | |
136 | for arg, section in doc.args.iteritems(): | |
137 | desc = str(section) | |
138 | opt = '' | |
139 | if "#optional" in desc: | |
140 | desc = desc.replace("#optional", "") | |
141 | opt = ' (optional)' | |
142 | body += "@item @code{'%s'}%s\n%s\n" % (arg, opt, | |
143 | texi_format(desc)) | |
144 | body += "@end table\n" | |
145 | ||
146 | for section in doc.sections: | |
147 | name, doc = (section.name, str(section)) | |
148 | func = texi_format | |
149 | if name.startswith("Example"): | |
150 | func = texi_example | |
151 | ||
152 | if name: | |
1ede77df MAL |
153 | # prefer @b over @strong, so txt doesn't translate it to *Foo:* |
154 | body += "\n\n@b{%s:}\n" % name | |
155 | ||
156 | body += func(doc) | |
3313b612 MAL |
157 | |
158 | return body | |
159 | ||
160 | ||
161 | def texi_alternate(expr, doc): | |
162 | """Format an alternate to texi""" | |
163 | body = texi_body(doc) | |
597494ab MAL |
164 | return TYPE_FMT(type="Alternate", |
165 | name=doc.symbol, | |
166 | body=body) | |
3313b612 MAL |
167 | |
168 | ||
169 | def texi_union(expr, doc): | |
170 | """Format a union to texi""" | |
171 | discriminator = expr.get("discriminator") | |
172 | if discriminator: | |
173 | union = "Flat Union" | |
174 | else: | |
175 | union = "Simple Union" | |
176 | ||
177 | body = texi_body(doc) | |
597494ab MAL |
178 | return TYPE_FMT(type=union, |
179 | name=doc.symbol, | |
180 | body=body) | |
3313b612 MAL |
181 | |
182 | ||
183 | def texi_enum(expr, doc): | |
184 | """Format an enum to texi""" | |
185 | for i in expr['data']: | |
186 | if i not in doc.args: | |
187 | doc.args[i] = '' | |
188 | body = texi_body(doc) | |
597494ab MAL |
189 | return TYPE_FMT(type="Enum", |
190 | name=doc.symbol, | |
3313b612 MAL |
191 | body=body) |
192 | ||
193 | ||
194 | def texi_struct(expr, doc): | |
195 | """Format a struct to texi""" | |
196 | body = texi_body(doc) | |
597494ab MAL |
197 | return TYPE_FMT(type="Struct", |
198 | name=doc.symbol, | |
199 | body=body) | |
3313b612 MAL |
200 | |
201 | ||
202 | def texi_command(expr, doc): | |
203 | """Format a command to texi""" | |
204 | body = texi_body(doc) | |
597494ab MAL |
205 | return MSG_FMT(type="Command", |
206 | name=doc.symbol, | |
207 | body=body) | |
3313b612 MAL |
208 | |
209 | ||
210 | def texi_event(expr, doc): | |
211 | """Format an event to texi""" | |
212 | body = texi_body(doc) | |
597494ab MAL |
213 | return MSG_FMT(type="Event", |
214 | name=doc.symbol, | |
215 | body=body) | |
3313b612 MAL |
216 | |
217 | ||
218 | def texi_expr(expr, doc): | |
219 | """Format an expr to texi""" | |
220 | (kind, _) = expr.items()[0] | |
221 | ||
222 | fmt = {"command": texi_command, | |
223 | "struct": texi_struct, | |
224 | "enum": texi_enum, | |
225 | "union": texi_union, | |
226 | "alternate": texi_alternate, | |
227 | "event": texi_event}[kind] | |
228 | ||
229 | return fmt(expr, doc) | |
230 | ||
231 | ||
232 | def texi(docs): | |
233 | """Convert QAPI schema expressions to texi documentation""" | |
234 | res = [] | |
235 | for doc in docs: | |
236 | expr = doc.expr | |
237 | if not expr: | |
238 | res.append(texi_body(doc)) | |
239 | continue | |
240 | try: | |
241 | doc = texi_expr(expr, doc) | |
242 | res.append(doc) | |
243 | except: | |
244 | print >>sys.stderr, "error at @%s" % doc.info | |
245 | raise | |
246 | ||
247 | return '\n'.join(res) | |
248 | ||
249 | ||
250 | def main(argv): | |
251 | """Takes schema argument, prints result to stdout""" | |
252 | if len(argv) != 2: | |
253 | print >>sys.stderr, "%s: need exactly 1 argument: SCHEMA" % argv[0] | |
254 | sys.exit(1) | |
255 | ||
256 | schema = qapi.QAPISchema(argv[1]) | |
257 | print texi(schema.docs) | |
258 | ||
259 | ||
260 | if __name__ == "__main__": | |
261 | main(sys.argv) |