Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 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 | |
Marc-André Lureau | 597494a | 2017-01-25 17:03:07 +0400 | [diff] [blame] | 12 | MSG_FMT = """ |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 13 | @deftypefn {type} {{}} {name} |
| 14 | |
| 15 | {body} |
| 16 | |
| 17 | @end deftypefn |
| 18 | |
| 19 | """.format |
| 20 | |
Marc-André Lureau | 597494a | 2017-01-25 17:03:07 +0400 | [diff] [blame] | 21 | TYPE_FMT = """ |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 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}""" |
Markus Armbruster | c32617a | 2017-03-20 14:11:55 +0100 | [diff] [blame] | 38 | return re.sub(r'\*([^*\n]+)\*', r'@strong{\1}', doc) |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 39 | |
| 40 | |
| 41 | def subst_emph(doc): |
| 42 | """Replaces _foo_ by @emph{foo}""" |
Markus Armbruster | c32617a | 2017-03-20 14:11:55 +0100 | [diff] [blame] | 43 | return re.sub(r'\b_([^_\n]+)_\b', r'@emph{\1}', doc) |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 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 @{ @}""" |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 53 | return doc.replace('{', '@{').replace('}', '@}') |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 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) |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 82 | inlist = '' |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 83 | lastempty = False |
| 84 | for line in doc.split('\n'): |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 85 | empty = line == '' |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 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 |
Philippe Mathieu-Daudé | b3125e7 | 2017-07-28 19:46:03 -0300 | [diff] [blame] | 94 | # docs/devel/qapi-code-gen.txt when fixing this. |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 95 | if line.startswith('| '): |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 96 | line = EXAMPLE_FMT(code=line[2:]) |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 97 | elif line.startswith('= '): |
| 98 | line = '@section ' + line[2:] |
| 99 | elif line.startswith('== '): |
| 100 | line = '@subsection ' + line[3:] |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 101 | elif re.match(r'^([0-9]*\.) ', line): |
| 102 | if not inlist: |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 103 | lines.append('@enumerate') |
| 104 | inlist = 'enumerate' |
| 105 | line = line[line.find(' ')+1:] |
| 106 | lines.append('@item') |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 107 | elif re.match(r'^[*-] ', line): |
| 108 | if not inlist: |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 109 | lines.append('@itemize %s' % {'*': '@bullet', |
| 110 | '-': '@minus'}[line[0]]) |
| 111 | inlist = 'itemize' |
| 112 | lines.append('@item') |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 113 | line = line[2:] |
| 114 | elif lastempty and inlist: |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 115 | lines.append('@end %s\n' % inlist) |
| 116 | inlist = '' |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 117 | |
| 118 | lastempty = empty |
| 119 | lines.append(line) |
| 120 | |
| 121 | if inlist: |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 122 | lines.append('@end %s\n' % inlist) |
| 123 | return '\n'.join(lines) |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 124 | |
| 125 | |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 126 | def texi_body(doc): |
| 127 | """Format the main documentation body""" |
| 128 | return texi_format(str(doc.body)) + '\n' |
Markus Armbruster | 860e877 | 2017-03-15 13:57:04 +0100 | [diff] [blame] | 129 | |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 130 | |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 131 | def texi_enum_value(value): |
| 132 | """Format a table of members item for an enumeration value""" |
Markus Armbruster | 71d918a | 2017-03-15 13:57:09 +0100 | [diff] [blame] | 133 | return '@item @code{%s}\n' % value.name |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 134 | |
| 135 | |
Markus Armbruster | 5169cd8 | 2017-03-15 13:57:16 +0100 | [diff] [blame] | 136 | def texi_member(member, suffix=''): |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 137 | """Format a table of members item for an object type member""" |
Markus Armbruster | 691e031 | 2017-03-15 13:57:14 +0100 | [diff] [blame] | 138 | typ = member.type.doc_type() |
Markus Armbruster | 5169cd8 | 2017-03-15 13:57:16 +0100 | [diff] [blame] | 139 | return '@item @code{%s%s%s}%s%s\n' % ( |
Markus Armbruster | 691e031 | 2017-03-15 13:57:14 +0100 | [diff] [blame] | 140 | member.name, |
| 141 | ': ' if typ else '', |
| 142 | typ if typ else '', |
Markus Armbruster | 5169cd8 | 2017-03-15 13:57:16 +0100 | [diff] [blame] | 143 | ' (optional)' if member.optional else '', |
| 144 | suffix) |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 145 | |
| 146 | |
Markus Armbruster | 5169cd8 | 2017-03-15 13:57:16 +0100 | [diff] [blame] | 147 | def texi_members(doc, what, base, variants, member_func): |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 148 | """Format the table of members""" |
| 149 | items = '' |
| 150 | for section in doc.args.itervalues(): |
Markus Armbruster | c19eaa6 | 2017-03-15 13:57:17 +0100 | [diff] [blame] | 151 | # TODO Drop fallbacks when undocumented members are outlawed |
Markus Armbruster | 5da19f1 | 2017-03-15 13:57:11 +0100 | [diff] [blame] | 152 | if section.content: |
Markus Armbruster | c19eaa6 | 2017-03-15 13:57:17 +0100 | [diff] [blame] | 153 | desc = texi_format(str(section)) |
| 154 | elif (variants and variants.tag_member == section.member |
| 155 | and not section.member.type.doc_type()): |
| 156 | values = section.member.type.member_names() |
| 157 | desc = 'One of ' + ', '.join(['@t{"%s"}' % v for v in values]) |
Markus Armbruster | 5da19f1 | 2017-03-15 13:57:11 +0100 | [diff] [blame] | 158 | else: |
| 159 | desc = 'Not documented' |
Markus Armbruster | c19eaa6 | 2017-03-15 13:57:17 +0100 | [diff] [blame] | 160 | items += member_func(section.member) + desc + '\n' |
Markus Armbruster | 88f6346 | 2017-03-15 13:57:15 +0100 | [diff] [blame] | 161 | if base: |
| 162 | items += '@item The members of @code{%s}\n' % base.doc_type() |
Markus Armbruster | 5169cd8 | 2017-03-15 13:57:16 +0100 | [diff] [blame] | 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) |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 174 | if not items: |
| 175 | return '' |
Markus Armbruster | 2a1183c | 2017-03-15 13:57:10 +0100 | [diff] [blame] | 176 | return '\n@b{%s:}\n@table @asis\n%s@end table\n' % (what, items) |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 177 | |
| 178 | |
| 179 | def texi_sections(doc): |
| 180 | """Format additional sections following arguments""" |
| 181 | body = '' |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 182 | for section in doc.sections: |
| 183 | name, doc = (section.name, str(section)) |
| 184 | func = texi_format |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 185 | if name.startswith('Example'): |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 186 | func = texi_example |
| 187 | |
| 188 | if name: |
Marc-André Lureau | 1ede77d | 2017-02-17 13:34:16 +0400 | [diff] [blame] | 189 | # prefer @b over @strong, so txt doesn't translate it to *Foo:* |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 190 | body += '\n\n@b{%s:}\n' % name |
Marc-André Lureau | 1ede77d | 2017-02-17 13:34:16 +0400 | [diff] [blame] | 191 | |
| 192 | body += func(doc) |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 193 | return body |
| 194 | |
| 195 | |
Markus Armbruster | 5169cd8 | 2017-03-15 13:57:16 +0100 | [diff] [blame] | 196 | def texi_entity(doc, what, base=None, variants=None, |
| 197 | member_func=texi_member): |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 198 | return (texi_body(doc) |
Markus Armbruster | 5169cd8 | 2017-03-15 13:57:16 +0100 | [diff] [blame] | 199 | + texi_members(doc, what, base, variants, member_func) |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 200 | + texi_sections(doc)) |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 201 | |
| 202 | |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 203 | class QAPISchemaGenDocVisitor(qapi.QAPISchemaVisitor): |
| 204 | def __init__(self): |
| 205 | self.out = None |
| 206 | self.cur_doc = None |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 207 | |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 208 | def visit_begin(self, schema): |
| 209 | self.out = '' |
| 210 | |
| 211 | def visit_enum_type(self, name, info, values, prefix): |
| 212 | doc = self.cur_doc |
| 213 | if self.out: |
| 214 | self.out += '\n' |
| 215 | self.out += TYPE_FMT(type='Enum', |
| 216 | name=doc.symbol, |
Markus Armbruster | 2a1183c | 2017-03-15 13:57:10 +0100 | [diff] [blame] | 217 | body=texi_entity(doc, 'Values', |
Markus Armbruster | 2c99f5f | 2017-03-15 13:57:12 +0100 | [diff] [blame] | 218 | member_func=texi_enum_value)) |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 219 | |
| 220 | def visit_object_type(self, name, info, base, members, variants): |
| 221 | doc = self.cur_doc |
Markus Armbruster | 88f6346 | 2017-03-15 13:57:15 +0100 | [diff] [blame] | 222 | if base and base.is_implicit(): |
| 223 | base = None |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 224 | if self.out: |
| 225 | self.out += '\n' |
Markus Armbruster | 75b5019 | 2017-03-15 13:57:18 +0100 | [diff] [blame] | 226 | self.out += TYPE_FMT(type='Object', |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 227 | name=doc.symbol, |
Markus Armbruster | 5169cd8 | 2017-03-15 13:57:16 +0100 | [diff] [blame] | 228 | body=texi_entity(doc, 'Members', base, variants)) |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 229 | |
| 230 | def visit_alternate_type(self, name, info, variants): |
| 231 | doc = self.cur_doc |
| 232 | if self.out: |
| 233 | self.out += '\n' |
| 234 | self.out += TYPE_FMT(type='Alternate', |
| 235 | name=doc.symbol, |
Markus Armbruster | 2a1183c | 2017-03-15 13:57:10 +0100 | [diff] [blame] | 236 | body=texi_entity(doc, 'Members')) |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 237 | |
| 238 | def visit_command(self, name, info, arg_type, ret_type, |
| 239 | gen, success_response, boxed): |
| 240 | doc = self.cur_doc |
| 241 | if self.out: |
| 242 | self.out += '\n' |
Markus Armbruster | c2dd311 | 2017-03-15 13:57:13 +0100 | [diff] [blame] | 243 | if boxed: |
| 244 | body = texi_body(doc) |
| 245 | body += '\n@b{Arguments:} the members of @code{%s}' % arg_type.name |
| 246 | body += texi_sections(doc) |
| 247 | else: |
| 248 | body = texi_entity(doc, 'Arguments') |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 249 | self.out += MSG_FMT(type='Command', |
| 250 | name=doc.symbol, |
Markus Armbruster | c2dd311 | 2017-03-15 13:57:13 +0100 | [diff] [blame] | 251 | body=body) |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 252 | |
| 253 | def visit_event(self, name, info, arg_type, boxed): |
| 254 | doc = self.cur_doc |
| 255 | if self.out: |
| 256 | self.out += '\n' |
| 257 | self.out += MSG_FMT(type='Event', |
| 258 | name=doc.symbol, |
Markus Armbruster | 2a1183c | 2017-03-15 13:57:10 +0100 | [diff] [blame] | 259 | body=texi_entity(doc, 'Arguments')) |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 260 | |
| 261 | def symbol(self, doc, entity): |
| 262 | self.cur_doc = doc |
| 263 | entity.visit(self) |
| 264 | self.cur_doc = None |
| 265 | |
| 266 | def freeform(self, doc): |
| 267 | assert not doc.args |
| 268 | if self.out: |
| 269 | self.out += '\n' |
| 270 | self.out += texi_body(doc) + texi_sections(doc) |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 271 | |
| 272 | |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 273 | def texi_schema(schema): |
| 274 | """Convert QAPI schema documentation to Texinfo""" |
| 275 | gen = QAPISchemaGenDocVisitor() |
| 276 | gen.visit_begin(schema) |
| 277 | for doc in schema.docs: |
| 278 | if doc.symbol: |
| 279 | gen.symbol(doc, schema.lookup_entity(doc.symbol)) |
| 280 | else: |
| 281 | gen.freeform(doc) |
| 282 | return gen.out |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 283 | |
| 284 | |
| 285 | def main(argv): |
| 286 | """Takes schema argument, prints result to stdout""" |
| 287 | if len(argv) != 2: |
| 288 | print >>sys.stderr, "%s: need exactly 1 argument: SCHEMA" % argv[0] |
| 289 | sys.exit(1) |
| 290 | |
| 291 | schema = qapi.QAPISchema(argv[1]) |
Markus Armbruster | bc52d03 | 2017-03-15 13:56:51 +0100 | [diff] [blame] | 292 | if not qapi.doc_required: |
| 293 | print >>sys.stderr, ("%s: need pragma 'doc-required' " |
| 294 | "to generate documentation" % argv[0]) |
Markus Armbruster | e8ba07e | 2017-03-20 14:11:51 +0100 | [diff] [blame] | 295 | sys.exit(1) |
Markus Armbruster | aa964b7 | 2017-03-15 13:57:05 +0100 | [diff] [blame] | 296 | print texi_schema(schema) |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 297 | |
| 298 | |
Markus Armbruster | ef801a9 | 2017-03-15 13:57:08 +0100 | [diff] [blame] | 299 | if __name__ == '__main__': |
Marc-André Lureau | 3313b61 | 2017-01-13 15:41:29 +0100 | [diff] [blame] | 300 | main(sys.argv) |