Skip to content

Commit 60bd769

Browse files
committed
Convert MarkdownEmitter into a non-static class, and separate SimpleWriter into its own source file
1 parent 9d0ebb4 commit 60bd769

File tree

4 files changed

+104
-96
lines changed

4 files changed

+104
-96
lines changed

apps/api-documenter/src/markdown/MarkdownDocumenter.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
IResolveDeclarationReferenceResult
3939
} from '@microsoft/api-extractor';
4040

41-
import { MarkdownEmitter } from '../utils/MarkdownEmitter';
41+
import { MarkdownEmitter } from '../markdown/MarkdownEmitter';
4242
import { CustomDocNodes } from '../nodes/CustomDocNodeKind';
4343
import { DocHeading } from '../nodes/DocHeading';
4444
import { DocTable } from '../nodes/DocTable';
@@ -53,13 +53,15 @@ import { Utilities } from '../utils/Utilities';
5353
* For more info: https://en.wikipedia.org/wiki/Markdown
5454
*/
5555
export class MarkdownDocumenter {
56-
private _apiModel: ApiModel;
56+
private readonly _apiModel: ApiModel;
57+
private readonly _tsdocConfiguration: TSDocConfiguration;
58+
private readonly _markdownEmitter: MarkdownEmitter;
5759
private _outputFolder: string;
58-
private _tsdocConfiguration: TSDocConfiguration;
5960

6061
public constructor(docItemSet: ApiModel) {
6162
this._apiModel = docItemSet;
6263
this._tsdocConfiguration = CustomDocNodes.configuration;
64+
this._markdownEmitter = new MarkdownEmitter();
6365
}
6466

6567
public generateFiles(outputFolder: string): void {
@@ -166,7 +168,7 @@ export class MarkdownDocumenter {
166168
const filename: string = path.join(this._outputFolder, this._getFilenameForApiItem(apiItem));
167169
const stringBuilder: StringBuilder = new StringBuilder();
168170

169-
MarkdownEmitter.renderNode(stringBuilder, output, {
171+
this._markdownEmitter.emit(stringBuilder, output, {
170172
onResolveTargetForCodeDestination: (docLinkTag: DocLinkTag) => {
171173
if (docLinkTag.codeDestination) {
172174
const result: IResolveDeclarationReferenceResult

apps/api-documenter/src/markdown/MarkdownEmitter.ts

Lines changed: 24 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -23,74 +23,7 @@ import { DocNoteBox } from '../nodes/DocNoteBox';
2323
import { DocTable } from '../nodes/DocTable';
2424
import { DocTableCell } from '../nodes/DocTableCell';
2525
import { DocEmphasisSpan } from '../nodes/DocEmphasisSpan';
26-
27-
/**
28-
* Helper class used by MarkdownPageRenderer
29-
*/
30-
class SimpleWriter {
31-
private _builder: StringBuilder;
32-
private _latestChunk: string | undefined = undefined;
33-
private _previousChunk: string | undefined = undefined;
34-
35-
public constructor(builder: StringBuilder) {
36-
this._builder = builder;
37-
}
38-
39-
public write(s: string): void {
40-
if (s.length > 0) {
41-
this._previousChunk = this._latestChunk;
42-
this._latestChunk = s;
43-
this._builder.append(s);
44-
}
45-
}
46-
47-
public writeLine(s: string = ''): void {
48-
this.write(s);
49-
this.write('\n');
50-
}
51-
52-
/**
53-
* Adds a newline if the file pointer is not already at the start of the line
54-
*/
55-
public ensureNewLine(): void {
56-
if (this.peekLastCharacter() !== '\n') {
57-
this.write('\n');
58-
}
59-
}
60-
61-
/**
62-
* Adds up to two newlines to ensure that there is a blank line above the current line.
63-
*/
64-
public ensureSkippedLine(): void {
65-
this.ensureNewLine();
66-
if (this.peekSecondLastCharacter() !== '\n') {
67-
this.write('\n');
68-
}
69-
}
70-
71-
public peekLastCharacter(): string {
72-
if (this._latestChunk !== undefined) {
73-
return this._latestChunk.substr(-1, 1);
74-
}
75-
return '';
76-
}
77-
78-
public peekSecondLastCharacter(): string {
79-
if (this._latestChunk !== undefined) {
80-
if (this._latestChunk.length > 1) {
81-
return this._latestChunk.substr(-2, 1);
82-
}
83-
if (this._previousChunk !== undefined) {
84-
return this._previousChunk.substr(-1, 1);
85-
}
86-
}
87-
return '';
88-
}
89-
90-
public toString(): string {
91-
return this._builder.toString();
92-
}
93-
}
26+
import { SimpleWriter } from './SimpleWriter';
9427

9528
export interface IMarkdownEmitterOptions {
9629
/**
@@ -100,7 +33,7 @@ export interface IMarkdownEmitterOptions {
10033
onResolveTargetForCodeDestination: (docLinkTag: DocLinkTag) => string | undefined;
10134
}
10235

103-
interface IRenderContext {
36+
interface IMarkdownEmitterContext {
10437
writer: SimpleWriter;
10538
insideTable: boolean;
10639

@@ -119,11 +52,11 @@ interface IRenderContext {
11952
*/
12053
export class MarkdownEmitter {
12154

122-
public static renderNode(stringBuilder: StringBuilder, docNode: DocNode, options: IMarkdownEmitterOptions): string {
55+
public emit(stringBuilder: StringBuilder, docNode: DocNode, options: IMarkdownEmitterOptions): string {
12356
const writer: SimpleWriter = new SimpleWriter(stringBuilder);
12457

125-
const context: IRenderContext = {
126-
writer: writer,
58+
const context: IMarkdownEmitterContext = {
59+
writer,
12760
insideTable: false,
12861

12962
boldRequested: false,
@@ -135,14 +68,14 @@ export class MarkdownEmitter {
13568
options
13669
};
13770

138-
MarkdownEmitter._writeNode(docNode, context);
71+
this._writeNode(docNode, context);
13972

14073
writer.ensureNewLine(); // finish the last line
14174

14275
return writer.toString();
14376
}
14477

145-
private static _getEscapedText(text: string): string {
78+
private _getEscapedText(text: string): string {
14679
const textWithBackslashes: string = text
14780
.replace(/\\/g, '\\\\') // first replace the escape character
14881
.replace(/[*#[\]_|`~]/g, (x) => '\\' + x) // then escape any special characters
@@ -153,13 +86,13 @@ export class MarkdownEmitter {
15386
return textWithBackslashes;
15487
}
15588

156-
private static _writeNode(docNode: DocNode, context: IRenderContext): void {
89+
private _writeNode(docNode: DocNode, context: IMarkdownEmitterContext): void {
15790
const writer: SimpleWriter = context.writer;
15891

15992
switch (docNode.kind) {
16093
case DocNodeKind.PlainText: {
16194
const docPlainText: DocPlainText = docNode as DocPlainText;
162-
MarkdownEmitter._writePlainText(docPlainText.text, context);
95+
this._writePlainText(docPlainText.text, context);
16396
break;
16497
}
16598
case DocNodeKind.HtmlStartTag:
@@ -184,7 +117,7 @@ export class MarkdownEmitter {
184117
case DocNodeKind.LinkTag: {
185118
const docLinkTag: DocLinkTag = docNode as DocLinkTag;
186119
if (docLinkTag.linkText !== undefined && docLinkTag.linkText.length > 0) {
187-
const encodedLinkText: string = MarkdownEmitter._getEscapedText(docLinkTag.linkText.replace(/\s+/g, ' '));
120+
const encodedLinkText: string = this._getEscapedText(docLinkTag.linkText.replace(/\s+/g, ' '));
188121
let destination: string | undefined = undefined;
189122
if (docLinkTag.codeDestination) {
190123
destination = context.options.onResolveTargetForCodeDestination(docLinkTag);
@@ -208,10 +141,10 @@ export class MarkdownEmitter {
208141
const trimmedParagraph: DocParagraph = DocNodeTransforms.trimSpacesInParagraph(docParagraph);
209142
if (context.insideTable) {
210143
writer.write('<p>');
211-
MarkdownEmitter._writeNodes(trimmedParagraph.nodes, context);
144+
this._writeNodes(trimmedParagraph.nodes, context);
212145
writer.write('</p>');
213146
} else {
214-
MarkdownEmitter._writeNodes(trimmedParagraph.nodes, context);
147+
this._writeNodes(trimmedParagraph.nodes, context);
215148
writer.ensureNewLine();
216149
writer.writeLine();
217150
}
@@ -230,7 +163,7 @@ export class MarkdownEmitter {
230163
prefix = '####';
231164
}
232165

233-
writer.writeLine(prefix + ' ' + MarkdownEmitter._getEscapedText(docHeading.title));
166+
writer.writeLine(prefix + ' ' + this._getEscapedText(docHeading.title));
234167
writer.writeLine();
235168
break;
236169
}
@@ -250,7 +183,7 @@ export class MarkdownEmitter {
250183
writer.ensureNewLine();
251184
writer.write('> ');
252185
// TODO: Handle newlines
253-
MarkdownEmitter._writeNode(docNoteBox.content, context);
186+
this._writeNode(docNoteBox.content, context);
254187
writer.ensureNewLine();
255188
writer.writeLine();
256189
break;
@@ -281,7 +214,7 @@ export class MarkdownEmitter {
281214
if (docTable.header) {
282215
const cell: DocTableCell | undefined = docTable.header.cells[i];
283216
if (cell) {
284-
MarkdownEmitter._writeNode(cell.content, context);
217+
this._writeNode(cell.content, context);
285218
}
286219
}
287220
writer.write(' |');
@@ -299,7 +232,7 @@ export class MarkdownEmitter {
299232
writer.write('| ');
300233
for (const cell of row.cells) {
301234
writer.write(' ');
302-
MarkdownEmitter._writeNode(cell.content, context);
235+
this._writeNode(cell.content, context);
303236
writer.write(' |');
304237
}
305238
writer.writeLine();
@@ -312,7 +245,7 @@ export class MarkdownEmitter {
312245
}
313246
case DocNodeKind.Section: {
314247
const docSection: DocSection = docNode as DocSection;
315-
MarkdownEmitter._writeNodes(docSection.nodes, context);
248+
this._writeNodes(docSection.nodes, context);
316249
break;
317250
}
318251
case CustomDocNodeKind.EmphasisSpan: {
@@ -321,7 +254,7 @@ export class MarkdownEmitter {
321254
const oldItalic: boolean = context.italicRequested;
322255
context.boldRequested = docEmphasisSpan.bold;
323256
context.italicRequested = docEmphasisSpan.italic;
324-
MarkdownEmitter._writeNodes(docEmphasisSpan.nodes, context);
257+
this._writeNodes(docEmphasisSpan.nodes, context);
325258
context.boldRequested = oldBold;
326259
context.italicRequested = oldItalic;
327260
break;
@@ -334,20 +267,20 @@ export class MarkdownEmitter {
334267
}
335268
case DocNodeKind.EscapedText: {
336269
const docEscapedText: DocEscapedText = docNode as DocEscapedText;
337-
MarkdownEmitter._writePlainText(docEscapedText.decodedText, context);
270+
this._writePlainText(docEscapedText.decodedText, context);
338271
break;
339272
}
340273
case DocNodeKind.ErrorText: {
341274
const docErrorText: DocErrorText = docNode as DocErrorText;
342-
MarkdownEmitter._writePlainText(docErrorText.text, context);
275+
this._writePlainText(docErrorText.text, context);
343276
break;
344277
}
345278
default:
346279
throw new Error('Unsupported element kind: ' + docNode.kind);
347280
}
348281
}
349282

350-
private static _writePlainText(text: string, context: IRenderContext): void {
283+
private _writePlainText(text: string, context: IMarkdownEmitterContext): void {
351284
const writer: SimpleWriter = context.writer;
352285

353286
// split out the [ leading whitespace, content, trailing whitespace ]
@@ -381,7 +314,7 @@ export class MarkdownEmitter {
381314
writer.write('<i>');
382315
}
383316

384-
writer.write(MarkdownEmitter._getEscapedText(middle));
317+
writer.write(this._getEscapedText(middle));
385318

386319
if (context.italicRequested) {
387320
writer.write('</i>');
@@ -394,9 +327,9 @@ export class MarkdownEmitter {
394327
writer.write(parts[3]); // write trailing whitespace
395328
}
396329

397-
private static _writeNodes(docNodes: ReadonlyArray<DocNode>, context: IRenderContext): void {
330+
private _writeNodes(docNodes: ReadonlyArray<DocNode>, context: IMarkdownEmitterContext): void {
398331
for (const docNode of docNodes) {
399-
MarkdownEmitter._writeNode(docNode, context);
332+
this._writeNode(docNode, context);
400333
}
401334
}
402335

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import { StringBuilder } from '@microsoft/tsdoc';
5+
6+
/**
7+
* Helper class used by MarkdownEmitter
8+
*/
9+
export class SimpleWriter {
10+
private _builder: StringBuilder;
11+
private _latestChunk: string | undefined = undefined;
12+
private _previousChunk: string | undefined = undefined;
13+
14+
public constructor(builder: StringBuilder) {
15+
this._builder = builder;
16+
}
17+
18+
public write(s: string): void {
19+
if (s.length > 0) {
20+
this._previousChunk = this._latestChunk;
21+
this._latestChunk = s;
22+
this._builder.append(s);
23+
}
24+
}
25+
26+
public writeLine(s: string = ''): void {
27+
this.write(s);
28+
this.write('\n');
29+
}
30+
31+
/**
32+
* Adds a newline if the file pointer is not already at the start of the line
33+
*/
34+
public ensureNewLine(): void {
35+
if (this.peekLastCharacter() !== '\n') {
36+
this.write('\n');
37+
}
38+
}
39+
40+
/**
41+
* Adds up to two newlines to ensure that there is a blank line above the current line.
42+
*/
43+
public ensureSkippedLine(): void {
44+
this.ensureNewLine();
45+
if (this.peekSecondLastCharacter() !== '\n') {
46+
this.write('\n');
47+
}
48+
}
49+
50+
public peekLastCharacter(): string {
51+
if (this._latestChunk !== undefined) {
52+
return this._latestChunk.substr(-1, 1);
53+
}
54+
return '';
55+
}
56+
57+
public peekSecondLastCharacter(): string {
58+
if (this._latestChunk !== undefined) {
59+
if (this._latestChunk.length > 1) {
60+
return this._latestChunk.substr(-2, 1);
61+
}
62+
if (this._previousChunk !== undefined) {
63+
return this._previousChunk.substr(-1, 1);
64+
}
65+
}
66+
return '';
67+
}
68+
69+
public toString(): string {
70+
return this._builder.toString();
71+
}
72+
}

apps/api-documenter/src/markdown/test/CustomMarkdownEmitter.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ test('render Markdown from TSDoc', done => {
197197

198198
const outputFilename: string = path.join(outputFolder, 'ActualOutput.md');
199199
const stringBuilder: StringBuilder = new StringBuilder();
200-
MarkdownEmitter.renderNode(stringBuilder, output, {
200+
const markdownEmitter: MarkdownEmitter = new MarkdownEmitter();
201+
markdownEmitter.emit(stringBuilder, output, {
201202
onResolveTargetForCodeDestination: (docLinkTag: DocLinkTag) => '#'
202203
});
203204
FileSystem.writeFile(outputFilename, stringBuilder.toString());

0 commit comments

Comments
 (0)