Skip to content

Commit d9c3abf

Browse files
committed
Add table of contents rendering
Resolves #1478
1 parent 0b9ed4f commit d9c3abf

16 files changed

+298
-188
lines changed

.config/typedoc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,6 @@
3636
"Enumerations": 2.0,
3737
"Type Aliases": 2.0
3838
},
39-
"searchInComments": true,
39+
"includeVersion": true,
4040
"logLevel": "Verbose"
4141
}

CHANGELOG.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,26 @@
3434
- Removed `Renderer.addExternalSymbolResolver`, use `Converter.addExternalSymbolResolver` instead.
3535
- Removed `CallbackLogger`.
3636
- Removed `SerializeEventData` from serialization events.
37-
- A `PageEvent` is no longer passed to `getRenderContext` by the default theme.
37+
- A `PageEvent` is now required for `getRenderContext`. If caching the context object, `page` must be updated when `getRenderContext` is called.
38+
- `PageEvent` no longer includes the `template` property. The `Theme.render` method is now expected to take the template to render the page with as its second argument.
3839
- Removed `secondaryNavigation` member on `DefaultThemeRenderContext`.
3940
- Renamed `navigation` to `sidebar` on `DefaultThemeRenderContext` and `navigation.begin`/`navigation.end` hooks to `sidebar.begin`/`sidebar.end`.
4041

4142
### Features
4243

44+
- Added `--useTsLinkResolution` option (on by default) which tells TypeDoc to use TypeScript's `@link` resolution.
45+
- Reworked default theme navigation to add support for a page table of contents, #1478, #2189.
4346
- Added support for `@interface` on type aliases to tell TypeDoc to convert the fully resolved type as an interface, #1519
47+
- Added support for `@namespace` on variable declarations to tell TypeDoc to convert the variable as a namespace, #2055.
4448
- Added support for `@prop`/`@property` to specify documentation for a child property of a symbol, intended for use with `@interface`.
49+
- TypeDoc will now produce more informative error messages for options which cannot be set from the cli, #2022.
50+
- TypeDoc will now attempt to guess what option you may have meant if given an invalid option name.
4551
- Plugins may now return a `Promise<void>` from their `load` function, #185.
4652
- TypeDoc now supports plugins written with ESM, #1635.
4753
- Added `Renderer.preRenderAsyncJobs` and `Renderer.postRenderAsyncJobs`, which may be used by plugins to perform async processing for rendering, #185.
4854
Note: Conversion is still intentionally a synchronous process to ensure stability of converted projects between runs.
49-
- TypeDoc will now produce more informative error messages for options which cannot be set from the cli, #2022.
50-
- TypeDoc will now attempt to guess what option you may have meant if given an invalid option name.
5155
- TypeDoc options may now be set under the `typedocOptions` key in `package.json`, #2112.
52-
- Moved sidebar to left of content for consistency with most other websites, #2189.
5356
- Added `--cacheBust` option to tell TypeDoc to include include the generation time in files, #2124.
54-
- Added support for `@namespace` on variable declarations to tell TypeDoc to convert the variable as a namespace, #2055.
5557
- Added `--excludeReferences` option to tell TypeDoc to omit re-exports of a symbol already included from the documentation.
5658
- Introduced new render hooks `pageSidebar.begin` and `pageSidebar.end`.
5759

src/lib/output/components.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import * as Path from "path";
22

33
import { Component, AbstractComponent } from "../utils/component";
4-
import {
4+
import type {
55
ProjectReflection,
6-
DeclarationReflection,
6+
Reflection,
77
} from "../models/reflections/index";
88
import type { Renderer } from "./renderer";
99
import { RendererEvent, PageEvent } from "./events";
@@ -24,7 +24,7 @@ export abstract class ContextAwareRendererComponent extends RendererComponent {
2424
/**
2525
* The reflection that is currently processed.
2626
*/
27-
protected reflection?: DeclarationReflection;
27+
protected page?: PageEvent<Reflection>;
2828

2929
/**
3030
* The url of the document that is being currently generated.
@@ -84,11 +84,8 @@ export abstract class ContextAwareRendererComponent extends RendererComponent {
8484
*
8585
* @param page An event object describing the current render operation.
8686
*/
87-
protected onBeginPage(page: PageEvent) {
87+
protected onBeginPage(page: PageEvent<Reflection>) {
8888
this.location = page.url;
89-
this.reflection =
90-
page.model instanceof DeclarationReflection
91-
? page.model
92-
: undefined;
89+
this.page = page;
9390
}
9491
}

src/lib/output/events.ts

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +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";
6+
import type { DeclarationReflection, ReflectionKind } from "../models";
77

88
/**
99
* An event emitted by the {@link Renderer} class at the very beginning and
@@ -61,14 +61,12 @@ export class RendererEvent extends Event {
6161
*/
6262
public createPageEvent<Model>(
6363
mapping: UrlMapping<Model>
64-
): PageEvent<Model> {
65-
const event = new PageEvent<Model>(PageEvent.BEGIN);
64+
): [RenderTemplate<PageEvent<Model>>, PageEvent<Model>] {
65+
const event = new PageEvent<Model>(PageEvent.BEGIN, mapping.model);
6666
event.project = this.project;
6767
event.url = mapping.url;
68-
event.model = mapping.model;
69-
event.template = mapping.template;
7068
event.filename = Path.join(this.outputDirectory, mapping.url);
71-
return event;
69+
return [mapping.template, event];
7270
}
7371
}
7472

@@ -79,7 +77,7 @@ export class RendererEvent extends Event {
7977
* @see {@link Renderer.EVENT_BEGIN_PAGE}
8078
* @see {@link Renderer.EVENT_END_PAGE}
8179
*/
82-
export class PageEvent<Model = unknown> extends Event {
80+
export class PageEvent<out Model = unknown> extends Event {
8381
/**
8482
* The project the renderer is currently processing.
8583
*/
@@ -98,12 +96,7 @@ export class PageEvent<Model = unknown> extends Event {
9896
/**
9997
* The model that should be rendered on this page.
10098
*/
101-
model!: Model;
102-
103-
/**
104-
* The template that should be used to render this page.
105-
*/
106-
template!: RenderTemplate<this>;
99+
readonly model: Model;
107100

108101
/**
109102
* The final html content of this page.
@@ -112,6 +105,18 @@ export class PageEvent<Model = unknown> extends Event {
112105
*/
113106
contents?: string;
114107

108+
/**
109+
* Links to content within this page that should be rendered in the page navigation.
110+
* This is built when rendering the document content.
111+
*/
112+
pageHeadings: Array<{
113+
link: string;
114+
text: string;
115+
level?: number;
116+
kind?: ReflectionKind;
117+
classes?: string;
118+
}> = [];
119+
115120
/**
116121
* Triggered before a document will be rendered.
117122
* @event
@@ -123,12 +128,17 @@ export class PageEvent<Model = unknown> extends Event {
123128
* @event
124129
*/
125130
static readonly END = "endPage";
131+
132+
constructor(name: string, model: Model) {
133+
super(name);
134+
this.model = model;
135+
}
126136
}
127137

128138
/**
129139
* An event emitted when markdown is being parsed. Allows other plugins to manipulate the result.
130140
*
131-
* @see {@link PARSE}
141+
* @see {@link MarkdownEvent.PARSE}
132142
*/
133143
export class MarkdownEvent extends Event {
134144
/**
@@ -141,14 +151,25 @@ export class MarkdownEvent extends Event {
141151
*/
142152
parsedText: string;
143153

154+
/**
155+
* The page that this markdown is being parsed for.
156+
*/
157+
readonly page: PageEvent;
158+
144159
/**
145160
* Triggered on the renderer when this plugin parses a markdown string.
146161
* @event
147162
*/
148163
static readonly PARSE = "parseMarkdown";
149164

150-
constructor(name: string, originalText: string, parsedText: string) {
165+
constructor(
166+
name: string,
167+
page: PageEvent,
168+
originalText: string,
169+
parsedText: string
170+
) {
151171
super(name);
172+
this.page = page;
152173
this.originalText = originalText;
153174
this.parsedText = parsedText;
154175
}

src/lib/output/plugins/JavascriptIndexPlugin.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,7 @@ export class JavascriptIndexPlugin extends RendererComponent {
105105
kind: reflection.kind,
106106
name: reflection.name,
107107
url: reflection.url,
108-
classes: this.owner.theme
109-
.getRenderContext()
110-
.getReflectionClasses(reflection),
108+
classes: this.owner.theme.getReflectionClasses(reflection),
111109
};
112110

113111
if (parent) {

src/lib/output/renderer.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { Application } from "../application";
1313
import type { Theme } from "./theme";
1414
import { RendererEvent, PageEvent, IndexEvent } from "./events";
1515
import type { ProjectReflection } from "../models/reflections/project";
16-
import type { UrlMapping } from "./models/UrlMapping";
16+
import type { RenderTemplate, UrlMapping } from "./models/UrlMapping";
1717
import { writeFileSync } from "../utils/fs";
1818
import { DefaultTheme } from "./themes/default/DefaultTheme";
1919
import { RendererComponent } from "./components";
@@ -260,7 +260,7 @@ export class Renderer extends ChildableComponent<
260260
);
261261
output.urls.forEach((mapping: UrlMapping) => {
262262
clearSeenIconCache();
263-
this.renderDocument(output.createPageEvent(mapping));
263+
this.renderDocument(...output.createPageEvent(mapping));
264264
validateStateIsClean(mapping.url);
265265
});
266266

@@ -282,7 +282,10 @@ export class Renderer extends ChildableComponent<
282282
* @param page An event describing the current page.
283283
* @return TRUE if the page has been saved to disc, otherwise FALSE.
284284
*/
285-
private renderDocument(page: PageEvent) {
285+
private renderDocument(
286+
template: RenderTemplate<PageEvent<Reflection>>,
287+
page: PageEvent<Reflection>
288+
) {
286289
const momento = this.hooks.saveMomento();
287290
this.trigger(PageEvent.BEGIN, page);
288291
if (page.isDefaultPrevented) {
@@ -291,7 +294,7 @@ export class Renderer extends ChildableComponent<
291294
}
292295

293296
if (page.model instanceof Reflection) {
294-
page.contents = this.theme!.render(page as PageEvent<Reflection>);
297+
page.contents = this.theme!.render(page, template);
295298
} else {
296299
throw new Error("Should be unreachable");
297300
}

src/lib/output/theme.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Renderer } from "./renderer";
22
import type { ProjectReflection } from "../models/reflections/project";
3-
import type { UrlMapping } from "./models/UrlMapping";
3+
import type { RenderTemplate, UrlMapping } from "./models/UrlMapping";
44
import { RendererComponent } from "./components";
55
import { Component } from "../utils/component";
66
import type { PageEvent } from "./events";
@@ -46,5 +46,8 @@ export abstract class Theme extends RendererComponent {
4646
/**
4747
* Renders the provided page to a string, which will be written to disk by the {@link Renderer}
4848
*/
49-
abstract render(page: PageEvent<Reflection>): string;
49+
abstract render(
50+
page: PageEvent<Reflection>,
51+
template: RenderTemplate<PageEvent<Reflection>>
52+
): string;
5053
}

src/lib/output/themes/MarkedPlugin.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,11 @@ import * as Path from "path";
33
import * as Marked from "marked";
44

55
import { Component, ContextAwareRendererComponent } from "../components";
6-
import { RendererEvent, MarkdownEvent } from "../events";
6+
import { RendererEvent, MarkdownEvent, PageEvent } from "../events";
77
import { BindOption, readFile, copySync, isFile } from "../../utils";
88
import { highlight, isSupportedLanguage } from "../../utils/highlighter";
99
import type { Theme } from "shiki";
1010

11-
const customMarkedRenderer = new Marked.Renderer();
12-
13-
customMarkedRenderer.heading = (text, level, _, slugger) => {
14-
const slug = slugger.slug(text);
15-
16-
return `
17-
<a href="#${slug}" id="${slug}" style="color: inherit; text-decoration: none;">
18-
<h${level}>${text}</h${level}>
19-
</a>
20-
`;
21-
};
22-
2311
/**
2412
* Implements markdown and relativeURL helpers for templates.
2513
* @internal
@@ -100,7 +88,7 @@ output file :
10088
* @param text The markdown string that should be parsed.
10189
* @returns The resulting html string.
10290
*/
103-
public parseMarkdown(text: string) {
91+
public parseMarkdown(text: string, page: PageEvent<any>) {
10492
if (this.includes) {
10593
text = text.replace(this.includePattern, (_match, path) => {
10694
path = Path.join(this.includes!, path.trim());
@@ -134,7 +122,7 @@ output file :
134122
);
135123
}
136124

137-
const event = new MarkdownEvent(MarkdownEvent.PARSE, text, text);
125+
const event = new MarkdownEvent(MarkdownEvent.PARSE, page, text, text);
138126

139127
this.owner.trigger(event);
140128
return event.parsedText;
@@ -195,7 +183,22 @@ output file :
195183
// Set some default values if they are not specified via the TypeDoc option
196184
markedOptions.highlight ??= (text, lang) =>
197185
this.getHighlighted(text, lang);
198-
markedOptions.renderer ??= customMarkedRenderer;
186+
187+
if (!markedOptions.renderer) {
188+
markedOptions.renderer = new Marked.Renderer();
189+
190+
markedOptions.renderer.heading = (text, level, _, slugger) => {
191+
const slug = slugger.slug(text);
192+
// Prefix the slug with an extra `$` to prevent conflicts with TypeDoc's anchors.
193+
this.page!.pageHeadings.push({
194+
link: `#$${slug}`,
195+
text,
196+
level,
197+
});
198+
return `<a id="$${slug}" class="tsd-anchor"></a><h${level}><a href="#$${slug}" style="color:inherit;text-decoration:none">${text}</a></h${level}>`;
199+
};
200+
}
201+
199202
markedOptions.mangle ??= false; // See https://github.com/TypeStrong/typedoc/issues/1395
200203

201204
return markedOptions;

0 commit comments

Comments
 (0)