Skip to content

Commit

Permalink
Merge pull request #1265 from Vitalius1/FabricDocumenter
Browse files Browse the repository at this point in the history
[api-documenter] Allow customization of table of contents
  • Loading branch information
iclanton authored May 16, 2019
2 parents 5c25309 + 7218cff commit 99720e6
Show file tree
Hide file tree
Showing 21 changed files with 458 additions and 38 deletions.
2 changes: 2 additions & 0 deletions apps/api-documenter/src/cli/ApiDocumenterCommandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { CommandLineParser } from '@microsoft/ts-command-line';
import { MarkdownAction } from './MarkdownAction';
import { YamlAction } from './YamlAction';
import { GenerateAction } from './GenerateAction';

export class ApiDocumenterCommandLine extends CommandLineParser {
constructor() {
Expand All @@ -22,5 +23,6 @@ export class ApiDocumenterCommandLine extends CommandLineParser {
private _populateActions(): void {
this.addAction(new MarkdownAction(this));
this.addAction(new YamlAction(this));
this.addAction(new GenerateAction(this));
}
}
47 changes: 47 additions & 0 deletions apps/api-documenter/src/cli/GenerateAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as path from 'path';

import { ApiDocumenterCommandLine } from './ApiDocumenterCommandLine';
import { BaseAction } from './BaseAction';
import { DocumenterConfig } from '../documenters/DocumenterConfig';
import { ExperimentalYamlDocumenter } from '../documenters/ExperimentalYamlDocumenter';
import { IConfigFile } from '../documenters/IConfigFile';

import { ApiModel } from '@microsoft/api-extractor-model';
import { FileSystem } from '@microsoft/node-core-library';

export class GenerateAction extends BaseAction {
constructor(parser: ApiDocumenterCommandLine) {
super({
actionName: 'generate',
summary: 'EXPERIMENTAL',
documentation: 'EXPERIMENTAL - This action is a prototype of a new config file driven mode of operation for'
+ ' API Documenter. It is not ready for general usage yet. Its design may change in the future.'
});
}

protected onExecute(): Promise<void> { // override
// Look for the config file under the current folder

let configFilePath: string = path.join(process.cwd(), DocumenterConfig.FILENAME);

// First try the current folder
if (!FileSystem.exists(configFilePath)) {
// Otherwise try the standard "config" subfolder
configFilePath = path.join(process.cwd(), 'config', DocumenterConfig.FILENAME);
if (!FileSystem.exists(configFilePath)) {
throw new Error(`Unable to find ${DocumenterConfig.FILENAME} in the current folder or in a "config" subfolder`);
}
}

const configFile: IConfigFile = DocumenterConfig.loadFile(configFilePath);

const apiModel: ApiModel = this.buildApiModel();

const yamlDocumenter: ExperimentalYamlDocumenter = new ExperimentalYamlDocumenter(apiModel, configFile);
yamlDocumenter.generateFiles(this.outputFolder);
return Promise.resolve();
}
}
33 changes: 33 additions & 0 deletions apps/api-documenter/src/documenters/DocumenterConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as path from 'path';
import { JsonSchema, JsonFile } from '@microsoft/node-core-library';
import { IConfigFile } from './IConfigFile';

/**
* Helper for loading the api-documenter.json file format. Later when the schema is more mature,
* this class will be used to represent the validated and normalized configuration, whereas `IConfigFile`
* represents the raw JSON file structure.
*/
export class DocumenterConfig {
/**
* The JSON Schema for API Extractor config file (api-extractor.schema.json).
*/
public static readonly jsonSchema: JsonSchema = JsonSchema.fromFile(
path.join(__dirname, '..', 'schemas', 'api-documenter.schema.json'));

/**
* The config file name "api-extractor.json".
*/
public static readonly FILENAME: string = 'api-documenter.json';

/**
* Load and validate an api-documenter.json file.
*/
public static loadFile(configFilePath: string): IConfigFile {
const configFile: IConfigFile = JsonFile.loadAndValidate(configFilePath, DocumenterConfig.jsonSchema);

return configFile;
}
}
139 changes: 139 additions & 0 deletions apps/api-documenter/src/documenters/ExperimentalYamlDocumenter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { PackageName } from '@microsoft/node-core-library';
import { DocComment, DocInlineTag } from '@microsoft/tsdoc';
import { ApiModel, ApiItem, ApiItemKind, ApiDocumentedItem } from '@microsoft/api-extractor-model';

import { IConfigFile, IConfigTableOfContents } from './IConfigFile';
import { IYamlTocItem, IYamlTocFile } from '../yaml/IYamlTocFile';
import { YamlDocumenter } from './YamlDocumenter';

/**
* EXPERIMENTAL - This documenter is a prototype of a new config file driven mode of operation for
* API Documenter. It is not ready for general usage yet. Its design may change in the future.
*/
export class ExperimentalYamlDocumenter extends YamlDocumenter {
private _config: IConfigTableOfContents;
private _tocPointerMap: { [key: string]: IYamlTocItem };
private _catchAllPointer: IYamlTocItem;

public constructor(apiModel: ApiModel, configFile: IConfigFile) {
super(apiModel);
this._config = configFile.tableOfContents!;

this._tocPointerMap = {};

this._generateTocPointersMap(this._config.tocConfig);
}

/** @override */
protected buildYamlTocFile(apiItems: ReadonlyArray<ApiItem>): IYamlTocFile {
this._buildTocItems2(apiItems);
return this._config.tocConfig;
}

private _buildTocItems2(apiItems: ReadonlyArray<ApiItem>): IYamlTocItem[] {
const tocItems: IYamlTocItem[] = [];
for (const apiItem of apiItems) {
let tocItem: IYamlTocItem;

if (apiItem.kind === ApiItemKind.Namespace) {
// Namespaces don't have nodes yet
tocItem = {
name: apiItem.displayName
};
} else {
if (this._shouldEmbed(apiItem.kind)) {
// Don't generate table of contents items for embedded definitions
continue;
}

if (apiItem.kind === ApiItemKind.Package) {
tocItem = {
name: PackageName.getUnscopedName(apiItem.displayName),
uid: this._getUid(apiItem)
};
} else {
tocItem = {
name: apiItem.displayName,
uid: this._getUid(apiItem)
};
// Filtering out the api-items as we build the tocItems array.
if (apiItem instanceof ApiDocumentedItem) {
const docInlineTag: DocInlineTag | undefined =
(this._config && this._config.filterByInlineTag)
? this._findInlineTagByName(this._config.filterByInlineTag, apiItem.tsdocComment)
: undefined;

const tagContent: string | undefined =
docInlineTag && docInlineTag.tagContent && docInlineTag.tagContent.trim();

if (tagContent && this._tocPointerMap[tagContent]) {
// null assertion used because when pointer map was created we checked for presence of empty `items` array
this._tocPointerMap[tagContent].items!.push(tocItem);
} else {
if (this._catchAllPointer && this._catchAllPointer.items) {
this._catchAllPointer.items.push(tocItem);
}
}
}

}
}

tocItems.push(tocItem);

let children: ReadonlyArray<ApiItem>;
if (apiItem.kind === ApiItemKind.Package) {
// Skip over the entry point, since it's not part of the documentation hierarchy
children = apiItem.members[0].members;
} else {
children = apiItem.members;
}

const childItems: IYamlTocItem[] = this._buildTocItems2(children);
if (childItems.length > 0) {
tocItem.items = childItems;
}
}
return tocItems;
}

// Parses the tocConfig object to build a pointers map of nodes where we want to sort out the API items
private _generateTocPointersMap(tocConfig: IYamlTocFile | IYamlTocItem): void {
if (tocConfig.items) {
for (const tocItem of tocConfig.items) {
if (tocItem.items && tocItem.items.length > 0) {
this._generateTocPointersMap(tocItem);
} else {
// check for presence of the `catchAllCategory` config option
if (this._config && this._config.catchAllCategory && tocItem.name === this._config.catchAllCategory) {
this._catchAllPointer = tocItem;
} else {
this._tocPointerMap[tocItem.name] = tocItem;
}
}
}
}
}

// This is a direct copy of a @docCategory inline tag finder in office-ui-fabric-react,
// but is generic enough to be used for any inline tag
private _findInlineTagByName(tagName: string, docComment: DocComment | undefined): DocInlineTag | undefined {
if (docComment instanceof DocInlineTag) {
if (docComment.tagName === tagName) {
return docComment;
}
}
if (docComment) {
for (const childNode of docComment.getChildNodes()) {
const result: DocInlineTag | undefined = this._findInlineTagByName(tagName, childNode as DocComment);
if (result !== undefined) {
return result;
}
}
}
return undefined;
}
}
51 changes: 51 additions & 0 deletions apps/api-documenter/src/documenters/IConfigFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { IYamlTocFile } from '../yaml/IYamlTocFile';

/**
* Typescript interface describing the config schema for toc.yml file format.
*/
export interface IConfigTableOfContents {
/**
* Represents the tree structure describing the toc.file format.
* Only the nodes that have an empty `items` array will be filled with API items
* that are matched with the filters provided. Everything else will be placed under a catchAll category
* that is highly recommended to be provided.
*/
tocConfig: IYamlTocFile;

/**
* Optional category name that is recommended to include in the `tocConfig`,
* along with one of the filters: `filterByApiItemName` or `filterByInlineTag`.
* Any items that are not matched to the mentioned filters will be placed under this
* catchAll category. If none provided the items will not be included in the final toc.yml file.
*/
catchAllCategory?: string;

/**
* When loading more than one api.json files that might include the same API items,
* toggle either to show duplicates or not.
*/
noDuplicateEntries?: boolean;

/**
* Toggle either sorting of the API items should be made based on category name presence
* in the API item's name.
*/
filterByApiItemName?: boolean;

/**
* Filter that can be used to sort the API items according to an inline custom tag
* that is present on them.
*/
filterByInlineTag?: string;
}

/**
* This interface represents the api-extractor.json file format.
*/
export interface IConfigFile {
/** {@inheritDoc IConfigTableOfContents} */
tableOfContents?: IConfigTableOfContents;
}
20 changes: 14 additions & 6 deletions apps/api-documenter/src/documenters/YamlDocumenter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

// tslint:disable:member-ordering

import * as path from 'path';

import yaml = require('js-yaml');
Expand Down Expand Up @@ -184,6 +186,15 @@ export class YamlDocumenter {
* Write the table of contents
*/
private _writeTocFile(apiItems: ReadonlyArray<ApiItem>): void {
const tocFile: IYamlTocFile = this.buildYamlTocFile(apiItems);

const tocFilePath: string = path.join(this._outputFolder, 'toc.yml');
console.log('Writing ' + tocFilePath);
this._writeYamlFile(tocFile, tocFilePath, '', undefined);
}

/** @virtual */
protected buildYamlTocFile(apiItems: ReadonlyArray<ApiItem>): IYamlTocFile {
const tocFile: IYamlTocFile = {
items: [ ]
};
Expand All @@ -192,10 +203,7 @@ export class YamlDocumenter {
tocFile.items.push(rootItem);

rootItem.items!.push(...this._buildTocItems(apiItems));

const tocFilePath: string = path.join(this._outputFolder, 'toc.yml');
console.log('Writing ' + tocFilePath);
this._writeYamlFile(tocFile, tocFilePath, '', undefined);
return tocFile;
}

private _buildTocItems(apiItems: ReadonlyArray<ApiItem>): IYamlTocItem[] {
Expand Down Expand Up @@ -245,7 +253,7 @@ export class YamlDocumenter {
return tocItems;
}

private _shouldEmbed(apiItemKind: ApiItemKind): boolean {
protected _shouldEmbed(apiItemKind: ApiItemKind): boolean {
switch (apiItemKind) {
case ApiItemKind.Class:
case ApiItemKind.Package:
Expand Down Expand Up @@ -511,7 +519,7 @@ export class YamlDocumenter {
* Calculate the DocFX "uid" for the ApiItem
* Example: node-core-library.JsonFile.load
*/
private _getUid(apiItem: ApiItem): string {
protected _getUid(apiItem: ApiItem): string {
let result: string = '';
for (const hierarchyItem of apiItem.getHierarchy()) {

Expand Down
Loading

0 comments on commit 99720e6

Please sign in to comment.