Skip to content

Commit

Permalink
Add support for @groupDescription and @categoryDescription
Browse files Browse the repository at this point in the history
Resolves #2494.
  • Loading branch information
Gerrit0 committed Feb 9, 2024
1 parent 285b537 commit d26f76f
Show file tree
Hide file tree
Showing 21 changed files with 376 additions and 24 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
- Added support for the `@class` tag. When added to a comment on a variable or function, TypeDoc will convert the member as a class, #2479.
Note: This should only be used on symbols which actually represent a class, but are not declared as a class for some reason.

## Features

- Added support for `@groupDescription` and `@categoryDescription` to provide a description of groups and categories, #2494.

## Bug Fixes

- Fixed an issue where a namespace would not be created for merged function-namespaces which are declared as variables, #2478.
Expand Down
6 changes: 6 additions & 0 deletions example/src/classes/CancellablePromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ function isPromiseWithCancel<T>(value: unknown): value is PromiseWithCancel<T> {
* [real-cancellable-promise](https://github.com/srmagura/real-cancellable-promise).
*
* @typeParam T what the `CancellablePromise` resolves to
*
* @groupDescription Methods
* Descriptions can be added for groups with `@groupDescription`, which will show up in
* the index where groups are listed. This works for both manually created groups which
* are created with `@group`, and implicit groups like the `Methods` group that this
* description is attached to.
*/
export class CancellablePromise<T> {
/**
Expand Down
6 changes: 6 additions & 0 deletions example/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* @packageDocumentation
* @categoryDescription Component
* React Components -- This description is added with the `@categoryDescription` tag
* on the entry point in src/index.ts
*/
export * from "./functions";
export * from "./variables";
export * from "./types";
Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ export { EventDispatcher, Event } from "./lib/utils/events";
export { resetReflectionID } from "./lib/models/reflections/abstract";
/**
* All symbols documented under the Models namespace are also available in the root import.
*
* @categoryDescription Types
* Describes a TypeScript type.
*
* @categoryDescription Reflections
* Describes a documentation entry. The root entry is a {@link ProjectReflection}
* and contains {@link DeclarationReflection} instances.
*/
export * as Models from "./lib/models";
/**
Expand Down
31 changes: 28 additions & 3 deletions src/lib/converter/plugins/CategoryPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ReflectionCategory } from "../../models";
import { Component, ConverterComponent } from "../components";
import { Converter } from "../converter";
import type { Context } from "../context";
import { Option, getSortFunction } from "../../utils";
import { Option, getSortFunction, removeIf } from "../../utils";

/**
* A handler that sorts and categorizes the found reflections in the resolving phase.
Expand Down Expand Up @@ -113,7 +113,10 @@ export class CategoryPlugin extends ConverterComponent {
obj.groups.forEach((group) => {
if (group.categories) return;

group.categories = this.getReflectionCategories(group.children);
group.categories = this.getReflectionCategories(
obj,
group.children,
);
if (group.categories && group.categories.length > 1) {
group.categories.sort(CategoryPlugin.sortCatCallback);
} else if (
Expand All @@ -130,7 +133,7 @@ export class CategoryPlugin extends ConverterComponent {
if (!obj.children || obj.children.length === 0 || obj.categories) {
return;
}
obj.categories = this.getReflectionCategories(obj.children);
obj.categories = this.getReflectionCategories(obj, obj.children);
if (obj.categories && obj.categories.length > 1) {
obj.categories.sort(CategoryPlugin.sortCatCallback);
} else if (
Expand All @@ -151,6 +154,7 @@ export class CategoryPlugin extends ConverterComponent {
* @returns An array containing all children of the given reflection categorized
*/
private getReflectionCategories(
parent: ContainerReflection,
reflections: DeclarationReflection[],
): ReflectionCategory[] {
const categories = new Map<string, ReflectionCategory>();
Expand All @@ -174,6 +178,27 @@ export class CategoryPlugin extends ConverterComponent {
}
}

if (parent.comment) {
removeIf(parent.comment.blockTags, (tag) => {
if (tag.tag === "@categoryDescription") {
const { header, body } = Comment.splitPartsToHeaderAndBody(
tag.content,
);
const cat = categories.get(header);
if (cat) {
cat.description = body;
} else {
this.application.logger.warn(
`Comment for ${parent.getFriendlyFullName()} includes @categoryDescription for "${header}", but no child is placed in that category.`,
);
}

return true;
}
return false;
});
}

for (const cat of categories.values()) {
this.sortFunction(cat.children);
}
Expand Down
27 changes: 26 additions & 1 deletion src/lib/converter/plugins/GroupPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ export class GroupPlugin extends ConverterComponent {
) {
this.sortFunction(reflection.children);
}
reflection.groups = this.getReflectionGroups(reflection.children);
reflection.groups = this.getReflectionGroups(
reflection,
reflection.children,
);
}
}

Expand Down Expand Up @@ -162,6 +165,7 @@ export class GroupPlugin extends ConverterComponent {
* @returns An array containing all children of the given reflection grouped by their kind.
*/
getReflectionGroups(
parent: ContainerReflection,
reflections: DeclarationReflection[],
): ReflectionGroup[] {
const groups = new Map<string, ReflectionGroup>();
Expand All @@ -178,6 +182,27 @@ export class GroupPlugin extends ConverterComponent {
}
});

if (parent.comment) {
removeIf(parent.comment.blockTags, (tag) => {
if (tag.tag === "@groupDescription") {
const { header, body } = Comment.splitPartsToHeaderAndBody(
tag.content,
);
const cat = groups.get(header);
if (cat) {
cat.description = body;
} else {
this.application.logger.warn(
`Comment for ${parent.getFriendlyFullName()} includes @groupDescription for "${header}", but no child is placed in that group.`,
);
}

return true;
}
return false;
});
}

return Array.from(groups.values()).sort(GroupPlugin.sortGroupCallback);
}

Expand Down
45 changes: 44 additions & 1 deletion src/lib/converter/plugins/LinkResolverPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { Component, ConverterComponent } from "../components";
import type { Context, ExternalResolveResult } from "../../converter";
import { ConverterEvents } from "../converter-events";
import { Option, ValidationOptions } from "../../utils";
import { DeclarationReflection, ProjectReflection } from "../../models";
import {
ContainerReflection,
DeclarationReflection,
ProjectReflection,
Reflection,
ReflectionCategory,
} from "../../models";
import { discoverAllReferenceTypes } from "../../utils/reflections";
import { ApplicationEvents } from "../../application-events";

Expand Down Expand Up @@ -45,6 +51,31 @@ export class LinkResolverPlugin extends ConverterComponent {
reflection,
);
}

if (reflection instanceof ContainerReflection) {
if (reflection.groups) {
for (const group of reflection.groups) {
if (group.description) {
group.description = this.owner.resolveLinks(
group.description,
reflection,
);
}

if (group.categories) {
for (const cat of group.categories) {
this.resolveCategoryLinks(cat, reflection);
}
}
}
}

if (reflection.categories) {
for (const cat of reflection.categories) {
this.resolveCategoryLinks(cat, reflection);
}
}
}
}

if (project.readme) {
Expand Down Expand Up @@ -75,4 +106,16 @@ export class LinkResolverPlugin extends ConverterComponent {
}
}
}

private resolveCategoryLinks(
category: ReflectionCategory,
owner: Reflection,
) {
if (category.description) {
category.description = this.owner.resolveLinks(
category.description,
owner,
);
}
}
}
23 changes: 21 additions & 2 deletions src/lib/models/ReflectionCategory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { DeclarationReflection } from ".";
import {
Comment,
type CommentDisplayPart,
type DeclarationReflection,
} from ".";
import type { Serializer, JSONOutput, Deserializer } from "../serialization";

/**
Expand All @@ -14,6 +18,11 @@ export class ReflectionCategory {
*/
title: string;

/**
* The user specified description, if any, set with `@categoryDescription`
*/
description?: CommentDisplayPart[];

/**
* All reflections of this category.
*/
Expand All @@ -35,9 +44,12 @@ export class ReflectionCategory {
return this.children.every((child) => child.hasOwnDocument);
}

toObject(_serializer: Serializer): JSONOutput.ReflectionCategory {
toObject(serializer: Serializer): JSONOutput.ReflectionCategory {
return {
title: this.title,
description: this.description
? Comment.serializeDisplayParts(serializer, this.description)
: undefined,
children:
this.children.length > 0
? this.children.map((child) => child.id)
Expand All @@ -46,6 +58,13 @@ export class ReflectionCategory {
}

fromObject(de: Deserializer, obj: JSONOutput.ReflectionCategory) {
if (obj.description) {
this.description = Comment.deserializeDisplayParts(
de,
obj.description,
);
}

if (obj.children) {
de.defer((project) => {
for (const childId of obj.children || []) {
Expand Down
22 changes: 21 additions & 1 deletion src/lib/models/ReflectionGroup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ReflectionCategory } from "./ReflectionCategory";
import type { DeclarationReflection, Reflection } from ".";
import {
Comment,
type CommentDisplayPart,
type DeclarationReflection,
type Reflection,
} from ".";
import type { Serializer, JSONOutput, Deserializer } from "../serialization";

/**
Expand All @@ -15,6 +20,11 @@ export class ReflectionGroup {
*/
title: string;

/**
* User specified description via `@groupDescription`, if specified.
*/
description?: CommentDisplayPart[];

/**
* All reflections of this group.
*/
Expand Down Expand Up @@ -48,6 +58,9 @@ export class ReflectionGroup {
toObject(serializer: Serializer): JSONOutput.ReflectionGroup {
return {
title: this.title,
description: this.description
? Comment.serializeDisplayParts(serializer, this.description)
: undefined,
children:
this.children.length > 0
? this.children.map((child) => child.id)
Expand All @@ -57,6 +70,13 @@ export class ReflectionGroup {
}

fromObject(de: Deserializer, obj: JSONOutput.ReflectionGroup) {
if (obj.description) {
this.description = Comment.deserializeDisplayParts(
de,
obj.description,
);
}

if (obj.categories) {
this.categories = obj.categories.map((catObj) => {
const cat = new ReflectionCategory(catObj.title);
Expand Down
57 changes: 56 additions & 1 deletion src/lib/models/comments/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export class Comment {
/**
* Helper utility to clone {@link Comment.summary} or {@link CommentTag.content}
*/
static cloneDisplayParts(parts: CommentDisplayPart[]) {
static cloneDisplayParts(parts: readonly CommentDisplayPart[]) {
return parts.map((p) => ({ ...p }));
}

Expand Down Expand Up @@ -304,6 +304,61 @@ export class Comment {
return result;
}

/**
* Splits the provided parts into a header (first line, as a string)
* and body (remaining lines). If the header line contains inline tags
* they will be serialized to a string.
*/
static splitPartsToHeaderAndBody(parts: readonly CommentDisplayPart[]): {
header: string;
body: CommentDisplayPart[];
} {
let index = parts.findIndex((part): boolean => {
switch (part.kind) {
case "text":
case "code":
return part.text.includes("\n");
case "inline-tag":
return false;
}
});

if (index === -1) {
return {
header: Comment.combineDisplayParts(parts),
body: [],
};
}

// Do not split a code block, stop the header at the end of the previous block
if (parts[index].kind === "code") {
--index;
}

if (index === -1) {
return { header: "", body: Comment.cloneDisplayParts(parts) };
}

let header = Comment.combineDisplayParts(parts.slice(0, index));
const split = parts[index].text.indexOf("\n");

let body: CommentDisplayPart[];
if (split === -1) {
header += parts[index].text;
body = Comment.cloneDisplayParts(parts.slice(index + 1));
} else {
header += parts[index].text.substring(0, split);
body = Comment.cloneDisplayParts(parts.slice(index));
body[0].text = body[0].text.substring(split + 1);
}

if (!body[0].text) {
body.shift();
}

return { header: header.trim(), body };
}

/**
* The content of the comment which is not associated with a block tag.
*/
Expand Down
Loading

0 comments on commit d26f76f

Please sign in to comment.