14078ee54SPeter Maydell# coding=utf-8 24078ee54SPeter Maydell# 34078ee54SPeter Maydell# QEMU qapidoc QAPI file parsing extension 44078ee54SPeter Maydell# 54078ee54SPeter Maydell# Copyright (c) 2020 Linaro 64078ee54SPeter Maydell# 74078ee54SPeter Maydell# This work is licensed under the terms of the GNU GPLv2 or later. 84078ee54SPeter Maydell# See the COPYING file in the top-level directory. 94078ee54SPeter Maydell 104078ee54SPeter Maydell""" 114078ee54SPeter Maydellqapidoc is a Sphinx extension that implements the qapi-doc directive 124078ee54SPeter Maydell 134078ee54SPeter MaydellThe purpose of this extension is to read the documentation comments 144078ee54SPeter Maydellin QAPI schema files, and insert them all into the current document. 154078ee54SPeter Maydell 164078ee54SPeter MaydellIt implements one new rST directive, "qapi-doc::". 174078ee54SPeter MaydellEach qapi-doc:: directive takes one argument, which is the 184078ee54SPeter Maydellpathname of the schema file to process, relative to the source tree. 194078ee54SPeter Maydell 204078ee54SPeter MaydellThe docs/conf.py file must set the qapidoc_srctree config value to 214078ee54SPeter Maydellthe root of the QEMU source tree. 224078ee54SPeter Maydell 234078ee54SPeter MaydellThe Sphinx documentation on writing extensions is at: 244078ee54SPeter Maydellhttps://www.sphinx-doc.org/en/master/development/index.html 254078ee54SPeter Maydell""" 264078ee54SPeter Maydell 274078ee54SPeter Maydellimport os 284078ee54SPeter Maydellimport re 2976e375fcSJohn Snowimport sys 30939c639eSJohn Snowimport textwrap 31547864f9SJohn Snowfrom typing import List 324078ee54SPeter Maydell 334078ee54SPeter Maydellfrom docutils import nodes 3436c6dcc2SJohn Snowfrom docutils.parsers.rst import Directive, directives 354078ee54SPeter Maydellfrom docutils.statemachine import ViewList 3636c6dcc2SJohn Snowfrom qapi.error import QAPIError, QAPISemError 3736c6dcc2SJohn Snowfrom qapi.gen import QAPISchemaVisitor 3836c6dcc2SJohn Snowfrom qapi.schema import QAPISchema 3936c6dcc2SJohn Snow 4076e375fcSJohn Snowfrom sphinx import addnodes 41547864f9SJohn Snowfrom sphinx.directives.code import CodeBlock 424078ee54SPeter Maydellfrom sphinx.errors import ExtensionError 434078ee54SPeter Maydellfrom sphinx.util.docutils import switch_source_input 44dd23f9ecSJohn Snowfrom sphinx.util.nodes import nested_parse_with_titles 454078ee54SPeter Maydell 464078ee54SPeter Maydell 4736c6dcc2SJohn Snow__version__ = "1.0" 4836c6dcc2SJohn Snow 4936c6dcc2SJohn Snow 50939c639eSJohn Snowdef dedent(text: str) -> str: 51939c639eSJohn Snow # Adjust indentation to make description text parse as paragraph. 52939c639eSJohn Snow 53939c639eSJohn Snow lines = text.splitlines(True) 54939c639eSJohn Snow if re.match(r"\s+", lines[0]): 55939c639eSJohn Snow # First line is indented; description started on the line after 56939c639eSJohn Snow # the name. dedent the whole block. 57939c639eSJohn Snow return textwrap.dedent(text) 58939c639eSJohn Snow 59939c639eSJohn Snow # Descr started on same line. Dedent line 2+. 60939c639eSJohn Snow return lines[0] + textwrap.dedent("".join(lines[1:])) 61939c639eSJohn Snow 62939c639eSJohn Snow 6336c6dcc2SJohn Snow# Disable black auto-formatter until re-enabled: 6436c6dcc2SJohn Snow# fmt: off 654078ee54SPeter Maydell 664078ee54SPeter Maydell 674078ee54SPeter Maydellclass QAPISchemaGenRSTVisitor(QAPISchemaVisitor): 684078ee54SPeter Maydell """A QAPI schema visitor which generates docutils/Sphinx nodes 694078ee54SPeter Maydell 704078ee54SPeter Maydell This class builds up a tree of docutils/Sphinx nodes corresponding 714078ee54SPeter Maydell to documentation for the various QAPI objects. To use it, first 724078ee54SPeter Maydell create a QAPISchemaGenRSTVisitor object, and call its 734078ee54SPeter Maydell visit_begin() method. Then you can call one of the two methods 744078ee54SPeter Maydell 'freeform' (to add documentation for a freeform documentation 754078ee54SPeter Maydell chunk) or 'symbol' (to add documentation for a QAPI symbol). These 764078ee54SPeter Maydell will cause the visitor to build up the tree of document 774078ee54SPeter Maydell nodes. Once you've added all the documentation via 'freeform' and 784078ee54SPeter Maydell 'symbol' method calls, you can call 'get_document_nodes' to get 794078ee54SPeter Maydell the final list of document nodes (in a form suitable for returning 804078ee54SPeter Maydell from a Sphinx directive's 'run' method). 814078ee54SPeter Maydell """ 824078ee54SPeter Maydell def __init__(self, sphinx_directive): 834078ee54SPeter Maydell self._cur_doc = None 844078ee54SPeter Maydell self._sphinx_directive = sphinx_directive 854078ee54SPeter Maydell self._top_node = nodes.section() 864078ee54SPeter Maydell self._active_headings = [self._top_node] 874078ee54SPeter Maydell 884078ee54SPeter Maydell def _make_dlitem(self, term, defn): 894078ee54SPeter Maydell """Return a dlitem node with the specified term and definition. 904078ee54SPeter Maydell 914078ee54SPeter Maydell term should be a list of Text and literal nodes. 924078ee54SPeter Maydell defn should be one of: 934078ee54SPeter Maydell - a string, which will be handed to _parse_text_into_node 944078ee54SPeter Maydell - a list of Text and literal nodes, which will be put into 954078ee54SPeter Maydell a paragraph node 964078ee54SPeter Maydell """ 974078ee54SPeter Maydell dlitem = nodes.definition_list_item() 984078ee54SPeter Maydell dlterm = nodes.term('', '', *term) 994078ee54SPeter Maydell dlitem += dlterm 1004078ee54SPeter Maydell if defn: 1014078ee54SPeter Maydell dldef = nodes.definition() 1024078ee54SPeter Maydell if isinstance(defn, list): 1034078ee54SPeter Maydell dldef += nodes.paragraph('', '', *defn) 1044078ee54SPeter Maydell else: 1054078ee54SPeter Maydell self._parse_text_into_node(defn, dldef) 1064078ee54SPeter Maydell dlitem += dldef 1074078ee54SPeter Maydell return dlitem 1084078ee54SPeter Maydell 1094078ee54SPeter Maydell def _make_section(self, title): 1104078ee54SPeter Maydell """Return a section node with optional title""" 1114078ee54SPeter Maydell section = nodes.section(ids=[self._sphinx_directive.new_serialno()]) 1124078ee54SPeter Maydell if title: 1134078ee54SPeter Maydell section += nodes.title(title, title) 1144078ee54SPeter Maydell return section 1154078ee54SPeter Maydell 1164078ee54SPeter Maydell def _nodes_for_ifcond(self, ifcond, with_if=True): 1174078ee54SPeter Maydell """Return list of Text, literal nodes for the ifcond 1184078ee54SPeter Maydell 119d806f89fSMarc-André Lureau Return a list which gives text like ' (If: condition)'. 1204078ee54SPeter Maydell If with_if is False, we don't return the "(If: " and ")". 1214078ee54SPeter Maydell """ 122d806f89fSMarc-André Lureau 123d806f89fSMarc-André Lureau doc = ifcond.docgen() 124d806f89fSMarc-André Lureau if not doc: 125d806f89fSMarc-André Lureau return [] 126d806f89fSMarc-André Lureau doc = nodes.literal('', doc) 1274078ee54SPeter Maydell if not with_if: 128d806f89fSMarc-André Lureau return [doc] 1294078ee54SPeter Maydell 1304078ee54SPeter Maydell nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')] 131d806f89fSMarc-André Lureau nodelist.append(doc) 1324078ee54SPeter Maydell nodelist.append(nodes.Text(')')) 1334078ee54SPeter Maydell return nodelist 1344078ee54SPeter Maydell 1354078ee54SPeter Maydell def _nodes_for_one_member(self, member): 1364078ee54SPeter Maydell """Return list of Text, literal nodes for this member 1374078ee54SPeter Maydell 1384078ee54SPeter Maydell Return a list of doctree nodes which give text like 1394078ee54SPeter Maydell 'name: type (optional) (If: ...)' suitable for use as the 1404078ee54SPeter Maydell 'term' part of a definition list item. 1414078ee54SPeter Maydell """ 1424078ee54SPeter Maydell term = [nodes.literal('', member.name)] 1434078ee54SPeter Maydell if member.type.doc_type(): 1444078ee54SPeter Maydell term.append(nodes.Text(': ')) 1454078ee54SPeter Maydell term.append(nodes.literal('', member.type.doc_type())) 1464078ee54SPeter Maydell if member.optional: 1474078ee54SPeter Maydell term.append(nodes.Text(' (optional)')) 14833aa3267SMarc-André Lureau if member.ifcond.is_present(): 1494078ee54SPeter Maydell term.extend(self._nodes_for_ifcond(member.ifcond)) 1504078ee54SPeter Maydell return term 1514078ee54SPeter Maydell 152d1da8af8SMarkus Armbruster def _nodes_for_variant_when(self, branches, variant): 1534078ee54SPeter Maydell """Return list of Text, literal nodes for variant 'when' clause 1544078ee54SPeter Maydell 1554078ee54SPeter Maydell Return a list of doctree nodes which give text like 1564078ee54SPeter Maydell 'when tagname is variant (If: ...)' suitable for use in 157d1da8af8SMarkus Armbruster the 'branches' part of a definition list. 1584078ee54SPeter Maydell """ 1594078ee54SPeter Maydell term = [nodes.Text(' when '), 160d1da8af8SMarkus Armbruster nodes.literal('', branches.tag_member.name), 1614078ee54SPeter Maydell nodes.Text(' is '), 1624078ee54SPeter Maydell nodes.literal('', '"%s"' % variant.name)] 16333aa3267SMarc-André Lureau if variant.ifcond.is_present(): 1644078ee54SPeter Maydell term.extend(self._nodes_for_ifcond(variant.ifcond)) 1654078ee54SPeter Maydell return term 1664078ee54SPeter Maydell 167d1da8af8SMarkus Armbruster def _nodes_for_members(self, doc, what, base=None, branches=None): 1684078ee54SPeter Maydell """Return list of doctree nodes for the table of members""" 1694078ee54SPeter Maydell dlnode = nodes.definition_list() 1704078ee54SPeter Maydell for section in doc.args.values(): 1714078ee54SPeter Maydell term = self._nodes_for_one_member(section.member) 1724078ee54SPeter Maydell # TODO drop fallbacks when undocumented members are outlawed 1734078ee54SPeter Maydell if section.text: 174939c639eSJohn Snow defn = dedent(section.text) 1754078ee54SPeter Maydell else: 1764078ee54SPeter Maydell defn = [nodes.Text('Not documented')] 1774078ee54SPeter Maydell 1784078ee54SPeter Maydell dlnode += self._make_dlitem(term, defn) 1794078ee54SPeter Maydell 1804078ee54SPeter Maydell if base: 1814078ee54SPeter Maydell dlnode += self._make_dlitem([nodes.Text('The members of '), 1824078ee54SPeter Maydell nodes.literal('', base.doc_type())], 1834078ee54SPeter Maydell None) 1844078ee54SPeter Maydell 185d1da8af8SMarkus Armbruster if branches: 186d1da8af8SMarkus Armbruster for v in branches.variants: 187e51e80ccSMarkus Armbruster if v.type.name == 'q_empty': 188e51e80ccSMarkus Armbruster continue 189e51e80ccSMarkus Armbruster assert not v.type.is_implicit() 1904078ee54SPeter Maydell term = [nodes.Text('The members of '), 1914078ee54SPeter Maydell nodes.literal('', v.type.doc_type())] 192d1da8af8SMarkus Armbruster term.extend(self._nodes_for_variant_when(branches, v)) 1934078ee54SPeter Maydell dlnode += self._make_dlitem(term, None) 1944078ee54SPeter Maydell 1954078ee54SPeter Maydell if not dlnode.children: 1964078ee54SPeter Maydell return [] 1974078ee54SPeter Maydell 1984078ee54SPeter Maydell section = self._make_section(what) 1994078ee54SPeter Maydell section += dlnode 2004078ee54SPeter Maydell return [section] 2014078ee54SPeter Maydell 2024078ee54SPeter Maydell def _nodes_for_enum_values(self, doc): 2034078ee54SPeter Maydell """Return list of doctree nodes for the table of enum values""" 2044078ee54SPeter Maydell seen_item = False 2054078ee54SPeter Maydell dlnode = nodes.definition_list() 2064078ee54SPeter Maydell for section in doc.args.values(): 2074078ee54SPeter Maydell termtext = [nodes.literal('', section.member.name)] 20833aa3267SMarc-André Lureau if section.member.ifcond.is_present(): 2094078ee54SPeter Maydell termtext.extend(self._nodes_for_ifcond(section.member.ifcond)) 2104078ee54SPeter Maydell # TODO drop fallbacks when undocumented members are outlawed 2114078ee54SPeter Maydell if section.text: 212939c639eSJohn Snow defn = dedent(section.text) 2134078ee54SPeter Maydell else: 2144078ee54SPeter Maydell defn = [nodes.Text('Not documented')] 2154078ee54SPeter Maydell 2164078ee54SPeter Maydell dlnode += self._make_dlitem(termtext, defn) 2174078ee54SPeter Maydell seen_item = True 2184078ee54SPeter Maydell 2194078ee54SPeter Maydell if not seen_item: 2204078ee54SPeter Maydell return [] 2214078ee54SPeter Maydell 2224078ee54SPeter Maydell section = self._make_section('Values') 2234078ee54SPeter Maydell section += dlnode 2244078ee54SPeter Maydell return [section] 2254078ee54SPeter Maydell 226e389929dSMarkus Armbruster def _nodes_for_arguments(self, doc, arg_type): 2274078ee54SPeter Maydell """Return list of doctree nodes for the arguments section""" 228e389929dSMarkus Armbruster if arg_type and not arg_type.is_implicit(): 2294078ee54SPeter Maydell assert not doc.args 2304078ee54SPeter Maydell section = self._make_section('Arguments') 2314078ee54SPeter Maydell dlnode = nodes.definition_list() 2324078ee54SPeter Maydell dlnode += self._make_dlitem( 2334078ee54SPeter Maydell [nodes.Text('The members of '), 234e389929dSMarkus Armbruster nodes.literal('', arg_type.name)], 2354078ee54SPeter Maydell None) 2364078ee54SPeter Maydell section += dlnode 2374078ee54SPeter Maydell return [section] 2384078ee54SPeter Maydell 2394078ee54SPeter Maydell return self._nodes_for_members(doc, 'Arguments') 2404078ee54SPeter Maydell 2414078ee54SPeter Maydell def _nodes_for_features(self, doc): 2424078ee54SPeter Maydell """Return list of doctree nodes for the table of features""" 2434078ee54SPeter Maydell seen_item = False 2444078ee54SPeter Maydell dlnode = nodes.definition_list() 2454078ee54SPeter Maydell for section in doc.features.values(): 246573e2223SMarkus Armbruster dlnode += self._make_dlitem( 247939c639eSJohn Snow [nodes.literal('', section.member.name)], dedent(section.text)) 2484078ee54SPeter Maydell seen_item = True 2494078ee54SPeter Maydell 2504078ee54SPeter Maydell if not seen_item: 2514078ee54SPeter Maydell return [] 2524078ee54SPeter Maydell 2534078ee54SPeter Maydell section = self._make_section('Features') 2544078ee54SPeter Maydell section += dlnode 2554078ee54SPeter Maydell return [section] 2564078ee54SPeter Maydell 2574078ee54SPeter Maydell def _nodes_for_example(self, exampletext): 2584078ee54SPeter Maydell """Return list of doctree nodes for a code example snippet""" 2594078ee54SPeter Maydell return [nodes.literal_block(exampletext, exampletext)] 2604078ee54SPeter Maydell 2614078ee54SPeter Maydell def _nodes_for_sections(self, doc): 2624078ee54SPeter Maydell """Return list of doctree nodes for additional sections""" 2634078ee54SPeter Maydell nodelist = [] 2644078ee54SPeter Maydell for section in doc.sections: 26531c54b92SMarkus Armbruster if section.tag and section.tag == 'TODO': 266f57e1d05SMarkus Armbruster # Hide TODO: sections 267f57e1d05SMarkus Armbruster continue 2682664f317SJohn Snow 2692664f317SJohn Snow if not section.tag: 2702664f317SJohn Snow # Sphinx cannot handle sectionless titles; 2712664f317SJohn Snow # Instead, just append the results to the prior section. 2722664f317SJohn Snow container = nodes.container() 2732664f317SJohn Snow self._parse_text_into_node(section.text, container) 2742664f317SJohn Snow nodelist += container.children 2752664f317SJohn Snow continue 2762664f317SJohn Snow 27731c54b92SMarkus Armbruster snode = self._make_section(section.tag) 2782664f317SJohn Snow if section.tag.startswith('Example'): 279939c639eSJohn Snow snode += self._nodes_for_example(dedent(section.text)) 2804078ee54SPeter Maydell else: 2812664f317SJohn Snow self._parse_text_into_node(dedent(section.text), snode) 2824078ee54SPeter Maydell nodelist.append(snode) 2834078ee54SPeter Maydell return nodelist 2844078ee54SPeter Maydell 2854078ee54SPeter Maydell def _nodes_for_if_section(self, ifcond): 2864078ee54SPeter Maydell """Return list of doctree nodes for the "If" section""" 2874078ee54SPeter Maydell nodelist = [] 28833aa3267SMarc-André Lureau if ifcond.is_present(): 2894078ee54SPeter Maydell snode = self._make_section('If') 2902d18b4caSJohn Snow snode += nodes.paragraph( 2912d18b4caSJohn Snow '', '', *self._nodes_for_ifcond(ifcond, with_if=False) 2922d18b4caSJohn Snow ) 2934078ee54SPeter Maydell nodelist.append(snode) 2944078ee54SPeter Maydell return nodelist 2954078ee54SPeter Maydell 2964078ee54SPeter Maydell def _add_doc(self, typ, sections): 2974078ee54SPeter Maydell """Add documentation for a command/object/enum... 2984078ee54SPeter Maydell 2994078ee54SPeter Maydell We assume we're documenting the thing defined in self._cur_doc. 3004078ee54SPeter Maydell typ is the type of thing being added ("Command", "Object", etc) 3014078ee54SPeter Maydell 3024078ee54SPeter Maydell sections is a list of nodes for sections to add to the definition. 3034078ee54SPeter Maydell """ 3044078ee54SPeter Maydell 3054078ee54SPeter Maydell doc = self._cur_doc 3064078ee54SPeter Maydell snode = nodes.section(ids=[self._sphinx_directive.new_serialno()]) 3074078ee54SPeter Maydell snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol), 3084078ee54SPeter Maydell nodes.Text(' (' + typ + ')')]) 3094078ee54SPeter Maydell self._parse_text_into_node(doc.body.text, snode) 3104078ee54SPeter Maydell for s in sections: 3114078ee54SPeter Maydell if s is not None: 3124078ee54SPeter Maydell snode += s 3134078ee54SPeter Maydell self._add_node_to_current_heading(snode) 3144078ee54SPeter Maydell 3154078ee54SPeter Maydell def visit_enum_type(self, name, info, ifcond, features, members, prefix): 3164078ee54SPeter Maydell doc = self._cur_doc 3174078ee54SPeter Maydell self._add_doc('Enum', 3184078ee54SPeter Maydell self._nodes_for_enum_values(doc) 3194078ee54SPeter Maydell + self._nodes_for_features(doc) 3204078ee54SPeter Maydell + self._nodes_for_sections(doc) 3214078ee54SPeter Maydell + self._nodes_for_if_section(ifcond)) 3224078ee54SPeter Maydell 3234078ee54SPeter Maydell def visit_object_type(self, name, info, ifcond, features, 324d1da8af8SMarkus Armbruster base, members, branches): 3254078ee54SPeter Maydell doc = self._cur_doc 3264078ee54SPeter Maydell if base and base.is_implicit(): 3274078ee54SPeter Maydell base = None 3284078ee54SPeter Maydell self._add_doc('Object', 329d1da8af8SMarkus Armbruster self._nodes_for_members(doc, 'Members', base, branches) 3304078ee54SPeter Maydell + self._nodes_for_features(doc) 3314078ee54SPeter Maydell + self._nodes_for_sections(doc) 3324078ee54SPeter Maydell + self._nodes_for_if_section(ifcond)) 3334078ee54SPeter Maydell 33441d0ad1dSMarkus Armbruster def visit_alternate_type(self, name, info, ifcond, features, 33541d0ad1dSMarkus Armbruster alternatives): 3364078ee54SPeter Maydell doc = self._cur_doc 3374078ee54SPeter Maydell self._add_doc('Alternate', 3384078ee54SPeter Maydell self._nodes_for_members(doc, 'Members') 3394078ee54SPeter Maydell + self._nodes_for_features(doc) 3404078ee54SPeter Maydell + self._nodes_for_sections(doc) 3414078ee54SPeter Maydell + self._nodes_for_if_section(ifcond)) 3424078ee54SPeter Maydell 3434078ee54SPeter Maydell def visit_command(self, name, info, ifcond, features, arg_type, 3444078ee54SPeter Maydell ret_type, gen, success_response, boxed, allow_oob, 34504f22362SKevin Wolf allow_preconfig, coroutine): 3464078ee54SPeter Maydell doc = self._cur_doc 3474078ee54SPeter Maydell self._add_doc('Command', 348e389929dSMarkus Armbruster self._nodes_for_arguments(doc, arg_type) 3494078ee54SPeter Maydell + self._nodes_for_features(doc) 3504078ee54SPeter Maydell + self._nodes_for_sections(doc) 3514078ee54SPeter Maydell + self._nodes_for_if_section(ifcond)) 3524078ee54SPeter Maydell 3534078ee54SPeter Maydell def visit_event(self, name, info, ifcond, features, arg_type, boxed): 3544078ee54SPeter Maydell doc = self._cur_doc 3554078ee54SPeter Maydell self._add_doc('Event', 356e389929dSMarkus Armbruster self._nodes_for_arguments(doc, arg_type) 3574078ee54SPeter Maydell + self._nodes_for_features(doc) 3584078ee54SPeter Maydell + self._nodes_for_sections(doc) 3594078ee54SPeter Maydell + self._nodes_for_if_section(ifcond)) 3604078ee54SPeter Maydell 3614078ee54SPeter Maydell def symbol(self, doc, entity): 3624078ee54SPeter Maydell """Add documentation for one symbol to the document tree 3634078ee54SPeter Maydell 3644078ee54SPeter Maydell This is the main entry point which causes us to add documentation 3654078ee54SPeter Maydell nodes for a symbol (which could be a 'command', 'object', 'event', 3664078ee54SPeter Maydell etc). We do this by calling 'visit' on the schema entity, which 3674078ee54SPeter Maydell will then call back into one of our visit_* methods, depending 3684078ee54SPeter Maydell on what kind of thing this symbol is. 3694078ee54SPeter Maydell """ 3704078ee54SPeter Maydell self._cur_doc = doc 3714078ee54SPeter Maydell entity.visit(self) 3724078ee54SPeter Maydell self._cur_doc = None 3734078ee54SPeter Maydell 3744078ee54SPeter Maydell def _start_new_heading(self, heading, level): 3754078ee54SPeter Maydell """Start a new heading at the specified heading level 3764078ee54SPeter Maydell 3774078ee54SPeter Maydell Create a new section whose title is 'heading' and which is placed 3784078ee54SPeter Maydell in the docutils node tree as a child of the most recent level-1 3794078ee54SPeter Maydell heading. Subsequent document sections (commands, freeform doc chunks, 3804078ee54SPeter Maydell etc) will be placed as children of this new heading section. 3814078ee54SPeter Maydell """ 3824078ee54SPeter Maydell if len(self._active_headings) < level: 3834078ee54SPeter Maydell raise QAPISemError(self._cur_doc.info, 3844078ee54SPeter Maydell 'Level %d subheading found outside a ' 3854078ee54SPeter Maydell 'level %d heading' 3864078ee54SPeter Maydell % (level, level - 1)) 3874078ee54SPeter Maydell snode = self._make_section(heading) 3884078ee54SPeter Maydell self._active_headings[level - 1] += snode 3894078ee54SPeter Maydell self._active_headings = self._active_headings[:level] 3904078ee54SPeter Maydell self._active_headings.append(snode) 391*43e0d14eSJohn Snow return snode 3924078ee54SPeter Maydell 3934078ee54SPeter Maydell def _add_node_to_current_heading(self, node): 3944078ee54SPeter Maydell """Add the node to whatever the current active heading is""" 3954078ee54SPeter Maydell self._active_headings[-1] += node 3964078ee54SPeter Maydell 3974078ee54SPeter Maydell def freeform(self, doc): 3984078ee54SPeter Maydell """Add a piece of 'freeform' documentation to the document tree 3994078ee54SPeter Maydell 4004078ee54SPeter Maydell A 'freeform' document chunk doesn't relate to any particular 4014078ee54SPeter Maydell symbol (for instance, it could be an introduction). 4024078ee54SPeter Maydell 4034078ee54SPeter Maydell If the freeform document starts with a line of the form 4044078ee54SPeter Maydell '= Heading text', this is a section or subsection heading, with 4054078ee54SPeter Maydell the heading level indicated by the number of '=' signs. 4064078ee54SPeter Maydell """ 4074078ee54SPeter Maydell 4084078ee54SPeter Maydell # QAPIDoc documentation says free-form documentation blocks 4094078ee54SPeter Maydell # must have only a body section, nothing else. 4104078ee54SPeter Maydell assert not doc.sections 4114078ee54SPeter Maydell assert not doc.args 4124078ee54SPeter Maydell assert not doc.features 4134078ee54SPeter Maydell self._cur_doc = doc 4144078ee54SPeter Maydell 4154078ee54SPeter Maydell text = doc.body.text 4164078ee54SPeter Maydell if re.match(r'=+ ', text): 4174078ee54SPeter Maydell # Section/subsection heading (if present, will always be 4184078ee54SPeter Maydell # the first line of the block) 4194078ee54SPeter Maydell (heading, _, text) = text.partition('\n') 4204078ee54SPeter Maydell (leader, _, heading) = heading.partition(' ') 421*43e0d14eSJohn Snow node = self._start_new_heading(heading, len(leader)) 4224078ee54SPeter Maydell if text == '': 4234078ee54SPeter Maydell return 4244078ee54SPeter Maydell 4254078ee54SPeter Maydell self._parse_text_into_node(text, node) 4264078ee54SPeter Maydell self._cur_doc = None 4274078ee54SPeter Maydell 4284078ee54SPeter Maydell def _parse_text_into_node(self, doctext, node): 4294078ee54SPeter Maydell """Parse a chunk of QAPI-doc-format text into the node 4304078ee54SPeter Maydell 4314078ee54SPeter Maydell The doc comment can contain most inline rST markup, including 4324078ee54SPeter Maydell bulleted and enumerated lists. 4334078ee54SPeter Maydell As an extra permitted piece of markup, @var will be turned 4344078ee54SPeter Maydell into ``var``. 4354078ee54SPeter Maydell """ 4364078ee54SPeter Maydell 4374078ee54SPeter Maydell # Handle the "@var means ``var`` case 4384078ee54SPeter Maydell doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext) 4394078ee54SPeter Maydell 4404078ee54SPeter Maydell rstlist = ViewList() 4414078ee54SPeter Maydell for line in doctext.splitlines(): 4424078ee54SPeter Maydell # The reported line number will always be that of the start line 4434078ee54SPeter Maydell # of the doc comment, rather than the actual location of the error. 4444078ee54SPeter Maydell # Being more precise would require overhaul of the QAPIDoc class 4454078ee54SPeter Maydell # to track lines more exactly within all the sub-parts of the doc 4464078ee54SPeter Maydell # comment, as well as counting lines here. 4474078ee54SPeter Maydell rstlist.append(line, self._cur_doc.info.fname, 4484078ee54SPeter Maydell self._cur_doc.info.line) 4494078ee54SPeter Maydell # Append a blank line -- in some cases rST syntax errors get 4504078ee54SPeter Maydell # attributed to the line after one with actual text, and if there 4514078ee54SPeter Maydell # isn't anything in the ViewList corresponding to that then Sphinx 4524078ee54SPeter Maydell # 1.6's AutodocReporter will then misidentify the source/line location 4534078ee54SPeter Maydell # in the error message (usually attributing it to the top-level 4544078ee54SPeter Maydell # .rst file rather than the offending .json file). The extra blank 4554078ee54SPeter Maydell # line won't affect the rendered output. 4564078ee54SPeter Maydell rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line) 4574078ee54SPeter Maydell self._sphinx_directive.do_parse(rstlist, node) 4584078ee54SPeter Maydell 4594078ee54SPeter Maydell def get_document_nodes(self): 4604078ee54SPeter Maydell """Return the list of docutils nodes which make up the document""" 4614078ee54SPeter Maydell return self._top_node.children 4624078ee54SPeter Maydell 4634078ee54SPeter Maydell 46436c6dcc2SJohn Snow# Turn the black formatter on for the rest of the file. 46536c6dcc2SJohn Snow# fmt: on 46636c6dcc2SJohn Snow 46736c6dcc2SJohn Snow 4684078ee54SPeter Maydellclass QAPISchemaGenDepVisitor(QAPISchemaVisitor): 4694078ee54SPeter Maydell """A QAPI schema visitor which adds Sphinx dependencies each module 4704078ee54SPeter Maydell 4714078ee54SPeter Maydell This class calls the Sphinx note_dependency() function to tell Sphinx 4724078ee54SPeter Maydell that the generated documentation output depends on the input 4734078ee54SPeter Maydell schema file associated with each module in the QAPI input. 4744078ee54SPeter Maydell """ 47536c6dcc2SJohn Snow 4764078ee54SPeter Maydell def __init__(self, env, qapidir): 4774078ee54SPeter Maydell self._env = env 4784078ee54SPeter Maydell self._qapidir = qapidir 4794078ee54SPeter Maydell 4804078ee54SPeter Maydell def visit_module(self, name): 48135f15acbSPeter Maydell if name != "./builtin": 48236c6dcc2SJohn Snow qapifile = self._qapidir + "/" + name 4834078ee54SPeter Maydell self._env.note_dependency(os.path.abspath(qapifile)) 4844078ee54SPeter Maydell super().visit_module(name) 4854078ee54SPeter Maydell 4864078ee54SPeter Maydell 487a7d07ccdSJohn Snowclass NestedDirective(Directive): 488a7d07ccdSJohn Snow def run(self): 489a7d07ccdSJohn Snow raise NotImplementedError 490a7d07ccdSJohn Snow 491a7d07ccdSJohn Snow def do_parse(self, rstlist, node): 492a7d07ccdSJohn Snow """ 493a7d07ccdSJohn Snow Parse rST source lines and add them to the specified node 494a7d07ccdSJohn Snow 495a7d07ccdSJohn Snow Take the list of rST source lines rstlist, parse them as 496a7d07ccdSJohn Snow rST, and add the resulting docutils nodes as children of node. 497a7d07ccdSJohn Snow The nodes are parsed in a way that allows them to include 498a7d07ccdSJohn Snow subheadings (titles) without confusing the rendering of 499a7d07ccdSJohn Snow anything else. 500a7d07ccdSJohn Snow """ 501a7d07ccdSJohn Snow with switch_source_input(self.state, rstlist): 502a7d07ccdSJohn Snow nested_parse_with_titles(self.state, rstlist, node) 503a7d07ccdSJohn Snow 504a7d07ccdSJohn Snow 505a7d07ccdSJohn Snowclass QAPIDocDirective(NestedDirective): 5064078ee54SPeter Maydell """Extract documentation from the specified QAPI .json file""" 50736c6dcc2SJohn Snow 5084078ee54SPeter Maydell required_argument = 1 5094078ee54SPeter Maydell optional_arguments = 1 51036c6dcc2SJohn Snow option_spec = {"qapifile": directives.unchanged_required} 5114078ee54SPeter Maydell has_content = False 5124078ee54SPeter Maydell 5134078ee54SPeter Maydell def new_serialno(self): 5144078ee54SPeter Maydell """Return a unique new ID string suitable for use as a node's ID""" 5154078ee54SPeter Maydell env = self.state.document.settings.env 51636c6dcc2SJohn Snow return "qapidoc-%d" % env.new_serialno("qapidoc") 5174078ee54SPeter Maydell 5184078ee54SPeter Maydell def run(self): 5194078ee54SPeter Maydell env = self.state.document.settings.env 52036c6dcc2SJohn Snow qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0] 5214078ee54SPeter Maydell qapidir = os.path.dirname(qapifile) 5224078ee54SPeter Maydell 5234078ee54SPeter Maydell try: 5244078ee54SPeter Maydell schema = QAPISchema(qapifile) 5254078ee54SPeter Maydell 5264078ee54SPeter Maydell # First tell Sphinx about all the schema files that the 5274078ee54SPeter Maydell # output documentation depends on (including 'qapifile' itself) 5284078ee54SPeter Maydell schema.visit(QAPISchemaGenDepVisitor(env, qapidir)) 5294078ee54SPeter Maydell 5304078ee54SPeter Maydell vis = QAPISchemaGenRSTVisitor(self) 5314078ee54SPeter Maydell vis.visit_begin(schema) 5324078ee54SPeter Maydell for doc in schema.docs: 5334078ee54SPeter Maydell if doc.symbol: 5344078ee54SPeter Maydell vis.symbol(doc, schema.lookup_entity(doc.symbol)) 5354078ee54SPeter Maydell else: 5364078ee54SPeter Maydell vis.freeform(doc) 5374078ee54SPeter Maydell return vis.get_document_nodes() 5384078ee54SPeter Maydell except QAPIError as err: 5394078ee54SPeter Maydell # Launder QAPI parse errors into Sphinx extension errors 5404078ee54SPeter Maydell # so they are displayed nicely to the user 541c375f05eSMarkus Armbruster raise ExtensionError(str(err)) from err 5424078ee54SPeter Maydell 5434078ee54SPeter Maydell 544547864f9SJohn Snowclass QMPExample(CodeBlock, NestedDirective): 545547864f9SJohn Snow """ 546547864f9SJohn Snow Custom admonition for QMP code examples. 547547864f9SJohn Snow 548547864f9SJohn Snow When the :annotated: option is present, the body of this directive 54976e375fcSJohn Snow is parsed as normal rST, but with any '::' code blocks set to use 55076e375fcSJohn Snow the QMP lexer. Code blocks must be explicitly written by the user, 55176e375fcSJohn Snow but this allows for intermingling explanatory paragraphs with 55276e375fcSJohn Snow arbitrary rST syntax and code blocks for more involved examples. 553547864f9SJohn Snow 554547864f9SJohn Snow When :annotated: is absent, the directive body is treated as a 555547864f9SJohn Snow simple standalone QMP code block literal. 556547864f9SJohn Snow """ 557547864f9SJohn Snow 558547864f9SJohn Snow required_argument = 0 559547864f9SJohn Snow optional_arguments = 0 560547864f9SJohn Snow has_content = True 561547864f9SJohn Snow option_spec = { 562547864f9SJohn Snow "annotated": directives.flag, 563547864f9SJohn Snow "title": directives.unchanged, 564547864f9SJohn Snow } 565547864f9SJohn Snow 56676e375fcSJohn Snow def _highlightlang(self) -> addnodes.highlightlang: 56776e375fcSJohn Snow """Return the current highlightlang setting for the document""" 56876e375fcSJohn Snow node = None 56976e375fcSJohn Snow doc = self.state.document 57076e375fcSJohn Snow 57176e375fcSJohn Snow if hasattr(doc, "findall"): 57276e375fcSJohn Snow # docutils >= 0.18.1 57376e375fcSJohn Snow for node in doc.findall(addnodes.highlightlang): 57476e375fcSJohn Snow pass 57576e375fcSJohn Snow else: 57676e375fcSJohn Snow for elem in doc.traverse(): 57776e375fcSJohn Snow if isinstance(elem, addnodes.highlightlang): 57876e375fcSJohn Snow node = elem 57976e375fcSJohn Snow 58076e375fcSJohn Snow if node: 58176e375fcSJohn Snow return node 58276e375fcSJohn Snow 58376e375fcSJohn Snow # No explicit directive found, use defaults 58476e375fcSJohn Snow node = addnodes.highlightlang( 58576e375fcSJohn Snow lang=self.env.config.highlight_language, 58676e375fcSJohn Snow force=False, 58776e375fcSJohn Snow # Yes, Sphinx uses this value to effectively disable line 58876e375fcSJohn Snow # numbers and not 0 or None or -1 or something. ¯\_(ツ)_/¯ 58976e375fcSJohn Snow linenothreshold=sys.maxsize, 59076e375fcSJohn Snow ) 59176e375fcSJohn Snow return node 59276e375fcSJohn Snow 593547864f9SJohn Snow def admonition_wrap(self, *content) -> List[nodes.Node]: 594547864f9SJohn Snow title = "Example:" 595547864f9SJohn Snow if "title" in self.options: 596547864f9SJohn Snow title = f"{title} {self.options['title']}" 597547864f9SJohn Snow 598547864f9SJohn Snow admon = nodes.admonition( 599547864f9SJohn Snow "", 600547864f9SJohn Snow nodes.title("", title), 601547864f9SJohn Snow *content, 602547864f9SJohn Snow classes=["admonition", "admonition-example"], 603547864f9SJohn Snow ) 604547864f9SJohn Snow return [admon] 605547864f9SJohn Snow 606547864f9SJohn Snow def run_annotated(self) -> List[nodes.Node]: 60776e375fcSJohn Snow lang_node = self._highlightlang() 60876e375fcSJohn Snow 609547864f9SJohn Snow content_node: nodes.Element = nodes.section() 61076e375fcSJohn Snow 61176e375fcSJohn Snow # Configure QMP highlighting for "::" blocks, if needed 61276e375fcSJohn Snow if lang_node["lang"] != "QMP": 61376e375fcSJohn Snow content_node += addnodes.highlightlang( 61476e375fcSJohn Snow lang="QMP", 61576e375fcSJohn Snow force=False, # "True" ignores lexing errors 61676e375fcSJohn Snow linenothreshold=lang_node["linenothreshold"], 61776e375fcSJohn Snow ) 61876e375fcSJohn Snow 619547864f9SJohn Snow self.do_parse(self.content, content_node) 62076e375fcSJohn Snow 62176e375fcSJohn Snow # Restore prior language highlighting, if needed 62276e375fcSJohn Snow if lang_node["lang"] != "QMP": 62376e375fcSJohn Snow content_node += addnodes.highlightlang(**lang_node.attributes) 62476e375fcSJohn Snow 625547864f9SJohn Snow return content_node.children 626547864f9SJohn Snow 627547864f9SJohn Snow def run(self) -> List[nodes.Node]: 628547864f9SJohn Snow annotated = "annotated" in self.options 629547864f9SJohn Snow 630547864f9SJohn Snow if annotated: 631547864f9SJohn Snow content_nodes = self.run_annotated() 632547864f9SJohn Snow else: 633547864f9SJohn Snow self.arguments = ["QMP"] 634547864f9SJohn Snow content_nodes = super().run() 635547864f9SJohn Snow 636547864f9SJohn Snow return self.admonition_wrap(*content_nodes) 637547864f9SJohn Snow 638547864f9SJohn Snow 6394078ee54SPeter Maydelldef setup(app): 6404078ee54SPeter Maydell """Register qapi-doc directive with Sphinx""" 64136c6dcc2SJohn Snow app.add_config_value("qapidoc_srctree", None, "env") 64236c6dcc2SJohn Snow app.add_directive("qapi-doc", QAPIDocDirective) 643547864f9SJohn Snow app.add_directive("qmp-example", QMPExample) 6444078ee54SPeter Maydell 64536c6dcc2SJohn Snow return { 64636c6dcc2SJohn Snow "version": __version__, 64736c6dcc2SJohn Snow "parallel_read_safe": True, 64836c6dcc2SJohn Snow "parallel_write_safe": True, 64936c6dcc2SJohn Snow } 650