Skip to content

Commit

Permalink
Wrap types in <span class='sphinx-js_type'> (#85)
Browse files Browse the repository at this point in the history
This gives a target for people to use css to alter the rendering of types.
  • Loading branch information
hoodmane authored Sep 28, 2023
1 parent de15ed3 commit 92c1882
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 7 deletions.
2 changes: 2 additions & 0 deletions sphinx_js/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions sphinx_js/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,48 @@
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

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 "\<space>" =>
# "<space>"
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 <span class="sphinx_js-type"> </span>
"""
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"""

Expand Down
9 changes: 8 additions & 1 deletion sphinx_js/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class JsRenderer:
_explicit_formal_params: str
_content: list[str]
_options: dict[str, Any]
# We turn the <span class="sphinx_js-type"> 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion sphinx_js/typedoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion tests/test_build_ts/test_build_ts.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def test_predicate(self):
predicate(c)
Arguments:
* **c** (*any*) --
* **c** (any) --
Returns:
c is "ConstructorlessClass()"
Expand Down Expand Up @@ -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<void>"
1 change: 1 addition & 0 deletions tests/test_renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
11 changes: 7 additions & 4 deletions tests/test_typedoc_analysis/test_typedoc_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
]

Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand Down

0 comments on commit 92c1882

Please sign in to comment.