A plugin for TypeDoc that enables TypeScript API documentation to be generated in Markdown.
—
Event system for hooking into the markdown generation lifecycle, enabling customization at various rendering stages through page events, renderer events, and hook points.
Event emitted before and after the markdown content of each page is rendered, providing access to page data and content modification.
/**
* An event emitted before and after the markdown of a page is rendered.
* Provides access to page model, content, and metadata for customization.
*/
class MarkdownPageEvent<out Model extends RouterTarget = RouterTarget> {
/** The project the renderer is currently processing */
project: ProjectReflection;
/** The filename the page will be written to */
filename: string;
/** The URL this page will be located at */
url: string;
/** The type of page this is (index, reflection, document, etc.) */
pageKind: PageKind;
/** The model that should be rendered on this page */
readonly model: Model;
/** The final markdown content of this page */
contents: string;
/** The frontmatter of this page represented as a key-value object */
frontmatter?: Record<string, any>;
/** Page headings for table of contents generation */
pageHeadings: PageHeading[];
/** Page sections for content organization */
pageSections: any;
/** Async jobs to run before page write */
preWriteAsyncJobs: Array<(page: MarkdownPageEvent) => Promise<void>>;
/**
* Creates a new page event
* @param model - The model to render on this page
*/
constructor(model: Model);
/**
* Hidden method to start a new section in the page
*/
startNewSection(): void;
/**
* Type guard to check if this is a reflection event
* @returns True if this event is for a reflection page
*/
isReflectionEvent(): this is MarkdownPageEvent<Reflection>;
/** Event name triggered before a document will be rendered */
static readonly BEGIN = 'beginPage';
/** Event name triggered after a document has been rendered */
static readonly END = 'endPage';
}Usage Example:
import { MarkdownPageEvent } from "typedoc-plugin-markdown";
// Listen for page events
renderer.on(MarkdownPageEvent.BEGIN, (page) => {
console.log(`Starting to render: ${page.filename}`);
// Modify frontmatter before rendering
page.frontmatter = {
...page.frontmatter,
author: 'Documentation Team',
lastModified: new Date().toISOString(),
pageType: page.pageKind
};
});
renderer.on(MarkdownPageEvent.END, (page) => {
console.log(`Finished rendering: ${page.filename}`);
// Post-process content
page.contents = page.contents.replace(
/<!-- INJECT_TOC -->/g,
generateTableOfContents(page.pageHeadings)
);
// Add custom footer
page.contents += '\n\n---\n*Generated with TypeDoc Plugin Markdown*';
});Event emitted at the beginning and end of the entire rendering process, providing access to project-wide data and navigation.
/**
* An event emitted at the beginning and end of the rendering process.
* Provides access to project data, output directory, and page collection.
*/
class MarkdownRendererEvent {
/** The project the renderer is currently processing */
readonly project: ProjectReflection;
/** The path of the directory the documentation should be written to */
readonly outputDirectory: string;
/** A list of all pages that will be generated */
pages: PageDefinition[];
/** The navigation structure of the project */
navigation?: NavigationItem[];
/**
* Creates a new renderer event
* @param outputDirectory - Directory path for output
* @param project - The TypeDoc project being rendered
* @param pages - Array of page definitions to generate
*/
constructor(
outputDirectory: string,
project: ProjectReflection,
pages: PageDefinition[]
);
/** Event name triggered before the renderer starts rendering */
static readonly BEGIN = 'beginRender';
/** Event name triggered after the renderer has written all documents */
static readonly END = 'endRender';
}Usage Example:
import { MarkdownRendererEvent } from "typedoc-plugin-markdown";
// Listen for renderer events
renderer.on(MarkdownRendererEvent.BEGIN, (event) => {
console.log(`Starting documentation generation for ${event.project.name}`);
console.log(`Output directory: ${event.outputDirectory}`);
console.log(`Total pages to generate: ${event.pages.length}`);
// Pre-process pages list
event.pages = event.pages.filter(page =>
!page.filename.includes('.internal.')
);
// Generate navigation if not present
if (!event.navigation) {
event.navigation = generateCustomNavigation(event.project);
}
});
renderer.on(MarkdownRendererEvent.END, (event) => {
console.log(`Documentation generation complete`);
// Generate index file
const indexContent = generateProjectIndex(event.project, event.navigation);
writeFileSync(
path.join(event.outputDirectory, 'index.md'),
indexContent
);
// Generate sitemap
generateSitemap(event.pages, event.outputDirectory);
});Extended renderer interface with custom hooks and async job support for markdown-specific functionality.
/**
* The MarkdownRenderer extends TypeDoc's Renderer with custom hooks and async jobs
*/
interface MarkdownRenderer extends Renderer {
/** Dedicated markdown hooks for injecting content */
markdownHooks: EventHooks<MarkdownRendererHooks, string>;
/** Pre-render async jobs that run before documentation generation */
preRenderAsyncJobs: Array<(output: MarkdownRendererEvent) => Promise<void>>;
/** Post-render async jobs that run after documentation generation */
postRenderAsyncJobs: Array<(output: MarkdownRendererEvent) => Promise<void>>;
/** Store metadata about packages for packages mode */
packagesMeta: Record<string, { description: string; options: Options }>;
/**
* Event listener for page events
* @param event - Page event type
* @param callback - Callback function for page events
*/
on(
event: typeof MarkdownPageEvent.BEGIN | typeof MarkdownPageEvent.END,
callback: (page: MarkdownPageEvent) => void
): void;
/**
* Event listener for renderer events
* @param event - Renderer event type
* @param callback - Callback function for renderer events
*/
on(
event: typeof MarkdownRendererEvent.BEGIN | typeof MarkdownRendererEvent.END,
callback: (event: MarkdownRendererEvent) => void
): void;
/**
* Define a new theme for the renderer
* @param name - Theme name
* @param theme - Theme constructor class
*/
defineTheme(name: string, theme: new (renderer: Renderer) => MarkdownTheme): void;
}Describes the hooks available for injecting content at various points in the markdown rendering process.
/**
* Describes the hooks available to inject output in the markdown theme.
* Each hook receives a MarkdownThemeContext for accessing page data and utilities.
*/
interface MarkdownRendererHooks {
/** Applied at the start of markdown output */
['page.begin']: [MarkdownThemeContext];
/** Applied at the end of markdown output */
['page.end']: [MarkdownThemeContext];
/** Applied before main markdown content is rendered */
['content.begin']: [MarkdownThemeContext];
/** Applied at start of markdown output on index page only */
['index.page.begin']: [MarkdownThemeContext];
/** Applied at end of markdown output on index page only */
['index.page.end']: [MarkdownThemeContext];
}Hook Usage Example:
import { MarkdownThemeContext } from "typedoc-plugin-markdown";
// Register hooks for content injection
renderer.markdownHooks.on('page.begin', (context: MarkdownThemeContext) => {
// Add custom header to all pages
return `<!-- Generated: ${new Date().toISOString()} -->
<!-- Project: ${context.page.project.name} -->
`;
});
renderer.markdownHooks.on('page.end', (context: MarkdownThemeContext) => {
// Add custom footer to all pages
const relativeHome = context.relativeURL('/');
return `
---
[Back to Home](${relativeHome}) | [View Source](${getSourceUrl(context.page.model)})
`;
});
renderer.markdownHooks.on('content.begin', (context: MarkdownThemeContext) => {
// Add table of contents before main content
if (context.page.pageHeadings?.length > 0) {
return generateTOC(context.page.pageHeadings);
}
return '';
});
renderer.markdownHooks.on('index.page.begin', (context: MarkdownThemeContext) => {
// Special header for index page only
return `# ${context.page.project.name} Documentation
Welcome to the API documentation for ${context.page.project.name}.
`;
});System for registering asynchronous jobs that run before or after the rendering process.
Pre-render Jobs:
// Register pre-render job
renderer.preRenderAsyncJobs.push(async (event: MarkdownRendererEvent) => {
console.log('Pre-processing documentation...');
// Generate additional metadata
const metadata = await analyzeProject(event.project);
// Store in renderer for use during rendering
renderer.packagesMeta[event.project.name] = {
description: metadata.description,
options: event.project.options
};
// Fetch external data
const changelog = await fetchChangelog(event.project);
// Add changelog page
event.pages.push({
model: event.project,
filename: 'CHANGELOG.md',
url: 'changelog.html',
contents: changelog
});
});Post-render Jobs:
// Register post-render job
renderer.postRenderAsyncJobs.push(async (event: MarkdownRendererEvent) => {
console.log('Post-processing documentation...');
// Generate search index
const searchIndex = await generateSearchIndex(event.pages);
await writeFile(
path.join(event.outputDirectory, 'search-index.json'),
JSON.stringify(searchIndex)
);
// Optimize images
await optimizeImages(event.outputDirectory);
// Generate RSS feed
const feed = generateRSSFeed(event.project, event.pages);
await writeFile(
path.join(event.outputDirectory, 'feed.xml'),
feed
);
console.log('Documentation post-processing complete');
});Complete Event Handling Example:
import {
MarkdownPageEvent,
MarkdownRendererEvent,
MarkdownThemeContext
} from "typedoc-plugin-markdown";
class DocumentationProcessor {
setupEventHandlers(renderer: MarkdownRenderer) {
// Page-level event handling
renderer.on(MarkdownPageEvent.BEGIN, this.handlePageBegin.bind(this));
renderer.on(MarkdownPageEvent.END, this.handlePageEnd.bind(this));
// Renderer-level event handling
renderer.on(MarkdownRendererEvent.BEGIN, this.handleRenderBegin.bind(this));
renderer.on(MarkdownRendererEvent.END, this.handleRenderEnd.bind(this));
// Content injection hooks
renderer.markdownHooks.on('page.begin', this.injectPageHeader.bind(this));
renderer.markdownHooks.on('page.end', this.injectPageFooter.bind(this));
renderer.markdownHooks.on('content.begin', this.injectTOC.bind(this));
// Async jobs
renderer.preRenderAsyncJobs.push(this.preRenderSetup.bind(this));
renderer.postRenderAsyncJobs.push(this.postRenderCleanup.bind(this));
}
private handlePageBegin(page: MarkdownPageEvent) {
console.log(`Rendering page: ${page.filename}`);
// Set up frontmatter
page.frontmatter = {
title: this.getPageTitle(page.model),
type: page.pageKind,
generated: new Date().toISOString()
};
}
private handlePageEnd(page: MarkdownPageEvent) {
// Validate generated content
this.validatePageContent(page);
// Add analytics tracking
page.contents += this.getAnalyticsCode(page);
}
private async preRenderSetup(event: MarkdownRendererEvent) {
// Initialize external services
await this.initializeServices();
// Pre-process project data
this.processProjectMetadata(event.project);
}
private async postRenderCleanup(event: MarkdownRendererEvent) {
// Generate additional files
await this.generateSupportingFiles(event);
// Clean up temporary resources
await this.cleanup();
}
}Install with Tessl CLI
npx tessl i tessl/npm-typedoc-plugin-markdown