Browse Source

Utilities/Sphinx: Add a directive to document command signatures

Add a `signature` directive to offer a CMake version of Sphinx's
`function` directive, similar to that found in other domains (py, cpp,
etc.).  Like others, this takes one or more signatures as arguments and
creates dt/dd nodes from the signatures and the directive contents.
Matthew Woehlke 2 năm trước cách đây
mục cha
commit
74e3c1d313

+ 82 - 13
Help/dev/documentation.rst

@@ -241,6 +241,69 @@ Document a "genex" object:
 
 
 The directive requires a single argument, the generator expression name.
 The directive requires a single argument, the generator expression name.
 
 
+``signature`` directive
+^^^^^^^^^^^^^^^^^^^^^^^
+
+Document `CMake Command Signatures <Style: CMake Command Signatures_>`_
+within a ``Help/command/<command-name>.rst`` document.
+
+.. code-block:: rst
+
+  .. signature:: <command-name>(<signature>)
+
+    This indented block documents one or more signatures of a CMake command.
+
+The ``signature`` directive requires one argument, the signature summary:
+
+* One or more signatures must immediately follow the ``::``.
+  The first signature may optionally be placed on the same line.
+  A blank line following the ``signature`` directive will result in a
+  documentation generation error: ``1 argument(s) required, 0 supplied``.
+
+* Signatures may be split across multiple lines, but the final ``)`` of each
+  signature must be the last character on its line.
+
+* Blank lines between signatures are not allowed.  (Content after a blank line
+  is treated as part of the description.)
+
+* Whitespace in signatures is not preserved.  To document a complex signature,
+  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:
+
+* Default target names are automatically extracted from leading "keyword"
+  arguments in the signatures, where a keyword is any sequence of
+  non-space starting with a letter.  For example, the signature
+  ``string(REGEX REPLACE <match-regex> ...)`` generates the target
+  ``REGEX REPLACE``, similar to ``.. _`REGEX REPLACE`:``.
+
+* Custom target names may be specified using a ``:target:`` option.
+  For example:
+
+  .. code-block:: rst
+
+    .. signature::
+      cmake_path(GET <path-var> ROOT_NAME <out-var>)
+      cmake_path(GET <path-var> ROOT_PATH <out-var>)
+      :target:
+        GET ROOT_NAME
+        GET ROOT_PATH
+
+  Provide a custom target name for each signature, one per line.
+  The first target may optionally be placed on the same line as ``:target:``.
+
+* If a target name is already in use earlier in the document, no hyperlink
+  target will be generated.
+
+* The targets may be referenced from within the same document using
+  ```REF`_`` or ```TEXT <REF_>`_`` syntax.  Like reStructuredText section
+  headers, the targets do not work with Sphinx ``:ref:`` syntax.
+
+The directive treats its content as the documentation of the signature(s).
+Indent the signature documentation accordingly.
+
 ``variable`` directive
 ``variable`` directive
 ^^^^^^^^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^^^^^^^^
 
 
@@ -374,11 +437,11 @@ paragraph.
 Style: CMake Command Signatures
 Style: CMake Command Signatures
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 
-Command signatures should be marked up as plain literal blocks, not as
-cmake ``code-blocks``.
-
-Signatures are separated from preceding content by a section header.
-That is, use:
+A ``Help/command/<command-name>.rst`` document defines one ``command``
+object in the `CMake Domain`_, but some commands have multiple signatures.
+Use the CMake Domain's `signature directive`_ to document each signature.
+Separate signatures from preceding content by a section header.
+For example:
 
 
 .. code-block:: rst
 .. code-block:: rst
 
 
@@ -387,17 +450,23 @@ That is, use:
   Normal Libraries
   Normal Libraries
   ^^^^^^^^^^^^^^^^
   ^^^^^^^^^^^^^^^^
 
 
-  ::
-
+  .. signature::
     add_library(<lib> ...)
     add_library(<lib> ...)
 
 
-  This signature is used for ...
+    This signature is used for ...
+
+Use the following conventions in command signature documentation:
+
+* Use an angle-bracket ``<placeholder>`` for arguments to be specified
+  by the caller.  Refer to them in prose using
+  `inline literal <Style: Inline Literals_>`_ syntax.
+
+* Wrap optional parts with square brackets.
+
+* Mark repeatable parts with a trailing ellipsis (``...``).
 
 
-Signatures of commands should wrap optional parts with square brackets,
-and should mark list of optional arguments with an ellipsis (``...``).
-Elements of the signature which are specified by the user should be
-specified with angle brackets, and may be referred to in prose using
-``inline-literal`` syntax.
+The ``signature`` directive may be used multiple times for different
+signatures of the same command.
 
 
 Style: Boolean Constants
 Style: Boolean Constants
 ^^^^^^^^^^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^^^^^^^^^^

+ 1 - 1
Source/cmRST.cxx

@@ -20,7 +20,7 @@ cmRST::cmRST(std::ostream& os, std::string docroot)
   : OS(os)
   : OS(os)
   , DocRoot(std::move(docroot))
   , DocRoot(std::move(docroot))
   , CMakeDirective("^.. (cmake:)?("
   , CMakeDirective("^.. (cmake:)?("
-                   "command|envvar|genex|variable"
+                   "command|envvar|genex|signature|variable"
                    ")::[ \t]+([^ \t\n]+)$")
                    ")::[ \t]+([^ \t\n]+)$")
   , CMakeModuleDirective("^.. cmake-module::[ \t]+([^ \t\n]+)$")
   , CMakeModuleDirective("^.. cmake-module::[ \t]+([^ \t\n]+)$")
   , ParsedLiteralDirective("^.. parsed-literal::[ \t]*(.*)$")
   , ParsedLiteralDirective("^.. parsed-literal::[ \t]*(.*)$")

+ 8 - 0
Tests/CMakeLib/testRST.expect

@@ -70,6 +70,14 @@ Bracket Comment Content
 
 
    Generator expression $<OTHER_GENEX> description.
    Generator expression $<OTHER_GENEX> description.
 
 
+.. cmake:signature:: some_command(SOME_SIGNATURE)
+
+   Command some_command SOME_SIGNATURE description.
+
+.. signature:: other_command(OTHER_SIGNATURE)
+
+   Command other_command OTHER_SIGNATURE description.
+
 .. cmake:variable:: some_var
 .. cmake:variable:: some_var
 
 
    Variable some_var description.
    Variable some_var description.

+ 8 - 0
Tests/CMakeLib/testRST.rst

@@ -73,6 +73,14 @@ Inline literal ``__`` followed by inline link `Link Text <InternalDest_>`_.
 
 
    Generator expression $<OTHER_GENEX> description.
    Generator expression $<OTHER_GENEX> description.
 
 
+.. cmake:signature:: some_command(SOME_SIGNATURE)
+
+   Command some_command SOME_SIGNATURE description.
+
+.. signature:: other_command(OTHER_SIGNATURE)
+
+   Command other_command OTHER_SIGNATURE description.
+
 .. cmake:variable:: some_var
 .. cmake:variable:: some_var
 
 
    Variable some_var description.
    Variable some_var description.

+ 82 - 3
Utilities/Sphinx/cmake.py

@@ -16,6 +16,9 @@ from pygments.lexers import CMakeLexer
 from pygments.token import Name, Operator, Punctuation, String, Text, Comment, Generic, Whitespace, Number
 from pygments.token import Name, Operator, Punctuation, String, Text, Comment, Generic, Whitespace, Number
 from pygments.lexer import bygroups
 from pygments.lexer import bygroups
 
 
+# RE to split multiple command signatures
+sig_end_re = re.compile(r'(?<=[)])\n')
+
 # Notes on regular expressions below:
 # Notes on regular expressions below:
 # - [\.\+-] are needed for string constants like gtk+-2.0
 # - [\.\+-] are needed for string constants like gtk+-2.0
 # - Unix paths are recognized by '/'; support for Windows paths may be added if needed
 # - Unix paths are recognized by '/'; support for Windows paths may be added if needed
@@ -57,14 +60,16 @@ CMakeLexer.tokens["root"] = [
   #  (r'[^<>\])\}\|$"# \t\n]+', Name.Exception),            # fallback, for debugging only
   #  (r'[^<>\])\}\|$"# \t\n]+', Name.Exception),            # fallback, for debugging only
 ]
 ]
 
 
+from docutils.utils.code_analyzer import Lexer, LexerError
 from docutils.parsers.rst import Directive, directives
 from docutils.parsers.rst import Directive, directives
 from docutils.transforms import Transform
 from docutils.transforms import Transform
 from docutils import io, nodes
 from docutils import io, nodes
 
 
-from sphinx.directives import ObjectDescription
+from sphinx.directives import ObjectDescription, nl_escape_re
 from sphinx.domains import Domain, ObjType
 from sphinx.domains import Domain, ObjType
 from sphinx.roles import XRefRole
 from sphinx.roles import XRefRole
 from sphinx.util.nodes import make_refnode
 from sphinx.util.nodes import make_refnode
+from sphinx.util import ws_re
 from sphinx import addnodes
 from sphinx import addnodes
 
 
 sphinx_before_1_4 = False
 sphinx_before_1_4 = False
@@ -286,9 +291,9 @@ class CMakeObject(ObjectDescription):
 
 
     def add_target_and_index(self, name, sig, signode):
     def add_target_and_index(self, name, sig, signode):
         if self.objtype == 'command':
         if self.objtype == 'command':
-           targetname = name.lower()
+            targetname = name.lower()
         else:
         else:
-           targetname = name
+            targetname = name
         targetid = '%s:%s' % (self.objtype, targetname)
         targetid = '%s:%s' % (self.objtype, targetname)
         if targetid not in self.state.document.ids:
         if targetid not in self.state.document.ids:
             signode['names'].append(targetid)
             signode['names'].append(targetid)
@@ -302,6 +307,79 @@ class CMakeObject(ObjectDescription):
         if make_index_entry:
         if make_index_entry:
             self.indexnode['entries'].append(make_index_entry(name, targetid))
             self.indexnode['entries'].append(make_index_entry(name, targetid))
 
 
+class CMakeSignatureObject(CMakeObject):
+    object_type = 'signature'
+
+    option_spec = {
+        'target': directives.unchanged,
+    }
+
+    def get_signatures(self):
+        content = nl_escape_re.sub('', self.arguments[0])
+        lines = sig_end_re.split(content)
+        return [ws_re.sub(' ', line.strip()) for line in lines]
+
+    def handle_signature(self, sig, signode):
+        language = 'cmake'
+        classes = ['code', 'cmake', 'highlight']
+
+        node = addnodes.desc_name(sig, '', classes=classes)
+
+        try:
+            tokens = Lexer(sig, language, 'short')
+        except LexerError as error:
+            if self.state.document.settings.report_level > 2:
+                # Silently insert without syntax highlighting.
+                tokens = Lexer(sig, language, 'none')
+            else:
+                raise self.warning(error)
+
+        for classes, value in tokens:
+            if classes:
+                node += nodes.inline(value, value, classes=classes)
+            else:
+                node += nodes.Text(value)
+
+        signode.clear()
+        signode += node
+
+        return sig
+
+    def __init__(self, *args, **kwargs):
+        self.targetnames = {}
+        super().__init__(*args, **kwargs)
+
+    def add_target_and_index(self, name, sig, signode):
+        if name in self.targetnames:
+            targetname = self.targetnames[name].lower()
+        else:
+            def extract_keywords(params):
+                for p in params:
+                    if p[0].isalpha():
+                        yield p
+                    else:
+                        return
+
+            keywords = extract_keywords(name.split('(')[1].split())
+            targetname = ' '.join(keywords).lower()
+        targetid = nodes.make_id(targetname)
+
+        if targetid not in self.state.document.ids:
+            signode['names'].append(targetname)
+            signode['ids'].append(targetid)
+            signode['first'] = (not self.names)
+            self.state.document.note_explicit_target(signode)
+
+    def run(self):
+        targets = self.options.get('target')
+        if targets is not None:
+            signatures = self.get_signatures()
+            targets = [t.strip() for t in targets.split('\n')]
+            for signature, target in zip(signatures, targets):
+                self.targetnames[signature] = target
+
+        return super().run()
+
 class CMakeXRefRole(XRefRole):
 class CMakeXRefRole(XRefRole):
 
 
     # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'.
     # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'.
@@ -411,6 +489,7 @@ class CMakeDomain(Domain):
         'command':    CMakeObject,
         'command':    CMakeObject,
         'envvar':     CMakeObject,
         'envvar':     CMakeObject,
         'genex':      CMakeObject,
         'genex':      CMakeObject,
+        'signature':  CMakeSignatureObject,
         'variable':   CMakeObject,
         'variable':   CMakeObject,
         # Other `object_types` cannot be created except by the `CMakeTransform`
         # Other `object_types` cannot be created except by the `CMakeTransform`
     }
     }

+ 23 - 0
Utilities/Sphinx/static/cmake.css

@@ -17,6 +17,29 @@ div.sphinxsidebarwrapper {
   background-color: #dfdfdf;
   background-color: #dfdfdf;
 }
 }
 
 
+/* Apply <pre> style (from classic.css) to signature directive argument. */
+.signature .sig {
+  padding: 5px;
+  background-color: #eeeeee;
+  color: #333333;
+  line-height: 120%;
+  border: 1px solid #ac9;
+  border-left: none;
+  border-right: none;
+}
+
+/* Add additional styling to signature directive argument. */
+.signature .sig {
+  margin-bottom: 5px;
+  padding-left: calc(5px + 3em);
+  text-indent: -3em;
+  font-family: monospace;
+}
+
+.signature .sig .code.sig-name {
+  font-weight: normal;
+}
+
 /* Remove unwanted margin in case list item contains a div-wrapping
 /* Remove unwanted margin in case list item contains a div-wrapping
    directive like `.. versionadded` or `.. deprecated`. */
    directive like `.. versionadded` or `.. deprecated`. */
 dd > :first-child > p {
 dd > :first-child > p {