Skip to content

Commit

Permalink
Add type parameters to rendered rst
Browse files Browse the repository at this point in the history
  • Loading branch information
hoodmane committed May 4, 2024
1 parent 10f1d75 commit 20f9f32
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 30 deletions.
4 changes: 2 additions & 2 deletions sphinx_js/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ 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
# app.add_source_parser(), but I think the kind of source it's referring to
# 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")
Expand Down
104 changes: 95 additions & 9 deletions sphinx_js/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,16 +20,19 @@
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 (
JavaScriptDomain,
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,
Expand Down Expand Up @@ -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,
Expand All @@ -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."""
Expand All @@ -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(
Expand All @@ -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"""
Expand All @@ -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()
Expand All @@ -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),
)
2 changes: 1 addition & 1 deletion sphinx_js/js/convertTopLevel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
39 changes: 30 additions & 9 deletions sphinx_js/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
Type,
TypeParam,
TypeXRef,
TypeXRefInternal,
)
from .jsdoc import Analyzer as JsAnalyzer
from .parsers import PathVisitor
Expand Down Expand Up @@ -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 <span class="sphinx_js-type"> in the analyzer tests because it
# makes a big mess.
_add_span: bool
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions sphinx_js/templates/attribute.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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) }}
Expand Down
8 changes: 3 additions & 5 deletions sphinx_js/templates/class.rst
Original file line number Diff line number Diff line change
@@ -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) }}
Expand Down Expand Up @@ -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) }}

Expand Down
6 changes: 6 additions & 0 deletions sphinx_js/templates/common.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
6 changes: 2 additions & 4 deletions sphinx_js/templates/function.rst
Original file line number Diff line number Diff line change
@@ -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 %}
Expand All @@ -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) }}

Expand Down

0 comments on commit 20f9f32

Please sign in to comment.