// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. import * as path from 'path'; import { PackageName, FileSystem, NewlineKind } from '@rushstack/node-core-library'; import { DocSection, DocPlainText, DocLinkTag, TSDocConfiguration, StringBuilder, DocNodeKind, DocParagraph, DocCodeSpan, DocFencedCode, StandardTags, DocBlock, DocComment, DocNodeContainer, DocHtmlStartTag, DocHtmlEndTag, DocHtmlAttribute } from '@microsoft/tsdoc'; import { ApiModel, ApiItem, ApiEnum, ApiPackage, ApiItemKind, ApiReleaseTagMixin, ApiDocumentedItem, ApiClass, ReleaseTag, ApiStaticMixin, ApiPropertyItem, ApiInterface, Excerpt, ApiParameterListMixin, ApiReturnTypeMixin, ApiDeclaredItem, ApiNamespace, ExcerptTokenKind, IResolveDeclarationReferenceResult, TypeParameter } from '@microsoft/api-extractor-model'; import { CustomDocNodes } from '../nodes/CustomDocNodeKind'; import { DocHeading } from '../nodes/DocHeading'; import { DocTable } from '../nodes/DocTable'; import { DocEmphasisSpan } from '../nodes/DocEmphasisSpan'; import { DocTableRow } from '../nodes/DocTableRow'; import { DocTableCell } from '../nodes/DocTableCell'; import { DocNoteBox } from '../nodes/DocNoteBox'; import { Utilities } from '../utils/Utilities'; import { CustomMarkdownEmitter } from '../markdown/CustomMarkdownEmitter'; import { PluginLoader } from '../plugin/PluginLoader'; import { IMarkdownDocumenterFeatureOnBeforeWritePageArgs, MarkdownDocumenterFeatureContext } from '../plugin/MarkdownDocumenterFeature'; import { DocumenterConfig } from './DocumenterConfig'; import { MarkdownDocumenterAccessor } from '../plugin/MarkdownDocumenterAccessor'; import { FrontMatter } from './FrontMatter'; import { DocGenMode } from './DocGenMode'; import { MarkdownHeadingLevel } from '../markdown/MarkdownHeadingLevel'; //import { getHeapStatistics } from 'v8'; /** * Renders API documentation in the Markdown file format. * For more info: https://en.wikipedia.org/wiki/Markdown */ export class MarkdownDocumenter { private readonly _apiModel: ApiModel; private readonly _documenterConfig: DocumenterConfig | undefined; private readonly _tsdocConfiguration: TSDocConfiguration; private readonly _markdownEmitter: CustomMarkdownEmitter; private _outputFolder: string; private readonly _pluginLoader: PluginLoader; private readonly _uriRoot: string; private get rootHeadingLevel(): MarkdownHeadingLevel { return this._documenterConfig === undefined || this._documenterConfig.rootHeadingLevel === undefined ? 1 : this._documenterConfig.rootHeadingLevel; } public constructor(apiModel: ApiModel, documenterConfig: DocumenterConfig | undefined) { this._apiModel = apiModel; this._documenterConfig = documenterConfig; this._tsdocConfiguration = CustomDocNodes.configuration; this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel); this._pluginLoader = new PluginLoader(); this._uriRoot = '/'; if (this._documenterConfig && this._documenterConfig.uriRoot !== undefined) { this._uriRoot = this._documenterConfig.uriRoot! + '/'; } } public generateFiles(outputFolder: string): void { this._outputFolder = outputFolder; if (this._documenterConfig) { this._pluginLoader.load(this._documenterConfig, () => { return new MarkdownDocumenterFeatureContext({ apiModel: this._apiModel, outputFolder: outputFolder, documenter: new MarkdownDocumenterAccessor({ getLinkForApiItem: (apiItem: ApiItem) => { return this._getLinkForApiItem(apiItem); } }) }); }); } this._deleteOldOutputFiles(); let mode = DocGenMode.Full; if (this._documenterConfig !== undefined && this._documenterConfig.mode !== undefined) { mode = this._documenterConfig.mode; } switch(mode) { case DocGenMode.PackageSummaries: this._writeSimplePackageSummaries(); break; case DocGenMode.Full: this._writeFullOutput(); break; default: throw new Error(`Unrecognized DocGenMode value: "${mode}"`); } if (this._pluginLoader.markdownDocumenterFeature) { this._pluginLoader.markdownDocumenterFeature.onFinished({}); } } /** * Writes complete suite of documentation for all packages in the model. * Corresponds with {@link DocGenMode.Full}. */ private _writeFullOutput(): void { const apiPackages = this._apiModel.packages; apiPackages.forEach((apiPackage) => this._writeApiItemToNewPage(apiPackage)); } /** * Generates simple package summary documents for each package, writing each to file. * Corresponds with {@link DocGenMode.PackageSummaries}. */ private _writeSimplePackageSummaries(): void { const apiPackages = this._apiModel.packages; apiPackages.forEach((apiPackage) => this._writeSimplePackageSummary(apiPackage)); } /** * Generates a simple package summary for the provided package overview, and writes it to file. * This summary is a very truncated view of the package's documentation, only including the package's * `summary` and `remarks` (including examples) blocks. * @param apiPackage - Package for which the summary document will be generated */ private _writeSimplePackageSummary(apiPackage: ApiPackage): void { const configuration = this._tsdocConfiguration; const output = new DocSection({ configuration }); // Write summary if (apiPackage instanceof ApiDocumentedItem) { const tsdocComment: DocComment | undefined = apiPackage.tsdocComment; if (tsdocComment) { this._appendSection(output, tsdocComment.summarySection); } else { this._appendSection(output, new DocSection( { configuration }, [ new DocParagraph({configuration}, [ new DocEmphasisSpan({ configuration, bold: true, italic: true }, [ new DocPlainText({ text: `${apiPackage.name} contains no package summary.`, configuration }) ]) ]) ]) ) } this._writeRemarksSection(output, apiPackage, { headingLevelOffset: this.rootHeadingLevel }); // Write to output file this._writeOutputToFile(apiPackage, output, { includeFrontMatter: false }); } } /** * Writes the provided API item to a new page file. */ private _writeApiItemToNewPage(apiItem: ApiItem): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const output = new DocSection({ configuration }); this._writeBreadcrumb(output, apiItem); this._writeApiItemToExistingOutput(apiItem, output, { headingLevelOffset: this.rootHeadingLevel }); // Write // we only generate top level package pages (which will generate class and interface sub-pages) const pkg: ApiPackage | undefined = apiItem.getAssociatedPackage(); if (!pkg || !this._isAllowedPackage(pkg)) { if (this._documenterConfig && this._documenterConfig.logLevel === 'verbose') { console.log(`skipping ${apiItem.getScopedNameWithinPackage()}`); if (pkg) { console.log(`\t${pkg.name} package isn't in the allowed list`); } } return; } this._writeOutputToFile(apiItem, output, { includeFrontMatter: true }); } /** * Writes the provided API item to the provided documentation `output`. * Note: this does not generate a file. It assumes that the `output` being rendered to will be handled * by the consumer. */ private _writeApiItemToExistingOutput( apiItem: ApiItem, output: DocSection | DocParagraph, options: PageRenderOptions ): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const scopedName: string = apiItem.getScopedNameWithinPackage(); // Creates a heading for the item on the page, unless the item is the parent of all page contents switch (apiItem.kind) { case ApiItemKind.Model: output.appendNode(new DocHeading({ configuration, title: `API Reference`, level: 1 + options.headingLevelOffset })); break; case ApiItemKind.Package: console.log(`Writing "${apiItem.displayName}" package`); break; case ApiItemKind.Class: case ApiItemKind.Interface: case ApiItemKind.Namespace: // Will create separate page instead break; case ApiItemKind.Enum: { output.appendNode(new DocHeading({ configuration, title: `${scopedName} enum`, level: this._getHeadingLevelForApiItem(apiItem, options.headingLevelOffset), id: this._htmlIDForItem(apiItem) })); break; } case ApiItemKind.Constructor: case ApiItemKind.ConstructSignature: { output.appendNode(new DocHeading({ configuration, title: scopedName, level: this._getHeadingLevelForApiItem(apiItem, options.headingLevelOffset), id: this._htmlIDForItem(apiItem) })); break; } case ApiItemKind.CallSignature: case ApiItemKind.Method: case ApiItemKind.MethodSignature: case ApiItemKind.Function: case ApiItemKind.Property: case ApiItemKind.PropertySignature: case ApiItemKind.TypeAlias: case ApiItemKind.Variable: { output.appendNode(new DocHeading({ configuration, title: apiItem.displayName, level: this._getHeadingLevelForApiItem(apiItem, options.headingLevelOffset), id: this._htmlIDForItem(apiItem) })); break; } default: throw new Error('Unsupported API item kind: ' + apiItem.kind); } if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { if (apiItem.releaseTag === ReleaseTag.Beta) { this._writeBetaWarning(output); } } if (apiItem instanceof ApiDocumentedItem) { const tsdocComment: DocComment | undefined = apiItem.tsdocComment; if (tsdocComment) { if (tsdocComment.deprecatedBlock) { if (this._documenterConfig && this._documenterConfig.logLevel === 'verbose') { for (const node of tsdocComment.deprecatedBlock.content.nodes) { console.log(`NODE: ${node.kind}, CHILDREN: [${node.getChildNodes().map(v => v.kind)}]`); } } output.appendNode( new DocNoteBox( { configuration: this._tsdocConfiguration, type: 'warning', title: 'Deprecated' }, [...tsdocComment.deprecatedBlock.content.nodes] ) ); } this._appendSection(output, tsdocComment.summarySection); } } if (apiItem instanceof ApiDeclaredItem) { if (apiItem.excerpt.text.length > 0) { output.appendNode( new DocParagraph({ configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Signature:' }) ]) ]) ); output.appendNode( new DocFencedCode({ configuration, code: apiItem.getExcerptWithModifiers(), language: 'typescript' }) ); } this._writeHeritageTypes(output, apiItem); } let appendRemarks: boolean = true; // For certain item kinds, we will render the remarks toward the top of the page. // For all others, render at the bottom. switch (apiItem.kind) { case ApiItemKind.Class: case ApiItemKind.Interface: case ApiItemKind.Namespace: case ApiItemKind.Package: this._writeRemarksSection(output, apiItem, options); appendRemarks = false; break; } // Write body contents for item. switch (apiItem.kind) { case ApiItemKind.Class: this._writeClassTables(output, apiItem as ApiClass, options); break; case ApiItemKind.Enum: this._writeEnumTables(output, apiItem as ApiEnum, options); break; case ApiItemKind.Interface: this._writeInterfaceTables(output, apiItem as ApiInterface, options); break; case ApiItemKind.CallSignature: case ApiItemKind.Constructor: case ApiItemKind.ConstructSignature: case ApiItemKind.Method: case ApiItemKind.MethodSignature: case ApiItemKind.Function: this._writeFunctionTables(output, apiItem, options); break; case ApiItemKind.Namespace: this._writePackageOrNamespaceTables(output, apiItem as ApiNamespace, options); break; case ApiItemKind.Model: this._writeModelTable(output, apiItem as ApiModel, options); break; case ApiItemKind.Package: this._writePackageOrNamespaceTables(output, apiItem as ApiPackage, options); break; case ApiItemKind.Property: case ApiItemKind.PropertySignature: break; case ApiItemKind.TypeAlias: break; case ApiItemKind.Variable: break; default: throw new Error('Unsupported API item kind: ' + apiItem.kind); } // If remarks were not appended earlier on the page, add them to the end. if (appendRemarks) { this._writeRemarksSection( output, apiItem, // Ensure remarks block is 1 level lower than item heading { headingLevelOffset: options.headingLevelOffset + 1 }); } } /** * Writes the specified `apiItem` and its built-up `output` to file * @param apiItem - The API member for which documentation is being generated * @param output - Existing documentation output for the `apiItem` * @param options - See {@link FileRenderOptions} */ private _writeOutputToFile(apiItem: ApiItem, output: DocSection | DocParagraph, options: FileRenderOptions): void { const { includeFrontMatter } = options ; const apiItemFilename = this._getFilePathForApiItem(apiItem); if (apiItemFilename === undefined) { throw new Error('No file was found to write API item docs to.'); } const filePath: string = path.join(this._outputFolder, apiItemFilename); const stringBuilder: StringBuilder = new StringBuilder(); // undefined => true if (includeFrontMatter !== false) { this._writeFrontMatter(stringBuilder, apiItem); } this._markdownEmitter.emit(stringBuilder, output, { contextApiItem: apiItem, onGetFilenameForApiItem: (apiItemForFilename: ApiItem) => { return this._getLinkForApiItem(apiItemForFilename); } }); let pageContent: string = stringBuilder.toString(); if (this._pluginLoader.markdownDocumenterFeature) { // Allow the plugin to customize the pageContent const eventArgs: IMarkdownDocumenterFeatureOnBeforeWritePageArgs = { apiItem: apiItem, outputFilename: filePath, pageContent: pageContent }; this._pluginLoader.markdownDocumenterFeature.onBeforeWritePage(eventArgs); pageContent = eventArgs.pageContent; } FileSystem.writeFile(filePath, pageContent, { convertLineEndings: this._documenterConfig ? this._documenterConfig.newlineKind : NewlineKind.CrLf, ensureFolderExists: true }); if (this._documenterConfig && this._documenterConfig.logLevel === 'verbose') { console.log(filePath, "saved to disk"); } } private _writeHeritageTypes(output: DocSection | DocParagraph, apiItem: ApiDeclaredItem): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; if (apiItem instanceof ApiClass) { if (apiItem.extendsType) { const extendsParagraph: DocParagraph = new DocParagraph({ configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Extends: ' }) ]) ]); if (this._appendExcerptWithHyperlinks(extendsParagraph, apiItem.extendsType.excerpt)) { output.appendNode(extendsParagraph); } } if (apiItem.implementsTypes.length > 0) { const extendsParagraph: DocParagraph = new DocParagraph({ configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Implements: ' }) ]) ]); let needsComma: boolean = false; for (const implementsType of apiItem.implementsTypes) { if (needsComma) { extendsParagraph.appendNode(new DocPlainText({ configuration, text: ', ' })); } if (this._appendExcerptWithHyperlinks(extendsParagraph, implementsType.excerpt)) { needsComma = true; output.appendNode(extendsParagraph); } } } if (apiItem.typeParameters.length > 0) { console.log(`HERITAGE GENERIC: ${JSON.stringify(apiItem.typeParameters.map(v => v.name))}`); const typeParamParagraph: DocParagraph = new DocParagraph({ configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Type parameters: ' }) ]) ]); output.appendNode(typeParamParagraph); this._appendTypeParams(output, apiItem.typeParameters); } } if (apiItem instanceof ApiInterface) { if (apiItem.extendsTypes.length > 0) { const extendsParagraph: DocParagraph = new DocParagraph({ configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Extends: ' }) ]) ]); let anythingWritten: boolean = false; for (const extendsType of apiItem.extendsTypes) { if (anythingWritten) { extendsParagraph.appendNode(new DocPlainText({ configuration, text: ', ' })); } if (this._appendExcerptWithHyperlinks(extendsParagraph, extendsType.excerpt)) { anythingWritten = true; } } output.appendNode(extendsParagraph); } } } /** * Renders contents for any `@remarks` and `@examples` comments. */ private _writeRemarksSection( output: DocSection | DocParagraph, apiItem: ApiItem, options: PageRenderOptions ): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; if (apiItem instanceof ApiDocumentedItem) { const tsdocComment: DocComment | undefined = apiItem.tsdocComment; if (tsdocComment) { const headingLevel = this._getHeadingLevelForApiItem(apiItem, options.headingLevelOffset); const apiItemId = this._htmlIDForItem(apiItem); // Write the @remarks block if (tsdocComment.remarksBlock) { const id = apiItemId === undefined ? undefined // If heading is for page-item, no need to assign ID : `${apiItemId}-remarks`; output.appendNode(new DocHeading({ configuration, title: 'Remarks', level: headingLevel, id })); this._appendSection(output, tsdocComment.remarksBlock.content); } // Write the @example blocks const exampleBlocks: DocBlock[] = tsdocComment.customBlocks.filter( (x) => x.blockTag.tagNameWithUpperCase === StandardTags.example.tagNameWithUpperCase ); let exampleNumber: number = 1; for (const exampleBlock of exampleBlocks) { const heading: string = exampleBlocks.length > 1 ? `Example ${exampleNumber}` : 'Example'; const idPostfix = exampleBlocks.length > 1 ? `example-${exampleNumber}` : `example`; const id = apiItemId === undefined ? exampleBlocks.length === 1 ? undefined // If heading is for page item, and there is only 1 example, no need for ID : idPostfix : `${apiItemId}-${idPostfix}`; output.appendNode(new DocHeading({ configuration, title: heading, level: headingLevel, id })); this._appendSection(output, exampleBlock.content); ++exampleNumber; } } } } private _writeThrowsSection(output: DocSection | DocParagraph, apiItem: ApiItem, headingLevel: number): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; if (apiItem instanceof ApiDocumentedItem) { const tsdocComment: DocComment | undefined = apiItem.tsdocComment; if (tsdocComment) { // Write the @throws blocks const throwsBlocks: DocBlock[] = tsdocComment.customBlocks.filter( (x) => x.blockTag.tagNameWithUpperCase === StandardTags.throws.tagNameWithUpperCase ); if (throwsBlocks.length > 0) { const id = `${this._htmlIDForItem(apiItem)}-exceptions`; output.appendNode( new DocParagraph({ configuration }, [ new DocHeading({ configuration, title: 'Exceptions:', level: headingLevel, id }) ]) ); for (const throwsBlock of throwsBlocks) { this._appendSection(output, throwsBlock.content); } } } } } /** * GENERATE PAGE: MODEL */ private _writeModelTable(output: DocSection | DocParagraph, apiModel: ApiModel, options: PageRenderOptions): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const packagesTable: DocTable = new DocTable({ configuration, headerTitles: ['Package', 'Description'], cssClass: 'package-list', caption: 'List of packages in this model' }); for (const apiMember of apiModel.members) { const row: DocTableRow = new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createDescriptionCell(apiMember) ]); switch (apiMember.kind) { case ApiItemKind.Package: packagesTable.addRow(row); this._writeApiItemToNewPage(apiMember); break; } } if (packagesTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Packages', level: 1 + options.headingLevelOffset })); output.appendNode(packagesTable); } } /** * GENERATE PAGE: PACKAGE or NAMESPACE */ private _writePackageOrNamespaceTables( output: DocSection | DocParagraph, apiContainer: ApiPackage | ApiNamespace, options: PageRenderOptions ): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const apiContainerKindString = apiContainer.kind.toLocaleLowerCase(); const classesTable: DocTable = new DocTable({ configuration, headerTitles: ['Class', 'Description'], cssClass: 'class-list', caption: `List of classes contained in this ${apiContainerKindString}` }); const enumerationsTable: DocTable = new DocTable({ configuration, headerTitles: ['Enumeration', 'Description'], cssClass: 'enum-list', caption: `List of enums contained in this ${apiContainerKindString}` }); const functionsTable: DocTable = new DocTable({ configuration, headerTitles: ['Function', 'Description'], cssClass: 'function-list', caption: `List of functions contained in this ${apiContainerKindString}` }); const interfacesTable: DocTable = new DocTable({ configuration, headerTitles: ['Interface', 'Description'], cssClass: 'interface-list', caption: `List of interfaces contained in this ${apiContainerKindString}` }); const namespacesTable: DocTable = new DocTable({ configuration, headerTitles: ['Namespace', 'Description'], cssClass: 'namespace-list', caption: `List of namespaces contained in this ${apiContainerKindString}` }); const variablesTable: DocTable = new DocTable({ configuration, headerTitles: ['Variable', 'Description'], cssClass: 'variable-list', caption: `List of variables contained in this ${apiContainerKindString}` }); const typeAliasesTable: DocTable = new DocTable({ configuration, headerTitles: ['Type Alias', 'Description'], cssClass: 'alias-list', caption: `List of type aliases contained in this ${apiContainerKindString}` }); const enumsParagraph: DocParagraph = new DocParagraph({ configuration }); const varsParagraph: DocParagraph = new DocParagraph({ configuration }); const functionsParagraph: DocParagraph = new DocParagraph({ configuration }); const aliasesParagraph: DocParagraph = new DocParagraph({ configuration }); const apiMembers: ReadonlyArray = apiContainer.kind === ApiItemKind.Package ? (apiContainer as ApiPackage).entryPoints[0].members : (apiContainer as ApiNamespace).members; // loop through the members of the package/namespace. for (const apiMember of apiMembers) { const row: DocTableRow = new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createDescriptionCell(apiMember) ]); switch (apiMember.kind) { case ApiItemKind.Class: classesTable.addRow(row); this._writeApiItemToNewPage(apiMember); break; case ApiItemKind.Enum: enumerationsTable.addRow(row); this._writeApiItemToExistingOutput(apiMember, enumsParagraph, options); break; case ApiItemKind.Interface: interfacesTable.addRow(row); this._writeApiItemToNewPage(apiMember); break; case ApiItemKind.Namespace: namespacesTable.addRow(row); this._writeApiItemToNewPage(apiMember); break; case ApiItemKind.Function: functionsTable.addRow(row); this._writeApiItemToExistingOutput(apiMember, functionsParagraph, options); break; case ApiItemKind.TypeAlias: typeAliasesTable.addRow(row); this._writeApiItemToExistingOutput(apiMember, aliasesParagraph, options); break; case ApiItemKind.Variable: variablesTable.addRow(row); this._writeApiItemToExistingOutput(apiMember, varsParagraph, options); break; } } const headingLevel = this._getHeadingLevelForApiItem(apiContainer, options.headingLevelOffset); if (classesTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Classes', level: headingLevel })); output.appendNode(classesTable); } if (enumerationsTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Enumerations', level: headingLevel })); output.appendNode(enumerationsTable); } if (functionsTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Functions', level: headingLevel })); output.appendNode(functionsTable); } if (interfacesTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Interfaces', level: headingLevel })); output.appendNode(interfacesTable); } if (namespacesTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Namespaces', level: headingLevel })); output.appendNode(namespacesTable); } if (variablesTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Variables', level: headingLevel })); output.appendNode(variablesTable); } if (typeAliasesTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Type Aliases', level: headingLevel })); output.appendNode(typeAliasesTable); } // Render details section, but only if there are any details contents to show const hasEnums = enumsParagraph.nodes.length > 0; const hasFunctions = functionsParagraph.nodes.length > 0; const hasVars = varsParagraph.nodes.length > 0; const hasAliases = aliasesParagraph.nodes.length > 0; if (hasEnums || hasFunctions || hasVars || hasAliases) { const details: DocSection = new DocSection({ configuration }, [ new DocHtmlStartTag({ configuration, name: "hr" }), new DocHtmlStartTag({ configuration, name: "div", htmlAttributes: [new DocHtmlAttribute({ configuration, name: "id", value: "package-details" })] }) ]); if (hasEnums) { details.appendNode(new DocHeading({ configuration, title: 'Enumeration Details', level: headingLevel, id: 'enumerations-details' })); details.appendNode(enumsParagraph); } if (hasFunctions) { details.appendNode(new DocHeading({ configuration, title: 'Function Details', level: headingLevel, id: 'functions-details' })); details.appendNode(functionsParagraph); } if (hasVars) { details.appendNode(new DocHeading({ configuration, title: 'Variable Details', level: headingLevel, id: 'variables-details' })); details.appendNode(varsParagraph); } if (hasAliases) { details.appendNode(new DocHeading({ configuration, title: 'Type Alias Details', level: headingLevel, id: 'type-aliases-details' })); details.appendNode(aliasesParagraph); } details.appendNode(new DocHtmlEndTag({ configuration, name: "div" })); output.appendNode(details); } } /** * GENERATE PAGE: CLASS */ private _writeClassTables(output: DocSection | DocParagraph, apiClass: ApiClass, options: PageRenderOptions): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const eventsTable: DocTable = new DocTable({ configuration, headerTitles: ['Property', 'Modifiers', 'Type', 'Description'], cssClass: 'event-list', caption: 'List of events in use in this class' }); const constructorsTable: DocTable = new DocTable({ configuration, headerTitles: ['Constructor', 'Modifiers', 'Description'], cssClass: 'constructor-list', caption: 'List of constructors for this class' }); const propertiesTable: DocTable = new DocTable({ configuration, headerTitles: ['Property', 'Modifiers', 'Type', 'Description'], cssClass: 'property-list', caption: 'List of properties on this class' }); const methodsTable: DocTable = new DocTable({ configuration, headerTitles: ['Method', 'Modifiers', 'Description'], cssClass: 'method-list', caption: 'List of methods on this class' }); const callSignaturesTable: DocTable = new DocTable({ configuration, headerTitles: ['Signature', 'Description'], cssClass: 'call-signature-list', caption: 'List of call signatures on this class' }); const constructorsParagraph: DocParagraph = new DocParagraph({ configuration }); const methodsParagraph: DocParagraph = new DocParagraph({ configuration }); const propertiesParagraph: DocParagraph = new DocParagraph({ configuration }); const eventsParagraph: DocParagraph = new DocParagraph({ configuration }); const callSignaturesParagraph: DocParagraph = new DocParagraph({ configuration }); for (const apiMember of apiClass.members) { switch (apiMember.kind) { case ApiItemKind.Constructor: { constructorsTable.addRow( new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createModifiersCell(apiMember), this._createDescriptionCell(apiMember) ]) ); this._writeApiItemToExistingOutput(apiMember, constructorsParagraph, options); break; } case ApiItemKind.Method: { methodsTable.addRow( new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createModifiersCell(apiMember), this._createDescriptionCell(apiMember) ]) ); this._writeApiItemToExistingOutput(apiMember, methodsParagraph, options); break; } case ApiItemKind.Property: { if ((apiMember as ApiPropertyItem).isEventProperty) { eventsTable.addRow( new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createModifiersCell(apiMember), this._createPropertyTypeCell(apiMember), this._createDescriptionCell(apiMember) ]) ); this._writeApiItemToExistingOutput(apiMember, eventsParagraph, options); } else { propertiesTable.addRow( new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createModifiersCell(apiMember), this._createPropertyTypeCell(apiMember), this._createDescriptionCell(apiMember) ]) ); this._writeApiItemToExistingOutput(apiMember, propertiesParagraph, options); } break; } case ApiItemKind.CallSignature: { callSignaturesTable.addRow( new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createDescriptionCell(apiMember) ]) ); this._writeApiItemToExistingOutput(apiMember, callSignaturesParagraph, options); break; } } } const headingLevel = this._getHeadingLevelForApiItem(apiClass, options.headingLevelOffset); if (eventsTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Events', level: headingLevel })); output.appendNode(eventsTable); } if (constructorsTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Constructors', level: headingLevel })); output.appendNode(constructorsTable); } if (propertiesTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Properties', level: headingLevel })); output.appendNode(propertiesTable); } if (methodsTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Methods', level: headingLevel })); output.appendNode(methodsTable); } if (callSignaturesTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Call Signatures', level: headingLevel })); output.appendNode(callSignaturesTable); } // Render details section, but only if there are any details contents to show const hasEvents = eventsParagraph.nodes.length > 0; const hasConstructors = constructorsParagraph.nodes.length > 0; const hasProperties = propertiesParagraph.nodes.length > 0; const hasMethods = methodsParagraph.nodes.length > 0; const hasCallSignatures = callSignaturesParagraph.nodes.length > 0; if (hasEvents || hasConstructors || hasProperties || hasMethods || hasCallSignatures) { const details: DocSection = new DocSection({ configuration }, [ new DocHtmlStartTag({ configuration, name: "hr" }), new DocHtmlStartTag({ configuration, name: "div", htmlAttributes: [ new DocHtmlAttribute({ configuration, name: "id", value: "class-details" })] }) ]); if (hasEvents) { details.appendNode(new DocHeading({ configuration, title: 'Event Details', level: headingLevel, id: 'events-details' })); details.appendNode(eventsParagraph); } if (hasConstructors) { details.appendNode(new DocHeading({ configuration, title: 'Constructor Details', level: headingLevel, id: 'constructors-details' })); details.appendNode(constructorsParagraph); } if (hasProperties) { details.appendNode(new DocHeading({ configuration, title: 'Property Details', level: headingLevel, id: 'properties-details' })); details.appendNode(propertiesParagraph); } if (hasMethods) { details.appendNode(new DocHeading({ configuration, title: 'Method Details', level: headingLevel, id: 'methods-details' })); details.appendNode(methodsParagraph); } if (hasCallSignatures) { details.appendNode(new DocHeading({ configuration, title: 'Call Signature Details', level: headingLevel, id: 'call-signatures-details' })); details.appendNode(callSignaturesParagraph); } details.appendNode(new DocHtmlEndTag({ configuration, name: "div" })); output.appendNode(details); } } /** * GENERATE PAGE: ENUM */ private _writeEnumTables(output: DocSection | DocParagraph, apiEnum: ApiEnum, options: PageRenderOptions): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const enumMembersTable: DocTable = new DocTable({ configuration, headerTitles: ['Member', 'Value', 'Description'], cssClass: 'enum-list', caption: 'List of members in use in this enum' }); for (const apiEnumMember of apiEnum.members) { enumMembersTable.addRow( new DocTableRow({ configuration }, [ new DocTableCell({ configuration }, [ new DocParagraph({ configuration }, [ new DocPlainText({ configuration, text: Utilities.getConciseSignature(apiEnumMember) }) ]) ]), new DocTableCell({ configuration }, [ new DocParagraph({ configuration }, [ new DocCodeSpan({ configuration, code: apiEnumMember.initializerExcerpt.text }) ]) ]), this._createDescriptionCell(apiEnumMember) ]) ); } const headingLevel = this._getHeadingLevelForApiItem(apiEnum, options.headingLevelOffset); if (enumMembersTable.rows.length > 0) { output.appendNode( new DocHeading({ configuration, title: 'Enumeration Members', level: headingLevel }) ); output.appendNode(enumMembersTable); } } /** * GENERATE PAGE: INTERFACE */ private _writeInterfaceTables(output: DocSection | DocParagraph, apiInterface: ApiInterface, options: PageRenderOptions): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const eventsTable: DocTable = new DocTable({ configuration, headerTitles: ['Property', 'Type', 'Description'], cssClass: 'event-list', caption: 'List of events in use in this interface' }); const propertiesTable: DocTable = new DocTable({ configuration, headerTitles: ['Property', 'Type', 'Description'], cssClass: 'property-list', caption: 'List of properties on this interface' }); const methodsTable: DocTable = new DocTable({ configuration, headerTitles: ['Method', 'Description'], cssClass: 'method-list', caption: 'List of methods on this interface' }); const callSignaturesTable: DocTable = new DocTable({ configuration, headerTitles: ['Signature', 'Description'], cssClass: 'call-signature-list', caption: 'List of call signatures on this class' }); const eventsParagraph: DocParagraph = new DocParagraph({ configuration }); const propertiesParagraph: DocParagraph = new DocParagraph({ configuration }); const methodsParagraph: DocParagraph = new DocParagraph({ configuration }); const callSignaturesParagraph: DocParagraph = new DocParagraph({ configuration }); for (const apiMember of apiInterface.members) { switch (apiMember.kind) { case ApiItemKind.ConstructSignature: case ApiItemKind.MethodSignature: { methodsTable.addRow( new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createDescriptionCell(apiMember) ]) ); this._writeApiItemToExistingOutput(apiMember, methodsParagraph, options); break; } case ApiItemKind.PropertySignature: { if ((apiMember as ApiPropertyItem).isEventProperty) { eventsTable.addRow( new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createPropertyTypeCell(apiMember), this._createDescriptionCell(apiMember) ]) ); this._writeApiItemToExistingOutput(apiMember, eventsParagraph, options); } else { propertiesTable.addRow( new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createPropertyTypeCell(apiMember), this._createDescriptionCell(apiMember) ]) ); this._writeApiItemToExistingOutput(apiMember, propertiesParagraph, options); } break; } case ApiItemKind.CallSignature: { callSignaturesTable.addRow( new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), this._createDescriptionCell(apiMember) ]) ); this._writeApiItemToExistingOutput(apiMember, callSignaturesParagraph, options); break; } } } const headingLevel = this._getHeadingLevelForApiItem(apiInterface, options.headingLevelOffset); if (eventsTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Events', level: headingLevel })); output.appendNode(eventsTable); } if (propertiesTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Properties', level: headingLevel })); output.appendNode(propertiesTable); } if (methodsTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Methods', level: headingLevel })); output.appendNode(methodsTable); } if (callSignaturesTable.rows.length > 0) { output.appendNode(new DocHeading({ configuration, title: 'Call Signatures', level: headingLevel })); output.appendNode(callSignaturesTable); } // Render details section, but only if there are any details contents to show const hasEvents = eventsParagraph.nodes.length > 0; const hasProperties = propertiesParagraph.nodes.length > 0; const hasMethods = methodsParagraph.nodes.length > 0; const hasCallSignatures = callSignaturesParagraph.nodes.length > 0; if (hasEvents || hasProperties || hasMethods || hasCallSignatures) { const details: DocSection = new DocSection({ configuration }, [ new DocHtmlStartTag({ configuration, name: "hr" }), new DocHtmlStartTag({ configuration, name: "div", htmlAttributes: [ new DocHtmlAttribute({ configuration, name: "id", value: "interface-details" })] }) ]); if (hasEvents) { details.appendNode(new DocHeading({ configuration, title: 'Event Details', level: headingLevel, id: 'events-details' })); details.appendNode(eventsParagraph); } if (hasProperties) { details.appendNode(new DocHeading({ configuration, title: 'Property Details', level: headingLevel, id: 'properties-details' })); details.appendNode(propertiesParagraph); } if (hasMethods) { details.appendNode(new DocHeading({ configuration, title: 'Method Details', level: headingLevel, id: 'methods-details' })); details.appendNode(methodsParagraph); } if (hasCallSignatures) { details.appendNode(new DocHeading({ configuration, title: 'Call Signature Details', level: headingLevel, id: 'call-signatures-details' })); details.appendNode(callSignaturesParagraph); } details.appendNode(new DocHtmlEndTag({ configuration, name: "div" })); output.appendNode(details); } } /** * GENERATE PAGE: FUNCTION-LIKE */ private _writeFunctionTables( output: DocSection | DocParagraph, apiItem: ApiItem, options: PageRenderOptions, ) : void { const childHeadingLevel = this._getHeadingLevelForApiItem(apiItem, options.headingLevelOffset) + 1; if (ApiParameterListMixin.isBaseClassOf(apiItem)) { this._writeParametersSection(output, apiItem, childHeadingLevel); } if (ApiReturnTypeMixin.isBaseClassOf(apiItem)) { this._writeReturnsSection(output, apiItem, childHeadingLevel) } this._writeThrowsSection(output, apiItem, childHeadingLevel); } private _writeParametersSection( output: DocSection | DocParagraph, apiParameterListMixin: ApiParameterListMixin, headingLevel: number, ): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const parametersTable: DocTable = new DocTable({ configuration, headerTitles: ['Parameter', 'Type', 'Description'], cssClass: 'param-list', caption: 'List of parameters' }); for (const apiParameter of apiParameterListMixin.parameters) { const parameterDescription: DocSection = new DocSection({ configuration }); if (apiParameter.tsdocParamBlock) { this._appendSection(parameterDescription, apiParameter.tsdocParamBlock.content); } parametersTable.addRow( new DocTableRow({ configuration }, [ new DocTableCell({ configuration }, [ new DocParagraph({ configuration }, [ new DocPlainText({ configuration, text: apiParameter.name }) ]) ]), new DocTableCell({ configuration }, [ this._createParagraphForTypeExcerpt(apiParameter.parameterTypeExcerpt) ]), new DocTableCell({ configuration }, parameterDescription.nodes) ]) ); } if (parametersTable.rows.length > 0) { const id = `${this._htmlIDForItem(apiParameterListMixin)}-parameters`; output.appendNode( new DocParagraph({ configuration }, [ new DocHeading({ configuration, title: 'Parameters', level: headingLevel, id }) ]) ); output.appendNode(parametersTable); } } private _writeReturnsSection( output: DocSection | DocParagraph, apiItem: ApiReturnTypeMixin, headingLevel: number, ): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; if (apiItem instanceof ApiDocumentedItem) { if (apiItem.tsdocComment && apiItem.tsdocComment.returnsBlock) { const id = `${this._htmlIDForItem(apiItem)}-returns`; output.appendNode( new DocParagraph({ configuration }, [ new DocHeading({ configuration, title: 'Returns', level: headingLevel, id }) ]) ); this._appendSection(output, apiItem.tsdocComment.returnsBlock.content); if(apiItem.returnTypeExcerpt.text.trim()) { output.appendNode( new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: 'Return type(s): ' }) ])); output.appendNode(this._createParagraphForTypeExcerpt(apiItem.returnTypeExcerpt)); } } } } private _createParagraphForTypeExcerpt(excerpt: Excerpt): DocParagraph { const configuration: TSDocConfiguration = this._tsdocConfiguration; const paragraph: DocParagraph = new DocParagraph({ configuration }); if (!excerpt.text.trim()) { paragraph.appendNode(new DocPlainText({ configuration, text: '(not declared)' })); } else { this._appendExcerptWithHyperlinks(paragraph, excerpt); } return paragraph; } /** * Appends any reference tokens in the given `excerpt` to the provided `docNodeContainer` as hyperlinks, * comma-separated. * * @returns whether or not any contents were appended. */ private _appendExcerptWithHyperlinks(docNodeContainer: DocNodeContainer, excerpt: Excerpt): boolean { const configuration: TSDocConfiguration = this._tsdocConfiguration; for (const token of excerpt.spannedTokens) { // Markdown doesn't provide a standardized syntax for hyperlinks inside code spans, so we will render // the type expression as DocPlainText. Instead of creating multiple DocParagraphs, we can simply // discard any newlines and let the renderer do normal word-wrapping. const unwrappedTokenText: string = token.text.replace(/[\r\n]+/g, ' '); let wroteHyperlink = false; // If it's hyperlink-able, then append a DocLinkTag if (token.kind === ExcerptTokenKind.Reference && token.canonicalReference) { const apiItemResult: IResolveDeclarationReferenceResult = this._apiModel.resolveDeclarationReference( token.canonicalReference, undefined ); if (apiItemResult.resolvedApiItem) { docNodeContainer.appendNode( new DocLinkTag({ configuration, tagName: '@link', linkText: unwrappedTokenText, urlDestination: this._getLinkForApiItem(apiItemResult.resolvedApiItem) }) ); wroteHyperlink = true; } } // If the token was not one from which we generated hyperlink text, write as plain text instead if(!wroteHyperlink) { docNodeContainer.appendNode(new DocPlainText({ configuration, text: unwrappedTokenText })); } } return excerpt.spannedTokens.length !== 0; } private _appendTypeParams(docNodeContainer: DocNodeContainer, params: readonly TypeParameter[], excerpt?: Excerpt): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; for (const typeParam of params) { const typeParamParagraph: DocParagraph = new DocParagraph({ configuration }, [ new DocEmphasisSpan({ configuration, bold: true }, [ new DocPlainText({ configuration, text: typeParam.name }), ]), new DocPlainText({ configuration, text: ` -- ` }) ]); if (typeParam.tsdocTypeParamBlock) { console.log(`Appending section for ${typeParam.name}`); this._appendSection(typeParamParagraph, typeParam.tsdocTypeParamBlock.content); } docNodeContainer.appendNode(typeParamParagraph); } } private _createTitleCell(apiItem: ApiItem): DocTableCell { const configuration: TSDocConfiguration = this._tsdocConfiguration; return new DocTableCell({ configuration }, [ new DocParagraph({ configuration }, [ new DocLinkTag({ configuration, tagName: '@link', linkText: Utilities.getConciseSignature(apiItem), urlDestination: this._getLinkForApiItem(apiItem) }) ]) ]); } /** * This generates a DocTableCell for an ApiItem including the summary section and "(BETA)" annotation. * * @remarks * We mostly assume that the input is an ApiDocumentedItem, but it's easier to perform this as a runtime * check than to have each caller perform a type cast. */ private _createDescriptionCell(apiItem: ApiItem): DocTableCell { const configuration: TSDocConfiguration = this._tsdocConfiguration; const section: DocSection = new DocSection({ configuration }); if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) { if (apiItem.releaseTag === ReleaseTag.Beta) { section.appendNodesInParagraph([ new DocEmphasisSpan({ configuration, bold: true, italic: true }, [ new DocPlainText({ configuration, text: '(BETA)' }) ]), new DocPlainText({ configuration, text: ' ' }) ]); } } if (apiItem instanceof ApiDocumentedItem) { if (apiItem.tsdocComment !== undefined) { this._appendAndMergeSection(section, apiItem.tsdocComment.summarySection); } } return new DocTableCell({ configuration }, section.nodes); } private _createModifiersCell(apiItem: ApiItem): DocTableCell { const configuration: TSDocConfiguration = this._tsdocConfiguration; const section: DocSection = new DocSection({ configuration }); if (ApiStaticMixin.isBaseClassOf(apiItem)) { if (apiItem.isStatic) { section.appendNodeInParagraph(new DocCodeSpan({ configuration, code: 'static' })); } } return new DocTableCell({ configuration }, section.nodes); } private _createPropertyTypeCell(apiItem: ApiItem): DocTableCell { const configuration: TSDocConfiguration = this._tsdocConfiguration; const section: DocSection = new DocSection({ configuration }); if (apiItem instanceof ApiPropertyItem) { section.appendNode(this._createParagraphForTypeExcerpt(apiItem.propertyTypeExcerpt)); } return new DocTableCell({ configuration }, section.nodes); } // prepare the markdown frontmatter by providing the metadata needed to nicely render the page. private _writeFrontMatter(stringBuilder: StringBuilder, item: ApiItem): void { const frontMatter = new FrontMatter(); frontMatter.kind = item.kind; frontMatter.title = item.displayName.replace(/"/g, '').replace(/!/g, ''); let apiMembers: ReadonlyArray = item.members; const mdEmitter = this._markdownEmitter; var extractSummary = (docComment: DocComment): string => { const tmpStrBuilder: StringBuilder = new StringBuilder(); const summary: DocSection = docComment!.summarySection; mdEmitter.emit(tmpStrBuilder, summary, { contextApiItem: item, onGetFilenameForApiItem: (apiItemForFilename: ApiItem) => { return this._getLinkForApiItem(apiItemForFilename); } }); return tmpStrBuilder.toString().replace(/"/g, "'").trim(); } switch (item.kind) { case ApiItemKind.Class: const classItem: ApiClass = item as ApiClass; if (classItem.tsdocComment) { frontMatter.summary = extractSummary(classItem.tsdocComment); } frontMatter.title += " Class" break; case ApiItemKind.Interface: frontMatter.title += " Interface" const interfaceItem: ApiInterface = item as ApiInterface; if (interfaceItem.tsdocComment) { frontMatter.summary = extractSummary(interfaceItem.tsdocComment); } break; case ApiItemKind.Package: frontMatter.title += " Package" const pkgItem: ApiPackage = item as ApiPackage; apiMembers = pkgItem.entryPoints[0].members; if (pkgItem.tsdocComment) { frontMatter.summary = extractSummary(pkgItem.tsdocComment); } break; case ApiItemKind.Namespace: frontMatter.title += " Namespace" const namespaceItem: ApiNamespace = item as ApiNamespace; apiMembers = namespaceItem.members; if (namespaceItem.tsdocComment) { frontMatter.summary = extractSummary(namespaceItem.tsdocComment); } break; default: break; } frontMatter.members = new Map>(); apiMembers.forEach(element => { if (element.displayName === "") { return } if (!frontMatter.members[element.kind]) { frontMatter.members[element.kind] = {} } frontMatter.members[element.kind][element.displayName] = this._getLinkForApiItem(element); }); const pkg: ApiPackage | undefined = item.getAssociatedPackage(); if (pkg) { frontMatter.package = pkg.name.replace(/"/g, '').replace(/!/g, ''); frontMatter.unscopedPackageName = PackageName.getUnscopedName(pkg.name); } else { frontMatter.package = "undefined"; } stringBuilder.append(JSON.stringify(frontMatter)); stringBuilder.append( '\n\n[//]: # (Do not edit this file. It is automatically generated by API Documenter.)\n\n' ); } private _writeBreadcrumb(output: DocSection, apiItem: ApiItem): void { // no breadcrumbs for inner content if ((apiItem.kind !== ApiItemKind.Package) && (apiItem.kind !== ApiItemKind.Class) && (apiItem.kind !== ApiItemKind.Interface)) { return; } output.appendNodeInParagraph( new DocLinkTag({ configuration: this._tsdocConfiguration, tagName: '@link', linkText: 'Packages', urlDestination: this._getLinkForApiItem(this._apiModel) }) ); for (const hierarchyItem of apiItem.getHierarchy()) { switch (hierarchyItem.kind) { case ApiItemKind.Model: case ApiItemKind.EntryPoint: // We don't show the model as part of the breadcrumb because it is the root-level container. // We don't show the entry point because today API Extractor doesn't support multiple entry points; // this may change in the future. break; default: output.appendNodesInParagraph([ new DocPlainText({ configuration: this._tsdocConfiguration, text: ' > ' }), new DocLinkTag({ configuration: this._tsdocConfiguration, tagName: '@link', linkText: hierarchyItem.displayName, urlDestination: this._getLinkForApiItem(hierarchyItem) }) ]); } } } private _writeBetaWarning(output: DocSection | DocParagraph): void { const configuration: TSDocConfiguration = this._tsdocConfiguration; const betaWarning: string = 'This API is provided as a preview for developers and may change' + ' based on feedback that we receive. Do not use this API in a production environment.'; output.appendNode( new DocNoteBox({ configuration }, [ new DocParagraph({ configuration }, [new DocPlainText({ configuration, text: betaWarning })]) ]) ); } private _appendSection(output: DocSection | DocParagraph, docSection: DocSection): void { for (const node of docSection.nodes) { output.appendNode(node); } } private _appendAndMergeSection(output: DocSection, docSection: DocSection): void { let firstNode: boolean = true; for (const node of docSection.nodes) { if (firstNode) { if (node.kind === DocNodeKind.Paragraph) { output.appendNodesInParagraph(node.getChildNodes()); firstNode = false; continue; } } firstNode = false; output.appendNode(node); } } /** * Adjusts the name of the item as needed. * Accounts for method overloads by adding a suffix such as "MyClass.myMethod_2". */ private _getQualifiedApiItemName(apiItem: ApiItem): string { let qualifiedName: string = Utilities.getSafeFilenameForName(apiItem.displayName); if (ApiParameterListMixin.isBaseClassOf(apiItem)) { if (apiItem.overloadIndex > 1) { // Subtract one for compatibility with earlier releases of API Documenter. // (This will get revamped when we fix GitHub issue #1308) qualifiedName += `_${apiItem.overloadIndex - 1}`; } } return qualifiedName; } /** * Gets the nearest ancestor of the provided item that will have its own rendered page. * This can be useful for determining the file path the item will ultimately be rendered under, * as well as for generating links. */ private getFirstAncestorWithOwnPage(apiItem: ApiItem): ApiItem { // Walk parentage until we reach an item kind that gets rendered to its own page. // That is the page we will target with the generated link. let result = apiItem; while (!this._shouldHaveStandalonePage(result)) { if (result.parent === undefined) { throw new Error( 'Walking site hierarchy does not converge on an item that is rendered to its own page.'); } result = result.parent; } return result; } /** * Gets the file path for the specified API item. * In the case of an item that does not get rendered to its own page, this will point to the page * of the ancestor item under which the provided item will be rendered. * * Will return undefined if the item is not directly rendered * (i.e. {@link ApiItemKind.Model} and {@link ApiItemKind.EntryPoint}). * * @internal */ public _getFilePathForApiItem(apiItem: ApiItem): string | undefined { if (apiItem.kind === ApiItemKind.Model || apiItem.kind === ApiItemKind.EntryPoint) { // Files are not generated for these types return undefined; } const targetPageItem = this.getFirstAncestorWithOwnPage(apiItem); // Walk the target page item's hierarchy to create full file path let baseName: string | undefined = undefined; for (const hierarchyItem of targetPageItem.getHierarchy()) { const qualifiedName = this._getQualifiedApiItemName(hierarchyItem); switch (hierarchyItem.kind) { case ApiItemKind.Model: case ApiItemKind.EntryPoint: // No content is generated for Model or EntryPoint, so skip these break; case ApiItemKind.Package: // The package item is the root-most page under our uri-base if (baseName) { throw new Error('Path root already found.') } const safePackageFileName = Utilities.getSafeFilenameForName( PackageName.getUnscopedName(hierarchyItem.displayName) ); baseName = `${safePackageFileName}`; break; default: baseName += `/${qualifiedName}`; break; } } if (baseName === undefined) { throw new Error("Item's hierarchy did not converge on a package root.") } return `${baseName}.md`; } /** * Gets the heading ID (if any) for the specified API item. * Will return undefined if the item corresponds to the root of a page, or if the item is one that does * not result in any rendered contents (i.e. Model and EntryPoint). * * @internal */ public _htmlIDForItem(apiItem: ApiItem): string | undefined { if (apiItem.kind === ApiItemKind.Model || apiItem.kind === ApiItemKind.EntryPoint) { return undefined; } if(this._shouldHaveStandalonePage(apiItem)) { return undefined; } let baseName: string | undefined = undefined; let apiItemKind: ApiItemKind = apiItem.kind; // Walk parentage up until we reach the ancestor on whose page we're being rendered. // Generate ID information for everything back to that point let hierarchyItem = apiItem; while (!this._shouldHaveStandalonePage(hierarchyItem)) { switch (hierarchyItem.kind) { case ApiItemKind.Model: case ApiItemKind.EntryPoint: // No content is generated for Model or EntryPoint, so skip these break; case ApiItemKind.EnumMember: // Enum members do not get their own headings; they are only populated in a table. // Heading link should point to Enum parent. apiItemKind = ApiItemKind.Enum; break; default: const qualifiedName = this._getQualifiedApiItemName(hierarchyItem); // Since we're walking up the tree, we'll build the string from the end for simplicity baseName = baseName ? `${qualifiedName}-${baseName}` : qualifiedName; break; } if (hierarchyItem.parent === undefined) { throw new Error( 'Walking site hierarchy does not converge on an item that is rendered to its own page.'); } hierarchyItem = hierarchyItem.parent; } return `${baseName}-${apiItemKind}`; } /** * Generates the fully qualified url for the specified API item. * * @internal */ public _getLinkForApiItem(apiItem: ApiItem): string { if (apiItem.kind === ApiItemKind.Model || apiItem.kind === ApiItemKind.EntryPoint) { return this._uriRoot; } const filePath = this._getFilePathForApiItem(apiItem); if (filePath === undefined) { throw new Error('No file was found to write API item docs to.'); } const filePathWithoutExtension = filePath.replace(/\.md/g, ''); // Omit file extensions const headingRef = this._htmlIDForItem(apiItem); const headingRefString = headingRef ? `#${headingRef}` : ''; // Only append heading ID if needed return `${this._uriRoot}${filePathWithoutExtension}${headingRefString}`; } /** * Gets the appropriate heading level for the specified API item. * This is calculated based on the depth of the item relative to the page on which it's being rendered. * * @param apiItem - The item for which the level is being calculated. * @param levelOffset - An offset to apply to the generated value. Must be an integer. * * @internal */ public _getHeadingLevelForApiItem(apiItem: ApiItem, levelOffset: number): number { let headingLevel = 1 + (levelOffset === undefined ? 0 : levelOffset); let hierarchyItem = apiItem; function logStringForApiItem(apiItem: ApiItem): string { return `${apiItem.displayName} (${hierarchyItem.kind})`; } let hierarchyLog = logStringForApiItem(apiItem); while (!this._shouldHaveStandalonePage(hierarchyItem)) { if (hierarchyItem.parent === undefined) { throw new Error( 'Walking site hierarchy does not converge on an item that is rendered to its own page.'); } // API items under a package root appear under Package -> EntryPoint. // Since we don't generate any sub-headings for the entry-point, we need to skip // those entries for heading level computation. if (hierarchyItem.kind !== ApiItemKind.EntryPoint) { headingLevel++; hierarchyLog = `${logStringForApiItem(hierarchyItem)} -> ${hierarchyLog}`; } hierarchyItem = hierarchyItem.parent; } if (headingLevel > 5) { throw new Error(`Item hierarchy depth of "${headingLevel}" exceeds maximum heading level of 5. Item Hierarchy: "${hierarchyLog}" with a base offset of ${levelOffset}`); } return headingLevel; } private _deleteOldOutputFiles(): void { console.log('Deleting old output from ' + this._outputFolder); FileSystem.ensureEmptyFolder(this._outputFolder); } /** * Policy for deciding if an API item should get a dedicated page rendered for it (true), * or if its contents should be rendered directly within the parent item's page (false). */ private _shouldHaveStandalonePage(apiItem: ApiItem): boolean { return (apiItem.kind === ApiItemKind.Package) || (apiItem.kind === ApiItemKind.Class) || (apiItem.kind === ApiItemKind.Interface) || (apiItem.kind === ApiItemKind.Namespace) } private _isAllowedPackage(pkg: ApiPackage): boolean { const config = this._documenterConfig; if (config && config.onlyPackagesStartingWith) { if (typeof config.onlyPackagesStartingWith === "string") { return pkg.name.startsWith(config.onlyPackagesStartingWith); } else { return config.onlyPackagesStartingWith.some((prefix) => pkg.name.startsWith(prefix)); } } return true; } } /** * Input options for {@link MarkdownDocumenter._writeApiItemPage} */ interface PageRenderOptions { /** * Offset to automatically-generated heading levels. * Can be used to further nest sub-items being rendered to a page. * Must be an integer, and final heading (after applying the offset) must be on [1,5]. * @defaultValue 0 */ headingLevelOffset: number; } /** * Input options for {@link MarkdownDocumenter._writePageFile} */ interface FileRenderOptions { /** * Whether or not Hugo-formatted front-matter should be inserted at the top of the page. * @defaultValue true */ includeFrontMatter: boolean; }