UNPKG

typedoc-plugin-markdown

Version:

A plugin for TypeDoc that enables TypeScript API documentation to be generated in Markdown.

191 lines (190 loc) 7.54 kB
import { MarkdownPageEvent, MarkdownRendererEvent, } from '../events/index.js'; import { constants } from '../options/index.js'; import { CategoryRouter, GroupRouter, KindDirRouter, KindRouter, MemberRouter, ModuleRouter, StructureDirRouter, StructureRouter, } from '../router/index.js'; import { MarkdownTheme } from '../theme/index.js'; import * as fs from 'fs'; import * as path from 'path'; import { i18n, } from 'typedoc'; import { formatWithPrettierIfAvailable } from './prettier.js'; import { copyMediaFiles, writeFileSync } from './utils.js'; /** * The render method for the Markdown plugin * * @remarks * * This is essentially a copy the default theme render method with some adjustments. * * - Removes unnecessary async calls to load highlighters only required for html theme. * - Removes hooks logic that are jsx specific. * - Adds any logic specific to markdown rendering. */ export async function render(renderer, project, outputDirectory) { // Setup output directory if (!prepareRouter(renderer) || !(await prepareOutputDirectory(renderer, outputDirectory))) { return; } prepareTheme(renderer); const pages = renderer.router.buildPages(project); const output = new MarkdownRendererEvent(outputDirectory, project, pages); output.navigation = renderer.theme.getNavigation(project); renderer.trigger(MarkdownRendererEvent.BEGIN, output); await executeAsyncRendererJobs(renderer.preRenderAsyncJobs, output); // for backwards compatibility await executeAsyncRendererJobs(renderer.preMarkdownRenderAsyncJobs, output); renderer.application.logger.verbose(`There are ${pages.length} pages to write.`); for (const page of pages) { await renderDocument(renderer, outputDirectory, page, project); } writeNavigationJson(renderer, output.navigation); await executeAsyncRendererJobs(renderer.postRenderAsyncJobs, output); // for backwards compatibility await executeAsyncRendererJobs(renderer.postMarkdownRenderAsyncJobs, output); renderer.trigger(MarkdownRendererEvent.END, output); copyMediaFiles(project, outputDirectory); renderer.router = void 0; renderer.theme = void 0; } function writeNavigationJson(renderer, navigation) { const navigationJson = renderer.application.options.getValue('navigationJson'); if (!navigationJson) return; try { writeFileSync(navigationJson, JSON.stringify(navigation, null, 2)); } catch { renderer.application.logger.warn(i18n.could_not_write_0(navigationJson)); } } /** * Output directory setup (this is essentially copied from TypeDoc) */ async function prepareOutputDirectory(renderer, outputDirectory) { if (renderer.application.options.getValue('cleanOutputDir')) { try { fs.rmSync(outputDirectory, { recursive: true, force: true }); } catch { renderer.application.logger.warn(i18n.could_not_empty_output_directory_0(outputDirectory)); return false; } } try { fs.mkdirSync(outputDirectory, { recursive: true }); } catch { renderer.application.logger.error(i18n.could_not_create_output_directory_0(outputDirectory)); return false; } if (renderer.application.options.isSet('githubPages') && renderer.application.options.getValue('githubPages')) { try { writeFileSync(path.join(outputDirectory, '.nojekyll'), ''); } catch { renderer.application.logger.warn(i18n.could_not_write_0(path.join(outputDirectory, '.nojekyll'))); } } return true; } /** * Prepare the Router for the renderer */ function prepareRouter(renderer) { const routerName = getRouterName(renderer); const router = getRouter(renderer, routerName); if (!router) { renderer.application.logger.error(i18n.router_0_is_not_defined_available_are_1(routerName, constants.AVAILABLE_ROUTERS.join(', '))); return false; } renderer.router = new router(renderer.application); return true; } function getRouterName(renderer) { const routerOption = renderer.application.options.getValue('router'); if (!renderer.application.options.isSet('router')) { if (renderer.application.options.isSet('outputFileStrategy')) { const outputFileStrategy = renderer.application.options.getValue('outputFileStrategy'); return outputFileStrategy === 'modules' ? 'module' : 'member'; } else { return 'member'; } } return routerOption; } function getRouter(renderer, routerName) { const routers = renderer.routers; const pluginRouters = new Map([ // custom routers ['member', MemberRouter], ['module', ModuleRouter], // core routers (decorated) ['kind', KindRouter], ['kind-dir', KindDirRouter], ['structure', StructureRouter], ['structure-dir', StructureDirRouter], ['group', GroupRouter], ['category', CategoryRouter], ]); if (constants.AVAILABLE_ROUTERS.includes(routerName)) { return pluginRouters.get(routerName); } return routers.get(routerName); } /** * Prepare the Theme for the renderer */ function prepareTheme(renderer) { const themes = renderer.themes; const themeName = getThemeName(renderer); const theme = themes.get(themeName); const ctor = new theme(renderer); if (ctor instanceof MarkdownTheme) { renderer.theme = ctor; return; } renderer.application.logger.warn(`[typedoc-plugin-markdown]: Skipping theme "${themeName}" as it is not an instance of the Markdown theme.`); renderer.theme = new (themes.get('markdown'))(renderer); } function getThemeName(renderer) { const themeOption = renderer.application.options.getValue('theme'); return themeOption === 'default' ? 'markdown' : themeOption; } /** * The main rendering method for a document. */ async function renderDocument(renderer, outputDirectory, page, project) { const formatWithPrettier = renderer.application.options.getValue('formatWithPrettier'); const pageEvent = new MarkdownPageEvent(page.model); pageEvent.url = page.url; pageEvent.filename = path.join(outputDirectory, page.url); pageEvent.pageKind = page.kind; pageEvent.project = project; renderer.trigger(MarkdownPageEvent.BEGIN, pageEvent); pageEvent.contents = renderer.theme.render(pageEvent); if (formatWithPrettier) { pageEvent.preWriteAsyncJobs.push(async (pageEvent) => { pageEvent.contents = await formatWithPrettierIfAvailable(renderer, pageEvent.contents); }); } renderer.trigger(MarkdownPageEvent.END, pageEvent); await executeAsyncPageJobs(pageEvent.preWriteAsyncJobs, pageEvent); try { writeFileSync(pageEvent.filename, pageEvent.contents); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { renderer.application.logger.error('renderer.application.i18n.could_not_write_0(event.filename)'); } } // Helper to execute async renderer jobs export async function executeAsyncRendererJobs(jobs, output) { await Promise.all(jobs.map((job) => job(output))); jobs = []; // Clear job queue } // Helper to execute async page jobs (page jobs should be run in series) export async function executeAsyncPageJobs(jobs, page) { for (const job of jobs) { await job(page); } jobs = []; // Clear job queue }