Skip to content

Commit

Permalink
Dynamically load navigation
Browse files Browse the repository at this point in the history
Resolves #2287
  • Loading branch information
Gerrit0 committed Sep 4, 2023
1 parent bba61bc commit 67ee6ac
Show file tree
Hide file tree
Showing 17 changed files with 427 additions and 192 deletions.
1 change: 0 additions & 1 deletion .config/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"Type Aliases": 2.0
},
"navigation": {
"fullTree": true,
"includeCategories": true,
"includeGroups": false
},
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
# Unreleased

### Features

- Navigation is now written to a JS file and built dynamically, which significantly decreases document generation time
with large projects and also provides large space benefits. Themes may now override `DefaultTheme.buildNavigation`
to customize the displayed navigation tree, #2287.
Note: This change renders `navigation.fullTree` obsolete. If you set it, TypeDoc will warn that it is being ignored.
It will be removed in v0.26.
- TypeDoc will now attempt to cache icons when `DefaultThemeRenderContext.icons` is overwritten by a custom theme.
Note: To perform this optimization, TypeDoc relies on `DefaultThemeRenderContext.iconCache` being rendered within
each page. TypeDoc does it in the `defaultLayout` template.

### Bug Fixes

- `@property` now works as expected if used to override a method's documentation.
- Deprecated functions/methods are now correctly rendered with a struck-out name.
- `--watch` mode works again, #2378.
- Improved support for optional names within JSDoc types, #2384.
- Fixed duplicate rendering of reflection flags on signature parameters, #2385.
- TypeDoc now handles the `intrinsic` keyword if TS intrinsic types are included in documentation.

### Thanks!

- @HemalPatil
- @typhonrt

# v0.25.0 (2023-08-25)

Expand Down
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export {
MarkdownEvent,
IndexEvent,
} from "./lib/output";
export type { RenderTemplate, RendererHooks } from "./lib/output";
export type {
RenderTemplate,
RendererHooks,
NavigationElement,
} from "./lib/output";

export {
ArgumentsReader,
Expand Down
14 changes: 14 additions & 0 deletions src/lib/converter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function loadConverters() {
indexedAccessConverter,
inferredConverter,
intersectionConverter,
intrinsicConverter,
jsDocVariadicTypeConverter,
keywordConverter,
optionalConverter,
Expand Down Expand Up @@ -460,6 +461,19 @@ const intersectionConverter: TypeConverter<
},
};

const intrinsicConverter: TypeConverter<
ts.KeywordTypeNode<ts.SyntaxKind.IntrinsicKeyword>,
ts.Type
> = {
kind: [ts.SyntaxKind.IntrinsicKeyword],
convert() {
return new IntrinsicType("intrinsic");
},
convertType() {
return new IntrinsicType("intrinsic");
},
};

const jsDocVariadicTypeConverter: TypeConverter<ts.JSDocVariadicType> = {
kind: [ts.SyntaxKind.JSDocVariadicType],
convert(context, node) {
Expand Down
5 changes: 4 additions & 1 deletion src/lib/output/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ 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 {
DefaultTheme,
type NavigationElement,
} from "./themes/default/DefaultTheme";
export { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderContext";
38 changes: 38 additions & 0 deletions src/lib/output/plugins/NavigationPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as Path from "path";
import { Component, RendererComponent } from "../components";
import { RendererEvent } from "../events";
import { writeFileSync } from "../../utils";
import { DefaultTheme } from "../themes/default/DefaultTheme";
import { gzipSync } from "zlib";

@Component({ name: "navigation-tree" })
export class NavigationPlugin extends RendererComponent {
override initialize() {
this.listenTo(this.owner, RendererEvent.BEGIN, this.onRendererBegin);
}

private onRendererBegin(event: RendererEvent) {
if (!(this.owner.theme instanceof DefaultTheme)) {
return;
}
if (event.isDefaultPrevented) {
return;
}

const navigationJs = Path.join(
event.outputDirectory,
"assets",
"navigation.js",
);

const nav = this.owner.theme.getNavigation(event.project);
const gz = gzipSync(Buffer.from(JSON.stringify(nav)));

writeFileSync(
navigationJs,
`window.navigationData = "data:application/octet-stream;base64,${gz.toString(
"base64",
)}"`,
);
}
}
3 changes: 2 additions & 1 deletion src/lib/output/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { MarkedPlugin } from "../themes/MarkedPlugin";
export { AssetsPlugin } from "./AssetsPlugin";
export { JavascriptIndexPlugin } from "./JavascriptIndexPlugin";
export { MarkedPlugin } from "../themes/MarkedPlugin";
export { NavigationPlugin } from "./NavigationPlugin";
2 changes: 0 additions & 2 deletions src/lib/output/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ 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";
import { clearSeenIconCache } from "./themes/default/partials/icon";
import { validateStateIsClean } from "./themes/default/partials/type";
import { setRenderSettings } from "../utils/jsx";

Expand Down Expand Up @@ -266,7 +265,6 @@ export class Renderer extends ChildableComponent<
`There are ${output.urls.length} pages to write.`,
);
output.urls.forEach((mapping) => {
clearSeenIconCache();
this.renderDocument(...output.createPageEvent(mapping));
validateStateIsClean(mapping.url);
});
Expand Down
109 changes: 108 additions & 1 deletion src/lib/output/themes/default/DefaultTheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
ContainerReflection,
DeclarationReflection,
SignatureReflection,
ReflectionCategory,
ReflectionGroup,
} from "../../../models";
import { RenderTemplate, UrlMapping } from "../../models/UrlMapping";
import type { PageEvent } from "../../events";
import type { MarkedPlugin } from "../../plugins";
import { DefaultThemeRenderContext } from "./DefaultThemeRenderContext";
import { JSX } from "../../../utils";
import { toStyleClass } from "../lib";
import { classNames, getDisplayName, toStyleClass } from "../lib";

/**
* Defines a mapping of a {@link Models.Kind} to a template file.
Expand All @@ -37,6 +39,14 @@ interface TemplateMapping {
template: RenderTemplate<PageEvent<any>>;
}

export interface NavigationElement {
text: string;
path?: string;
kind?: ReflectionKind;
class?: string;
children?: NavigationElement[];
}

/**
* Default theme implementation of TypeDoc. If a theme does not provide a custom
* {@link Theme} implementation, this theme class will be used.
Expand Down Expand Up @@ -217,6 +227,103 @@ export class DefaultTheme extends Theme {
return "<!DOCTYPE html>" + JSX.renderElement(templateOutput);
}

private _navigationCache: NavigationElement[] | undefined;

/**
* If implementing a custom theme, it is recommended to override {@link buildNavigation} instead.
*/
getNavigation(project: ProjectReflection): NavigationElement[] {
// This is ok because currently TypeDoc wipes out the theme after each render.
// Might need to change in the future, but it's fine for now.
if (this._navigationCache) {
return this._navigationCache;
}

return (this._navigationCache = this.buildNavigation(project));
}

buildNavigation(project: ProjectReflection): NavigationElement[] {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const theme = this;
const opts = this.application.options.getValue("navigation");

if (opts.fullTree) {
this.application.logger.warn(
`The navigation.fullTree option no longer has any affect and will be removed in v0.26`,
);
}

return getNavigationElements(project) || [];

function toNavigation(
element: ReflectionCategory | ReflectionGroup | DeclarationReflection,
): NavigationElement {
if (element instanceof ReflectionCategory || element instanceof ReflectionGroup) {
return {
text: element.title,
children: getNavigationElements(element),
};
}

return {
text: getDisplayName(element),
path: element.url,
kind: element.kind,
class: classNames({ deprecated: element.isDeprecated() }, theme.getReflectionClasses(element)),
children: getNavigationElements(element),
};
}

function getNavigationElements(
parent: ReflectionCategory | ReflectionGroup | DeclarationReflection | ProjectReflection,
): undefined | NavigationElement[] {
if (parent instanceof ReflectionCategory) {
return parent.children.map(toNavigation);
}

if (parent instanceof ReflectionGroup) {
if (shouldShowCategories(parent.owningReflection, opts) && parent.categories) {
return parent.categories.map(toNavigation);
}
return parent.children.map(toNavigation);
}

if (!parent.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project)) {
return;
}

if (parent.categories && shouldShowCategories(parent, opts)) {
return parent.categories.map(toNavigation);
}

if (parent.groups && shouldShowGroups(parent, opts)) {
return parent.groups.map(toNavigation);
}

return parent.children?.map(toNavigation);
}

function shouldShowCategories(
reflection: Reflection,
opts: { includeCategories: boolean; includeGroups: boolean },
) {
if (opts.includeCategories) {
return !reflection.comment?.hasModifier("@hideCategories");
}
return reflection.comment?.hasModifier("@showCategories") === true;
}

function shouldShowGroups(
reflection: Reflection,
opts: { includeCategories: boolean; includeGroups: boolean },
) {
if (opts.includeGroups) {
return !reflection.comment?.hasModifier("@hideGroups");
}
return reflection.comment?.hasModifier("@showGroups") === true;
}
}

/**
* Generate an anchor url for the given reflection and all of its children.
*
Expand Down
25 changes: 22 additions & 3 deletions src/lib/output/themes/default/DefaultThemeRenderContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
DeclarationReflection,
Reflection,
} from "../../../models";
import type { NeverIfInternal, Options } from "../../../utils";
import type { JSX, NeverIfInternal, Options } from "../../../utils";
import type { DefaultTheme } from "./DefaultTheme";
import { defaultLayout } from "./layouts/default";
import { index } from "./partials";
Expand All @@ -19,7 +19,7 @@ import {
import { footer } from "./partials/footer";
import { header } from "./partials/header";
import { hierarchy } from "./partials/hierarchy";
import { icons } from "./partials/icon";
import { buildRefIcons, icons } from "./partials/icon";
import { member } from "./partials/member";
import { memberDeclaration } from "./partials/member.declaration";
import { memberGetterSetter } from "./partials/member.getterSetter";
Expand Down Expand Up @@ -51,6 +51,8 @@ function bind<F, L extends any[], R>(fn: (f: F, ...a: L) => R, first: F) {
}

export class DefaultThemeRenderContext {
private _iconsCache: JSX.Element;
private _refIcons: typeof icons;
options: Options;

constructor(
Expand All @@ -59,9 +61,24 @@ export class DefaultThemeRenderContext {
options: Options,
) {
this.options = options;

const { refs, cache } = buildRefIcons(icons);
this._refIcons = refs;
this._iconsCache = cache;
}

iconsCache(): JSX.Element {
return this._iconsCache;
}

icons = icons;
get icons(): Readonly<typeof icons> {
return this._refIcons;
}
set icons(value: Readonly<typeof icons>) {
const { refs, cache } = buildRefIcons(value);
this._refIcons = refs;
this._iconsCache = cache;
}

hook = (name: keyof RendererHooks) =>
this.theme.owner.hooks.emit(name, this);
Expand Down Expand Up @@ -91,6 +108,8 @@ export class DefaultThemeRenderContext {
return md ? this.theme.markedPlugin.parseMarkdown(md, this.page) : "";
};

getNavigation = () => this.theme.getNavigation(this.page.project);

getReflectionClasses = (refl: DeclarationReflection) =>
this.theme.getReflectionClasses(refl);

Expand Down
15 changes: 6 additions & 9 deletions src/lib/output/themes/default/assets/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { Toggle } from "./typedoc/components/Toggle";
import { Filter } from "./typedoc/components/Filter";
import { Accordion } from "./typedoc/components/Accordion";
import { initTheme } from "./typedoc/Theme";

initSearch();
import { initNav } from "./typedoc/Navigation";

registerComponent(Toggle, "a[data-toggle]");
registerComponent(Accordion, ".tsd-index-accordion");
Expand All @@ -16,14 +15,12 @@ if (themeChoice) {
initTheme(themeChoice as HTMLOptionElement);
}

declare global {
var app: Application;
}
const app = new Application();

Object.defineProperty(window, "app", { value: app });

// Safari is broken and doesn't let you click on a link within
// a <summary> tag, so we have to manually handle clicks there.
document.querySelectorAll("summary a").forEach((el) => {
el.addEventListener("click", () => {
location.assign((el as HTMLAnchorElement).href);
});
});
initSearch();
initNav();
4 changes: 2 additions & 2 deletions src/lib/output/themes/default/assets/typedoc/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class Application {
/**
* Create all components beneath the given element.
*/
private createComponents(context: HTMLElement) {
public createComponents(context: HTMLElement) {
components.forEach((c) => {
context.querySelectorAll<HTMLElement>(c.selector).forEach((el) => {
if (!el.dataset["hasInstance"]) {
Expand All @@ -63,7 +63,7 @@ export class Application {
this.ensureFocusedElementVisible();
}

private ensureActivePageVisible() {
public ensureActivePageVisible() {
const pageLink = document.querySelector(".tsd-navigation .current");
let iter = pageLink?.parentElement;
while (iter && !iter.classList.contains(".tsd-navigation")) {
Expand Down

0 comments on commit 67ee6ac

Please sign in to comment.