From a67b9c2ae0adca72cd20ee520aba46d84a58d66c Mon Sep 17 00:00:00 2001 From: Gerrit Birkeland Date: Sun, 15 Dec 2024 07:49:16 -0700 Subject: [PATCH] WIP: Create a router for TypeDoc --- .vscode/settings.json | 1 + CHANGELOG.md | 6 + scripts/download_plugins.cjs | 4 +- src/index.ts | 4 + src/lib/converter/symbols.ts | 8 +- src/lib/models/ReflectionCategory.ts | 7 - src/lib/models/ReflectionGroup.ts | 7 - src/lib/models/reflections/abstract.ts | 28 +- src/lib/models/reflections/declaration.ts | 2 +- src/lib/models/reflections/signature.ts | 4 + src/lib/models/reflections/type-parameter.ts | 4 + src/lib/output/events.ts | 40 +-- src/lib/output/formatter.tsx | 123 +++++-- src/lib/output/index.ts | 13 +- src/lib/output/plugins/HierarchyPlugin.ts | 6 +- .../output/plugins/JavascriptIndexPlugin.ts | 34 +- src/lib/output/plugins/SitemapPlugin.ts | 37 +- src/lib/output/renderer.ts | 68 ++-- src/lib/output/router.ts | 325 ++++++++++++++++++ src/lib/output/theme.ts | 46 +-- src/lib/output/themes/MarkedPlugin.tsx | 4 +- .../output/themes/default/DefaultTheme.tsx | 219 ++---------- .../default/DefaultThemeRenderContext.ts | 38 +- .../assets/typedoc/components/Search.ts | 5 +- .../output/themes/default/layouts/default.tsx | 2 +- .../themes/default/partials/anchor-icon.tsx | 13 + .../themes/default/partials/breadcrumb.tsx | 29 +- .../themes/default/partials/comment.tsx | 4 +- .../output/themes/default/partials/header.tsx | 2 +- .../themes/default/partials/hierarchy.tsx | 4 +- .../output/themes/default/partials/index.tsx | 36 +- .../default/partials/member.declaration.tsx | 2 +- .../default/partials/member.getterSetter.tsx | 4 +- .../partials/member.signature.title.tsx | 2 +- .../default/partials/member.signatures.tsx | 6 +- .../output/themes/default/partials/member.tsx | 18 +- .../themes/default/partials/members.tsx | 2 +- .../default/partials/moduleReflection.tsx | 4 +- .../default/partials/reflectionPreview.tsx | 2 +- .../output/themes/default/partials/type.tsx | 2 +- .../themes/default/partials/typeAndParent.tsx | 5 +- .../themes/default/partials/typeDetails.tsx | 41 ++- .../default/partials/typeParameters.tsx | 3 +- .../themes/default/templates/hierarchy.tsx | 3 +- src/lib/utils/perf.ts | 36 +- src/test/converter2/behavior/benchmark.ts | 54 +++ src/test/converter2/behavior/routerBugs.ts | 10 + src/test/issues.c2.test.ts | 4 +- src/test/output/formatter.test.ts | 38 +- 49 files changed, 856 insertions(+), 503 deletions(-) create mode 100644 src/lib/output/router.ts create mode 100644 src/test/converter2/behavior/benchmark.ts create mode 100644 src/test/converter2/behavior/routerBugs.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 6da3bedd6..408b857aa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -55,6 +55,7 @@ "linkcode", "linkify", "linkplain", + "momento", "Msys", "nodoc", "shiki", diff --git a/CHANGELOG.md b/CHANGELOG.md index 36609185a..556181779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ title: Changelog --- +## Beta + +- TypeDoc will now only create references for symbols re-exported from modules. +- API: Introduced a `Router` which is used for URL creation. `Reflection.url`, + `Reflection.anchor`, and `Reflection.hasOwnDocument` have been removed. + ## Unreleased ## v0.27.2 (2024-11-29) diff --git a/scripts/download_plugins.cjs b/scripts/download_plugins.cjs index 910970ae9..18bfa7ded 100644 --- a/scripts/download_plugins.cjs +++ b/scripts/download_plugins.cjs @@ -39,8 +39,8 @@ async function getPlugins() { }); } -function getTarballUrl(package) { - return exec(`npm view ${package.name} dist.tarball`); +function getTarballUrl(pack) { + return exec(`npm view ${pack.name} dist.tarball`); } function downloadTarball(url, outDir) { diff --git a/src/index.ts b/src/index.ts index 4b4c7fd43..677e5ce92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,6 +51,8 @@ export { RendererEvent, MarkdownEvent, IndexEvent, + DefaultRouter, + PageKind, } from "./lib/output/index.js"; export type { RenderTemplate, @@ -58,6 +60,8 @@ export type { NavigationElement, RendererEvents, PageHeading, + Router, + PageDefinition, } from "./lib/output/index.js"; export { Outputs } from "./lib/output/output.js"; diff --git a/src/lib/converter/symbols.ts b/src/lib/converter/symbols.ts index 73a22a012..090939371 100644 --- a/src/lib/converter/symbols.ts +++ b/src/lib/converter/symbols.ts @@ -906,7 +906,13 @@ function convertAlias( const reflection = context.project.getReflectionFromSymbol( context.resolveAliasedSymbol(symbol), ); - if (!reflection) { + if ( + !reflection || + (reflection && + !reflection.parent?.kindOf( + ReflectionKind.Project | ReflectionKind.SomeModule, + )) + ) { // We don't have this, convert it. convertSymbol( context, diff --git a/src/lib/models/ReflectionCategory.ts b/src/lib/models/ReflectionCategory.ts index 2812dd8bf..774b2e6d0 100644 --- a/src/lib/models/ReflectionCategory.ts +++ b/src/lib/models/ReflectionCategory.ts @@ -42,13 +42,6 @@ export class ReflectionCategory { this.title = title; } - /** - * Do all children of this category have a separate document? - */ - allChildrenHaveOwnDocument(): boolean { - return this.children.every((child) => child.hasOwnDocument); - } - toObject(serializer: Serializer): JSONOutput.ReflectionCategory { return { title: this.title, diff --git a/src/lib/models/ReflectionGroup.ts b/src/lib/models/ReflectionGroup.ts index c72d2f810..f056a845b 100644 --- a/src/lib/models/ReflectionGroup.ts +++ b/src/lib/models/ReflectionGroup.ts @@ -53,13 +53,6 @@ export class ReflectionGroup { this.title = title; } - /** - * Do all children of this group have a separate document? - */ - allChildrenHaveOwnDocument(): boolean { - return this.children.every((child) => child.hasOwnDocument); - } - toObject(serializer: Serializer): JSONOutput.ReflectionGroup { return { title: this.title, diff --git a/src/lib/models/reflections/abstract.ts b/src/lib/models/reflections/abstract.ts index 293c7b00c..86a70d5f4 100644 --- a/src/lib/models/reflections/abstract.ts +++ b/src/lib/models/reflections/abstract.ts @@ -18,6 +18,8 @@ import type { } from "../../internationalization/index.js"; import type { ParameterReflection } from "./parameter.js"; import type { ReferenceReflection } from "./reference.js"; +import type { SignatureReflection } from "./signature.js"; +import type { TypeParameterReflection } from "./type-parameter.js"; /** * Current reflection id. @@ -294,26 +296,6 @@ export abstract class Reflection { */ comment?: Comment; - /** - * The url of this reflection in the generated documentation. - * TODO: Reflections shouldn't know urls exist. Move this to a serializer. - */ - url?: string; - - /** - * The name of the anchor of this child. - * TODO: Reflections shouldn't know anchors exist. Move this to a serializer. - */ - anchor?: string; - - /** - * Is the url pointing to an individual document? - * - * When FALSE, the url points to an anchor tag on a page of a different reflection. - * TODO: Reflections shouldn't know how they are rendered. Move this to the correct serializer. - */ - hasOwnDocument?: boolean; - constructor(name: string, kind: ReflectionKind, parent?: Reflection) { this.id = REFLECTION_ID++ as ReflectionId; this.parent = parent; @@ -434,6 +416,12 @@ export abstract class Reflection { isDeclaration(): this is DeclarationReflection { return false; } + isSignature(): this is SignatureReflection { + return false; + } + isTypeParameter(): this is TypeParameterReflection { + return false; + } isParameter(): this is ParameterReflection { return false; } diff --git a/src/lib/models/reflections/declaration.ts b/src/lib/models/reflections/declaration.ts index e53f7f8d4..7ffa48623 100644 --- a/src/lib/models/reflections/declaration.ts +++ b/src/lib/models/reflections/declaration.ts @@ -40,7 +40,7 @@ export interface DeclarationHierarchy { * A reflection that represents a single declaration emitted by the TypeScript compiler. * * All parts of a project are represented by DeclarationReflection instances. The actual - * kind of a reflection is stored in its ´kind´ member. + * kind of a reflection is stored in its `kind` member. * @category Reflections */ export class DeclarationReflection extends ContainerReflection { diff --git a/src/lib/models/reflections/signature.ts b/src/lib/models/reflections/signature.ts index f988e0726..115dedc37 100644 --- a/src/lib/models/reflections/signature.ts +++ b/src/lib/models/reflections/signature.ts @@ -97,6 +97,10 @@ export class SignatureReflection extends Reflection { } } + override isSignature(): this is SignatureReflection { + return true; + } + /** * Return a string representation of this reflection. */ diff --git a/src/lib/models/reflections/type-parameter.ts b/src/lib/models/reflections/type-parameter.ts index 9feac59ea..a0dcf521a 100644 --- a/src/lib/models/reflections/type-parameter.ts +++ b/src/lib/models/reflections/type-parameter.ts @@ -44,6 +44,10 @@ export class TypeParameterReflection extends Reflection { this.varianceModifier = varianceModifier; } + override isTypeParameter(): this is TypeParameterReflection { + return true; + } + override toObject( serializer: Serializer, ): JSONOutput.TypeParameterReflection { diff --git a/src/lib/output/events.ts b/src/lib/output/events.ts index 3910eff3c..fbd37e6f7 100644 --- a/src/lib/output/events.ts +++ b/src/lib/output/events.ts @@ -1,13 +1,10 @@ -import * as Path from "path"; - import type { ProjectReflection } from "../models/reflections/project.js"; -import type { RenderTemplate, UrlMapping } from "./models/UrlMapping.js"; import type { DeclarationReflection, DocumentReflection, - Reflection, ReflectionKind, } from "../models/index.js"; +import type { PageDefinition, PageKind } from "./router.js"; /** * An event emitted by the {@link Renderer} class at the very beginning and @@ -28,11 +25,9 @@ export class RendererEvent { readonly outputDirectory: string; /** - * A list of all pages that should be generated. - * - * This list can be altered during the {@link Renderer.EVENT_BEGIN} event. + * A list of all pages that will be generated. */ - urls?: UrlMapping[]; + pages: PageDefinition[]; /** * Triggered before the renderer starts rendering a project. @@ -46,26 +41,14 @@ export class RendererEvent { */ static readonly END = "endRender"; - constructor(outputDirectory: string, project: ProjectReflection) { + constructor( + outputDirectory: string, + project: ProjectReflection, + pages: PageDefinition[], + ) { this.outputDirectory = outputDirectory; this.project = project; - } - - /** - * Create an {@link PageEvent} event based on this event and the given url mapping. - * - * @internal - * @param mapping The mapping that defines the generated {@link PageEvent} state. - * @returns A newly created {@link PageEvent} instance. - */ - public createPageEvent( - mapping: UrlMapping, - ): [RenderTemplate>, PageEvent] { - const event = new PageEvent(mapping.model); - event.project = this.project; - event.url = mapping.url; - event.filename = Path.join(this.outputDirectory, mapping.url); - return [mapping.template, event]; + this.pages = pages; } } @@ -100,6 +83,11 @@ export class PageEvent { */ url!: string; + /** + * The type of page this is. + */ + pageKind!: PageKind; + /** * The model that should be rendered on this page. */ diff --git a/src/lib/output/formatter.tsx b/src/lib/output/formatter.tsx index 4e7944508..33395424a 100644 --- a/src/lib/output/formatter.tsx +++ b/src/lib/output/formatter.tsx @@ -20,6 +20,7 @@ import { type TypeParameterReflection, type ParameterReflection, } from "../models/index.js"; +import type { Router } from "./index.js"; // Non breaking space const INDENT = "\u00A0\u00A0\u00A0\u00A0"; @@ -286,15 +287,28 @@ const typeBuilder: TypeVisitor< ]); if (childReflection) { const displayed = stringify(type.indexType.value); - indexType = { - type: "element", - content: ( - + + if (builder.router.hasUrl(childReflection)) { + indexType = { + type: "element", + content: ( + + + {displayed} + + + ), + length: displayed.length, + }; + } else { + indexType = { + type: "element", + content: ( {displayed} - - ), - length: displayed.length, - }; + ), + length: displayed.length, + }; + } } } @@ -489,29 +503,51 @@ const typeBuilder: TypeVisitor< if (reflection) { if (reflection.kindOf(ReflectionKind.TypeParameter)) { - name = simpleElement( - - {reflection.name} - , - ); + if (builder.router.hasUrl(reflection)) { + name = simpleElement( + + {reflection.name} + , + ); + } else { + name = simpleElement( + + {reflection.name} + , + ); + } } else { name = join( simpleElement(.), getUniquePath(reflection), - (item) => - simpleElement( - { + if (builder.router.hasUrl(item)) { + return simpleElement( + + {item.name} + , + ); + } + + return simpleElement( + {item.name} - , - ), + , + ); + }, ); } } else if (type.externalUrl) { @@ -681,7 +717,14 @@ export class FormattedCodeBuilder { forceWrap = new Set(); id = 0; - constructor(readonly urlTo: (refl: Reflection) => string) {} + constructor( + readonly router: Router, + readonly relativeReflection: Reflection, + ) {} + + urlTo(refl: Reflection) { + return this.router.relativeUrl(this.relativeReflection, refl); + } newId() { return ++this.id; @@ -978,17 +1021,29 @@ export class FormattedCodeBuilder { space(), ); } - const content = [ - prefix, - simpleElement( - - {param.name} - , - ), - ]; + + const content = [prefix]; + + if (this.router.hasUrl(param)) { + content.push( + simpleElement( + + {param.name} + , + ), + ); + } else { + content.push( + simpleElement( + + {param.name} + , + ), + ); + } if (param.type) { content.push( diff --git a/src/lib/output/index.ts b/src/lib/output/index.ts index eaa3c924f..d69f8a31e 100644 --- a/src/lib/output/index.ts +++ b/src/lib/output/index.ts @@ -1,8 +1,8 @@ export { + IndexEvent, + MarkdownEvent, PageEvent, RendererEvent, - MarkdownEvent, - IndexEvent, type PageHeading, } from "./events.js"; export { UrlMapping } from "./models/UrlMapping.js"; @@ -14,5 +14,12 @@ export { DefaultTheme, type NavigationElement, } from "./themes/default/DefaultTheme.js"; -export { Slugger } from "./themes/default/Slugger.js"; export { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderContext.js"; +export { Slugger } from "./themes/default/Slugger.js"; + +export { + DefaultRouter, + PageKind, + type PageDefinition, + type Router, +} from "./router.js"; diff --git a/src/lib/output/plugins/HierarchyPlugin.ts b/src/lib/output/plugins/HierarchyPlugin.ts index 1e4e9b25e..15b207d3a 100644 --- a/src/lib/output/plugins/HierarchyPlugin.ts +++ b/src/lib/output/plugins/HierarchyPlugin.ts @@ -61,12 +61,14 @@ export class HierarchyPlugin extends RendererComponent { const id = queue.pop()!; const refl = project.getReflectionById(id) as DeclarationReflection; if (id in hierarchy.reflections) continue; - if (!refl.url) continue; + + const url = this.owner.router!.getFullUrl(refl); + if (!url) continue; const jsonRecord: JsonHierarchyElement = { name: refl.name, kind: refl.kind, - url: refl.url, + url, class: getKindClass(refl), }; diff --git a/src/lib/output/plugins/JavascriptIndexPlugin.ts b/src/lib/output/plugins/JavascriptIndexPlugin.ts index 64b25ec14..e9204b455 100644 --- a/src/lib/output/plugins/JavascriptIndexPlugin.ts +++ b/src/lib/output/plugins/JavascriptIndexPlugin.ts @@ -1,12 +1,11 @@ import * as Path from "path"; import lunr from "lunr"; -import { - type Comment, +import type { + Comment, DeclarationReflection, DocumentReflection, - ProjectReflection, - type Reflection, + Reflection, } from "../../models/index.js"; import { RendererComponent } from "../components.js"; import { IndexEvent, RendererEvent } from "../events.js"; @@ -78,17 +77,14 @@ export class JavascriptIndexPlugin extends RendererComponent { const rows: SearchDocument[] = []; - const initialSearchResults = Object.values( - event.project.reflections, - ).filter((refl) => { - return ( - (refl instanceof DeclarationReflection || - refl instanceof DocumentReflection) && - refl.url && - refl.name && - !refl.flags.isExternal - ); - }) as Array; + const initialSearchResults = this.owner + .router!.getLinkableReflections() + .filter( + (refl) => + (refl.isDeclaration() || refl.isDocument()) && + refl.name && + !refl.flags.isExternal, + ) as Array; const indexEvent = new IndexEvent(initialSearchResults); @@ -105,10 +101,6 @@ export class JavascriptIndexPlugin extends RendererComponent { } for (const reflection of indexEvent.searchResults) { - if (!reflection.url) { - continue; - } - const boost = this.getBoost(reflection); if (boost <= 0) { @@ -116,14 +108,14 @@ export class JavascriptIndexPlugin extends RendererComponent { } let parent = reflection.parent; - if (parent instanceof ProjectReflection) { + if (parent?.isProject()) { parent = undefined; } const row: SearchDocument = { kind: reflection.kind, name: reflection.name, - url: reflection.url, + url: theme.router.getFullUrl(reflection), classes: theme.getReflectionClasses(reflection), }; diff --git a/src/lib/output/plugins/SitemapPlugin.ts b/src/lib/output/plugins/SitemapPlugin.ts index db401ebe0..c11d4f233 100644 --- a/src/lib/output/plugins/SitemapPlugin.ts +++ b/src/lib/output/plugins/SitemapPlugin.ts @@ -45,25 +45,24 @@ export class SitemapPlugin extends RendererComponent { const sitemapXml = Path.join(event.outputDirectory, "sitemap.xml"); const lastmod = new Date(this.owner.renderStartTime).toISOString(); - const urls: XmlElementData[] = - event.urls?.map((url) => { - return { - tag: "url", - children: [ - { - tag: "loc", - children: new URL( - url.url, - this.hostedBaseUrl, - ).toString(), - }, - { - tag: "lastmod", - children: lastmod, - }, - ], - }; - }) ?? []; + const urls: XmlElementData[] = event.pages.map((page) => { + return { + tag: "url", + children: [ + { + tag: "loc", + children: new URL( + page.url, + this.hostedBaseUrl, + ).toString(), + }, + { + tag: "lastmod", + children: lastmod, + }, + ], + }; + }); const sitemap = `\n` + diff --git a/src/lib/output/renderer.ts b/src/lib/output/renderer.ts index 1df30cbdc..f52027666 100644 --- a/src/lib/output/renderer.ts +++ b/src/lib/output/renderer.ts @@ -18,7 +18,6 @@ import { type MarkdownEvent, } from "./events.js"; import type { ProjectReflection } from "../models/reflections/project.js"; -import type { RenderTemplate } from "./models/UrlMapping.js"; import { writeFileSync } from "../utils/fs.js"; import { DefaultTheme } from "./themes/default/DefaultTheme.js"; import { Option, EventHooks, AbstractComponent } from "../utils/index.js"; @@ -27,7 +26,7 @@ import type { BundledLanguage, BundledTheme as ShikiTheme, } from "@gerrit0/mini-shiki"; -import { type Comment, Reflection } from "../models/index.js"; +import type { Comment, Reflection } from "../models/index.js"; import type { JsxElement } from "../utils/jsx.elements.js"; import type { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderContext.js"; import { setRenderSettings } from "../utils/jsx.js"; @@ -41,6 +40,7 @@ import { NavigationPlugin, SitemapPlugin, } from "./plugins/index.js"; +import { DefaultRouter, type PageDefinition, type Router } from "./router.js"; /** * Describes the hooks available to inject output in the default theme. @@ -168,6 +168,10 @@ export interface RendererEvents { * @group Common */ export class Renderer extends AbstractComponent { + private routers = new Map Router>([ + ["default", DefaultRouter], + ]); + private themes = new Map Theme>([ ["default", DefaultTheme], ]); @@ -211,6 +215,12 @@ export class Renderer extends AbstractComponent { */ theme?: Theme; + /** + * The router which is used to determine the pages to render and + * how to link between pages. + */ + router?: Router; + /** * Hooks which will be called when rendering pages. * Note: @@ -280,6 +290,18 @@ export class Renderer extends AbstractComponent { this.themes.set(name, theme); } + /** + * Define a new router that can be used to determine the output structure. + * @param name + * @param router + */ + defineRouter(name: string, router: new (app: Application) => Router) { + if (this.routers.has(name)) { + throw new Error(`The router "${name}" has already been defined.`); + } + this.routers.set(name, router); + } + /** * Render the given project reflection to the specified output directory. * @@ -295,6 +317,9 @@ export class Renderer extends AbstractComponent { const momento = this.hooks.saveMomento(); this.renderStartTime = Date.now(); + // GERRIT: Support user input + this.router = new (this.routers.get("default")!)(this.application); + if ( !this.prepareTheme() || !(await this.prepareOutputDirectory(outputDirectory)) @@ -302,18 +327,18 @@ export class Renderer extends AbstractComponent { return; } - const output = new RendererEvent(outputDirectory, project); - output.urls = this.theme!.getUrls(project); + const pages = this.router.buildPages(project); + const output = new RendererEvent(outputDirectory, project, pages); this.trigger(RendererEvent.BEGIN, output); await this.runPreRenderJobs(output); this.application.logger.verbose( - `There are ${output.urls.length} pages to write.`, + `There are ${pages.length} pages to write.`, ); - output.urls.forEach((mapping) => { - this.renderDocument(...output.createPageEvent(mapping)); - }); + for (const page of pages) { + this.renderDocument(outputDirectory, page); + } await Promise.all(this.postRenderAsyncJobs.map((job) => job(output))); this.postRenderAsyncJobs = []; @@ -321,6 +346,7 @@ export class Renderer extends AbstractComponent { this.trigger(RendererEvent.END, output); this.theme = void 0; + this.router = void 0; this.hooks.restoreMomento(momento); } @@ -351,27 +377,27 @@ export class Renderer extends AbstractComponent { * @param page An event describing the current page. * @return TRUE if the page has been saved to disc, otherwise FALSE. */ - private renderDocument( - template: RenderTemplate>, - page: PageEvent, - ) { + private renderDocument(outputDirectory: string, page: PageDefinition) { const momento = this.hooks.saveMomento(); - this.trigger(PageEvent.BEGIN, page); - if (page.model instanceof Reflection) { - page.contents = this.theme!.render(page, template); - } else { - throw new Error("Should be unreachable"); - } + const event = new PageEvent(page.model); + event.url = page.url; + event.filename = path.join(outputDirectory, page.url); + event.pageKind = page.kind; + event.project = page.model.project; + + this.trigger(PageEvent.BEGIN, event); + + event.contents = this.theme!.render(event); - this.trigger(PageEvent.END, page); + this.trigger(PageEvent.END, event); this.hooks.restoreMomento(momento); try { - writeFileSync(page.filename, page.contents); + writeFileSync(event.filename, event.contents); } catch (error) { this.application.logger.error( - this.application.i18n.could_not_write_0(page.filename), + this.application.i18n.could_not_write_0(event.filename), ); } } diff --git a/src/lib/output/router.ts b/src/lib/output/router.ts new file mode 100644 index 000000000..69d51f8b2 --- /dev/null +++ b/src/lib/output/router.ts @@ -0,0 +1,325 @@ +import type { Application } from "../application.js"; +import { + ReflectionKind, + TraverseProperty, + type ProjectReflection, + type Reflection, +} from "../models/index.js"; +import { createNormalizedUrl } from "../utils/html.js"; +import { Option, type TypeDocOptionMap } from "../utils/index.js"; +import { Slugger } from "./themes/default/Slugger.js"; +import { getHierarchyRoots } from "./themes/lib.js"; + +/** + * The type of page which should be rendered. This may be extended in the future. + * @enum + */ +export const PageKind = { + Index: "index", + Reflection: "reflection", + Document: "document", + Hierarchy: "hierarchy", +} as const; +export type PageKind = (typeof PageKind)[keyof typeof PageKind]; + +export interface PageDefinition { + readonly url: string; + readonly kind: PageKind; + readonly model: Reflection; +} + +/** + * Interface which routers must conform to. + */ +export interface Router { + /** + * Should return a list of pages which should be rendered. + * This will be called once per render. + */ + buildPages(project: ProjectReflection): PageDefinition[]; + + /** + * Can be used to check if the reflection can be linked to. + */ + hasUrl(reflection: Reflection): boolean; + + /** + * Get a list of all reflections which can be linked to. + * This is used for creating the search index. + */ + getLinkableReflections(): Reflection[]; + + /** + * Gets an anchor for this reflection within its containing page. + * May be undefined if this reflection owns its own page. + */ + getAnchor(refl: Reflection): string | undefined; + + /** + * Returns true if the reflection has its own page, false if embedded within + * another page. + */ + hasOwnDocument(refl: Reflection): boolean; + + /** + * Should return a URL which when clicked on the page containing `from` + * takes the user to the page/anchor containing `to`. + */ + relativeUrl(from: Reflection, to: Reflection): string; + + /** + * Should return a URL relative to the project base. This is used for + * determining links to items in the assets folder. + */ + baseRelativeUrl(from: Reflection, target: string): string; + + /** + * Get the full URL to the reflection. In TypeDoc's default router this + * is equivalent to `relativeUrl(project, refl)`, but this might not be + * the case for custom routers which place the project somewhere else + * besides `index.html`. + * + * The URL returned by this by the frontend JS when building dynamic URLs + * for the search, full hierarchy, and navigation components. + */ + getFullUrl(refl: Reflection): string; + + /** + * Responsible for getting a slugger for the given reflection. If a + * reflection is not associated with a page, the slugger for the parent + * reflection should be returned instead. + */ + getSlugger(reflection: Reflection): Slugger; +} + +export class DefaultRouter implements Router { + // Note: This will always contain lowercased names to avoid issues with + // case-insensitive file systems. + protected usedFileNames = new Set(); + protected sluggers = new Map(); + protected fullUrls = new Map(); + protected anchors = new Map(); + + // #2386, we get URLs a lot, saving a cache helps. + // ... but does it actually? Retest. + // private fullToRelativeUrlCache = new Map(); + + @Option("sluggerConfiguration") + private accessor sluggerConfiguration!: TypeDocOptionMap["sluggerConfiguration"]; + + @Option("includeHierarchySummary") + private accessor includeHierarchySummary!: boolean; + + constructor(readonly application: Application) {} + + directories = new Map([ + [ReflectionKind.Class, ["classes", PageKind.Reflection]], + [ReflectionKind.Interface, ["interfaces", PageKind.Reflection]], + [ReflectionKind.Enum, ["enums", PageKind.Reflection]], + [ReflectionKind.Namespace, ["modules", PageKind.Reflection]], + [ReflectionKind.Module, ["modules", PageKind.Reflection]], + [ReflectionKind.TypeAlias, ["types", PageKind.Reflection]], + [ReflectionKind.Function, ["functions", PageKind.Reflection]], + [ReflectionKind.Variable, ["variables", PageKind.Reflection]], + [ReflectionKind.Document, ["documents", PageKind.Document]], + ]); + + buildPages(project: ProjectReflection): PageDefinition[] { + this.usedFileNames = new Set(); + this.sluggers = new Map([ + [project, new Slugger(this.sluggerConfiguration)], + ]); + + const pages: PageDefinition[] = []; + + if (project.readme?.length) { + pages.push({ + url: "index.html", + kind: PageKind.Reflection, + model: project, + }); + } else { + pages.push({ + url: "index.html", + kind: PageKind.Index, + model: project, + }); + pages.push({ + url: "modules.html", + kind: PageKind.Reflection, + model: project, + }); + } + + this.fullUrls.set(project, pages[pages.length - 1].url); + + if (this.includeHierarchySummary && getHierarchyRoots(project)) { + pages.push({ + url: "hierarchy.html", + kind: PageKind.Hierarchy, + model: project, + }); + } + + for (const child of project.childrenIncludingDocuments || []) { + this.buildChildPages(child, pages); + } + + return pages; + } + + hasUrl(reflection: Reflection): boolean { + return this.fullUrls.has(reflection); + } + + getLinkableReflections(): Reflection[] { + return Array.from(this.fullUrls.keys()); + } + + getAnchor(refl: Reflection): string | undefined { + return this.anchors.get(refl); + } + + hasOwnDocument(refl: Reflection): boolean { + return this.anchors.get(refl) === undefined && this.hasUrl(refl); + } + + relativeUrl(from: Reflection, to: Reflection): string { + let slashes = 0; + const full = this.getFullUrl(from); + for (let i = 0; i < full.length; ++i) { + if (full[i] === "/") ++slashes; + } + + return "../".repeat(slashes) + this.getFullUrl(to); + } + + baseRelativeUrl(from: Reflection, target: string): string { + let slashes = 0; + const full = this.getFullUrl(from); + for (let i = 0; i < full.length; ++i) { + if (full[i] === "/") ++slashes; + } + + return "../".repeat(slashes) + target; + } + + getFullUrl(refl: Reflection): string { + const url = this.fullUrls.get(refl); + if (!url) { + throw new Error( + `Tried to get a URL of a reflection ${refl.getFullName()} which did not receive a URL`, + ); + } + + return url; + } + + getSlugger(reflection: Reflection): Slugger { + if (this.sluggers.has(reflection)) { + return this.sluggers.get(reflection)!; + } + // A slugger should always be defined at least for the project + return this.getSlugger(reflection.parent!); + } + + protected buildChildPages( + reflection: Reflection, + outPages: PageDefinition[], + ): void { + const mapping = this.directories.get(reflection.kind); + + if (mapping) { + const url = [mapping[0], this.getFileName(reflection)].join("/"); + this.fullUrls.set(reflection, url); + this.sluggers.set( + reflection, + new Slugger(this.sluggerConfiguration), + ); + + outPages.push({ + kind: PageKind.Reflection, + model: reflection, + url, + }); + + reflection.traverse((child) => { + if (child.isDeclaration() || child.isDocument()) { + this.buildChildPages(child, outPages); + } else { + this.buildAnchors(child, reflection); + } + return true; + }); + } else { + this.buildAnchors(reflection, reflection.parent!); + } + } + + protected buildAnchors( + reflection: Reflection, + pageReflection: Reflection, + ): void { + if ( + !reflection.isDeclaration() && + !reflection.isSignature() && + !reflection.isTypeParameter() + ) { + return; + } + + let refl: Reflection | undefined = reflection; + const parts = [refl.name]; + while (refl.parent && refl.parent !== pageReflection) { + refl = refl.parent; + // Avoid duplicate names for signatures + if (parts[0] !== refl.name) { + parts.unshift(refl.name); + } + } + + const anchor = this.getSlugger(pageReflection).slug(parts.join(".")); + + this.fullUrls.set( + reflection, + this.fullUrls.get(pageReflection)! + "#" + anchor, + ); + this.anchors.set(reflection, anchor); + + reflection.traverse((child, prop) => { + switch (prop) { + case TraverseProperty.Children: + case TraverseProperty.GetSignature: + case TraverseProperty.SetSignature: + case TraverseProperty.IndexSignature: + case TraverseProperty.Signatures: + case TraverseProperty.TypeParameter: + this.buildAnchors(child, pageReflection); + } + return true; + }); + } + + protected getFileName(reflection: Reflection): string { + const parts = [createNormalizedUrl(reflection.name)]; + while (reflection.parent && !reflection.parent.isProject()) { + reflection = reflection.parent; + parts.unshift(createNormalizedUrl(reflection.name)); + } + + const baseName = parts.join("."); + const lowerBaseName = baseName.toLocaleLowerCase(); + if (this.usedFileNames.has(lowerBaseName)) { + let index = 1; + while (this.usedFileNames.has(`${lowerBaseName}-${index}`)) { + ++index; + } + + this.usedFileNames.add(`${lowerBaseName}-${index}`); + return `${baseName}-${index}.html`; + } + + this.usedFileNames.add(lowerBaseName); + return `${baseName}.html`; + } +} diff --git a/src/lib/output/theme.ts b/src/lib/output/theme.ts index c44c05a4f..632920931 100644 --- a/src/lib/output/theme.ts +++ b/src/lib/output/theme.ts @@ -1,57 +1,17 @@ import type { Renderer } from "./renderer.js"; -import type { ProjectReflection } from "../models/reflections/project.js"; -import type { RenderTemplate, UrlMapping } from "./models/UrlMapping.js"; import { RendererComponent } from "./components.js"; import type { PageEvent } from "./events.js"; import type { Reflection } from "../models/index.js"; -import type { Slugger } from "./themes/default/Slugger.js"; /** * Base class of all themes. * - * The theme class controls which files will be created through the {@link Theme.getUrls} - * function. It returns an array of {@link UrlMapping} instances defining the target files, models - * and templates to use. Additionally themes can subscribe to the events emitted by - * {@link Renderer} to control and manipulate the output process. + * The theme class determines how a page is rendered. It is loosely coupled with a router + * class instance which is also created by the {@link Renderer} class. */ export abstract class Theme extends RendererComponent { - private sluggers = new Map(); - - /** - * Map the models of the given project to the desired output files. - * It is assumed that with the project structure: - * ```text - * A - * |- B - * |- C - * ``` - * If `B` has a UrlMapping, then `A` also has a UrlMapping, and `C` may or - * may not have a UrlMapping. If `B` does not have a UrlMapping, then `A` - * may or may not have a UrlMapping, but `C` must not have a UrlMapping. - * - * @param project The project whose urls should be generated. - * @returns A list of {@link UrlMapping} instances defining which models - * should be rendered to which files. - */ - abstract getUrls(project: ProjectReflection): UrlMapping[]; - /** * Renders the provided page to a string, which will be written to disk by the {@link Renderer} */ - abstract render( - page: PageEvent, - template: RenderTemplate>, - ): string; - - setSlugger(reflection: Reflection, slugger: Slugger) { - this.sluggers.set(reflection, slugger); - } - - getSlugger(reflection: Reflection): Slugger { - if (this.sluggers.has(reflection)) { - return this.sluggers.get(reflection)!; - } - // A slugger should always be defined at least for the project - return this.getSlugger(reflection.parent!); - } + abstract render(event: PageEvent): string; } diff --git a/src/lib/output/themes/MarkedPlugin.tsx b/src/lib/output/themes/MarkedPlugin.tsx index 9dd5e1b64..ca58fe69e 100644 --- a/src/lib/output/themes/MarkedPlugin.tsx +++ b/src/lib/output/themes/MarkedPlugin.tsx @@ -215,7 +215,7 @@ export class MarkedPlugin extends ContextAwareRendererComponent { private onEnd() { for (const { source, target, link } of this.renderedRelativeLinks) { - const slugger = this.owner.theme!.getSlugger(target); + const slugger = this.owner.router!.getSlugger(target); if (!slugger.hasAnchor(link.targetAnchor!)) { this.application.logger.warn( this.application.i18n.reflection_0_links_to_1_but_anchor_does_not_exist_try_2( @@ -245,7 +245,7 @@ export class MarkedPlugin extends ContextAwareRendererComponent { } private getSlugger() { - return this.owner.theme!.getSlugger(this.page!.model); + return this.owner.router!.getSlugger(this.page!.model); } /** diff --git a/src/lib/output/themes/default/DefaultTheme.tsx b/src/lib/output/themes/default/DefaultTheme.tsx index 73020e458..196c95a47 100644 --- a/src/lib/output/themes/default/DefaultTheme.tsx +++ b/src/lib/output/themes/default/DefaultTheme.tsx @@ -2,48 +2,24 @@ import { Theme } from "../../theme.js"; import type { Renderer } from "../../renderer.js"; import { ReflectionKind, - ProjectReflection, + type ProjectReflection, type ContainerReflection, - DeclarationReflection, + type DeclarationReflection, type Reflection, - SignatureReflection, ReflectionCategory, ReflectionGroup, - TypeParameterReflection, type DocumentReflection, ReferenceReflection, } from "../../../models/index.js"; -import { type RenderTemplate, UrlMapping } from "../../models/UrlMapping.js"; +import { type RenderTemplate } from "../../models/UrlMapping.js"; import type { PageEvent } from "../../events.js"; import type { MarkedPlugin } from "../../plugins/index.js"; import { DefaultThemeRenderContext } from "./DefaultThemeRenderContext.js"; -import { filterMap, JSX, Option, type TypeDocOptionMap } from "../../../utils/index.js"; -import { classNames, getDisplayName, getHierarchyRoots, toStyleClass } from "../lib.js"; +import { filterMap, JSX } from "../../../utils/index.js"; +import { classNames, getDisplayName, toStyleClass } from "../lib.js"; import { icons } from "./partials/icon.js"; -import { Slugger } from "./Slugger.js"; import { createNormalizedUrl } from "../../../utils/html.js"; - -/** - * Defines a mapping of a {@link Models.Kind} to a template file. - * - * Used by {@link DefaultTheme} to map reflections to output files. - */ -interface TemplateMapping { - /** - * {@link DeclarationReflection.kind} this rule applies to. - */ - kind: ReflectionKind[]; - - /** - * The name of the directory the output files should be written to. - */ - directory: string; - - /** - * The name of the template that should be used to render the reflection. - */ - template: RenderTemplate>; -} +import { PageKind, type Router } from "../../router.js"; export interface NavigationElement { text: string; @@ -58,12 +34,11 @@ export class DefaultTheme extends Theme { // case-insensitive file systems. usedFileNames = new Set(); - @Option("sluggerConfiguration") - private accessor sluggerConfiguration!: TypeDocOptionMap["sluggerConfiguration"]; - /** @internal */ markedPlugin: MarkedPlugin; + router: Router; + /** * The icons which will actually be rendered. The source of truth lives on the theme, and * the {@link DefaultThemeRenderContext.icons} member will produce references to these. @@ -79,8 +54,10 @@ export class DefaultTheme extends Theme { */ icons = { ...icons }; + ContextClass = DefaultThemeRenderContext; + getRenderContext(pageEvent: PageEvent) { - return new DefaultThemeRenderContext(this, pageEvent, this.application.options); + return new this.ContextClass(this.router, this, pageEvent, this.application.options); } documentTemplate = (pageEvent: PageEvent) => { @@ -104,54 +81,6 @@ export class DefaultTheme extends Theme { return getReflectionClasses(reflection, filters); } - /** - * Mappings of reflections kinds to templates used by this theme. - */ - private mappings: TemplateMapping[] = [ - { - kind: [ReflectionKind.Class], - directory: "classes", - template: this.reflectionTemplate, - }, - { - kind: [ReflectionKind.Interface], - directory: "interfaces", - template: this.reflectionTemplate, - }, - { - kind: [ReflectionKind.Enum], - directory: "enums", - template: this.reflectionTemplate, - }, - { - kind: [ReflectionKind.Namespace, ReflectionKind.Module], - directory: "modules", - template: this.reflectionTemplate, - }, - { - kind: [ReflectionKind.TypeAlias], - directory: "types", - template: this.reflectionTemplate, - }, - { - kind: [ReflectionKind.Function], - directory: "functions", - template: this.reflectionTemplate, - }, - { - kind: [ReflectionKind.Variable], - directory: "variables", - template: this.reflectionTemplate, - }, - { - kind: [ReflectionKind.Document], - directory: "documents", - template: this.documentTemplate, - }, - ]; - - static URL_PREFIX = /^(http|ftp)s?:\/\//; - /** * Create a new DefaultTheme instance. * @@ -160,36 +89,7 @@ export class DefaultTheme extends Theme { constructor(renderer: Renderer) { super(renderer); this.markedPlugin = renderer.markedPlugin; - } - - /** - * Map the models of the given project to the desired output files. - * - * @param project The project whose urls should be generated. - * @returns A list of {@link UrlMapping} instances defining which models - * should be rendered to which files. - */ - getUrls(project: ProjectReflection): UrlMapping[] { - this.usedFileNames = new Set(); - const urls: UrlMapping[] = []; - this.setSlugger(project, new Slugger(this.sluggerConfiguration)); - - if (!project.readme?.length) { - project.url = "index.html"; - urls.push(new UrlMapping("index.html", project, this.reflectionTemplate)); - } else { - project.url = "modules.html"; - urls.push(new UrlMapping("modules.html", project, this.reflectionTemplate)); - urls.push(new UrlMapping("index.html", project, this.indexTemplate)); - } - - if (this.application.options.getValue("includeHierarchySummary") && getHierarchyRoots(project).length) { - urls.push(new UrlMapping("hierarchy.html", project, this.hierarchyTemplate)); - } - - project.childrenIncludingDocuments?.forEach((child) => this.buildUrls(child, urls)); - - return urls; + this.router = renderer.router!; } /** @@ -218,51 +118,14 @@ export class DefaultTheme extends Theme { return `${baseName}.html`; } - /** - * Return the template mapping for the given reflection. - * - * @param reflection The reflection whose mapping should be resolved. - * @returns The found mapping or undefined if no mapping could be found. - */ - private getMapping(reflection: DeclarationReflection | DocumentReflection): TemplateMapping | undefined { - return this.mappings.find((mapping) => reflection.kindOf(mapping.kind)); - } - - /** - * Build the url for the the given reflection and all of its children. - * - * @param reflection The reflection the url should be created for. - * @param urls The array the url should be appended to. - * @returns The altered urls array. - */ - buildUrls(reflection: DeclarationReflection | DocumentReflection, urls: UrlMapping[]): UrlMapping[] { - const mapping = this.getMapping(reflection); - if (mapping) { - if (!reflection.url || !DefaultTheme.URL_PREFIX.test(reflection.url)) { - const url = [mapping.directory, this.getFileName(reflection)].join("/"); - urls.push(new UrlMapping(url, reflection, mapping.template)); - this.setSlugger(reflection, new Slugger(this.sluggerConfiguration)); - - reflection.url = url; - reflection.hasOwnDocument = true; - } - - reflection.traverse((child) => { - if (child.isDeclaration() || child.isDocument()) { - this.buildUrls(child, urls); - } else { - this.applyAnchorUrl(child, reflection); - } - return true; - }); - } else if (reflection.parent) { - this.applyAnchorUrl(reflection, reflection.parent); - } - - return urls; - } + render(page: PageEvent): string { + const template = { + [PageKind.Index]: this.indexTemplate, + [PageKind.Document]: this.documentTemplate, + [PageKind.Hierarchy]: this.hierarchyTemplate, + [PageKind.Reflection]: this.reflectionTemplate, + }[page.pageKind] as RenderTemplate>; - render(page: PageEvent, template: RenderTemplate>): string { const templateOutput = this.defaultLayoutTemplate(page, template); return "" + JSX.renderElement(templateOutput) + "\n"; } @@ -285,6 +148,7 @@ export class DefaultTheme extends Theme { buildNavigation(project: ProjectReflection): NavigationElement[] { // eslint-disable-next-line @typescript-eslint/no-this-alias const theme = this; + const router = this.router; const opts = this.application.options.getValue("navigation"); const leaves = this.application.options.getValue("navigationLeaves"); @@ -311,7 +175,7 @@ export class DefaultTheme extends Theme { return { text: getDisplayName(element), - path: element.url, + path: router.getFullUrl(element), kind: element.kind & ReflectionKind.Project ? undefined : element.kind, class: classNames({ deprecated: element.isDeprecated() }, theme.getReflectionClasses(element)), children: children?.length ? children : undefined, @@ -410,7 +274,7 @@ export class DefaultTheme extends Theme { // Note: This might end up putting a module within another module if we document // both foo/index.ts and foo/bar.ts. - for (const child of children.filter((c) => c.hasOwnDocument)) { + for (const child of children.filter((c) => router.hasOwnDocument(c))) { const nav = toNavigation(child); if (nav) { const parts = child.name.split("/"); @@ -443,45 +307,6 @@ export class DefaultTheme extends Theme { return result; } } - - /** - * Generate an anchor url for the given reflection and all of its children. - * - * @param reflection The reflection an anchor url should be created for. - * @param container The nearest reflection having an own document. - */ - applyAnchorUrl(reflection: Reflection, container: Reflection) { - if ( - !(reflection instanceof DeclarationReflection) && - !(reflection instanceof SignatureReflection) && - !(reflection instanceof TypeParameterReflection) - ) { - return; - } - - if (!reflection.url || !DefaultTheme.URL_PREFIX.test(reflection.url)) { - let refl: Reflection | undefined = reflection; - const parts = [refl.name]; - while (refl.parent && refl.parent !== container && !(reflection.parent instanceof ProjectReflection)) { - refl = refl.parent; - // Avoid duplicate names for signatures - if (parts[0] !== refl.name) { - parts.unshift(refl.name); - } - } - - const anchor = this.getSlugger(reflection).slug(parts.join(".")); - - reflection.url = container.url! + "#" + anchor; - reflection.anchor = anchor; - reflection.hasOwnDocument = false; - } - - reflection.traverse((child) => { - this.applyAnchorUrl(child, container); - return true; - }); - } } function getReflectionClasses( diff --git a/src/lib/output/themes/default/DefaultThemeRenderContext.ts b/src/lib/output/themes/default/DefaultThemeRenderContext.ts index d5634fea3..9c71a05bb 100644 --- a/src/lib/output/themes/default/DefaultThemeRenderContext.ts +++ b/src/lib/output/themes/default/DefaultThemeRenderContext.ts @@ -1,6 +1,7 @@ import type { PageEvent, Renderer } from "../../index.js"; import type { Internationalization, + TranslatedString, TranslationProxy, } from "../../../internationalization/internationalization.js"; import type { @@ -13,7 +14,7 @@ import { type NeverIfInternal, type Options } from "../../../utils/index.js"; import type { DefaultTheme } from "./DefaultTheme.js"; import { defaultLayout } from "./layouts/default.js"; import { index } from "./partials/index.js"; -import { breadcrumb } from "./partials/breadcrumb.js"; +import { breadcrumbs } from "./partials/breadcrumb.js"; import { commentShortSummary, commentSummary, @@ -59,6 +60,7 @@ import { moduleMemberSummary, moduleReflection, } from "./partials/moduleReflection.js"; +import type { Router } from "../../router.js"; function bind(fn: (f: F, ...a: L) => R, first: F) { return (...r: L) => fn(first, ...r); @@ -70,15 +72,19 @@ export class DefaultThemeRenderContext { internationalization: Internationalization; i18n: TranslationProxy; + model: Reflection; + constructor( + readonly router: Router, readonly theme: DefaultTheme, public page: PageEvent, options: Options, ) { + this._refIcons = buildRefIcons(theme.icons, this); this.options = options; this.internationalization = theme.application.internationalization; this.i18n = this.internationalization.proxy; - this._refIcons = buildRefIcons(theme.icons, this); + this.model = page.model; } /** @@ -92,7 +98,7 @@ export class DefaultThemeRenderContext { } get slugger() { - return this.theme.getSlugger(this.page.model); + return this.router.getSlugger(this.page.model); } hook: Renderer["hooks"]["emit"] = (...params) => { @@ -101,15 +107,33 @@ export class DefaultThemeRenderContext { /** Avoid this in favor of urlTo if possible */ relativeURL = (url: string, cacheBust = false) => { - const result = this.theme.markedPlugin.getRelativeUrl(url); + const result = this.theme.router!.baseRelativeUrl(this.page.model, url); if (cacheBust && this.theme.owner.cacheBust) { return result + `?cache=${this.theme.owner.renderStartTime}`; } return result; }; - urlTo = (reflection: Reflection) => { - return reflection.url ? this.relativeURL(reflection.url) : ""; + getAnchor = (reflection: Reflection): string | undefined => { + const anchor = this.router.getAnchor(reflection); + if (!anchor) { + // This will go to debug level before release + this.theme.application.logger.warn( + `${reflection.getFullName()} does not have an anchor but one was requested when generating page for ${this.model.getFullName()}, this is a bug` as TranslatedString, + ); + } + return anchor; + }; + + urlTo = (reflection: Reflection): string | undefined => { + if (this.router.hasUrl(reflection)) { + return this.router.relativeUrl(this.page.model, reflection); + } + // This will go to debug level before release + this.theme.application.logger.warn( + `${reflection.getFullName()} does not have a URL but was linked to when generating page for ${this.model.getFullName()}, this is a bug` as TranslatedString, + ); + return undefined; }; markdown = ( @@ -160,7 +184,7 @@ export class DefaultThemeRenderContext { */ typeDeclaration = bind(typeDeclaration, this); - breadcrumb = bind(breadcrumb, this); + breadcrumbs = bind(breadcrumbs, this); commentShortSummary = bind(commentShortSummary, this); commentSummary = bind(commentSummary, this); commentTags = bind(commentTags, this); diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index 592bf0d12..ec4608cd5 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -53,8 +53,11 @@ export function initSearch() { if (!searchEl) return; const state: SearchState = { - base: document.documentElement.dataset.base! + "/", + base: document.documentElement.dataset.base!, }; + if (!state.base.endsWith("/")) { + state.base += "/"; + } const searchScript = document.getElementById( "tsd-search-script", diff --git a/src/lib/output/themes/default/layouts/default.tsx b/src/lib/output/themes/default/layouts/default.tsx index 27320f3da..7f7614850 100644 --- a/src/lib/output/themes/default/layouts/default.tsx +++ b/src/lib/output/themes/default/layouts/default.tsx @@ -37,7 +37,7 @@ function buildSiteMetadata(context: DefaultThemeRenderContext) { html={JSON.stringify({ "@context": "https://schema.org", "@type": "WebSite", - name: context.page.project.name, + name: context.model.project.name, url: url.toString(), })} /> diff --git a/src/lib/output/themes/default/partials/anchor-icon.tsx b/src/lib/output/themes/default/partials/anchor-icon.tsx index d6ff3fed0..83c69ef7a 100644 --- a/src/lib/output/themes/default/partials/anchor-icon.tsx +++ b/src/lib/output/themes/default/partials/anchor-icon.tsx @@ -1,3 +1,4 @@ +import type { Reflection } from "../../../../models/index.js"; import { JSX } from "../../../../utils/index.js"; import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext.js"; @@ -10,3 +11,15 @@ export function anchorIcon(context: DefaultThemeRenderContext, anchor: string | ); } + +export function anchorLink(anchor: string | undefined) { + if (!anchor) return <>; + + return ; +} + +export function anchorLinkIfPresent(context: DefaultThemeRenderContext, refl: Reflection) { + if (!context.router.hasUrl(refl)) return <>; + + return anchorLink(context.router.getAnchor(refl)); +} diff --git a/src/lib/output/themes/default/partials/breadcrumb.tsx b/src/lib/output/themes/default/partials/breadcrumb.tsx index 42130e568..36f38504d 100644 --- a/src/lib/output/themes/default/partials/breadcrumb.tsx +++ b/src/lib/output/themes/default/partials/breadcrumb.tsx @@ -2,14 +2,21 @@ import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext.js" import { JSX } from "../../../../utils/index.js"; import type { Reflection } from "../../../../models/index.js"; -export const breadcrumb = (context: DefaultThemeRenderContext, props: Reflection): JSX.Element | undefined => - props.parent ? ( - <> - {context.breadcrumb(props.parent)} -
  • {props.url ? {props.name} : {props.name}}
  • - - ) : props.url ? ( -
  • - {props.name} -
  • - ) : undefined; +export function breadcrumbs(context: DefaultThemeRenderContext, props: Reflection): JSX.Element { + const path: Reflection[] = []; + let refl: Reflection = props; + while (refl.parent) { + path.push(refl); + refl = refl.parent; + } + + return ( +
      + {path.reverse().map((r) => ( +
    • + {r.name} +
    • + ))} +
    + ); +} diff --git a/src/lib/output/themes/default/partials/comment.tsx b/src/lib/output/themes/default/partials/comment.tsx index 164f3e5e7..141a3de12 100644 --- a/src/lib/output/themes/default/partials/comment.tsx +++ b/src/lib/output/themes/default/partials/comment.tsx @@ -1,7 +1,7 @@ import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext.js"; import { JSX, Raw } from "../../../../utils/index.js"; import { type CommentDisplayPart, type Reflection, ReflectionKind } from "../../../../models/index.js"; -import { anchorIcon } from "./anchor-icon.js"; +import { anchorIcon, anchorLink } from "./anchor-icon.js"; import { join } from "../../lib.js"; // Note: Comment modifiers are handled in `renderFlags` @@ -81,7 +81,7 @@ export function commentTags(context: DefaultThemeRenderContext, props: Reflectio <>
    diff --git a/src/lib/output/themes/default/partials/header.tsx b/src/lib/output/themes/default/partials/header.tsx index 1d562d0d1..1fd0931f5 100644 --- a/src/lib/output/themes/default/partials/header.tsx +++ b/src/lib/output/themes/default/partials/header.tsx @@ -31,7 +31,7 @@ export const header = (context: DefaultThemeRenderContext, props: PageEvent - {renderBreadcrumbs &&
      {context.breadcrumb(props.model)}
    } + {renderBreadcrumbs && context.breadcrumbs(props.model)} {renderTitle && (

    {titleKindString} diff --git a/src/lib/output/themes/default/partials/hierarchy.tsx b/src/lib/output/themes/default/partials/hierarchy.tsx index d65e53a25..68d061046 100644 --- a/src/lib/output/themes/default/partials/hierarchy.tsx +++ b/src/lib/output/themes/default/partials/hierarchy.tsx @@ -23,7 +23,7 @@ export function hierarchy(context: DefaultThemeRenderContext, typeHierarchy: Dec <> {" "} ( - + {context.i18n.theme_hierarchy_view_summary()} ) @@ -33,7 +33,7 @@ export function hierarchy(context: DefaultThemeRenderContext, typeHierarchy: Dec ); return ( -
    +

    {context.i18n.theme_hierarchy()} {summaryLink} diff --git a/src/lib/output/themes/default/partials/index.tsx b/src/lib/output/themes/default/partials/index.tsx index 57480966d..0139baf06 100644 --- a/src/lib/output/themes/default/partials/index.tsx +++ b/src/lib/output/themes/default/partials/index.tsx @@ -50,35 +50,19 @@ export function index(context: DefaultThemeRenderContext, props: ContainerReflec ); } - // Accordion is only needed if any children don't have their own document. - if ( - [...(props.groups ?? []), ...(props.categories ?? [])].some( - (category) => !category.allChildrenHaveOwnDocument(), - ) - ) { - content = ( -
    - - - -
    {content}
    -
    - ); - } else { - content = ( - <> -

    {context.i18n.theme_index()}

    - {content} - - ); - } - return ( <>
    -
    {content}
    +
    +
    + + + +
    {content}
    +
    +
    ); diff --git a/src/lib/output/themes/default/partials/member.declaration.tsx b/src/lib/output/themes/default/partials/member.declaration.tsx index fb0a8f87a..59571349a 100644 --- a/src/lib/output/themes/default/partials/member.declaration.tsx +++ b/src/lib/output/themes/default/partials/member.declaration.tsx @@ -5,7 +5,7 @@ import { hasTypeParameters } from "../../lib.js"; import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext.js"; export function memberDeclaration(context: DefaultThemeRenderContext, props: DeclarationReflection) { - const builder = new FormattedCodeBuilder(context.urlTo); + const builder = new FormattedCodeBuilder(context.router, context.model); const content: FormatterNode[] = []; builder.member(content, props, { topLevelLinks: false }); const generator = new FormattedCodeGenerator(context.options.getValue("typePrintWidth")); diff --git a/src/lib/output/themes/default/partials/member.getterSetter.tsx b/src/lib/output/themes/default/partials/member.getterSetter.tsx index e4bfad1c2..718053a99 100644 --- a/src/lib/output/themes/default/partials/member.getterSetter.tsx +++ b/src/lib/output/themes/default/partials/member.getterSetter.tsx @@ -15,7 +15,7 @@ export const memberGetterSetter = (context: DefaultThemeRenderContext, props: De > {!!props.getSignature && ( <> -
  • +
  • {context.memberSignatureTitle(props.getSignature)}
  • {context.memberSignatureBody(props.getSignature)}
  • @@ -23,7 +23,7 @@ export const memberGetterSetter = (context: DefaultThemeRenderContext, props: De )} {!!props.setSignature && ( <> -
  • +
  • {context.memberSignatureTitle(props.setSignature)}
  • {context.memberSignatureBody(props.setSignature)}
  • diff --git a/src/lib/output/themes/default/partials/member.signature.title.tsx b/src/lib/output/themes/default/partials/member.signature.title.tsx index 002369feb..1173b81e4 100644 --- a/src/lib/output/themes/default/partials/member.signature.title.tsx +++ b/src/lib/output/themes/default/partials/member.signature.title.tsx @@ -7,7 +7,7 @@ export function memberSignatureTitle( props: SignatureReflection, options: { hideName?: boolean } = {}, ) { - const builder = new FormattedCodeBuilder(context.urlTo); + const builder = new FormattedCodeBuilder(context.router, context.model); const tree = builder.signature(props, options); const generator = new FormattedCodeGenerator(context.options.getValue("typePrintWidth")); generator.node(tree, Wrap.Detect); diff --git a/src/lib/output/themes/default/partials/member.signatures.tsx b/src/lib/output/themes/default/partials/member.signatures.tsx index 99e9bff3c..9d0caf380 100644 --- a/src/lib/output/themes/default/partials/member.signatures.tsx +++ b/src/lib/output/themes/default/partials/member.signatures.tsx @@ -1,7 +1,7 @@ import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext.js"; import { JSX } from "../../../../utils/index.js"; import type { DeclarationReflection } from "../../../../models/index.js"; -import { anchorIcon } from "./anchor-icon.js"; +import { anchorIcon, anchorLink } from "./anchor-icon.js"; import { classNames } from "../../lib.js"; export const memberSignatures = (context: DefaultThemeRenderContext, props: DeclarationReflection) => ( @@ -10,9 +10,9 @@ export const memberSignatures = (context: DefaultThemeRenderContext, props: Decl {props.signatures?.map((item) => ( <>
  • {context.memberSignatureBody(item)}
  • diff --git a/src/lib/output/themes/default/partials/member.tsx b/src/lib/output/themes/default/partials/member.tsx index ad5d60b2d..dbbe3db0e 100644 --- a/src/lib/output/themes/default/partials/member.tsx +++ b/src/lib/output/themes/default/partials/member.tsx @@ -2,11 +2,13 @@ import { classNames, getDisplayName, wbr } from "../../lib.js"; import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext.js"; import { JSX, Raw } from "../../../../utils/index.js"; import { type DeclarationReflection, type DocumentReflection } from "../../../../models/index.js"; -import { anchorIcon } from "./anchor-icon.js"; +import { anchorIcon, anchorLink } from "./anchor-icon.js"; export function member(context: DefaultThemeRenderContext, props: DeclarationReflection | DocumentReflection) { + const anchor = context.getAnchor(props); + context.page.pageHeadings.push({ - link: `#${props.anchor}`, + link: `#${anchor}`, text: getDisplayName(props), kind: props.kind, classes: context.getReflectionClasses(props), @@ -17,12 +19,12 @@ export function member(context: DefaultThemeRenderContext, props: DeclarationRef if (props.isDocument()) { return (
    - + {anchorLink(anchor)} {!!props.name && ( )}
    @@ -34,12 +36,12 @@ export function member(context: DefaultThemeRenderContext, props: DeclarationRef return (
    - + {anchorLink(anchor)} {!!props.name && ( )} {props.signatures @@ -48,7 +50,9 @@ export function member(context: DefaultThemeRenderContext, props: DeclarationRef ? context.memberGetterSetter(props) : context.memberDeclaration(props)} - {props.groups?.map((item) => item.children.map((item) => !item.hasOwnDocument && context.member(item)))} + {props.groups?.map((item) => + item.children.map((item) => !context.router.hasOwnDocument(item) && context.member(item)), + )}
    ); } diff --git a/src/lib/output/themes/default/partials/members.tsx b/src/lib/output/themes/default/partials/members.tsx index e8b06a026..2d137b34b 100644 --- a/src/lib/output/themes/default/partials/members.tsx +++ b/src/lib/output/themes/default/partials/members.tsx @@ -4,7 +4,7 @@ import { type ContainerReflection } from "../../../../models/index.js"; import { getMemberSections } from "../../lib.js"; export function members(context: DefaultThemeRenderContext, props: ContainerReflection) { - const sections = getMemberSections(props, (child) => !child.hasOwnDocument); + const sections = getMemberSections(props, (child) => !context.router.hasOwnDocument(child)); return ( <> diff --git a/src/lib/output/themes/default/partials/moduleReflection.tsx b/src/lib/output/themes/default/partials/moduleReflection.tsx index 281fcad60..db1d8f7cc 100644 --- a/src/lib/output/themes/default/partials/moduleReflection.tsx +++ b/src/lib/output/themes/default/partials/moduleReflection.tsx @@ -9,7 +9,7 @@ import { import { JSX, Raw } from "../../../../utils/index.js"; import { classNames, getDisplayName, getMemberSections, getUniquePath, join } from "../../lib.js"; import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext.js"; -import { anchorIcon } from "./anchor-icon.js"; +import { anchorIcon, anchorLink } from "./anchor-icon.js"; export function moduleReflection(context: DefaultThemeRenderContext, mod: DeclarationReflection | ProjectReflection) { const sections = getMemberSections(mod); @@ -94,7 +94,7 @@ export function moduleMemberSummary( return ( <>
    - + {anchorLink(id)} {name}
    diff --git a/src/lib/output/themes/default/partials/reflectionPreview.tsx b/src/lib/output/themes/default/partials/reflectionPreview.tsx index abcbc7ffe..a27ba2087 100644 --- a/src/lib/output/themes/default/partials/reflectionPreview.tsx +++ b/src/lib/output/themes/default/partials/reflectionPreview.tsx @@ -10,7 +10,7 @@ export function reflectionPreview(context: DefaultThemeRenderContext, props: Ref // a type-like object with links to each member. Don't do this if we don't have any children as it will // generate a broken looking interface. (See TraverseCallback) if (props.kindOf(ReflectionKind.Interface) && props.children) { - const builder = new FormattedCodeBuilder(context.urlTo); + const builder = new FormattedCodeBuilder(context.router, context.model); const tree = builder.interface(props); const generator = new FormattedCodeGenerator(context.options.getValue("typePrintWidth")); generator.forceWrap(builder.forceWrap); // Ensure elements are added to new lines. diff --git a/src/lib/output/themes/default/partials/type.tsx b/src/lib/output/themes/default/partials/type.tsx index 3017d7381..cbbb6f41c 100644 --- a/src/lib/output/themes/default/partials/type.tsx +++ b/src/lib/output/themes/default/partials/type.tsx @@ -7,7 +7,7 @@ export function type( type: SomeType | undefined, options: { topLevelLinks: boolean } = { topLevelLinks: false }, ) { - const builder = new FormattedCodeBuilder(context.urlTo); + const builder = new FormattedCodeBuilder(context.router, context.model); const tree = builder.type(type, TypeContext.none, options); const generator = new FormattedCodeGenerator(context.options.getValue("typePrintWidth")); generator.node(tree, Wrap.Detect); diff --git a/src/lib/output/themes/default/partials/typeAndParent.tsx b/src/lib/output/themes/default/partials/typeAndParent.tsx index 3bb156a88..2b840849d 100644 --- a/src/lib/output/themes/default/partials/typeAndParent.tsx +++ b/src/lib/output/themes/default/partials/typeAndParent.tsx @@ -14,12 +14,11 @@ export const typeAndParent = (context: DefaultThemeRenderContext, props: Type): if (props instanceof ReferenceType && props.reflection) { const refl = props.reflection instanceof SignatureReflection ? props.reflection.parent : props.reflection; - const parent = refl.parent; + const parent = refl.parent!; return ( <> - {parent?.url ? {parent.name} : parent?.name}. - {refl.url ? {refl.name} : refl.name} + {{parent.name}}.{{refl.name}} ); } diff --git a/src/lib/output/themes/default/partials/typeDetails.tsx b/src/lib/output/themes/default/partials/typeDetails.tsx index bf92fe808..1993b4095 100644 --- a/src/lib/output/themes/default/partials/typeDetails.tsx +++ b/src/lib/output/themes/default/partials/typeDetails.tsx @@ -9,6 +9,7 @@ import type { ReferenceType, SomeType, TypeVisitor } from "../../../../models/ty import { JSX } from "../../../../utils/index.js"; import { classNames, getKindClass } from "../../lib.js"; import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext.js"; +import { anchorLinkIfPresent } from "./anchor-icon.js"; const isUsefulVisitor: Partial> = { array(type) { @@ -179,20 +180,24 @@ function declarationDetails( {declaration.signatures && (
    • - {declaration.signatures.map((item) => ( - <> -
    • - {context.memberSignatureTitle(item, { - hideName: true, - })} -
    • -
    • - {context.memberSignatureBody(item, { - hideSources: true, - })} -
    • - - ))} + {declaration.signatures.map((item) => { + const anchor = context.router.hasUrl(item) ? context.getAnchor(item) : undefined; + + return ( + <> +
    • + {context.memberSignatureTitle(item, { + hideName: true, + })} +
    • +
    • + {context.memberSignatureBody(item, { + hideSources: true, + })} +
    • + + ); + })}
  • )} @@ -215,7 +220,7 @@ function renderChild(
    {!!child.flags.isRest && ...} {child.name} - + {anchorLinkIfPresent(context, child)} {!!child.flags.isOptional && "?"}: function
    @@ -245,7 +250,7 @@ function renderChild( {context.reflectionFlags(child)} {!!child.flags.isRest && ...} {child.name} - + {anchorLinkIfPresent(context, child)} {!!child.flags.isOptional && "?"} {": "} @@ -271,7 +276,7 @@ function renderChild( {context.reflectionFlags(child.getSignature)} get {child.name} - + {anchorLinkIfPresent(context, child)} (): {context.type(child.getSignature.type)}

    @@ -285,7 +290,7 @@ function renderChild( {context.reflectionFlags(child.setSignature)} set {child.name} - {!child.getSignature && } + {!child.getSignature && anchorLinkIfPresent(context, child)} ( {child.setSignature.parameters?.map((item) => ( <> diff --git a/src/lib/output/themes/default/partials/typeParameters.tsx b/src/lib/output/themes/default/partials/typeParameters.tsx index 3bfb507cb..c5a42b068 100644 --- a/src/lib/output/themes/default/partials/typeParameters.tsx +++ b/src/lib/output/themes/default/partials/typeParameters.tsx @@ -1,6 +1,7 @@ import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext.js"; import type { TypeParameterReflection } from "../../../../models/index.js"; import { JSX } from "../../../../utils/index.js"; +import { anchorLinkIfPresent } from "./anchor-icon.js"; export function typeParameters(context: DefaultThemeRenderContext, typeParameters: TypeParameterReflection[]) { return ( @@ -11,7 +12,7 @@ export function typeParameters(context: DefaultThemeRenderContext, typeParameter {typeParameters.map((item) => (
  • - + {anchorLinkIfPresent(context, item)} {item.flags.isConst && const } {item.varianceModifier && ( {item.varianceModifier} diff --git a/src/lib/output/themes/default/templates/hierarchy.tsx b/src/lib/output/themes/default/templates/hierarchy.tsx index faeae1488..22f13aa4e 100644 --- a/src/lib/output/themes/default/templates/hierarchy.tsx +++ b/src/lib/output/themes/default/templates/hierarchy.tsx @@ -3,6 +3,7 @@ import type { PageEvent } from "../../../events.js"; import { JSX } from "../../../../utils/index.js"; import { getHierarchyRoots } from "../../lib.js"; import type { DeclarationReflection, ProjectReflection } from "../../../../models/index.js"; +import { anchorLink } from "../partials/anchor-icon.js"; function fullHierarchy( context: DefaultThemeRenderContext, @@ -33,7 +34,7 @@ function fullHierarchy( // Full name should be safe here, since this list only includes classes/interfaces. return (
  • - + {anchorLink(root.getFullName())} {context.icons[root.kind]()} {root.name} diff --git a/src/lib/utils/perf.ts b/src/lib/utils/perf.ts index de6cc92f7..0a00c9f89 100644 --- a/src/lib/utils/perf.ts +++ b/src/lib/utils/perf.ts @@ -34,8 +34,9 @@ export function bench any>( end(); return res; }, - () => { + (reason) => { end(); + throw reason; }, ); } else { @@ -46,11 +47,32 @@ export function bench any>( } as any; } -export function Bench any>( +function BenchField any>( + _value: undefined, + context: ClassFieldDecoratorContext, +): (value: T) => T { + let runner: T | undefined; + + return function (this: any, value: T) { + if (!runner) { + const className = context.static + ? this.name + : Object.getPrototypeOf(this).constructor.name; + runner = bench(value, `${className}.${String(context.name)}`); + } + + return function (this: any, ...args: any) { + return runner!.apply(this, args); + } as T; + }; +} + +function BenchMethod any>( value: T, context: ClassMethodDecoratorContext, ): T { let runner: T | undefined; + return function (this: any, ...args: any) { if (!runner) { const className = context.static @@ -62,6 +84,16 @@ export function Bench any>( } as any; } +export const Bench: typeof BenchField & typeof BenchMethod = ( + value: any, + context, +) => { + if (context.kind === "field") { + return BenchField(value, context); + } + return BenchMethod(value, context); +}; + export function measure(cb: () => T): T { return bench(cb, "measure()")(); } diff --git a/src/test/converter2/behavior/benchmark.ts b/src/test/converter2/behavior/benchmark.ts new file mode 100644 index 000000000..34cf63e1a --- /dev/null +++ b/src/test/converter2/behavior/benchmark.ts @@ -0,0 +1,54 @@ +export { default as assert } from "assert" +export { default as "assert/strict" } from "assert/strict" +export { default as async_hooks } from "async_hooks" +export { default as buffer } from "buffer" +export { default as child_process } from "child_process" +export { default as cluster } from "cluster" +export { default as console } from "console" +export { default as constants } from "constants" +export { default as crypto } from "crypto" +export { default as dgram } from "dgram" +export { default as diagnostics_channel } from "diagnostics_channel" +export { default as dns } from "dns" +export { default as "dns/promises" } from "dns/promises" +export { default as domain } from "domain" +export { default as events } from "events" +export { default as fs } from "fs" +export { default as "fs/promises" } from "fs/promises" +export { default as http } from "http" +export { default as http2 } from "http2" +export { default as https } from "https" +export { default as inspector } from "inspector" +// export { default as "inspector/promises" } from "inspector/promises" +export { default as module } from "module" +export { default as net } from "net" +export { default as os } from "os" +export { default as path } from "path" +export { default as "path/posix" } from "path/posix" +export { default as "path/win32" } from "path/win32" +export { default as perf_hooks } from "perf_hooks" +export { default as process } from "process" +export { default as punycode } from "punycode" +export { default as querystring } from "querystring" +export { default as readline } from "readline" +export { default as "readline/promises" } from "readline/promises" +export { default as repl } from "repl" +export { default as stream } from "stream" +export { default as "stream/consumers" } from "stream/consumers" +export { default as "stream/promises" } from "stream/promises" +export { default as "stream/web" } from "stream/web" +export { default as string_decoder } from "string_decoder" +// export { default as sys } from "sys" +export { default as timers } from "timers" +export { default as "timers/promises" } from "timers/promises" +export { default as tls } from "tls" +export { default as trace_events } from "trace_events" +export { default as tty } from "tty" +export { default as url } from "url" +export { default as util } from "util" +export { default as "util/types" } from "util/types" +export { default as v8 } from "v8" +export { default as vm } from "vm" +export { default as wasi } from "wasi" +export { default as worker_threads } from "worker_threads" +export { default as zlib } from "zlib" diff --git a/src/test/converter2/behavior/routerBugs.ts b/src/test/converter2/behavior/routerBugs.ts new file mode 100644 index 000000000..facb4b6a6 --- /dev/null +++ b/src/test/converter2/behavior/routerBugs.ts @@ -0,0 +1,10 @@ +export interface Foo { + codeGeneration?: { + strings: boolean; + wasm: boolean; + }; + + iterator(options?: { + destroyOnReturn?: boolean; + }): AsyncIterableIterator; +} diff --git a/src/test/issues.c2.test.ts b/src/test/issues.c2.test.ts index e8f88df22..9c29340e7 100644 --- a/src/test/issues.c2.test.ts +++ b/src/test/issues.c2.test.ts @@ -36,7 +36,7 @@ import { query, querySig, } from "./utils.js"; -import { DefaultTheme, PageEvent } from "../index.js"; +import { DefaultRouter, DefaultTheme, PageEvent } from "../index.js"; const base = getConverter2Base(); const app = getConverter2App(); @@ -1409,6 +1409,8 @@ describe("Issue Tests", () => { const project = convert(); const theme = new DefaultTheme(app.renderer); + theme.router = new DefaultRouter(app); + theme.router.buildPages(project); const page = new PageEvent(project); page.project = project; const context = theme.getRenderContext(page); diff --git a/src/test/output/formatter.test.ts b/src/test/output/formatter.test.ts index d0b653551..1e0d7c3c7 100644 --- a/src/test/output/formatter.test.ts +++ b/src/test/output/formatter.test.ts @@ -29,6 +29,7 @@ import { FileRegistry, ParameterReflection, ProjectReflection, + Reflection, ReflectionFlag, ReflectionKind, SignatureReflection, @@ -39,9 +40,44 @@ import { FormattedCodeGenerator, Wrap, } from "../../lib/output/formatter.js"; +import { + type Router, + Slugger, + type PageDefinition, +} from "../../lib/output/index.js"; export function renderType(type: SomeType, maxWidth = 80, startWidth = 0) { - const builder = new FormattedCodeBuilder(() => ""); + class DummyRouter implements Router { + buildPages(): PageDefinition[] { + return []; + } + hasUrl(): boolean { + return true; + } + getLinkableReflections(): Reflection[] { + return []; + } + getAnchor(): string | undefined { + return ""; + } + hasOwnDocument(): boolean { + return true; + } + relativeUrl(): string { + return ""; + } + baseRelativeUrl(): string { + return ""; + } + getFullUrl(): string { + return ""; + } + getSlugger(): Slugger { + return new Slugger({ lowercase: false }); + } + } + + const builder = new FormattedCodeBuilder(new DummyRouter(), null!); const tree = builder.type(type, TypeContext.none); const generator = new FormattedCodeGenerator(maxWidth, startWidth); generator.node(tree, Wrap.Detect);