diff --git a/sphinx_js/__init__.py b/sphinx_js/__init__.py index 793602e5..dc72e988 100644 --- a/sphinx_js/__init__.py +++ b/sphinx_js/__init__.py @@ -10,6 +10,7 @@ auto_attribute_directive_bound_to_app, auto_class_directive_bound_to_app, auto_function_directive_bound_to_app, + sphinx_js_type_role, ) from .jsdoc import Analyzer as JsAnalyzer from .typedoc import Analyzer as TsAnalyzer @@ -145,6 +146,7 @@ def setup(app: Sphinx) -> None: app.add_config_value("ts_type_bold", False, "env") app.add_config_value("ts_should_destructure_arg", None, "env") app.add_config_value("ts_post_convert", None, "env") + app.add_role("sphinx_js_type", sphinx_js_type_role) # We could use a callable as the "default" param here, but then we would # have had to duplicate or build framework around the logic that promotes diff --git a/sphinx_js/directives.py b/sphinx_js/directives.py index 91202c81..ba3ba6b0 100644 --- a/sphinx_js/directives.py +++ b/sphinx_js/directives.py @@ -7,13 +7,17 @@ can access each other and collaborate. """ +import re from collections.abc import Iterable from os.path import join, relpath from typing import Any +from docutils import nodes from docutils.nodes import Node from docutils.parsers.rst import Directive +from docutils.parsers.rst import Parser as RstParser from docutils.parsers.rst.directives import flag +from docutils.utils import new_document from sphinx import addnodes from sphinx.application import Sphinx from sphinx.domains.javascript import JSCallable @@ -21,6 +25,30 @@ from .renderers import AutoAttributeRenderer, AutoClassRenderer, AutoFunctionRenderer +def unescape(escaped: str) -> str: + # For some reason the string we get has a bunch of null bytes in it?? + # Remove them... + escaped = escaped.replace("\x00", "") + # For some reason the extra slash before spaces gets lost between the .rst + # source and when this directive is called. So don't replace "\" => + # "" + return re.sub(r"\\([^ ])", r"\1", escaped) + + +def sphinx_js_type_role(role, rawtext, text, lineno, inliner, options=None, content=None): # type: ignore[no-untyped-def] + """ + The body should be escaped rst. This renders its body as rst and wraps the + result in + """ + unescaped = unescape(text) + doc = new_document("", inliner.document.settings) + RstParser().parse(unescaped, doc) + n = nodes.inline(text) + n["classes"].append("sphinx_js-type") + n += doc.children[0].children + return [n], [] + + class JsDirective(Directive): """Abstract directive which knows how to pull things out of our IR""" diff --git a/sphinx_js/renderers.py b/sphinx_js/renderers.py index 22f1d6b9..7fd81bb2 100644 --- a/sphinx_js/renderers.py +++ b/sphinx_js/renderers.py @@ -61,6 +61,9 @@ class JsRenderer: _explicit_formal_params: str _content: list[str] _options: dict[str, Any] + # We turn the in the analyzer tests because it + # makes a big mess. + _add_span: bool def _template_vars(self, name: str, obj: TopLevel) -> dict[str, Any]: raise NotImplementedError @@ -95,6 +98,7 @@ def __init__( content: list[str] | None = None, options: dict[str, Any] | None = None, ): + self._add_span = True # Fix crash when calling eval_rst with CommonMarkParser: if not hasattr(directive.state.document.settings, "tab_width"): directive.state.document.settings.tab_width = 8 @@ -331,7 +335,10 @@ def strs() -> Iterator[str]: break res.append(self.render_xref(xref[0], escape)) - return r"\ ".join(res) + joined = r"\ ".join(res) + if self._add_span: + return f":sphinx_js_type:`{rst.escape(joined)}`" + return joined def render_xref(self, s: TypeXRef, escape: bool = False) -> str: result = self._type_xref_formatter(s) diff --git a/sphinx_js/typedoc.py b/sphinx_js/typedoc.py index 66ae160f..545cc5fa 100644 --- a/sphinx_js/typedoc.py +++ b/sphinx_js/typedoc.py @@ -1023,7 +1023,7 @@ class AndOrType(TypeBase): def _render_name_root(self, converter: Converter) -> Iterator[str | ir.TypeXRef]: if self.type == "union": - symbol = "|" + symbol = " | " elif self.type == "intersection": symbol = " & " gen = (t._render_name(converter) for t in self.types) diff --git a/tests/test_build_ts/test_build_ts.py b/tests/test_build_ts/test_build_ts.py index 26103fb7..ea554139 100644 --- a/tests/test_build_ts/test_build_ts.py +++ b/tests/test_build_ts/test_build_ts.py @@ -182,7 +182,7 @@ def test_predicate(self): predicate(c) Arguments: - * **c** (*any*) -- + * **c** (any) -- Returns: c is "ConstructorlessClass()" @@ -297,3 +297,9 @@ def test_sphinx_link_in_description(self): href = soup.find(id="spinxLinkInDescription").parent.find_all("a")[1] assert href.get_text() == "abc" assert href.attrs["href"] == "http://example.com" + + def test_sphinx_js_type_class(self): + soup = BeautifulSoup(self._file_contents("async_function"), "html.parser") + href = soup.find_all(class_="sphinx_js-type") + assert len(href) == 1 + assert href[0].get_text() == "Promise" diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 231c3864..d2f7dd29 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -74,6 +74,7 @@ class _app: renderer._content = [] renderer._set_type_xref_formatter(ts_xref_formatter) renderer._set_type_text_formatter(None) + renderer._add_span = False return renderer diff --git a/tests/test_typedoc_analysis/test_typedoc_analysis.py b/tests/test_typedoc_analysis/test_typedoc_analysis.py index 89bb750a..ed392b19 100644 --- a/tests/test_typedoc_analysis/test_typedoc_analysis.py +++ b/tests/test_typedoc_analysis/test_typedoc_analysis.py @@ -453,9 +453,9 @@ def test_unions(self): obj = self.analyzer.get_object(["union"]) assert obj.type == [ "number", - "|", + " | ", "string", - "|", + " | ", TypeXRefInternal(name="Color", path=["./", "types.", "Color"]), ] @@ -515,19 +515,21 @@ def test_constrained_by_key(self): tp.extends = join_type(tp.extends) assert tp == TypeParam( name="K", - extends="string|number|symbol", + extends="string | number | symbol", description=[DescriptionText("The type of the key")], ) # TODO: this part maybe belongs in a unit test for the renderer or something a = AutoFunctionRenderer.__new__(AutoFunctionRenderer) + a._add_span = False a._set_type_text_formatter(None) a._explicit_formal_params = None a._content = [] rst = a.rst([obj.name], obj) assert ":typeparam T: The type of the object" in rst assert ( - ":typeparam K extends string\\|number\\|symbol: The type of the key" in rst + ":typeparam K extends string \\| number \\| symbol: The type of the key" + in rst ) def test_class_constrained(self): @@ -543,6 +545,7 @@ def test_class_constrained(self): a = AutoClassRenderer.__new__(AutoClassRenderer) a._set_type_text_formatter(None) a._explicit_formal_params = None + a._add_span = False a._content = [] a._options = {} rst = a.rst([obj.name], obj)