/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ApiItem,
ApiModel,
IResolveDeclarationReferenceResult
} from '@microsoft/api-extractor-model';
import { DocLinkTag, DocNode } from '@microsoft/tsdoc';
import { CustomDocNodeKind } from '../nodes/customDocNodeKind';
import { DocAnchor } from '../nodes/docAnchor';
import { DocEmphasisSpan } from '../nodes/docEmphasisSpan';
import { DocHeading } from '../nodes/docHeading';
import { DocList } from '../nodes/docList';
import { DocNoteBox } from '../nodes/docNoteBox';
import { DocTable } from '../nodes/docTable';
import { DocTableCell } from '../nodes/docTableCell';
import { IndentedWriter } from '../utils/indentedWriter';
import {
MarkdownEmitter,
MarkdownEmitterContext,
MarkdownEmitterOptions
} from './markdownEmitter';
export interface CustomMarkdownEmitterOptions extends MarkdownEmitterOptions {
contextApiItem: ApiItem | undefined;
onGetFilenameForApiItem(apiItem: ApiItem): string | undefined;
}
export class CustomMarkdownEmitter extends MarkdownEmitter {
private _apiModel: ApiModel;
constructor(apiModel: ApiModel) {
super();
this._apiModel = apiModel;
}
/** @override */
protected writeNode(
docNode: DocNode,
context: MarkdownEmitterContext,
hasNextSibling: boolean
): void {
const writer: IndentedWriter = context.writer;
switch (docNode.kind) {
case CustomDocNodeKind.Anchor: {
const docAnchor: DocAnchor = docNode as DocAnchor;
writer.ensureSkippedLine();
writer.writeLine(``);
writer.writeLine();
break;
}
case CustomDocNodeKind.Heading: {
const docHeading: DocHeading = docNode as DocHeading;
writer.ensureSkippedLine();
let prefix: string;
switch (docHeading.level) {
case 1:
prefix = '#';
break;
case 2:
prefix = '##';
break;
case 3:
prefix = '###';
break;
default:
prefix = '####';
}
writer.writeLine(`${prefix} ${this.getEscapedText(docHeading.title)}`);
writer.writeLine();
break;
}
case CustomDocNodeKind.List: {
const docList = docNode as DocList;
context.listLevel += 1;
if (context.listLevel === 1) {
writer.ensureSkippedLine();
}
for (const node of docList.nodes) {
if (node.kind === CustomDocNodeKind.List) {
this.writeNode(node, context, false);
} else {
writer.ensureNewLine();
writer.write(' '.repeat((context.listLevel - 1) * 2) + '- ');
this.writeNode(node, context, false);
}
}
context.listLevel -= 1;
if (context.listLevel === 1) {
writer.ensureSkippedLine();
}
break;
}
case CustomDocNodeKind.NoteBox: {
const docNoteBox: DocNoteBox = docNode as DocNoteBox;
writer.ensureNewLine();
writer.increaseIndent('> ');
this.writeNode(docNoteBox.content, context, false);
writer.ensureNewLine();
writer.decreaseIndent();
writer.writeLine();
break;
}
case CustomDocNodeKind.Table: {
const docTable: DocTable = docNode as DocTable;
// GitHub's markdown renderer chokes on tables that don't have a blank line above them,
// whereas VS Code's renderer is totally fine with it.
writer.ensureSkippedLine();
context.insideTable = true;
// Markdown table rows can have inconsistent cell counts. Size the table based on the longest row.
let columnCount: number = 0;
if (docTable.header) {
columnCount = docTable.header.cells.length;
}
for (const row of docTable.rows) {
if (row.cells.length > columnCount) {
columnCount = row.cells.length;
}
}
// write the table header (which is required by Markdown)
writer.write('| ');
for (let i: number = 0; i < columnCount; i += 1) {
writer.write(' ');
if (docTable.header) {
const cell: DocTableCell | undefined = docTable.header.cells[i];
if (cell) {
this.writeNode(cell.content, context, false);
}
}
writer.write(' |');
}
writer.writeLine();
// write the divider
writer.write('| ');
for (let i: number = 0; i < columnCount; i += 1) {
writer.write(' --- |');
}
writer.writeLine();
for (const row of docTable.rows) {
writer.write('| ');
for (const cell of row.cells) {
writer.write(' ');
this.writeNode(cell.content, context, false);
writer.write(' |');
}
writer.writeLine();
}
writer.writeLine();
context.insideTable = false;
break;
}
case CustomDocNodeKind.EmphasisSpan: {
const docEmphasisSpan: DocEmphasisSpan = docNode as DocEmphasisSpan;
if (docEmphasisSpan.bold) {
writer.write('');
}
if (docEmphasisSpan.italic) {
writer.write('');
}
this.writeNodes(docEmphasisSpan.nodes, context);
if (docEmphasisSpan.italic) {
writer.write('');
}
if (docEmphasisSpan.bold) {
writer.write('');
}
break;
}
default:
super.writeNode(docNode, context, hasNextSibling);
}
}
/** @override */
protected writeLinkTagWithCodeDestination(
docLinkTag: DocLinkTag,
context: MarkdownEmitterContext
): void {
const options: CustomMarkdownEmitterOptions = context.options;
const result: IResolveDeclarationReferenceResult = this._apiModel.resolveDeclarationReference(
docLinkTag.codeDestination!,
options.contextApiItem
);
if (result.resolvedApiItem) {
const filename: string | undefined = options.onGetFilenameForApiItem(
result.resolvedApiItem
);
if (filename) {
let linkText: string = docLinkTag.linkText || '';
if (linkText.length === 0) {
// Generate a name such as Namespace1.Namespace2.MyClass.myMethod()
linkText = result.resolvedApiItem.getScopedNameWithinPackage();
}
if (linkText.length > 0) {
const encodedLinkText: string = this.getEscapedText(
linkText.replace(/\s+/g, ' ')
);
context.writer.write('[');
context.writer.write(encodedLinkText);
context.writer.write(`](${filename})`);
} else {
console.log('WARNING: Unable to determine link text');
}
}
} else if (result.errorMessage) {
console.log(
'WARNING: Unable to resolve reference: ' + result.errorMessage
);
}
}
}