Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api-documenter] Allow customization of table of contents #1265

Merged
merged 21 commits into from
May 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate [](start = 19, length = 8)

This seems like a weird verb. Maybe "experimental-yaml" or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See this comment. We discussed it with @octogonz that we might wanna move to a using this generic generate action along with a config file that will command which documenter to be used.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@octogonz - did you ever update this rule?

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