docs/qapi-domain: add QAPI xref roles

Add domain-specific cross-reference syntax. As of this commit, that
means new :qapi:any:`block-core` referencing syntax.

The :any: role will find anything registered to the QAPI domain,
including modules, commands, events, etc.

Creating the cross-references is powered by the QAPIXRefRole class;
resolving them is handled by QAPIDomain.resolve_xref().

QAPIXrefRole is based heavily on Sphinx's own PyXrefRole, with
modifications necessary for QAPI features.

Signed-off-by: John Snow <jsnow@redhat.com>
Message-ID: <20250311034303.75779-9-jsnow@redhat.com>
Acked-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
This commit is contained in:
John Snow 2025-03-10 23:42:06 -04:00 committed by Markus Armbruster
parent dca2f3c471
commit 760b37e1df

View file

@ -26,6 +26,7 @@ from sphinx.domains import (
ObjType,
)
from sphinx.locale import _, __
from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.nodes import make_refnode
@ -47,6 +48,54 @@ class ObjectEntry(NamedTuple):
aliased: bool
class QAPIXRefRole(XRefRole):
def process_link(
self,
env: BuildEnvironment,
refnode: Element,
has_explicit_title: bool,
title: str,
target: str,
) -> tuple[str, str]:
refnode["qapi:module"] = env.ref_context.get("qapi:module")
# Cross-references that begin with a tilde adjust the title to
# only show the reference without a leading module, even if one
# was provided. This is a Sphinx-standard syntax; give it
# priority over QAPI-specific type markup below.
hide_module = False
if target.startswith("~"):
hide_module = True
target = target[1:]
# Type names that end with "?" are considered optional
# arguments and should be documented as such, but it's not
# part of the xref itself.
if target.endswith("?"):
refnode["qapi:optional"] = True
target = target[:-1]
# Type names wrapped in brackets denote lists. strip the
# brackets and remember to add them back later.
if target.startswith("[") and target.endswith("]"):
refnode["qapi:array"] = True
target = target[1:-1]
if has_explicit_title:
# Don't mess with the title at all if it was explicitly set.
# Explicit title syntax for references is e.g.
# :qapi:type:`target <explicit title>`
# and this explicit title overrides everything else here.
return title, target
title = target
if hide_module:
title = target.split(".")[-1]
return title, target
class QAPIIndex(Index):
"""
Index subclass to provide the QAPI definition index.
@ -118,7 +167,13 @@ class QAPIDomain(Domain):
object_types: Dict[str, ObjType] = {}
directives = {}
roles = {}
# These are all cross-reference roles; e.g.
# :qapi:cmd:`query-block`. The keys correlate to the names used in
# the object_types table values above.
roles = {
"any": QAPIXRefRole(), # reference *any* type of QAPI object.
}
# Moved into the data property at runtime;
# this is the internal index of reference-able objects.
@ -252,6 +307,37 @@ class QAPIDomain(Domain):
matches = [m for m in matches if not m[1].aliased]
return matches
def resolve_xref(
self,
env: BuildEnvironment,
fromdocname: str,
builder: Builder,
typ: str,
target: str,
node: pending_xref,
contnode: Element,
) -> nodes.reference | None:
modname = node.get("qapi:module")
matches = self.find_obj(modname, target, typ)
if not matches:
return None
if len(matches) > 1:
logger.warning(
__("more than one target found for cross-reference %r: %s"),
target,
", ".join(match[0] for match in matches),
type="ref",
subtype="qapi",
location=node,
)
name, obj = matches[0]
return make_refnode(
builder, fromdocname, obj.docname, obj.node_id, contnode, name
)
def resolve_any_xref(
self,
env: BuildEnvironment,