voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
321 lines (286 loc) • 9.3 kB
text/typescript
/**
* @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, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
return textWithBackslashes;
}
protected getTableEscapedText(text: string): string {
return text
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\|/g, '|');
}
/**
* @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);
}
}
}