xref: /openbmc/qemu/docs/sphinx/qapidoc.py (revision 23e67bd74021c47107c93622ac9b342df8291c9b)
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