Skip to content

Commit d26f76f

Browse files
committed
Add support for @groupDescription and @categoryDescription
Resolves #2494.
1 parent 285b537 commit d26f76f

File tree

21 files changed

+376
-24
lines changed

21 files changed

+376
-24
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
- 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.
77
Note: This should only be used on symbols which actually represent a class, but are not declared as a class for some reason.
88

9+
## Features
10+
11+
- Added support for `@groupDescription` and `@categoryDescription` to provide a description of groups and categories, #2494.
12+
913
## Bug Fixes
1014

1115
- Fixed an issue where a namespace would not be created for merged function-namespaces which are declared as variables, #2478.

example/src/classes/CancellablePromise.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ function isPromiseWithCancel<T>(value: unknown): value is PromiseWithCancel<T> {
4242
* [real-cancellable-promise](https://github.com/srmagura/real-cancellable-promise).
4343
*
4444
* @typeParam T what the `CancellablePromise` resolves to
45+
*
46+
* @groupDescription Methods
47+
* Descriptions can be added for groups with `@groupDescription`, which will show up in
48+
* the index where groups are listed. This works for both manually created groups which
49+
* are created with `@group`, and implicit groups like the `Methods` group that this
50+
* description is attached to.
4551
*/
4652
export class CancellablePromise<T> {
4753
/**

example/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* @packageDocumentation
3+
* @categoryDescription Component
4+
* React Components -- This description is added with the `@categoryDescription` tag
5+
* on the entry point in src/index.ts
6+
*/
17
export * from "./functions";
28
export * from "./variables";
39
export * from "./types";

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ export { EventDispatcher, Event } from "./lib/utils/events";
44
export { resetReflectionID } from "./lib/models/reflections/abstract";
55
/**
66
* All symbols documented under the Models namespace are also available in the root import.
7+
*
8+
* @categoryDescription Types
9+
* Describes a TypeScript type.
10+
*
11+
* @categoryDescription Reflections
12+
* Describes a documentation entry. The root entry is a {@link ProjectReflection}
13+
* and contains {@link DeclarationReflection} instances.
714
*/
815
export * as Models from "./lib/models";
916
/**

src/lib/converter/plugins/CategoryPlugin.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ReflectionCategory } from "../../models";
77
import { Component, ConverterComponent } from "../components";
88
import { Converter } from "../converter";
99
import type { Context } from "../context";
10-
import { Option, getSortFunction } from "../../utils";
10+
import { Option, getSortFunction, removeIf } from "../../utils";
1111

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

116-
group.categories = this.getReflectionCategories(group.children);
116+
group.categories = this.getReflectionCategories(
117+
obj,
118+
group.children,
119+
);
117120
if (group.categories && group.categories.length > 1) {
118121
group.categories.sort(CategoryPlugin.sortCatCallback);
119122
} else if (
@@ -130,7 +133,7 @@ export class CategoryPlugin extends ConverterComponent {
130133
if (!obj.children || obj.children.length === 0 || obj.categories) {
131134
return;
132135
}
133-
obj.categories = this.getReflectionCategories(obj.children);
136+
obj.categories = this.getReflectionCategories(obj, obj.children);
134137
if (obj.categories && obj.categories.length > 1) {
135138
obj.categories.sort(CategoryPlugin.sortCatCallback);
136139
} else if (
@@ -151,6 +154,7 @@ export class CategoryPlugin extends ConverterComponent {
151154
* @returns An array containing all children of the given reflection categorized
152155
*/
153156
private getReflectionCategories(
157+
parent: ContainerReflection,
154158
reflections: DeclarationReflection[],
155159
): ReflectionCategory[] {
156160
const categories = new Map<string, ReflectionCategory>();
@@ -174,6 +178,27 @@ export class CategoryPlugin extends ConverterComponent {
174178
}
175179
}
176180

181+
if (parent.comment) {
182+
removeIf(parent.comment.blockTags, (tag) => {
183+
if (tag.tag === "@categoryDescription") {
184+
const { header, body } = Comment.splitPartsToHeaderAndBody(
185+
tag.content,
186+
);
187+
const cat = categories.get(header);
188+
if (cat) {
189+
cat.description = body;
190+
} else {
191+
this.application.logger.warn(
192+
`Comment for ${parent.getFriendlyFullName()} includes @categoryDescription for "${header}", but no child is placed in that category.`,
193+
);
194+
}
195+
196+
return true;
197+
}
198+
return false;
199+
});
200+
}
201+
177202
for (const cat of categories.values()) {
178203
this.sortFunction(cat.children);
179204
}

src/lib/converter/plugins/GroupPlugin.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ export class GroupPlugin extends ConverterComponent {
101101
) {
102102
this.sortFunction(reflection.children);
103103
}
104-
reflection.groups = this.getReflectionGroups(reflection.children);
104+
reflection.groups = this.getReflectionGroups(
105+
reflection,
106+
reflection.children,
107+
);
105108
}
106109
}
107110

@@ -162,6 +165,7 @@ export class GroupPlugin extends ConverterComponent {
162165
* @returns An array containing all children of the given reflection grouped by their kind.
163166
*/
164167
getReflectionGroups(
168+
parent: ContainerReflection,
165169
reflections: DeclarationReflection[],
166170
): ReflectionGroup[] {
167171
const groups = new Map<string, ReflectionGroup>();
@@ -178,6 +182,27 @@ export class GroupPlugin extends ConverterComponent {
178182
}
179183
});
180184

185+
if (parent.comment) {
186+
removeIf(parent.comment.blockTags, (tag) => {
187+
if (tag.tag === "@groupDescription") {
188+
const { header, body } = Comment.splitPartsToHeaderAndBody(
189+
tag.content,
190+
);
191+
const cat = groups.get(header);
192+
if (cat) {
193+
cat.description = body;
194+
} else {
195+
this.application.logger.warn(
196+
`Comment for ${parent.getFriendlyFullName()} includes @groupDescription for "${header}", but no child is placed in that group.`,
197+
);
198+
}
199+
200+
return true;
201+
}
202+
return false;
203+
});
204+
}
205+
181206
return Array.from(groups.values()).sort(GroupPlugin.sortGroupCallback);
182207
}
183208

src/lib/converter/plugins/LinkResolverPlugin.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { Component, ConverterComponent } from "../components";
22
import type { Context, ExternalResolveResult } from "../../converter";
33
import { ConverterEvents } from "../converter-events";
44
import { Option, ValidationOptions } from "../../utils";
5-
import { DeclarationReflection, ProjectReflection } from "../../models";
5+
import {
6+
ContainerReflection,
7+
DeclarationReflection,
8+
ProjectReflection,
9+
Reflection,
10+
ReflectionCategory,
11+
} from "../../models";
612
import { discoverAllReferenceTypes } from "../../utils/reflections";
713
import { ApplicationEvents } from "../../application-events";
814

@@ -45,6 +51,31 @@ export class LinkResolverPlugin extends ConverterComponent {
4551
reflection,
4652
);
4753
}
54+
55+
if (reflection instanceof ContainerReflection) {
56+
if (reflection.groups) {
57+
for (const group of reflection.groups) {
58+
if (group.description) {
59+
group.description = this.owner.resolveLinks(
60+
group.description,
61+
reflection,
62+
);
63+
}
64+
65+
if (group.categories) {
66+
for (const cat of group.categories) {
67+
this.resolveCategoryLinks(cat, reflection);
68+
}
69+
}
70+
}
71+
}
72+
73+
if (reflection.categories) {
74+
for (const cat of reflection.categories) {
75+
this.resolveCategoryLinks(cat, reflection);
76+
}
77+
}
78+
}
4879
}
4980

5081
if (project.readme) {
@@ -75,4 +106,16 @@ export class LinkResolverPlugin extends ConverterComponent {
75106
}
76107
}
77108
}
109+
110+
private resolveCategoryLinks(
111+
category: ReflectionCategory,
112+
owner: Reflection,
113+
) {
114+
if (category.description) {
115+
category.description = this.owner.resolveLinks(
116+
category.description,
117+
owner,
118+
);
119+
}
120+
}
78121
}

src/lib/models/ReflectionCategory.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { DeclarationReflection } from ".";
1+
import {
2+
Comment,
3+
type CommentDisplayPart,
4+
type DeclarationReflection,
5+
} from ".";
26
import type { Serializer, JSONOutput, Deserializer } from "../serialization";
37

48
/**
@@ -14,6 +18,11 @@ export class ReflectionCategory {
1418
*/
1519
title: string;
1620

21+
/**
22+
* The user specified description, if any, set with `@categoryDescription`
23+
*/
24+
description?: CommentDisplayPart[];
25+
1726
/**
1827
* All reflections of this category.
1928
*/
@@ -35,9 +44,12 @@ export class ReflectionCategory {
3544
return this.children.every((child) => child.hasOwnDocument);
3645
}
3746

38-
toObject(_serializer: Serializer): JSONOutput.ReflectionCategory {
47+
toObject(serializer: Serializer): JSONOutput.ReflectionCategory {
3948
return {
4049
title: this.title,
50+
description: this.description
51+
? Comment.serializeDisplayParts(serializer, this.description)
52+
: undefined,
4153
children:
4254
this.children.length > 0
4355
? this.children.map((child) => child.id)
@@ -46,6 +58,13 @@ export class ReflectionCategory {
4658
}
4759

4860
fromObject(de: Deserializer, obj: JSONOutput.ReflectionCategory) {
61+
if (obj.description) {
62+
this.description = Comment.deserializeDisplayParts(
63+
de,
64+
obj.description,
65+
);
66+
}
67+
4968
if (obj.children) {
5069
de.defer((project) => {
5170
for (const childId of obj.children || []) {

src/lib/models/ReflectionGroup.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { ReflectionCategory } from "./ReflectionCategory";
2-
import type { DeclarationReflection, Reflection } from ".";
2+
import {
3+
Comment,
4+
type CommentDisplayPart,
5+
type DeclarationReflection,
6+
type Reflection,
7+
} from ".";
38
import type { Serializer, JSONOutput, Deserializer } from "../serialization";
49

510
/**
@@ -15,6 +20,11 @@ export class ReflectionGroup {
1520
*/
1621
title: string;
1722

23+
/**
24+
* User specified description via `@groupDescription`, if specified.
25+
*/
26+
description?: CommentDisplayPart[];
27+
1828
/**
1929
* All reflections of this group.
2030
*/
@@ -48,6 +58,9 @@ export class ReflectionGroup {
4858
toObject(serializer: Serializer): JSONOutput.ReflectionGroup {
4959
return {
5060
title: this.title,
61+
description: this.description
62+
? Comment.serializeDisplayParts(serializer, this.description)
63+
: undefined,
5164
children:
5265
this.children.length > 0
5366
? this.children.map((child) => child.id)
@@ -57,6 +70,13 @@ export class ReflectionGroup {
5770
}
5871

5972
fromObject(de: Deserializer, obj: JSONOutput.ReflectionGroup) {
73+
if (obj.description) {
74+
this.description = Comment.deserializeDisplayParts(
75+
de,
76+
obj.description,
77+
);
78+
}
79+
6080
if (obj.categories) {
6181
this.categories = obj.categories.map((catObj) => {
6282
const cat = new ReflectionCategory(catObj.title);

src/lib/models/comments/comment.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export class Comment {
194194
/**
195195
* Helper utility to clone {@link Comment.summary} or {@link CommentTag.content}
196196
*/
197-
static cloneDisplayParts(parts: CommentDisplayPart[]) {
197+
static cloneDisplayParts(parts: readonly CommentDisplayPart[]) {
198198
return parts.map((p) => ({ ...p }));
199199
}
200200

@@ -304,6 +304,61 @@ export class Comment {
304304
return result;
305305
}
306306

307+
/**
308+
* Splits the provided parts into a header (first line, as a string)
309+
* and body (remaining lines). If the header line contains inline tags
310+
* they will be serialized to a string.
311+
*/
312+
static splitPartsToHeaderAndBody(parts: readonly CommentDisplayPart[]): {
313+
header: string;
314+
body: CommentDisplayPart[];
315+
} {
316+
let index = parts.findIndex((part): boolean => {
317+
switch (part.kind) {
318+
case "text":
319+
case "code":
320+
return part.text.includes("\n");
321+
case "inline-tag":
322+
return false;
323+
}
324+
});
325+
326+
if (index === -1) {
327+
return {
328+
header: Comment.combineDisplayParts(parts),
329+
body: [],
330+
};
331+
}
332+
333+
// Do not split a code block, stop the header at the end of the previous block
334+
if (parts[index].kind === "code") {
335+
--index;
336+
}
337+
338+
if (index === -1) {
339+
return { header: "", body: Comment.cloneDisplayParts(parts) };
340+
}
341+
342+
let header = Comment.combineDisplayParts(parts.slice(0, index));
343+
const split = parts[index].text.indexOf("\n");
344+
345+
let body: CommentDisplayPart[];
346+
if (split === -1) {
347+
header += parts[index].text;
348+
body = Comment.cloneDisplayParts(parts.slice(index + 1));
349+
} else {
350+
header += parts[index].text.substring(0, split);
351+
body = Comment.cloneDisplayParts(parts.slice(index));
352+
body[0].text = body[0].text.substring(split + 1);
353+
}
354+
355+
if (!body[0].text) {
356+
body.shift();
357+
}
358+
359+
return { header: header.trim(), body };
360+
}
361+
307362
/**
308363
* The content of the comment which is not associated with a block tag.
309364
*/

0 commit comments

Comments
 (0)