qapidoc.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  1. # coding=utf-8
  2. #
  3. # QEMU qapidoc QAPI file parsing extension
  4. #
  5. # Copyright (c) 2024-2025 Red Hat
  6. # Copyright (c) 2020 Linaro
  7. #
  8. # This work is licensed under the terms of the GNU GPLv2 or later.
  9. # See the COPYING file in the top-level directory.
  10. """
  11. qapidoc is a Sphinx extension that implements the qapi-doc directive
  12. The purpose of this extension is to read the documentation comments
  13. in QAPI schema files, and insert them all into the current document.
  14. It implements one new rST directive, "qapi-doc::".
  15. Each qapi-doc:: directive takes one argument, which is the
  16. pathname of the schema file to process, relative to the source tree.
  17. The docs/conf.py file must set the qapidoc_srctree config value to
  18. the root of the QEMU source tree.
  19. The Sphinx documentation on writing extensions is at:
  20. https://www.sphinx-doc.org/en/master/development/index.html
  21. """
  22. from __future__ import annotations
  23. __version__ = "2.0"
  24. from contextlib import contextmanager
  25. import os
  26. from pathlib import Path
  27. import re
  28. import sys
  29. from typing import TYPE_CHECKING
  30. from docutils import nodes
  31. from docutils.parsers.rst import directives
  32. from docutils.statemachine import StringList
  33. from qapi.error import QAPIError
  34. from qapi.parser import QAPIDoc
  35. from qapi.schema import (
  36. QAPISchema,
  37. QAPISchemaArrayType,
  38. QAPISchemaCommand,
  39. QAPISchemaDefinition,
  40. QAPISchemaEnumMember,
  41. QAPISchemaEvent,
  42. QAPISchemaFeature,
  43. QAPISchemaMember,
  44. QAPISchemaObjectType,
  45. QAPISchemaObjectTypeMember,
  46. QAPISchemaType,
  47. QAPISchemaVisitor,
  48. )
  49. from qapi.source import QAPISourceInfo
  50. from qapidoc_legacy import QAPISchemaGenRSTVisitor # type: ignore
  51. from sphinx import addnodes
  52. from sphinx.directives.code import CodeBlock
  53. from sphinx.errors import ExtensionError
  54. from sphinx.util import logging
  55. from sphinx.util.docutils import SphinxDirective, switch_source_input
  56. from sphinx.util.nodes import nested_parse_with_titles
  57. if TYPE_CHECKING:
  58. from typing import (
  59. Any,
  60. Generator,
  61. List,
  62. Optional,
  63. Sequence,
  64. Union,
  65. )
  66. from sphinx.application import Sphinx
  67. from sphinx.util.typing import ExtensionMetadata
  68. logger = logging.getLogger(__name__)
  69. class Transmogrifier:
  70. # pylint: disable=too-many-public-methods
  71. # Field names used for different entity types:
  72. field_types = {
  73. "enum": "value",
  74. "struct": "memb",
  75. "union": "memb",
  76. "event": "memb",
  77. "command": "arg",
  78. "alternate": "alt",
  79. }
  80. def __init__(self) -> None:
  81. self._curr_ent: Optional[QAPISchemaDefinition] = None
  82. self._result = StringList()
  83. self.indent = 0
  84. @property
  85. def result(self) -> StringList:
  86. return self._result
  87. @property
  88. def entity(self) -> QAPISchemaDefinition:
  89. assert self._curr_ent is not None
  90. return self._curr_ent
  91. @property
  92. def member_field_type(self) -> str:
  93. return self.field_types[self.entity.meta]
  94. # General-purpose rST generation functions
  95. def get_indent(self) -> str:
  96. return " " * self.indent
  97. @contextmanager
  98. def indented(self) -> Generator[None]:
  99. self.indent += 1
  100. try:
  101. yield
  102. finally:
  103. self.indent -= 1
  104. def add_line_raw(self, line: str, source: str, *lineno: int) -> None:
  105. """Append one line of generated reST to the output."""
  106. # NB: Sphinx uses zero-indexed lines; subtract one.
  107. lineno = tuple((n - 1 for n in lineno))
  108. if line.strip():
  109. # not a blank line
  110. self._result.append(
  111. self.get_indent() + line.rstrip("\n"), source, *lineno
  112. )
  113. else:
  114. self._result.append("", source, *lineno)
  115. def add_line(self, content: str, info: QAPISourceInfo) -> None:
  116. # NB: We *require* an info object; this works out OK because we
  117. # don't document built-in objects that don't have
  118. # one. Everything else should.
  119. self.add_line_raw(content, info.fname, info.line)
  120. def add_lines(
  121. self,
  122. content: str,
  123. info: QAPISourceInfo,
  124. ) -> None:
  125. lines = content.splitlines(True)
  126. for i, line in enumerate(lines):
  127. self.add_line_raw(line, info.fname, info.line + i)
  128. def ensure_blank_line(self) -> None:
  129. # Empty document -- no blank line required.
  130. if not self._result:
  131. return
  132. # Last line isn't blank, add one.
  133. if self._result[-1].strip(): # pylint: disable=no-member
  134. fname, line = self._result.info(-1)
  135. assert isinstance(line, int)
  136. # New blank line is credited to one-after the current last line.
  137. # +2: correct for zero/one index, then increment by one.
  138. self.add_line_raw("", fname, line + 2)
  139. def add_field(
  140. self,
  141. kind: str,
  142. name: str,
  143. body: str,
  144. info: QAPISourceInfo,
  145. typ: Optional[str] = None,
  146. ) -> None:
  147. if typ:
  148. text = f":{kind} {typ} {name}: {body}"
  149. else:
  150. text = f":{kind} {name}: {body}"
  151. self.add_lines(text, info)
  152. def format_type(
  153. self, ent: Union[QAPISchemaDefinition | QAPISchemaMember]
  154. ) -> Optional[str]:
  155. if isinstance(ent, (QAPISchemaEnumMember, QAPISchemaFeature)):
  156. return None
  157. qapi_type = ent
  158. optional = False
  159. if isinstance(ent, QAPISchemaObjectTypeMember):
  160. qapi_type = ent.type
  161. optional = ent.optional
  162. if isinstance(qapi_type, QAPISchemaArrayType):
  163. ret = f"[{qapi_type.element_type.doc_type()}]"
  164. else:
  165. assert isinstance(qapi_type, QAPISchemaType)
  166. tmp = qapi_type.doc_type()
  167. assert tmp
  168. ret = tmp
  169. if optional:
  170. ret += "?"
  171. return ret
  172. def generate_field(
  173. self,
  174. kind: str,
  175. member: QAPISchemaMember,
  176. body: str,
  177. info: QAPISourceInfo,
  178. ) -> None:
  179. typ = self.format_type(member)
  180. self.add_field(kind, member.name, body, info, typ)
  181. # Transmogrification helpers
  182. def visit_paragraph(self, section: QAPIDoc.Section) -> None:
  183. # Squelch empty paragraphs.
  184. if not section.text:
  185. return
  186. self.ensure_blank_line()
  187. self.add_lines(section.text, section.info)
  188. self.ensure_blank_line()
  189. def visit_member(self, section: QAPIDoc.ArgSection) -> None:
  190. # FIXME: ifcond for members
  191. # TODO: features for members (documented at entity-level,
  192. # but sometimes defined per-member. Should we add such
  193. # information to member descriptions when we can?)
  194. assert section.member
  195. self.generate_field(
  196. self.member_field_type,
  197. section.member,
  198. # TODO drop fallbacks when undocumented members are outlawed
  199. section.text if section.text else "Not documented",
  200. section.info,
  201. )
  202. def visit_feature(self, section: QAPIDoc.ArgSection) -> None:
  203. # FIXME - ifcond for features is not handled at all yet!
  204. # Proposal: decorate the right-hand column with some graphical
  205. # element to indicate conditional availability?
  206. assert section.text # Guaranteed by parser.py
  207. assert section.member
  208. self.generate_field("feat", section.member, section.text, section.info)
  209. def visit_returns(self, section: QAPIDoc.Section) -> None:
  210. assert isinstance(self.entity, QAPISchemaCommand)
  211. rtype = self.entity.ret_type
  212. # q_empty can produce None, but we won't be documenting anything
  213. # without an explicit return statement in the doc block, and we
  214. # should not have any such explicit statements when there is no
  215. # return value.
  216. assert rtype
  217. typ = self.format_type(rtype)
  218. assert typ
  219. assert section.text
  220. self.add_field("return", typ, section.text, section.info)
  221. def visit_errors(self, section: QAPIDoc.Section) -> None:
  222. # FIXME: the formatting for errors may be inconsistent and may
  223. # or may not require different newline placement to ensure
  224. # proper rendering as a nested list.
  225. self.add_lines(f":error:\n{section.text}", section.info)
  226. def preamble(self, ent: QAPISchemaDefinition) -> None:
  227. """
  228. Generate option lines for QAPI entity directives.
  229. """
  230. if ent.doc and ent.doc.since:
  231. assert ent.doc.since.kind == QAPIDoc.Kind.SINCE
  232. # Generated from the entity's docblock; info location is exact.
  233. self.add_line(f":since: {ent.doc.since.text}", ent.doc.since.info)
  234. if ent.ifcond.is_present():
  235. doc = ent.ifcond.docgen()
  236. assert ent.info
  237. # Generated from entity definition; info location is approximate.
  238. self.add_line(f":ifcond: {doc}", ent.info)
  239. # Hoist special features such as :deprecated: and :unstable:
  240. # into the options block for the entity. If, in the future, new
  241. # special features are added, qapi-domain will chirp about
  242. # unrecognized options and fail until they are handled in
  243. # qapi-domain.
  244. for feat in ent.features:
  245. if feat.is_special():
  246. # FIXME: handle ifcond if present. How to display that
  247. # information is TBD.
  248. # Generated from entity def; info location is approximate.
  249. assert feat.info
  250. self.add_line(f":{feat.name}:", feat.info)
  251. self.ensure_blank_line()
  252. def _insert_member_pointer(self, ent: QAPISchemaDefinition) -> None:
  253. def _get_target(
  254. ent: QAPISchemaDefinition,
  255. ) -> Optional[QAPISchemaDefinition]:
  256. if isinstance(ent, (QAPISchemaCommand, QAPISchemaEvent)):
  257. return ent.arg_type
  258. if isinstance(ent, QAPISchemaObjectType):
  259. return ent.base
  260. return None
  261. target = _get_target(ent)
  262. if target is not None and not target.is_implicit():
  263. assert ent.info
  264. self.add_field(
  265. self.member_field_type,
  266. "q_dummy",
  267. f"The members of :qapi:type:`{target.name}`.",
  268. ent.info,
  269. "q_dummy",
  270. )
  271. if isinstance(ent, QAPISchemaObjectType) and ent.branches is not None:
  272. for variant in ent.branches.variants:
  273. if variant.type.name == "q_empty":
  274. continue
  275. assert ent.info
  276. self.add_field(
  277. self.member_field_type,
  278. "q_dummy",
  279. f" When ``{ent.branches.tag_member.name}`` is "
  280. f"``{variant.name}``: "
  281. f"The members of :qapi:type:`{variant.type.name}`.",
  282. ent.info,
  283. "q_dummy",
  284. )
  285. def visit_sections(self, ent: QAPISchemaDefinition) -> None:
  286. sections = ent.doc.all_sections if ent.doc else []
  287. # Determine the index location at which we should generate
  288. # documentation for "The members of ..." pointers. This should
  289. # go at the end of the members section(s) if any. Note that
  290. # index 0 is assumed to be a plain intro section, even if it is
  291. # empty; and that a members section if present will always
  292. # immediately follow the opening PLAIN section.
  293. gen_index = 1
  294. if len(sections) > 1:
  295. while sections[gen_index].kind == QAPIDoc.Kind.MEMBER:
  296. gen_index += 1
  297. if gen_index >= len(sections):
  298. break
  299. # Add sections in source order:
  300. for i, section in enumerate(sections):
  301. # @var is translated to ``var``:
  302. section.text = re.sub(r"@([\w-]+)", r"``\1``", section.text)
  303. if section.kind == QAPIDoc.Kind.PLAIN:
  304. self.visit_paragraph(section)
  305. elif section.kind == QAPIDoc.Kind.MEMBER:
  306. assert isinstance(section, QAPIDoc.ArgSection)
  307. self.visit_member(section)
  308. elif section.kind == QAPIDoc.Kind.FEATURE:
  309. assert isinstance(section, QAPIDoc.ArgSection)
  310. self.visit_feature(section)
  311. elif section.kind in (QAPIDoc.Kind.SINCE, QAPIDoc.Kind.TODO):
  312. # Since is handled in preamble, TODO is skipped intentionally.
  313. pass
  314. elif section.kind == QAPIDoc.Kind.RETURNS:
  315. self.visit_returns(section)
  316. elif section.kind == QAPIDoc.Kind.ERRORS:
  317. self.visit_errors(section)
  318. else:
  319. assert False
  320. # Generate "The members of ..." entries if necessary:
  321. if i == gen_index - 1:
  322. self._insert_member_pointer(ent)
  323. self.ensure_blank_line()
  324. # Transmogrification core methods
  325. def visit_module(self, path: str) -> None:
  326. name = Path(path).stem
  327. # module directives are credited to the first line of a module file.
  328. self.add_line_raw(f".. qapi:module:: {name}", path, 1)
  329. self.ensure_blank_line()
  330. def visit_freeform(self, doc: QAPIDoc) -> None:
  331. # TODO: Once the old qapidoc transformer is deprecated, freeform
  332. # sections can be updated to pure rST, and this transformed removed.
  333. #
  334. # For now, translate our micro-format into rST. Code adapted
  335. # from Peter Maydell's freeform().
  336. assert len(doc.all_sections) == 1, doc.all_sections
  337. body = doc.all_sections[0]
  338. text = body.text
  339. info = doc.info
  340. if re.match(r"=+ ", text):
  341. # Section/subsection heading (if present, will always be the
  342. # first line of the block)
  343. (heading, _, text) = text.partition("\n")
  344. (leader, _, heading) = heading.partition(" ")
  345. # Implicit +1 for heading in the containing .rst doc
  346. level = len(leader) + 1
  347. # https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#sections
  348. markers = ' #*=_^"'
  349. overline = level <= 2
  350. marker = markers[level]
  351. self.ensure_blank_line()
  352. # This credits all 2 or 3 lines to the single source line.
  353. if overline:
  354. self.add_line(marker * len(heading), info)
  355. self.add_line(heading, info)
  356. self.add_line(marker * len(heading), info)
  357. self.ensure_blank_line()
  358. # Eat blank line(s) and advance info
  359. trimmed = text.lstrip("\n")
  360. text = trimmed
  361. info = info.next_line(len(text) - len(trimmed) + 1)
  362. self.add_lines(text, info)
  363. self.ensure_blank_line()
  364. def visit_entity(self, ent: QAPISchemaDefinition) -> None:
  365. assert ent.info
  366. try:
  367. self._curr_ent = ent
  368. # Squish structs and unions together into an "object" directive.
  369. meta = ent.meta
  370. if meta in ("struct", "union"):
  371. meta = "object"
  372. # This line gets credited to the start of the /definition/.
  373. self.add_line(f".. qapi:{meta}:: {ent.name}", ent.info)
  374. with self.indented():
  375. self.preamble(ent)
  376. self.visit_sections(ent)
  377. finally:
  378. self._curr_ent = None
  379. def set_namespace(self, namespace: str, source: str, lineno: int) -> None:
  380. self.add_line_raw(
  381. f".. qapi:namespace:: {namespace}", source, lineno + 1
  382. )
  383. self.ensure_blank_line()
  384. class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
  385. """A QAPI schema visitor which adds Sphinx dependencies each module
  386. This class calls the Sphinx note_dependency() function to tell Sphinx
  387. that the generated documentation output depends on the input
  388. schema file associated with each module in the QAPI input.
  389. """
  390. def __init__(self, env: Any, qapidir: str) -> None:
  391. self._env = env
  392. self._qapidir = qapidir
  393. def visit_module(self, name: str) -> None:
  394. if name != "./builtin":
  395. qapifile = self._qapidir + "/" + name
  396. self._env.note_dependency(os.path.abspath(qapifile))
  397. super().visit_module(name)
  398. class NestedDirective(SphinxDirective):
  399. def run(self) -> Sequence[nodes.Node]:
  400. raise NotImplementedError
  401. def do_parse(self, rstlist: StringList, node: nodes.Node) -> None:
  402. """
  403. Parse rST source lines and add them to the specified node
  404. Take the list of rST source lines rstlist, parse them as
  405. rST, and add the resulting docutils nodes as children of node.
  406. The nodes are parsed in a way that allows them to include
  407. subheadings (titles) without confusing the rendering of
  408. anything else.
  409. """
  410. with switch_source_input(self.state, rstlist):
  411. nested_parse_with_titles(self.state, rstlist, node)
  412. class QAPIDocDirective(NestedDirective):
  413. """Extract documentation from the specified QAPI .json file"""
  414. required_argument = 1
  415. optional_arguments = 1
  416. option_spec = {
  417. "qapifile": directives.unchanged_required,
  418. "namespace": directives.unchanged,
  419. "transmogrify": directives.flag,
  420. }
  421. has_content = False
  422. def new_serialno(self) -> str:
  423. """Return a unique new ID string suitable for use as a node's ID"""
  424. env = self.state.document.settings.env
  425. return "qapidoc-%d" % env.new_serialno("qapidoc")
  426. def transmogrify(self, schema: QAPISchema) -> nodes.Element:
  427. logger.info("Transmogrifying QAPI to rST ...")
  428. vis = Transmogrifier()
  429. modules = set()
  430. if "namespace" in self.options:
  431. vis.set_namespace(
  432. self.options["namespace"], *self.get_source_info()
  433. )
  434. for doc in schema.docs:
  435. module_source = doc.info.fname
  436. if module_source not in modules:
  437. vis.visit_module(module_source)
  438. modules.add(module_source)
  439. if doc.symbol:
  440. ent = schema.lookup_entity(doc.symbol)
  441. assert isinstance(ent, QAPISchemaDefinition)
  442. vis.visit_entity(ent)
  443. else:
  444. vis.visit_freeform(doc)
  445. logger.info("Transmogrification complete.")
  446. contentnode = nodes.section()
  447. content = vis.result
  448. titles_allowed = True
  449. logger.info("Transmogrifier running nested parse ...")
  450. with switch_source_input(self.state, content):
  451. if titles_allowed:
  452. node: nodes.Element = nodes.section()
  453. node.document = self.state.document
  454. nested_parse_with_titles(self.state, content, contentnode)
  455. else:
  456. node = nodes.paragraph()
  457. node.document = self.state.document
  458. self.state.nested_parse(content, 0, contentnode)
  459. logger.info("Transmogrifier's nested parse completed.")
  460. if self.env.app.verbosity >= 2 or os.environ.get("DEBUG"):
  461. argname = "_".join(Path(self.arguments[0]).parts)
  462. name = Path(argname).stem + ".ir"
  463. self.write_intermediate(content, name)
  464. sys.stdout.flush()
  465. return contentnode
  466. def write_intermediate(self, content: StringList, filename: str) -> None:
  467. logger.info(
  468. "writing intermediate rST for '%s' to '%s'",
  469. self.arguments[0],
  470. filename,
  471. )
  472. srctree = Path(self.env.app.config.qapidoc_srctree).resolve()
  473. outlines = []
  474. lcol_width = 0
  475. for i, line in enumerate(content):
  476. src, lineno = content.info(i)
  477. srcpath = Path(src).resolve()
  478. srcpath = srcpath.relative_to(srctree)
  479. lcol = f"{srcpath}:{lineno:04d}"
  480. lcol_width = max(lcol_width, len(lcol))
  481. outlines.append((lcol, line))
  482. with open(filename, "w", encoding="UTF-8") as outfile:
  483. for lcol, rcol in outlines:
  484. outfile.write(lcol.rjust(lcol_width))
  485. outfile.write(" |")
  486. if rcol:
  487. outfile.write(f" {rcol}")
  488. outfile.write("\n")
  489. def legacy(self, schema: QAPISchema) -> nodes.Element:
  490. vis = QAPISchemaGenRSTVisitor(self)
  491. vis.visit_begin(schema)
  492. for doc in schema.docs:
  493. if doc.symbol:
  494. vis.symbol(doc, schema.lookup_entity(doc.symbol))
  495. else:
  496. vis.freeform(doc)
  497. return vis.get_document_node() # type: ignore
  498. def run(self) -> Sequence[nodes.Node]:
  499. env = self.state.document.settings.env
  500. qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0]
  501. qapidir = os.path.dirname(qapifile)
  502. transmogrify = "transmogrify" in self.options
  503. try:
  504. schema = QAPISchema(qapifile)
  505. # First tell Sphinx about all the schema files that the
  506. # output documentation depends on (including 'qapifile' itself)
  507. schema.visit(QAPISchemaGenDepVisitor(env, qapidir))
  508. except QAPIError as err:
  509. # Launder QAPI parse errors into Sphinx extension errors
  510. # so they are displayed nicely to the user
  511. raise ExtensionError(str(err)) from err
  512. if transmogrify:
  513. contentnode = self.transmogrify(schema)
  514. else:
  515. contentnode = self.legacy(schema)
  516. return contentnode.children
  517. class QMPExample(CodeBlock, NestedDirective):
  518. """
  519. Custom admonition for QMP code examples.
  520. When the :annotated: option is present, the body of this directive
  521. is parsed as normal rST, but with any '::' code blocks set to use
  522. the QMP lexer. Code blocks must be explicitly written by the user,
  523. but this allows for intermingling explanatory paragraphs with
  524. arbitrary rST syntax and code blocks for more involved examples.
  525. When :annotated: is absent, the directive body is treated as a
  526. simple standalone QMP code block literal.
  527. """
  528. required_argument = 0
  529. optional_arguments = 0
  530. has_content = True
  531. option_spec = {
  532. "annotated": directives.flag,
  533. "title": directives.unchanged,
  534. }
  535. def _highlightlang(self) -> addnodes.highlightlang:
  536. """Return the current highlightlang setting for the document"""
  537. node = None
  538. doc = self.state.document
  539. if hasattr(doc, "findall"):
  540. # docutils >= 0.18.1
  541. for node in doc.findall(addnodes.highlightlang):
  542. pass
  543. else:
  544. for elem in doc.traverse():
  545. if isinstance(elem, addnodes.highlightlang):
  546. node = elem
  547. if node:
  548. return node
  549. # No explicit directive found, use defaults
  550. node = addnodes.highlightlang(
  551. lang=self.env.config.highlight_language,
  552. force=False,
  553. # Yes, Sphinx uses this value to effectively disable line
  554. # numbers and not 0 or None or -1 or something. ¯\_(ツ)_/¯
  555. linenothreshold=sys.maxsize,
  556. )
  557. return node
  558. def admonition_wrap(self, *content: nodes.Node) -> List[nodes.Node]:
  559. title = "Example:"
  560. if "title" in self.options:
  561. title = f"{title} {self.options['title']}"
  562. admon = nodes.admonition(
  563. "",
  564. nodes.title("", title),
  565. *content,
  566. classes=["admonition", "admonition-example"],
  567. )
  568. return [admon]
  569. def run_annotated(self) -> List[nodes.Node]:
  570. lang_node = self._highlightlang()
  571. content_node: nodes.Element = nodes.section()
  572. # Configure QMP highlighting for "::" blocks, if needed
  573. if lang_node["lang"] != "QMP":
  574. content_node += addnodes.highlightlang(
  575. lang="QMP",
  576. force=False, # "True" ignores lexing errors
  577. linenothreshold=lang_node["linenothreshold"],
  578. )
  579. self.do_parse(self.content, content_node)
  580. # Restore prior language highlighting, if needed
  581. if lang_node["lang"] != "QMP":
  582. content_node += addnodes.highlightlang(**lang_node.attributes)
  583. return content_node.children
  584. def run(self) -> List[nodes.Node]:
  585. annotated = "annotated" in self.options
  586. if annotated:
  587. content_nodes = self.run_annotated()
  588. else:
  589. self.arguments = ["QMP"]
  590. content_nodes = super().run()
  591. return self.admonition_wrap(*content_nodes)
  592. def setup(app: Sphinx) -> ExtensionMetadata:
  593. """Register qapi-doc directive with Sphinx"""
  594. app.setup_extension("qapi_domain")
  595. app.add_config_value("qapidoc_srctree", None, "env")
  596. app.add_directive("qapi-doc", QAPIDocDirective)
  597. app.add_directive("qmp-example", QMPExample)
  598. return {
  599. "version": __version__,
  600. "parallel_read_safe": True,
  601. "parallel_write_safe": True,
  602. }