diff --git a/package.json b/package.json index 5248ca21b24..55f7f90f7b6 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,8 @@ "axe-core": "^2.0.7", "axe-webdriverjs": "^0.4.0", "conventional-changelog": "^1.1.0", + "dgeni": "^0.4.2", + "dgeni-packages": "^0.16.2", "express": "^4.14.0", "firebase-tools": "^2.2.1", "fs-extra": "^0.26.5", diff --git a/tools/dgeni/index.js b/tools/dgeni/index.js new file mode 100644 index 00000000000..4153eecc91e --- /dev/null +++ b/tools/dgeni/index.js @@ -0,0 +1,148 @@ +const path = require('path'); +const fs = require('fs'); +const Dgeni = require('dgeni'); +const DgeniPackage = Dgeni.Package; + +// dgeni packages +const jsdocPackage = require('dgeni-packages/jsdoc'); +const nunjucksPackage = require('dgeni-packages/nunjucks'); +const typescriptPackage = require('dgeni-packages/typescript'); + + +// Project configuration. +const projectRootDir = path.resolve(__dirname, '../..'); +const sourceDir = path.resolve(projectRootDir, 'src/lib'); +const outputDir = path.resolve(projectRootDir, 'dist/docs'); +const templateDir = path.resolve(__dirname, './templates'); + +// Package definition for material2 api docs. This only *defines* the package- it does not yet +// actually *run* anything. +// +// A dgeni package is very similar to an Angular 1 module. Modules contain: +// "services" (injectables) +// "processors" (injectables that conform to a specific interface) +// "templates": nunjucks templates that can be used to render content +// +// A dgeni package also has a `config` method, similar to an Angular 1 module. +// A config block can inject any services/processors and configure them before +// docs processing begins. + +const dgeniPackageDeps = [ + jsdocPackage, + nunjucksPackage, + typescriptPackage, +]; + +let apiDocsPackage = new DgeniPackage('material2-api-docs', dgeniPackageDeps) + +// Processor that filters out symbols that should not be shown in the docs. +.processor(require('./processors/docs-private-filter')) + +// Processor that appends categorization flags to the docs, e.g. `isDirective`, `isNgModule`, etc. +.processor(require('./processors/categorizer')) + +// Processor to group components into top-level groups such as "Tabs", "Sidenav", etc. +.processor(require('./processors/component-grouper')) + +.config(function(log) { + log.level = 'info'; +}) + +// Configure the processor for reading files from the file system. +.config(function(readFilesProcessor, writeFilesProcessor) { + readFilesProcessor.basePath = sourceDir; + readFilesProcessor.$enabled = false; // disable for now as we are using readTypeScriptModules + + writeFilesProcessor.outputFolder = outputDir; +}) + +// Configure the output path for written files (i.e., file names). +.config(function(computePathsProcessor) { + computePathsProcessor.pathTemplates = [{ + docTypes: ['componentGroup'], + pathTemplate: '${name}', + outputPathTemplate: '${name}.html', + }]; +}) + +// Configure custom JsDoc tags. +.config(function(parseTagsProcessor) { + parseTagsProcessor.tagDefinitions = parseTagsProcessor.tagDefinitions.concat([ + {name: 'docs-private'} + ]); +}) + +// Configure the processor for understanding TypeScript. +.config(function(readTypeScriptModules) { + console.log(sourceDir); + readTypeScriptModules.basePath = sourceDir; + readTypeScriptModules.ignoreExportsMatching = [/^_/]; + readTypeScriptModules.hidePrivateMembers = true; + + // Entry points for docs generation. All publically exported symbols found through these + // files will have docs generated. + readTypeScriptModules.sourceFiles = [ + 'autocomplete/index.ts', + 'button/index.ts', + 'button-toggle/index.ts', + 'card/index.ts', + 'checkbox/index.ts', + 'chips/index.ts', + 'core/index.ts', + 'dialog/index.ts', + 'grid-list/index.ts', + 'icon/index.ts', + 'input/index.ts', + 'list/index.ts', + 'menu/index.ts', + 'progress-bar/index.ts', + 'progress-circle/index.ts', + 'radio/index.ts', + 'select/index.ts', + 'sidenav/index.ts', + 'slide-toggle/index.ts', + 'slider/index.ts', + 'snack-bar/index.ts', + 'tabs/index.ts', + 'toolbar/index.ts', + 'tooltip/index.ts', + ]; +}) + + +// Configure processor for finding nunjucks templates. +.config(function(templateFinder, templateEngine) { + // Where to find the templates for the doc rendering + templateFinder.templateFolders = [templateDir]; + + // Standard patterns for matching docs to templates + templateFinder.templatePatterns = [ + '${ doc.template }', + '${ doc.id }.${ doc.docType }.template.html', + '${ doc.id }.template.html', + '${ doc.docType }.template.html', + '${ doc.id }.${ doc.docType }.template.js', + '${ doc.id }.template.js', + '${ doc.docType }.template.js', + '${ doc.id }.${ doc.docType }.template.json', + '${ doc.id }.template.json', + '${ doc.docType }.template.json', + 'common.template.html' + ]; + + // Nunjucks and Angular conflict in their template bindings so change Nunjucks + templateEngine.config.tags = { + variableStart: '{$', + variableEnd: '$}' + }; +}); + + +module.exports = apiDocsPackage; + +// Run the dgeni pipeline, generating documentation. +// TODO(jelbourn): remove this once the process is more final in favor of gulp. +let dgeni = new Dgeni([apiDocsPackage]); +dgeni.generate().then(docs => { + console.log(docs); +}); diff --git a/tools/dgeni/processors/categorizer.js b/tools/dgeni/processors/categorizer.js new file mode 100644 index 00000000000..2a90b33811f --- /dev/null +++ b/tools/dgeni/processors/categorizer.js @@ -0,0 +1,112 @@ +/** + * Processor to add properties to docs objects. + * + * isMethod | Whether the doc is for a method on a class. + * isDirective | Whether the doc is for a @Component or a @Directive + * isService | Whether the doc is for an @Injectable + * isNgModule | Whether the doc is for an NgModule + */ +module.exports = function categorizer() { + return { + $runBefore: ['docs-processed'], + $process: function(docs) { + docs.forEach(doc => { + // The typescriptPackage groups both methods and parameters into "members". + // Use the presence of `parameters` as a proxy to determine if this is a method. + if (doc.classDoc && doc.hasOwnProperty('parameters')) { + doc.isMethod = true; + + // Mark methods with a `void` return type so we can omit show the return type in the docs. + doc.showReturns = doc.returnType && doc.returnType != 'void'; + + normalizeMethodParameters(doc); + + // Maintain a list of methods on the associated class so we can + // iterate through them while rendering. + doc.classDoc.methods ? + doc.classDoc.methods.push(doc) : + doc.classDoc.methods = [doc]; + } else if (isDirective(doc)) { + doc.isDirective = true; + } else if (isService(doc)) { + doc.isService = true; + } else if (isNgModule(doc)) { + doc.isNgModule = true; + } else if (doc.docType == 'member') { + doc.isDirectiveInput = isDirectiveInput(doc); + doc.isDirectiveOutput = isDirectiveOutput(doc); + + doc.classDoc.properties ? + doc.classDoc.properties.push(doc) : + doc.classDoc.properties = [doc]; + } + }); + } + }; +}; + + +/** + * The `parameters` property are the parameters extracted from TypeScript and are strings + * of the form "propertyName: propertyType" (literally what's written in the source). + * + * The `params` property is pulled from the `@param` JsDoc tag. We need to merge + * the information of these to get name + type + description. + * + * We will use the `params` property to store the final normalized form since it is already + * an object. + */ +function normalizeMethodParameters(method) { + if (method.parameters) { + method.parameters.forEach(parameter => { + let [parameterName, parameterType] = parameter.split(':'); + + if (!method.params) { + method.params = []; + } + + let jsDocParam = method.params.find(p => p.name == parameterName); + + if (!jsDocParam) { + jsDocParam = {name: parameterName}; + method.params.push(jsDocParam); + } + + jsDocParam.type = parameterType.trim(); + }); + } +} + +function isDirective(doc) { + return hasClassDecorator(doc, 'Component') || hasClassDecorator(doc, 'Directive'); +} + +function isService(doc) { + return hasClassDecorator(doc, 'Injectable') +} + +function isNgModule(doc) { + return hasClassDecorator(doc, 'NgModule'); +} + +function isDirectiveOutput(doc) { + return hasMemberDecorator(doc, 'Output'); +} + +function isDirectiveInput(doc) { + return hasMemberDecorator(doc, 'Input'); +} + +function hasMemberDecorator(doc, decoratorName) { + return doc.docType == 'member' && hasDecorator(doc, decoratorName); +} + +function hasClassDecorator(doc, decoratorName) { + return doc.docType == 'class' && hasDecorator(doc, decoratorName); +} + +function hasDecorator(doc, decoratorName) { + return doc.decorators && + doc.decorators.length && + doc.decorators.some(d => d.name == decoratorName); +} diff --git a/tools/dgeni/processors/component-grouper.js b/tools/dgeni/processors/component-grouper.js new file mode 100644 index 00000000000..8bab3d52506 --- /dev/null +++ b/tools/dgeni/processors/component-grouper.js @@ -0,0 +1,60 @@ +const path = require('path'); + +/** + * Processor to group docs into top-level "Components" WRT material design, e.g., "Button", "Tabs", + * where each group may conists of several directives and services. + */ + + +/** Component group data structure. */ +class ComponentGroup { + constructor(name) { + this.name = name; + this.id = `component-group-${name}`; + this.docType = 'componentGroup'; + this.directives = []; + this.services = []; + this.ngModule = null; + } +} + +module.exports = function componentGrouper() { + return { + $runBefore: ['docs-processed'], + $process: function(docs) { + // Map of group name to group instance. + let groups = new Map(); + + docs.forEach(doc => { + // Full path to the file for this doc. + let basePath = doc.fileInfo.basePath; + let filePath = doc.fileInfo.filePath; + + // All of the component documentation is under `src/lib`, which will be the basePath. + // We group the docs up by the directory immediately under `src/lib` (e.g., "button"). + let groupName = path.relative(basePath, filePath).split(path.sep)[0]; + + // Get the group for this doc, or, if one does not exist, create it. + let group; + if (groups.has(groupName)) { + group = groups.get(groupName); + } else { + group = new ComponentGroup(groupName); + groups.set(groupName, group); + } + + // Put this doc into the appropriate list in this group. + if (doc.isDirective) { + group.directives.push(doc); + } else if (doc.isService) { + group.services.push(doc); + } else if (doc.isNgModule) { + group.ngModule = doc; + } + }); + + return Array.from(groups.values()); + } + }; +}; + diff --git a/tools/dgeni/processors/docs-private-filter.js b/tools/dgeni/processors/docs-private-filter.js new file mode 100644 index 00000000000..7507e58c1c1 --- /dev/null +++ b/tools/dgeni/processors/docs-private-filter.js @@ -0,0 +1,41 @@ +/** + * Processor to filter out symbols that should not be shown in the Material docs. + */ + +const INTERNAL_METHODS = [ + // Lifecycle methods + 'ngOnInit', + 'ngOnChanges', + 'ngDoCheck', + 'ngAfterContentInit', + 'ngAfterContentChecked', + 'ngAfterViewInit', + 'ngAfterViewChecked', + 'ngOnDestroy', + + // ControlValueAccessor methods + 'writeValue', + 'registerOnChange', + 'registerOnTouched', + 'setDisabledState', + + // Don't ever need to document constructors + 'constructor', + + // tabIndex exists on all elements, no need to document it + 'tabIndex', +]; + +module.exports = function docsPrivateFilter() { + return { + $runBefore: ['docs-processed'], + $process: function(docs) { + return docs.filter(d => !(hasDocsPrivateTag(d) || INTERNAL_METHODS.includes(d.name))); + } + }; +}; + +function hasDocsPrivateTag(doc) { + let tags = doc.tags && doc.tags.tags; + return tags ? tags.find(d => d.tagName == 'docs-private') : false; +} diff --git a/tools/dgeni/templates/common.template.html b/tools/dgeni/templates/common.template.html new file mode 100644 index 00000000000..2ca6024237c --- /dev/null +++ b/tools/dgeni/templates/common.template.html @@ -0,0 +1 @@ +## {$ doc.name $} ({$ doc.docType $}) \ No newline at end of file diff --git a/tools/dgeni/templates/componentGroup.template.html b/tools/dgeni/templates/componentGroup.template.html new file mode 100644 index 00000000000..e39cdf5bd58 --- /dev/null +++ b/tools/dgeni/templates/componentGroup.template.html @@ -0,0 +1,91 @@ + + +
Module: {$ doc.ngModule.name $}
{$ directive.description $}
+ + {%- if directive.properties.length -%} +| Name | +Description | +
|---|---|
|
+ {%- if property.isDirectiveInput -%}
+ @Input + {%- endif -%} + +{$ property.name $} +
|
+ {$ property.description $} | +
| {$ method.name $} | +|
|---|---|
| {$ method.description $} | +|
| Parameters | +|
|
+ {$ parameter.name $} +{$ parameter.type $} + |
+ + {$ parameter.description $} + | +
| Returns | +|
| {$ method.returnType $} | +{$ method.returns.description $} | +