Skip to content

Commit f172a24

Browse files
committed
Make search functionality more customizable
Resolves #1553 Resolves #1953
1 parent f66dbd5 commit f172a24

File tree

9 files changed

+175
-37
lines changed

9 files changed

+175
-37
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
### Bug Fixes
44

55
- TypeDoc no longer ignores project references if `--entryPointStrategy Packages` is set, #1976.
6+
- Boost computations are now done when creating the search index, resulting in a smaller `search.js` generated file.
67

78
### Features
89

910
- The `--exclude` option will now be respected by `--entryPointStrategy Packages` and can be used to exclude package directories, #1959.
11+
- TypeDoc now emits an `IndexEvent` on the `Renderer` when preparing the search index, #1953.
12+
- Added new `--searchInComments` option to include comment text in the search index, #1553.
13+
Turning this option on will increase the size of your search index, potentially by an order of magnitude.
1014

1115
## v0.23.3 (2022-07-01)
1216

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export {
1515
PageEvent,
1616
RendererEvent,
1717
MarkdownEvent,
18+
IndexEvent,
1819
} from "./lib/output";
1920
export type { RenderTemplate, RendererHooks } from "./lib/output";
2021

src/lib/output/events.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as Path from "path";
33
import { Event } from "../utils/events";
44
import type { ProjectReflection } from "../models/reflections/project";
55
import type { RenderTemplate, UrlMapping } from "./models/UrlMapping";
6+
import type { DeclarationReflection } from "../models";
67

78
/**
89
* An event emitted by the {@link Renderer} class at the very beginning and
@@ -152,3 +153,64 @@ export class MarkdownEvent extends Event {
152153
this.parsedText = parsedText;
153154
}
154155
}
156+
157+
/**
158+
* An event emitted when the search index is being prepared.
159+
*/
160+
export class IndexEvent extends Event {
161+
/**
162+
* Triggered on the renderer when the search index is being prepared.
163+
* @event
164+
*/
165+
static readonly PREPARE_INDEX = "prepareIndex";
166+
167+
/**
168+
* May be filtered by plugins to reduce the results available.
169+
* Additional items *should not* be added to this array.
170+
*
171+
* If you remove an index from this array, you must also remove the
172+
* same index from {@link searchFields}. The {@link removeResult} helper
173+
* will do this for you.
174+
*/
175+
searchResults: DeclarationReflection[];
176+
177+
/**
178+
* Additional search fields to be used when creating the search index.
179+
* `name` and `comment` may be specified to overwrite TypeDoc's search fields.
180+
*
181+
* Do not use `id` as a custom search field.
182+
*/
183+
searchFields: Record<string, string>[];
184+
185+
/**
186+
* Weights for the fields defined in `searchFields`. The default will weight
187+
* `name` as 10x more important than comment content.
188+
*
189+
* If a field added to {@link searchFields} is not added to this object, it
190+
* will **not** be searchable.
191+
*
192+
* Do not replace this object, instead, set new properties on it for custom search
193+
* fields added by your plugin.
194+
*/
195+
readonly searchFieldWeights: Record<string, number> = {
196+
name: 10,
197+
comment: 1,
198+
};
199+
200+
/**
201+
* Remove a search result by index.
202+
*/
203+
removeResult(index: number) {
204+
this.searchResults.splice(index, 1);
205+
this.searchFields.splice(index, 1);
206+
}
207+
208+
constructor(name: string, searchResults: DeclarationReflection[]) {
209+
super(name);
210+
this.searchResults = searchResults;
211+
this.searchFields = Array.from(
212+
{ length: this.searchResults.length },
213+
() => ({})
214+
);
215+
}
216+
}

src/lib/output/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { PageEvent, RendererEvent, MarkdownEvent } from "./events";
1+
export { PageEvent, RendererEvent, MarkdownEvent, IndexEvent } from "./events";
22
export { UrlMapping } from "./models/UrlMapping";
33
export type { RenderTemplate } from "./models/UrlMapping";
44
export { Renderer } from "./renderer";

src/lib/output/plugins/JavascriptIndexPlugin.ts

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,38 @@ import * as Path from "path";
22
import { Builder, trimmer } from "lunr";
33

44
import {
5+
Comment,
56
DeclarationReflection,
67
ProjectReflection,
78
ReflectionKind,
89
} from "../../models";
910
import { GroupPlugin } from "../../converter/plugins";
1011
import { Component, RendererComponent } from "../components";
11-
import { RendererEvent } from "../events";
12-
import { writeFileSync } from "../../utils";
12+
import { IndexEvent, RendererEvent } from "../events";
13+
import { BindOption, writeFileSync } from "../../utils";
1314
import { DefaultTheme } from "../themes/default/DefaultTheme";
1415

16+
/**
17+
* Keep this in sync with the interface in src/lib/output/themes/default/assets/typedoc/components/Search.ts
18+
*/
19+
interface SearchDocument {
20+
kind: number;
21+
name: string;
22+
url: string;
23+
classes?: string;
24+
parent?: string;
25+
}
26+
1527
/**
1628
* A plugin that exports an index of the project to a javascript file.
1729
*
1830
* The resulting javascript file can be used to build a simple search function.
1931
*/
2032
@Component({ name: "javascript-index" })
2133
export class JavascriptIndexPlugin extends RendererComponent {
34+
@BindOption("searchInComments")
35+
searchComments!: boolean;
36+
2237
/**
2338
* Create a new JavascriptIndexPlugin instance.
2439
*/
@@ -39,30 +54,52 @@ export class JavascriptIndexPlugin extends RendererComponent {
3954
return;
4055
}
4156

42-
const rows: any[] = [];
57+
const rows: SearchDocument[] = [];
4358
const kinds: { [K in ReflectionKind]?: string } = {};
4459

45-
for (const reflection of event.project.getReflectionsByKind(
46-
ReflectionKind.All
60+
const initialSearchResults = Object.values(
61+
event.project.reflections
62+
).filter((refl) => {
63+
return (
64+
refl instanceof DeclarationReflection &&
65+
refl.url &&
66+
refl.name &&
67+
!refl.flags.isExternal
68+
);
69+
}) as DeclarationReflection[];
70+
71+
const indexEvent = new IndexEvent(
72+
IndexEvent.PREPARE_INDEX,
73+
initialSearchResults
74+
);
75+
76+
this.owner.trigger(indexEvent);
77+
78+
if (indexEvent.isDefaultPrevented) {
79+
return;
80+
}
81+
82+
const builder = new Builder();
83+
builder.pipeline.add(trimmer);
84+
85+
builder.ref("id");
86+
for (const [key, boost] of Object.entries(
87+
indexEvent.searchFieldWeights
4788
)) {
48-
if (!(reflection instanceof DeclarationReflection)) {
49-
continue;
50-
}
89+
builder.field(key, { boost });
90+
}
5191

52-
if (
53-
!reflection.url ||
54-
!reflection.name ||
55-
reflection.flags.isExternal
56-
) {
92+
for (const reflection of indexEvent.searchResults) {
93+
if (!reflection.url) {
5794
continue;
5895
}
5996

60-
let parent = reflection.parent;
6197
const boost = reflection.relevanceBoost ?? 1;
6298
if (boost <= 0) {
6399
continue;
64100
}
65101

102+
let parent = reflection.parent;
66103
if (parent instanceof ProjectReflection) {
67104
parent = undefined;
68105
}
@@ -73,34 +110,29 @@ export class JavascriptIndexPlugin extends RendererComponent {
73110
);
74111
}
75112

76-
const row: any = {
77-
id: rows.length,
113+
const row: SearchDocument = {
78114
kind: reflection.kind,
79115
name: reflection.name,
80116
url: reflection.url,
81117
classes: reflection.cssClasses,
82118
};
83119

84-
if (boost !== 1) {
85-
row.boost = boost;
86-
}
87-
88120
if (parent) {
89121
row.parent = parent.getFullName();
90122
}
91123

124+
builder.add(
125+
{
126+
name: reflection.name,
127+
comment: this.getCommentSearchText(reflection),
128+
...indexEvent.searchFields[rows.length],
129+
id: rows.length,
130+
},
131+
{ boost }
132+
);
92133
rows.push(row);
93134
}
94135

95-
const builder = new Builder();
96-
builder.pipeline.add(trimmer);
97-
98-
builder.ref("id");
99-
builder.field("name", { boost: 10 });
100-
builder.field("parent");
101-
102-
rows.forEach((row) => builder.add(row));
103-
104136
const index = builder.build();
105137

106138
const jsonFileName = Path.join(
@@ -120,4 +152,29 @@ export class JavascriptIndexPlugin extends RendererComponent {
120152
`window.searchData = JSON.parse(${JSON.stringify(jsonData)});`
121153
);
122154
}
155+
156+
private getCommentSearchText(reflection: DeclarationReflection) {
157+
if (!this.searchComments) return;
158+
159+
const comments: Comment[] = [];
160+
if (reflection.comment) comments.push(reflection.comment);
161+
reflection.signatures?.forEach(
162+
(s) => s.comment && comments.push(s.comment)
163+
);
164+
reflection.getSignature?.comment &&
165+
comments.push(reflection.getSignature.comment);
166+
reflection.setSignature?.comment &&
167+
comments.push(reflection.setSignature.comment);
168+
169+
if (!comments.length) {
170+
return;
171+
}
172+
173+
return comments
174+
.flatMap((c) => {
175+
return [...c.summary, ...c.blockTags.flatMap((t) => t.content)];
176+
})
177+
.map((part) => part.text)
178+
.join("\n");
179+
}
123180
}

src/lib/output/renderer.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as path from "path";
1111

1212
import type { Application } from "../application";
1313
import type { Theme } from "./theme";
14-
import { RendererEvent, PageEvent } from "./events";
14+
import { RendererEvent, PageEvent, IndexEvent } from "./events";
1515
import type { ProjectReflection } from "../models/reflections/project";
1616
import type { UrlMapping } from "./models/UrlMapping";
1717
import { writeFileSync } from "../utils/fs";
@@ -99,6 +99,10 @@ export interface RendererHooks {
9999
* * {@link Renderer.EVENT_END}<br>
100100
* Triggered after the renderer has written all documents. The listener receives
101101
* an instance of {@link RendererEvent}.
102+
*
103+
* * {@link Renderer.EVENT_PREPARE_INDEX}<br>
104+
* Triggered when the JavascriptIndexPlugin is preparing the search index. Listeners receive
105+
* an instance of {@link IndexEvent}.
102106
*/
103107
@Component({ name: "renderer", internal: true, childClass: RendererComponent })
104108
export class Renderer extends ChildableComponent<
@@ -123,6 +127,9 @@ export class Renderer extends ChildableComponent<
123127
/** @event */
124128
static readonly EVENT_END = RendererEvent.END;
125129

130+
/** @event */
131+
static readonly EVENT_PREPARE_INDEX = IndexEvent.PREPARE_INDEX;
132+
126133
/**
127134
* The theme that is used to render the documentation.
128135
*/

src/lib/output/themes/default/assets/typedoc/components/Search.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import { debounce } from "../utils/debounce";
22
import { Index } from "lunr";
33

4-
interface IDocument {
4+
/**
5+
* Keep this in sync with the interface in src/lib/output/plugins/JavascriptIndexPlugin.ts
6+
* It's not imported because these are separate TS projects today.
7+
*/
8+
interface SearchDocument {
59
id: number;
10+
611
kind: number;
712
name: string;
813
url: string;
914
classes?: string;
1015
parent?: string;
11-
boost?: number;
1216
}
1317

1418
interface IData {
1519
kinds: { [kind: number]: string };
16-
rows: IDocument[];
20+
rows: SearchDocument[];
1721
index: object;
1822
}
1923

@@ -168,9 +172,6 @@ function updateResults(
168172
1 + 1 / (Math.abs(row.name.length - searchText.length) * 10);
169173
}
170174

171-
// boost by relevanceBoost
172-
boost *= row.boost ?? 1;
173-
174175
item.score *= boost;
175176
}
176177

src/lib/utils/options/declaration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export interface TypeDocOptionMap {
126126
githubPages: boolean;
127127
htmlLang: string;
128128
hideGenerator: boolean;
129+
searchInComments: boolean;
129130
cleanOutputDir: boolean;
130131

131132
commentStyle: typeof CommentStyle;

src/lib/utils/options/sources/typedoc.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
270270
help: "Do not print the TypeDoc link at the end of the page.",
271271
type: ParameterType.Boolean,
272272
});
273+
options.addDeclaration({
274+
name: "searchInComments",
275+
help: "If set, the search index will also include comments. This will greatly increase the size of the search index.",
276+
type: ParameterType.Boolean,
277+
});
273278
options.addDeclaration({
274279
name: "cleanOutputDir",
275280
help: "If set, TypeDoc will remove the output directory before writing output.",

0 commit comments

Comments
 (0)