typedoc-plugin-markdown
Version:
A plugin for TypeDoc that enables TypeScript API documentation to be generated in Markdown.
245 lines (244 loc) • 10.4 kB
JavaScript
import { isQuoted, removeFirstScopedDirectory, toPascalCase, } from '../libs/utils/index.js';
import { getHierarchyRoots } from '../theme/lib/index.js';
import path from 'path';
import { BaseRouter, EntryPointStrategy, PageKind, Reflection, ReflectionKind, Slugger, } from 'typedoc';
export class MarkdownRouter extends BaseRouter {
extension = this.application.options.getValue('fileExtension');
outputFileStrategy = this.application.options.getValue('outputFileStrategy');
entryModule = this.application.options.getValue('entryModule');
ignoreScopes = this.application.options.getValue('excludeScopesInPaths');
modulesFileName = path.parse(this.application.options.getValue('modulesFileName')).name;
entryFileName = path.parse(this.application.options.getValue('entryFileName'))
.name;
isPackages = this.application.options.getValue('entryPointStrategy') ===
EntryPointStrategy.Packages;
membersWithOwnFile = this.application.options.getValue('membersWithOwnFile');
mergeReadme = this.application.options.getValue('mergeReadme');
anchorPrefix = this.application.options.getValue('anchorPrefix');
directories = new Map([
[ReflectionKind.Class, 'classes'],
[ReflectionKind.Interface, 'interfaces'],
[ReflectionKind.Enum, 'enumerations'],
[ReflectionKind.Namespace, 'namespaces'],
[ReflectionKind.TypeAlias, 'type-aliases'],
[ReflectionKind.Function, 'functions'],
[ReflectionKind.Variable, 'variables'],
[ReflectionKind.Document, 'documents'],
]);
kindsToString = new Map([
[ReflectionKind.Module, 'Module'],
[ReflectionKind.Namespace, 'Namespace'],
[ReflectionKind.Document, 'Document'],
[ReflectionKind.Class, 'Class'],
[ReflectionKind.Interface, 'Interface'],
[ReflectionKind.Enum, 'Enum'],
[ReflectionKind.TypeAlias, 'TypeAlias'],
[ReflectionKind.Function, 'Function'],
[ReflectionKind.Variable, 'Variable'],
]);
tableAnchorRules = [
{
targetKind: ReflectionKind.Property,
parentKind: ReflectionKind.TypeAlias,
option: 'typeAliasPropertiesFormat',
},
{
targetKind: ReflectionKind.Property,
parentKind: ReflectionKind.Interface,
option: 'interfacePropertiesFormat',
},
{
targetKind: ReflectionKind.Property,
parentKind: ReflectionKind.TypeLiteral,
option: 'typeDeclarationFormat',
},
{
targetKind: ReflectionKind.Property,
parentKind: ReflectionKind.Class,
option: 'classPropertiesFormat',
},
{
targetKind: ReflectionKind.EnumMember,
parentKind: ReflectionKind.Enum,
option: 'enumMembersFormat',
},
];
buildPages(project) {
this.usedFileNames = new Set();
this.sluggers = new Map([
[project, new Slugger(this.sluggerConfiguration)],
]);
const pages = [];
// Note: The concept of "entryModule" is being deprecated in favour of custom router implementations.
// The concept is hard to understand and makes the code unnecessarily complex.
const entryModule = project?.groups?.[0]?.children.find((child) => child.name === this.entryModule);
if (entryModule) {
pages.push({
url: this.getFileName(this.entryFileName),
kind: PageKind.Reflection,
model: entryModule,
});
if (project.readme?.length) {
pages.push({
url: this.getFileName(this.entryFileName),
kind: PageKind.Index,
model: project,
});
this.fullUrls.set(project, pages[0].url);
}
}
else {
if (project.readme?.length && !this.mergeReadme) {
pages.push({
url: this.getFileName(this.entryFileName),
kind: PageKind.Index,
model: project,
});
pages.push({
url: this.getFileName(this.getModulesFileName(project)),
kind: PageKind.Reflection,
model: project,
});
}
else {
pages.push({
url: this.getFileName(this.entryFileName),
kind: PageKind.Reflection,
model: project,
});
}
this.fullUrls.set(project, pages[pages.length - 1].url);
}
if (this.application.options.isSet('includeHierarchySummary') &&
this.includeHierarchySummary &&
getHierarchyRoots(project)?.length) {
pages.push({
url: this.getFileName('hierarchy'),
kind: PageKind.Hierarchy,
model: project,
});
}
this.parseChildPages(project, pages);
return pages;
}
getAnchor(target) {
if (this.anchorPrefix) {
return `${this.anchorPrefix}${super.getAnchor(target)}`;
}
return super.getAnchor(target);
}
/**
* This is essentially a copy of the BaseRouter implementation, but adjusted to
* generate anchors in a way that is compatible with markdown links.
*/
buildAnchors(target, pageTarget) {
if (!(target instanceof Reflection) ||
!(pageTarget instanceof Reflection)) {
return;
}
if (!target.isDeclaration() &&
!target.isSignature() &&
!target.isTypeParameter() &&
!target.kindOf(ReflectionKind.TypeLiteral)) {
return;
}
// We support linking to reflections for types directly contained within an export
// but not any deeper. This is because TypeDoc may or may not render the type details
// for a property depending on whether or not it is deemed useful, and defining a link
// which might not be used may result in a link being generated which isn't valid. #2808.
// This should be kept in sync with the renderingChildIsUseful function.
if (target.kindOf(ReflectionKind.TypeLiteral) &&
(!target.parent?.kindOf(ReflectionKind.SomeExport) ||
target.parent.type?.type !== 'reflection')) {
return;
}
if (!target.kindOf(ReflectionKind.TypeLiteral)) {
let refl = target;
const parts = [refl.name];
while (refl.parent && refl.parent !== pageTarget) {
refl = refl.parent;
// Avoid duplicate names for signatures and useless __type in anchors
if (!refl.kindOf(ReflectionKind.TypeLiteral | ReflectionKind.FunctionOrMethod)) {
parts.unshift(refl.name);
}
}
// --------------------------------------------
// typedoc-plugin-markdown customization (start)
// --------------------------------------------
if (target.kindOf(ReflectionKind.CallSignature) &&
target.parent?.kindOf(ReflectionKind.Method)) {
return;
}
let toSlug = parts.join('.');
if (this.tableAnchorRules.some((r) => this.isTableAnchor(target, r.targetKind, r.parentKind, r.option))) {
toSlug = `${ReflectionKind.singularString(target.kind)}-${toSlug}`;
}
if (target.kindOf(ReflectionKind.Class) && target.flags?.isAbstract) {
toSlug = `abstract-${toSlug}`;
}
const anchor = this.getSlugger(pageTarget).slug(toSlug);
// ------------------------------------------
// typedoc-plugin-markdown customization (end)
// -------------------------------------------
this.fullUrls.set(target, this.fullUrls.get(pageTarget) + '#' + anchor);
this.anchors.set(target, anchor);
}
target.traverse((child) => {
this.buildAnchors(child, pageTarget);
return true;
});
}
isTableAnchor(target, targetKind, parentKind, option) {
return (!!target.kindOf(targetKind) &&
!!target.parent?.kindOf(parentKind) &&
this.application.options.getValue(option)
.toLowerCase()
.endsWith('table'));
}
parseChildPages(project, pages) {
for (const child of project.childrenIncludingDocuments || []) {
this.buildChildPages(child, pages);
}
}
getIdealBaseNameFlattened(reflection) {
const fullName = reflection.getFullName();
const fullNameParts = fullName.replace(/\//g, '.').split('.');
if (reflection.kind !== ReflectionKind.Module) {
if (reflection.kind === ReflectionKind.Document &&
reflection?.parent?.kind === ReflectionKind.Project) {
fullNameParts.splice(0, 0, toPascalCase(ReflectionKind.singularString(reflection.kind)));
}
else {
fullNameParts.splice(fullNameParts.length - 1, 0, toPascalCase(ReflectionKind.singularString(reflection.kind)));
}
}
const finalName = `${fullNameParts.join('.')}`
.replace(/"/g, '')
.replace(/ /g, '-')
.replace(/^\./g, '');
if (this.ignoreScopes) {
return removeFirstScopedDirectory(finalName, '.');
}
return finalName;
}
getReflectionAlias(reflection) {
let name = reflection.name;
if (isQuoted(reflection.name)) {
name = name.replace(/\//g, '_');
}
return name
.replace(/"/g, '')
.replace(/^_+|_+$/g, '')
.replace(/[<>]/g, '-');
}
getModulesFileName(reflection) {
if (this.modulesFileName) {
return this.modulesFileName;
}
if (this.isPackages && reflection.kind === ReflectionKind.Project) {
return 'packages';
}
const isModules = reflection.children?.every((child) => child.kind === ReflectionKind.Module);
return isModules ? 'modules' : 'globals';
}
}