UNPKG

voluptasmollitia

Version:
321 lines (286 loc) 9.3 kB
/** * @license * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. import { DocNode, DocNodeKind, StringBuilder, DocPlainText, DocHtmlStartTag, DocHtmlEndTag, DocCodeSpan, DocLinkTag, DocParagraph, DocFencedCode, DocSection, DocNodeTransforms, DocEscapedText, DocErrorText, DocBlockTag } from '@microsoft/tsdoc'; import { InternalError } from '@rushstack/node-core-library'; import { IndentedWriter } from '../utils/IndentedWriter'; export interface IMarkdownEmitterOptions {} export interface IMarkdownEmitterContext<TOptions = IMarkdownEmitterOptions> { writer: IndentedWriter; insideTable: boolean; boldRequested: boolean; italicRequested: boolean; writingBold: boolean; writingItalic: boolean; options: TOptions; } /** * Renders MarkupElement content in the Markdown file format. * For more info: https://en.wikipedia.org/wiki/Markdown */ export class MarkdownEmitter { public emit( stringBuilder: StringBuilder, docNode: DocNode, options: IMarkdownEmitterOptions ): string { const writer: IndentedWriter = new IndentedWriter(stringBuilder); const context: IMarkdownEmitterContext = { writer, insideTable: false, boldRequested: false, italicRequested: false, writingBold: false, writingItalic: false, options }; this.writeNode(docNode, context, false); writer.ensureNewLine(); // finish the last line return writer.toString(); } protected getEscapedText(text: string): string { const textWithBackslashes: string = text .replace(/\\/g, '\\\\') // first replace the escape character .replace(/[*#[\]_|`~]/g, x => '\\' + x) // then escape any special characters .replace(/---/g, '\\-\\-\\-') // hyphens only if it's 3 or more .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;'); return textWithBackslashes; } protected getTableEscapedText(text: string): string { return text .replace(/&/g, '&amp;') .replace(/"/g, '&quot;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/\|/g, '&#124;'); } /** * @virtual */ protected writeNode( docNode: DocNode, context: IMarkdownEmitterContext, docNodeSiblings: boolean ): void { const writer: IndentedWriter = context.writer; switch (docNode.kind) { case DocNodeKind.PlainText: { const docPlainText: DocPlainText = docNode as DocPlainText; this.writePlainText(docPlainText.text, context); break; } case DocNodeKind.HtmlStartTag: case DocNodeKind.HtmlEndTag: { const docHtmlTag: DocHtmlStartTag | DocHtmlEndTag = docNode as | DocHtmlStartTag | DocHtmlEndTag; // write the HTML element verbatim into the output writer.write(docHtmlTag.emitAsHtml()); break; } case DocNodeKind.CodeSpan: { const docCodeSpan: DocCodeSpan = docNode as DocCodeSpan; if (context.insideTable) { writer.write('<code>'); } else { writer.write('`'); } if (context.insideTable) { const code: string = this.getTableEscapedText(docCodeSpan.code); const parts: string[] = code.split(/\r?\n/g); writer.write(parts.join('</code><br/><code>')); } else { writer.write(docCodeSpan.code); } if (context.insideTable) { writer.write('</code>'); } else { writer.write('`'); } break; } case DocNodeKind.LinkTag: { const docLinkTag: DocLinkTag = docNode as DocLinkTag; if (docLinkTag.codeDestination) { this.writeLinkTagWithCodeDestination(docLinkTag, context); } else if (docLinkTag.urlDestination) { this.writeLinkTagWithUrlDestination(docLinkTag, context); } else if (docLinkTag.linkText) { this.writePlainText(docLinkTag.linkText, context); } break; } case DocNodeKind.Paragraph: { const docParagraph: DocParagraph = docNode as DocParagraph; const trimmedParagraph: DocParagraph = DocNodeTransforms.trimSpacesInParagraph( docParagraph ); if (context.insideTable) { if (docNodeSiblings) { writer.write('<p>'); this.writeNodes(trimmedParagraph.nodes, context); writer.write('</p>'); } else { // Special case: If we are the only element inside this table cell, then we can omit the <p></p> container. this.writeNodes(trimmedParagraph.nodes, context); } } else { this.writeNodes(trimmedParagraph.nodes, context); writer.ensureNewLine(); writer.writeLine(); } break; } case DocNodeKind.FencedCode: { const docFencedCode: DocFencedCode = docNode as DocFencedCode; writer.ensureNewLine(); writer.write('```'); writer.write(docFencedCode.language); writer.writeLine(); writer.write(docFencedCode.code); writer.writeLine(); writer.writeLine('```'); break; } case DocNodeKind.Section: { const docSection: DocSection = docNode as DocSection; this.writeNodes(docSection.nodes, context); break; } case DocNodeKind.SoftBreak: { if (!/^\s?$/.test(writer.peekLastCharacter())) { writer.write(' '); } break; } case DocNodeKind.EscapedText: { const docEscapedText: DocEscapedText = docNode as DocEscapedText; this.writePlainText(docEscapedText.decodedText, context); break; } case DocNodeKind.ErrorText: { const docErrorText: DocErrorText = docNode as DocErrorText; this.writePlainText(docErrorText.text, context); break; } case DocNodeKind.InlineTag: { break; } case DocNodeKind.BlockTag: { const tagNode: DocBlockTag = docNode as DocBlockTag; console.warn('Unsupported block tag: ' + tagNode.tagName); break; } default: throw new InternalError( 'Unsupported DocNodeKind kind: ' + docNode.kind ); } } /** @virtual */ protected writeLinkTagWithCodeDestination( docLinkTag: DocLinkTag, context: IMarkdownEmitterContext ): void { // The subclass needs to implement this to support code destinations throw new InternalError('writeLinkTagWithCodeDestination()'); } /** @virtual */ protected writeLinkTagWithUrlDestination( docLinkTag: DocLinkTag, context: IMarkdownEmitterContext ): void { const linkText: string = docLinkTag.linkText !== undefined ? docLinkTag.linkText : docLinkTag.urlDestination!; const encodedLinkText: string = this.getEscapedText( linkText.replace(/\s+/g, ' ') ); context.writer.write('['); context.writer.write(encodedLinkText); context.writer.write(`](${docLinkTag.urlDestination!})`); } protected writePlainText( text: string, context: IMarkdownEmitterContext ): void { const writer: IndentedWriter = context.writer; // split out the [ leading whitespace, content, trailing whitespace ] const parts: string[] = text.match(/^(\s*)(.*?)(\s*)$/) || []; writer.write(parts[1]); // write leading whitespace const middle: string = parts[2]; if (middle !== '') { switch (writer.peekLastCharacter()) { case '': case '\n': case ' ': case '[': case '>': // okay to put a symbol break; default: // This is no problem: "**one** *two* **three**" // But this is trouble: "**one***two***three**" // The most general solution: "**one**<!-- -->*two*<!-- -->**three**" writer.write('<!-- -->'); break; } if (context.boldRequested) { writer.write('<b>'); } if (context.italicRequested) { writer.write('<i>'); } writer.write(this.getEscapedText(middle)); if (context.italicRequested) { writer.write('</i>'); } if (context.boldRequested) { writer.write('</b>'); } } writer.write(parts[3]); // write trailing whitespace } protected writeNodes( docNodes: ReadonlyArray<DocNode>, context: IMarkdownEmitterContext ): void { for (const docNode of docNodes) { this.writeNode(docNode, context, docNodes.length > 1); } } }