From 67ee6acccfa8c4079f2f351b61f5366333fc4ab6 Mon Sep 17 00:00:00 2001 From: Gerrit Birkeland Date: Mon, 4 Sep 2023 10:10:14 -0600 Subject: [PATCH] Dynamically load navigation Resolves #2287 --- .config/typedoc.json | 1 - CHANGELOG.md | 13 ++ src/index.ts | 6 +- src/lib/converter/types.ts | 14 ++ src/lib/output/index.ts | 5 +- src/lib/output/plugins/NavigationPlugin.ts | 38 ++++ src/lib/output/plugins/index.ts | 3 +- src/lib/output/renderer.ts | 2 - .../output/themes/default/DefaultTheme.tsx | 109 +++++++++++- .../default/DefaultThemeRenderContext.ts | 25 ++- .../output/themes/default/assets/bootstrap.ts | 15 +- .../default/assets/typedoc/Application.ts | 4 +- .../default/assets/typedoc/Navigation.ts | 107 ++++++++++++ .../assets/typedoc/components/Accordion.ts | 9 + .../output/themes/default/layouts/default.tsx | 2 + .../output/themes/default/partials/icon.tsx | 102 +++++------ .../themes/default/partials/navigation.tsx | 164 ++++++------------ 17 files changed, 427 insertions(+), 192 deletions(-) create mode 100644 src/lib/output/plugins/NavigationPlugin.ts create mode 100644 src/lib/output/themes/default/assets/typedoc/Navigation.ts diff --git a/.config/typedoc.json b/.config/typedoc.json index 53e36d83e..d0186774a 100644 --- a/.config/typedoc.json +++ b/.config/typedoc.json @@ -40,7 +40,6 @@ "Type Aliases": 2.0 }, "navigation": { - "fullTree": true, "includeCategories": true, "includeGroups": false }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 5040d1ab4..97e72e854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 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. @@ -7,10 +18,12 @@ - `--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) diff --git a/src/index.ts b/src/index.ts index f1425b96b..7bad26177 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/src/lib/converter/types.ts b/src/lib/converter/types.ts index 2e536f02b..2183ebc89 100644 --- a/src/lib/converter/types.ts +++ b/src/lib/converter/types.ts @@ -67,6 +67,7 @@ export function loadConverters() { indexedAccessConverter, inferredConverter, intersectionConverter, + intrinsicConverter, jsDocVariadicTypeConverter, keywordConverter, optionalConverter, @@ -460,6 +461,19 @@ const intersectionConverter: TypeConverter< }, }; +const intrinsicConverter: TypeConverter< + ts.KeywordTypeNode, + ts.Type +> = { + kind: [ts.SyntaxKind.IntrinsicKeyword], + convert() { + return new IntrinsicType("intrinsic"); + }, + convertType() { + return new IntrinsicType("intrinsic"); + }, +}; + const jsDocVariadicTypeConverter: TypeConverter = { kind: [ts.SyntaxKind.JSDocVariadicType], convert(context, node) { diff --git a/src/lib/output/index.ts b/src/lib/output/index.ts index 2cc7750a3..9e63faa68 100644 --- a/src/lib/output/index.ts +++ b/src/lib/output/index.ts @@ -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"; diff --git a/src/lib/output/plugins/NavigationPlugin.ts b/src/lib/output/plugins/NavigationPlugin.ts new file mode 100644 index 000000000..bbcd8c5b0 --- /dev/null +++ b/src/lib/output/plugins/NavigationPlugin.ts @@ -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", + )}"`, + ); + } +} diff --git a/src/lib/output/plugins/index.ts b/src/lib/output/plugins/index.ts index 5eefc0379..a6fd002ba 100644 --- a/src/lib/output/plugins/index.ts +++ b/src/lib/output/plugins/index.ts @@ -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"; diff --git a/src/lib/output/renderer.ts b/src/lib/output/renderer.ts index 0a3ab1335..c033150f2 100644 --- a/src/lib/output/renderer.ts +++ b/src/lib/output/renderer.ts @@ -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"; @@ -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); }); diff --git a/src/lib/output/themes/default/DefaultTheme.tsx b/src/lib/output/themes/default/DefaultTheme.tsx index 96b5b8718..d351cd0ec 100644 --- a/src/lib/output/themes/default/DefaultTheme.tsx +++ b/src/lib/output/themes/default/DefaultTheme.tsx @@ -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. @@ -37,6 +39,14 @@ interface TemplateMapping { template: RenderTemplate>; } +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. @@ -217,6 +227,103 @@ export class DefaultTheme extends Theme { return "" + 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. * diff --git a/src/lib/output/themes/default/DefaultThemeRenderContext.ts b/src/lib/output/themes/default/DefaultThemeRenderContext.ts index 16b2f2d0b..f257ff436 100644 --- a/src/lib/output/themes/default/DefaultThemeRenderContext.ts +++ b/src/lib/output/themes/default/DefaultThemeRenderContext.ts @@ -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"; @@ -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"; @@ -51,6 +51,8 @@ function bind(fn: (f: F, ...a: L) => R, first: F) { } export class DefaultThemeRenderContext { + private _iconsCache: JSX.Element; + private _refIcons: typeof icons; options: Options; constructor( @@ -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 { + return this._refIcons; + } + set icons(value: Readonly) { + const { refs, cache } = buildRefIcons(value); + this._refIcons = refs; + this._iconsCache = cache; + } hook = (name: keyof RendererHooks) => this.theme.owner.hooks.emit(name, this); @@ -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); diff --git a/src/lib/output/themes/default/assets/bootstrap.ts b/src/lib/output/themes/default/assets/bootstrap.ts index d6dfc4765..5184eb086 100644 --- a/src/lib/output/themes/default/assets/bootstrap.ts +++ b/src/lib/output/themes/default/assets/bootstrap.ts @@ -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"); @@ -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 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(); diff --git a/src/lib/output/themes/default/assets/typedoc/Application.ts b/src/lib/output/themes/default/assets/typedoc/Application.ts index f304c783a..0c53725b4 100644 --- a/src/lib/output/themes/default/assets/typedoc/Application.ts +++ b/src/lib/output/themes/default/assets/typedoc/Application.ts @@ -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(c.selector).forEach((el) => { if (!el.dataset["hasInstance"]) { @@ -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")) { diff --git a/src/lib/output/themes/default/assets/typedoc/Navigation.ts b/src/lib/output/themes/default/assets/typedoc/Navigation.ts new file mode 100644 index 000000000..9d93fe7f6 --- /dev/null +++ b/src/lib/output/themes/default/assets/typedoc/Navigation.ts @@ -0,0 +1,107 @@ +export interface NavigationElement { + text: string; + path?: string; + kind?: number; + class?: string; + children?: NavigationElement[]; +} + +let BASE_URL: string; + +declare global { + interface Window { + // Base64 encoded data url, gzipped, JSON encoded NavigationElement[] + navigationData?: string; + } +} + +export function initNav() { + const script = document.getElementById("tsd-nav-script"); + if (!script) return; + + script.addEventListener("load", buildNav); + buildNav(); +} + +async function buildNav() { + const container = document.getElementById("tsd-nav-container"); + if (!container || !window.navigationData) return; + + const res = await fetch(window.navigationData); + const data = await res.arrayBuffer(); + const json = new Blob([data]) + .stream() + .pipeThrough(new DecompressionStream("gzip")); + const nav: NavigationElement[] = await new Response(json).json(); + + BASE_URL = container.dataset.base + "/"; + container.innerHTML = ""; + for (const el of nav) { + buildNavElement(el, container, []); + } + + window.app.createComponents(container); + window.app.ensureActivePageVisible(); +} + +function buildNavElement( + el: NavigationElement, + parent: HTMLElement, + path: string[], +) { + const li = parent.appendChild(document.createElement("li")); + + if (el.children) { + const fullPath = [...path, el.text]; + const details = li.appendChild(document.createElement("details")); + details.className = el.class + ? `${el.class} tsd-index-accordion` + : "tsd-index-accordion"; + details.dataset.key = fullPath.join("$"); + + const summary = details.appendChild(document.createElement("summary")); + summary.className = "tsd-accordion-summary"; + // Would be nice to not hardcode this here, if someone overwrites the chevronDown icon with an + // then this won't work... going to wait to worry about that until it actually breaks some custom theme. + // Also very annoying that we can't encode the svg in the cache, since that gets duplicated here... + // If that breaks someone, we probably have to get the svg element from the cached div (and include them..) + // and clone that into place... + summary.innerHTML = ``; + addNavText(el, summary); + + const data = details.appendChild(document.createElement("div")); + data.className = "tsd-accordion-details"; + const ul = data.appendChild(document.createElement("ul")); + ul.className = "tsd-nested-navigation"; + + for (const child of el.children) { + buildNavElement(child, ul, fullPath); + } + } else { + addNavText(el, li, el.class); + } +} + +function addNavText( + el: NavigationElement, + parent: HTMLElement, + classes?: string | 0, +) { + if (el.path) { + const a = parent.appendChild(document.createElement("a")); + a.href = BASE_URL + el.path; // relativity! + if (classes) { + a.className = classes; + } + if (location.href === a.href) { + a.classList.add("current"); + } + if (el.kind) { + a.innerHTML = ``; + } + a.appendChild(document.createElement("span")).textContent = el.text; + } else { + parent.appendChild(document.createElement("span")).textContent = + el.text; + } +} diff --git a/src/lib/output/themes/default/assets/typedoc/components/Accordion.ts b/src/lib/output/themes/default/assets/typedoc/components/Accordion.ts index e5b43e8d1..ed8df8b7e 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Accordion.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Accordion.ts @@ -37,6 +37,15 @@ export class Accordion extends Component { this.el.addEventListener("toggle", () => this.update()); + // Safari is broken and doesn't let you click on a link within + // a tag, so we have to manually handle clicks there. + const link = this.summary.querySelector("a"); + if (link) { + link.addEventListener("click", () => { + location.assign(link.href); + }); + } + this.update(); } diff --git a/src/lib/output/themes/default/layouts/default.tsx b/src/lib/output/themes/default/layouts/default.tsx index dcf011445..b7dedee11 100644 --- a/src/lib/output/themes/default/layouts/default.tsx +++ b/src/lib/output/themes/default/layouts/default.tsx @@ -30,6 +30,7 @@ export const defaultLayout = ( )} + {context.hook("head.end")} @@ -65,6 +66,7 @@ export const defaultLayout = (
{context.analytics()} + {context.iconsCache()} {context.hook("body.end")} diff --git a/src/lib/output/themes/default/partials/icon.tsx b/src/lib/output/themes/default/partials/icon.tsx index 95dd11319..434f526fb 100644 --- a/src/lib/output/themes/default/partials/icon.tsx +++ b/src/lib/output/themes/default/partials/icon.tsx @@ -1,55 +1,52 @@ +import assert from "assert"; import { ReflectionKind } from "../../../../models"; import { JSX } from "../../../../utils"; -const seenIcons = new Set(); +const kindIcon = (letterPath: JSX.Element, color: string, circular = false) => ( + + + {letterPath} + +); -export function clearSeenIconCache() { - seenIcons.clear(); -} +export function buildRefIcons JSX.Element>>(icons: T): { refs: T; cache: JSX.Element } { + const refs: Record JSX.Element> = {}; + const children: JSX.Element[] = []; + + for (const [name, builder] of Object.entries(icons)) { + const jsx = builder.call(icons); + assert(jsx.tag === "svg", "TypeDoc's frontend assumes that icons are written as svg elements"); -function cachedPart(key: string, svgPart: JSX.Element) { - if (seenIcons.has(key)) { - return ; + children.push({jsx.children}); + const ref = ( + + + + ); + refs[name] = () => ref; } - seenIcons.add(key); return { - ...svgPart, - props: { - ...svgPart.props, - id: `icon-${key}`, - }, + refs: refs as T, + cache: {children}, }; } -const kindIcon = (kind: ReflectionKind, letterPath: JSX.Element, color: string, circular = false) => ( - - {cachedPart( - `${kind}`, - - - {letterPath} - , - )} - -); - export const icons: Record< ReflectionKind | "chevronDown" | "checkbox" | "menu" | "search" | "chevronSmall" | "anchor", () => JSX.Element > = { [ReflectionKind.Accessor]: () => kindIcon( - ReflectionKind.Accessor, kindIcon( - ReflectionKind.Class, kindIcon( - ReflectionKind.Constructor, kindIcon( - ReflectionKind.Enum, kindIcon( - ReflectionKind.Function, , "var(--color-ts-function)", ), @@ -108,7 +101,6 @@ export const icons: Record< }, [ReflectionKind.Interface]: () => kindIcon( - ReflectionKind.Interface, kindIcon( - ReflectionKind.Method, kindIcon( - ReflectionKind.Namespace, kindIcon( - ReflectionKind.Property, kindIcon( - ReflectionKind.Reference, kindIcon( - ReflectionKind.TypeAlias, , "var(--color-ts-type-alias)", ), @@ -181,7 +168,6 @@ export const icons: Record< }, [ReflectionKind.Variable]: () => kindIcon( - ReflectionKind.Variable, ( - {cachedPart( - "chevronDown", - , - )} + ), chevronSmall: () => ( @@ -237,14 +220,11 @@ export const icons: Record< ), anchor: () => ( - {cachedPart( - "anchor", - - - - - , - )} + + + + + ), }; diff --git a/src/lib/output/themes/default/partials/navigation.tsx b/src/lib/output/themes/default/partials/navigation.tsx index 561ab2e45..edac2d18a 100644 --- a/src/lib/output/themes/default/partials/navigation.tsx +++ b/src/lib/output/themes/default/partials/navigation.tsx @@ -1,16 +1,12 @@ -import { - DeclarationReflection, - ProjectReflection, - Reflection, - ReflectionCategory, - ReflectionGroup, - ReflectionKind, -} from "../../../../models"; +import { Reflection, ReflectionKind } from "../../../../models"; import { JSX } from "../../../../utils"; import type { PageEvent } from "../../../events"; import { camelToTitleCase, classNames, getDisplayName, wbr } from "../../lib"; +import type { NavigationElement } from "../DefaultTheme"; import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext"; +const MAX_EMBEDDED_NAV_SIZE = 20; + export function sidebar(context: DefaultThemeRenderContext, props: PageEvent) { return ( <> @@ -103,122 +99,70 @@ export function settings(context: DefaultThemeRenderContext) { ); } -type NavigationElement = ReflectionCategory | ReflectionGroup | DeclarationReflection; - -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; -} +export const navigation = function navigation(context: DefaultThemeRenderContext, props: PageEvent) { + const nav = context.getNavigation(); -const getNavigationElements = function getNavigationElements( - parent: NavigationElement | ProjectReflection, - opts: { includeCategories: boolean; includeGroups: boolean }, -): undefined | readonly NavigationElement[] { - if (parent instanceof ReflectionCategory) { - return parent.children; - } + let elements = 0; + function link(el: NavigationElement, path: string[] = []) { + if (elements > MAX_EMBEDDED_NAV_SIZE) { + return <>; + } - if (parent instanceof ReflectionGroup) { - if (shouldShowCategories(parent.owningReflection, opts) && parent.categories) { - return parent.categories; + if (el.path) { + ++elements; + return ( +
  • + + {el.kind && context.icons[el.kind]()} + {el.text} + +
  • + ); } - return parent.children; - } - if (!parent.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project)) { - return; - } + // Top level element is a group/category, recurse so that we don't have a half-broken + // navigation tree for people with JS turned off. + if (el.children) { + ++elements; + const fullPath = [...path, el.text]; - if (parent.categories && shouldShowCategories(parent, opts)) { - return parent.categories; - } + return ( +
    + + {context.icons.chevronDown()} + {el.text} + +
    +
      {el.children.map((c) => link(c, fullPath))}
    +
    +
    + ); + } - if (parent.groups && shouldShowGroups(parent, opts)) { - return parent.groups; + return ( +
  • + {el.text} +
  • + ); } - return parent.children; -}; - -export const navigation = function navigation(context: DefaultThemeRenderContext, props: PageEvent) { - const opts = context.options.getValue("navigation"); - // Create the navigation for the current page - // Recurse to children if the parent is some kind of module + const navEl = nav.map((el) => link(el)); return ( ); - - function links(mod: NavigationElement, parents: string[]) { - const nameClasses = classNames( - { deprecated: mod instanceof Reflection && mod.isDeprecated() }, - mod instanceof DeclarationReflection ? context.getReflectionClasses(mod) : void 0, - ); - - const children = getNavigationElements(mod, opts); - - if (!children || (!opts.fullTree && mod instanceof Reflection && !inPath(mod))) { - return createNavElement(mod, nameClasses); - } - - const childParents = mod instanceof Reflection ? [mod.getFullName()] : [...parents, mod.title]; - - return ( -
    - - {context.icons.chevronDown()} - {createNavElement(mod)} - -
    -
      - {children.map((c) => ( -
    • {links(c, childParents)}
    • - ))} -
    -
    -
    - ); - } - - function createNavElement(child: NavigationElement | ProjectReflection, nameClasses?: string) { - if (child instanceof Reflection) { - return ( - - {context.icons[child.kind]()} - {wbr(getDisplayName(child))} - - ); - } - - return {child.title}; - } - - function inPath(mod: DeclarationReflection | ProjectReflection) { - let iter: Reflection | undefined = props.model; - do { - if (iter == mod) return true; - iter = iter.parent; - } while (iter); - return false; - } }; export function pageSidebar(context: DefaultThemeRenderContext, props: PageEvent) {