diff --git a/docs/api.rst b/docs/api.rst index 84f3f99aa..bae4a1a0f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,11 +1,112 @@ General API customization ========================= -.. confval:: include_object_description_fields_in_toc +This theme supports a number of options that can be customized for each +domain/object type pair. - :python:`bool` indicating whether to include domain object description - fields, like "Parameters", "Returns", "Raises", etc. in the table of - contents. Defaults to :python:`True`. +.. confval:: object_description_options + + :python:`list` of :python:`(pattern, options)` pairs, where :python:`pattern` + is a regular expression matching strings of the form + :python:`"domain:objtype"` and :python:`options` is a dictionary of supported + `object-description-options`. + + We need something `object_description_options`. + + The actual options for a given object type are determined by first + initializing each option to its default value, and then applying as overrides + the options associated with each matching pattern in order. + +.. _object-description-options: + +Object description options +-------------------------- + +The following options can be customized for each object type using +:confval:`object_description_options`. + +.. objconf:: include_in_toc + + Indicates whether to include the object description in the table of contents. + + .. admonition:: Example + :class: example + + To prevent C++ parameter descriptions from appearing in the TOC, add the + following to :file:`conf.py`: + + .. code-block:: python + + object_description_options = [ + ("cpp:.*Param": dict(include_in_toc=False)), + ] + +.. objconf:: generate_synopses + + Indicates whether to generate a *synopsis* from the object description. The + synopsis is shown as a tooltip when hovering over a cross-reference link, and + is also shown in the search results list. Supported values are: + + :python:`None` + Disables synopsis generation. + + :python:`"first_paragraph"` + Uses the first paragraph of the description as the synopsis. + + :python:`"first_sentence"` + Uses the first sentence of the first paragraph of the description as the synopsis. + + The default is :python:`"first_paragraph"` except for :regexp:`c(pp)?:.*Param` + where the default is :python:`"first_sentence"`. + + .. note:: + + Synopsis generation is currently supported only for the following domains: + + - ``std`` (including object types added using :py:obj:`sphinx.application.Sphinx.add_object_type`) + - ``c`` and ``cpp`` + + .. admonition:: Example + :class: example + + To use the first sentence rather than the first paragraph as the synopsis + for C++ class descriptions, add the following to :file:`conf.py`: + + .. code-block:: python + + object_description_options = [ + ("cpp:class", dict(generate_synopses="first_sentence")), + ] + +.. objconf:: include_object_type_in_xref_tooltip + + Indicates whether to include the object type in cross-reference and TOC + tooltips. + + .. note:: + + For TOC entries, this is supported for all domains. For regular cross + references, this is supported only for the following domains: + + - ``std`` (including object types added using :py:obj:`sphinx.application.Sphinx.add_object_type`) + - ``c`` and ``cpp`` + + .. admonition:: Example + :class: example + + To exclude the object type from all ``py`` domain xrefs, add the following + to :file:`conf.py`: + + .. code-block:: python + + object_description_options = [ + ("py:.*", dict(include_object_type_in_xref_tooltip=False)), + ] + +.. objconf:: include_fields_in_toc + + Indicates whether to include fields, like "Parameters", "Returns", "Raises", + etc. in the table of contents. For an example, see: :cpp:expr:`synopses_ex::Foo` and note the ``Template Parameters``, ``Parameters``, and ``Returns`` headings shown in the @@ -13,11 +114,73 @@ General API customization .. note:: - This option does not control whether there are separate TOC entries for - individual parameters, such as for :cpp:expr:`synopses_ex::Foo::T`, + To control whether there are separate TOC entries for individual + parameters, such as for :cpp:expr:`synopses_ex::Foo::T`, :cpp:expr:`synopses_ex::Foo::N`, :cpp:expr:`synopses_ex::Foo::param`, and - :cpp:expr:`synopses_ex::Foo::arr`. Currently, for the C and C++ domains, - any parameter documented by a :rst:``:param x:`` field will always result - in a TOC entry, regardless of the value of - :confval:`include_object_description_fields_in_toc`. Other domains are - not yet supported. + :cpp:expr:`synopses_ex::Foo::arr`, use the :objconf:`include_in_toc` + option. + + + .. admonition:: Example + :class: example + + To exclude object description fields from the table of contents for all + ``py`` domain objects, add the following to :file:`conf.py`: + + .. code-block:: python + + object_description_options = [ + ("py:.*", dict(include_fields_in_toc=False)), + ] + +Other options described elsewhere include: + +- :objconf:`wrap_signatures_with_css` +- :objconf:`wrap_signatures_column_limit` +- :objconf:`clang_format_style` + +Table of contents icons +^^^^^^^^^^^^^^^^^^^^^^^ + +For object descriptions included in the table of contents (when +:objconf:`include_in_toc` is :python:`True`), a text-based "icon" can optionally +be included to indicate the object type. + +Default icons are specified for a number of object types, but they can be +overridden using the following options: + +.. objconf:: toc_icon_class + + Indicates the icon class, or :python:`None` to disable the icon. The class + must be one of: + + - :python:`"alias"` + - :python:`"procedure"` + - :python:`"data"` + - :python:`"sub-data"` + +.. objconf:: toc_icon_text + + Indicates the text content of the icon, or :python:`None` to disable the + icon. This should normally be a single character, such as :python:`"C"` to + indicate a class or :python:`"F"` to indicate a function. + +.. admonition:: Example + :class: example + + To define a custom object type and specify an icon for it, add the following to + :file:`conf.py`: + + .. code-block:: python + + object_description_options = [ + ("std:confval", dict(toc_icon_class="data", toc_icon_text="C")), + ] + + def setup(app): + app.add_object_type( + "confval", + "confval", + objname="configuration value", + indextemplate="pair: %s; configuration value", + ) diff --git a/docs/conf.py b/docs/conf.py index 92546c394..9f1a1029f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,6 +12,17 @@ sys.path.insert(0, os.path.abspath(".")) +import docutils +import sphinx +import sphinx.domains.python +import sphinx.environment +import sphinx.util.logging +import sphinx.util.typing + +from sphinx_immaterial import apidoc_formatting + +logger = sphinx.util.logging.getLogger(__name__) + # -- Project information ----------------------------------------------------- project = "Sphinx-Immaterial" @@ -161,16 +172,15 @@ "dudir": ("http://docutils.sourceforge.net/docs/ref/rst/directives.html#%s", ""), } +object_description_options = [] + # BEGIN: sphinx_immaterial.format_signatures extension options -clang_format_signatures_domain_styles = { - "cpp": """{ - BasedOnStyle: LLVM, - ColumnLimit: 68, - }""", -} +object_description_options.append( + ("cpp:.*", dict(clang_format_style={"BasedOnStyle": "LLVM"})) +) # END: sphinx_immaterial.format_signatures extension options -html_wrap_signatures_with_css = ["py"] +object_description_options.append(("py:.*", dict(wrap_signatures_with_css=True))) # BEGIN: sphinx_immaterial.external_cpp_references extension options external_cpp_references = { @@ -210,6 +220,57 @@ """ +object_description_options.append( + ( + "std:confval", + dict( + toc_icon_class="data", toc_icon_text="C", generate_synopses="first_sentence" + ), + ) +) + +object_description_options.append( + ( + "std:objconf", + dict( + toc_icon_class="data", toc_icon_text="O", generate_synopses=None, + ), + ) +) + + +def _validate_parallel_build(app): + # Verifies that all of the extensions defined by this theme support parallel + # building. + assert app.is_parallel_allowed("read") + assert app.is_parallel_allowed("write") + + +def _parse_object_description_signature( + env: sphinx.environment.BuildEnvironment, signature: str, node: docutils.nodes.Node +) -> str: + registry = apidoc_formatting.get_object_description_option_registry(env.app) + registry_option = registry.get(signature) + node += sphinx.addnodes.desc_name(signature, signature) + if registry_option is None: + logger.error("Invalid object description option: %r", signature) + else: + node += sphinx.addnodes.desc_sig_punctuation(" : ", " : ") + annotations = sphinx.domains.python._parse_annotation( + sphinx.util.typing.stringify(registry_option.type_constraint), env + ) + node += sphinx.addnodes.desc_type("", "", *annotations) + node += sphinx.addnodes.desc_sig_punctuation(" = ", " = ") + default_repr = repr(registry_option.default) + node += docutils.nodes.literal( + default_repr, + default_repr, + language="python", + classes=["python", "code", "highlight"], + ) + return signature + + def setup(app): app.add_object_type( "confval", @@ -217,3 +278,12 @@ def setup(app): objname="configuration value", indextemplate="pair: %s; configuration value", ) + + app.add_object_type( + "objconf", + "objconf", + objname="object description option", + indextemplate="pair: %s; object description option", + parse_node=_parse_object_description_signature, + ) + app.connect("builder-inited", _validate_parallel_build) diff --git a/docs/cpp.rst b/docs/cpp.rst index d147c77d9..b6f11b389 100644 --- a/docs/cpp.rst +++ b/docs/cpp.rst @@ -1,59 +1,6 @@ C++ domain customization ======================== -.. confval:: cpp_generate_synopses - - :python:`bool` specifying whether to generate a *synopsis* for C++ domain - objects based on the first paragraph of their content (first sentence for - parameters). The synopsis is shown as a tooltip when hovering over a - cross-reference link, and is also shown in the search results list. - - Defaults to :python:`True`. - - .. rst-example:: C++ synopses - - .. cpp:type:: synopses_ex::SomeType - - Description will be shown as a tooltip when hovering over - cross-references to :cpp:expr:`SomeType` in other signatures as well as - in the TOC. - - Additional description not shown in tooltip. This is the return type - for :cpp:expr:`Foo`. - - .. cpp:function:: template \ - synopses_ex::SomeType synopses_ex::Foo(\ - T param, \ - const int (&arr)[N]\ - ); - - Synopsis for this function, shown when hovering over cross references - as well as in the TOC. - - :tparam T: Tooltip shown when hovering over cross-references to this - template parameter. Additional description not included in - tooltip. - :tparam N: Tooltip shown for N. - :param param: Tooltip shown for cross-references to this function - parameter param. - :param arr: Tooltip shown for cross-references to this function - parameter arr. To cross reference another parameter, use the - :rst:role:`cpp:expr` role, e.g.: :cpp:expr:`N`. Parameters can - also be referenced via their fake qualified name, - e.g. :cpp:expr:`synopses_ex::Foo::N`. - :returns: Something or other. - - - .. rst-example:: - - .. cpp:class:: synopses_ex::Class - - .. cpp:function:: Class(uint16_t _cepin, uint16_t _cspin, uint32_t _spi_speed=RF24_SPI_SPEED) - - :param _cepin: The pin attached to Chip Enable on the RF module - :param _cspin: The pin attached to Chip Select (often labeled CSN) on the radio module. - :param _spi_speed: The SPI speed in Hz ie: 1000000 == 1Mhz - .. confval:: cpp_strip_namespaces_from_signatures :python:`list[str]` specifying namespaces to strip from signatures. This diff --git a/docs/demo_api.rst b/docs/demo_api.rst index 900a37dba..6db2e2cb8 100644 --- a/docs/demo_api.rst +++ b/docs/demo_api.rst @@ -47,7 +47,7 @@ C++ API :param len: Length of :cpp:expr:`arr`. :param baz: Baz parameter. -.. rst-example:: +.. rst-example:: Cross-linking of macro parameters. .. c:macro:: MY_MACRO(X, Y, Z) @@ -102,6 +102,49 @@ C++ API .. cpp:enumerator:: B +.. rst-example:: C++ synopses + + .. cpp:type:: synopses_ex::SomeType + + Description will be shown as a tooltip when hovering over + cross-references to :cpp:expr:`SomeType` in other signatures as well as + in the TOC. + + Additional description not shown in tooltip. This is the return type + for :cpp:expr:`Foo`. + + .. cpp:function:: template \ + synopses_ex::SomeType synopses_ex::Foo(\ + T param, \ + const int (&arr)[N]\ + ); + + Synopsis for this function, shown when hovering over cross references + as well as in the TOC. + + :tparam T: Tooltip shown when hovering over cross-references to this + template parameter. Additional description not included in + tooltip. + :tparam N: Tooltip shown for N. + :param param: Tooltip shown for cross-references to this function + parameter param. + :param arr: Tooltip shown for cross-references to this function + parameter arr. To cross reference another parameter, use the + :rst:role:`cpp:expr` role, e.g.: :cpp:expr:`N`. Parameters can + also be referenced via their fake qualified name, + e.g. :cpp:expr:`synopses_ex::Foo::N`. + :returns: Something or other. + +.. rst-example:: C++ function with parameter descriptions nested within class. + + .. cpp:class:: synopses_ex::Class + + .. cpp:function:: Class(uint16_t _cepin, uint16_t _cspin, uint32_t _spi_speed=RF24_SPI_SPEED) + + :param _cepin: The pin attached to Chip Enable on the RF module + :param _cspin: The pin attached to Chip Select (often labeled CSN) on the radio module. + :param _spi_speed: The SPI speed in Hz ie: 1000000 == 1Mhz + JavaScript API ============== diff --git a/docs/format_signatures.rst b/docs/format_signatures.rst index 6f79ec5cb..49aea9093 100644 --- a/docs/format_signatures.rst +++ b/docs/format_signatures.rst @@ -11,34 +11,37 @@ There is a CSS-based formatting rule that can be enabled for long function signatures that displays each function parameter on a separate line. This is enabled by default, and works fairly well for Python signatures. -.. confval:: html_wrap_signatures_with_css +It is controlled by the following `object description +options`: - Specifies for which `Sphinx domains - `__ - this CSS-based formatting is enabled. +.. objconf:: wrap_signatures_with_css - Supported types are: + Indicates whether CSS-based formatting is enabled. Disabled automatically if + :objconf:`clang_format_style` is specified. - :py:obj:`bool` - If `True`, enable CSS-based formatting for all domains. If `False`, - disable CSS-based formatting for all domains. +.. objconf:: wrap_signatures_column_limit - :py:obj:`list[str]` - List of domains for which to enable CSS-based formatting. + Maximum signature length before function parameters are displayed on separate + lines. - The default value is :python:`True` + Only applies if :objconf:`wrap_signatures_with_css` is set to :python:`True`, + or if :objconf:`clang_format_style` is enabled and does not specify a + ``ColumnLimit`` value. -.. confval:: html_wrap_signatures_with_css_column_limit + The default value is :python:`68`. This is the number of characters that fit + under typical display settings with both left and right side bars displayed. - Maximum signature length before function parameters are displayed on separate - lines. +For example, to disable this option for every domain except ``py``, add the +following to :file:`conf.py`: - Only applies to domains for which :confval:`html_wrap_signatures_with_css` is - enabled. +.. code-block:: python - The default value is :python:`68`. + object_description_options = [ + (".*": dict(wrap_signatures_with_css=False)), + ("py:.*": dict(wrap_signatures_with_css=True)), + ] -.. rst-example:: +.. rst-example:: CSS-based wrapping of Python signature .. py:function:: long_function_signature_example(\ name: int, \ @@ -55,8 +58,9 @@ There is a more powerful alternative formatting mechanism based on `clang-format JavaScript, Objective-C, and C#. This functionality is available as a separate extension included with this -theme. To use it, you must include it in your conf.py file and you must also -set the :confval:`clang_format_signatures_domain_styles` configuration option. +theme. To use it, you must include it in your :file:`conf.py` file and you must +also specify the :objconf:`clang_format_style` option for the object types for +which the extension should be used. .. code-block:: python @@ -65,35 +69,19 @@ set the :confval:`clang_format_signatures_domain_styles` configuration option. "sphinx_immaterial.format_signatures", ] - clang_format_signatures_domain_styles = { - "cpp": """{ - BasedOnStyle: LLVM, - ColumnLimit: 68, - }""", - } - -.. confval:: clang_format_signatures_domain_styles - - Dictionary that specifies the `clang-format style options - `__ for each - domain. Formatting is enabled *only* for domains that are listed. - - .. literalinclude:: conf.py - :language: python - :start-after: # BEGIN: sphinx_immaterial.format_signatures extension options - :end-before: # END: sphinx_immaterial.format_signatures extension options + object_description_options = [ + # ... + ("cpp:.*": dict(clang_format_style={"BasedOnStyle": "LLVM"})), + ] - When specifying an inline style (as opposed to a predefined style), it is - necessary to enclose the style in curly braces, as in the example above. - Since the predefined styles (such as ``Google``, ``LLVM``, etc.) do not use a - column limit of 68, it is not recommended to use a predefined style. +.. objconf:: clang_format_style - .. tip:: + Specifies the `clang-format style options + `__ as a + :python:`dict` (JSON object), or :python:`None` to disable clang-format. - It is recommended to include ``ColumnLimit: 68`` as a style option. This - is the default for the CSS-based wrapping, and is the number of characters - that fit under typical display settings with both left and right side bars - displayed. + If the style does not explicitly specify a ``ColumnLimit``, the value of + :objconf:`wrap_signatures_column_limit` is used. .. warning:: @@ -107,6 +95,7 @@ set the :confval:`clang_format_signatures_domain_styles` configuration option. Name of ``clang-format`` executable. May either be a plain filename, in which case normal ``PATH`` resolution applies, or a path to the executable. + Defaults to :python:`"clang-format"`. To ensure that a consistent version of ``clang-format`` is available when building your documentation, add the `clang-format PyPI package diff --git a/requirements.txt b/requirements.txt index c5858527d..330bcd099 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ sphinx>=4.0 markupsafe +pydantic diff --git a/sphinx_immaterial/__init__.py b/sphinx_immaterial/__init__.py index 14f026c29..5b22653dd 100644 --- a/sphinx_immaterial/__init__.py +++ b/sphinx_immaterial/__init__.py @@ -18,6 +18,7 @@ from . import autodoc_property_type from . import cpp_domain_fixes from . import inlinesyntaxhighlight +from . import generic_synopses from . import nav_adapt from . import object_toc from . import postprocess_html @@ -310,6 +311,7 @@ def setup(app): app.setup_extension(inlinesyntaxhighlight.__name__) app.setup_extension(object_toc.__name__) app.setup_extension(search_adapt.__name__) + app.setup_extension(generic_synopses.__name__) app.connect("html-page-context", html_page_context) app.connect("builder-inited", _builder_inited) app.add_config_value( diff --git a/sphinx_immaterial/apidoc_formatting.py b/sphinx_immaterial/apidoc_formatting.py index 835224bc2..7d34b1746 100644 --- a/sphinx_immaterial/apidoc_formatting.py +++ b/sphinx_immaterial/apidoc_formatting.py @@ -1,15 +1,34 @@ """Modifies the formatting of API documentation.""" -from typing import List, TYPE_CHECKING, cast +import functools +import re +from typing import ( + List, + TYPE_CHECKING, + cast, + Literal, + Optional, + Dict, + Tuple, + NamedTuple, + Any, + Pattern, +) import docutils.nodes +import pydantic import sphinx.addnodes import sphinx.application import sphinx.locale +import sphinx.util.logging import sphinx.writers.html5 _ = sphinx.locale._ +logger = sphinx.util.logging.getLogger(__name__) + +ObjectDescriptionOptions = Dict[str, Any] + if TYPE_CHECKING: HTMLTranslatorMixinBase = sphinx.writers.html5.HTML5Translator @@ -141,18 +160,15 @@ def _wrap_signatures( objtype: str, content: docutils.nodes.Element, ) -> None: - enabled = app.config.html_wrap_signatures_with_css - if enabled is True or enabled is None: - pass - elif enabled is False: - return - elif domain not in enabled: + options = get_object_description_options(app.env, domain, objtype) + if ( + not options["wrap_signatures_with_css"] + or options.get("clang_format_style") is not None + ): return signatures = content.parent[:-1] for signature in signatures: - _wrap_signature( - signature, app.config.html_wrap_signatures_with_css_column_limit - ) + _wrap_signature(signature, options["wrap_signatures_column_limit"]) def _monkey_patch_object_description_to_include_fields_in_toc(): @@ -161,7 +177,8 @@ def _monkey_patch_object_description_to_include_fields_in_toc(): def run(self: sphinx.directives.ObjectDescription) -> List[docutils.nodes.Node]: nodes = orig_run(self) - if not self.env.config.include_object_description_fields_in_toc: + options = get_object_description_options(self.env, self.domain, self.objtype) + if not options["include_fields_in_toc"]: return nodes obj_desc = nodes[-1] @@ -191,6 +208,212 @@ def run(self: sphinx.directives.ObjectDescription) -> List[docutils.nodes.Node]: sphinx.directives.ObjectDescription.run = run +def format_object_description_tooltip( + env: sphinx.environment.BuildEnvironment, + options: ObjectDescriptionOptions, + base_title: str, + synopsis: Optional[str], +) -> str: + title = base_title + + domain = env.get_domain(options["domain"]) + + if options["include_object_type_in_xref_tooltip"]: + object_type = options["object_type"] + title += f" ({domain.get_type_name(domain.object_types[object_type])})" + + if synopsis: + title += f" — {synopsis}" + + return title + + +DEFAULT_OBJECT_DESCRIPTION_OPTIONS: List[Tuple[str, dict]] = [ + ("std:envvar", {"toc_icon_class": "alias", "toc_icon_text": "$"}), + ("js:module", {"toc_icon_class": "data", "toc_icon_text": "r"}), + ("js:function", {"toc_icon_class": "procedure", "toc_icon_text": "M"}), + ("js:method", {"toc_icon_class": "procedure", "toc_icon_text": "M"}), + ("js:class", {"toc_icon_class": "data", "toc_icon_text": "C"}), + ("js:data", {"toc_icon_class": "alias", "toc_icon_text": "V"}), + ("js:attribute", {"toc_icon_class": "alias", "toc_icon_text": "V"}), + ("json:schema", {"toc_icon_class": "data", "toc_icon_text": "J"}), + ("json:subschema", {"toc_icon_class": "sub-data", "toc_icon_text": "j"}), + ("py:class", {"toc_icon_class": "data", "toc_icon_text": "C"}), + ("py:function", {"toc_icon_class": "procedure", "toc_icon_text": "F"}), + ("py:method", {"toc_icon_class": "procedure", "toc_icon_text": "M"}), + ("py:classmethod", {"toc_icon_class": "procedure", "toc_icon_text": "M"}), + ("py:staticmethod", {"toc_icon_class": "procedure", "toc_icon_text": "M"}), + ("py:property", {"toc_icon_class": "alias", "toc_icon_text": "P"}), + ("py:attribute", {"toc_icon_class": "alias", "toc_icon_text": "A"}), + ("py:data", {"toc_icon_class": "alias", "toc_icon_text": "V"}), + ("py:parameter", {"toc_icon_class": "sub-data", "toc_icon_text": "p"}), + ("c:member", {"toc_icon_class": "alias", "toc_icon_text": "V"}), + ("c:var", {"toc_icon_class": "alias", "toc_icon_text": "V"}), + ("c:function", {"toc_icon_class": "procedure", "toc_icon_text": "F"}), + ("c:macro", {"toc_icon_class": "alias", "toc_icon_text": "D"}), + ("c:union", {"toc_icon_class": "data", "toc_icon_text": "U"}), + ("c:struct", {"toc_icon_class": "data", "toc_icon_text": "S"}), + ("c:enum", {"toc_icon_class": "data", "toc_icon_text": "E"}), + ("c:enumerator", {"toc_icon_class": "data", "toc_icon_text": "e"}), + ("c:type", {"toc_icon_class": "alias", "toc_icon_text": "T"}), + ( + "c:macroParam", + { + "toc_icon_class": "sub-data", + "toc_icon_text": "p", + "generate_synopses": "first_sentence", + }, + ), + ("cpp:class", {"toc_icon_class": "data", "toc_icon_text": "C"}), + ("cpp:struct", {"toc_icon_class": "data", "toc_icon_text": "S"}), + ("cpp:enum", {"toc_icon_class": "data", "toc_icon_text": "E"}), + ("cpp:enum-class", {"toc_icon_class": "data", "toc_icon_text": "E"}), + ("cpp:enum-struct", {"toc_icon_class": "data", "toc_icon_text": "E"}), + ("cpp:enumerator", {"toc_icon_class": "data", "toc_icon_text": "e"}), + ("cpp:union", {"toc_icon_class": "data", "toc_icon_text": "U"}), + ("cpp:concept", {"toc_icon_class": "data", "toc_icon_text": "t"}), + ("cpp:function", {"toc_icon_class": "procedure", "toc_icon_text": "F"}), + ("cpp:alias", {"toc_icon_class": "procedure", "toc_icon_text": "F"}), + ("cpp:member", {"toc_icon_class": "alias", "toc_icon_text": "V"}), + ("cpp:var", {"toc_icon_class": "alias", "toc_icon_text": "V"}), + ("cpp:type", {"toc_icon_class": "alias", "toc_icon_text": "T"}), + ("cpp:namespace", {"toc_icon_class": "alias", "toc_icon_text": "N"}), + ( + "cpp:functionParam", + { + "toc_icon_class": "sub-data", + "toc_icon_text": "p", + "generate_synopses": "first_sentence", + }, + ), + ( + "cpp:templateTypeParam", + { + "toc_icon_class": "alias", + "toc_icon_text": "T", + "generate_synopses": "first_sentence", + }, + ), + ( + "cpp:templateNonTypeParam", + { + "toc_icon_class": "data", + "toc_icon_text": "N", + "generate_synopses": "first_sentence", + }, + ), + ( + "cpp:templateTemplateParam", + { + "toc_icon_class": "alias", + "toc_icon_text": "T", + "generate_synopses": "first_sentence", + }, + ), +] + + +def get_object_description_option_registry(app: sphinx.application.Sphinx): + key = "sphinx_immaterial_object_description_option_registry" + registry = getattr(app, key, None) + if registry is None: + registry = {} + setattr(app, key, registry) + return registry + + +class RegisteredObjectDescriptionOption(NamedTuple): + type_constraint: Any + default: Any + + +def add_object_description_option( + app: sphinx.application.Sphinx, name: str, default: Any, type_constraint: type = Any +) -> None: + registry = get_object_description_option_registry(app) + if name in registry: + logger.error(f"Object description option {name!r} already registered") + default = pydantic.parse_obj_as(type_constraint, default) + registry[name] = RegisteredObjectDescriptionOption( + default=default, type_constraint=type_constraint + ) + + +def get_object_description_options( + env: sphinx.environment.BuildEnvironment, domain: str, object_type: str +) -> ObjectDescriptionOptions: + + return env.app._sphinx_immaterial_get_object_description_options( + domain, object_type + ) + + +def _builder_inited(app: sphinx.application.Sphinx) -> None: + + registry = get_object_description_option_registry(app) + options_map: Dict[str, List[Tuple[int, Dict[str, Any]]]] = {} + options_patterns: List[Tuple[Pattern, int, Dict[str, Any]]] = [] + + default_options = {} + for name, registered_option in registry.items(): + default_options[name] = registered_option.default + + # Validate options + for i, (pattern, options) in enumerate( + pydantic.parse_obj_as( + List[Tuple[Pattern, Dict[str, Any]]], + DEFAULT_OBJECT_DESCRIPTION_OPTIONS + app.config.object_description_options, + ) + ): + for name, value in options.items(): + registered_option = registry.get(name) + if registered_option is None: + logger.error( + "Undefined object description option %r specified for pattern %r", + name, + pattern.pattern, + ) + continue + try: + options[name] = pydantic.parse_obj_as( + registered_option.type_constraint, value + ) + except Exception as e: # pylint: disable=broad-except + logger.error( + "Invalid value %r for object description option" + " %r specified for pattern %r: %s", + value, + name, + pattern.pattern, + e, + ) + if pattern.pattern == re.escape(pattern.pattern): + # Pattern just matches a single string. + options_map.setdefault(pattern.pattern, []).append((i, options)) + else: + options_patterns.append((pattern, i, options)) + + @functools.lru_cache(maxsize=None) + def get_options(domain: str, object_type: str) -> Dict[str, Any]: + key = f"{domain}:{object_type}" + matches = options_map.get(key) + if matches is None: + matches = [] + else: + matches = list(matches) + for pattern, i, options in options_patterns: + if pattern.fullmatch(key): + matches.append((i, options)) + matches.sort(key=lambda x: x[0]) + full_options = default_options.copy() + for _, m in matches: + full_options.update(m) + full_options.update(domain=domain, object_type=object_type) + return full_options + + app._sphinx_immaterial_get_object_description_options = get_options + + def setup(app: sphinx.application.Sphinx): """Registers the monkey patches. @@ -200,17 +423,41 @@ def setup(app: sphinx.application.Sphinx): # to apply. sphinx.addnodes.desc_signature.classes.append("highlight") - app.add_config_value("html_wrap_signatures_with_css", default=None, rebuild="env") - app.add_config_value( - "html_wrap_signatures_with_css_column_limit", default=68, rebuild="env" + app.connect("object-description-transform", _wrap_signatures) + _monkey_patch_object_description_to_include_fields_in_toc() + + add_object_description_option( + app, "wrap_signatures_with_css", type_constraint=bool, default=True + ) + add_object_description_option( + app, "wrap_signatures_column_limit", type_constraint=int, default=68 + ) + add_object_description_option( + app, "include_in_toc", type_constraint=bool, default=True + ) + add_object_description_option( + app, "include_fields_in_toc", type_constraint=bool, default=True + ) + add_object_description_option( + app, + "generate_synopses", + type_constraint=Optional[Literal["first_paragraph", "first_sentence"]], + default="first_paragraph", + ) + add_object_description_option( + app, "include_object_type_in_xref_tooltip", type_constraint=bool, default=True + ) + add_object_description_option( + app, "toc_icon_text", type_constraint=Optional[str], default=None + ) + add_object_description_option( + app, "toc_icon_class", type_constraint=Optional[str], default=None ) app.add_config_value( - "include_object_description_fields_in_toc", default=True, rebuild="env" + "object_description_options", default=[], rebuild="env", types=(list,) ) - - app.connect("object-description-transform", _wrap_signatures) - _monkey_patch_object_description_to_include_fields_in_toc() + app.connect("builder-inited", _builder_inited) return { "parallel_read_safe": True, diff --git a/sphinx_immaterial/cpp_domain_fixes.py b/sphinx_immaterial/cpp_domain_fixes.py index 8ec95c2b3..72bf00b24 100644 --- a/sphinx_immaterial/cpp_domain_fixes.py +++ b/sphinx_immaterial/cpp_domain_fixes.py @@ -22,6 +22,7 @@ import sphinx.domains.cpp import sphinx.util.logging +from . import apidoc_formatting from . import sphinx_utils logger = sphinx.util.logging.getLogger(__name__) @@ -72,7 +73,6 @@ def handle_item( term_node["paramname"] = param_name if kind is not None: term_node["param_kind"] = kind - term_node["toc_title"] = f"{param_name} [{kind}]" term_node += sphinx.addnodes.desc_name(param_name, param_name) node += term_node def_node = docutils.nodes.definition() @@ -717,6 +717,8 @@ def _add_parameter_documentation_ids( starting_id_version, ) -> None: + domain = obj_content.parent["domain"] + qualify_parameter_ids = env.config.cpp_qualify_parameter_ids def cross_link_single_parameter(param_node: docutils.nodes.term) -> None: @@ -750,10 +752,8 @@ def cross_link_single_parameter(param_node: docutils.nodes.term) -> None: ) return - if env.config.cpp_generate_synopses: - synopsis = sphinx_utils.summarize_element_text(param_node.parent[-1]) - else: - synopsis = None + object_type = None + synopsis = None # Set ids of the parameter node. for symbol_i, _ in unique_decls.values(): @@ -772,6 +772,19 @@ def cross_link_single_parameter(param_node: docutils.nodes.term) -> None: ) continue + if object_type is None: + object_type = _get_precise_template_parameter_object_type( + param_symbol.declaration.objectType, param_symbol + ) + param_options = apidoc_formatting.get_object_description_options( + env, domain, object_type + ) + generate_synopses = param_options["generate_synopses"] + if generate_synopses is not None: + synopsis = sphinx_utils.summarize_element_text( + param_node.parent[-1], generate_synopses + ) + if synopsis: set_synopsis(param_symbol, synopsis) @@ -810,6 +823,13 @@ def cross_link_single_parameter(param_node: docutils.nodes.term) -> None: param_id = id_prefix + param_id_suffix param_node["ids"].append(param_id) + if object_type is not None: + if param_options["include_in_toc"]: + toc_title = param_name + if kind: + toc_title += f" [{kind}]" + param_node["toc_title"] = toc_title + if not qualify_parameter_ids: param_node["ids"].append(param_id_suffix) @@ -948,9 +968,14 @@ def after_content(self: object_class) -> None: app=self.env.app, domain=domain, content=self.contentnode, symbols=symbols ) - if self.env.config.cpp_generate_synopses: + options = apidoc_formatting.get_object_description_options( + self.env, self.domain, self.objtype + ) + generate_synopses = options["generate_synopses"] + + if generate_synopses is not None: synopsis = sphinx_utils.summarize_element_text( - self.contentnode, first_sentence_only=False + self.contentnode, generate_synopses ) if synopsis: for symbol in symbols: @@ -1066,8 +1091,6 @@ def setup(app: sphinx.application.Sphinx): _monkey_patch_override_ast_id(sphinx.domains.cpp.ASTDeclaration) _monkey_patch_domain_get_object_synopses(sphinx.domains.c.CDomain) _monkey_patch_domain_get_object_synopses(sphinx.domains.cpp.CPPDomain) - app.add_config_value("cpp_generate_synopses", default=True, rebuild="env") - _monkey_patch_cpp_add_precise_template_parameter_object_types() return { diff --git a/sphinx_immaterial/format_signatures.py b/sphinx_immaterial/format_signatures.py index bb3725ec2..bcb9f673c 100644 --- a/sphinx_immaterial/format_signatures.py +++ b/sphinx_immaterial/format_signatures.py @@ -6,9 +6,10 @@ import collections import hashlib import io +import json import re import subprocess -from typing import Dict, List, Any +from typing import Dict, List, Any, Union, Optional import docutils.nodes import sphinx.addnodes @@ -17,6 +18,8 @@ import sphinx.transforms import sphinx.util.logging +from . import apidoc_formatting + logger = sphinx.util.logging.getLogger(__name__) _SIGNATURE_FORMAT_ID = "signature_format_id" @@ -55,12 +58,14 @@ class CollectSignaturesTransform(sphinx.transforms.SphinxTransform): def apply(self, **kwargs: Any) -> None: collected_signatures = _get_collected_signatures(self.env) - domain_styles = self.config.clang_format_signatures_domain_styles - for node in self.document.traverse(sphinx.addnodes.desc_signature): parent = node.parent domain = parent.get("domain") - if domain not in domain_styles: + objtype = parent.get("objtype") + options = apidoc_formatting.get_object_description_options( + self.env, domain, objtype + ) + if options.get("clang_format_style") is None: continue if "api-include-path" in node["classes"]: continue @@ -68,9 +73,11 @@ def apply(self, **kwargs: Any) -> None: for child in node.children: parts.append(child.astext()) signature = " ".join(parts) - sig_id = hashlib.md5(signature.encode("utf-8")).hexdigest() + sig_id = hashlib.md5( + (f"{domain}:{objtype}:" + signature).encode("utf-8") + ).hexdigest() node[_SIGNATURE_FORMAT_ID] = sig_id - collected_signatures[domain][sig_id] = signature + collected_signatures[domain, objtype][sig_id] = signature def _append_child_copy_source_info( @@ -221,8 +228,7 @@ def apply(self, **kwargs: Any) -> None: signature_id = node.get(_SIGNATURE_FORMAT_ID) if signature_id is None: continue - domain = node.parent["domain"] - formatted_signature = formatted_signatures[domain].get(signature_id) + formatted_signature = formatted_signatures.get(signature_id) if formatted_signature is None: continue _format_signature(node, formatted_signature) @@ -237,14 +243,40 @@ def merge_info( _get_collected_signatures(env).update(_get_collected_signatures(other)) +DOMAIN_CLANG_FORMAT_LANGUAGE = { + "cpp": "Cpp", + "c": "Cpp", + "js": "JavaScript", +} + +ClangFormatStyle = Union[str, Dict[str, Any]] + + def env_updated( app: sphinx.application.Sphinx, env: sphinx.environment.BuildEnvironment ) -> None: - domain_signatures = _get_collected_signatures(env) - domain_formatted_signatures = collections.defaultdict(dict) - domain_styles = app.config.clang_format_signatures_domain_styles + all_signatures = _get_collected_signatures(env) + formatted_signatures = {} + + signatures_for_style = collections.defaultdict(dict) - for domain, signatures in domain_signatures.items(): + for (domain, objtype), signatures in all_signatures.items(): + options = apidoc_formatting.get_object_description_options(env, domain, objtype) + + style: ClangFormatStyle = options["clang_format_style"] + if isinstance(style, str): + style = {"BasedOnStyle"} + else: + style = style.copy() + + style.setdefault("ColumnLimit", options["wrap_signatures_column_limit"]) + language = DOMAIN_CLANG_FORMAT_LANGUAGE.get(domain) + if language is not None: + style.setdefault("Language", language) + style_key = json.dumps(style, sort_keys=True) + signatures_for_style[style_key].update(signatures) + + for style_key, signatures in signatures_for_style.items(): source = io.StringIO() for sig_id, signature in signatures.items(): @@ -252,9 +284,8 @@ def env_updated( source.write(signature.strip().strip(";")) source.write(";\n") - style = domain_styles[domain] result = subprocess.run( - [app.config.clang_format_command, f"-style={style}"], + [app.config.clang_format_command, f"-style={style_key}"], input=source.getvalue(), encoding="utf-8", stdout=subprocess.PIPE, @@ -271,26 +302,27 @@ def env_updated( result.check_returncode() stdout = result.stdout - formatted_signatures = domain_formatted_signatures[domain] - for m in re.finditer( "^// ([0-9a-f]+)\n((?:(?!\n//).)+)", stdout, re.MULTILINE | re.DOTALL ): formatted_signatures[m.group(1)] = m.group(2) - setattr(env, _FORMATTED_SIGNATURES, domain_formatted_signatures) + setattr(env, _FORMATTED_SIGNATURES, formatted_signatures) def setup(app: sphinx.application.Sphinx): app.add_transform(CollectSignaturesTransform) app.add_post_transform(FormatSignaturesTransform) + apidoc_formatting.add_object_description_option( + app, + "clang_format_style", + default=None, + type_constraint=Optional[ClangFormatStyle], + ) app.connect("env-merge-info", merge_info) app.connect("env-updated", env_updated) app.add_node(SignatureText, html=(visit_signature_text, depart_signature_text)) - app.add_config_value( - name="clang_format_signatures_domain_styles", default={}, rebuild="env" - ) app.add_config_value( name="clang_format_command", default="clang-format", rebuild="env" ) diff --git a/sphinx_immaterial/generic_synopses.py b/sphinx_immaterial/generic_synopses.py new file mode 100644 index 000000000..d740563af --- /dev/null +++ b/sphinx_immaterial/generic_synopses.py @@ -0,0 +1,185 @@ +"""Adds synopsis support to the std domain.""" + +from typing import cast, Optional, List, Iterator, Tuple + +import docutils.nodes +import sphinx.application + +from . import apidoc_formatting +from . import sphinx_utils + + +def _monkey_patch_generic_object_to_support_synopses(): + + StandardDomain = sphinx.domains.std.StandardDomain + + object_class = sphinx.domains.std.GenericObject + + orig_after_content = object_class.after_content + + orig_transform_content = object_class.transform_content + + def transform_content(self: object_class, contentnode) -> None: + self.contentnode = contentnode + orig_transform_content(self, contentnode) + + orig_add_target_and_index = object_class.add_target_and_index + + def add_target_and_index( + self: object_class, name: str, sig: str, signode: sphinx.addnodes.desc_signature + ) -> None: + self._saved_object_name = name + orig_add_target_and_index(self, name, sig, signode) + + object_class.add_target_and_index = add_target_and_index + + object_class.transform_content = transform_content + + def after_content(self: object_class) -> None: + orig_after_content(self) + options = apidoc_formatting.get_object_description_options( + self.env, self.domain, self.objtype + ) + generate_synopses = options["generate_synopses"] + if generate_synopses is None: + return + synopsis = sphinx_utils.summarize_element_text( + self.contentnode, generate_synopses + ) + std = cast(StandardDomain, self.env.get_domain("std")) + std.data["synopses"][self.objtype, self._saved_object_name] = synopsis + + object_class.after_content = after_content + + orig_merge_domaindata = StandardDomain.merge_domaindata + + def merge_domaindata(self, docnames: List[str], otherdata: dict) -> None: + orig_merge_domaindata(self, docnames, otherdata) + self.data["synopses"].update(otherdata["synopses"]) + + StandardDomain.merge_domaindata = merge_domaindata + + def make_refnode( + std: StandardDomain, + builder: sphinx.builders.Builder, + fromdocname: str, + docname: str, + labelid: str, + contnode: docutils.nodes.Element, + objtype: str, + target: str, + ): + return sphinx.util.nodes.make_refnode( + builder, + fromdocname, + docname, + labelid, + contnode, + title=apidoc_formatting.format_object_description_tooltip( + env=builder.env, + options=apidoc_formatting.get_object_description_options( + std.env, "std", objtype + ), + base_title=target, + synopsis=std.data["synopses"].get((objtype, target)), + ), + ) + + def _resolve_obj_xref( + self: StandardDomain, + env: sphinx.environment.BuildEnvironment, + fromdocname: str, + builder: sphinx.builders.Builder, + typ: str, + target: str, + node: sphinx.addnodes.pending_xref, + contnode: docutils.nodes.Element, + ) -> Optional[docutils.nodes.Element]: + objtypes = self.objtypes_for_role(typ) or [] + objtype = None + for objtype in objtypes: + result = self.objects.get((objtype, target)) + if result is not None: + docname, labelid = result + break + else: + return None + + return make_refnode( + self, builder, fromdocname, docname, labelid, contnode, objtype, target + ) + + StandardDomain._resolve_obj_xref = _resolve_obj_xref + + def resolve_any_xref( + self: StandardDomain, + env: sphinx.environment.BuildEnvironment, + fromdocname: str, + builder: sphinx.builders.Builder, + target: str, + node: sphinx.addnodes.pending_xref, + contnode: docutils.nodes.Element, + ) -> List[Tuple[str, docutils.nodes.Element]]: + results: List[Tuple[str, docutils.nodes.Element]] = [] + ltarget = target.lower() # :ref: lowercases its target automatically + for role in ("ref", "option"): # do not try "keyword" + res = self.resolve_xref( + env, + fromdocname, + builder, + role, + ltarget if role == "ref" else target, + node, + contnode, + ) + if res: + results.append(("std:" + role, res)) + # all others + for objtype in self.object_types: + key = (objtype, target) + if objtype == "term": + key = (objtype, ltarget) + if key in self.objects: + docname, labelid = self.objects[key] + results.append( + ( + "std:" + self.role_for_objtype(objtype), + make_refnode( + self, + builder, + fromdocname, + docname, + labelid, + contnode, + objtype, + target, + ), + ) + ) + return results + + StandardDomain.resolve_any_xref = resolve_any_xref + + def get_object_synopses( + self: StandardDomain, + ) -> Iterator[Tuple[Tuple[str, str], str]]: + synopses = self.data["synopses"] + for (objtype, name), (docname, labelid) in self.objects.items(): + synopsis = synopses.get((objtype, name)) + if not synopsis: + continue + yield ((docname, labelid), synopsis) + + StandardDomain.get_object_synopses = get_object_synopses + + +def setup(app: sphinx.application.Sphinx): + sphinx.domains.std.StandardDomain.initial_data[ + "synopses" + ] = {} # (type, name) -> synopsis + _monkey_patch_generic_object_to_support_synopses() + + return { + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/sphinx_immaterial/nav_adapt.py b/sphinx_immaterial/nav_adapt.py index 1cbfa489d..a055a8d23 100644 --- a/sphinx_immaterial/nav_adapt.py +++ b/sphinx_immaterial/nav_adapt.py @@ -23,6 +23,8 @@ import sphinx.environment.adapters.toctree import sphinx.util.osutil +from . import apidoc_formatting + # env var is only defined in RTD hosted builds READTHEDOCS = os.getenv("READTHEDOCS") @@ -226,61 +228,6 @@ class DomainAnchorEntry(NamedTuple): synopsis: Optional[str] -class ObjectIconInfo(NamedTuple): - icon_class: str - icon_text: str - - -OBJECT_ICON_INFO: Dict[Tuple[str, str], ObjectIconInfo] = { - ("std", "envvar"): ObjectIconInfo(icon_class="alias", icon_text="$"), - ("js", "module"): ObjectIconInfo(icon_class="data", icon_text="r"), - ("js", "function"): ObjectIconInfo(icon_class="procedure", icon_text="M"), - ("js", "method"): ObjectIconInfo(icon_class="procedure", icon_text="M"), - ("js", "class"): ObjectIconInfo(icon_class="data", icon_text="C"), - ("js", "data"): ObjectIconInfo(icon_class="alias", icon_text="V"), - ("js", "attribute"): ObjectIconInfo(icon_class="alias", icon_text="V"), - ("json", "schema"): ObjectIconInfo(icon_class="data", icon_text="J"), - ("json", "subschema"): ObjectIconInfo(icon_class="sub-data", icon_text="j"), - ("py", "class"): ObjectIconInfo(icon_class="data", icon_text="C"), - ("py", "function"): ObjectIconInfo(icon_class="procedure", icon_text="F"), - ("py", "method"): ObjectIconInfo(icon_class="procedure", icon_text="M"), - ("py", "classmethod"): ObjectIconInfo(icon_class="procedure", icon_text="M"), - ("py", "staticmethod"): ObjectIconInfo(icon_class="procedure", icon_text="M"), - ("py", "property"): ObjectIconInfo(icon_class="alias", icon_text="P"), - ("py", "attribute"): ObjectIconInfo(icon_class="alias", icon_text="A"), - ("py", "data"): ObjectIconInfo(icon_class="alias", icon_text="V"), - ("py", "parameter"): ObjectIconInfo(icon_class="sub-data", icon_text="p"), - ("c", "member"): ObjectIconInfo(icon_class="alias", icon_text="V"), - ("c", "var"): ObjectIconInfo(icon_class="alias", icon_text="V"), - ("c", "function"): ObjectIconInfo(icon_class="procedure", icon_text="F"), - ("c", "macro"): ObjectIconInfo(icon_class="alias", icon_text="D"), - ("c", "union"): ObjectIconInfo(icon_class="data", icon_text="U"), - ("c", "struct"): ObjectIconInfo(icon_class="data", icon_text="S"), - ("c", "enum"): ObjectIconInfo(icon_class="data", icon_text="E"), - ("c", "enumerator"): ObjectIconInfo(icon_class="data", icon_text="e"), - ("c", "type"): ObjectIconInfo(icon_class="alias", icon_text="T"), - ("c", "macroParam"): ObjectIconInfo(icon_class="sub-data", icon_text="p"), - ("cpp", "class"): ObjectIconInfo(icon_class="data", icon_text="C"), - ("cpp", "struct"): ObjectIconInfo(icon_class="data", icon_text="S"), - ("cpp", "enum"): ObjectIconInfo(icon_class="data", icon_text="E"), - ("cpp", "enum-class"): ObjectIconInfo(icon_class="data", icon_text="E"), - ("cpp", "enum-struct"): ObjectIconInfo(icon_class="data", icon_text="E"), - ("cpp", "enumerator"): ObjectIconInfo(icon_class="data", icon_text="e"), - ("cpp", "union"): ObjectIconInfo(icon_class="data", icon_text="U"), - ("cpp", "concept"): ObjectIconInfo(icon_class="data", icon_text="t"), - ("cpp", "function"): ObjectIconInfo(icon_class="procedure", icon_text="F"), - ("cpp", "alias"): ObjectIconInfo(icon_class="procedure", icon_text="F"), - ("cpp", "member"): ObjectIconInfo(icon_class="alias", icon_text="V"), - ("cpp", "var"): ObjectIconInfo(icon_class="alias", icon_text="V"), - ("cpp", "type"): ObjectIconInfo(icon_class="alias", icon_text="T"), - ("cpp", "namespace"): ObjectIconInfo(icon_class="alias", icon_text="N"), - ("cpp", "functionParam"): ObjectIconInfo(icon_class="sub-data", icon_text="p"), - ("cpp", "templateTypeParam"): ObjectIconInfo(icon_class="alias", icon_text="T"), - ("cpp", "templateNonTypeParam"): ObjectIconInfo(icon_class="data", icon_text="N"), - ("cpp", "templateTemplateParam"): ObjectIconInfo(icon_class="alias", icon_text="T"), -} - - def _make_domain_anchor_map( env: sphinx.environment.BuildEnvironment, ) -> Dict[Tuple[str, str], DomainAnchorEntry]: @@ -303,9 +250,10 @@ def _make_domain_anchor_map( anchor, priority, ) in domain.get_objects(): - if (domain_name, objtype) not in OBJECT_ICON_INFO: + url = docname_to_url.get(docname) + if url is None: continue - key = (docname_to_url[docname], anchor) + key = (url, anchor) synopsis = synopses.get((docname, anchor)) m.setdefault( key, @@ -344,17 +292,20 @@ def _add_domain_info_to_toc( continue domain = app.env.domains[objinfo.domain_name] label = domain.get_type_name(domain.object_types[objinfo.objtype]) - tooltip = f"{objinfo.name} ({label})" - synopsis = (objinfo.synopsis or "").strip() - if synopsis: - tooltip += f" — {synopsis}" - icon_info = OBJECT_ICON_INFO.get((objinfo.domain_name, objinfo.objtype)) + options = apidoc_formatting.get_object_description_options( + app.env, objinfo.domain_name, objinfo.objtype + ) + tooltip = apidoc_formatting.format_object_description_tooltip( + app.env, options, objinfo.name, objinfo.synopsis + ) + toc_icon_text = options["toc_icon_text"] + toc_icon_class = options["toc_icon_class"] title_prefix = "" - if icon_info is not None: + if toc_icon_text is not None and toc_icon_class is not None: title_prefix = ( f'{icon_info.icon_text}' + f'class="objinfo-icon objinfo-icon__{toc_icon_class}" ' + f'title="{label}">{toc_icon_text}' ) span_prefix = " Optional[docutils.nodes.section]: + options = apidoc_formatting.get_object_description_options( + app.env, source["domain"], source["objtype"] + ) + if not options["include_in_toc"]: + return False + signature: sphinx.addnodes.desc_signature for child in source._traverse(): if not isinstance(child, sphinx.addnodes.desc_signature): @@ -73,7 +81,7 @@ def _make_section_from_field( section += titlenode return section - def _make_section_from_parameter( + def _make_section_from_term( source: docutils.nodes.term, ) -> Optional[docutils.nodes.section]: ids = source["ids"] @@ -82,7 +90,7 @@ def _make_section_from_parameter( return None section = docutils.nodes.section() section["ids"] = ids - title = source.get("toc_title", source["paramname"]) + title = source["toc_title"] titlenode = docutils.nodes.comment(title, title) section += titlenode return section @@ -128,9 +136,9 @@ def _collect( if new_node is not None: target += new_node target = new_node - elif isinstance(source, docutils.nodes.term) and source.get("paramname"): - # Parameter within object description. Try to create synthetic section. - new_node = _make_section_from_parameter(source) + elif isinstance(source, docutils.nodes.term) and source.get("toc_title"): + # Term with toc title. Try to create synthetic section. + new_node = _make_section_from_term(source) if new_node is not None: target += new_node # Parameters cannot contain sub-sections diff --git a/sphinx_immaterial/sphinx_utils.py b/sphinx_immaterial/sphinx_utils.py index 3655743df..2571bbbb7 100644 --- a/sphinx_immaterial/sphinx_utils.py +++ b/sphinx_immaterial/sphinx_utils.py @@ -1,10 +1,13 @@ """Utilities for use with Sphinx.""" +from typing import Literal + import docutils.nodes def summarize_element_text( - node: docutils.nodes.Element, first_sentence_only=True + node: docutils.nodes.Element, + mode: Literal["first_paragraph", "first_sentence"] = "first_paragraph", ) -> str: """Extracts a short text synopsis, e.g. for use as a tooltip.""" @@ -19,7 +22,7 @@ def summarize_element_text( break text = node.astext() - if first_sentence_only: + if mode == "first_sentence": sentence_end = text.find(". ") if sentence_end != -1: text = text[: sentence_end + 1] diff --git a/src/assets/stylesheets/main/_api.scss b/src/assets/stylesheets/main/_api.scss index 58352b894..caf8f07a9 100644 --- a/src/assets/stylesheets/main/_api.scss +++ b/src/assets/stylesheets/main/_api.scss @@ -244,7 +244,7 @@ $objinfo-icon-size: 16px; border: 1px solid var(--objinfo-icon-fg-default); border-radius: 2px; - @each $objclass in (data, alias, procedure, data, sub-data) { + @each $objclass in (alias, procedure, data, sub-data) { &__#{$objclass} { color: var(--objinfo-icon-bg-default); background-color: var(--objinfo-icon-fg-#{$objclass});