Skip to content
Closed
72 changes: 62 additions & 10 deletions apps/api-documenter/src/documenters/MarkdownDocumenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DocLinkTag,
TSDocConfiguration,
StringBuilder,
DocNode,
DocNodeKind,
DocParagraph,
DocCodeSpan,
Expand Down Expand Up @@ -169,6 +170,28 @@ export class MarkdownDocumenter {
output.appendNode(
new DocFencedCode({ configuration, code: apiItem.getExcerptWithModifiers(), language: 'typescript' })
);

/**
* Previously for each declaration item, we generate a code section like:
*
* ```typescript
* export class SubClass extends SuperClass implements IInterface
* ```
*
* But there is no place to inject reference links.
* Instead following code generates pure texts with links like:
*
* export class [SubClass](./subclass.md) extends [SuperClass](./superclass.md)
* implements [IInterface](./iinterface.md)
*
* But the display format looks bad.
* Uncomment the code to show reference links when finding a better display format.
*/
// show api references if there're.
// const excerptContentWithLinks: DocNode[] = this._createExcerptContent(apiItem.excerpt, apiItem);
// if (excerptContentWithLinks.some(x => x instanceof DocLinkTag)) {
// output.appendNodesInParagraph(excerptContentWithLinks);
// }
}
}

Expand Down Expand Up @@ -465,9 +488,10 @@ export class MarkdownDocumenter {
]),

new DocTableCell({ configuration }, [
new DocParagraph({ configuration }, [
new DocCodeSpan({ configuration, code: apiEnumMember.initializerExcerpt.text })
])
new DocParagraph(
{ configuration },
this._createExcerptContent(apiEnumMember.initializerExcerpt, apiEnumMember)
)
]),

this._createDescriptionCell(apiEnumMember)
Expand Down Expand Up @@ -580,9 +604,10 @@ export class MarkdownDocumenter {
])
]),
new DocTableCell({configuration}, [
new DocParagraph({ configuration }, [
new DocCodeSpan({ configuration, code: apiParameter.parameterTypeExcerpt.text })
])
new DocParagraph(
{ configuration },
this._createExcerptContent(apiParameter.parameterTypeExcerpt, apiParameterListMixin)
)
]),
new DocTableCell({configuration}, parameterDescription.nodes)
])
Expand All @@ -605,9 +630,10 @@ export class MarkdownDocumenter {
);

output.appendNode(
new DocParagraph({ configuration }, [
new DocCodeSpan({ configuration, code: returnTypeExcerpt.text.trim() || '(not declared)' })
])
new DocParagraph(
{ configuration },
this._createExcerptContent(returnTypeExcerpt, apiParameterListMixin, '(not declared)')
)
);

if (apiParameterListMixin instanceof ApiDocumentedItem) {
Expand Down Expand Up @@ -685,12 +711,38 @@ export class MarkdownDocumenter {
const section: DocSection = new DocSection({ configuration });

if (apiItem instanceof ApiPropertyItem) {
section.appendNodeInParagraph(new DocCodeSpan({ configuration, code: apiItem.propertyTypeExcerpt.text }));
const excerpt: Excerpt = apiItem.propertyTypeExcerpt;
section.appendNodesInParagraph(
this._createExcerptContent(excerpt, apiItem)
);
}

return new DocTableCell({ configuration }, section.nodes);
}

private _createExcerptContent(excerpt: Excerpt, contextApiItem: ApiItem, defaultText?: string): DocNode[] {
const configuration: TSDocConfiguration = this._tsdocConfiguration;
if (!excerpt.text.trim()) {
return [new DocPlainText({ configuration, text: defaultText || '' })];
}
return excerpt.tokens.slice(excerpt.tokenRange.startIndex, excerpt.tokenRange.endIndex)
.reduce<DocNode[]>((acc, token) => {
const referencedApiItem: ApiItem|undefined = token.reference
&& this._apiModel.resolveDeclarationReference(token.reference, contextApiItem).resolvedApiItem;
if (referencedApiItem) {
acc.push(new DocLinkTag({
configuration,
tagName: '@link',
linkText: token.text,
urlDestination: this._getLinkFilenameForApiItem(referencedApiItem)
}));
} else {
acc.push(new DocPlainText({ configuration, text: token.text.replace(/\n/g, '') }));
}
return acc;
}, []);
}

private _writeBreadcrumb(output: DocSection, apiItem: ApiItem): void {
output.appendNodeInParagraph(new DocLinkTag({
configuration: this._tsdocConfiguration,
Expand Down
12 changes: 8 additions & 4 deletions apps/api-extractor-model/src/items/ApiDeclaredItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { Excerpt, ExcerptToken, IExcerptTokenRange, IExcerptToken } from '../mix
* @public
*/
export interface IApiDeclaredItemOptions extends IApiDocumentedItemOptions {
excerptTokens: IExcerptToken[];
excerptTokens: Array<IExcerptToken | ExcerptToken>;
}

export interface IApiDeclaredItemJson extends IApiDocumentedItemJson {
excerptTokens: IExcerptToken[];
excerptTokens: Array<IExcerptToken | ExcerptToken>;
}

/**
Expand Down Expand Up @@ -46,7 +46,7 @@ export class ApiDeclaredItem extends ApiDocumentedItem {
public constructor(options: IApiDeclaredItemOptions) {
super(options);

this._excerptTokens = options.excerptTokens.map(x => new ExcerptToken(x.kind, x.text));
this._excerptTokens = options.excerptTokens.map(x => x instanceof ExcerptToken ? x : new ExcerptToken(x));
this._excerpt = new Excerpt(this.excerptTokens, { startIndex: 0, endIndex: this.excerptTokens.length });
}

Expand Down Expand Up @@ -98,7 +98,11 @@ export class ApiDeclaredItem extends ApiDocumentedItem {
/** @override */
public serializeInto(jsonObject: Partial<IApiDeclaredItemJson>): void {
super.serializeInto(jsonObject);
jsonObject.excerptTokens = this.excerptTokens.map(x => ({ kind: x.kind, text: x.text }));
jsonObject.excerptTokens = this.excerptTokens.map((x): IExcerptToken => ({
kind: x.kind,
text: x.text,
...(x.reference ? { reference: x.reference.emitAsTsdoc() } : {})
}));
}

/**
Expand Down
45 changes: 45 additions & 0 deletions apps/api-extractor-model/src/items/ApiItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

import { Constructor, PropertiesOf } from '../mixins/Mixin';
import { ApiPackage } from '../model/ApiPackage';
import { ApiEntryPoint } from '../model/ApiEntryPoint';
import { ApiNameMixin } from '../mixins/ApiNameMixin';
import { DocDeclarationReference, DocMemberReference, DocMemberIdentifier, TSDocConfiguration } from '@microsoft/tsdoc';
import { ApiParameterListMixin } from '../mixins/ApiParameterListMixin';

/**
Expand Down Expand Up @@ -175,6 +178,48 @@ export class ApiItem {
return reversedParts.reverse().join('');
}

/**
* Get {@link @microsoft/tsdoc#DocDeclarationReference} node which referring to the api item.
* The result should be resolved exactly to the same api item by {@link ApiModel.resolveDeclarationReference}
*/
public getDocDeclarationReference(): DocDeclarationReference {
const reversedPath: string[] = [];
let packageName: string|undefined;
let entryPointPath: string|undefined;

for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) {
if (current.kind === ApiItemKind.Model) {
break;
} else if (current.kind === ApiItemKind.Package) {
packageName = (current as ApiPackage).name;
} else if (current.kind === ApiItemKind.EntryPoint) {
entryPointPath = (current as ApiEntryPoint).name;
} else {
if (ApiNameMixin.isBaseClassOf(current)) {
reversedPath.push(current.name);
}
}
}

const emptyTsdocConfig: TSDocConfiguration = new TSDocConfiguration();
return new DocDeclarationReference({
configuration: emptyTsdocConfig,
packageName,
importPath: entryPointPath,
memberReferences: reversedPath.reverse().map((name, idx) => {
return new DocMemberReference({
configuration: emptyTsdocConfig,
hasDot: idx !== 0,
memberIdentifier: new DocMemberIdentifier({
configuration: emptyTsdocConfig,
identifier: name
})
// selector: ??? // TODO: support declaration selector
});
})
});
}

/**
* If this item is an ApiPackage or has an ApiPackage as one of its parents, then that object is returned.
* Otherwise undefined is returned.
Expand Down
45 changes: 41 additions & 4 deletions apps/api-extractor-model/src/mixins/Excerpt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// See LICENSE in the project root for license information.

import { Text } from '@microsoft/node-core-library';
// import * as ts from 'typescript';
import { DocDeclarationReference, DocNode, DocLinkTag, TSDocParser, ParserContext } from '@microsoft/tsdoc';

/** @public */
export const enum ExcerptTokenKind {
Expand All @@ -21,20 +23,43 @@ export interface IExcerptTokenRange {
export interface IExcerptToken {
readonly kind: ExcerptTokenKind;
text: string;
/** reference to the api item, as {@link @microsoft/tsdoc#DocDeclarationReference} format. */
readonly reference?: string;
}

/** @public */
export class ExcerptToken {
private static tsdocParser: TSDocParser = new TSDocParser();
private static parseReference(reference: string): DocDeclarationReference | undefined {
const docLinkComment: string = `/** {@link ${reference}} */`;
const parserContext: ParserContext = ExcerptToken.tsdocParser.parseString(docLinkComment);
let docDeclarationReference: DocDeclarationReference | undefined;

function forEachDocNode(docNode: DocNode): void {
if (docNode instanceof DocLinkTag) {
docDeclarationReference = docNode.codeDestination;
} else {
docNode.getChildNodes().forEach(forEachDocNode);
}
}

forEachDocNode(parserContext.docComment);
return docDeclarationReference;
}

private readonly _kind: ExcerptTokenKind;
private readonly _text: string;
private _text: string;
private _reference?: DocDeclarationReference;

public constructor(kind: ExcerptTokenKind, text: string) {
this._kind = kind;
public constructor(data: IExcerptToken) {
this._kind = data.kind;

// Standardize the newlines across operating systems. Even though this may deviate from the actual
// input source file that was parsed, it's useful because the newline gets serialized inside
// a string literal in .api.json, which cannot be automatically normalized by Git.
this._text = Text.convertToLf(text);
this._text = Text.convertToLf(data.text);

this._reference = data.reference ? ExcerptToken.parseReference(data.reference) : undefined;
}

public get kind(): ExcerptTokenKind {
Expand All @@ -44,6 +69,18 @@ export class ExcerptToken {
public get text(): string {
return this._text;
}

public get reference(): DocDeclarationReference | undefined {
return this._reference;
}

public setReference(reference: DocDeclarationReference): void {
this._reference = reference;
}

public setText(text: string): void {
this._text = text;
}
}

/**
Expand Down
Loading