From 096df5e1bef938631808cbbe27aa509e4eef81ca Mon Sep 17 00:00:00 2001 From: Gerrit Birkeland Date: Sun, 15 Dec 2024 15:44:18 -0700 Subject: [PATCH] Introduce `--router` option Resolves #2111 --- CHANGELOG.md | 6 +- site/options/output.md | 70 +++++ src/index.ts | 8 +- src/lib/internationalization/locales/en.cts | 3 + src/lib/output/index.ts | 8 +- src/lib/output/renderer.ts | 55 +++- src/lib/output/router.ts | 303 +++++++++++++++++--- src/lib/utils/options/declaration.ts | 1 + src/lib/utils/options/sources/typedoc.ts | 6 + src/test/converter2/behavior/router.ts | 4 + src/test/issues.c2.test.ts | 4 +- src/test/output/router.test.ts | 221 +++++++++++++- 12 files changed, 613 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8837938de..65897dbaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,12 @@ title: Changelog ## Beta +- Added a `--router` option which can be used to modify TypeDoc's output folder + structure. This can be extended with plugins. - 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. -TODO: - -- Add option for choosing router - ## Unreleased ## v0.27.5 (2024-12-14) diff --git a/site/options/output.md b/site/options/output.md index 4e280f7e1..ff7d64c8e 100644 --- a/site/options/output.md +++ b/site/options/output.md @@ -124,6 +124,76 @@ $ typedoc --theme default Specify the theme name that should be used. +## router + +```bash +$ typedoc --router default +``` + +Specify the router that should be used to determine what files to create for the +HTML output and how to link between pages. Additional routers may be added by +plugins/themes. TypeDoc ships with the following builtin routers: + +- **kind** (default) - Creates folders according to their the documented member's kind. +- **kind-dir** - Like **kind**, but renders each page as `index.html` within a directory for the page name. This can be used to make "clean" urls. +- **structure** - Creates folders according to the module structure. +- **structure-dir** - Like **structure**, but renders each page as `index.html` within a directory for the page name. This can be used to make "clean" urls. +- **group** - Creates folders according to the reflection's [`@group`](../tags/group.md). +- **category** - Creates folders according to the reflection's [`@category`](../tags/category.md). + +This is easiest to understand with an example. Given the following API: + +```ts +export function initialize(): void; +/** @group Opts */ +export class Options {} +export namespace TypeDoc { + export const VERSION: string; +} +``` + +TypeDoc will create a folder structure resembling the following, the common +`assets` folder and `index.html` / `modules.html` files have been omitted for +brevity. + +**kind** + +```text +docs +├── classes +│ └── Options.html +├── functions +│ └── initialize.html +├── modules +│ └── TypeDoc.html +└── variables + └── TypeDoc.VERSION.html +``` + +**structure** + +```text +├── initialize.html +├── Options.html +├── TypeDoc +│ └── VERSION.html +└── TypeDoc.html +``` + +**groups** + +```text +docs +├── Opts +│ └── Options.html +├── Functions +│ └── initialize.html +├── Namespaces +│ └── TypeDoc.html +└── Variables + └── TypeDoc.VERSION.html +``` + ## lightHighlightTheme ```bash diff --git a/src/index.ts b/src/index.ts index a0aa1a3c9..21d03f9be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,7 +56,13 @@ export { RendererEvent, MarkdownEvent, IndexEvent, - DefaultRouter, + BaseRouter, + KindRouter, + KindDirRouter, + StructureRouter, + StructureDirRouter, + GroupRouter, + CategoryRouter, PageKind, } from "./lib/output/index.js"; export type { diff --git a/src/lib/internationalization/locales/en.cts b/src/lib/internationalization/locales/en.cts index 8b38479a6..9a5ef0b30 100644 --- a/src/lib/internationalization/locales/en.cts +++ b/src/lib/internationalization/locales/en.cts @@ -132,6 +132,7 @@ export = { could_not_empty_output_directory_0: `Could not empty the output directory {0}`, could_not_create_output_directory_0: `Could not create the output directory {0}`, theme_0_is_not_defined_available_are_1: `The theme '{0}' is not defined. The available themes are: {1}`, + router_0_is_not_defined_available_are_1: `The router '{0}' is not defined. The available routers are: {1}`, reflection_0_links_to_1_but_anchor_does_not_exist_try_2: `{0} links to {1}, but the anchor does not exist. You may have meant:\n\t{2}`, // entry points @@ -232,6 +233,8 @@ export = { "Specify whether the output JSON should be formatted with tabs", help_emit: "Specify what TypeDoc should emit, 'docs', 'both', or 'none'", help_theme: "Specify the theme name to render the documentation with", + help_router: + "Specify the router name to use to determine file names in the documentation", help_lightHighlightTheme: "Specify the code highlighting theme in light mode", help_darkHighlightTheme: "Specify the code highlighting theme in dark mode", diff --git a/src/lib/output/index.ts b/src/lib/output/index.ts index d69f8a31e..f2a99b752 100644 --- a/src/lib/output/index.ts +++ b/src/lib/output/index.ts @@ -18,7 +18,13 @@ export { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderCo export { Slugger } from "./themes/default/Slugger.js"; export { - DefaultRouter, + BaseRouter, + KindRouter, + KindDirRouter, + StructureRouter, + StructureDirRouter, + GroupRouter, + CategoryRouter, PageKind, type PageDefinition, type Router, diff --git a/src/lib/output/renderer.ts b/src/lib/output/renderer.ts index f52027666..a04092b9d 100644 --- a/src/lib/output/renderer.ts +++ b/src/lib/output/renderer.ts @@ -40,7 +40,16 @@ import { NavigationPlugin, SitemapPlugin, } from "./plugins/index.js"; -import { DefaultRouter, type PageDefinition, type Router } from "./router.js"; +import { + CategoryRouter, + GroupRouter, + KindDirRouter, + KindRouter, + StructureDirRouter, + StructureRouter, + type PageDefinition, + type Router, +} from "./router.js"; /** * Describes the hooks available to inject output in the default theme. @@ -169,7 +178,12 @@ export interface RendererEvents { */ export class Renderer extends AbstractComponent { private routers = new Map Router>([ - ["default", DefaultRouter], + ["kind", KindRouter], + ["structure", StructureRouter], + ["kind-dir", KindDirRouter], + ["structure-dir", StructureDirRouter], + ["group", GroupRouter], + ["category", CategoryRouter], ]); private themes = new Map Theme>([ @@ -235,6 +249,10 @@ export class Renderer extends AbstractComponent { @Option("theme") private accessor themeName!: string; + /** @internal */ + @Option("router") + private accessor routerName!: string; + @Option("cleanOutputDir") private accessor cleanOutputDir!: boolean; @@ -317,17 +335,15 @@ 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.prepareRouter() || !this.prepareTheme() || !(await this.prepareOutputDirectory(outputDirectory)) ) { return; } - const pages = this.router.buildPages(project); + const pages = this.router!.buildPages(project); const output = new RendererEvent(outputDirectory, project, pages); this.trigger(RendererEvent.BEGIN, output); @@ -402,14 +418,25 @@ export class Renderer extends AbstractComponent { } } - /** - * Ensure that a theme has been setup. - * - * If a the user has set a theme we try to find and load it. If no theme has - * been specified we load the default theme. - * - * @returns TRUE if a theme has been setup, otherwise FALSE. - */ + private prepareRouter(): boolean { + if (!this.theme) { + const ctor = this.routers.get(this.routerName); + if (!ctor) { + this.application.logger.error( + this.application.i18n.router_0_is_not_defined_available_are_1( + this.routerName, + [...this.routers.keys()].join(", "), + ), + ); + return false; + } else { + this.router = new ctor(this.application); + } + } + + return true; + } + private prepareTheme(): boolean { if (!this.theme) { const ctor = this.themes.get(this.themeName); diff --git a/src/lib/output/router.ts b/src/lib/output/router.ts index cc6560a2e..a3782fb79 100644 --- a/src/lib/output/router.ts +++ b/src/lib/output/router.ts @@ -1,9 +1,11 @@ import type { Application } from "../application.js"; +import { CategoryPlugin } from "../converter/plugins/CategoryPlugin.js"; +import { GroupPlugin } from "../converter/plugins/GroupPlugin.js"; import { + Reflection, type DeclarationReflection, ReflectionKind, type ProjectReflection, - type Reflection, } from "../models/index.js"; import { createNormalizedUrl } from "../utils/html.js"; import { Option, type TypeDocOptionMap } from "../utils/index.js"; @@ -93,9 +95,15 @@ export interface Router { } /** - * TypeDoc's default router implementation. + * Base router class intended to make it easier to implement a router. + * + * Child classes need only {@link getIdealBaseName}, this class will take care + * of the recursing through child reflections. + * @group Routers */ -export class DefaultRouter implements Router { +export abstract class BaseRouter implements Router { + extension = ".html"; + // Note: This will always contain lowercased names to avoid issues with // case-insensitive file systems. protected usedFileNames = new Set(); @@ -104,26 +112,20 @@ export class DefaultRouter implements Router { protected anchors = new Map(); @Option("sluggerConfiguration") - private accessor sluggerConfiguration!: TypeDocOptionMap["sluggerConfiguration"]; + protected accessor sluggerConfiguration!: TypeDocOptionMap["sluggerConfiguration"]; @Option("includeHierarchySummary") - private accessor includeHierarchySummary!: boolean; + protected accessor includeHierarchySummary!: boolean; constructor(readonly application: Application) {} - extension = "html"; - - 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]], - ]); + /** + * Should return the base-relative desired file name for a reflection. + * This name may not be used exactly as TypeDoc will detect conflicts + * and automatically introduce a unique identifier to the URL to resolve + * them. + */ + protected abstract getIdealBaseName(reflection: Reflection): string; buildPages(project: ProjectReflection): PageDefinition[] { this.usedFileNames = new Set(); @@ -135,18 +137,18 @@ export class DefaultRouter implements Router { if (project.readme?.length) { pages.push({ - url: `index.${this.extension}`, + url: this.getFileName("index"), kind: PageKind.Index, model: project, }); pages.push({ - url: `modules.${this.extension}`, + url: this.getFileName("modules"), kind: PageKind.Reflection, model: project, }); } else { pages.push({ - url: `index.${this.extension}`, + url: this.getFileName("index"), kind: PageKind.Reflection, model: project, }); @@ -156,7 +158,7 @@ export class DefaultRouter implements Router { if (this.includeHierarchySummary && getHierarchyRoots(project)) { pages.push({ - url: `hierarchy.${this.extension}`, + url: this.getFileName("hierarchy"), kind: PageKind.Hierarchy, model: project, }); @@ -206,7 +208,7 @@ export class DefaultRouter implements Router { } } - if (equal) { + if (equal && !to.isProject()) { return `#${this.getAnchor(to)}`; } @@ -242,32 +244,54 @@ export class DefaultRouter implements Router { return this.getSlugger(reflection.parent!); } + /** + * Should the page kind to use if a reflection should have its own rendered + * page in the output. Note that once `undefined` is returned, children of + * that reflection will not have their own document. + */ + protected getPageKind(reflection: Reflection): PageKind | undefined { + const pageReflectionKinds = + ReflectionKind.Class | + ReflectionKind.Interface | + ReflectionKind.Enum | + ReflectionKind.Module | + ReflectionKind.Namespace | + ReflectionKind.TypeAlias | + ReflectionKind.Function | + ReflectionKind.Variable; + const documentReflectionKinds = ReflectionKind.Document; + + if (reflection.kindOf(pageReflectionKinds)) { + return PageKind.Reflection; + } + + if (reflection.kindOf(documentReflectionKinds)) { + return PageKind.Document; + } + } + 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); + const kind = this.getPageKind(reflection); + if (kind) { + const idealName = this.getIdealBaseName(reflection); + const actualName = this.getFileName(idealName); + this.fullUrls.set(reflection, actualName); this.sluggers.set( reflection, new Slugger(this.sluggerConfiguration), ); outPages.push({ - kind: PageKind.Reflection, + kind, model: reflection, - url, + url: actualName, }); reflection.traverse((child) => { - if (child.isDeclaration() || child.isDocument()) { - this.buildChildPages(child, outPages); - } else { - this.buildAnchors(child, reflection); - } + this.buildChildPages(child, outPages); return true; }); } else { @@ -334,14 +358,12 @@ export class DefaultRouter implements Router { }); } - protected getFileName(reflection: Reflection): string { - const parts = [createNormalizedUrl(reflection.name)]; - while (reflection.parent && !reflection.parent.isProject()) { - reflection = reflection.parent; - parts.unshift(createNormalizedUrl(reflection.name)); - } + /** Strip non-url safe characters from the specified string. */ + protected getUrlSafeName(name: string): string { + return createNormalizedUrl(name); + } - const baseName = parts.join("."); + protected getFileName(baseName: string): string { const lowerBaseName = baseName.toLocaleLowerCase(); if (this.usedFileNames.has(lowerBaseName)) { let index = 1; @@ -350,10 +372,203 @@ export class DefaultRouter implements Router { } this.usedFileNames.add(`${lowerBaseName}-${index}`); - return `${baseName}-${index}.${this.extension}`; + return `${baseName}-${index}${this.extension}`; } this.usedFileNames.add(lowerBaseName); - return `${baseName}.${this.extension}`; + return `${baseName}${this.extension}`; + } +} + +/** + * Router which places reflections in folders according to their kind. + * @group Routers + */ +export class KindRouter extends BaseRouter { + directories = new Map([ + [ReflectionKind.Class, "classes"], + [ReflectionKind.Interface, "interfaces"], + [ReflectionKind.Enum, "enums"], + [ReflectionKind.Namespace, "modules"], + [ReflectionKind.Module, "modules"], + [ReflectionKind.TypeAlias, "types"], + [ReflectionKind.Function, "functions"], + [ReflectionKind.Variable, "variables"], + [ReflectionKind.Document, "documents"], + ]); + + protected override getIdealBaseName(reflection: Reflection): string { + const dir = this.directories.get(reflection.kind)!; + const parts = [createNormalizedUrl(reflection.name)]; + while (reflection.parent && !reflection.parent.isProject()) { + reflection = reflection.parent; + parts.unshift(createNormalizedUrl(reflection.name)); + } + + const baseName = parts.join("."); + return `${dir}/${baseName}`; + } +} + +/** + * Router which places reflections in folders according to their kind, + * but creates each page as `/index.html` to allow for clean URLs. + * @group Routers + */ +export class KindDirRouter extends KindRouter { + private fixLink(link: string) { + return link.replace(/\/index\.html(#|$)/, "/$1"); + } + + protected override buildChildPages( + reflection: Reflection, + outPages: PageDefinition[], + ): void { + this.extension = `/index.html`; + return super.buildChildPages(reflection, outPages); + } + + override getFullUrl(refl: Reflection): string { + return this.fixLink(super.getFullUrl(refl)); + } + + override relativeUrl(from: Reflection, to: Reflection): string { + return this.fixLink(super.relativeUrl(from, to)); + } +} + +/** + * Router which places reflections in folders according to the module structure. + * @group Routers + */ +export class StructureRouter extends BaseRouter { + protected override getIdealBaseName(reflection: Reflection): string { + // Special case: Modules allow slashes in their name. We actually want + // to allow that here to mirror file structures. + const parts = [...reflection.name.split("/").map(createNormalizedUrl)]; + while (reflection.parent && !reflection.parent.isProject()) { + reflection = reflection.parent; + parts.unshift( + ...reflection.name.split("/").map(createNormalizedUrl), + ); + } + + // This should only happen if someone tries to break things with @module + // I don't think it will ever occur in normal usage. + if (parts.includes("..")) { + throw new Error( + "structure router cannot be used with a project that has a name containing '..'", + ); + } + + return parts.join("/"); + } +} + +/** + * Router which places reflections in folders according to the module structure, + * but creates each page as `/index.html` to allow for clean URLs. + * @group Routers + */ +export class StructureDirRouter extends StructureRouter { + private fixLink(link: string) { + return link.replace(/\/index\.html(#|$)/, "/$1"); + } + + protected override buildChildPages( + reflection: Reflection, + outPages: PageDefinition[], + ): void { + this.extension = `/index.html`; + return super.buildChildPages(reflection, outPages); + } + + override getFullUrl(refl: Reflection): string { + return this.fixLink(super.getFullUrl(refl)); + } + + override relativeUrl(from: Reflection, to: Reflection): string { + return this.fixLink(super.relativeUrl(from, to)); + } +} + +/** + * Router which places reflections in folders according to `@group` tags. + * @group Routers + */ +export class GroupRouter extends BaseRouter { + @Option("groupReferencesByType") + private accessor groupReferencesByType!: boolean; + + private getGroup(reflection: Reflection) { + if (reflection.isDeclaration() || reflection.isDocument()) { + const group = GroupPlugin.getGroups( + reflection, + this.groupReferencesByType, + this.application.internationalization, + ); + + return group.values().next().value!; + } + + throw new Error( + "Tried to render a non declaration/document to a page, not supported by GroupRouter", + ); + } + + protected override getIdealBaseName(reflection: Reflection): string { + const group = [ + this.getGroup(reflection) + .split("/") + .map(createNormalizedUrl) + .join("/"), + ]; + const parts = [createNormalizedUrl(reflection.name)]; + while (reflection.parent && !reflection.parent.isProject()) { + reflection = reflection.parent; + parts.unshift(createNormalizedUrl(reflection.name)); + } + + const baseName = parts.join("."); + return `${group}/${baseName}`; + } +} + +/** + * Router which places reflections in folders according to `@category` tags. + * @group Routers + */ +export class CategoryRouter extends BaseRouter { + @Option("defaultCategory") + private accessor defaultCategory!: string; + + private getCategory(reflection: Reflection) { + if (reflection.isDeclaration() || reflection.isDocument()) { + const cats = CategoryPlugin.getCategories(reflection); + return cats.size + ? cats.values().next().value! + : this.defaultCategory; + } + + throw new Error( + "Tried to render a non declaration/document to a page, not supported by GroupRouter", + ); + } + + protected override getIdealBaseName(reflection: Reflection): string { + const cat = [ + this.getCategory(reflection) + .split("/") + .map(createNormalizedUrl) + .join("/"), + ]; + const parts = [createNormalizedUrl(reflection.name)]; + while (reflection.parent && !reflection.parent.isProject()) { + reflection = reflection.parent; + parts.unshift(createNormalizedUrl(reflection.name)); + } + + const baseName = parts.join("."); + return `${cat}/${baseName}`; } } diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index a55b214bc..611ccb895 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -142,6 +142,7 @@ export interface TypeDocOptionMap { pretty: boolean; emit: typeof EmitStrategy; theme: string; + router: string; lightHighlightTheme: ShikiTheme; darkHighlightTheme: ShikiTheme; highlightLanguages: string[]; diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index d862771bb..261469bd3 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -314,6 +314,12 @@ export function addTypeDocOptions(options: Pick) { type: ParameterType.String, defaultValue: "default", }); + options.addDeclaration({ + name: "router", + help: (i18n) => i18n.help_router(), + type: ParameterType.String, + defaultValue: "kind", + }); const defaultLightTheme: BundledTheme = "light-plus"; const defaultDarkTheme: BundledTheme = "dark-plus"; diff --git a/src/test/converter2/behavior/router.ts b/src/test/converter2/behavior/router.ts index 4eed92e4e..d76bbc2c0 100644 --- a/src/test/converter2/behavior/router.ts +++ b/src/test/converter2/behavior/router.ts @@ -1,4 +1,5 @@ export interface Foo { + /** {@link Nested.refl} */ codeGeneration?: { strings: boolean; wasm: boolean; @@ -10,8 +11,10 @@ export interface Foo { } // `a` gets an anchor because it is directly within a type alias +/** @category CustomCat */ export type Obj = { a: string }; +/** @group Abc/Group */ export const abc = { abcProp: { nested: true } }; // `b` does NOT get an anchor as it isn't a direct descendent @@ -25,5 +28,6 @@ export function Func(param: string): { noUrl: boolean } { export function func() {} export namespace Nested { + /** {@link Foo.codeGeneration} */ export const refl = 1; } diff --git a/src/test/issues.c2.test.ts b/src/test/issues.c2.test.ts index 98895724d..0cc497d78 100644 --- a/src/test/issues.c2.test.ts +++ b/src/test/issues.c2.test.ts @@ -29,7 +29,7 @@ import { query, querySig, } from "./utils.js"; -import { DefaultRouter, DefaultTheme, PageEvent } from "../index.js"; +import { KindRouter, DefaultTheme, PageEvent } from "../index.js"; const app = getConverter2App(); @@ -1370,7 +1370,7 @@ describe("Issue Tests", () => { const project = convert(); const theme = new DefaultTheme(app.renderer); - theme.router = new DefaultRouter(app); + theme.router = new KindRouter(app); theme.router.buildPages(project); const page = new PageEvent(project); page.project = project; diff --git a/src/test/output/router.test.ts b/src/test/output/router.test.ts index 2bd17ecf9..67fe457c2 100644 --- a/src/test/output/router.test.ts +++ b/src/test/output/router.test.ts @@ -1,5 +1,12 @@ import { deepStrictEqual as equal, ok, throws } from "assert"; -import { DefaultRouter } from "../../lib/output/index.js"; +import { + CategoryRouter, + GroupRouter, + KindDirRouter, + KindRouter, + StructureDirRouter, + StructureRouter, +} from "../../lib/output/index.js"; import { getConverter2App, getConverter2Project } from "../programs.js"; import { query } from "../utils.js"; import { @@ -11,11 +18,11 @@ const app = getConverter2App(); const getProject = () => getConverter2Project(["router"], "behavior"); -describe("DefaultRouter", () => { +describe("KindRouter", () => { it("Creates index page if project has a readme", () => { const project = getProject(); project.readme = [{ kind: "text", text: "text" }]; - const router = new DefaultRouter(app); + const router = new KindRouter(app); const pages = router.buildPages(project); equal(pages.map((p) => [p.model.getFullName(), p.url]).slice(0, 3), [ @@ -28,7 +35,7 @@ describe("DefaultRouter", () => { it("Defines URLs for expected reflections", () => { const project = getProject(); delete project.readme; - const router = new DefaultRouter(app); + const router = new KindRouter(app); const pages = router.buildPages(project); equal( @@ -76,7 +83,7 @@ describe("DefaultRouter", () => { it("Can retrieve the anchor for a reflection", () => { const project = getProject(); - const router = new DefaultRouter(app); + const router = new KindRouter(app); router.buildPages(project); equal(router.getAnchor(query(project, "Obj")), undefined); @@ -88,7 +95,7 @@ describe("DefaultRouter", () => { it("Can check if a reflection has its own page", () => { const project = getProject(); - const router = new DefaultRouter(app); + const router = new KindRouter(app); router.buildPages(project); equal(router.hasOwnDocument(query(project, "Obj")), true); @@ -107,7 +114,7 @@ describe("DefaultRouter", () => { it("Can get relative URLs between pages", () => { const project = getProject(); - const router = new DefaultRouter(app); + const router = new KindRouter(app); router.buildPages(project); const Foo = query(project, "Foo"); @@ -131,7 +138,7 @@ describe("DefaultRouter", () => { it("Can get a URL to an asset relative to the base", () => { const project = getProject(); - const router = new DefaultRouter(app); + const router = new KindRouter(app); router.buildPages(project); const Foo = query(project, "Foo"); @@ -148,7 +155,7 @@ describe("DefaultRouter", () => { it("Can get a full URL to a reflection", () => { const project = getProject(); - const router = new DefaultRouter(app); + const router = new KindRouter(app); router.buildPages(project); const Foo = query(project, "Foo"); @@ -164,11 +171,205 @@ describe("DefaultRouter", () => { it("Can get the slugger for the appropriate page", () => { const project = getProject(); - const router = new DefaultRouter(app); + const router = new KindRouter(app); router.buildPages(project); const Foo = query(project, "Foo"); const codeGen = query(project, "Foo.codeGeneration"); ok(router.getSlugger(Foo) === router.getSlugger(codeGen)); }); + + it("Can link to the modules page", () => { + const project = getProject(); + const router = new KindRouter(app); + router.buildPages(project); + + const Foo = query(project, "Foo"); + equal(router.relativeUrl(project, project), "modules.html"); + equal(router.relativeUrl(Foo, project), "../modules.html"); + }); +}); + +describe("KindDirRouter", () => { + it("Defines URLs for expected reflections", () => { + const project = getProject(); + delete project.readme; + const router = new KindDirRouter(app); + + const pages = router.buildPages(project); + equal( + pages.map((p) => [p.model.getFullName(), p.url]), + [ + ["typedoc", "index.html"], + ["typedoc", "hierarchy.html"], + ["Nested", "modules/Nested/index.html"], + ["Nested.refl", "variables/Nested.refl/index.html"], + ["Foo", "interfaces/Foo/index.html"], + ["Obj", "types/Obj/index.html"], + ["ObjArray", "types/ObjArray/index.html"], + ["abc", "variables/abc/index.html"], + ["func", "functions/func/index.html"], + ["Func", "functions/Func-1/index.html"], + ], + ); + }); + + it("Does not include trailing /index.html in full URLs", () => { + const project = getProject(); + delete project.readme; + const router = new KindDirRouter(app); + router.buildPages(project); + + equal( + router.getFullUrl(query(project, "Nested.refl")), + "variables/Nested.refl/", + ); + equal( + router.getFullUrl(query(project, "Foo.codeGeneration")), + "interfaces/Foo/#codegeneration", + ); + }); + + it("Does not include trailing /index.html in relative URLs", () => { + const project = getProject(); + delete project.readme; + const router = new KindDirRouter(app); + router.buildPages(project); + + equal( + router.relativeUrl( + query(project, "Nested.refl"), + query(project, "Foo.codeGeneration"), + ), + "../../interfaces/Foo/#codegeneration", + ); + }); +}); + +describe("StructureRouter", () => { + it("Defines URLs for expected reflections", () => { + const project = getProject(); + delete project.readme; + const router = new StructureRouter(app); + + const pages = router.buildPages(project); + equal( + pages.map((p) => [p.model.getFullName(), p.url]), + [ + ["typedoc", "index.html"], + ["typedoc", "hierarchy.html"], + ["Nested", "Nested.html"], + ["Nested.refl", "Nested/refl.html"], + ["Foo", "Foo.html"], + ["Obj", "Obj.html"], + ["ObjArray", "ObjArray.html"], + ["abc", "abc.html"], + ["func", "func.html"], + ["Func", "Func-1.html"], + ], + ); + }); +}); + +describe("StructureDirRouter", () => { + it("Defines URLs for expected reflections", () => { + const project = getProject(); + delete project.readme; + const router = new StructureDirRouter(app); + + const pages = router.buildPages(project); + equal( + pages.map((p) => [p.model.getFullName(), p.url]), + [ + ["typedoc", "index.html"], + ["typedoc", "hierarchy.html"], + ["Nested", "Nested/index.html"], + ["Nested.refl", "Nested/refl/index.html"], + ["Foo", "Foo/index.html"], + ["Obj", "Obj/index.html"], + ["ObjArray", "ObjArray/index.html"], + ["abc", "abc/index.html"], + ["func", "func/index.html"], + ["Func", "Func-1/index.html"], + ], + ); + }); + + it("Does not include trailing /index.html in full URLs", () => { + const project = getProject(); + delete project.readme; + const router = new StructureDirRouter(app); + router.buildPages(project); + + equal(router.getFullUrl(query(project, "Nested.refl")), "Nested/refl/"); + equal( + router.getFullUrl(query(project, "Foo.codeGeneration")), + "Foo/#codegeneration", + ); + }); + + it("Does not include trailing /index.html in relative URLs", () => { + const project = getProject(); + delete project.readme; + const router = new StructureDirRouter(app); + router.buildPages(project); + + equal( + router.relativeUrl( + query(project, "Nested.refl"), + query(project, "Foo.codeGeneration"), + ), + "../../Foo/#codegeneration", + ); + }); +}); + +describe("GroupRouter", () => { + it("Defines URLs for expected reflections", () => { + const project = getProject(); + delete project.readme; + const router = new GroupRouter(app); + + const pages = router.buildPages(project); + equal( + pages.map((p) => [p.model.getFullName(), p.url]), + [ + ["typedoc", "index.html"], + ["typedoc", "hierarchy.html"], + ["Nested", "Namespaces/Nested.html"], + ["Nested.refl", "Variables/Nested.refl.html"], + ["Foo", "Interfaces/Foo.html"], + ["Obj", "Type_Aliases/Obj.html"], + ["ObjArray", "Type_Aliases/ObjArray.html"], + ["abc", "Abc/Group/abc.html"], + ["func", "Functions/func.html"], + ["Func", "Functions/Func-1.html"], + ], + ); + }); +}); + +describe("CategoryRouter", () => { + it("Defines URLs for expected reflections", () => { + const project = getProject(); + delete project.readme; + const router = new CategoryRouter(app); + + const pages = router.buildPages(project); + equal( + pages.map((p) => [p.model.getFullName(), p.url]), + [ + ["typedoc", "index.html"], + ["typedoc", "hierarchy.html"], + ["Nested", "Other/Nested.html"], + ["Nested.refl", "Other/Nested.refl.html"], + ["Foo", "Other/Foo.html"], + ["Obj", "CustomCat/Obj.html"], + ["ObjArray", "Other/ObjArray.html"], + ["abc", "Other/abc.html"], + ["func", "Other/func.html"], + ["Func", "Other/Func-1.html"], + ], + ); + }); });