Skip to content

Commit

Permalink
Move description rendering from typedoc.py to renderers.py (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoodmane authored Sep 24, 2023
1 parent febf92f commit 17b28bc
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 110 deletions.
38 changes: 21 additions & 17 deletions sphinx_js/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
survive template changes.
"""
from collections.abc import Sequence
from dataclasses import dataclass, field
from typing import Any

Expand All @@ -45,19 +46,22 @@ class TypeXRefExternal(TypeXRef):
qualifiedName: str


#: Human-readable type of a value. None if we don't know the type.
Type = str | list[str | TypeXRef] | None
# In the far future, we may take full control of our RST templates rather than
# using the js-domain directives provided by Sphinx. This would give us the
# freedom to link type names in formal param lists and param description lists
# to their definitions. To do this, we could replace the string-based Type with
# a class-based Type which internally preserves the structure of the type
# (simple for JS, fancy for TS) and can, on request, render it out as either
# text or link-having RST.
@dataclass
class DescriptionText:
text: str


@dataclass
class DescriptionCode:
code: str


DescriptionItem = DescriptionText | DescriptionCode

Description = str | Sequence[DescriptionItem]

#: Pathname, full or not, to an object:
ReStructuredText = str
#: Human-readable type of a value. None if we don't know the type.
Type = str | list[str | TypeXRef] | None


class Pathname:
Expand Down Expand Up @@ -117,7 +121,7 @@ class or interface"""
class TypeParam:
name: str
extends: Type
description: ReStructuredText = ReStructuredText("")
description: Description = ""


@dataclass
Expand All @@ -129,7 +133,7 @@ class Param:
#: The description text (like all other description fields in the IR)
#: retains any line breaks and subsequent indentation whitespace that were
#: in the source code.
description: ReStructuredText = ReStructuredText("")
description: Description = ""
has_default: bool = False
is_variadic: bool = False
type: Type | None = None
Expand All @@ -152,7 +156,7 @@ class Exc:

#: The type of exception can have
type: Type
description: ReStructuredText
description: Description


@dataclass
Expand All @@ -161,7 +165,7 @@ class Return:

#: The type this kind of return value can have
type: Type
description: ReStructuredText
description: Description


@dataclass
Expand Down Expand Up @@ -198,11 +202,11 @@ class TopLevel:
#: Either absolute or relative to the root_for_relative_js_paths.
deppath: str | None
#: The human-readable description of the entity or '' if absent
description: ReStructuredText
description: Description
#: Line number where the object (excluding any prefixing comment) begins
line: int | None
#: Explanation of the deprecation (which implies True) or True or False
deprecated: ReStructuredText | bool
deprecated: Description | bool
#: List of preformatted textual examples
examples: list[str]
#: List of paths to also refer the reader to
Expand Down
66 changes: 52 additions & 14 deletions sphinx_js/renderers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import textwrap
from collections.abc import Callable, Iterator
from re import sub
from typing import Any, Literal
Expand All @@ -16,6 +17,8 @@
from .ir import (
Attribute,
Class,
Description,
DescriptionText,
Exc,
Function,
Interface,
Expand Down Expand Up @@ -220,7 +223,42 @@ def _formal_params(self, obj: Function | Class) -> str:

return "({})".format(", ".join(formals))

def format_type(self, type: Type, escape: bool = False, bold: bool = True) -> str:
def render_description(self, description: Description) -> str:
"""Construct a single comment string from a fancy object."""
if isinstance(description, str):
return description
content = []
prev = ""
for s in description:
if isinstance(s, DescriptionText):
prev = s.text
content.append(prev)
continue
# code
if s.code.startswith("```"):
first_line, rest = s.code.split("\n", 1)
mid, _last_line = rest.rsplit("\n", 1)
code_type = first_line.removeprefix("```")
start = f".. code-block:: {code_type}\n\n"
codeblock = textwrap.indent(mid, " " * 4)
end = "\n\n"
content.append(start + codeblock + end)
# A code pen
continue

if s.code.startswith("``"):
# Sphinx-style escaped, leave it alone.
content.append(s.code)
continue
if prev.endswith(":"):
# A sphinx role, leave it alone
content.append(s.code)
continue
# Used single uptick with code, put double upticks
content.append(f"`{s.code}`")
return "".join(content)

def render_type(self, type: Type, escape: bool = False, bold: bool = True) -> str:
if not type:
return ""
if isinstance(type, str):
Expand Down Expand Up @@ -266,17 +304,17 @@ def _return_formatter(self, return_: Return) -> tuple[list[str], str]:
"""Derive heads and tail from ``@returns`` blocks."""
tail = []
if return_.type:
tail.append(self.format_type(return_.type, escape=False))
tail.append(self.render_type(return_.type, escape=False))
if return_.description:
tail.append(return_.description)
tail.append(self.render_description(return_.description))
return ["returns"], " -- ".join(tail)

def _type_param_formatter(self, tparam: TypeParam) -> tuple[list[str], str] | None:
v = tparam.name
if tparam.extends:
v += " extends " + self.format_type(tparam.extends)
v += " extends " + self.render_type(tparam.extends)
heads = ["typeparam", v]
return heads, tparam.description
return heads, self.render_description(tparam.description)

def _param_formatter(self, param: Param) -> tuple[list[str], str] | None:
"""Derive heads and tail from ``@param`` blocks."""
Expand All @@ -286,23 +324,23 @@ def _param_formatter(self, param: Param) -> tuple[list[str], str] | None:
heads = ["param"]
heads.append(param.name)

tail = param.description
tail = self.render_description(param.description)
return heads, tail

def _param_type_formatter(self, param: Param) -> tuple[list[str], str] | None:
"""Generate types for function parameters specified in field."""
if not param.type:
return None
heads = ["type", param.name]
tail = self.format_type(param.type)
tail = self.render_type(param.type)
return heads, tail

def _exception_formatter(self, exception: Exc) -> tuple[list[str], str]:
"""Derive heads and tail from ``@throws`` blocks."""
heads = ["throws"]
if exception.type:
heads.append(self.format_type(exception.type, bold=False))
tail = exception.description
heads.append(self.render_type(exception.type, bold=False))
tail = self.render_description(exception.description)
return heads, tail

def _fields(self, obj: TopLevel) -> Iterator[tuple[list[str], str]]:
Expand Down Expand Up @@ -347,7 +385,7 @@ def _template_vars(self, name: str, obj: Function) -> dict[str, Any]: # type: i
name=name,
params=self._formal_params(obj),
fields=self._fields(obj),
description=obj.description,
description=self.render_description(obj.description),
examples=obj.examples,
deprecated=obj.deprecated,
is_optional=obj.is_optional,
Expand Down Expand Up @@ -400,14 +438,14 @@ def _template_vars(self, name: str, obj: Class | Interface) -> dict[str, Any]:
deprecated=constructor.deprecated,
see_also=constructor.see_alsos,
exported_from=obj.exported_from,
class_comment=obj.description,
class_comment=self.render_description(obj.description),
is_abstract=isinstance(obj, Class) and obj.is_abstract,
interfaces=obj.interfaces if isinstance(obj, Class) else [],
is_interface=isinstance(
obj, Interface
), # TODO: Make interfaces not look so much like classes. This will require taking complete control of templating from Sphinx.
supers=obj.supers,
constructor_comment=constructor.description,
constructor_comment=self.render_description(constructor.description),
content="\n".join(self._content),
members=self._members_of(
obj,
Expand Down Expand Up @@ -507,12 +545,12 @@ class AutoAttributeRenderer(JsRenderer):
def _template_vars(self, name: str, obj: Attribute) -> dict[str, Any]: # type: ignore[override]
return dict(
name=name,
description=obj.description,
description=self.render_description(obj.description),
deprecated=obj.deprecated,
is_optional=obj.is_optional,
see_also=obj.see_alsos,
examples=obj.examples,
type=self.format_type(obj.type),
type=self.render_type(obj.type),
content="\n".join(self._content),
)

Expand Down
83 changes: 30 additions & 53 deletions sphinx_js/typedoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import pathlib
import re
import subprocess
import textwrap
from collections import defaultdict
from collections.abc import Iterable, Iterator, Sequence
from errno import ENOENT
from functools import cache
from functools import cache, partial
from inspect import isclass
from json import load
from os.path import basename, relpath, sep, splitext
Expand Down Expand Up @@ -277,29 +277,42 @@ class Source(BaseModel):
line: int


class Summary(BaseModel):
class DescriptionItem(BaseModel):
kind: Literal["text", "code"]
text: str

def to_ir(self) -> ir.DescriptionItem:
if self.kind == "text":
return ir.DescriptionText(self.text)
return ir.DescriptionCode(self.text)


class Tag(BaseModel):
tag: str
content: list[Summary]
content: list[DescriptionItem]


def description_to_ir(desc: Sequence[DescriptionItem]) -> Sequence[ir.DescriptionItem]:
return [item.to_ir() for item in desc]


class Comment(BaseModel):
returns: str = ""
summary: list[Summary] = []
summary: list[DescriptionItem] = []
blockTags: list[Tag] = []
tags: dict[str, list[Sequence[DescriptionItem]]] = Field(
default_factory=partial(defaultdict, list)
)

def get_returns(self) -> str:
result = self.returns.strip()
if result:
return result
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
for tag in self.blockTags:
if tag.tag == "@returns":
return tag.content[0].text.strip()
return ""
self.tags[tag.tag.removeprefix("@")].append(tag.content)

def get_description(self) -> Sequence[ir.DescriptionItem]:
return description_to_ir(self.summary)

def get_returns(self) -> Sequence[ir.DescriptionItem]:
return description_to_ir(next(iter(self.tags["returns"]), []))


class Flags(BaseModel):
Expand Down Expand Up @@ -353,7 +366,7 @@ class TopLevelPropertiesDict(TypedDict):
path: ir.Pathname
filename: str
deppath: str | None
description: str
description: Sequence[ir.DescriptionItem]
line: int | None
deprecated: bool
examples: list[str]
Expand All @@ -377,7 +390,7 @@ def _top_level_properties(self) -> TopLevelPropertiesDict:
path=ir.Pathname(self.path),
filename=basename(self.filename),
deppath=self.filename,
description=make_description(self.comment),
description=self.comment.get_description(),
line=self.sources[0].line if self.sources else None,
# These properties aren't supported by TypeDoc:
deprecated=False,
Expand Down Expand Up @@ -682,42 +695,6 @@ class OtherNode(NodeBase):
ClassChild = Annotated[Accessor | Callable | Member, Field(discriminator="kindString")]


def make_description(comment: Comment) -> str:
"""Construct a single comment string from a fancy object."""
if not comment.summary:
return ""
content = []
prev = ""
for s in comment.summary:
if s.kind == "text":
prev = s.text
content.append(prev)
continue
# code
if s.text.startswith("```"):
first_line, rest = s.text.split("\n", 1)
mid, _last_line = rest.rsplit("\n", 1)
code_type = first_line.removeprefix("```")
start = f".. code-block:: {code_type}\n\n"
codeblock = textwrap.indent(mid, " " * 4)
end = "\n\n"
content.append(start + codeblock + end)
# A code pen
continue

if s.text.startswith("``"):
# Sphinx-style escaped, leave it alone.
content.append(s.text)
continue
if prev.endswith(":"):
# A sphinx role, leave it alone
content.append(s.text)
continue
# Used single uptick with code, put double upticks
content.append(f"`{s.text}`")
return "".join(content)


class TypeParameter(Base):
kindString: Literal["Type parameter"]
name: str
Expand All @@ -729,7 +706,7 @@ def to_ir(self, converter: Converter) -> ir.TypeParam:
if self.type:
extends = self.type.render_name(converter)
return ir.TypeParam(
self.name, extends, description=make_description(self.comment)
self.name, extends, description=self.comment.get_description()
)

def _path_segments(self, base_dir: str) -> list[str]:
Expand All @@ -749,7 +726,7 @@ def to_ir(self, converter: Converter) -> ir.Param:
default = self.defaultValue or ir.NO_DEFAULT
return ir.Param(
name=self.name,
description=make_description(self.comment),
description=self.comment.get_description(),
has_default=self.defaultValue is not None,
is_variadic=self.flags.isRest,
# For now, we just pass a single string in as the type rather than
Expand Down
Loading

0 comments on commit 17b28bc

Please sign in to comment.