typedoc
Version:
Create api documentation for TypeScript projects.
388 lines (387 loc) • 20.6 kB
JavaScript
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
var _, done = false;
for (var i = decorators.length - 1; i >= 0; i--) {
var context = {};
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
if (kind === "accessor") {
if (result === void 0) continue;
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
if (_ = accept(result.get)) descriptor.get = _;
if (_ = accept(result.set)) descriptor.set = _;
if (_ = accept(result.init)) initializers.unshift(_);
}
else if (_ = accept(result)) {
if (kind === "field") initializers.unshift(_);
else descriptor[key] = _;
}
}
if (target) Object.defineProperty(target, contextIn.name, descriptor);
done = true;
};
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
var useValue = arguments.length > 2;
for (var i = 0; i < initializers.length; i++) {
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
}
return useValue ? value : void 0;
};
/**
* Holds all logic used render and output the final documentation.
*
* The {@link Renderer} class is the central controller within this namespace. When invoked it creates
* an instance of {@link Theme} which defines the layout of the documentation and fires a
* series of {@link RendererEvent} events. Instances of {@link BasePlugin} can listen to these events and
* alter the generated output.
*/
import * as fs from "fs";
import * as path from "path";
import { IndexEvent, PageEvent, RendererEvent } from "./events.js";
import { writeFileSync } from "../utils/fs.js";
import { DefaultTheme } from "./themes/default/DefaultTheme.js";
import { AbstractComponent, Option } from "../utils/index.js";
import { EventHooks, i18n, JSX } from "#utils";
import { AssetsPlugin, HierarchyPlugin, IconsPlugin, JavascriptIndexPlugin, MarkedPlugin, NavigationPlugin, SitemapPlugin, } from "./plugins/index.js";
import { CategoryRouter, GroupRouter, KindDirRouter, KindRouter, StructureDirRouter, StructureRouter, } from "./router.js";
/**
* The renderer processes a {@link ProjectReflection} using a {@link Theme} instance and writes
* the emitted html documents to a output directory. You can specify which theme should be used
* using the `--theme <name>` command line argument.
*
* {@link Renderer} is a subclass of {@link EventDispatcher} and triggers a series of events while
* a project is being processed. You can listen to these events to control the flow or manipulate
* the output.
*
* * {@link Renderer.EVENT_BEGIN}<br>
* Triggered before the renderer starts rendering a project. The listener receives
* an instance of {@link RendererEvent}.
*
* * {@link Renderer.EVENT_BEGIN_PAGE}<br>
* Triggered before a document will be rendered. The listener receives an instance of
* {@link PageEvent}.
*
* * {@link Renderer.EVENT_END_PAGE}<br>
* Triggered after a document has been rendered, just before it is written to disc. The
* listener receives an instance of {@link PageEvent}.
*
* * {@link Renderer.EVENT_END}<br>
* Triggered after the renderer has written all documents. The listener receives
* an instance of {@link RendererEvent}.
*
* * {@link Renderer.EVENT_PREPARE_INDEX}<br>
* Triggered when the JavascriptIndexPlugin is preparing the search index. Listeners receive
* an instance of {@link IndexEvent}.
*
* @summary Writes HTML output from TypeDoc's models
* @group None
*/
let Renderer = (() => {
let _classSuper = AbstractComponent;
let _themeName_decorators;
let _themeName_initializers = [];
let _themeName_extraInitializers = [];
let _routerName_decorators;
let _routerName_initializers = [];
let _routerName_extraInitializers = [];
let _cleanOutputDir_decorators;
let _cleanOutputDir_initializers = [];
let _cleanOutputDir_extraInitializers = [];
let _cname_decorators;
let _cname_initializers = [];
let _cname_extraInitializers = [];
let _githubPages_decorators;
let _githubPages_initializers = [];
let _githubPages_extraInitializers = [];
let _cacheBust_decorators;
let _cacheBust_initializers = [];
let _cacheBust_extraInitializers = [];
let _pretty_decorators;
let _pretty_initializers = [];
let _pretty_extraInitializers = [];
return class Renderer extends _classSuper {
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
_themeName_decorators = [Option("theme")];
_routerName_decorators = [Option("router")];
_cleanOutputDir_decorators = [Option("cleanOutputDir")];
_cname_decorators = [Option("cname")];
_githubPages_decorators = [Option("githubPages")];
_cacheBust_decorators = [Option("cacheBust")];
_pretty_decorators = [Option("pretty")];
__esDecorate(this, null, _themeName_decorators, { kind: "accessor", name: "themeName", static: false, private: false, access: { has: obj => "themeName" in obj, get: obj => obj.themeName, set: (obj, value) => { obj.themeName = value; } }, metadata: _metadata }, _themeName_initializers, _themeName_extraInitializers);
__esDecorate(this, null, _routerName_decorators, { kind: "accessor", name: "routerName", static: false, private: false, access: { has: obj => "routerName" in obj, get: obj => obj.routerName, set: (obj, value) => { obj.routerName = value; } }, metadata: _metadata }, _routerName_initializers, _routerName_extraInitializers);
__esDecorate(this, null, _cleanOutputDir_decorators, { kind: "accessor", name: "cleanOutputDir", static: false, private: false, access: { has: obj => "cleanOutputDir" in obj, get: obj => obj.cleanOutputDir, set: (obj, value) => { obj.cleanOutputDir = value; } }, metadata: _metadata }, _cleanOutputDir_initializers, _cleanOutputDir_extraInitializers);
__esDecorate(this, null, _cname_decorators, { kind: "accessor", name: "cname", static: false, private: false, access: { has: obj => "cname" in obj, get: obj => obj.cname, set: (obj, value) => { obj.cname = value; } }, metadata: _metadata }, _cname_initializers, _cname_extraInitializers);
__esDecorate(this, null, _githubPages_decorators, { kind: "accessor", name: "githubPages", static: false, private: false, access: { has: obj => "githubPages" in obj, get: obj => obj.githubPages, set: (obj, value) => { obj.githubPages = value; } }, metadata: _metadata }, _githubPages_initializers, _githubPages_extraInitializers);
__esDecorate(this, null, _cacheBust_decorators, { kind: "accessor", name: "cacheBust", static: false, private: false, access: { has: obj => "cacheBust" in obj, get: obj => obj.cacheBust, set: (obj, value) => { obj.cacheBust = value; } }, metadata: _metadata }, _cacheBust_initializers, _cacheBust_extraInitializers);
__esDecorate(this, null, _pretty_decorators, { kind: "accessor", name: "pretty", static: false, private: false, access: { has: obj => "pretty" in obj, get: obj => obj.pretty, set: (obj, value) => { obj.pretty = value; } }, metadata: _metadata }, _pretty_initializers, _pretty_extraInitializers);
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
}
routers = new Map([
["kind", KindRouter],
["structure", StructureRouter],
["kind-dir", KindDirRouter],
["structure-dir", StructureDirRouter],
["group", GroupRouter],
["category", CategoryRouter],
]);
themes = new Map([
["default", DefaultTheme],
]);
/** @event */
static EVENT_BEGIN_PAGE = PageEvent.BEGIN;
/** @event */
static EVENT_END_PAGE = PageEvent.END;
/** @event */
static EVENT_BEGIN = RendererEvent.BEGIN;
/** @event */
static EVENT_END = RendererEvent.END;
/** @event */
static EVENT_PREPARE_INDEX = IndexEvent.PREPARE_INDEX;
/**
* A list of async jobs which must be completed *before* rendering output.
* They will be called after {@link RendererEvent.BEGIN} has fired, but before any files have been written.
*
* This may be used by plugins to register work that must be done to prepare output files. For example: asynchronously
* transform markdown to HTML.
*
* Note: This array is cleared after calling the contained functions on each {@link Renderer.render} call.
*/
preRenderAsyncJobs = [];
/**
* A list of async jobs which must be completed after rendering output files but before generation is considered successful.
* These functions will be called after all documents have been written to the filesystem.
*
* This may be used by plugins to register work that must be done to finalize output files. For example: asynchronously
* generating an image referenced in a render hook.
*
* Note: This array is cleared after calling the contained functions on each {@link Renderer.render} call.
*/
postRenderAsyncJobs = [];
/**
* The theme that is used to render the documentation.
*/
theme;
/**
* The router which is used to determine the pages to render and
* how to link between pages.
*/
router;
/**
* Hooks which will be called when rendering pages.
* Note:
* - Hooks added during output will be discarded at the end of rendering.
* - Hooks added during a page render will be discarded at the end of that page's render.
*
* See {@link RendererHooks} for a description of each available hook, and when it will be called.
*/
hooks = new EventHooks();
#themeName_accessor_storage = __runInitializers(this, _themeName_initializers, void 0);
/** @internal */
get themeName() { return this.#themeName_accessor_storage; }
set themeName(value) { this.#themeName_accessor_storage = value; }
#routerName_accessor_storage = (__runInitializers(this, _themeName_extraInitializers), __runInitializers(this, _routerName_initializers, void 0));
/** @internal */
get routerName() { return this.#routerName_accessor_storage; }
set routerName(value) { this.#routerName_accessor_storage = value; }
#cleanOutputDir_accessor_storage = (__runInitializers(this, _routerName_extraInitializers), __runInitializers(this, _cleanOutputDir_initializers, void 0));
get cleanOutputDir() { return this.#cleanOutputDir_accessor_storage; }
set cleanOutputDir(value) { this.#cleanOutputDir_accessor_storage = value; }
#cname_accessor_storage = (__runInitializers(this, _cleanOutputDir_extraInitializers), __runInitializers(this, _cname_initializers, void 0));
get cname() { return this.#cname_accessor_storage; }
set cname(value) { this.#cname_accessor_storage = value; }
#githubPages_accessor_storage = (__runInitializers(this, _cname_extraInitializers), __runInitializers(this, _githubPages_initializers, void 0));
get githubPages() { return this.#githubPages_accessor_storage; }
set githubPages(value) { this.#githubPages_accessor_storage = value; }
#cacheBust_accessor_storage = (__runInitializers(this, _githubPages_extraInitializers), __runInitializers(this, _cacheBust_initializers, void 0));
/** @internal */
get cacheBust() { return this.#cacheBust_accessor_storage; }
set cacheBust(value) { this.#cacheBust_accessor_storage = value; }
#pretty_accessor_storage = (__runInitializers(this, _cacheBust_extraInitializers), __runInitializers(this, _pretty_initializers, void 0));
get pretty() { return this.#pretty_accessor_storage; }
set pretty(value) { this.#pretty_accessor_storage = value; }
renderStartTime = (__runInitializers(this, _pretty_extraInitializers), -1);
markedPlugin;
constructor(owner) {
super(owner);
this.markedPlugin = new MarkedPlugin(this);
new AssetsPlugin(this);
new IconsPlugin(this);
new HierarchyPlugin(this);
new JavascriptIndexPlugin(this);
new NavigationPlugin(this);
new SitemapPlugin(this);
}
/**
* Define a new theme that can be used to render output.
* This API will likely be changing at some point, to allow more easily overriding parts of the theme without
* requiring additional boilerplate.
* @param name
* @param theme
*/
defineTheme(name, theme) {
if (this.themes.has(name)) {
throw new Error(`The theme "${name}" has already been defined.`);
}
this.themes.set(name, theme);
}
/** @internal intended for test usage only */
removeTheme(name) {
this.themes.delete(name);
}
/**
* Define a new router that can be used to determine the output structure.
* @param name
* @param router
*/
defineRouter(name, router) {
if (this.routers.has(name)) {
throw new Error(`The router "${name}" has already been defined.`);
}
this.routers.set(name, router);
}
/** @internal intended for test usage only */
removeRouter(name) {
this.routers.delete(name);
}
/**
* Render the given project reflection to the specified output directory.
*
* @param project The project that should be rendered.
* @param outputDirectory The path of the directory the documentation should be rendered to.
*/
async render(project, outputDirectory) {
JSX.setRenderSettings({ pretty: this.pretty });
const momento = this.hooks.saveMomento();
this.renderStartTime = Date.now();
if (!this.prepareRouter() ||
!this.prepareTheme() ||
!(await this.prepareOutputDirectory(outputDirectory))) {
return;
}
const pages = this.router.buildPages(project);
const output = new RendererEvent(outputDirectory, project, pages);
this.trigger(RendererEvent.BEGIN, output);
await this.runPreRenderJobs(output);
this.application.logger.verbose(`There are ${pages.length} pages to write.`);
for (const page of pages) {
this.renderDocument(outputDirectory, page, project);
}
this.postRenderAsyncJobs.push(async (o) => await this.theme.postRender(o));
await Promise.all(this.postRenderAsyncJobs.map((job) => job(output)));
this.postRenderAsyncJobs = [];
this.trigger(RendererEvent.END, output);
this.theme = void 0;
this.router = void 0;
this.hooks.restoreMomento(momento);
}
async runPreRenderJobs(output) {
const start = Date.now();
this.preRenderAsyncJobs.push(async (o) => await this.theme.preRender(o));
await Promise.all(this.preRenderAsyncJobs.map((job) => job(output)));
this.preRenderAsyncJobs = [];
this.application.logger.verbose(`Pre render async jobs took ${Date.now() - start}ms`);
}
/**
* Render a single page.
*
* @param page An event describing the current page.
* @return TRUE if the page has been saved to disc, otherwise FALSE.
*/
renderDocument(outputDirectory, page, project) {
const momento = this.hooks.saveMomento();
const event = new PageEvent(page.model);
event.url = page.url;
event.filename = path.join(outputDirectory, page.url);
event.pageKind = page.kind;
event.project = project;
this.trigger(PageEvent.BEGIN, event);
event.contents = this.theme.render(event);
this.trigger(PageEvent.END, event);
this.hooks.restoreMomento(momento);
try {
writeFileSync(event.filename, event.contents);
}
catch (error) {
this.application.logger.error(i18n.could_not_write_0(event.filename));
}
}
prepareRouter() {
if (!this.theme) {
const ctor = this.routers.get(this.routerName);
if (!ctor) {
this.application.logger.error(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;
}
prepareTheme() {
if (!this.theme) {
const ctor = this.themes.get(this.themeName);
if (!ctor) {
this.application.logger.error(i18n.theme_0_is_not_defined_available_are_1(this.themeName, [...this.themes.keys()].join(", ")));
return false;
}
else {
this.theme = new ctor(this);
}
}
return true;
}
/**
* Prepare the output directory. If the directory does not exist, it will be
* created. If the directory exists, it will be emptied.
*
* @param directory The path to the directory that should be prepared.
* @returns TRUE if the directory could be prepared, otherwise FALSE.
*/
async prepareOutputDirectory(directory) {
if (this.cleanOutputDir) {
try {
await fs.promises.rm(directory, {
recursive: true,
force: true,
});
}
catch (error) {
this.application.logger.warn(i18n.could_not_empty_output_directory_0(directory));
return false;
}
}
try {
fs.mkdirSync(directory, { recursive: true });
}
catch (error) {
this.application.logger.error(i18n.could_not_create_output_directory_0(directory));
return false;
}
if (this.githubPages) {
try {
const text = "TypeDoc added this file to prevent GitHub Pages from " +
"using Jekyll. You can turn off this behavior by setting " +
"the `githubPages` option to false.";
fs.writeFileSync(path.join(directory, ".nojekyll"), text);
}
catch (error) {
this.application.logger.warn(i18n.could_not_write_0(path.join(directory, ".nojekyll")));
return false;
}
}
if (this.cname) {
fs.writeFileSync(path.join(directory, "CNAME"), this.cname);
}
return true;
}
};
})();
export { Renderer };