From 20f9f32329be428cc98e175d348560cdd18c0159 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Sat, 4 May 2024 15:27:53 +0200 Subject: [PATCH] Add type parameters to rendered rst --- sphinx_js/__init__.py | 4 +- sphinx_js/directives.py | 104 +++++++++++++++++++++++++++--- sphinx_js/js/convertTopLevel.ts | 2 +- sphinx_js/renderers.py | 39 ++++++++--- sphinx_js/templates/attribute.rst | 4 ++ sphinx_js/templates/class.rst | 8 +-- sphinx_js/templates/common.rst | 6 ++ sphinx_js/templates/function.rst | 6 +- 8 files changed, 143 insertions(+), 30 deletions(-) diff --git a/sphinx_js/__init__.py b/sphinx_js/__init__.py index a997e450..dd12a52a 100644 --- a/sphinx_js/__init__.py +++ b/sphinx_js/__init__.py @@ -36,6 +36,8 @@ def on_build_finished(app: Sphinx, exc: Exception | None) -> None: def setup(app: Sphinx) -> None: + add_directives(app) + app.setup_extension("sphinx.ext.autosummary") # I believe this is the best place to run jsdoc. I was tempted to use @@ -43,8 +45,6 @@ def setup(app: Sphinx) -> None: # is RSTs. app.connect("builder-inited", analyze) - add_directives(app) - # TODO: We could add a js:module with app.add_directive_to_domain(). app.add_config_value("js_language", default="javascript", rebuild="env") diff --git a/sphinx_js/directives.py b/sphinx_js/directives.py index d6eab8d0..95e18399 100644 --- a/sphinx_js/directives.py +++ b/sphinx_js/directives.py @@ -11,7 +11,7 @@ from collections.abc import Iterable from functools import cache from os.path import join, relpath -from typing import Any +from typing import Any, cast from docutils import nodes from docutils.nodes import Node @@ -20,6 +20,7 @@ from docutils.parsers.rst.directives import flag from docutils.utils import new_document from sphinx import addnodes +from sphinx.addnodes import desc_signature from sphinx.application import Sphinx from sphinx.domains import ObjType, javascript from sphinx.domains.javascript import ( @@ -27,9 +28,11 @@ JSCallable, JSConstructor, JSObject, + JSXRefRole, ) from sphinx.locale import _ from sphinx.util.docfields import GroupedField, TypedField +from sphinx.writers.html5 import HTML5Translator from .renderers import ( AutoAttributeRenderer, @@ -256,6 +259,48 @@ def run(self) -> list[Node]: return AutoAttributeDirective +class desc_js_type_parameter_list(addnodes.desc_type_parameter_list): + """Node for a general type parameter list. + + As default the type parameters list is written in line with the rest of the signature. + Set ``multi_line_parameter_list = True`` to describe a multi-line type parameters list. + In that case each type parameter will then be written on its own, indented line. + """ + + child_text_separator = ", " + + def astext(self) -> str: + return f"<{nodes.FixedTextElement.astext(self)}>" + + +def add_param_list_to_signode(signode: desc_signature, params: str) -> None: + paramlist = desc_js_type_parameter_list() + for arg in params.split(","): + paramlist += addnodes.desc_parameter("", "", addnodes.desc_sig_name(arg, arg)) + signode += paramlist + + +def handle_signature( + self: JSObject, sig: str, signode: desc_signature, *, keep_callsig: bool +) -> tuple[str, str]: + typeparams = None + if "<" in sig and ">" in sig: + base, _, rest = sig.partition("<") + typeparams, _, params = rest.partition(">") + sig = base + params + res = JSCallable.handle_signature(cast(JSCallable, self), sig, signode) + sig = sig.strip() + lastchild = None + if signode.children[-1].astext().endswith(")"): + lastchild = signode.children[-1] + signode.remove(lastchild) + if typeparams: + add_param_list_to_signode(signode, typeparams) + if keep_callsig and lastchild: + signode += lastchild + return res + + class JSFunction(JSCallable): option_spec = { **JSCallable.option_spec, @@ -277,6 +322,9 @@ def get_display_prefix( ) return result + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + return handle_signature(self, sig, signode, keep_callsig=True) + class JSInterface(JSCallable): """Like a callable but with a different prefix.""" @@ -289,9 +337,31 @@ def get_display_prefix(self) -> list[Node]: addnodes.desc_sig_space(), ] + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + return handle_signature(self, sig, signode, keep_callsig=False) + + +class JSTypeAlias(JSObject): + doc_field_types = [ + JSGroupedField( + "typeparam", + label="Type parameters", + names=("typeparam",), + can_collapse=True, + ) + ] + + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + return handle_signature(self, sig, signode, keep_callsig=False) + + +class JSClass(JSConstructor): + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + return handle_signature(self, sig, signode, keep_callsig=True) + @cache -def patch_js_interface() -> None: +def patch_JsObject_get_index_text() -> None: orig_get_index_text = JSObject.get_index_text def patched_get_index_text( @@ -300,17 +370,13 @@ def patched_get_index_text( name, obj = name_obj if self.objtype == "interface": return _("%s() (interface)") % name + if self.objtype == "typealias": + return _("%s (type alias)") % name return orig_get_index_text(self, objectname, name_obj) JSObject.get_index_text = patched_get_index_text # type:ignore[method-assign] -def add_js_interface(app: Sphinx) -> None: - patch_js_interface() - JavaScriptDomain.object_types["interface"] = ObjType(_("interface"), "interface") - app.add_directive_to_domain("js", "interface", JSInterface) - - def auto_module_directive_bound_to_app(app: Sphinx) -> type[Directive]: class AutoModuleDirective(JsDirectiveWithChildren): """TODO: words here""" @@ -333,6 +399,18 @@ def run(self) -> list[Node]: return JsDocSummary +def visit_desc_js_type_parameter_list( + self: HTML5Translator, node: nodes.Element +) -> None: + self._visit_sig_parameter_list(node, addnodes.desc_parameter, "<", ">") + + +def depart_desc_js_type_parameter_list( + self: HTML5Translator, node: nodes.Element +) -> None: + self._depart_sig_parameter_list(node) + + def add_directives(app: Sphinx) -> None: fix_js_make_xref() fix_staticfunction_objtype() @@ -353,4 +431,12 @@ def add_directives(app: Sphinx) -> None: app.add_directive_to_domain( "js", "autosummary", auto_summary_directive_bound_to_app(app) ) - add_js_interface(app) + app.add_directive_to_domain("js", "class", JSClass) + app.add_role_to_domain("js", "class", JSXRefRole()) + JavaScriptDomain.object_types["interface"] = ObjType(_("interface"), "interface") + app.add_directive_to_domain("js", "interface", JSInterface) + app.add_role_to_domain("js", "interface", JSXRefRole()) + app.add_node( + desc_js_type_parameter_list, + html=(visit_desc_js_type_parameter_list, depart_desc_js_type_parameter_list), + ) diff --git a/sphinx_js/js/convertTopLevel.ts b/sphinx_js/js/convertTopLevel.ts index 4d30f207..1357950f 100644 --- a/sphinx_js/js/convertTopLevel.ts +++ b/sphinx_js/js/convertTopLevel.ts @@ -436,7 +436,7 @@ export class Converter { // be too? return [undefined, (object as DeclarationReflection).children]; } - const kind = ReflectionKind.singularString(object.kind); + const kind = ReflectionKind[object.kind]; const convertFunc = `convert${kind}` as keyof this; if (!this[convertFunc]) { throw new Error(`No known converter for kind ${kind}`); diff --git a/sphinx_js/renderers.py b/sphinx_js/renderers.py index 8bf497f8..03e36428 100644 --- a/sphinx_js/renderers.py +++ b/sphinx_js/renderers.py @@ -38,6 +38,7 @@ Type, TypeParam, TypeXRef, + TypeXRefInternal, ) from .jsdoc import Analyzer as JsAnalyzer from .parsers import PathVisitor @@ -173,7 +174,7 @@ class HasDepPath(Protocol): class Renderer: - _type_xref_formatter: Callable[[TypeXRef], str] + _type_xref_formatter: Callable[[TypeXRef, str | None], str] # We turn the in the analyzer tests because it # makes a big mess. _add_span: bool @@ -233,13 +234,13 @@ def from_directive(cls: type[R], directive: Directive, app: Sphinx) -> R: ) def _set_type_xref_formatter( - self, formatter: Callable[[Config, TypeXRef], str] | None + self, formatter: Callable[[Config, TypeXRef, str | None], str] | None ) -> None: if formatter: self._type_xref_formatter = partial(formatter, self._app.config) return - def default_type_xref_formatter(xref: TypeXRef) -> str: + def default_type_xref_formatter(xref: TypeXRef, _: str | None) -> str: return xref.name self._type_xref_formatter = default_type_xref_formatter @@ -282,13 +283,16 @@ class JsRenderer(Renderer): def _template_vars(self, name: str, obj: TopLevel) -> dict[str, Any]: raise NotImplementedError - def get_object(self) -> TopLevel: - """Return the IR object rendered by this renderer.""" + def lookup_object( + self, + partial_path: list[str], + renderer_type: Literal["function", "class", "attribute"] = "attribute", + ) -> TopLevel: try: analyzer: Analyzer = ( self._app._sphinxjs_analyzer # type:ignore[attr-defined] ) - obj = analyzer.get_object(self._partial_path, self._renderer_type) + obj = analyzer.get_object(partial_path, renderer_type) return obj except SuffixNotFound as exc: raise SphinxError( @@ -302,6 +306,10 @@ def get_object(self) -> TopLevel: ) ) + def get_object(self) -> TopLevel: + """Return the IR object rendered by this renderer.""" + return self.lookup_object(self._partial_path, self._renderer_type) + def rst_nodes(self) -> list[Node]: """Render into RST nodes a thing shaped like a function, having a name and arguments. @@ -366,6 +374,11 @@ def rst( result = "\n".join(lines) + "\n" return result + def _type_params(self, obj: Function | Class | Interface) -> str: + if not obj.type_params: + return "" + return "<{}>".format(", ".join(tp.name for tp in obj.type_params)) + def _formal_params(self, obj: Function) -> str: """Return the JS function params, looking first to any explicit params written into the directive and falling back to those in comments or JS @@ -438,7 +451,12 @@ def strs() -> Iterator[str]: return joined def render_xref(self, s: TypeXRef, escape: bool = False) -> str: - result = self._type_xref_formatter(s) + obj = None + kind = None + if isinstance(s, TypeXRefInternal | TypeXRefInternal): + obj = self.lookup_object(s.path) + kind = type(obj).__name__.lower() + result = self._type_xref_formatter(s, kind) if escape: result = rst.escape(result) return result @@ -454,10 +472,11 @@ def _return_formatter(self, return_: Return) -> tuple[list[str], str]: def _type_param_formatter(self, tparam: TypeParam) -> tuple[list[str], str] | None: v = tparam.name + descr = render_description(tparam.description) if tparam.extends: - v += " extends " + self.render_type(tparam.extends) + descr += " (extends " + self.render_type(tparam.extends) + ")" heads = ["typeparam", v] - return heads, render_description(tparam.description) + return heads, descr def _param_formatter(self, param: Param) -> tuple[list[str], str] | None: """Derive heads and tail from ``@param`` blocks.""" @@ -529,6 +548,7 @@ def _template_vars(self, name: str, obj: Function) -> dict[str, Any]: # type: i deprecated = render_description(deprecated) return dict( name=name, + type_params=self._type_params(obj), params=self._formal_params(obj), fields=self._fields(obj), description=render_description(obj.description), @@ -581,6 +601,7 @@ def _template_vars(self, name: str, obj: Class | Interface) -> dict[str, Any]: return dict( name=name, params=self._formal_params(constructor), + type_params=self._type_params(obj), fields=self._fields(constructor), examples=[render_description(ex) for ex in constructor.examples], deprecated=constructor.deprecated, diff --git a/sphinx_js/templates/attribute.rst b/sphinx_js/templates/attribute.rst index 065c88da..e9763e98 100644 --- a/sphinx_js/templates/attribute.rst +++ b/sphinx_js/templates/attribute.rst @@ -14,6 +14,10 @@ {{ description|indent(3) }} {%- endif %} + {% if is_type_alias -%} + {{ common.fields(fields) | indent(3) }} + {%- endif %} + {{ common.examples(examples)|indent(3) }} {{ content|indent(3) }} diff --git a/sphinx_js/templates/class.rst b/sphinx_js/templates/class.rst index ea3ef49b..f1165de5 100644 --- a/sphinx_js/templates/class.rst +++ b/sphinx_js/templates/class.rst @@ -1,9 +1,9 @@ {% import 'common.rst' as common %} {% if is_interface -%} -.. js:interface:: {{ name }}{{ params }} +.. js:interface:: {{ name }}{{ type_params }}{{ params }} {%- else -%} -.. js:class:: {{ name }}{{ params }} +.. js:class:: {{ name }}{{ type_params }}{{ params }} {%- endif %} {{ common.deprecated(deprecated)|indent(3) }} @@ -36,9 +36,7 @@ {{ constructor_comment|indent(3) }} {%- endif %} - {% for heads, tail in fields -%} - :{{ heads|join(' ') }}: {{ tail }} - {% endfor %} + {{ common.fields(fields) | indent(3) }} {{ common.examples(examples)|indent(3) }} diff --git a/sphinx_js/templates/common.rst b/sphinx_js/templates/common.rst index 34086492..e1c47b55 100644 --- a/sphinx_js/templates/common.rst +++ b/sphinx_js/templates/common.rst @@ -31,3 +31,9 @@ *exported from* :js:mod:`{{ pathname.dotted() }}` {%- endif %} {% endmacro %} + +{% macro fields(items) %} +{% for heads, tail in items -%} + :{{ heads|join(' ') }}: {{ tail }} +{% endfor %} +{% endmacro %} diff --git a/sphinx_js/templates/function.rst b/sphinx_js/templates/function.rst index 456a35f3..2930a9db 100644 --- a/sphinx_js/templates/function.rst +++ b/sphinx_js/templates/function.rst @@ -1,6 +1,6 @@ {% import 'common.rst' as common %} -.. js:function:: {{ name }}{{ '?' if is_optional else '' }}{{ params }} +.. js:function:: {{ name }}{{ '?' if is_optional else '' }}{{ type_params }}{{ params }} {% if is_static -%} :static: {% endif %} @@ -14,9 +14,7 @@ {{ description|indent(3) }} {%- endif %} - {% for heads, tail in fields -%} - :{{ heads|join(' ') }}: {{ tail }} - {% endfor %} + {{ common.fields(fields) | indent(3) }} {{ common.examples(examples)|indent(3) }}