tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

custom_markdown_documenter.ts (47592B)


      1 /**
      2 * @license
      3 * Copyright 2022 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the
      8 // MIT license. See LICENSE in the project root for license information.
      9 
     10 // Taken from
     11 // https://github.com/microsoft/rushstack/blob/main/apps/api-documenter/src/documenters/MarkdownDocumenter.ts
     12 // This file has been edited to morph into Docusaurus's expected inputs.
     13 
     14 import * as path from 'node:path';
     15 
     16 import type {DocumenterConfig} from '@microsoft/api-documenter/lib/documenters/DocumenterConfig.js';
     17 import {CustomMarkdownEmitter as ApiFormatterMarkdownEmitter} from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter.js';
     18 import {CustomDocNodes} from '@microsoft/api-documenter/lib/nodes/CustomDocNodeKind.js';
     19 import {DocEmphasisSpan} from '@microsoft/api-documenter/lib/nodes/DocEmphasisSpan.js';
     20 import {DocHeading} from '@microsoft/api-documenter/lib/nodes/DocHeading.js';
     21 import {DocNoteBox} from '@microsoft/api-documenter/lib/nodes/DocNoteBox.js';
     22 import {DocTable} from '@microsoft/api-documenter/lib/nodes/DocTable.js';
     23 import {DocTableCell} from '@microsoft/api-documenter/lib/nodes/DocTableCell.js';
     24 import {DocTableRow} from '@microsoft/api-documenter/lib/nodes/DocTableRow.js';
     25 import {MarkdownDocumenterAccessor} from '@microsoft/api-documenter/lib/plugin/MarkdownDocumenterAccessor.js';
     26 import {
     27  type IMarkdownDocumenterFeatureOnBeforeWritePageArgs,
     28  MarkdownDocumenterFeatureContext,
     29 } from '@microsoft/api-documenter/lib/plugin/MarkdownDocumenterFeature.js';
     30 import {PluginLoader} from '@microsoft/api-documenter/lib/plugin/PluginLoader.js';
     31 import {Utilities} from '@microsoft/api-documenter/lib/utils/Utilities.js';
     32 import {
     33  ApiClass,
     34  ApiDeclaredItem,
     35  ApiDocumentedItem,
     36  type ApiEnum,
     37  ApiInitializerMixin,
     38  ApiInterface,
     39  type ApiItem,
     40  ApiItemKind,
     41  type ApiModel,
     42  type ApiNamespace,
     43  ApiOptionalMixin,
     44  type ApiPackage,
     45  ApiParameterListMixin,
     46  ApiPropertyItem,
     47  ApiProtectedMixin,
     48  ApiReadonlyMixin,
     49  ApiReleaseTagMixin,
     50  ApiReturnTypeMixin,
     51  ApiStaticMixin,
     52  ApiTypeAlias,
     53  type Excerpt,
     54  type ExcerptToken,
     55  ExcerptTokenKind,
     56  type IResolveDeclarationReferenceResult,
     57  ReleaseTag,
     58 } from '@microsoft/api-extractor-model';
     59 import {
     60  type DocBlock,
     61  DocCodeSpan,
     62  type DocComment,
     63  DocFencedCode,
     64  DocLinkTag,
     65  type DocNodeContainer,
     66  DocNodeKind,
     67  DocParagraph,
     68  DocPlainText,
     69  DocSection,
     70  StandardTags,
     71  StringBuilder,
     72  DocHtmlStartTag,
     73  DocHtmlEndTag,
     74  DocHtmlAttribute,
     75  type TSDocConfiguration,
     76 } from '@microsoft/tsdoc';
     77 import {
     78  FileSystem,
     79  NewlineKind,
     80  PackageName,
     81 } from '@rushstack/node-core-library';
     82 
     83 export interface IMarkdownDocumenterOptions {
     84  apiModel: ApiModel;
     85  documenterConfig: DocumenterConfig | undefined;
     86  outputFolder: string;
     87 }
     88 
     89 export class CustomMarkdownEmitter extends ApiFormatterMarkdownEmitter {
     90  protected override getEscapedText(text: string): string {
     91    const textWithBackslashes = text
     92      .replace(/\\/g, '\\\\') // first replace the escape character
     93      .replace(/[*#[\]_|`~]/g, x => {
     94        return '\\' + x;
     95      }) // then escape any special characters
     96      .replace(/---/g, '\\-\\-\\-') // hyphens only if it's 3 or more
     97      .replace(/&/g, '&')
     98      .replace(/</g, '&lt;')
     99      .replace(/>/g, '&gt;')
    100      .replace(/\{/g, '&#123;')
    101      .replace(/\}/g, '&#125;');
    102    return textWithBackslashes;
    103  }
    104 }
    105 
    106 /**
    107 * Renders API documentation in the Markdown file format.
    108 * For more info: https://en.wikipedia.org/wiki/Markdown
    109 */
    110 export class MarkdownDocumenter {
    111  private readonly _apiModel: ApiModel;
    112  private readonly _documenterConfig: DocumenterConfig | undefined;
    113  private readonly _tsdocConfiguration: TSDocConfiguration;
    114  private readonly _markdownEmitter: CustomMarkdownEmitter;
    115  private readonly _outputFolder: string;
    116  private readonly _pluginLoader: PluginLoader;
    117 
    118  public constructor(options: IMarkdownDocumenterOptions) {
    119    this._apiModel = options.apiModel;
    120    this._documenterConfig = options.documenterConfig;
    121    this._outputFolder = options.outputFolder;
    122    this._tsdocConfiguration = CustomDocNodes.configuration;
    123    this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel);
    124 
    125    this._pluginLoader = new PluginLoader();
    126  }
    127 
    128  public generateFiles(): void {
    129    if (this._documenterConfig) {
    130      this._pluginLoader.load(this._documenterConfig, () => {
    131        return new MarkdownDocumenterFeatureContext({
    132          apiModel: this._apiModel,
    133          outputFolder: this._outputFolder,
    134          documenter: new MarkdownDocumenterAccessor({
    135            getLinkForApiItem: (apiItem: ApiItem) => {
    136              return this._getLinkFilenameForApiItem(apiItem);
    137            },
    138          }),
    139        });
    140      });
    141    }
    142 
    143    this._deleteOldOutputFiles();
    144 
    145    this._writeApiItemPage(this._apiModel.members[0]!);
    146 
    147    if (this._pluginLoader.markdownDocumenterFeature) {
    148      this._pluginLoader.markdownDocumenterFeature.onFinished({});
    149    }
    150  }
    151 
    152  private _getBaseApiItemPage(apiItem: ApiItem): DocSection {
    153    const configuration = this._tsdocConfiguration;
    154    const output = new DocSection({
    155      configuration,
    156    });
    157 
    158    const scopedName = apiItem.getScopedNameWithinPackage();
    159    switch (apiItem.kind) {
    160      case ApiItemKind.Class:
    161        output.appendNode(
    162          new DocHeading({configuration, title: `${scopedName} class`}),
    163        );
    164        break;
    165      case ApiItemKind.Enum:
    166        output.appendNode(
    167          new DocHeading({configuration, title: `${scopedName} enum`}),
    168        );
    169        break;
    170      case ApiItemKind.Interface:
    171        output.appendNode(
    172          new DocHeading({
    173            configuration,
    174            title: `${scopedName} interface`,
    175          }),
    176        );
    177        break;
    178      case ApiItemKind.Constructor:
    179      case ApiItemKind.ConstructSignature:
    180        output.appendNode(new DocHeading({configuration, title: scopedName}));
    181        break;
    182      case ApiItemKind.Method:
    183      case ApiItemKind.MethodSignature:
    184        output.appendNode(
    185          new DocHeading({configuration, title: `${scopedName} method`}),
    186        );
    187        break;
    188      case ApiItemKind.Function:
    189        output.appendNode(
    190          new DocHeading({configuration, title: `${scopedName} function`}),
    191        );
    192        break;
    193      case ApiItemKind.Model:
    194        output.appendNode(
    195          new DocHeading({configuration, title: `API Reference`}),
    196        );
    197        break;
    198      case ApiItemKind.Namespace:
    199        output.appendNode(
    200          new DocHeading({
    201            configuration,
    202            title: `${scopedName} namespace`,
    203          }),
    204        );
    205        break;
    206      case ApiItemKind.Package:
    207        console.log(`Writing ${apiItem.displayName} package`);
    208        output.appendNode(
    209          new DocHeading({
    210            configuration,
    211            title: `API Reference`,
    212          }),
    213        );
    214        break;
    215      case ApiItemKind.Property:
    216      case ApiItemKind.PropertySignature:
    217        output.appendNode(
    218          new DocHeading({configuration, title: `${scopedName} property`}),
    219        );
    220        break;
    221      case ApiItemKind.TypeAlias:
    222        output.appendNode(
    223          new DocHeading({configuration, title: `${scopedName} type`}),
    224        );
    225        break;
    226      case ApiItemKind.Variable:
    227        output.appendNode(
    228          new DocHeading({configuration, title: `${scopedName} variable`}),
    229        );
    230        break;
    231      default:
    232        throw new Error('Unsupported API item kind: ' + apiItem.kind);
    233    }
    234 
    235    return output;
    236  }
    237 
    238  private _getApiItemPage(apiItem: ApiItem): DocSection {
    239    const configuration = this._tsdocConfiguration;
    240    const output = new DocSection({
    241      configuration,
    242    });
    243 
    244    if (
    245      apiItem instanceof ApiDeclaredItem &&
    246      apiItem.excerpt.text.length > 0 &&
    247      ApiParameterListMixin.isBaseClassOf(apiItem) &&
    248      ApiReturnTypeMixin.isBaseClassOf(apiItem) &&
    249      apiItem.getMergedSiblings().length > 1
    250    ) {
    251      const name = apiItem.displayName;
    252      const overloadIndex = apiItem.overloadIndex - 1;
    253      const overloadId =
    254        overloadIndex === 0 ? name : `${name}-${overloadIndex}`;
    255 
    256      // TODO: See if we don't need to create all of the on our own.
    257      const overLoadHeader = `${apiItem.displayName}(): ${apiItem.returnTypeExcerpt.text}`;
    258      output.appendNode(
    259        new DocParagraph({configuration}, [
    260          new DocHtmlStartTag({
    261            configuration,
    262            name: 'h2',
    263            htmlAttributes: [
    264              new DocHtmlAttribute({
    265                configuration,
    266                name: 'id',
    267                value: `"${overloadId}"`,
    268              }),
    269            ],
    270          }),
    271          new DocPlainText({
    272            configuration,
    273            text: overLoadHeader,
    274          }),
    275          new DocHtmlEndTag({
    276            configuration,
    277            name: 'h2',
    278          }),
    279        ]),
    280      );
    281    }
    282 
    283    if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) {
    284      if (apiItem.releaseTag === ReleaseTag.Beta) {
    285        this._writeBetaWarning(output);
    286      }
    287    }
    288 
    289    const decoratorBlocks: DocBlock[] = [];
    290 
    291    if (apiItem instanceof ApiDocumentedItem) {
    292      const tsdocComment: DocComment | undefined = apiItem.tsdocComment;
    293 
    294      if (tsdocComment) {
    295        decoratorBlocks.push(
    296          ...tsdocComment.customBlocks.filter(block => {
    297            return (
    298              block.blockTag.tagNameWithUpperCase ===
    299              StandardTags.decorator.tagNameWithUpperCase
    300            );
    301          }),
    302        );
    303 
    304        if (tsdocComment.deprecatedBlock) {
    305          output.appendNode(
    306            new DocNoteBox({configuration: this._tsdocConfiguration}, [
    307              new DocParagraph({configuration: this._tsdocConfiguration}, [
    308                new DocPlainText({
    309                  configuration,
    310                  text: 'Warning: This API is now obsolete. ',
    311                }),
    312              ]),
    313              ...tsdocComment.deprecatedBlock.content.nodes,
    314            ]),
    315          );
    316        }
    317 
    318        this._appendSection(output, tsdocComment.summarySection);
    319      }
    320    }
    321 
    322    if (apiItem instanceof ApiDeclaredItem && apiItem.excerpt.text.length > 0) {
    323      let code: string | undefined;
    324      switch (apiItem.parent?.kind) {
    325        case ApiItemKind.Class:
    326          code = `class ${
    327            apiItem.parent.displayName
    328          } {${apiItem.getExcerptWithModifiers()}}`;
    329          break;
    330        case ApiItemKind.Interface:
    331          code = `interface ${
    332            apiItem.parent.displayName
    333          } {${apiItem.getExcerptWithModifiers()}}`;
    334          break;
    335        default:
    336          code = apiItem.getExcerptWithModifiers();
    337      }
    338      if (code) {
    339        output.appendNode(
    340          new DocHeading({
    341            configuration,
    342            title: 'Signature',
    343            level: 3,
    344          }),
    345        );
    346        output.appendNode(
    347          new DocFencedCode({
    348            configuration,
    349            code: code,
    350            language: 'typescript',
    351          }),
    352        );
    353      }
    354 
    355      this._writeHeritageTypes(output, apiItem);
    356    }
    357 
    358    if (decoratorBlocks.length > 0) {
    359      output.appendNode(
    360        new DocHeading({configuration, title: 'Decorators:', level: 4}),
    361      );
    362      for (const decoratorBlock of decoratorBlocks) {
    363        output.appendNodes(decoratorBlock.content.nodes);
    364      }
    365    }
    366 
    367    let appendRemarks = true;
    368    switch (apiItem.kind) {
    369      case ApiItemKind.Class:
    370      case ApiItemKind.Interface:
    371      case ApiItemKind.Namespace:
    372      case ApiItemKind.Package:
    373        this._writeRemarksSection(output, apiItem);
    374        appendRemarks = false;
    375        break;
    376    }
    377 
    378    switch (apiItem.kind) {
    379      case ApiItemKind.Class:
    380        this._writeClassTables(output, apiItem as ApiClass);
    381        break;
    382      case ApiItemKind.Enum:
    383        this._writeEnumTables(output, apiItem as ApiEnum);
    384        break;
    385      case ApiItemKind.Interface:
    386        this._writeInterfaceTables(output, apiItem as ApiInterface);
    387        break;
    388      case ApiItemKind.Constructor:
    389      case ApiItemKind.ConstructSignature:
    390      case ApiItemKind.Method:
    391      case ApiItemKind.MethodSignature:
    392      case ApiItemKind.Function:
    393        this._writeParameterTables(output, apiItem as ApiParameterListMixin);
    394        this._writeThrowsSection(output, apiItem);
    395        break;
    396      case ApiItemKind.Namespace:
    397        this._writePackageOrNamespaceTables(output, apiItem as ApiNamespace);
    398        break;
    399      case ApiItemKind.Model:
    400        this._writeModelTable(output, apiItem as ApiModel);
    401        break;
    402      case ApiItemKind.Package:
    403        this._writePackageOrNamespaceTables(output, apiItem as ApiPackage);
    404        break;
    405      case ApiItemKind.Property:
    406      case ApiItemKind.PropertySignature:
    407        break;
    408      case ApiItemKind.TypeAlias:
    409        break;
    410      case ApiItemKind.Variable:
    411        break;
    412      default:
    413        throw new Error('Unsupported API item kind: ' + apiItem.kind);
    414    }
    415 
    416    this._writeDefaultValueSection(output, apiItem);
    417 
    418    if (appendRemarks) {
    419      this._writeRemarksSection(output, apiItem);
    420    }
    421 
    422    return output;
    423  }
    424 
    425  private _writeApiItemPage(apiItem: ApiItem) {
    426    const output = this._getBaseApiItemPage(apiItem);
    427    if (ApiParameterListMixin.isBaseClassOf(apiItem)) {
    428      if (apiItem.overloadIndex > 1) {
    429        return;
    430      }
    431 
    432      for (const item of apiItem.getMergedSiblings()) {
    433        const itemOutput = this._getApiItemPage(item);
    434        output.appendNodes(itemOutput.nodes);
    435      }
    436    } else {
    437      const itemOutput = this._getApiItemPage(apiItem);
    438      output.appendNodes(itemOutput.nodes);
    439    }
    440 
    441    const filename = path.join(
    442      this._outputFolder,
    443      this._getFilenameForApiItem(apiItem),
    444    );
    445    const stringBuilder = new StringBuilder();
    446    this._markdownEmitter.emit(stringBuilder, output, {
    447      contextApiItem: apiItem,
    448      onGetFilenameForApiItem: (apiItemForFilename: ApiItem) => {
    449        return this._getLinkFilenameForApiItem(apiItemForFilename);
    450      },
    451    });
    452 
    453    let pageContent = stringBuilder.toString();
    454 
    455    if (this._pluginLoader.markdownDocumenterFeature) {
    456      // Allow the plugin to customize the pageContent
    457      const eventArgs: IMarkdownDocumenterFeatureOnBeforeWritePageArgs = {
    458        apiItem: apiItem,
    459        outputFilename: filename,
    460        pageContent: pageContent,
    461      };
    462      this._pluginLoader.markdownDocumenterFeature.onBeforeWritePage(eventArgs);
    463      pageContent = eventArgs.pageContent;
    464    }
    465 
    466    pageContent =
    467      `---\nsidebar_label: ${this._getSidebarLabelForApiItem(apiItem)}\n---` +
    468      pageContent;
    469    pageContent = pageContent.replace('##', '#');
    470    pageContent = pageContent.replace(/<!-- -->/g, '');
    471    pageContent = pageContent.replace(/\\\*\\\*/g, '**');
    472    pageContent = pageContent.replace(/<b>|<\/b>/g, '**');
    473    FileSystem.writeFile(filename, pageContent, {
    474      convertLineEndings: this._documenterConfig
    475        ? this._documenterConfig.newlineKind
    476        : NewlineKind.CrLf,
    477    });
    478  }
    479 
    480  private _writeHeritageTypes(
    481    output: DocSection,
    482    apiItem: ApiDeclaredItem,
    483  ): void {
    484    const configuration = this._tsdocConfiguration;
    485 
    486    if (apiItem instanceof ApiClass) {
    487      if (apiItem.extendsType) {
    488        const extendsParagraph = new DocParagraph({configuration}, [
    489          new DocEmphasisSpan({configuration, bold: true}, [
    490            new DocPlainText({configuration, text: 'Extends: '}),
    491          ]),
    492        ]);
    493        this._appendExcerptWithHyperlinks(
    494          extendsParagraph,
    495          apiItem.extendsType.excerpt,
    496        );
    497        output.appendNode(extendsParagraph);
    498      }
    499      if (apiItem.implementsTypes.length > 0) {
    500        const extendsParagraph = new DocParagraph({configuration}, [
    501          new DocEmphasisSpan({configuration, bold: true}, [
    502            new DocPlainText({configuration, text: 'Implements: '}),
    503          ]),
    504        ]);
    505        let needsComma = false;
    506        for (const implementsType of apiItem.implementsTypes) {
    507          if (needsComma) {
    508            extendsParagraph.appendNode(
    509              new DocPlainText({configuration, text: ', '}),
    510            );
    511          }
    512          this._appendExcerptWithHyperlinks(
    513            extendsParagraph,
    514            implementsType.excerpt,
    515          );
    516          needsComma = true;
    517        }
    518        output.appendNode(extendsParagraph);
    519      }
    520    }
    521 
    522    if (apiItem instanceof ApiInterface) {
    523      if (apiItem.extendsTypes.length > 0) {
    524        const extendsParagraph = new DocParagraph({configuration}, [
    525          new DocEmphasisSpan({configuration, bold: true}, [
    526            new DocPlainText({configuration, text: 'Extends: '}),
    527          ]),
    528        ]);
    529        let needsComma = false;
    530        for (const extendsType of apiItem.extendsTypes) {
    531          if (needsComma) {
    532            extendsParagraph.appendNode(
    533              new DocPlainText({configuration, text: ', '}),
    534            );
    535          }
    536          this._appendExcerptWithHyperlinks(
    537            extendsParagraph,
    538            extendsType.excerpt,
    539          );
    540          needsComma = true;
    541        }
    542        output.appendNode(extendsParagraph);
    543      }
    544    }
    545 
    546    if (apiItem instanceof ApiTypeAlias) {
    547      const refs: ExcerptToken[] = apiItem.excerptTokens.filter(token => {
    548        return (
    549          token.kind === ExcerptTokenKind.Reference &&
    550          token.canonicalReference &&
    551          this._apiModel.resolveDeclarationReference(
    552            token.canonicalReference,
    553            undefined,
    554          ).resolvedApiItem
    555        );
    556      });
    557      if (refs.length > 0) {
    558        const referencesParagraph = new DocParagraph({configuration}, [
    559          new DocEmphasisSpan({configuration, bold: true}, [
    560            new DocPlainText({configuration, text: 'References: '}),
    561          ]),
    562        ]);
    563        let needsComma = false;
    564        const visited = new Set<string>();
    565        for (const ref of refs) {
    566          if (visited.has(ref.text)) {
    567            continue;
    568          }
    569          visited.add(ref.text);
    570 
    571          if (needsComma) {
    572            referencesParagraph.appendNode(
    573              new DocPlainText({configuration, text: ', '}),
    574            );
    575          }
    576 
    577          this._appendExcerptTokenWithHyperlinks(referencesParagraph, ref);
    578          needsComma = true;
    579        }
    580        output.appendNode(referencesParagraph);
    581      }
    582    }
    583  }
    584 
    585  private _writeDefaultValueSection(output: DocSection, apiItem: ApiItem) {
    586    if (apiItem instanceof ApiDocumentedItem) {
    587      const block = apiItem.tsdocComment?.customBlocks.find(block => {
    588        return (
    589          block.blockTag.tagNameWithUpperCase ===
    590          StandardTags.defaultValue.tagNameWithUpperCase
    591        );
    592      });
    593      if (block) {
    594        output.appendNode(
    595          new DocHeading({
    596            configuration: this._tsdocConfiguration,
    597            title: 'Default value:',
    598            level: 4,
    599          }),
    600        );
    601        this._appendSection(output, block.content);
    602      }
    603    }
    604  }
    605 
    606  private _writeRemarksSection(output: DocSection, apiItem: ApiItem): void {
    607    const configuration = this._tsdocConfiguration;
    608 
    609    if (apiItem instanceof ApiDocumentedItem) {
    610      const tsdocComment: DocComment | undefined = apiItem.tsdocComment;
    611 
    612      if (tsdocComment) {
    613        // Write the @remarks block
    614        if (tsdocComment.remarksBlock) {
    615          output.appendNode(
    616            new DocHeading({
    617              configuration,
    618              title: 'Remarks',
    619            }),
    620          );
    621          this._appendSection(output, tsdocComment.remarksBlock.content);
    622        }
    623 
    624        // Write the @example blocks
    625        const exampleBlocks: DocBlock[] = tsdocComment.customBlocks.filter(
    626          x => {
    627            return (
    628              x.blockTag.tagNameWithUpperCase ===
    629              StandardTags.example.tagNameWithUpperCase
    630            );
    631          },
    632        );
    633 
    634        let exampleNumber = 1;
    635        for (const exampleBlock of exampleBlocks) {
    636          const heading: string =
    637            exampleBlocks.length > 1 ? `Example ${exampleNumber}` : 'Example';
    638 
    639          output.appendNode(
    640            new DocHeading({
    641              configuration,
    642              title: heading,
    643            }),
    644          );
    645 
    646          this._appendSection(output, exampleBlock.content);
    647 
    648          ++exampleNumber;
    649        }
    650      }
    651    }
    652  }
    653 
    654  private _writeThrowsSection(output: DocSection, apiItem: ApiItem): void {
    655    if (apiItem instanceof ApiDocumentedItem) {
    656      const tsdocComment: DocComment | undefined = apiItem.tsdocComment;
    657 
    658      if (tsdocComment) {
    659        // Write the @throws blocks
    660        const throwsBlocks: DocBlock[] = tsdocComment.customBlocks.filter(x => {
    661          return (
    662            x.blockTag.tagNameWithUpperCase ===
    663            StandardTags.throws.tagNameWithUpperCase
    664          );
    665        });
    666 
    667        if (throwsBlocks.length > 0) {
    668          const heading = 'Exceptions';
    669          output.appendNode(
    670            new DocHeading({
    671              configuration: this._tsdocConfiguration,
    672              title: heading,
    673            }),
    674          );
    675 
    676          for (const throwsBlock of throwsBlocks) {
    677            this._appendSection(output, throwsBlock.content);
    678          }
    679        }
    680      }
    681    }
    682  }
    683 
    684  /**
    685   * GENERATE PAGE: MODEL
    686   */
    687  private _writeModelTable(output: DocSection, apiModel: ApiModel): void {
    688    const configuration = this._tsdocConfiguration;
    689 
    690    const packagesTable = new DocTable({
    691      configuration,
    692      headerTitles: ['Package', 'Description'],
    693    });
    694 
    695    for (const apiMember of apiModel.members) {
    696      const row = new DocTableRow({configuration}, [
    697        this._createTitleCell(apiMember),
    698        this._createDescriptionCell(apiMember),
    699      ]);
    700 
    701      switch (apiMember.kind) {
    702        case ApiItemKind.Package:
    703          packagesTable.addRow(row);
    704          this._writeApiItemPage(apiMember);
    705          break;
    706      }
    707    }
    708 
    709    if (packagesTable.rows.length > 0) {
    710      output.appendNode(
    711        new DocHeading({
    712          configuration,
    713          title: 'Packages',
    714        }),
    715      );
    716      output.appendNode(packagesTable);
    717    }
    718  }
    719 
    720  /**
    721   * GENERATE PAGE: PACKAGE or NAMESPACE
    722   */
    723  private _writePackageOrNamespaceTables(
    724    output: DocSection,
    725    apiContainer: ApiPackage | ApiNamespace,
    726  ): void {
    727    const configuration = this._tsdocConfiguration;
    728 
    729    const classesTable = new DocTable({
    730      configuration,
    731      headerTitles: ['Class', 'Description'],
    732    });
    733 
    734    const enumerationsTable = new DocTable({
    735      configuration,
    736      headerTitles: ['Enumeration', 'Description'],
    737    });
    738 
    739    const functionsTable = new DocTable({
    740      configuration,
    741      headerTitles: ['Function', 'Description'],
    742    });
    743 
    744    const interfacesTable = new DocTable({
    745      configuration,
    746      headerTitles: ['Interface', 'Description'],
    747    });
    748 
    749    const namespacesTable = new DocTable({
    750      configuration,
    751      headerTitles: ['Namespace', 'Description'],
    752    });
    753 
    754    const variablesTable = new DocTable({
    755      configuration,
    756      headerTitles: ['Variable', 'Description'],
    757    });
    758 
    759    const typeAliasesTable = new DocTable({
    760      configuration,
    761      headerTitles: ['Type Alias', 'Description'],
    762    });
    763 
    764    const apiMembers: readonly ApiItem[] =
    765      apiContainer.kind === ApiItemKind.Package
    766        ? (apiContainer as ApiPackage).entryPoints[0]!.members
    767        : (apiContainer as ApiNamespace).members;
    768 
    769    for (const apiMember of apiMembers) {
    770      const row = new DocTableRow({configuration}, [
    771        this._createTitleCell(apiMember),
    772        this._createDescriptionCell(apiMember),
    773      ]);
    774 
    775      switch (apiMember.kind) {
    776        case ApiItemKind.Class:
    777          classesTable.addRow(row);
    778          this._writeApiItemPage(apiMember);
    779          break;
    780 
    781        case ApiItemKind.Enum:
    782          enumerationsTable.addRow(row);
    783          this._writeApiItemPage(apiMember);
    784          break;
    785 
    786        case ApiItemKind.Interface:
    787          interfacesTable.addRow(row);
    788          this._writeApiItemPage(apiMember);
    789          break;
    790 
    791        case ApiItemKind.Namespace:
    792          namespacesTable.addRow(row);
    793          this._writeApiItemPage(apiMember);
    794          break;
    795 
    796        case ApiItemKind.Function:
    797          functionsTable.addRow(row);
    798          this._writeApiItemPage(apiMember);
    799          break;
    800 
    801        case ApiItemKind.TypeAlias:
    802          typeAliasesTable.addRow(row);
    803          this._writeApiItemPage(apiMember);
    804          break;
    805 
    806        case ApiItemKind.Variable:
    807          variablesTable.addRow(row);
    808          this._writeApiItemPage(apiMember);
    809          break;
    810      }
    811    }
    812 
    813    if (classesTable.rows.length > 0) {
    814      output.appendNode(
    815        new DocHeading({
    816          configuration,
    817          title: 'Classes',
    818        }),
    819      );
    820      output.appendNode(classesTable);
    821    }
    822 
    823    if (enumerationsTable.rows.length > 0) {
    824      output.appendNode(
    825        new DocHeading({
    826          configuration,
    827          title: 'Enumerations',
    828        }),
    829      );
    830      output.appendNode(enumerationsTable);
    831    }
    832    if (functionsTable.rows.length > 0) {
    833      output.appendNode(
    834        new DocHeading({
    835          configuration,
    836          title: 'Functions',
    837        }),
    838      );
    839      output.appendNode(functionsTable);
    840    }
    841 
    842    if (interfacesTable.rows.length > 0) {
    843      output.appendNode(
    844        new DocHeading({
    845          configuration,
    846          title: 'Interfaces',
    847        }),
    848      );
    849      output.appendNode(interfacesTable);
    850    }
    851 
    852    if (namespacesTable.rows.length > 0) {
    853      output.appendNode(
    854        new DocHeading({
    855          configuration,
    856          title: 'Namespaces',
    857        }),
    858      );
    859      output.appendNode(namespacesTable);
    860    }
    861 
    862    if (variablesTable.rows.length > 0) {
    863      output.appendNode(
    864        new DocHeading({
    865          configuration,
    866          title: 'Variables',
    867        }),
    868      );
    869      output.appendNode(variablesTable);
    870    }
    871 
    872    if (typeAliasesTable.rows.length > 0) {
    873      output.appendNode(
    874        new DocHeading({
    875          configuration,
    876          title: 'Type Aliases',
    877        }),
    878      );
    879      output.appendNode(typeAliasesTable);
    880    }
    881  }
    882 
    883  /**
    884   * GENERATE PAGE: CLASS
    885   */
    886  private _writeClassTables(output: DocSection, apiClass: ApiClass): void {
    887    const configuration = this._tsdocConfiguration;
    888 
    889    const eventsTable = new DocTable({
    890      configuration,
    891      headerTitles: ['Property', 'Modifiers', 'Type', 'Description'],
    892    });
    893 
    894    const constructorsTable = new DocTable({
    895      configuration,
    896      headerTitles: ['Constructor', 'Modifiers', 'Description'],
    897    });
    898 
    899    const propertiesTable = new DocTable({
    900      configuration,
    901      headerTitles: ['Property', 'Modifiers', 'Type', 'Description'],
    902    });
    903 
    904    const methodsTable = new DocTable({
    905      configuration,
    906      headerTitles: ['Method', 'Modifiers', 'Description'],
    907    });
    908 
    909    for (const apiMember of apiClass.members) {
    910      switch (apiMember.kind) {
    911        case ApiItemKind.Constructor: {
    912          constructorsTable.addRow(
    913            new DocTableRow({configuration}, [
    914              this._createTitleCell(apiMember),
    915              this._createModifiersCell(apiMember),
    916              this._createDescriptionCell(apiMember),
    917            ]),
    918          );
    919 
    920          this._writeApiItemPage(apiMember);
    921          break;
    922        }
    923        case ApiItemKind.Method: {
    924          methodsTable.addRow(
    925            new DocTableRow({configuration}, [
    926              this._createTitleCell(apiMember),
    927              this._createModifiersCell(apiMember),
    928              this._createDescriptionCell(apiMember),
    929            ]),
    930          );
    931 
    932          this._writeApiItemPage(apiMember);
    933          break;
    934        }
    935        case ApiItemKind.Property: {
    936          if ((apiMember as ApiPropertyItem).isEventProperty) {
    937            eventsTable.addRow(
    938              new DocTableRow({configuration}, [
    939                this._createTitleCell(apiMember, true),
    940                this._createModifiersCell(apiMember),
    941                this._createPropertyTypeCell(apiMember),
    942                this._createDescriptionCell(apiMember),
    943              ]),
    944            );
    945          } else {
    946            propertiesTable.addRow(
    947              new DocTableRow({configuration}, [
    948                this._createTitleCell(apiMember, true),
    949                this._createModifiersCell(apiMember),
    950                this._createPropertyTypeCell(apiMember),
    951                this._createDescriptionCell(apiMember),
    952              ]),
    953            );
    954          }
    955          break;
    956        }
    957      }
    958    }
    959 
    960    if (eventsTable.rows.length > 0) {
    961      output.appendNode(
    962        new DocHeading({
    963          configuration,
    964          title: 'Events',
    965        }),
    966      );
    967      output.appendNode(eventsTable);
    968    }
    969 
    970    if (constructorsTable.rows.length > 0) {
    971      output.appendNode(
    972        new DocHeading({
    973          configuration,
    974          title: 'Constructors',
    975        }),
    976      );
    977      output.appendNode(constructorsTable);
    978    }
    979 
    980    if (propertiesTable.rows.length > 0) {
    981      output.appendNode(
    982        new DocHeading({
    983          configuration,
    984          title: 'Properties',
    985        }),
    986      );
    987      output.appendNode(propertiesTable);
    988    }
    989 
    990    if (methodsTable.rows.length > 0) {
    991      output.appendNode(
    992        new DocHeading({
    993          configuration,
    994          title: 'Methods',
    995        }),
    996      );
    997      output.appendNode(methodsTable);
    998    }
    999  }
   1000 
   1001  /**
   1002   * GENERATE PAGE: ENUM
   1003   */
   1004  private _writeEnumTables(output: DocSection, apiEnum: ApiEnum): void {
   1005    const configuration = this._tsdocConfiguration;
   1006 
   1007    const enumMembersTable = new DocTable({
   1008      configuration,
   1009      headerTitles: ['Member', 'Value', 'Description'],
   1010    });
   1011 
   1012    for (const apiEnumMember of apiEnum.members) {
   1013      enumMembersTable.addRow(
   1014        new DocTableRow({configuration}, [
   1015          new DocTableCell({configuration}, [
   1016            new DocParagraph({configuration}, [
   1017              new DocPlainText({
   1018                configuration,
   1019                text: Utilities.getConciseSignature(apiEnumMember),
   1020              }),
   1021            ]),
   1022          ]),
   1023          this._createInitializerCell(apiEnumMember),
   1024          this._createDescriptionCell(apiEnumMember),
   1025        ]),
   1026      );
   1027    }
   1028 
   1029    if (enumMembersTable.rows.length > 0) {
   1030      output.appendNode(
   1031        new DocHeading({
   1032          configuration,
   1033          title: 'Enumeration Members',
   1034        }),
   1035      );
   1036      output.appendNode(enumMembersTable);
   1037    }
   1038  }
   1039 
   1040  /**
   1041   * GENERATE PAGE: INTERFACE
   1042   */
   1043  private _writeInterfaceTables(
   1044    output: DocSection,
   1045    apiClass: ApiInterface,
   1046  ): void {
   1047    const configuration = this._tsdocConfiguration;
   1048 
   1049    const eventsTable = new DocTable({
   1050      configuration,
   1051      headerTitles: ['Property', 'Modifiers', 'Type', 'Description'],
   1052    });
   1053 
   1054    const propertiesTable = new DocTable({
   1055      configuration,
   1056      headerTitles: ['Property', 'Modifiers', 'Type', 'Description', 'Default'],
   1057    });
   1058 
   1059    const methodsTable = new DocTable({
   1060      configuration,
   1061      headerTitles: ['Method', 'Description'],
   1062    });
   1063 
   1064    for (const apiMember of apiClass.members) {
   1065      switch (apiMember.kind) {
   1066        case ApiItemKind.ConstructSignature:
   1067        case ApiItemKind.MethodSignature: {
   1068          methodsTable.addRow(
   1069            new DocTableRow({configuration}, [
   1070              this._createTitleCell(apiMember),
   1071              this._createDescriptionCell(apiMember),
   1072            ]),
   1073          );
   1074 
   1075          this._writeApiItemPage(apiMember);
   1076          break;
   1077        }
   1078        case ApiItemKind.PropertySignature: {
   1079          if ((apiMember as ApiPropertyItem).isEventProperty) {
   1080            eventsTable.addRow(
   1081              new DocTableRow({configuration}, [
   1082                this._createTitleCell(apiMember, true),
   1083                this._createModifiersCell(apiMember),
   1084                this._createPropertyTypeCell(apiMember),
   1085                this._createDescriptionCell(apiMember),
   1086              ]),
   1087            );
   1088          } else {
   1089            propertiesTable.addRow(
   1090              new DocTableRow({configuration}, [
   1091                this._createTitleCell(apiMember, true),
   1092                this._createModifiersCell(apiMember),
   1093                this._createPropertyTypeCell(apiMember),
   1094                this._createDescriptionCell(apiMember),
   1095                this._createDefaultCell(apiMember),
   1096              ]),
   1097            );
   1098          }
   1099          break;
   1100        }
   1101      }
   1102    }
   1103 
   1104    if (eventsTable.rows.length > 0) {
   1105      output.appendNode(
   1106        new DocHeading({
   1107          configuration,
   1108          title: 'Events',
   1109        }),
   1110      );
   1111      output.appendNode(eventsTable);
   1112    }
   1113 
   1114    if (propertiesTable.rows.length > 0) {
   1115      output.appendNode(
   1116        new DocHeading({
   1117          configuration,
   1118          title: 'Properties',
   1119        }),
   1120      );
   1121      output.appendNode(propertiesTable);
   1122    }
   1123 
   1124    if (methodsTable.rows.length > 0) {
   1125      output.appendNode(
   1126        new DocHeading({
   1127          configuration,
   1128          title: 'Methods',
   1129        }),
   1130      );
   1131      output.appendNode(methodsTable);
   1132    }
   1133  }
   1134 
   1135  /**
   1136   * GENERATE PAGE: FUNCTION-LIKE
   1137   */
   1138  private _writeParameterTables(
   1139    output: DocSection,
   1140    apiParameterListMixin: ApiParameterListMixin,
   1141  ): void {
   1142    const configuration = this._tsdocConfiguration;
   1143 
   1144    const parametersTable = new DocTable({
   1145      configuration,
   1146      headerTitles: ['Parameter', 'Type', 'Description'],
   1147    });
   1148    for (const apiParameter of apiParameterListMixin.parameters) {
   1149      const parameterDescription = new DocSection({configuration});
   1150 
   1151      if (apiParameter.isOptional) {
   1152        parameterDescription.appendNodesInParagraph([
   1153          new DocEmphasisSpan({configuration, italic: true}, [
   1154            new DocPlainText({configuration, text: '(Optional)'}),
   1155          ]),
   1156          new DocPlainText({configuration, text: ' '}),
   1157        ]);
   1158      }
   1159 
   1160      if (apiParameter.tsdocParamBlock) {
   1161        this._appendAndMergeSection(
   1162          parameterDescription,
   1163          apiParameter.tsdocParamBlock.content,
   1164        );
   1165      }
   1166 
   1167      parametersTable.addRow(
   1168        new DocTableRow({configuration}, [
   1169          new DocTableCell({configuration}, [
   1170            new DocParagraph({configuration}, [
   1171              new DocPlainText({configuration, text: apiParameter.name}),
   1172            ]),
   1173          ]),
   1174          new DocTableCell({configuration}, [
   1175            this._createParagraphForTypeExcerpt(
   1176              apiParameter.parameterTypeExcerpt,
   1177            ),
   1178          ]),
   1179          new DocTableCell({configuration}, parameterDescription.nodes),
   1180        ]),
   1181      );
   1182    }
   1183 
   1184    if (parametersTable.rows.length > 0) {
   1185      output.appendNode(
   1186        new DocHeading({
   1187          configuration,
   1188          title: 'Parameters',
   1189        }),
   1190      );
   1191      output.appendNode(parametersTable);
   1192    }
   1193 
   1194    if (ApiReturnTypeMixin.isBaseClassOf(apiParameterListMixin)) {
   1195      const returnTypeExcerpt: Excerpt =
   1196        apiParameterListMixin.returnTypeExcerpt;
   1197      output.appendNode(
   1198        new DocParagraph({configuration}, [
   1199          new DocEmphasisSpan({configuration, bold: true}, [
   1200            new DocPlainText({configuration, text: 'Returns:'}),
   1201          ]),
   1202        ]),
   1203      );
   1204 
   1205      output.appendNode(this._createParagraphForTypeExcerpt(returnTypeExcerpt));
   1206 
   1207      if (apiParameterListMixin instanceof ApiDocumentedItem) {
   1208        if (
   1209          apiParameterListMixin.tsdocComment &&
   1210          apiParameterListMixin.tsdocComment.returnsBlock
   1211        ) {
   1212          this._appendSection(
   1213            output,
   1214            apiParameterListMixin.tsdocComment.returnsBlock.content,
   1215          );
   1216        }
   1217      }
   1218    }
   1219  }
   1220 
   1221  private _createParagraphForTypeExcerpt(excerpt: Excerpt): DocParagraph {
   1222    const configuration = this._tsdocConfiguration;
   1223 
   1224    const paragraph = new DocParagraph({configuration});
   1225    if (!excerpt.text.trim()) {
   1226      paragraph.appendNode(
   1227        new DocPlainText({configuration, text: '(not declared)'}),
   1228      );
   1229    } else {
   1230      this._appendExcerptWithHyperlinks(paragraph, excerpt);
   1231    }
   1232 
   1233    return paragraph;
   1234  }
   1235 
   1236  private _appendExcerptWithHyperlinks(
   1237    docNodeContainer: DocNodeContainer,
   1238    excerpt: Excerpt,
   1239  ): void {
   1240    for (const token of excerpt.spannedTokens) {
   1241      this._appendExcerptTokenWithHyperlinks(docNodeContainer, token);
   1242    }
   1243  }
   1244 
   1245  private _appendExcerptTokenWithHyperlinks(
   1246    docNodeContainer: DocNodeContainer,
   1247    token: ExcerptToken,
   1248  ): void {
   1249    const configuration = this._tsdocConfiguration;
   1250 
   1251    // Markdown doesn't provide a standardized syntax for hyperlinks inside code
   1252    // spans, so we will render the type expression as DocPlainText.  Instead of
   1253    // creating multiple DocParagraphs, we can simply discard any newlines and
   1254    // let the renderer do normal word-wrapping.
   1255    const unwrappedTokenText = token.text.replace(/[\r\n]+/g, ' ');
   1256 
   1257    // If it's hyperlinkable, then append a DocLinkTag
   1258    if (token.kind === ExcerptTokenKind.Reference && token.canonicalReference) {
   1259      const apiItemResult: IResolveDeclarationReferenceResult =
   1260        this._apiModel.resolveDeclarationReference(
   1261          token.canonicalReference,
   1262          undefined,
   1263        );
   1264 
   1265      if (apiItemResult.resolvedApiItem) {
   1266        docNodeContainer.appendNode(
   1267          new DocLinkTag({
   1268            configuration,
   1269            tagName: StandardTags.link.tagName,
   1270            linkText: unwrappedTokenText,
   1271            urlDestination: this._getLinkFilenameForApiItem(
   1272              apiItemResult.resolvedApiItem,
   1273            ),
   1274          }),
   1275        );
   1276        return;
   1277      }
   1278    }
   1279 
   1280    // Otherwise append non-hyperlinked text
   1281    docNodeContainer.appendNode(
   1282      new DocPlainText({configuration, text: unwrappedTokenText}),
   1283    );
   1284  }
   1285 
   1286  private _createTitleCell(apiItem: ApiItem, plain = false): DocTableCell {
   1287    const configuration = this._tsdocConfiguration;
   1288 
   1289    const text = Utilities.getConciseSignature(apiItem);
   1290 
   1291    return new DocTableCell({configuration}, [
   1292      new DocParagraph({configuration}, [
   1293        new DocHtmlStartTag({
   1294          configuration,
   1295          name: 'span',
   1296          htmlAttributes: [
   1297            new DocHtmlAttribute({
   1298              configuration,
   1299              name: 'id',
   1300              value: `"${Utilities.getSafeFilenameForName(apiItem.displayName)}"`,
   1301            }),
   1302          ],
   1303        }),
   1304        plain
   1305          ? new DocPlainText({configuration, text})
   1306          : new DocLinkTag({
   1307              configuration,
   1308              tagName: '@link',
   1309              linkText: text,
   1310              urlDestination: this._getLinkFilenameForApiItem(apiItem),
   1311            }),
   1312        new DocHtmlEndTag({
   1313          configuration,
   1314          name: 'span',
   1315        }),
   1316      ]),
   1317    ]);
   1318  }
   1319 
   1320  /**
   1321   * This generates a DocTableCell for an ApiItem including the summary section
   1322   * and "(BETA)" annotation.
   1323   *
   1324   * @remarks
   1325   * We mostly assume that the input is an ApiDocumentedItem, but it's easier to
   1326   * perform this as a runtime check than to have each caller perform a type
   1327   * cast.
   1328   */
   1329  private _createDescriptionCell(apiItem: ApiItem): DocTableCell {
   1330    const configuration = this._tsdocConfiguration;
   1331 
   1332    const section = new DocSection({configuration});
   1333 
   1334    if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) {
   1335      if (apiItem.releaseTag === ReleaseTag.Beta) {
   1336        section.appendNodesInParagraph([
   1337          new DocEmphasisSpan({configuration, bold: true, italic: true}, [
   1338            new DocPlainText({configuration, text: '(BETA)'}),
   1339          ]),
   1340          new DocPlainText({configuration, text: ' '}),
   1341        ]);
   1342      }
   1343    }
   1344 
   1345    if (apiItem instanceof ApiDocumentedItem) {
   1346      const isExperimental =
   1347        apiItem.tsdocComment?.modifierTagSet.isExperimental();
   1348      if (isExperimental) {
   1349        section.appendNodesInParagraph([
   1350          new DocEmphasisSpan({configuration, bold: true, italic: true}, [
   1351            new DocPlainText({configuration, text: '(Experimental)'}),
   1352          ]),
   1353          new DocPlainText({configuration, text: ' '}),
   1354        ]);
   1355      }
   1356    }
   1357 
   1358    if (apiItem instanceof ApiDocumentedItem) {
   1359      if (apiItem.tsdocComment !== undefined) {
   1360        this._appendAndMergeSection(
   1361          section,
   1362          apiItem.tsdocComment.summarySection,
   1363        );
   1364 
   1365        if (apiItem.tsdocComment.deprecatedBlock) {
   1366          section.appendNode(
   1367            new DocParagraph({configuration}, [
   1368              new DocEmphasisSpan({configuration, bold: true}, [
   1369                new DocPlainText({configuration, text: 'Deprecated: '}),
   1370              ]),
   1371            ]),
   1372          );
   1373 
   1374          section.appendNodes(
   1375            apiItem.tsdocComment.deprecatedBlock.content.getChildNodes(),
   1376          );
   1377        }
   1378      }
   1379    }
   1380 
   1381    if (apiItem instanceof ApiDocumentedItem) {
   1382      const remarks = apiItem.tsdocComment?.remarksBlock;
   1383      if (remarks) {
   1384        section.appendNode(
   1385          new DocParagraph({configuration}, [
   1386            new DocEmphasisSpan({configuration, bold: true}, [
   1387              new DocPlainText({configuration, text: 'Remarks: '}),
   1388            ]),
   1389          ]),
   1390        );
   1391 
   1392        section.appendNodes(remarks.content.getChildNodes());
   1393      }
   1394    }
   1395 
   1396    return new DocTableCell({configuration}, section.nodes);
   1397  }
   1398 
   1399  private _createDefaultCell(apiItem: ApiItem): DocTableCell {
   1400    const configuration = this._tsdocConfiguration;
   1401 
   1402    if (apiItem instanceof ApiDocumentedItem) {
   1403      const block = apiItem.tsdocComment?.customBlocks.find(block => {
   1404        return (
   1405          block.blockTag.tagNameWithUpperCase ===
   1406          StandardTags.defaultValue.tagNameWithUpperCase
   1407        );
   1408      });
   1409      if (block !== undefined) {
   1410        return new DocTableCell({configuration}, block.content.getChildNodes());
   1411      }
   1412    }
   1413 
   1414    return new DocTableCell({configuration}, []);
   1415  }
   1416 
   1417  private _createModifiersCell(apiItem: ApiItem): DocTableCell {
   1418    const configuration = this._tsdocConfiguration;
   1419 
   1420    const section = new DocSection({configuration});
   1421 
   1422    const codes = [];
   1423 
   1424    if (ApiProtectedMixin.isBaseClassOf(apiItem)) {
   1425      if (apiItem.isProtected) {
   1426        codes.push('protected');
   1427      }
   1428    }
   1429 
   1430    if (ApiReadonlyMixin.isBaseClassOf(apiItem)) {
   1431      if (apiItem.isReadonly) {
   1432        codes.push('readonly');
   1433      }
   1434    }
   1435 
   1436    if (ApiStaticMixin.isBaseClassOf(apiItem)) {
   1437      if (apiItem.isStatic) {
   1438        codes.push('static');
   1439      }
   1440    }
   1441 
   1442    if (ApiOptionalMixin.isBaseClassOf(apiItem)) {
   1443      if (apiItem.isOptional) {
   1444        codes.push('optional');
   1445      }
   1446    }
   1447 
   1448    if (apiItem instanceof ApiDocumentedItem) {
   1449      if (apiItem.tsdocComment?.deprecatedBlock) {
   1450        codes.push('deprecated');
   1451      }
   1452    }
   1453    if (codes.length) {
   1454      section.appendNode(
   1455        new DocParagraph({configuration}, [
   1456          new DocCodeSpan({configuration, code: codes.join(', ')}),
   1457        ]),
   1458      );
   1459    }
   1460 
   1461    return new DocTableCell({configuration}, section.nodes);
   1462  }
   1463 
   1464  private _createPropertyTypeCell(apiItem: ApiItem): DocTableCell {
   1465    const configuration = this._tsdocConfiguration;
   1466 
   1467    const section = new DocSection({configuration});
   1468 
   1469    if (apiItem instanceof ApiPropertyItem) {
   1470      section.appendNode(
   1471        this._createParagraphForTypeExcerpt(apiItem.propertyTypeExcerpt),
   1472      );
   1473    }
   1474 
   1475    return new DocTableCell({configuration}, section.nodes);
   1476  }
   1477 
   1478  private _createInitializerCell(apiItem: ApiItem): DocTableCell {
   1479    const configuration = this._tsdocConfiguration;
   1480 
   1481    const section = new DocSection({configuration});
   1482 
   1483    if (ApiInitializerMixin.isBaseClassOf(apiItem)) {
   1484      if (apiItem.initializerExcerpt) {
   1485        section.appendNodeInParagraph(
   1486          new DocCodeSpan({
   1487            configuration,
   1488            code: apiItem.initializerExcerpt.text,
   1489          }),
   1490        );
   1491      }
   1492    }
   1493 
   1494    return new DocTableCell({configuration}, section.nodes);
   1495  }
   1496 
   1497  private _writeBetaWarning(output: DocSection): void {
   1498    const configuration = this._tsdocConfiguration;
   1499    const betaWarning =
   1500      'This API is provided as a preview for developers and may change' +
   1501      ' based on feedback that we receive.  Do not use this API in a production environment.';
   1502    output.appendNode(
   1503      new DocNoteBox({configuration}, [
   1504        new DocParagraph({configuration}, [
   1505          new DocPlainText({configuration, text: betaWarning}),
   1506        ]),
   1507      ]),
   1508    );
   1509  }
   1510 
   1511  private _appendSection(output: DocSection, docSection: DocSection): void {
   1512    for (const node of docSection.nodes) {
   1513      output.appendNode(node);
   1514    }
   1515  }
   1516 
   1517  private _appendAndMergeSection(
   1518    output: DocSection,
   1519    docSection: DocSection,
   1520  ): void {
   1521    let firstNode = true;
   1522    for (const node of docSection.nodes) {
   1523      if (firstNode) {
   1524        if (node.kind === DocNodeKind.Paragraph) {
   1525          output.appendNodesInParagraph(node.getChildNodes());
   1526          firstNode = false;
   1527          continue;
   1528        }
   1529      }
   1530      firstNode = false;
   1531 
   1532      output.appendNode(node);
   1533    }
   1534  }
   1535 
   1536  private _getSidebarLabelForApiItem(apiItem: ApiItem): string {
   1537    if (apiItem.kind === ApiItemKind.Package) {
   1538      return 'API';
   1539    }
   1540 
   1541    let baseName = '';
   1542    for (const hierarchyItem of apiItem.getHierarchy()) {
   1543      // For overloaded methods, add a suffix such as "MyClass.myMethod_2".
   1544      let qualifiedName = hierarchyItem.displayName;
   1545      if (ApiParameterListMixin.isBaseClassOf(hierarchyItem)) {
   1546        if (hierarchyItem.overloadIndex > 1) {
   1547          // Subtract one for compatibility with earlier releases of API Documenter.
   1548          qualifiedName += `_${hierarchyItem.overloadIndex - 1}`;
   1549        }
   1550      }
   1551 
   1552      switch (hierarchyItem.kind) {
   1553        case ApiItemKind.Model:
   1554        case ApiItemKind.EntryPoint:
   1555        case ApiItemKind.EnumMember:
   1556        case ApiItemKind.Package:
   1557          break;
   1558        default:
   1559          baseName += qualifiedName + '.';
   1560      }
   1561    }
   1562    return baseName.slice(0, baseName.length - 1);
   1563  }
   1564 
   1565  private _getFilenameForApiItem(apiItem: ApiItem, link = false): string {
   1566    if (apiItem.kind === ApiItemKind.Package) {
   1567      return 'index.md';
   1568    }
   1569 
   1570    let baseName = '';
   1571    let suffix = '';
   1572    for (const hierarchyItem of apiItem.getHierarchy()) {
   1573      // For overloaded methods, add a suffix such as "MyClass.myMethod_2".
   1574      const qualifiedName = Utilities.getSafeFilenameForName(
   1575        hierarchyItem.displayName,
   1576      );
   1577 
   1578      switch (hierarchyItem.kind) {
   1579        case ApiItemKind.Model:
   1580        case ApiItemKind.EntryPoint:
   1581        case ApiItemKind.EnumMember:
   1582        // Properties don't have separate pages
   1583        case ApiItemKind.Property:
   1584        case ApiItemKind.PropertySignature:
   1585          break;
   1586        case ApiItemKind.Package:
   1587          baseName = Utilities.getSafeFilenameForName(
   1588            PackageName.getUnscopedName(hierarchyItem.displayName),
   1589          );
   1590          break;
   1591        default:
   1592          baseName += '.' + qualifiedName;
   1593      }
   1594 
   1595      if (link) {
   1596        switch (hierarchyItem.kind) {
   1597          case ApiItemKind.Property:
   1598          case ApiItemKind.PropertySignature:
   1599            suffix =
   1600              '#' +
   1601              Utilities.getSafeFilenameForName(
   1602                PackageName.getUnscopedName(hierarchyItem.displayName),
   1603              );
   1604            break;
   1605        }
   1606      }
   1607    }
   1608 
   1609    return `${baseName}.md${suffix}`;
   1610  }
   1611 
   1612  private _getLinkFilenameForApiItem(apiItem: ApiItem): string {
   1613    return './' + this._getFilenameForApiItem(apiItem, true);
   1614  }
   1615 
   1616  private _deleteOldOutputFiles(): void {
   1617    console.log('Deleting old output from ' + this._outputFolder);
   1618    FileSystem.ensureEmptyFolder(this._outputFolder);
   1619  }
   1620 }