qapidoc.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  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.text and section.member
  195. self.generate_field(
  196. self.member_field_type,
  197. section.member,
  198. section.text,
  199. section.info,
  200. )
  201. def visit_feature(self, section: QAPIDoc.ArgSection) -> None:
  202. # FIXME - ifcond for features is not handled at all yet!
  203. # Proposal: decorate the right-hand column with some graphical
  204. # element to indicate conditional availability?
  205. assert section.text # Guaranteed by parser.py
  206. assert section.member
  207. self.generate_field("feat", section.member, section.text, section.info)
  208. def visit_returns(self, section: QAPIDoc.Section) -> None:
  209. assert isinstance(self.entity, QAPISchemaCommand)
  210. rtype = self.entity.ret_type
  211. # q_empty can produce None, but we won't be documenting anything
  212. # without an explicit return statement in the doc block, and we
  213. # should not have any such explicit statements when there is no
  214. # return value.
  215. assert rtype
  216. typ = self.format_type(rtype)
  217. assert typ
  218. assert section.text
  219. self.add_field("return", typ, section.text, section.info)
  220. def visit_errors(self, section: QAPIDoc.Section) -> None:
  221. # FIXME: the formatting for errors may be inconsistent and may
  222. # or may not require different newline placement to ensure
  223. # proper rendering as a nested list.
  224. self.add_lines(f":error:\n{section.text}", section.info)
  225. def preamble(self, ent: QAPISchemaDefinition) -> None:
  226. """
  227. Generate option lines for QAPI entity directives.
  228. """
  229. if ent.doc and ent.doc.since:
  230. assert ent.doc.since.kind == QAPIDoc.Kind.SINCE
  231. # Generated from the entity's docblock; info location is exact.
  232. self.add_line(f":since: {ent.doc.since.text}", ent.doc.since.info)
  233. if ent.ifcond.is_present():
  234. doc = ent.ifcond.docgen()
  235. assert ent.info
  236. # Generated from entity definition; info location is approximate.
  237. self.add_line(f":ifcond: {doc}", ent.info)
  238. # Hoist special features such as :deprecated: and :unstable:
  239. # into the options block for the entity. If, in the future, new
  240. # special features are added, qapi-domain will chirp about
  241. # unrecognized options and fail until they are handled in
  242. # qapi-domain.
  243. for feat in ent.features:
  244. if feat.is_special():
  245. # FIXME: handle ifcond if present. How to display that
  246. # information is TBD.
  247. # Generated from entity def; info location is approximate.
  248. assert feat.info
  249. self.add_line(f":{feat.name}:", feat.info)
  250. self.ensure_blank_line()
  251. def _insert_member_pointer(self, ent: QAPISchemaDefinition) -> None:
  252. def _get_target(
  253. ent: QAPISchemaDefinition,
  254. ) -> Optional[QAPISchemaDefinition]:
  255. if isinstance(ent, (QAPISchemaCommand, QAPISchemaEvent)):
  256. return ent.arg_type
  257. if isinstance(ent, QAPISchemaObjectType):
  258. return ent.base
  259. return None
  260. target = _get_target(ent)
  261. if target is not None and not target.is_implicit():
  262. assert ent.info
  263. self.add_field(
  264. self.member_field_type,
  265. "q_dummy",
  266. f"The members of :qapi:type:`{target.name}`.",
  267. ent.info,
  268. "q_dummy",
  269. )
  270. if isinstance(ent, QAPISchemaObjectType) and ent.branches is not None:
  271. for variant in ent.branches.variants:
  272. if variant.type.name == "q_empty":
  273. continue
  274. assert ent.info
  275. self.add_field(
  276. self.member_field_type,
  277. "q_dummy",
  278. f" When ``{ent.branches.tag_member.name}`` is "
  279. f"``{variant.name}``: "
  280. f"The members of :qapi:type:`{variant.type.name}`.",
  281. ent.info,
  282. "q_dummy",
  283. )
  284. def visit_sections(self, ent: QAPISchemaDefinition) -> None:
  285. sections = ent.doc.all_sections if ent.doc else []
  286. # Determine the index location at which we should generate
  287. # documentation for "The members of ..." pointers. This should
  288. # go at the end of the members section(s) if any. Note that
  289. # index 0 is assumed to be a plain intro section, even if it is
  290. # empty; and that a members section if present will always
  291. # immediately follow the opening PLAIN section.
  292. gen_index = 1
  293. if len(sections) > 1:
  294. while sections[gen_index].kind == QAPIDoc.Kind.MEMBER:
  295. gen_index += 1
  296. if gen_index >= len(sections):
  297. break
  298. # Add sections in source order:
  299. for i, section in enumerate(sections):
  300. # @var is translated to ``var``:
  301. section.text = re.sub(r"@([\w-]+)", r"``\1``", section.text)
  302. if section.kind == QAPIDoc.Kind.PLAIN:
  303. self.visit_paragraph(section)
  304. elif section.kind == QAPIDoc.Kind.MEMBER:
  305. assert isinstance(section, QAPIDoc.ArgSection)
  306. self.visit_member(section)
  307. elif section.kind == QAPIDoc.Kind.FEATURE:
  308. assert isinstance(section, QAPIDoc.ArgSection)
  309. self.visit_feature(section)
  310. elif section.kind in (QAPIDoc.Kind.SINCE, QAPIDoc.Kind.TODO):
  311. # Since is handled in preamble, TODO is skipped intentionally.
  312. pass
  313. elif section.kind == QAPIDoc.Kind.RETURNS:
  314. self.visit_returns(section)
  315. elif section.kind == QAPIDoc.Kind.ERRORS:
  316. self.visit_errors(section)
  317. else:
  318. assert False
  319. # Generate "The members of ..." entries if necessary:
  320. if i == gen_index - 1:
  321. self._insert_member_pointer(ent)
  322. self.ensure_blank_line()
  323. # Transmogrification core methods
  324. def visit_module(self, path: str) -> None:
  325. name = Path(path).stem
  326. # module directives are credited to the first line of a module file.
  327. self.add_line_raw(f".. qapi:module:: {name}", path, 1)
  328. self.ensure_blank_line()
  329. def visit_freeform(self, doc: QAPIDoc) -> None:
  330. # TODO: Once the old qapidoc transformer is deprecated, freeform
  331. # sections can be updated to pure rST, and this transformed removed.
  332. #
  333. # For now, translate our micro-format into rST. Code adapted
  334. # from Peter Maydell's freeform().
  335. assert len(doc.all_sections) == 1, doc.all_sections
  336. body = doc.all_sections[0]
  337. text = body.text
  338. info = doc.info
  339. if re.match(r"=+ ", text):
  340. # Section/subsection heading (if present, will always be the
  341. # first line of the block)
  342. (heading, _, text) = text.partition("\n")
  343. (leader, _, heading) = heading.partition(" ")
  344. # Implicit +1 for heading in the containing .rst doc
  345. level = len(leader) + 1
  346. # https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#sections
  347. markers = ' #*=_^"'
  348. overline = level <= 2
  349. marker = markers[level]
  350. self.ensure_blank_line()
  351. # This credits all 2 or 3 lines to the single source line.
  352. if overline:
  353. self.add_line(marker * len(heading), info)
  354. self.add_line(heading, info)
  355. self.add_line(marker * len(heading), info)
  356. self.ensure_blank_line()
  357. # Eat blank line(s) and advance info
  358. trimmed = text.lstrip("\n")
  359. text = trimmed
  360. info = info.next_line(len(text) - len(trimmed) + 1)
  361. self.add_lines(text, info)
  362. self.ensure_blank_line()
  363. def visit_entity(self, ent: QAPISchemaDefinition) -> None:
  364. assert ent.info
  365. try:
  366. self._curr_ent = ent
  367. # Squish structs and unions together into an "object" directive.
  368. meta = ent.meta
  369. if meta in ("struct", "union"):
  370. meta = "object"
  371. # This line gets credited to the start of the /definition/.
  372. self.add_line(f".. qapi:{meta}:: {ent.name}", ent.info)
  373. with self.indented():
  374. self.preamble(ent)
  375. self.visit_sections(ent)
  376. finally:
  377. self._curr_ent = None
  378. class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
  379. """A QAPI schema visitor which adds Sphinx dependencies each module
  380. This class calls the Sphinx note_dependency() function to tell Sphinx
  381. that the generated documentation output depends on the input
  382. schema file associated with each module in the QAPI input.
  383. """
  384. def __init__(self, env: Any, qapidir: str) -> None:
  385. self._env = env
  386. self._qapidir = qapidir
  387. def visit_module(self, name: str) -> None:
  388. if name != "./builtin":
  389. qapifile = self._qapidir + "/" + name
  390. self._env.note_dependency(os.path.abspath(qapifile))
  391. super().visit_module(name)
  392. class NestedDirective(SphinxDirective):
  393. def run(self) -> Sequence[nodes.Node]:
  394. raise NotImplementedError
  395. def do_parse(self, rstlist: StringList, node: nodes.Node) -> None:
  396. """
  397. Parse rST source lines and add them to the specified node
  398. Take the list of rST source lines rstlist, parse them as
  399. rST, and add the resulting docutils nodes as children of node.
  400. The nodes are parsed in a way that allows them to include
  401. subheadings (titles) without confusing the rendering of
  402. anything else.
  403. """
  404. with switch_source_input(self.state, rstlist):
  405. nested_parse_with_titles(self.state, rstlist, node)
  406. class QAPIDocDirective(NestedDirective):
  407. """Extract documentation from the specified QAPI .json file"""
  408. required_argument = 1
  409. optional_arguments = 1
  410. option_spec = {
  411. "qapifile": directives.unchanged_required,
  412. "transmogrify": directives.flag,
  413. }
  414. has_content = False
  415. def new_serialno(self) -> str:
  416. """Return a unique new ID string suitable for use as a node's ID"""
  417. env = self.state.document.settings.env
  418. return "qapidoc-%d" % env.new_serialno("qapidoc")
  419. def transmogrify(self, schema: QAPISchema) -> nodes.Element:
  420. logger.info("Transmogrifying QAPI to rST ...")
  421. vis = Transmogrifier()
  422. modules = set()
  423. for doc in schema.docs:
  424. module_source = doc.info.fname
  425. if module_source not in modules:
  426. vis.visit_module(module_source)
  427. modules.add(module_source)
  428. if doc.symbol:
  429. ent = schema.lookup_entity(doc.symbol)
  430. assert isinstance(ent, QAPISchemaDefinition)
  431. vis.visit_entity(ent)
  432. else:
  433. vis.visit_freeform(doc)
  434. logger.info("Transmogrification complete.")
  435. contentnode = nodes.section()
  436. content = vis.result
  437. titles_allowed = True
  438. logger.info("Transmogrifier running nested parse ...")
  439. with switch_source_input(self.state, content):
  440. if titles_allowed:
  441. node: nodes.Element = nodes.section()
  442. node.document = self.state.document
  443. nested_parse_with_titles(self.state, content, contentnode)
  444. else:
  445. node = nodes.paragraph()
  446. node.document = self.state.document
  447. self.state.nested_parse(content, 0, contentnode)
  448. logger.info("Transmogrifier's nested parse completed.")
  449. if self.env.app.verbosity >= 2 or os.environ.get("DEBUG"):
  450. argname = "_".join(Path(self.arguments[0]).parts)
  451. name = Path(argname).stem + ".ir"
  452. self.write_intermediate(content, name)
  453. sys.stdout.flush()
  454. return contentnode
  455. def write_intermediate(self, content: StringList, filename: str) -> None:
  456. logger.info(
  457. "writing intermediate rST for '%s' to '%s'",
  458. self.arguments[0],
  459. filename,
  460. )
  461. srctree = Path(self.env.app.config.qapidoc_srctree).resolve()
  462. outlines = []
  463. lcol_width = 0
  464. for i, line in enumerate(content):
  465. src, lineno = content.info(i)
  466. srcpath = Path(src).resolve()
  467. srcpath = srcpath.relative_to(srctree)
  468. lcol = f"{srcpath}:{lineno:04d}"
  469. lcol_width = max(lcol_width, len(lcol))
  470. outlines.append((lcol, line))
  471. with open(filename, "w", encoding="UTF-8") as outfile:
  472. for lcol, rcol in outlines:
  473. outfile.write(lcol.rjust(lcol_width))
  474. outfile.write(" |")
  475. if rcol:
  476. outfile.write(f" {rcol}")
  477. outfile.write("\n")
  478. def legacy(self, schema: QAPISchema) -> nodes.Element:
  479. vis = QAPISchemaGenRSTVisitor(self)
  480. vis.visit_begin(schema)
  481. for doc in schema.docs:
  482. if doc.symbol:
  483. vis.symbol(doc, schema.lookup_entity(doc.symbol))
  484. else:
  485. vis.freeform(doc)
  486. return vis.get_document_node() # type: ignore
  487. def run(self) -> Sequence[nodes.Node]:
  488. env = self.state.document.settings.env
  489. qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0]
  490. qapidir = os.path.dirname(qapifile)
  491. transmogrify = "transmogrify" in self.options
  492. try:
  493. schema = QAPISchema(qapifile)
  494. # First tell Sphinx about all the schema files that the
  495. # output documentation depends on (including 'qapifile' itself)
  496. schema.visit(QAPISchemaGenDepVisitor(env, qapidir))
  497. except QAPIError as err:
  498. # Launder QAPI parse errors into Sphinx extension errors
  499. # so they are displayed nicely to the user
  500. raise ExtensionError(str(err)) from err
  501. if transmogrify:
  502. contentnode = self.transmogrify(schema)
  503. else:
  504. contentnode = self.legacy(schema)
  505. return contentnode.children
  506. class QMPExample(CodeBlock, NestedDirective):
  507. """
  508. Custom admonition for QMP code examples.
  509. When the :annotated: option is present, the body of this directive
  510. is parsed as normal rST, but with any '::' code blocks set to use
  511. the QMP lexer. Code blocks must be explicitly written by the user,
  512. but this allows for intermingling explanatory paragraphs with
  513. arbitrary rST syntax and code blocks for more involved examples.
  514. When :annotated: is absent, the directive body is treated as a
  515. simple standalone QMP code block literal.
  516. """
  517. required_argument = 0
  518. optional_arguments = 0
  519. has_content = True
  520. option_spec = {
  521. "annotated": directives.flag,
  522. "title": directives.unchanged,
  523. }
  524. def _highlightlang(self) -> addnodes.highlightlang:
  525. """Return the current highlightlang setting for the document"""
  526. node = None
  527. doc = self.state.document
  528. if hasattr(doc, "findall"):
  529. # docutils >= 0.18.1
  530. for node in doc.findall(addnodes.highlightlang):
  531. pass
  532. else:
  533. for elem in doc.traverse():
  534. if isinstance(elem, addnodes.highlightlang):
  535. node = elem
  536. if node:
  537. return node
  538. # No explicit directive found, use defaults
  539. node = addnodes.highlightlang(
  540. lang=self.env.config.highlight_language,
  541. force=False,
  542. # Yes, Sphinx uses this value to effectively disable line
  543. # numbers and not 0 or None or -1 or something. ¯\_(ツ)_/¯
  544. linenothreshold=sys.maxsize,
  545. )
  546. return node
  547. def admonition_wrap(self, *content: nodes.Node) -> List[nodes.Node]:
  548. title = "Example:"
  549. if "title" in self.options:
  550. title = f"{title} {self.options['title']}"
  551. admon = nodes.admonition(
  552. "",
  553. nodes.title("", title),
  554. *content,
  555. classes=["admonition", "admonition-example"],
  556. )
  557. return [admon]
  558. def run_annotated(self) -> List[nodes.Node]:
  559. lang_node = self._highlightlang()
  560. content_node: nodes.Element = nodes.section()
  561. # Configure QMP highlighting for "::" blocks, if needed
  562. if lang_node["lang"] != "QMP":
  563. content_node += addnodes.highlightlang(
  564. lang="QMP",
  565. force=False, # "True" ignores lexing errors
  566. linenothreshold=lang_node["linenothreshold"],
  567. )
  568. self.do_parse(self.content, content_node)
  569. # Restore prior language highlighting, if needed
  570. if lang_node["lang"] != "QMP":
  571. content_node += addnodes.highlightlang(**lang_node.attributes)
  572. return content_node.children
  573. def run(self) -> List[nodes.Node]:
  574. annotated = "annotated" in self.options
  575. if annotated:
  576. content_nodes = self.run_annotated()
  577. else:
  578. self.arguments = ["QMP"]
  579. content_nodes = super().run()
  580. return self.admonition_wrap(*content_nodes)
  581. def setup(app: Sphinx) -> ExtensionMetadata:
  582. """Register qapi-doc directive with Sphinx"""
  583. app.setup_extension("qapi_domain")
  584. app.add_config_value("qapidoc_srctree", None, "env")
  585. app.add_directive("qapi-doc", QAPIDocDirective)
  586. app.add_directive("qmp-example", QMPExample)
  587. return {
  588. "version": __version__,
  589. "parallel_read_safe": True,
  590. "parallel_write_safe": True,
  591. }