From 3313b6124b524893683311e01437a82b40784e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= Date: Fri, 13 Jan 2017 15:41:29 +0100 Subject: qapi: add qapi2texi script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As the name suggests, the qapi2texi script converts JSON QAPI description into a texi file suitable for different target formats (info/man/txt/pdf/html...). It parses the following kind of blocks: Free-form: ## # = Section # == Subsection # # Some text foo with *emphasis* # 1. with a list # 2. like that # # And some code: # | $ echo foo # | -> do this # | <- get that # ## Symbol description: ## # @symbol: # # Symbol body ditto ergo sum. Foo bar # baz ding. # # @param1: the frob to frobnicate # @param2: #optional how hard to frobnicate # # Returns: the frobnicated frob. # If frob isn't frobnicatable, GenericError. # # Since: version # Notes: notes, comments can have # - itemized list # - like this # # Example: # # -> { "execute": "quit" } # <- { "return": {} } # ## That's roughly following the following EBNF grammar: api_comment = "##\n" comment "##\n" comment = freeform_comment | symbol_comment freeform_comment = { "# " text "\n" | "#\n" } symbol_comment = "# @" name ":\n" { member | tag_section | freeform_comment } member = "# @" name ':' [ text ] "\n" freeform_comment tag_section = "# " ( "Returns:", "Since:", "Note:", "Notes:", "Example:", "Examples:" ) [ text ] "\n" freeform_comment text = free text with markup Note that the grammar is ambiguous: a line "# @foo:\n" can be parsed both as freeform_comment and as symbol_comment. The actual parser recognizes symbol_comment. See docs/qapi-code-gen.txt for more details. Deficiencies and limitations: - the generated QMP documentation includes internal types - union type support is lacking - type information is lacking in generated documentation - doc comment error message positions are imprecise, they point to the beginning of the comment. - a few minor issues, all marked TODO/FIXME in the code Signed-off-by: Marc-André Lureau Message-Id: <20170113144135.5150-16-marcandre.lureau@redhat.com> Reviewed-by: Markus Armbruster [test-qapi.py tweaked to avoid trailing empty lines in .out] Signed-off-by: Markus Armbruster --- scripts/qapi2texi.py | 271 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100755 scripts/qapi2texi.py (limited to 'scripts/qapi2texi.py') diff --git a/scripts/qapi2texi.py b/scripts/qapi2texi.py new file mode 100755 index 0000000000..83ded95c2d --- /dev/null +++ b/scripts/qapi2texi.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python +# QAPI texi generator +# +# This work is licensed under the terms of the GNU LGPL, version 2+. +# See the COPYING file in the top-level directory. +"""This script produces the documentation of a qapi schema in texinfo format""" +import re +import sys + +import qapi + +COMMAND_FMT = """ +@deftypefn {type} {{}} {name} + +{body} + +@end deftypefn + +""".format + +ENUM_FMT = """ +@deftp Enum {name} + +{body} + +@end deftp + +""".format + +STRUCT_FMT = """ +@deftp {{{type}}} {name} + +{body} + +@end deftp + +""".format + +EXAMPLE_FMT = """@example +{code} +@end example +""".format + + +def subst_strong(doc): + """Replaces *foo* by @strong{foo}""" + return re.sub(r'\*([^*\n]+)\*', r'@emph{\1}', doc) + + +def subst_emph(doc): + """Replaces _foo_ by @emph{foo}""" + return re.sub(r'\b_([^_\n]+)_\b', r' @emph{\1} ', doc) + + +def subst_vars(doc): + """Replaces @var by @code{var}""" + return re.sub(r'@([\w-]+)', r'@code{\1}', doc) + + +def subst_braces(doc): + """Replaces {} with @{ @}""" + return doc.replace("{", "@{").replace("}", "@}") + + +def texi_example(doc): + """Format @example""" + # TODO: Neglects to escape @ characters. + # We should probably escape them in subst_braces(), and rename the + # function to subst_special() or subs_texi_special(). If we do that, we + # need to delay it until after subst_vars() in texi_format(). + doc = subst_braces(doc).strip('\n') + return EXAMPLE_FMT(code=doc) + + +def texi_format(doc): + """ + Format documentation + + Lines starting with: + - |: generates an @example + - =: generates @section + - ==: generates @subsection + - 1. or 1): generates an @enumerate @item + - */-: generates an @itemize list + """ + lines = [] + doc = subst_braces(doc) + doc = subst_vars(doc) + doc = subst_emph(doc) + doc = subst_strong(doc) + inlist = "" + lastempty = False + for line in doc.split('\n'): + empty = line == "" + + # FIXME: Doing this in a single if / elif chain is + # problematic. For instance, a line without markup terminates + # a list if it follows a blank line (reaches the final elif), + # but a line with some *other* markup, such as a = title + # doesn't. + # + # Make sure to update section "Documentation markup" in + # docs/qapi-code-gen.txt when fixing this. + if line.startswith("| "): + line = EXAMPLE_FMT(code=line[2:]) + elif line.startswith("= "): + line = "@section " + line[2:] + elif line.startswith("== "): + line = "@subsection " + line[3:] + elif re.match(r'^([0-9]*\.) ', line): + if not inlist: + lines.append("@enumerate") + inlist = "enumerate" + line = line[line.find(" ")+1:] + lines.append("@item") + elif re.match(r'^[*-] ', line): + if not inlist: + lines.append("@itemize %s" % {'*': "@bullet", + '-': "@minus"}[line[0]]) + inlist = "itemize" + lines.append("@item") + line = line[2:] + elif lastempty and inlist: + lines.append("@end %s\n" % inlist) + inlist = "" + + lastempty = empty + lines.append(line) + + if inlist: + lines.append("@end %s\n" % inlist) + return "\n".join(lines) + + +def texi_body(doc): + """ + Format the body of a symbol documentation: + - main body + - table of arguments + - followed by "Returns/Notes/Since/Example" sections + """ + body = texi_format(str(doc.body)) + "\n" + if doc.args: + body += "@table @asis\n" + for arg, section in doc.args.iteritems(): + desc = str(section) + opt = '' + if "#optional" in desc: + desc = desc.replace("#optional", "") + opt = ' (optional)' + body += "@item @code{'%s'}%s\n%s\n" % (arg, opt, + texi_format(desc)) + body += "@end table\n" + + for section in doc.sections: + name, doc = (section.name, str(section)) + func = texi_format + if name.startswith("Example"): + func = texi_example + + if name: + # FIXME the indentation produced by @quotation in .txt and + # .html output is confusing + body += "\n@quotation %s\n%s\n@end quotation" % \ + (name, func(doc)) + else: + body += func(doc) + + return body + + +def texi_alternate(expr, doc): + """Format an alternate to texi""" + body = texi_body(doc) + return STRUCT_FMT(type="Alternate", + name=doc.symbol, + body=body) + + +def texi_union(expr, doc): + """Format a union to texi""" + discriminator = expr.get("discriminator") + if discriminator: + union = "Flat Union" + else: + union = "Simple Union" + + body = texi_body(doc) + return STRUCT_FMT(type=union, + name=doc.symbol, + body=body) + + +def texi_enum(expr, doc): + """Format an enum to texi""" + for i in expr['data']: + if i not in doc.args: + doc.args[i] = '' + body = texi_body(doc) + return ENUM_FMT(name=doc.symbol, + body=body) + + +def texi_struct(expr, doc): + """Format a struct to texi""" + body = texi_body(doc) + return STRUCT_FMT(type="Struct", + name=doc.symbol, + body=body) + + +def texi_command(expr, doc): + """Format a command to texi""" + body = texi_body(doc) + return COMMAND_FMT(type="Command", + name=doc.symbol, + body=body) + + +def texi_event(expr, doc): + """Format an event to texi""" + body = texi_body(doc) + return COMMAND_FMT(type="Event", + name=doc.symbol, + body=body) + + +def texi_expr(expr, doc): + """Format an expr to texi""" + (kind, _) = expr.items()[0] + + fmt = {"command": texi_command, + "struct": texi_struct, + "enum": texi_enum, + "union": texi_union, + "alternate": texi_alternate, + "event": texi_event}[kind] + + return fmt(expr, doc) + + +def texi(docs): + """Convert QAPI schema expressions to texi documentation""" + res = [] + for doc in docs: + expr = doc.expr + if not expr: + res.append(texi_body(doc)) + continue + try: + doc = texi_expr(expr, doc) + res.append(doc) + except: + print >>sys.stderr, "error at @%s" % doc.info + raise + + return '\n'.join(res) + + +def main(argv): + """Takes schema argument, prints result to stdout""" + if len(argv) != 2: + print >>sys.stderr, "%s: need exactly 1 argument: SCHEMA" % argv[0] + sys.exit(1) + + schema = qapi.QAPISchema(argv[1]) + print texi(schema.docs) + + +if __name__ == "__main__": + main(sys.argv) -- cgit v1.2.3