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 | |
| 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) |