Skip to content

Commit

Permalink
Add type parameters to rendered rst for functions, classes, and inter…
Browse files Browse the repository at this point in the history
…faces (#148)

When rendering a jsfunction, jsclass, or jsinterface, if there are type params
include them. Like:
```js
function f<S, T>(x, y)
```
I also removed the parens from classes and interfaces because they look weird.

We had to add an extra docutils node for js-type-params because the normal type
params docutils node uses square brackets not angle brackets.
  • Loading branch information
hoodmane authored May 6, 2024
1 parent 31eac8d commit fb88024
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 69 deletions.
98 changes: 94 additions & 4 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,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 (
Expand All @@ -31,6 +32,7 @@
)
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 @@ -257,7 +259,76 @@ def run(self) -> list[Node]:
return AutoAttributeDirective


class desc_js_type_parameter_list(nodes.Part, nodes.Inline, nodes.FixedTextElement):
"""Node for a javascript type parameter list.
Unlike normal parameter lists, we use angle braces <> as the braces. Based
on sphinx.addnodes.desc_type_parameter_list
"""

child_text_separator = ", "

def astext(self) -> str:
return f"<{nodes.FixedTextElement.astext(self)}>"


def visit_desc_js_type_parameter_list(
self: HTML5Translator, node: nodes.Element
) -> None:
"""Define the html/text rendering for desc_js_type_parameter_list. Based on
sphinx.writers.html5.visit_desc_type_parameter_list
"""
self._visit_sig_parameter_list(node, addnodes.desc_parameter, "<", ">")


def depart_desc_js_type_parameter_list(
self: HTML5Translator, node: nodes.Element
) -> None:
"""Define the html/text rendering for desc_js_type_parameter_list. Based on
sphinx.writers.html5.depart_desc_type_parameter_list
"""
self._depart_sig_parameter_list(node)


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_typeparams_for_signature(
self: JSObject, sig: str, signode: desc_signature, *, keep_callsig: bool
) -> tuple[str, str]:
"""Generic function to handle type params in the sig line for interfaces,
classes, and functions.
For interfaces and classes we don't prefer the look with parentheses so we
also remove them (by setting keep_callsig to False).
"""
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
# Check for call signature, if present take it off
if signode.children[-1].astext().endswith(")"):
lastchild = signode.children[-1]
signode.remove(lastchild)
if typeparams:
add_param_list_to_signode(signode, typeparams)
# if we took off a call signature and we want to keep it put it back.
if keep_callsig and lastchild:
signode += lastchild
return res


class JSFunction(JSCallable):
"""Variant of JSCallable that can take static/async prefixes"""

option_spec = {
**JSCallable.option_spec,
"static": flag,
Expand All @@ -278,9 +349,15 @@ def get_display_prefix(
)
return result

def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
return handle_typeparams_for_signature(self, sig, signode, keep_callsig=True)


class JSInterface(JSCallable):
"""Like a callable but with a different prefix."""
"""An interface directive.
Based on sphinx.domains.javascript.JSConstructor.
"""

allow_nesting = True

Expand All @@ -290,9 +367,18 @@ 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_typeparams_for_signature(self, sig, signode, keep_callsig=False)


class JSClass(JSConstructor):
def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
return handle_typeparams_for_signature(self, sig, signode, keep_callsig=True)


@cache
def patch_JsObject_get_index_text() -> None:
"""Add our additional object types to the index"""
orig_get_index_text = JSObject.get_index_text

def patched_get_index_text(
Expand All @@ -308,8 +394,6 @@ def patched_get_index_text(

def auto_module_directive_bound_to_app(app: Sphinx) -> type[Directive]:
class AutoModuleDirective(JsDirectiveWithChildren):
"""TODO: words here"""

required_arguments = 1

def run(self) -> list[Node]:
Expand Down Expand Up @@ -349,7 +433,13 @@ def add_directives(app: Sphinx) -> None:
app.add_directive_to_domain(
"js", "autosummary", auto_summary_directive_bound_to_app(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),
text=(visit_desc_js_type_parameter_list, depart_desc_js_type_parameter_list),
)
7 changes: 7 additions & 0 deletions sphinx_js/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,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 @@ -543,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 @@ -595,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
4 changes: 2 additions & 2 deletions tests/test_build_ts/source/docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
js_language = "typescript"
from sphinx.util import rst

from sphinx_js.ir import TypeXRefInternal
from sphinx_js.ir import TypeXRef, TypeXRefInternal


def ts_type_xref_formatter(config, xref, kind):
def ts_type_xref_formatter(config, xref: TypeXRef, kind: str) -> str:
if isinstance(xref, TypeXRefInternal):
name = rst.escape(xref.name)
return f":js:{kind}:`{name}`"
Expand Down
26 changes: 26 additions & 0 deletions tests/test_build_ts/source/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export class A {
}
}

/**
* An instance of class A
*/
export let aInstance: A;

/**
* @typeParam T Description of T
*/
Expand All @@ -35,6 +40,8 @@ export class Z<T extends A> {
z() {}
}

export let zInstance: Z<A>;

/**
* Another thing.
*/
Expand All @@ -44,3 +51,22 @@ export const q = { a: "z29", b: 76 };
* Documentation for the interface I
*/
export interface I {}

/**
* An instance of the interface
*/
export let interfaceInstance: I = {};

/**
* A function with a type parameter!
*
* We'll refer to ourselves: :js:func:`functionWithTypeParam`
*
* @typeParam T The type parameter
* @typeParam S Another type param
* @param z A Z of T
* @returns The x field of z
*/
export function functionWithTypeParam<T extends A>(z: Z<T>): T {
return z.x;
}
Loading

0 comments on commit fb88024

Please sign in to comment.