Skip to content

Commit

Permalink
Add support for hooks within templates
Browse files Browse the repository at this point in the history
Resolves #1773.
  • Loading branch information
Gerrit0 committed Nov 7, 2021
1 parent 0853a1a commit 8c707b3
Show file tree
Hide file tree
Showing 13 changed files with 396 additions and 16 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,10 @@
# Unreleased

### Features

- Added hooks which can be used to inject HTML without completely replacing a template, #1773.
See the documentation in [custom-themes.md](https://github.com/TypeStrong/typedoc/blob/master/internal-docs/custom-themes.md) for details.

### Bug Fixes

- Actually fixed `@category` tag incorrectly appearing on function types if used on a type alias, #1745.
Expand Down
38 changes: 31 additions & 7 deletions internal-docs/custom-themes.md
Expand Up @@ -30,14 +30,18 @@ class MyThemeContext extends DefaultThemeRenderContext {

const site = this.options.getValue("gaSite");

const script = `
(function() {
var _owa = document.createElement('script'); _owa.type = 'text/javascript';
_owa.async = true; _owa.src = '${site}' + '/modules/base/js/owa.tracker-combined-min.js';
var _owa_s = document.getElementsByTagName('script')[0]; _owa_s.parentNode.insertBefore(_owa,
_owa_s);
}());
`.trim();

return (
<script>
(function() {"{"}
var _owa = document.createElement('script'); _owa.type = 'text/javascript';
_owa.async = true; _owa.src = '{site}' + '/modules/base/js/owa.tracker-combined-min.js';
var _owa_s = document.getElementsByTagName('script')[0]; _owa_s.parentNode.insertBefore(_owa,
_owa_s);
{"}"}());
<JSX.Raw html={script} />
</script>
);
};
Expand All @@ -59,10 +63,30 @@ export function load(app: Application) {
}
```

## Hooks (v0.22.8+)

When rendering themes, TypeDoc's default theme will call several functions to allow plugins to inject HTML
into a page without completely overwriting a theme. Hooks live on the parent `Renderer` and may be called
by child themes which overwrite a helper with a custom implementation. As an example, the following plugin
will cause a popup on every page when loaded.

```tsx
import { Application, JSX } from "typedoc";
export function load(app: Application) {
app.renderer.hooks.on("head.end", () => (
<script>
<JSX.Raw html="alert('hi!');" />
</script>
));
}
```

For documentation on the available hooks, see the [RendererHooks](https://typedoc.org/api/interfaces/RendererHooks.html)
documentation on the website.

## Future Work

The following is not currently supported by TypeDoc, but is planned on being included in a future version.

- Support for injecting HTML without completely overwriting a template.
- Support for pre-render and post-render async actions for copying files, preparing the output directory, etc.
In the meantime, listen to `RendererEvent.BEGIN` or `RendererEvent.END` and perform processing there.
3 changes: 2 additions & 1 deletion src/index.ts
Expand Up @@ -16,7 +16,7 @@ export {
RendererEvent,
MarkdownEvent,
} from "./lib/output";
export type { RenderTemplate } from "./lib/output";
export type { RenderTemplate, RendererHooks } from "./lib/output";

export {
ArgumentsReader,
Expand All @@ -30,6 +30,7 @@ export {
TSConfigReader,
TypeDocReader,
EntryPointStrategy,
EventHooks,
} from "./lib/utils";

export type {
Expand Down
1 change: 1 addition & 0 deletions src/lib/output/index.ts
Expand Up @@ -2,6 +2,7 @@ export { PageEvent, RendererEvent, MarkdownEvent } from "./events";
export { UrlMapping } from "./models/UrlMapping";
export type { RenderTemplate } from "./models/UrlMapping";
export { Renderer } from "./renderer";
export type { RendererHooks } from "./renderer";
export { Theme } from "./theme";
export { DefaultTheme } from "./themes/default/DefaultTheme";
export { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderContext";
46 changes: 45 additions & 1 deletion src/lib/output/renderer.ts
Expand Up @@ -19,10 +19,38 @@ import { remove, writeFileSync } from "../utils/fs";
import { DefaultTheme } from "./themes/default/DefaultTheme";
import { RendererComponent } from "./components";
import { Component, ChildableComponent } from "../utils/component";
import { BindOption } from "../utils";
import { BindOption, EventHooks } from "../utils";
import { loadHighlighter } from "../utils/highlighter";
import type { Theme as ShikiTheme } from "shiki";
import { Reflection } from "../models";
import type { JsxElement } from "../utils/jsx.elements";
import type { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderContext";

/**
* Describes the hooks available to inject output in the default theme.
* If the available hooks don't let you put something where you'd like, please open an issue!
*/
export interface RendererHooks {
/**
* Applied immediately after the opening `<head>` tag.
*/
"head.begin": [DefaultThemeRenderContext];

/**
* Applied immediately before the closing `</head>` tag.
*/
"head.end": [DefaultThemeRenderContext];

/**
* Applied immediately after the opening `<body>` tag.
*/
"body.begin": [DefaultThemeRenderContext];

/**
* Applied immediately before the closing `</body>` tag.
*/
"body.end": [DefaultThemeRenderContext];
}

/**
* The renderer processes a {@link ProjectReflection} using a {@link Theme} instance and writes
Expand Down Expand Up @@ -80,6 +108,16 @@ export class Renderer extends ChildableComponent<
*/
theme?: Theme;

/**
* Hooks which will be called when rendering pages.
* Note:
* - Hooks added during output will be discarded at the end of rendering.
* - Hooks added during a page render will be discarded at the end of that page's render.
*
* See {@link RendererHooks} for a description of each available hook, and when it will be called.
*/
hooks = new EventHooks<RendererHooks, JsxElement>();

/** @internal */
@BindOption("theme")
themeName!: string;
Expand Down Expand Up @@ -195,6 +233,7 @@ export class Renderer extends ChildableComponent<
project: ProjectReflection,
outputDirectory: string
): Promise<void> {
const momento = this.hooks.saveMomento();
const start = Date.now();
await loadHighlighter(this.lightTheme, this.darkTheme);
this.application.logger.verbose(
Expand Down Expand Up @@ -225,6 +264,7 @@ export class Renderer extends ChildableComponent<
}

this.theme = void 0;
this.hooks.restoreMomento(momento);
}

/**
Expand All @@ -234,8 +274,10 @@ export class Renderer extends ChildableComponent<
* @return TRUE if the page has been saved to disc, otherwise FALSE.
*/
private renderDocument(page: PageEvent): boolean {
const momento = this.hooks.saveMomento();
this.trigger(PageEvent.BEGIN, page);
if (page.isDefaultPrevented) {
this.hooks.restoreMomento(momento);
return false;
}

Expand All @@ -246,6 +288,8 @@ export class Renderer extends ChildableComponent<
}

this.trigger(PageEvent.END, page);
this.hooks.restoreMomento(momento);

if (page.isDefaultPrevented) {
return false;
}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/output/themes/default/DefaultThemeRenderContext.ts
@@ -1,4 +1,5 @@
import type * as ts from "typescript";
import type { RendererHooks } from "../..";
import type { Reflection } from "../../../models";
import type { Options } from "../../../utils";
import type { DefaultTheme } from "./DefaultTheme";
Expand Down Expand Up @@ -39,6 +40,9 @@ export class DefaultThemeRenderContext {
this.options = options;
}

hook = (name: keyof RendererHooks) =>
this.theme.owner.hooks.emit(name, this);

/** Avoid this in favor of urlTo if possible */
relativeURL = (url: string | undefined) => {
return url ? this.theme.markedPlugin.getRelativeUrl(url) : url;
Expand Down
14 changes: 7 additions & 7 deletions src/lib/output/themes/default/layouts/default.tsx
Expand Up @@ -6,16 +6,13 @@ import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext";
export const defaultLayout = (context: DefaultThemeRenderContext, props: PageEvent<Reflection>) => (
<html class="default">
<head>
{context.hook("head.begin")}

This comment has been minimized.

Copy link
@RunDevelopment

This comment has been minimized.

Copy link
@Gerrit0

Gerrit0 Nov 7, 2021

Author Collaborator

Based on that page you linked, looks like we should just get rid of the meta tag entirely, since utf-8 is the only valid valid.

This comment has been minimized.

Copy link
@RunDevelopment

RunDevelopment Nov 7, 2021

Contributor

Here's what W3 says about this:

You should always specify the encoding used for an HTML or XML page. If you don't, you risk that characters in your content are incorrectly interpreted.

So it depends. The browser has to somehow know the encoding of the generated HTML file. If the server sets UTF-8 in the response headers, then <meta charSet="utf-8" /> will actually get ignored.

the HTTP header has a higher precedence than the in-document meta declarations

So if whatever you to host docs generated by TypeDoc doesn't provide this optional header, the browser will have to guess.

This comment has been minimized.

Copy link
@Gerrit0

Gerrit0 Nov 7, 2021

Author Collaborator

Huh, things I never knew! PR welcome :)

<meta charSet="utf-8" />
<meta http-equiv="x-ua-compatible" content="IE=edge" />
<title>
{props.model.name === props.project.name ? (
props.project.name
) : (
<>
{props.model.name} | {props.project.name}
</>
)}
{props.model.name === props.project.name
? props.project.name
: `${props.model.name} | ${props.project.name}`}
</title>
<meta name="description" content={"Documentation for " + props.project.name} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
Expand All @@ -26,8 +23,10 @@ export const defaultLayout = (context: DefaultThemeRenderContext, props: PageEve
<link rel="stylesheet" href={context.relativeURL("assets/custom.css")} />
)}
<script async src={context.relativeURL("assets/search.js")} id="search-script"></script>
{context.hook("head.end")}
</head>
<body>
{context.hook("body.begin")}
<script>
<Raw html='document.body.classList.add(localStorage.getItem("tsd-theme") || "os")' />
</script>
Expand All @@ -46,6 +45,7 @@ export const defaultLayout = (context: DefaultThemeRenderContext, props: PageEve
<script src={context.relativeURL("assets/main.js")}></script>

{context.analytics()}
{context.hook("body.end")}
</body>
</html>
);
17 changes: 17 additions & 0 deletions src/lib/utils/array.ts
Expand Up @@ -3,6 +3,7 @@
* the item will be inserted later will be placed earlier in the array.
* @param arr modified by inserting item.
* @param item
* @deprecated this is confusing, it sorts with lower priority being placed earlier. Prefer insertOrderSorted, which is nearly the same.
*/
export function insertPrioritySorted<T extends { priority: number }>(
arr: T[],
Expand All @@ -13,6 +14,22 @@ export function insertPrioritySorted<T extends { priority: number }>(
return arr;
}

/**
* Inserts an item into an array sorted by order. If two items have the same order,
* the item inserted later will be placed later in the array.
* The array will be sorted with lower order being placed sooner.
* @param arr modified by inserting item.
* @param item
*/
export function insertOrderSorted<T extends { order: number }>(
arr: T[],
item: T
): T[] {
const index = binaryFindPartition(arr, (v) => v.order > item.order);
arr.splice(index === -1 ? arr.length : index, 0, item);
return arr;
}

/**
* Performs a binary search of a given array, returning the index of the first item
* for which `partition` returns true. Returns the -1 if there are no items in `arr`
Expand Down

0 comments on commit 8c707b3

Please sign in to comment.