From 33113614e034702c79d864423792c2d786e385c0 Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Fri, 29 Jul 2022 04:46:17 -0400 Subject: [PATCH] feat(website): parse tsdoc comments (#8386) --- packages/website/src/DocModel/DocEnum.ts | 9 ++- packages/website/src/DocModel/DocItem.ts | 16 +++-- .../src/DocModel/comment/CommentNode.ts | 22 +++++++ .../DocModel/comment/CommentNodeContainer.ts | 20 ++++++ .../DocModel/comment/FencedCodeCommentNode.ts | 22 +++++++ .../DocModel/comment/LinkTagCommentNode.ts | 54 ++++++++++++++++ .../DocModel/comment/PlainTextCommentNode.ts | 19 ++++++ .../website/src/DocModel/comment/index.ts | 22 +++++++ .../website/src/components/CodeListing.tsx | 6 +- packages/website/src/components/Comment.tsx | 61 +++++++++++++++++++ .../website/src/components/DocContainer.tsx | 10 ++- .../website/src/components/MethodItem.tsx | 3 +- packages/website/src/util/parse.server.ts | 2 +- 13 files changed, 253 insertions(+), 13 deletions(-) create mode 100644 packages/website/src/DocModel/comment/CommentNode.ts create mode 100644 packages/website/src/DocModel/comment/CommentNodeContainer.ts create mode 100644 packages/website/src/DocModel/comment/FencedCodeCommentNode.ts create mode 100644 packages/website/src/DocModel/comment/LinkTagCommentNode.ts create mode 100644 packages/website/src/DocModel/comment/PlainTextCommentNode.ts create mode 100644 packages/website/src/DocModel/comment/index.ts create mode 100644 packages/website/src/components/Comment.tsx diff --git a/packages/website/src/DocModel/DocEnum.ts b/packages/website/src/DocModel/DocEnum.ts index 5d93616b9..6b6e649a7 100644 --- a/packages/website/src/DocModel/DocEnum.ts +++ b/packages/website/src/DocModel/DocEnum.ts @@ -1,11 +1,12 @@ import type { ApiEnum, ApiModel } from '@microsoft/api-extractor-model'; import { DocItem } from './DocItem'; -import { genToken, resolveDocComment, TokenDocumentation } from '~/util/parse.server'; +import { CommentNodeContainer } from './comment/CommentNodeContainer'; +import { genToken, TokenDocumentation } from '~/util/parse.server'; export interface EnumMemberData { name: string; initializerTokens: TokenDocumentation[]; - summary: string | null; + summary: ReturnType['summary']; } export class DocEnum extends DocItem { @@ -17,7 +18,9 @@ export class DocEnum extends DocItem { this.members = item.members.map((member) => ({ name: member.name, initializerTokens: member.initializerExcerpt?.spannedTokens.map((token) => genToken(this.model, token)) ?? [], - summary: resolveDocComment(member), + summary: member.tsdocComment + ? new CommentNodeContainer(member.tsdocComment.summarySection, model, member).toJSON() + : null, })); } diff --git a/packages/website/src/DocModel/DocItem.ts b/packages/website/src/DocModel/DocItem.ts index c4505595d..c99607e4d 100644 --- a/packages/website/src/DocModel/DocItem.ts +++ b/packages/website/src/DocModel/DocItem.ts @@ -1,6 +1,7 @@ import type { ApiModel, ApiDeclaredItem } from '@microsoft/api-extractor-model'; +import { CommentNodeContainer } from './comment/CommentNodeContainer'; import type { ReferenceData } from '~/util/model.server'; -import { resolveName, genReference, resolveDocComment, TokenDocumentation, genToken } from '~/util/parse.server'; +import { resolveName, genReference, TokenDocumentation, genToken } from '~/util/parse.server'; export type DocItemConstructor = new (...args: any[]) => T; @@ -8,11 +9,12 @@ export class DocItem { public readonly item: T; public readonly name: string; public readonly referenceData: ReferenceData; - public readonly summary: string | null; public readonly model: ApiModel; public readonly excerpt: string; public readonly excerptTokens: TokenDocumentation[] = []; public readonly kind: string; + public readonly remarks: CommentNodeContainer | null; + public readonly summary: CommentNodeContainer | null; public constructor(model: ApiModel, item: T) { this.item = item; @@ -20,19 +22,25 @@ export class DocItem { this.model = model; this.name = resolveName(item); this.referenceData = genReference(item); - this.summary = resolveDocComment(item); this.excerpt = item.excerpt.text; this.excerptTokens = item.excerpt.spannedTokens.map((token) => genToken(model, token)); + this.remarks = item.tsdocComment?.remarksBlock + ? new CommentNodeContainer(item.tsdocComment.remarksBlock.content, model, item.parent) + : null; + this.summary = item.tsdocComment?.summarySection + ? new CommentNodeContainer(item.tsdocComment.summarySection, model, item.parent) + : null; } public toJSON() { return { name: this.name, referenceData: this.referenceData, - summary: this.summary, + summary: this.summary?.toJSON() ?? null, excerpt: this.excerpt, excerptTokens: this.excerptTokens, kind: this.kind, + remarks: this.remarks?.toJSON() ?? null, }; } } diff --git a/packages/website/src/DocModel/comment/CommentNode.ts b/packages/website/src/DocModel/comment/CommentNode.ts new file mode 100644 index 000000000..bcaf74a5a --- /dev/null +++ b/packages/website/src/DocModel/comment/CommentNode.ts @@ -0,0 +1,22 @@ +import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; +import type { DocNode } from '@microsoft/tsdoc'; + +export class CommentNode { + public readonly node: T; + public readonly kind: string; + public readonly model: ApiModel; + public readonly parentItem: ApiItem | null; + + public constructor(node: T, model: ApiModel, parentItem?: ApiItem) { + this.node = node; + this.kind = node.kind; + this.model = model; + this.parentItem = parentItem ?? null; + } + + public toJSON() { + return { + kind: this.kind, + }; + } +} diff --git a/packages/website/src/DocModel/comment/CommentNodeContainer.ts b/packages/website/src/DocModel/comment/CommentNodeContainer.ts new file mode 100644 index 000000000..8ca4210df --- /dev/null +++ b/packages/website/src/DocModel/comment/CommentNodeContainer.ts @@ -0,0 +1,20 @@ +import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; +import type { DocNodeContainer } from '@microsoft/tsdoc'; +import { createCommentNode } from '.'; +import { CommentNode } from './CommentNode'; + +export class CommentNodeContainer extends CommentNode { + public readonly nodes: CommentNode[]; + + public constructor(container: T, model: ApiModel, parentItem?: ApiItem) { + super(container, model, parentItem); + this.nodes = container.nodes.map((node) => createCommentNode(node, model, parentItem)); + } + + public override toJSON() { + return { + ...super.toJSON(), + nodes: this.nodes.map((node) => node.toJSON()), + }; + } +} diff --git a/packages/website/src/DocModel/comment/FencedCodeCommentNode.ts b/packages/website/src/DocModel/comment/FencedCodeCommentNode.ts new file mode 100644 index 000000000..11f7f3e0b --- /dev/null +++ b/packages/website/src/DocModel/comment/FencedCodeCommentNode.ts @@ -0,0 +1,22 @@ +import type { ApiModel, ApiItem } from '@microsoft/api-extractor-model'; +import type { DocFencedCode } from '@microsoft/tsdoc'; +import { CommentNode } from './CommentNode'; + +export class FencedCodeCommentNode extends CommentNode { + public readonly code: string; + public readonly language: string; + + public constructor(node: DocFencedCode, model: ApiModel, parentItem?: ApiItem) { + super(node, model, parentItem); + this.code = node.code; + this.language = node.language; + } + + public override toJSON() { + return { + ...super.toJSON(), + code: this.code, + language: this.language, + }; + } +} diff --git a/packages/website/src/DocModel/comment/LinkTagCommentNode.ts b/packages/website/src/DocModel/comment/LinkTagCommentNode.ts new file mode 100644 index 000000000..7b1253d38 --- /dev/null +++ b/packages/website/src/DocModel/comment/LinkTagCommentNode.ts @@ -0,0 +1,54 @@ +import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; +import type { DocDeclarationReference, DocLinkTag } from '@microsoft/tsdoc'; +import { CommentNode } from './CommentNode'; +import { generatePath, resolveName } from '~/util/parse.server'; + +export function genToken( + model: ApiModel, + ref: DocDeclarationReference, + context: ApiItem | null, +): LinkTagCodeLink | null { + if (!context) { + return null; + } + + const item = model.resolveDeclarationReference(ref, context).resolvedApiItem ?? null; + + if (!item) { + return null; + } + + return { + name: resolveName(item), + kind: item.kind, + path: generatePath(item.getHierarchy()), + }; +} + +export interface LinkTagCodeLink { + name: string; + kind: string; + path: string; +} + +export class LinkTagCommentNode extends CommentNode { + public readonly codeDestination: LinkTagCodeLink | null; + public readonly text: string | null; + public readonly urlDestination: string | null; + + public constructor(node: DocLinkTag, model: ApiModel, parentItem?: ApiItem) { + super(node, model, parentItem); + this.codeDestination = node.codeDestination ? genToken(model, node.codeDestination, this.parentItem) : null; + this.text = node.linkText ?? null; + this.urlDestination = node.urlDestination ?? null; + } + + public override toJSON() { + return { + ...super.toJSON(), + text: this.text, + codeDestination: this.codeDestination, + urlDestination: this.urlDestination, + }; + } +} diff --git a/packages/website/src/DocModel/comment/PlainTextCommentNode.ts b/packages/website/src/DocModel/comment/PlainTextCommentNode.ts new file mode 100644 index 000000000..37f960717 --- /dev/null +++ b/packages/website/src/DocModel/comment/PlainTextCommentNode.ts @@ -0,0 +1,19 @@ +import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; +import type { DocPlainText } from '@microsoft/tsdoc'; +import { CommentNode } from './CommentNode'; + +export class PlainTextCommentNode extends CommentNode { + public readonly text: string; + + public constructor(node: DocPlainText, model: ApiModel, parentItem?: ApiItem) { + super(node, model, parentItem); + this.text = node.text; + } + + public override toJSON() { + return { + ...super.toJSON(), + text: this.text, + }; + } +} diff --git a/packages/website/src/DocModel/comment/index.ts b/packages/website/src/DocModel/comment/index.ts new file mode 100644 index 000000000..641e61aeb --- /dev/null +++ b/packages/website/src/DocModel/comment/index.ts @@ -0,0 +1,22 @@ +import type { ApiModel, ApiItem } from '@microsoft/api-extractor-model'; +import type { DocNode, DocPlainText, DocLinkTag, DocParagraph, DocFencedCode } from '@microsoft/tsdoc'; +import { CommentNode } from './CommentNode'; +import { CommentNodeContainer } from './CommentNodeContainer'; +import { FencedCodeCommentNode } from './FencedCodeCommentNode'; +import { LinkTagCommentNode } from './LinkTagCommentNode'; +import { PlainTextCommentNode } from './PlainTextCommentNode'; + +export function createCommentNode(node: DocNode, model: ApiModel, parentItem?: ApiItem): CommentNode { + switch (node.kind) { + case 'PlainText': + return new PlainTextCommentNode(node as DocPlainText, model, parentItem); + case 'LinkTag': + return new LinkTagCommentNode(node as DocLinkTag, model, parentItem); + case 'Paragraph': + return new CommentNodeContainer(node as DocParagraph, model, parentItem); + case 'FencedCode': + return new FencedCodeCommentNode(node as DocFencedCode, model, parentItem); + default: + return new CommentNode(node, model, parentItem); + } +} diff --git a/packages/website/src/components/CodeListing.tsx b/packages/website/src/components/CodeListing.tsx index c9b6b4caa..cb78ac9a3 100644 --- a/packages/website/src/components/CodeListing.tsx +++ b/packages/website/src/components/CodeListing.tsx @@ -1,5 +1,7 @@ import type { ReactNode } from 'react'; +import { CommentSection } from './Comment'; import { HyperlinkedText } from './HyperlinkedText'; +import type { DocItem } from '~/DocModel/DocItem'; import type { TokenDocumentation } from '~/util/parse.server'; export enum CodeListingSeparatorType { @@ -9,7 +11,7 @@ export enum CodeListingSeparatorType { export interface CodeListingProps { name: string; - summary?: string | null; + summary?: ReturnType['summary']; typeTokens: TokenDocumentation[]; separator?: CodeListingSeparatorType; children?: ReactNode; @@ -34,7 +36,7 @@ export function CodeListing({ - {summary &&

{summary}

} + {summary && } {children} diff --git a/packages/website/src/components/Comment.tsx b/packages/website/src/components/Comment.tsx new file mode 100644 index 000000000..90e51f056 --- /dev/null +++ b/packages/website/src/components/Comment.tsx @@ -0,0 +1,61 @@ +import Link from 'next/link'; +import type { ReactNode } from 'react'; +import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'; +import type { CommentNode } from '~/DocModel/comment/CommentNode'; +import type { CommentNodeContainer } from '~/DocModel/comment/CommentNodeContainer'; +import type { FencedCodeCommentNode } from '~/DocModel/comment/FencedCodeCommentNode'; +import type { LinkTagCommentNode } from '~/DocModel/comment/LinkTagCommentNode'; +import type { PlainTextCommentNode } from '~/DocModel/comment/PlainTextCommentNode'; + +export interface RemarksBlockProps { + node: ReturnType; + textClassName?: string | undefined; +} + +export function CommentSection({ node, textClassName }: RemarksBlockProps): JSX.Element { + const createNode = (node: ReturnType): ReactNode => { + switch (node.kind) { + case 'PlainText': + return {(node as ReturnType).text}; + case 'Paragraph': + return ( +

+ {(node as ReturnType).nodes.map((node) => createNode(node))} +

+ ); + case 'SoftBreak': + return
; + case 'LinkTag': { + const { codeDestination, urlDestination, text } = node as ReturnType; + + if (codeDestination) { + return {text ?? codeDestination.name}; + } + + if (urlDestination) { + return {text ?? urlDestination}; + } + + return null; + } + case 'FencedCodeBlock': { + const { language, code } = node as ReturnType; + return {code}; + } + default: + break; + } + + return null; + }; + + return ( +
+ {node.kind === 'Paragraph' || node.kind === 'Section' ? ( + <>{(node as CommentNodeContainer).nodes.map(createNode)} + ) : ( + <>{createNode(node)} + )} +
+ ); +} diff --git a/packages/website/src/components/DocContainer.tsx b/packages/website/src/components/DocContainer.tsx index e795ebfd7..7ce0fa701 100644 --- a/packages/website/src/components/DocContainer.tsx +++ b/packages/website/src/components/DocContainer.tsx @@ -3,9 +3,11 @@ import { VscListSelection, VscSymbolParameter } from 'react-icons/vsc'; import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { CodeListingSeparatorType } from './CodeListing'; +import { CommentSection } from './Comment'; import { HyperlinkedText } from './HyperlinkedText'; import { Section } from './Section'; import { TypeParamTable } from './TypeParamTable'; +import type { DocItem } from '~/DocModel/DocItem'; import { generateIcon } from '~/util/icon'; import type { TokenDocumentation, TypeParameterData } from '~/util/parse.server'; @@ -13,7 +15,7 @@ export interface DocContainerProps { name: string; kind: string; excerpt: string; - summary?: string | null; + summary?: ReturnType['summary']; children?: ReactNode; extendsTokens?: TokenDocumentation[] | null; typeParams?: TypeParameterData[]; @@ -50,7 +52,11 @@ export function DocContainer({ name, kind, excerpt, summary, typeParams, childre ) : null}
} title="Summary" className="dark:text-white"> -

{summary ?? 'No summary provided.'}

+ {summary ? ( + + ) : ( +

No summary provided.

+ )}
{typeParams?.length ? (
- {data.summary &&

{data.summary}

} + {data.summary && } {data.parameters.length ? : null}
diff --git a/packages/website/src/util/parse.server.ts b/packages/website/src/util/parse.server.ts index 81fda76a0..6cff797e3 100644 --- a/packages/website/src/util/parse.server.ts +++ b/packages/website/src/util/parse.server.ts @@ -21,7 +21,7 @@ export function findPackage(model: ApiModel, name: string): ApiPackage | undefin | undefined; } -function generatePath(items: readonly ApiItem[]) { +export function generatePath(items: readonly ApiItem[]) { let path = '/docs/main/packages/'; for (const item of items) { switch (item.kind) {