diff --git a/example/src/classes/Customer.ts b/example/src/classes/Customer.ts index 94713cb9a..360e9c3c3 100644 --- a/example/src/classes/Customer.ts +++ b/example/src/classes/Customer.ts @@ -79,6 +79,8 @@ export abstract class Customer { /** * A class that extends {@link Customer | `Customer`}. + * + * Notice how TypeDoc shows the inheritance hierarchy for our class. */ export class DeliveryCustomer extends Customer { /** A property defined on the subclass. */ @@ -118,3 +120,31 @@ export class DeliveryCustomer extends Customer { return typeof this.preferredCourierId === "undefined"; } } + +/** + * A class that extends {@link Customer | `Customer`}. + * + * Notice how TypeDoc shows the inheritance hierarchy for our class. + */ +export class WalkInCustomer extends Customer { + /** A property defined on the subclass. */ + trustedCustomer?: boolean; + + /** A private property defined on the subclass. */ + private _ordersPlacedCount: number = 0; + + /** + * An example of overriding a public method. + */ + onOrderPlaced(): void { + super.onOrderPlaced(); + + this._ordersPlacedCount++; + + if ( + this._ordersPlacedCount > 10 && + typeof this.trustedCustomer === "undefined" + ) + this.trustedCustomer = true; + } +} diff --git a/src/lib/internationalization/locales/jp.cts b/src/lib/internationalization/locales/jp.cts index d7ffd796f..9503dcae2 100644 --- a/src/lib/internationalization/locales/jp.cts +++ b/src/lib/internationalization/locales/jp.cts @@ -458,7 +458,6 @@ export = buildIncompleteTranslation({ theme_type_declaration: "型宣言", theme_index: "インデックス", theme_hierarchy: "階層", - theme_hierarchy_view_full: "完全な階層を表示", theme_implemented_by: "実装者", theme_defined_in: "定義", theme_implementation_of: "の実装", @@ -487,6 +486,5 @@ export = buildIncompleteTranslation({ theme_copied: "コピー完了!", theme_normally_hidden: "このメンバーは、フィルター設定のため、通常は非表示になっています。", - theme_class_hierarchy_title: "クラス継承図", theme_loading: "読み込み中...", }); diff --git a/src/lib/internationalization/locales/ko.cts b/src/lib/internationalization/locales/ko.cts index 471be8de1..dddfb6efe 100644 --- a/src/lib/internationalization/locales/ko.cts +++ b/src/lib/internationalization/locales/ko.cts @@ -314,7 +314,6 @@ export = buildIncompleteTranslation({ theme_type_declaration: "타입 선언", theme_index: "둘러보기", theme_hierarchy: "계층", - theme_hierarchy_view_full: "전체 보기", theme_implemented_by: "구현", theme_defined_in: "정의 위치:", theme_implementation_of: "구현하는 타입:", diff --git a/src/lib/internationalization/locales/zh.cts b/src/lib/internationalization/locales/zh.cts index 0dbd39a69..092d41d63 100644 --- a/src/lib/internationalization/locales/zh.cts +++ b/src/lib/internationalization/locales/zh.cts @@ -426,7 +426,6 @@ export = buildIncompleteTranslation({ theme_type_declaration: "类型声明", theme_index: "索引", theme_hierarchy: "层级", - theme_hierarchy_view_full: "查看完整内容", theme_implemented_by: "实现于", theme_defined_in: "定义于", theme_implementation_of: "实现了", @@ -451,7 +450,6 @@ export = buildIncompleteTranslation({ theme_copy: "复制", theme_copied: "已复制!", theme_normally_hidden: "由于您的过滤器设置,该成员已被隐藏。", - theme_class_hierarchy_title: "类继承图表", theme_loading: "加载中……", tag_defaultValue: "默认值", diff --git a/src/lib/internationalization/translatable.ts b/src/lib/internationalization/translatable.ts index 52c014bcf..6ff306843 100644 --- a/src/lib/internationalization/translatable.ts +++ b/src/lib/internationalization/translatable.ts @@ -301,6 +301,8 @@ export const translatable = { "Branches of the navigation tree which should not be expanded", help_navigation: "Determines how the navigation sidebar is organized", help_headings: "Determines which optional headings are rendered", + help_includeHierarchySummary: + "If set, a reflections hierarchy summary will be rendered to a summary page. Defaults to `true`", help_visibilityFilters: "Specify the default visibility for builtin filters and additional filters according to modifier tags", help_searchCategoryBoosts: @@ -466,7 +468,10 @@ export const translatable = { theme_type_declaration: "Type declaration", theme_index: "Index", theme_hierarchy: "Hierarchy", - theme_hierarchy_view_full: "view full", + theme_hierarchy_summary: "Hierarchy Summary", + theme_hierarchy_view_summary: "View Summary", + theme_hierarchy_expand: "Expand", + theme_hierarchy_collapse: "Collapse", theme_implemented_by: "Implemented by", theme_defined_in: "Defined in", theme_implementation_of: "Implementation of", @@ -476,7 +481,6 @@ export const translatable = { theme_re_exports: "Re-exports", theme_renames_and_re_exports: "Renames and re-exports", theme_generated_using_typedoc: "Generated using TypeDoc", // If this includes "TypeDoc", theme will insert a link at that location. - theme_class_hierarchy_title: "Class Hierarchy", // Search theme_preparing_search_index: "Preparing search index...", theme_search_index_not_available: "The search index is not available", diff --git a/src/lib/output/plugins/HierarchyPlugin.ts b/src/lib/output/plugins/HierarchyPlugin.ts new file mode 100644 index 000000000..d83293465 --- /dev/null +++ b/src/lib/output/plugins/HierarchyPlugin.ts @@ -0,0 +1,111 @@ +import * as Path from "path"; +import { Component, RendererComponent } from "../components"; +import { PageEvent, RendererEvent } from "../events"; +import { JSX, writeFile } from "../../utils"; +import { DefaultTheme } from "../themes/default/DefaultTheme"; +import { gzip } from "zlib"; +import { promisify } from "util"; +import { + DeclarationReflection, + ReferenceType, + ReflectionKind, +} from "../../models"; + +const gzipP = promisify(gzip); + +export interface HierarchyElement { + html: string; + text: string; + path: string; + parents?: ({ path: string } | { html: string; text: string })[]; + children?: ({ path: string } | { html: string; text: string })[]; +} + +@Component({ name: "hierarchy" }) +export class HierarchyPlugin extends RendererComponent { + override initialize() { + this.owner.on(RendererEvent.BEGIN, this.onRendererBegin.bind(this)); + } + + private onRendererBegin(_event: RendererEvent) { + if (!(this.owner.theme instanceof DefaultTheme)) { + return; + } + + this.owner.preRenderAsyncJobs.push((event) => + this.buildHierarchy(event), + ); + } + + private async buildHierarchy(event: RendererEvent) { + const theme = this.owner.theme as DefaultTheme; + + const context = theme.getRenderContext(new PageEvent(event.project)); + + const hierarchy = ( + event.project.getReflectionsByKind( + ReflectionKind.ClassOrInterface, + ) as DeclarationReflection[] + ) + .filter( + (reflection) => + reflection.extendedTypes?.length || + reflection.extendedBy?.length, + ) + .map((reflection) => ({ + html: JSX.renderElement( + context.type( + ReferenceType.createResolvedReference( + reflection.name, + reflection, + reflection.project, + ), + ), + ), + // Full name should be safe here, since this list only includes classes/interfaces. + text: reflection.getFullName(), + path: reflection.url!, + parents: reflection.extendedTypes?.map((type) => + !(type instanceof ReferenceType) || + !(type.reflection instanceof DeclarationReflection) + ? { + html: JSX.renderElement(context.type(type)), + text: type.toString(), + } + : { path: type.reflection.url! }, + ), + children: reflection.extendedBy?.map((type) => + !(type instanceof ReferenceType) || + !(type.reflection instanceof DeclarationReflection) + ? { + html: JSX.renderElement(context.type(type)), + text: type.toString(), + } + : { path: type.reflection.url! }, + ), + })); + + if (!hierarchy.length) return; + + hierarchy.forEach((element) => { + if (!element.parents?.length) delete element.parents; + + if (!element.children?.length) delete element.children; + }); + + const hierarchyJs = Path.join( + event.outputDirectory, + "assets", + "hierarchy.js", + ); + + const gz = await gzipP(Buffer.from(JSON.stringify(hierarchy))); + + await writeFile( + hierarchyJs, + `window.hierarchyData = "data:application/octet-stream;base64,${gz.toString( + "base64", + )}"`, + ); + } +} diff --git a/src/lib/output/plugins/index.ts b/src/lib/output/plugins/index.ts index b2435aaaa..f818cae1c 100644 --- a/src/lib/output/plugins/index.ts +++ b/src/lib/output/plugins/index.ts @@ -3,4 +3,5 @@ export { AssetsPlugin } from "./AssetsPlugin"; export { IconsPlugin } from "./IconsPlugin"; export { JavascriptIndexPlugin } from "./JavascriptIndexPlugin"; export { NavigationPlugin } from "./NavigationPlugin"; +export { HierarchyPlugin } from "./HierarchyPlugin"; export { SitemapPlugin } from "./SitemapPlugin"; diff --git a/src/lib/output/themes/default/DefaultTheme.tsx b/src/lib/output/themes/default/DefaultTheme.tsx index 1a58fd514..a5dfe3226 100644 --- a/src/lib/output/themes/default/DefaultTheme.tsx +++ b/src/lib/output/themes/default/DefaultTheme.tsx @@ -214,7 +214,7 @@ export class DefaultTheme extends Theme { urls.push(new UrlMapping("index.html", project, this.indexTemplate)); } - if (getHierarchyRoots(project).length) { + if (this.application.options.getValue("includeHierarchySummary") && getHierarchyRoots(project).length) { urls.push(new UrlMapping("hierarchy.html", project, this.hierarchyTemplate)); } diff --git a/src/lib/output/themes/default/assets/bootstrap.ts b/src/lib/output/themes/default/assets/bootstrap.ts index a7168939d..23b763b6c 100644 --- a/src/lib/output/themes/default/assets/bootstrap.ts +++ b/src/lib/output/themes/default/assets/bootstrap.ts @@ -5,6 +5,7 @@ import { Filter } from "./typedoc/components/Filter"; import { Accordion } from "./typedoc/components/Accordion"; import { initTheme } from "./typedoc/Theme"; import { initNav } from "./typedoc/Navigation"; +import { initHierarchy } from "./typedoc/Hierarchy"; registerComponent(Toggle, "a[data-toggle]"); registerComponent(Accordion, ".tsd-accordion"); @@ -24,3 +25,4 @@ Object.defineProperty(window, "app", { value: app }); initSearch(); initNav(); +initHierarchy(); diff --git a/src/lib/output/themes/default/assets/typedoc/Hierarchy.ts b/src/lib/output/themes/default/assets/typedoc/Hierarchy.ts new file mode 100644 index 000000000..cf97f061d --- /dev/null +++ b/src/lib/output/themes/default/assets/typedoc/Hierarchy.ts @@ -0,0 +1,327 @@ +export interface HierarchyElement { + html: string; + text: string; + path?: string; + parents?: HierarchyElement[]; + children?: HierarchyElement[]; + class: string; + depth?: number; +} + +declare global { + interface Window { + // Base64 encoded data url, gzipped, JSON encoded HierarchyElement[] + hierarchyData?: string; + } +} + +export function initHierarchy() { + const script = document.getElementById("tsd-hierarchy-script"); + if (!script) return; + + script.addEventListener("load", buildHierarchy); + buildHierarchy(); +} + +async function buildHierarchy() { + const container = document.getElementById("tsd-hierarchy-container"); + if (!container || !window.hierarchyData) return; + + const res = await fetch(window.hierarchyData); + const data = await res.arrayBuffer(); + const json = new Blob([data]) + .stream() + .pipeThrough(new DecompressionStream("gzip")); + + const hierarchy = loadJson(await new Response(json).json()); + let baseUrl = container.dataset.base; + const targetPath = container.dataset.targetPath; + + if (!hierarchy.length || !baseUrl || !targetPath) return; + + if (!baseUrl.endsWith("/")) baseUrl += "/"; + + const seeds = hierarchy.filter((element) => { + if (element.path === targetPath) { + element.class += " tsd-hierarchy-target"; + + element.parents?.forEach( + (parent) => (parent.class += " tsd-hierarchy-close-relative"), + ); + + element.children?.forEach( + (child) => (child.class += " tsd-hierarchy-close-relative"), + ); + + return true; + } + + if ( + !element.parents?.some(({ path }) => path === targetPath) && + !element.children?.some(({ path }) => path === targetPath) + ) { + element.class += " tsd-hierarchy-distant-relative"; + + return false; + } + + return false; + }); + + const trees = getTrees(seeds); + + if (!trees.length) return; + + container + .querySelectorAll("ul.tsd-full-hierarchy") + ?.forEach((list) => list.remove()); + + trees.forEach((tree) => { + const list = buildList( + tree.filter((branch) => !branch.parents?.length), + baseUrl, + )!; + + list.classList.add("tsd-full-hierarchy"); + + container.append(list); + }); +} + +function loadJson(hierarchy: HierarchyElement[]) { + const leaves: HierarchyElement[] = []; + + hierarchy.forEach((element) => { + element.class = "tsd-hierarchy-item"; + + element.parents = element.parents + ?.map((parent) => { + if (parent.path) + return hierarchy.find(({ path }) => path === parent.path); + + parent.class = "tsd-hierarchy-item"; + parent.children = [element]; + + leaves.push(parent); + + return parent; + }) + .filter((parent) => !!parent); + + if (!element.parents?.length) delete element.parents; + + element.children = element.children + ?.map((child) => { + if (child.path) + return hierarchy.find(({ path }) => path === child.path); + + child.class = "tsd-hierarchy-item"; + child.parents = [element]; + + leaves.push(child); + + return child; + }) + .filter((child) => !!child); + + if (!element.children?.length) delete element.children; + }); + + hierarchy.push(...leaves); + + hierarchy.forEach((element) => delete element.depth); + + return hierarchy; +} + +function getTrees(seeds: HierarchyElement[]) { + const stack = seeds.slice(); + + const trees: HierarchyElement[][] = []; + + while (stack.length > 0) { + const seed = stack.shift()!; + + let tree = findBranches(seed); + + tree.forEach((branch) => { + const idx = stack.indexOf(branch); + + if (idx !== -1) stack.splice(idx, 1); + }); + + tree = sortBranches(tree); + + tree = growBranches(tree); + + tree = pruneBranches(tree); + + trees.push(tree); + } + + return trees; +} + +function findBranches( + branch: HierarchyElement, + tree: HierarchyElement[] = [], + depth: number = 0, +) { + if (tree.includes(branch)) { + branch.depth = Math.min(branch.depth!, depth); + + return tree; + } + + tree.push(branch); + + branch.depth = depth; + + branch.parents?.forEach((parent) => findBranches(parent, tree, depth - 1)); + + branch.children?.forEach((child) => findBranches(child, tree, depth + 1)); + + return tree; +} + +function sortBranches(tree: HierarchyElement[]) { + tree = tree.slice(); + + tree.sort((a, b) => b.text.localeCompare(a.text)); + + tree.forEach((branch) => { + branch.parents?.sort((a, b) => a.text.localeCompare(b.text)); + + branch.children?.sort((a, b) => a.text.localeCompare(b.text)); + }); + + const reverseTree: HierarchyElement[] = []; + + const visited: Set = new Set(); + + function visit(branch: HierarchyElement) { + visited.add(branch); + + branch.children?.forEach( + (child) => !visited.has(child) && visit(child), + ); + + reverseTree.push(branch); + } + + tree.forEach((branch) => !visited.has(branch) && visit(branch)); + + return reverseTree.reverse(); +} + +function growBranches(tree: HierarchyElement[]) { + tree = tree.slice(); + + const roots = tree.filter((branch) => !branch.parents?.length); + + const minDepth = Math.min(...roots.map((branch) => branch.depth!)); + + roots.forEach((root) => { + while (root.depth! > minDepth) { + const newRoot = { + html: "", + text: "", + children: [root], + class: "tsd-hierarchy-spacer", + depth: root.depth! - 1, + }; + + root.parents = [newRoot]; + + tree.splice(tree.indexOf(root), 0, newRoot); + + root = newRoot; + } + }); + + return tree; +} + +function pruneBranches( + tree: HierarchyElement[], + branches?: HierarchyElement[], + seenBranches: Set = new Set(), +) { + if (!branches) + branches = tree.filter((branch) => !branch.parents?.length).reverse(); + + branches.forEach((branch) => { + if (seenBranches.has(branch)) return; + + seenBranches.add(branch); + + branch.children = branch.children?.filter((child) => { + if (!seenBranches.has(child)) return true; + + child.parents = child.parents?.filter( + (parent) => parent !== branch, + ); + + return false; + }); + + if (!branch.children?.length) { + delete branch.children; + + return; + } + + pruneBranches(tree, branch.children, seenBranches); + }); + + return tree; +} + +function buildList( + branches: HierarchyElement[], + baseUrl: string, + isRoots: boolean = true, +) { + if (!branches.length) return undefined; + + const list = document.createElement("ul"); + list.classList.add("tsd-hierarchy"); + + if (!isRoots) list.classList.add("tsd-full-hierarchy"); + + branches.forEach((branch) => { + const item = document.createElement("li"); + item.className = branch.class; + list.append(item); + + if (branch.text) { + if (item.classList.contains("tsd-hierarchy-target")) { + item.innerHTML += `${branch.text}`; + } else { + item.innerHTML += branch.html; + + const anchors = item.querySelectorAll("a"); + + anchors.forEach((anchor) => { + const href = anchor.getAttribute("href"); + + if ( + typeof href !== "string" || + /^[a-zA-Z]+:\/\//.test(href) + ) + return; + + anchor.setAttribute("href", baseUrl + href); + }); + } + } + + if (!branch.children?.length) return; + + const childrenList = buildList(branch.children, baseUrl, false); + + if (childrenList) item.append(childrenList); + }); + + return list; +} diff --git a/src/lib/output/themes/default/layouts/default.tsx b/src/lib/output/themes/default/layouts/default.tsx index ac8219ebb..c51de7e51 100644 --- a/src/lib/output/themes/default/layouts/default.tsx +++ b/src/lib/output/themes/default/layouts/default.tsx @@ -2,7 +2,7 @@ import type { RenderTemplate } from "../../.."; import type { Reflection } from "../../../../models"; import { JSX, Raw } from "../../../../utils"; import type { PageEvent } from "../../../events"; -import { getDisplayName } from "../../lib"; +import { getDisplayName, getHierarchyRoots } from "../../lib"; import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext"; export const defaultLayout = ( @@ -35,6 +35,9 @@ export const defaultLayout = ( + {!!getHierarchyRoots(props.project).length && ( + + )} {context.hook("head.end", context)} diff --git a/src/lib/output/themes/default/partials/hierarchy.tsx b/src/lib/output/themes/default/partials/hierarchy.tsx index 5d13d14fa..bf13691d9 100644 --- a/src/lib/output/themes/default/partials/hierarchy.tsx +++ b/src/lib/output/themes/default/partials/hierarchy.tsx @@ -15,29 +15,42 @@ function hasAnyLinkedReferenceType(h: DeclarationHierarchy | undefined): boolean return hasAnyLinkedReferenceType(h.next); } -export function hierarchy(context: DefaultThemeRenderContext, props: DeclarationHierarchy | undefined) { - if (!props) return; - - const fullLink = hasAnyLinkedReferenceType(props) ? ( - <> - {" "} - ( - - {context.i18n.theme_hierarchy_view_full()} - - ) - - ) : ( - <> - ); +export function hierarchy(context: DefaultThemeRenderContext, typeHierarchy: DeclarationHierarchy | undefined) { + if (!typeHierarchy) return; + + const summaryLink = + context.options.getValue("includeHierarchySummary") && hasAnyLinkedReferenceType(typeHierarchy) ? ( + <> + {" "} + ( + + {context.i18n.theme_hierarchy_view_summary()} + + ) + + ) : ( + <> + ); return ( -
+
+ +

{context.i18n.theme_hierarchy()} - {fullLink} + {summaryLink}{" "} +

- {hierarchyList(context, props)} + + {hierarchyList(context, typeHierarchy)}
); } @@ -46,8 +59,10 @@ function hierarchyList(context: DefaultThemeRenderContext, props: DeclarationHie return (
    {props.types.map((item, i, l) => ( -
  • - {props.isTarget ? {item.toString()} : context.type(item)} +
  • + {props.isTarget ? {item.toString()} : context.type(item)} {i === l.length - 1 && !!props.next && hierarchyList(context, props.next)}
  • ))} diff --git a/src/lib/output/themes/default/templates/hierarchy.tsx b/src/lib/output/themes/default/templates/hierarchy.tsx index 0a641acfc..6f0e75c67 100644 --- a/src/lib/output/themes/default/templates/hierarchy.tsx +++ b/src/lib/output/themes/default/templates/hierarchy.tsx @@ -1,46 +1,226 @@ import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext"; import type { PageEvent } from "../../../events"; import { JSX } from "../../../../utils"; -import { getHierarchyRoots } from "../../lib"; -import type { DeclarationReflection, ProjectReflection } from "../../../../models"; +import { DeclarationReflection, type ProjectReflection, ReferenceType, ReflectionKind } from "../../../../models"; -function fullHierarchy( - context: DefaultThemeRenderContext, - root: DeclarationReflection, - seen = new Set(), -) { - if (seen.has(root)) return; - seen.add(root); +interface HierarchyElement { + text: string; + path?: string; + kind?: ReflectionKind; + parents?: HierarchyElement[]; + children?: HierarchyElement[]; + depth?: number; +} + +export function hierarchyTemplate(context: DefaultThemeRenderContext, props: PageEvent) { + const trees = getTrees(props.project); - // Note: We don't use root.anchor for the anchor, because those are built on a per page basis. - // And classes/interfaces get their own page, so all the anchors will be empty anyways. - // Full name should be safe here, since this list only includes classes/interfaces. return ( -
  • - - - {context.icons[root.kind]()} - {root.name} - -
      - {root.implementedBy?.map((child) => { - return child.reflection && fullHierarchy(context, child.reflection as DeclarationReflection, seen); - })} - {root.extendedBy?.map((child) => { - return child.reflection && fullHierarchy(context, child.reflection as DeclarationReflection, seen); - })} -
    -
  • +
    +

    {context.i18n.theme_hierarchy_summary()}

    + + {trees.map((tree) => + hierarchyList( + context, + tree.filter((branch) => !branch.parents?.length), + ), + )} +
    ); } -export function hierarchyTemplate(context: DefaultThemeRenderContext, props: PageEvent) { +function getTrees(project: ProjectReflection) { + const stack = getStack(project); + + const trees: HierarchyElement[][] = []; + + while (stack.length > 0) { + const seed = stack.shift()!; + + let tree = findBranches(seed); + + tree.forEach((branch) => { + const idx = stack.indexOf(branch); + + if (idx !== -1) stack.splice(idx, 1); + }); + + if (tree.length <= 1) continue; + + tree = sortBranches(tree); + + tree = growBranches(tree); + + tree = pruneBranches(tree); + + trees.push(tree); + } + + return trees; +} + +function getStack(project: ProjectReflection) { + const stack = (project.getReflectionsByKind(ReflectionKind.ClassOrInterface) as DeclarationReflection[]) + .filter((reflection) => reflection.extendedTypes?.length || reflection.extendedBy?.length) + .map((reflection) => ({ + // Full name should be safe here, since this list only includes classes/interfaces. + text: reflection.getFullName(), + path: reflection.url!, + kind: reflection.kind, + parents: reflection.extendedTypes + ?.map((type) => + !(type instanceof ReferenceType) || !(type.reflection instanceof DeclarationReflection) + ? undefined + : { path: type.reflection.url! }, + ) + .filter((parent) => !!parent), + children: reflection.extendedBy + ?.map((type) => + !(type instanceof ReferenceType) || !(type.reflection instanceof DeclarationReflection) + ? undefined + : { path: type.reflection.url! }, + ) + .filter((child) => !!child), + })) as HierarchyElement[]; + + stack.forEach((element) => { + element.parents = element.parents + ?.map((parent) => stack.find(({ path }) => path === parent.path)) + .filter((parent) => !!parent); + + if (!element.parents?.length) delete element.parents; + + element.children = element.children + ?.map((child) => stack.find(({ path }) => path === child.path)) + .filter((child) => !!child); + + if (!element.children?.length) delete element.children; + }); + + return stack; +} + +function findBranches(branch: HierarchyElement, tree: HierarchyElement[] = [], depth: number = 0) { + if (tree.includes(branch)) { + branch.depth = Math.min(branch.depth!, depth); + + return tree; + } + + tree.push(branch); + + branch.depth = depth; + + branch.parents?.forEach((parent) => findBranches(parent, tree, depth - 1)); + + branch.children?.forEach((child) => findBranches(child, tree, depth + 1)); + + return tree; +} + +function sortBranches(tree: HierarchyElement[]) { + tree = tree.slice(); + + tree.sort((a, b) => b.text.localeCompare(a.text)); + + tree.forEach((branch) => { + branch.parents?.sort((a, b) => a.text.localeCompare(b.text)); + + branch.children?.sort((a, b) => a.text.localeCompare(b.text)); + }); + + const reverseTree: HierarchyElement[] = []; + + const visited: Set = new Set(); + + function visit(branch: HierarchyElement) { + visited.add(branch); + + branch.children?.forEach((child) => !visited.has(child) && visit(child)); + + reverseTree.push(branch); + } + + tree.forEach((branch) => !visited.has(branch) && visit(branch)); + + return reverseTree.reverse(); +} + +function growBranches(tree: HierarchyElement[]) { + tree = tree.slice(); + + const roots = tree.filter((branch) => !branch.parents?.length); + + const minDepth = Math.min(...roots.map((branch) => branch.depth!)); + + roots.forEach((root) => { + while (root.depth! > minDepth) { + const newRoot = { + text: "", + children: [root], + depth: root.depth! - 1, + }; + + root.parents = [newRoot]; + + tree.splice(tree.indexOf(root), 0, newRoot); + + root = newRoot; + } + }); + + return tree; +} + +function pruneBranches( + tree: HierarchyElement[], + branches?: HierarchyElement[], + seenBranches: Set = new Set(), +) { + if (!branches) branches = tree.filter((branch) => !branch.parents?.length).reverse(); + + branches.forEach((branch) => { + if (seenBranches.has(branch)) return; + + seenBranches.add(branch); + + branch.children = branch.children?.filter((child) => { + if (!seenBranches.has(child)) return true; + + child.parents = child.parents?.filter((parent) => parent !== branch); + + return false; + }); + + if (!branch.children?.length) { + delete branch.children; + + return; + } + + pruneBranches(tree, branch.children, seenBranches); + }); + + return tree; +} + +function hierarchyList(context: DefaultThemeRenderContext, branches: HierarchyElement[], isRoots: boolean = true) { return ( - <> -

    {context.i18n.theme_class_hierarchy_title()}

    - {getHierarchyRoots(props.project).map((root) => ( -
      {fullHierarchy(context, root)}
    + ); } diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index d9236a2a3..536c55782 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -189,6 +189,7 @@ export interface TypeDocOptionMap { readme: boolean; document: boolean; }; + includeHierarchySummary: boolean; visibilityFilters: ManuallyValidatedOption<{ protected?: boolean; private?: boolean; diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index 963ea5e8b..9bd079465 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -574,6 +574,13 @@ export function addTypeDocOptions(options: Pick) { }, }); + options.addDeclaration({ + name: "includeHierarchySummary", + help: (i18n) => i18n.help_includeHierarchySummary(), + type: ParameterType.Boolean, + defaultValue: true, + }); + options.addDeclaration({ name: "visibilityFilters", help: (i18n) => i18n.help_visibilityFilters(), diff --git a/static/style.css b/static/style.css index 178bfb023..cb5ef7993 100644 --- a/static/style.css +++ b/static/style.css @@ -690,29 +690,58 @@ input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark { margin: 0.75rem 0.75rem 0 0; } +.tsd-hierarchy input, +.tsd-hierarchy:not(:has(.tsd-hierarchy-distant-relative)) label, +.tsd-hierarchy input:not(:checked) ~ h4 label .collapse, +.tsd-hierarchy input:checked ~ h4 label .expand { + display: none; +} + +.tsd-hierarchy h4 label span { + color: var(--color-link); + cursor: pointer; +} + +.tsd-hierarchy h4 label:hover span { + text-decoration: underline; +} + .tsd-hierarchy { - list-style: square; + list-style: none; margin: 0; } -.tsd-hierarchy .target { + +.tsd-hierarchy-item { + list-style: square; +} + +.tsd-hierarchy-target > span { font-weight: bold; } -.tsd-full-hierarchy:not(:last-child) { - margin-bottom: 1em; - padding-bottom: 1em; - border-bottom: 1px solid var(--color-accent); +.tsd-hierarchy input:not(:checked) ~ .tsd-full-hierarchy { + display: none; } -.tsd-full-hierarchy, -.tsd-full-hierarchy ul { - list-style: none; - margin: 0; - padding: 0; + +.tsd-hierarchy input:checked ~ .tsd-hierarchy:not(.tsd-full-hierarchy) { + display: none; } -.tsd-full-hierarchy ul { + +ul.tsd-full-hierarchy:has(~ ul.tsd-full-hierarchy) { + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--color-accent); +} + +ul.tsd-full-hierarchy:has(svg) { padding-left: 1.5rem; } -.tsd-full-hierarchy a { + +ul.tsd-full-hierarchy:has(svg) li { + list-style: none; +} + +ul.tsd-full-hierarchy:has(svg) a { padding: 0.25rem 0 !important; font-size: 1rem; display: inline-flex;