From 37e015d4a65566386266a99c70226b4082defce2 Mon Sep 17 00:00:00 2001 From: Matthew Woehlke Date: Wed, 8 Mar 2023 13:19:50 -0500 Subject: Utilities/Sphinx: Refactor Sphinx reference recording Rewrite how we record objects in our Sphinx extensions to more closely conform to how other domains do likewise, and to store more information than was previously being stored. This is a first step toward being able to record and reference signatures. --- Utilities/Sphinx/cmake.py | 57 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/Utilities/Sphinx/cmake.py b/Utilities/Sphinx/cmake.py index 9043709..fb22767 100644 --- a/Utilities/Sphinx/cmake.py +++ b/Utilities/Sphinx/cmake.py @@ -4,6 +4,9 @@ import os import re +from dataclasses import dataclass +from typing import Any, cast + # Override much of pygments' CMakeLexer. # We need to parse CMake syntax definitions, not CMake code. @@ -69,9 +72,11 @@ from sphinx.directives import ObjectDescription, nl_escape_re from sphinx.domains import Domain, ObjType from sphinx.roles import XRefRole from sphinx.util.nodes import make_refnode -from sphinx.util import ws_re +from sphinx.util import logging, ws_re from sphinx import addnodes +logger = logging.getLogger(__name__) + sphinx_before_1_4 = False sphinx_before_1_7_2 = False try: @@ -104,6 +109,14 @@ if sphinx_before_1_7_2: return new_items QtHelpBuilder.build_keywords = new_build_keywords +@dataclass +class ObjectEntry: + docname: str + objtype: str + node_id: str + name: str + + class CMakeModule(Directive): required_arguments = 1 optional_arguments = 0 @@ -205,14 +218,6 @@ _cmake_index_objs = { 'variable': _cmake_index_entry('variable'), } -def _cmake_object_inventory(env, document, line, objtype, targetid): - inv = env.domaindata['cmake']['objects'] - if targetid in inv: - document.reporter.warning( - 'CMake object "%s" also described in "%s".' % - (targetid, env.doc2path(inv[targetid][0])), line=line) - inv[targetid] = (env.docname, objtype) - class CMakeTransform(Transform): # Run this transform early since we insert nodes we want @@ -275,8 +280,10 @@ class CMakeTransform(Transform): indexnode = addnodes.index() indexnode['entries'] = [make_index_entry(title, targetid)] self.document.insert(0, indexnode) + # Add to cmake domain object inventory - _cmake_object_inventory(env, self.document, 1, objtype, targetid) + domain = cast(CMakeDomain, env.get_domain('cmake')) + domain.note_object(objtype, targetname, targetid, targetid) class CMakeObject(ObjectDescription): @@ -300,8 +307,10 @@ class CMakeObject(ObjectDescription): signode['ids'].append(targetid) signode['first'] = (not self.names) self.state.document.note_explicit_target(signode) - _cmake_object_inventory(self.env, self.state.document, - self.lineno, self.objtype, targetid) + + domain = cast(CMakeDomain, self.env.get_domain('cmake')) + domain.note_object(self.objtype, targetname, targetid, targetid, + location=signode) make_index_entry = _cmake_index_objs.get(self.objtype) if make_index_entry: @@ -518,25 +527,37 @@ class CMakeDomain(Domain): def clear_doc(self, docname): to_clear = set() - for fullname, (fn, _) in self.data['objects'].items(): - if fn == docname: + for fullname, obj in self.data['objects'].items(): + if obj.docname == docname: to_clear.add(fullname) for fullname in to_clear: del self.data['objects'][fullname] def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): - targetid = '%s:%s' % (typ, target) + targetid = f'{typ}:{target}' obj = self.data['objects'].get(targetid) if obj is None: # TODO: warn somehow? return None - return make_refnode(builder, fromdocname, obj[0], targetid, + + return make_refnode(builder, fromdocname, obj.docname, obj.node_id, contnode, target) + def note_object(self, objtype: str, name: str, target_id: str, + node_id: str, location: Any = None): + if target_id in self.data['objects']: + other = self.data['objects'][target_id].docname + logger.warning( + f'CMake object {target_id!r} also described in {other!r}', + location=location) + + self.data['objects'][target_id] = ObjectEntry( + self.env.docname, objtype, node_id, name) + def get_objects(self): - for refname, (docname, type) in self.data['objects'].items(): - yield (refname, refname, type, docname, refname, 1) + for refname, obj in self.data['objects'].items(): + yield (refname, obj.name, obj.objtype, obj.docname, obj.node_id, 1) def setup(app): app.add_directive('cmake-module', CMakeModule) -- cgit v0.12 From cc21d0e47802fb1d054eb56b8bb4bd8ee66f9caa Mon Sep 17 00:00:00 2001 From: Matthew Woehlke Date: Thu, 9 Mar 2023 11:32:07 -0500 Subject: Utilities/Sphinx: Make signatures linkable Add signatures to the collection of observed objects (which can be referenced elsewhere). Don't automatically strip parameters from a :command: reference, as these may now link signatures. (Do, however, munge them into 'text ' form if they aren't already, as not doing so adds an extra '()' for some reason.) Correspondingly, change xref resolution to try to match 'command' when a ref like 'command(args)' is not matched, so that existing links to commands that have not been converted to use the new signature directive don't immediately break. --- Help/dev/documentation.rst | 6 +++--- Utilities/Sphinx/cmake.py | 39 +++++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/Help/dev/documentation.rst b/Help/dev/documentation.rst index a340739..8cd71b4 100644 --- a/Help/dev/documentation.rst +++ b/Help/dev/documentation.rst @@ -270,8 +270,7 @@ The ``signature`` directive requires one argument, the signature summary: abbreviate it in the ``signature`` directive argument and specify the full signature in a ``code-block`` in the description. -The ``signature`` directive generates a document-local hyperlink target -for each signature: +The ``signature`` directive generates a hyperlink target for each signature: * Default target names are automatically extracted from leading "keyword" arguments in the signatures, where a keyword is any sequence of @@ -299,7 +298,8 @@ for each signature: * The targets may be referenced from within the same document using ```REF`_`` or ```TEXT `_`` syntax. Like reStructuredText section - headers, the targets do not work with Sphinx ``:ref:`` syntax. + headers, the targets do not work with Sphinx ``:ref:`` syntax, however + they can be globally referenced using e.g. ``:command:`string(APPEND)```. The directive treats its content as the documentation of the signature(s). Indent the signature documentation accordingly. diff --git a/Utilities/Sphinx/cmake.py b/Utilities/Sphinx/cmake.py index fb22767..a07b6e9 100644 --- a/Utilities/Sphinx/cmake.py +++ b/Utilities/Sphinx/cmake.py @@ -360,7 +360,7 @@ class CMakeSignatureObject(CMakeObject): def add_target_and_index(self, name, sig, signode): if name in self.targetnames: - targetname = self.targetnames[name].lower() + sigargs = self.targetnames[name] else: def extract_keywords(params): for p in params: @@ -370,7 +370,8 @@ class CMakeSignatureObject(CMakeObject): return keywords = extract_keywords(name.split('(')[1].split()) - targetname = ' '.join(keywords).lower() + sigargs = ' '.join(keywords) + targetname = sigargs.lower() targetid = nodes.make_id(targetname) if targetid not in self.state.document.ids: @@ -379,6 +380,15 @@ class CMakeSignatureObject(CMakeObject): signode['first'] = (not self.names) self.state.document.note_explicit_target(signode) + # Register the signature as a command object. + command = name.split('(')[0].lower() + refname = f'{command}({sigargs})' + refid = f'command:{command}({targetname})' + + domain = cast(CMakeDomain, self.env.get_domain('cmake')) + domain.note_object('command', name=refname, target_id=refid, + node_id=targetid, location=signode) + def run(self): targets = self.options.get('target') if targets is not None: @@ -393,19 +403,15 @@ class CMakeXRefRole(XRefRole): # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'. _re = re.compile(r'^(.+?)(\s*)(?$', re.DOTALL) - _re_sub = re.compile(r'^([^()\s]+)\s*\(([^()]*)\)$', re.DOTALL) + _re_ref = re.compile(r'^.*\s<\w+([(][\w\s]+[)])?>$', re.DOTALL) _re_genex = re.compile(r'^\$<([^<>:]+)(:[^<>]+)?>$', re.DOTALL) _re_guide = re.compile(r'^([^<>/]+)/([^<>]*)$', re.DOTALL) def __call__(self, typ, rawtext, text, *args, **keys): - # Translate CMake command cross-references of the form: - # `command_name(SUB_COMMAND)` - # to have an explicit target: - # `command_name(SUB_COMMAND) ` if typ == 'cmake:command': - m = CMakeXRefRole._re_sub.match(text) - if m: - text = '%s <%s>' % (text, m.group(1)) + m = CMakeXRefRole._re_ref.match(text) + if m is None: + text = f'{text} <{text}>' elif typ == 'cmake:genex': m = CMakeXRefRole._re_genex.match(text) if m: @@ -461,6 +467,10 @@ class CMakeXRefTransform(Transform): # Do not index cross-references to guide sections. continue + if objtype == 'command': + # Index signature references to their parent command. + objname = objname.split('(')[0].lower() + targetnum = env.new_serialno('index-%s:%s' % (objtype, objname)) targetid = 'index-%s-%s:%s' % (targetnum, objtype, objname) @@ -537,6 +547,15 @@ class CMakeDomain(Domain): typ, target, node, contnode): targetid = f'{typ}:{target}' obj = self.data['objects'].get(targetid) + + if obj is None and typ == 'command': + # If 'command(args)' wasn't found, try just 'command'. + # TODO: remove this fallback? warn? + # logger.warning(f'no match for {targetid}') + command = target.split('(')[0] + targetid = f'{typ}:{command}' + obj = self.data['objects'].get(targetid) + if obj is None: # TODO: warn somehow? return None -- cgit v0.12