Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions apps/api-documenter/src/documenters/MarkdownDocumenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ import {
ApiTypeAlias,
ExcerptToken,
ApiOptionalMixin,
ApiInitializerMixin
ApiInitializerMixin,
ApiProtectedMixin,
ApiReadonlyMixin
} from '@microsoft/api-extractor-model';

import { CustomDocNodes } from '../nodes/CustomDocNodeKind';
Expand Down Expand Up @@ -755,12 +757,12 @@ export class MarkdownDocumenter {

const eventsTable: DocTable = new DocTable({
configuration,
headerTitles: ['Property', 'Type', 'Description']
headerTitles: ['Property', 'Modifiers', 'Type', 'Description']
});

const propertiesTable: DocTable = new DocTable({
configuration,
headerTitles: ['Property', 'Type', 'Description']
headerTitles: ['Property', 'Modifiers', 'Type', 'Description']
});

const methodsTable: DocTable = new DocTable({
Expand All @@ -787,6 +789,7 @@ export class MarkdownDocumenter {
eventsTable.addRow(
new DocTableRow({ configuration }, [
this._createTitleCell(apiMember),
this._createModifiersCell(apiMember),
this._createPropertyTypeCell(apiMember),
this._createDescriptionCell(apiMember)
])
Expand All @@ -795,6 +798,7 @@ export class MarkdownDocumenter {
propertiesTable.addRow(
new DocTableRow({ configuration }, [
this._createTitleCell(apiMember),
this._createModifiersCell(apiMember),
this._createPropertyTypeCell(apiMember),
this._createDescriptionCell(apiMember)
])
Expand Down Expand Up @@ -1007,9 +1011,27 @@ export class MarkdownDocumenter {

const section: DocSection = new DocSection({ configuration });

if (ApiProtectedMixin.isBaseClassOf(apiItem)) {
if (apiItem.isProtected) {
section.appendNode(
new DocParagraph({ configuration }, [new DocCodeSpan({ configuration, code: 'protected' })])
);
}
}

if (ApiReadonlyMixin.isBaseClassOf(apiItem)) {
if (apiItem.isReadonly) {
section.appendNode(
new DocParagraph({ configuration }, [new DocCodeSpan({ configuration, code: 'readonly' })])
);
}
}

if (ApiStaticMixin.isBaseClassOf(apiItem)) {
if (apiItem.isStatic) {
section.appendNodeInParagraph(new DocCodeSpan({ configuration, code: 'static' }));
section.appendNode(
new DocParagraph({ configuration }, [new DocCodeSpan({ configuration, code: 'static' })])
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export class CustomMarkdownEmitter extends MarkdownEmitter {
break;
}
default:
super.writeNode(docNode, context, false);
super.writeNode(docNode, context, docNodeSiblings);
}
}

Expand Down
10 changes: 7 additions & 3 deletions apps/api-documenter/src/markdown/MarkdownEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,13 @@ export class MarkdownEmitter {
const trimmedParagraph: DocParagraph = DocNodeTransforms.trimSpacesInParagraph(docParagraph);
if (context.insideTable) {
if (docNodeSiblings) {
writer.write('<p>');
this.writeNodes(trimmedParagraph.nodes, context);
writer.write('</p>');
// This tentative write is necessary to avoid writing empty paragraph tags (i.e. `<p></p>`). At the
// time this code runs, we do not know whether the `writeNodes` call below will actually write
// anything. Thus, we want to only write a `<p>` tag (as well as eventually a corresponding
// `</p>` tag) if something ends up being written within the tags.
writer.writeTentative('<p>', '</p>', () => {
this.writeNodes(trimmedParagraph.nodes, context);
});
} 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);
Expand Down
52 changes: 52 additions & 0 deletions apps/api-documenter/src/utils/IndentedWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export class IndentedWriter {
private readonly _indentStack: string[];
private _indentText: string;

private _beforeStack: string[];
private _isWritingBeforeStack: boolean;

public constructor(builder?: IStringBuilder) {
this._builder = builder === undefined ? new StringBuilder() : builder;

Expand All @@ -55,6 +58,9 @@ export class IndentedWriter {

this._indentStack = [];
this._indentText = '';

this._beforeStack = [];
this._isWritingBeforeStack = false;
}

/**
Expand Down Expand Up @@ -149,6 +155,30 @@ export class IndentedWriter {
return '';
}

/**
* Writes `before` and `after` messages if and only if `mayWrite` writes anything.
*
* If `mayWrite` writes "CONTENT", this method will write "<before>CONTENT<after>".
* If `mayWrite` writes nothing, this method will write nothing.
*/
public writeTentative(before: string, after: string, mayWrite: () => void): void {
this._beforeStack.push(before);

// If this function writes anything, then _all_ messages in the "before stack" will also be
// written. This means that the stack will be empty (as when we write a message from the stack,
// we remove it from the stack).
mayWrite();

// If the stack is not empty, it means that `mayWrite` didn't write anything. Pop the last-
// added message from the stack, we'll never write it. Otherwise, if the stack is empty, then
// write the "after" message.
if (this._beforeStack.length > 0) {
this._beforeStack.pop();
} else {
this.write(after);
}
}

/**
* Writes some text to the internal string buffer, applying indentation according
* to the current indentation level. If the string contains multiple newlines,
Expand All @@ -159,6 +189,10 @@ export class IndentedWriter {
return;
}

if (!this._isWritingBeforeStack) {
this._writeBeforeStack();
}

// If there are no newline characters, then append the string verbatim
if (!/[\r\n]/.test(message)) {
this._writeLinePart(message);
Expand Down Expand Up @@ -186,7 +220,10 @@ export class IndentedWriter {
public writeLine(message: string = ''): void {
if (message.length > 0) {
this.write(message);
} else if (!this._isWritingBeforeStack) {
this._writeBeforeStack();
}

this._writeNewLine();
}

Expand Down Expand Up @@ -218,6 +255,21 @@ export class IndentedWriter {
this._builder.append(s);
}

/**
* Writes all messages in our before stack, processing them in FIFO order. This stack is
* populated by the `writeTentative` method.
*/
private _writeBeforeStack(): void {
this._isWritingBeforeStack = true;

for (const message of this._beforeStack) {
this.write(message);
}

this._isWritingBeforeStack = false;
this._beforeStack = [];
}

private _updateIndentText(): void {
this._indentText = this._indentStack.join('');
}
Expand Down
55 changes: 35 additions & 20 deletions apps/api-extractor/src/generators/ApiModelGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,14 +609,16 @@ export class ApiModelGenerator {
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const isReadonly: boolean = this._isReadonly(astDeclaration);

apiIndexSignature = new ApiIndexSignature({
docComment,
releaseTag,
parameters,
overloadIndex,
excerptTokens,
returnTypeTokenRange
returnTypeTokenRange,
isReadonly
});

parentApiItem.addMember(apiIndexSignature);
Expand Down Expand Up @@ -862,7 +864,7 @@ export class ApiModelGenerator {
const isOptional: boolean =
(astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0;
const isProtected: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0;
const isReadonly: boolean = this._determineReadonly(astDeclaration);
const isReadonly: boolean = this._isReadonly(astDeclaration);

apiProperty = new ApiProperty({
name,
Expand Down Expand Up @@ -909,7 +911,7 @@ export class ApiModelGenerator {
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const isOptional: boolean =
(astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0;
const isReadonly: boolean = this._determineReadonly(astDeclaration);
const isReadonly: boolean = this._isReadonly(astDeclaration);

apiPropertySignature = new ApiPropertySignature({
name,
Expand Down Expand Up @@ -1003,7 +1005,7 @@ export class ApiModelGenerator {
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const isReadonly: boolean = this._determineReadonly(astDeclaration);
const isReadonly: boolean = this._isReadonly(astDeclaration);

apiVariable = new ApiVariable({
name,
Expand Down Expand Up @@ -1087,21 +1089,34 @@ export class ApiModelGenerator {
return parameters;
}

private _determineReadonly(astDeclaration: AstDeclaration): boolean {
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const declarationMetadata: DeclarationMetadata = this._collector.fetchDeclarationMetadata(astDeclaration);
//Line 1: sees whether the readonly or const modifiers are present
//Line 2: sees if the TSDoc comment for @readonly is present
//Line 3: sees whether a getter is present for a property with no setter
//Line 4: sees if the var declaration has Const keyword
return (
(astDeclaration.modifierFlags & (ts.ModifierFlags.Readonly + ts.ModifierFlags.Const)) !== 0 ||
(docComment !== undefined && docComment.modifierTagSet.hasTagName('@readonly')) ||
(declarationMetadata.ancillaryDeclarations.length === 0 &&
astDeclaration.declaration.kind === ts.SyntaxKind.GetAccessor) ||
(ts.isVariableDeclaration(astDeclaration.declaration) &&
TypeScriptInternals.isVarConst(astDeclaration.declaration))
);
private _isReadonly(astDeclaration: AstDeclaration): boolean {
switch (astDeclaration.declaration.kind) {
case ts.SyntaxKind.GetAccessor:
case ts.SyntaxKind.IndexSignature:
case ts.SyntaxKind.PropertyDeclaration:
case ts.SyntaxKind.PropertySignature:
case ts.SyntaxKind.SetAccessor:
case ts.SyntaxKind.VariableDeclaration: {
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const declarationMetadata: DeclarationMetadata =
this._collector.fetchDeclarationMetadata(astDeclaration);

const hasReadonlyModifier: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Readonly) !== 0;
const hasReadonlyDocTag: boolean = !!docComment?.modifierTagSet?.hasTagName('@readonly');
const isGetterWithNoSetter: boolean =
ts.isGetAccessorDeclaration(astDeclaration.declaration) &&
declarationMetadata.ancillaryDeclarations.length === 0;
const isVarConst: boolean =
ts.isVariableDeclaration(astDeclaration.declaration) &&
TypeScriptInternals.isVarConst(astDeclaration.declaration);

return hasReadonlyModifier || hasReadonlyDocTag || isGetterWithNoSetter || isVarConst;
}
default: {
// Readonly-ness does not make sense for any other declaration kind.
return false;
}
}
}
}
Loading