Skip to content

Commit 83a904a

Browse files
committed
If the link text is omitted, automatically generate it based on the ApiItem name
1 parent e6e2b83 commit 83a904a

File tree

5 files changed

+121
-50
lines changed

5 files changed

+121
-50
lines changed

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

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4+
import * as colors from 'colors';
5+
46
import {
5-
DocNode
7+
DocNode, DocLinkTag, StringBuilder
68
} from '@microsoft/tsdoc';
79
import { CustomDocNodeKind } from '../nodes/CustomDocNodeKind';
810
import { DocHeading } from '../nodes/DocHeading';
@@ -11,9 +13,27 @@ import { DocTable } from '../nodes/DocTable';
1113
import { DocTableCell } from '../nodes/DocTableCell';
1214
import { DocEmphasisSpan } from '../nodes/DocEmphasisSpan';
1315
import { SimpleWriter } from './SimpleWriter';
14-
import { MarkdownEmitter, IMarkdownEmitterContext } from './MarkdownEmitter';
16+
import { MarkdownEmitter, IMarkdownEmitterContext, IMarkdownEmitterOptions } from './MarkdownEmitter';
17+
import { ApiModel, IResolveDeclarationReferenceResult, ApiItem } from '@microsoft/api-extractor';
18+
19+
export interface ICustomMarkdownEmitterOptions extends IMarkdownEmitterOptions {
20+
contextApiItem: ApiItem | undefined;
21+
22+
onGetFilenameForApiItem: (apiItem: ApiItem) => string | undefined;
23+
}
1524

1625
export class CustomMarkdownEmitter extends MarkdownEmitter {
26+
private _apiModel: ApiModel;
27+
28+
public constructor (apiModel: ApiModel) {
29+
super();
30+
31+
this._apiModel = apiModel;
32+
}
33+
34+
public emit(stringBuilder: StringBuilder, docNode: DocNode, options: ICustomMarkdownEmitterOptions): string {
35+
return super.emit(stringBuilder, docNode, options);
36+
}
1737

1838
/** @override */
1939
protected writeNode(docNode: DocNode, context: IMarkdownEmitterContext): void {
@@ -118,4 +138,38 @@ export class CustomMarkdownEmitter extends MarkdownEmitter {
118138
}
119139
}
120140

141+
/** @override */
142+
protected writeLinkTagWithCodeDestination(docLinkTag: DocLinkTag,
143+
context: IMarkdownEmitterContext<ICustomMarkdownEmitterOptions>): void {
144+
145+
const options: ICustomMarkdownEmitterOptions = context.options;
146+
147+
const result: IResolveDeclarationReferenceResult
148+
= this._apiModel.resolveDeclarationReference(docLinkTag.codeDestination!, options.contextApiItem);
149+
150+
if (result.resolvedApiItem) {
151+
const filename: string | undefined = options.onGetFilenameForApiItem(result.resolvedApiItem);
152+
153+
if (filename) {
154+
let linkText: string = docLinkTag.linkText || '';
155+
if (linkText.length === 0) {
156+
157+
// Generate a name such as Namespace1.Namespace2.MyClass.myMethod()
158+
linkText = result.resolvedApiItem.getScopedNameWithinPackage();
159+
}
160+
if (linkText.length > 0) {
161+
const encodedLinkText: string = this.getEscapedText(linkText.replace(/\s+/g, ' '));
162+
163+
context.writer.write('[');
164+
context.writer.write(encodedLinkText);
165+
context.writer.write(`](${filename!})`);
166+
} else {
167+
console.log(colors.red('WARNING: Unable to determine link text'));
168+
}
169+
}
170+
} else if (result.errorMessage) {
171+
console.log(colors.red('WARNING: Unable to resolve reference: ' + result.errorMessage));
172+
}
173+
}
174+
121175
}

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

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import * as path from 'path';
66
import {
77
PackageName,
88
FileSystem,
9-
NewlineKind,
10-
Colors
9+
NewlineKind
1110
} from '@microsoft/node-core-library';
1211
import {
1312
DocSection,
@@ -34,8 +33,7 @@ import {
3433
ApiStaticMixin,
3534
ApiResultTypeMixin,
3635
ApiPropertyItem,
37-
ApiFunctionLikeMixin,
38-
IResolveDeclarationReferenceResult
36+
ApiFunctionLikeMixin
3937
} from '@microsoft/api-extractor';
4038

4139
import { CustomDocNodes } from '../nodes/CustomDocNodeKind';
@@ -61,7 +59,7 @@ export class MarkdownDocumenter {
6159
public constructor(docItemSet: ApiModel) {
6260
this._apiModel = docItemSet;
6361
this._tsdocConfiguration = CustomDocNodes.configuration;
64-
this._markdownEmitter = new CustomMarkdownEmitter();
62+
this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel);
6563
}
6664

6765
public generateFiles(outputFolder: string): void {
@@ -169,23 +167,11 @@ export class MarkdownDocumenter {
169167
const stringBuilder: StringBuilder = new StringBuilder();
170168

171169
this._markdownEmitter.emit(stringBuilder, output, {
172-
onResolveTargetForCodeDestination: (docLinkTag: DocLinkTag) => {
173-
if (docLinkTag.codeDestination) {
174-
const result: IResolveDeclarationReferenceResult
175-
= this._apiModel.resolveDeclarationReference(docLinkTag.codeDestination, apiItem);
176-
177-
if (result.resolvedApiItem) {
178-
// NOTE: GitHub's markdown renderer does not resolve relative hyperlinks correctly
179-
// unless they start with "./" or "../".
180-
return './' + this._getFilenameForApiItem(result.resolvedApiItem);
181-
}
182-
183-
if (result.errorMessage) {
184-
console.log(Colors.red('WARNING: Unable to resolve reference: ' + result.errorMessage));
185-
}
186-
}
187-
188-
return undefined;
170+
contextApiItem: apiItem,
171+
onGetFilenameForApiItem: (apiItemForFilename: ApiItem) => {
172+
// NOTE: GitHub's markdown renderer does not resolve relative hyperlinks correctly
173+
// unless they start with "./" or "../".
174+
return './' + this._getFilenameForApiItem(apiItemForFilename);
189175
}
190176
});
191177

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

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,9 @@ import {
2020
import { SimpleWriter } from './SimpleWriter';
2121

2222
export interface IMarkdownEmitterOptions {
23-
/**
24-
* Given a DocLinkTag with a codeDestination property, determine the target link that should be emitted
25-
* in the "[link text](target URL)" Markdown notation. If the link cannot be resolved, undefined is returned.
26-
*/
27-
onResolveTargetForCodeDestination: (docLinkTag: DocLinkTag) => string | undefined;
2823
}
2924

30-
export interface IMarkdownEmitterContext {
25+
export interface IMarkdownEmitterContext<TOptions = IMarkdownEmitterOptions> {
3126
writer: SimpleWriter;
3227
insideTable: boolean;
3328

@@ -37,7 +32,7 @@ export interface IMarkdownEmitterContext {
3732
writingBold: boolean;
3833
writingItalic: boolean;
3934

40-
options: IMarkdownEmitterOptions;
35+
options: TOptions;
4136
}
4237

4338
/**
@@ -113,24 +108,13 @@ export class MarkdownEmitter {
113108
}
114109
case DocNodeKind.LinkTag: {
115110
const docLinkTag: DocLinkTag = docNode as DocLinkTag;
116-
if (docLinkTag.linkText !== undefined && docLinkTag.linkText.length > 0) {
117-
const encodedLinkText: string = this.getEscapedText(docLinkTag.linkText.replace(/\s+/g, ' '));
118-
let destination: string | undefined = undefined;
119-
if (docLinkTag.codeDestination) {
120-
destination = context.options.onResolveTargetForCodeDestination(docLinkTag);
121-
} else if (docLinkTag.urlDestination) {
122-
destination = docLinkTag.urlDestination;
123-
}
124-
125-
if (destination !== undefined) {
126-
writer.write('[');
127-
writer.write(encodedLinkText);
128-
writer.write(`](${destination})`);
129-
} else {
130-
writer.write(encodedLinkText);
131-
}
111+
if (docLinkTag.codeDestination) {
112+
this.writeLinkTagWithCodeDestination(docLinkTag, context);
113+
} else if (docLinkTag.urlDestination) {
114+
this.writeLinkTagWithUrlDestination(docLinkTag, context);
115+
} else if (docLinkTag.linkText) {
116+
this.writePlainText(docLinkTag.linkText, context);
132117
}
133-
134118
break;
135119
}
136120
case DocNodeKind.Paragraph: {
@@ -184,6 +168,25 @@ export class MarkdownEmitter {
184168
}
185169
}
186170

171+
/** @virtual */
172+
protected writeLinkTagWithCodeDestination(docLinkTag: DocLinkTag, context: IMarkdownEmitterContext): void {
173+
174+
// The subclass needs to implement this to support code destinations
175+
throw new Error('writeLinkTagWithCodeDestination()');
176+
}
177+
178+
/** @virtual */
179+
protected writeLinkTagWithUrlDestination(docLinkTag: DocLinkTag, context: IMarkdownEmitterContext): void {
180+
const linkText: string = docLinkTag.linkText !== undefined ? docLinkTag.linkText
181+
: docLinkTag.urlDestination!;
182+
183+
const encodedLinkText: string = this.getEscapedText(linkText.replace(/\s+/g, ' '));
184+
185+
context.writer.write('[');
186+
context.writer.write(encodedLinkText);
187+
context.writer.write(`](${docLinkTag.urlDestination!})`);
188+
}
189+
187190
protected writePlainText(text: string, context: IMarkdownEmitterContext): void {
188191
const writer: SimpleWriter = context.writer;
189192

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { DocTable } from '../../nodes/DocTable';
2222
import { DocTableRow } from '../../nodes/DocTableRow';
2323
import { DocTableCell } from '../../nodes/DocTableCell';
2424
import { CustomMarkdownEmitter } from '../CustomMarkdownEmitter';
25+
import { ApiModel, ApiItem } from '@microsoft/api-extractor';
2526

2627
test('render Markdown from TSDoc', done => {
2728
const outputFolder: string = FileDiffTest.prepareFolder(__dirname, 'MarkdownPageRenderer');
@@ -197,9 +198,13 @@ test('render Markdown from TSDoc', done => {
197198

198199
const outputFilename: string = path.join(outputFolder, 'ActualOutput.md');
199200
const stringBuilder: StringBuilder = new StringBuilder();
200-
const markdownEmitter: CustomMarkdownEmitter = new CustomMarkdownEmitter();
201+
const apiModel: ApiModel = new ApiModel();
202+
const markdownEmitter: CustomMarkdownEmitter = new CustomMarkdownEmitter(apiModel);
201203
markdownEmitter.emit(stringBuilder, output, {
202-
onResolveTargetForCodeDestination: (docLinkTag: DocLinkTag) => '#'
204+
contextApiItem: undefined,
205+
onGetFilenameForApiItem: (apiItem: ApiItem) => {
206+
return '#';
207+
}
203208
});
204209
FileSystem.writeFile(outputFilename, stringBuilder.toString());
205210

apps/api-extractor/src/api/model/ApiItem.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,28 @@ export class ApiItem {
110110
return hierarchy;
111111
}
112112

113+
/**
114+
* This returns a scoped name such as `"Namespace1.Namespace2.MyClass.myMember()"`. It does not include the
115+
* package name or entry point.
116+
*/
117+
public getScopedNameWithinPackage(): string {
118+
const reversedParts: string[] = [];
119+
120+
for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) {
121+
if (current.kind === ApiItemKind.EntryPoint) {
122+
break;
123+
}
124+
if (reversedParts.length !== 0) {
125+
reversedParts.push('.');
126+
} else if (ApiFunctionLikeMixin.isBaseClassOf(current)) { // tslint:disable-line:no-use-before-declare
127+
reversedParts.push('()');
128+
}
129+
reversedParts.push(current.name);
130+
}
131+
132+
return reversedParts.reverse().join('');
133+
}
134+
113135
/**
114136
* If this item is an ApiPackage or has an ApiPackage as one of its parents, then that object is returned.
115137
* Otherwise undefined is returned.
@@ -135,3 +157,4 @@ export interface IApiItemConstructor extends Constructor<ApiItem>, PropertiesOf<
135157
// Circular import
136158
import { Deserializer } from './Deserializer';
137159
import { ApiPackage } from './ApiPackage';
160+
import { ApiFunctionLikeMixin } from '../mixins/ApiFunctionLikeMixin';

0 commit comments

Comments
 (0)