Browse Source

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 <ref>' 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.
Matthew Woehlke 2 years ago
parent
commit
cc21d0e478
2 changed files with 32 additions and 13 deletions
  1. 3 3
      Help/dev/documentation.rst
  2. 29 10
      Utilities/Sphinx/cmake.py

+ 3 - 3
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
   abbreviate it in the ``signature`` directive argument and specify the full
   signature in a ``code-block`` in the description.
   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"
 * Default target names are automatically extracted from leading "keyword"
   arguments in the signatures, where a keyword is any sequence of
   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
 * The targets may be referenced from within the same document using
   ```REF`_`` or ```TEXT <REF_>`_`` syntax.  Like reStructuredText section
   ```REF`_`` or ```TEXT <REF_>`_`` 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).
 The directive treats its content as the documentation of the signature(s).
 Indent the signature documentation accordingly.
 Indent the signature documentation accordingly.

+ 29 - 10
Utilities/Sphinx/cmake.py

@@ -360,7 +360,7 @@ class CMakeSignatureObject(CMakeObject):
 
 
     def add_target_and_index(self, name, sig, signode):
     def add_target_and_index(self, name, sig, signode):
         if name in self.targetnames:
         if name in self.targetnames:
-            targetname = self.targetnames[name].lower()
+            sigargs = self.targetnames[name]
         else:
         else:
             def extract_keywords(params):
             def extract_keywords(params):
                 for p in params:
                 for p in params:
@@ -370,7 +370,8 @@ class CMakeSignatureObject(CMakeObject):
                         return
                         return
 
 
             keywords = extract_keywords(name.split('(')[1].split())
             keywords = extract_keywords(name.split('(')[1].split())
-            targetname = ' '.join(keywords).lower()
+            sigargs = ' '.join(keywords)
+        targetname = sigargs.lower()
         targetid = nodes.make_id(targetname)
         targetid = nodes.make_id(targetname)
 
 
         if targetid not in self.state.document.ids:
         if targetid not in self.state.document.ids:
@@ -379,6 +380,15 @@ class CMakeSignatureObject(CMakeObject):
             signode['first'] = (not self.names)
             signode['first'] = (not self.names)
             self.state.document.note_explicit_target(signode)
             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):
     def run(self):
         targets = self.options.get('target')
         targets = self.options.get('target')
         if targets is not None:
         if targets is not None:
@@ -393,19 +403,15 @@ class CMakeXRefRole(XRefRole):
 
 
     # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'.
     # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'.
     _re = re.compile(r'^(.+?)(\s*)(?<!\x00)<(.*?)>$', re.DOTALL)
     _re = re.compile(r'^(.+?)(\s*)(?<!\x00)<(.*?)>$', 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_genex = re.compile(r'^\$<([^<>:]+)(:[^<>]+)?>$', re.DOTALL)
     _re_guide = re.compile(r'^([^<>/]+)/([^<>]*)$', re.DOTALL)
     _re_guide = re.compile(r'^([^<>/]+)/([^<>]*)$', re.DOTALL)
 
 
     def __call__(self, typ, rawtext, text, *args, **keys):
     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) <command_name>`
         if typ == 'cmake: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':
         elif typ == 'cmake:genex':
             m = CMakeXRefRole._re_genex.match(text)
             m = CMakeXRefRole._re_genex.match(text)
             if m:
             if m:
@@ -461,6 +467,10 @@ class CMakeXRefTransform(Transform):
                 # Do not index cross-references to guide sections.
                 # Do not index cross-references to guide sections.
                 continue
                 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))
             targetnum = env.new_serialno('index-%s:%s' % (objtype, objname))
 
 
             targetid = 'index-%s-%s:%s' % (targetnum, objtype, objname)
             targetid = 'index-%s-%s:%s' % (targetnum, objtype, objname)
@@ -537,6 +547,15 @@ class CMakeDomain(Domain):
                      typ, target, node, contnode):
                      typ, target, node, contnode):
         targetid = f'{typ}:{target}'
         targetid = f'{typ}:{target}'
         obj = self.data['objects'].get(targetid)
         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:
         if obj is None:
             # TODO: warn somehow?
             # TODO: warn somehow?
             return None
             return None