From b8b1a5969d07a81acb53e9b1a4c4231cd51d74d8 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Mon, 25 Sep 2023 12:57:40 -0700 Subject: [PATCH] Add external xref formatting This depends on the project specifically, so we added a config field called `ts_xref_formatter` which is a function that formats the external references. --- sphinx_js/__init__.py | 1 + sphinx_js/ir.py | 1 + sphinx_js/renderers.py | 29 +++++++++++--- sphinx_js/typedoc.py | 7 +++- tests/test_renderers.py | 38 ++++++++++++++++++- .../test_typedoc_analysis.py | 7 +++- 6 files changed, 74 insertions(+), 9 deletions(-) diff --git a/sphinx_js/__init__.py b/sphinx_js/__init__.py index 0d7ee9c5..d56255eb 100644 --- a/sphinx_js/__init__.py +++ b/sphinx_js/__init__.py @@ -141,6 +141,7 @@ def setup(app: Sphinx) -> None: "js_source_path", default=["../"], rebuild="env", types=[str, list] ) app.add_config_value("jsdoc_config_path", default=None, rebuild="env") + app.add_config_value("ts_xref_formatter", None, "env") # 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/ir.py b/sphinx_js/ir.py index ae2396a5..d638e617 100644 --- a/sphinx_js/ir.py +++ b/sphinx_js/ir.py @@ -43,6 +43,7 @@ class TypeXRefInternal(TypeXRef): @define class TypeXRefExternal(TypeXRef): + package: str sourcefilename: str qualifiedName: str diff --git a/sphinx_js/renderers.py b/sphinx_js/renderers.py index c8481c62..0421a13a 100644 --- a/sphinx_js/renderers.py +++ b/sphinx_js/renderers.py @@ -1,5 +1,6 @@ import textwrap from collections.abc import Callable, Iterator +from functools import partial from re import sub from typing import Any, Literal @@ -10,6 +11,7 @@ from docutils.utils import new_document from jinja2 import Environment, PackageLoader from sphinx.application import Sphinx +from sphinx.config import Config from sphinx.errors import SphinxError from sphinx.util import logging, rst @@ -29,6 +31,7 @@ Type, TypeParam, TypeXRef, + TypeXRefExternal, TypeXRefInternal, ) from .jsdoc import Analyzer as JsAnalyzer @@ -54,10 +57,27 @@ class JsRenderer: _renderer_type: Literal["function", "class", "attribute"] _template: str + _xref_formatter: Callable[[TypeXRefExternal], str] + _partial_path: list[str] + _explicit_formal_params: str + _content: list[str] + _options: dict[str, Any] def _template_vars(self, name: str, obj: TopLevel) -> dict[str, Any]: raise NotImplementedError + def _set_xref_formatter( + self, formatter: Callable[[Config, TypeXRefExternal], str] | None + ) -> None: + if formatter: + self._xref_formatter = partial(formatter, self._app.config) + return + + def default_xref_formatter(xref: TypeXRefExternal) -> str: + return xref.name + + self._xref_formatter = default_xref_formatter + def __init__( self, directive: Directive, @@ -71,15 +91,13 @@ def __init__( directive.state.document.settings.tab_width = 8 self._directive = directive + self._app = app + self._set_xref_formatter(app.config.ts_xref_formatter) # content, arguments, options, app: all need to be accessible to # template_vars, so we bring them in on construction and stow them away # on the instance so calls to template_vars don't need to concern # themselves with what it needs. - self._app = app - self._partial_path: list[str] - self._explicit_formal_params: str - ( self._partial_path, self._explicit_formal_params, @@ -295,7 +313,8 @@ def render_xref(self, s: TypeXRef, escape: bool = False) -> str: name = rst.escape(s.name) result = f":js:class:`{name}`" else: - result = s.name + assert isinstance(s, TypeXRefExternal) + result = self._xref_formatter(s) if escape: result = rst.escape(result) return result diff --git a/sphinx_js/typedoc.py b/sphinx_js/typedoc.py index 93a6cebc..dcc8edb6 100644 --- a/sphinx_js/typedoc.py +++ b/sphinx_js/typedoc.py @@ -985,6 +985,7 @@ class ReferenceType(TypeBase): type: Literal["reference"] name: str target: int | Target | None + package: str | None = None refersToTypeParameter: bool = False def _render_name_root(self, converter: Converter) -> Iterator[str | ir.TypeXRef]: @@ -996,8 +997,12 @@ def _render_name_root(self, converter: Converter) -> Iterator[str | ir.TypeXRef] yield ir.TypeXRefInternal(self.name, node.path) return assert isinstance(self.target, Target) + assert self.package yield ir.TypeXRefExternal( - self.name, self.target.sourceFileName, self.target.qualifiedName + self.name, + self.package, + self.target.sourceFileName, + self.target.qualifiedName, ) diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 0efffd72..b19bb6d9 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -10,6 +10,7 @@ Param, Return, TypeParam, + TypeXRefExternal, TypeXRefInternal, ) from sphinx_js.renderers import AutoFunctionRenderer, JsRenderer @@ -50,15 +51,29 @@ def test_render_description(): @pytest.fixture() -def function_render() -> AutoFunctionRenderer: +def function_renderer(): + class _config: + pass + + class _app: + config = _config + renderer = AutoFunctionRenderer.__new__(AutoFunctionRenderer) + renderer._app = _app renderer._explicit_formal_params = None renderer._content = [] + renderer._set_xref_formatter(None) + return renderer + +@pytest.fixture() +def function_render(function_renderer) -> AutoFunctionRenderer: def function_render(partial_path=None, use_short_name=False, **args): if not partial_path: partial_path = ["blah"] - return renderer.rst(partial_path, make_function(**args), use_short_name) + return function_renderer.rst( + partial_path, make_function(**args), use_short_name + ) return function_render @@ -202,6 +217,25 @@ def test_func_render_type_params(function_render): ) +def test_render_xref(function_renderer: AutoFunctionRenderer): + assert ( + function_renderer.render_type([TypeXRefInternal(name="A", path=["a.", "A"])]) + == ":js:class:`A`" + ) + xref_external = TypeXRefExternal("A", "blah", "a.ts", "a.A") + assert function_renderer.render_type([xref_external]) == "A" + res = [] + + def xref_render(config, val): + res.append([config, val]) + return val.package + "::" + val.name + + function_renderer._set_xref_formatter(xref_render) + assert function_renderer.render_type([xref_external]) == "blah::A" + assert res[0][0] == function_renderer._app.config + assert res[0][1] == xref_external + + def test_func_render_param_type(function_render): assert function_render( description="this is a description", diff --git a/tests/test_typedoc_analysis/test_typedoc_analysis.py b/tests/test_typedoc_analysis/test_typedoc_analysis.py index 82b25dba..a210de79 100644 --- a/tests/test_typedoc_analysis/test_typedoc_analysis.py +++ b/tests/test_typedoc_analysis/test_typedoc_analysis.py @@ -587,7 +587,12 @@ def test_utility_types(self): obj = self.analyzer.get_object(["partial"]) t = deepcopy(obj.type) t[0].sourcefilename = "xxx" - assert t == [TypeXRefExternal("Partial", "xxx", "Partial"), "<", "string", ">"] + assert t == [ + TypeXRefExternal("Partial", "typescript", "xxx", "Partial"), + "<", + "string", + ">", + ] def test_constrained_by_property(self):