build(aio): move doc-gen stuff from angular.io (#14097)

This commit is contained in:
Peter Bacon Darwin
2017-01-26 14:03:53 +00:00
committed by Igor Minar
parent d1d0ce7613
commit b7763559cd
135 changed files with 5031 additions and 1 deletions

View File

@ -0,0 +1,37 @@
module.exports = function addNotYetDocumentedProperty(EXPORT_DOC_TYPES, log, createDocMessage) {
return {
$runAfter: ['tags-parsed'],
$runBefore: ['rendering-docs'],
$process: function(docs) {
docs.forEach(function(doc) {
if (EXPORT_DOC_TYPES.indexOf(doc.docType) === -1) return;
// NotYetDocumented means that no top level comments and no member level comments
doc.notYetDocumented = notYetDocumented(doc);
if (doc.constructorDoc) {
doc.constructorDoc.notYetDocumented = notYetDocumented(doc.constructorDoc);
doc.notYetDocumented = doc.notYetDocumented && doc.constructorDoc.notYetDocumented;
}
if (doc.members) {
doc.members.forEach(function(member) {
member.notYetDocumented = notYetDocumented(member);
doc.notYetDocumented = doc.notYetDocumented && member.notYetDocumented;
});
}
if (doc.notYetDocumented) {
log.debug(createDocMessage('Not yet documented', doc));
}
});
return docs;
}
};
};
function notYetDocumented(doc) {
return !doc.noDescription && doc.description.trim().length == 0;
}

View File

@ -0,0 +1,148 @@
var testPackage = require('../../helpers/test-package');
var Dgeni = require('dgeni');
describe('addNotYetDocumentedProperty', function() {
var dgeni, injector, processor, log;
beforeEach(function() {
dgeni = new Dgeni([testPackage('angular.io-package')]);
injector = dgeni.configureInjector();
processor = injector.get('addNotYetDocumentedProperty');
log = injector.get('log');
});
it('should mark export docs with no description as "not yet documented"', function() {
var a, b, c, d, a1, b1, c1, d1;
var docs = [
a = {id: 'a', docType: 'interface', description: 'some content'},
b = {id: 'b', docType: 'class', description: 'some content'},
c = {id: 'c', docType: 'var', description: 'some content'},
d = {id: 'd', docType: 'function', description: 'some content'},
a1 = {id: 'a1', docType: 'interface', description: ''},
b1 = {id: 'b1', docType: 'class', description: ''},
c1 = {id: 'c1', docType: 'var', description: ''},
d1 = {id: 'd1', docType: 'function', description: ''}
];
processor.$process(docs);
expect(a.notYetDocumented).toBeFalsy();
expect(b.notYetDocumented).toBeFalsy();
expect(c.notYetDocumented).toBeFalsy();
expect(d.notYetDocumented).toBeFalsy();
expect(a1.notYetDocumented).toBeTruthy();
expect(b1.notYetDocumented).toBeTruthy();
expect(c1.notYetDocumented).toBeTruthy();
expect(d1.notYetDocumented).toBeTruthy();
});
it('should mark member docs with no description as "not yet documented"', function() {
var a, a1, a2, b, b1, b2, c, c1, c2;
var docs = [
a = {
id: 'a',
docType: 'interface',
description: 'some content',
members: [a1 = {id: 'a1', description: 'some content'}, a2 = {id: 'a2', description: ''}]
},
b = {
id: 'b',
docType: 'class',
description: '',
members: [b1 = {id: 'b1', description: 'some content'}, b2 = {id: 'b2', description: ''}]
},
c = {
id: 'c',
docType: 'class',
description: '',
members: [c1 = {id: 'c1', description: ''}, c2 = {id: 'c2', description: ''}]
},
];
processor.$process(docs);
expect(a.notYetDocumented).toBeFalsy();
expect(b.notYetDocumented).toBeFalsy();
expect(c.notYetDocumented).toBeTruthy();
expect(a1.notYetDocumented).toBeFalsy();
expect(a2.notYetDocumented).toBeTruthy();
expect(b1.notYetDocumented).toBeFalsy();
expect(b2.notYetDocumented).toBeTruthy();
expect(c1.notYetDocumented).toBeTruthy();
expect(c2.notYetDocumented).toBeTruthy();
});
it('should mark constructor doc with no description as "not yet documented"', function() {
var a, a1, b, b1;
var docs = [
a = {
id: 'a',
docType: 'interface',
description: '',
constructorDoc: a1 = {id: 'a1', description: 'some content'}
},
b = {
id: 'b',
docType: 'interface',
description: '',
constructorDoc: b1 = {id: 'b1', description: ''}
}
];
processor.$process(docs);
expect(a.notYetDocumented).toBeFalsy();
expect(b.notYetDocumented).toBeTruthy();
expect(a1.notYetDocumented).toBeFalsy();
expect(b1.notYetDocumented).toBeTruthy();
});
it('should not mark documents explicity tagged as `@noDescription`', function() {
var a, a1, a2, b, b1, b2, c, c1, c2;
var docs = [
a = {
id: 'a',
docType: 'interface',
description: 'some content',
members: [
a1 = {id: 'a1', description: 'some content'},
a2 = {id: 'a2', description: '', noDescription: true}
]
},
b = {
id: 'b',
docType: 'class',
description: '',
members: [
b1 = {id: 'b1', description: 'some content'},
b2 = {id: 'b2', description: '', noDescription: true}
]
},
c = {
id: 'c',
docType: 'class',
description: '',
noDescription: true,
members: [c1 = {id: 'c1', description: ''}, c2 = {id: 'c2', description: ''}]
},
];
processor.$process(docs);
expect(a.notYetDocumented).toBeFalsy();
expect(b.notYetDocumented).toBeFalsy();
expect(c.notYetDocumented).toBeFalsy();
expect(a1.notYetDocumented).toBeFalsy();
expect(a2.notYetDocumented).toBeFalsy();
expect(b1.notYetDocumented).toBeFalsy();
expect(b2.notYetDocumented).toBeFalsy();
expect(c1.notYetDocumented).toBeTruthy();
expect(c2.notYetDocumented).toBeTruthy();
});
});

View File

@ -0,0 +1,33 @@
var _ = require('lodash');
/**
* @dgProcessor checkUnbalancedBackTicks
* @description
* Searches the rendered content for an odd number of (```) backticks,
* which would indicate an unbalanced pair and potentially a typo in the
* source content.
*/
module.exports = function checkUnbalancedBackTicks(log, createDocMessage) {
var BACKTICK_REGEX = /^ *```/gm;
return {
// $runAfter: ['checkAnchorLinksProcessor'],
$runAfter: ['inlineTagProcessor'],
$runBefore: ['writeFilesProcessor'],
$process: function(docs) {
_.forEach(docs, function(doc) {
if (doc.renderedContent) {
var matches = doc.renderedContent.match(BACKTICK_REGEX);
if (matches && matches.length % 2 !== 0) {
doc.unbalancedBackTicks = true;
log.warn(createDocMessage(
'checkUnbalancedBackTicks processor: unbalanced backticks found in rendered content',
doc));
log.warn(doc.renderedContent);
}
}
});
}
};
};

View File

@ -0,0 +1,30 @@
var testPackage = require('../../helpers/test-package');
var Dgeni = require('dgeni');
var path = require('canonical-path');
describe('checkUnbalancedBackTicks', function() {
var dgeni, injector, processor, log;
beforeEach(function() {
dgeni = new Dgeni([testPackage('angular.io-package')]);
injector = dgeni.configureInjector();
processor = injector.get('checkUnbalancedBackTicks');
log = injector.get('log');
});
it('should warn if there are an odd number of back ticks in the rendered content', function() {
var docs = [{
renderedContent: '```\n' +
'code block\n' +
'```\n' +
'```\n' +
'code block with missing closing back ticks\n'
}];
processor.$process(docs);
expect(log.warn).toHaveBeenCalledWith(
'checkUnbalancedBackTicks processor: unbalanced backticks found in rendered content - doc');
expect(docs[0].unbalancedBackTicks).toBe(true);
});
});

View File

@ -0,0 +1,11 @@
module.exports = function convertPrivateClassesToInterfacesProcessor(
convertPrivateClassesToInterfaces) {
return {
$runAfter: ['processing-docs'],
$runBefore: ['docs-processed'],
$process: function(docs) {
convertPrivateClassesToInterfaces(docs, false);
return docs;
}
};
};

View File

@ -0,0 +1,24 @@
var _ = require('lodash');
module.exports = function createOverviewDump() {
return {
$runAfter: ['processing-docs'],
$runBefore: ['docs-processed'],
$process: function(docs) {
var overviewDoc = {
id: 'overview-dump',
aliases: ['overview-dump'],
path: 'overview-dump',
outputPath: 'overview-dump.html',
modules: []
};
_.forEach(docs, function(doc) {
if (doc.docType === 'module') {
overviewDoc.modules.push(doc);
}
});
docs.push(overviewDoc);
}
};
};

View File

@ -0,0 +1,29 @@
var _ = require('lodash');
module.exports = function extractDecoratedClassesProcessor(EXPORT_DOC_TYPES) {
// Add the "directive" docType into those that can be exported from a module
EXPORT_DOC_TYPES.push('directive', 'pipe');
return {
$runAfter: ['processing-docs'],
$runBefore: ['docs-processed'],
decoratorTypes: ['Directive', 'Component', 'Pipe'],
$process: function(docs) {
var decoratorTypes = this.decoratorTypes;
_.forEach(docs, function(doc) {
_.forEach(doc.decorators, function(decorator) {
if (decoratorTypes.indexOf(decorator.name) !== -1) {
doc.docType = decorator.name.toLowerCase();
doc[doc.docType + 'Options'] = decorator.argumentInfo[0];
}
});
});
return docs;
}
};
};

View File

@ -0,0 +1,48 @@
var testPackage = require('../../helpers/test-package');
var Dgeni = require('dgeni');
describe('extractDecoratedClasses processor', function() {
var dgeni, injector, processor;
beforeEach(function() {
dgeni = new Dgeni([testPackage('angular.io-package')]);
injector = dgeni.configureInjector();
processor = injector.get('extractDecoratedClassesProcessor');
});
it('should extract specified decorator arguments', function() {
var doc1 = {
id: '@angular/common/ngFor',
name: 'ngFor',
docType: 'class',
decorators: [{
name: 'Directive',
arguments: ['{selector: \'[ng-for][ng-for-of]\', properties: [\'ngForOf\']}'],
argumentInfo: [{selector: '[ng-for][ng-for-of]', properties: ['ngForOf']}]
}]
};
var doc2 = {
id: '@angular/core/DecimalPipe',
name: 'DecimalPipe',
docType: 'class',
decorators:
[{name: 'Pipe', arguments: ['{name: \'number\'}'], argumentInfo: [{name: 'number'}]}]
};
processor.$process([doc1, doc2]);
expect(doc1).toEqual(jasmine.objectContaining({
id: '@angular/common/ngFor',
name: 'ngFor',
docType: 'directive',
directiveOptions: {selector: '[ng-for][ng-for-of]', properties: ['ngForOf']}
}));
expect(doc2).toEqual(jasmine.objectContaining({
id: '@angular/core/DecimalPipe',
name: 'DecimalPipe',
docType: 'pipe',
pipeOptions: {name: 'number'}
}));
});
});

View File

@ -0,0 +1,24 @@
var _ = require('lodash');
module.exports = function extractTitleFromGuides() {
return {
$runAfter: ['processing-docs'],
$runBefore: ['docs-processed'],
$process: function(docs) {
_(docs).forEach(function(doc) {
if (doc.docType === 'guide') {
doc.name = doc.name || getNameFromHeading(doc.description);
}
});
}
};
};
function getNameFromHeading(text) {
var match = /^\s*#\s*(.*)/.exec(text);
if (match) {
return match[1];
}
}

View File

@ -0,0 +1,7 @@
module.exports = function filterMemberDocs() {
return {
$runAfter: ['extra-docs-added'], $runBefore: ['computing-paths'], $process: function(docs) {
return docs.filter(function(doc) { return doc.docType !== 'member'; });
}
}
};

View File

@ -0,0 +1,139 @@
'use strict';
var fs = require('fs');
var path = require('canonical-path');
/**
* @dgProcessor generateKeywordsProcessor
* @description
* This processor extracts all the keywords from each document and creates
* a new document that will be rendered as a JavaScript file containing all
* this data.
*/
module.exports = function generateKeywordsProcessor(log, readFilesProcessor) {
return {
ignoreWordsFile: undefined,
propertiesToIgnore: [],
docTypesToIgnore: [],
outputFolder: '',
$validate: {
ignoreWordsFile: {},
docTypesToIgnore: {},
propertiesToIgnore: {},
outputFolder: {presence: true}
},
$runAfter: ['paths-computed'],
$runBefore: ['rendering-docs'],
$process: function(docs) {
// Keywords to ignore
var wordsToIgnore = [];
var propertiesToIgnore;
var docTypesToIgnore;
// Keywords start with "ng:" or one of $, _ or a letter
var KEYWORD_REGEX = /^((ng:|[$_a-z])[\w\-_]+)/;
// Load up the keywords to ignore, if specified in the config
if (this.ignoreWordsFile) {
var ignoreWordsPath = path.resolve(readFilesProcessor.basePath, this.ignoreWordsFile);
wordsToIgnore = fs.readFileSync(ignoreWordsPath, 'utf8').toString().split(/[,\s\n\r]+/gm);
log.debug('Loaded ignore words from "' + ignoreWordsPath + '"');
log.silly(wordsToIgnore);
}
propertiesToIgnore = convertToMap(this.propertiesToIgnore);
log.debug('Properties to ignore', propertiesToIgnore);
docTypesToIgnore = convertToMap(this.docTypesToIgnore);
log.debug('Doc types to ignore', docTypesToIgnore);
var ignoreWordsMap = convertToMap(wordsToIgnore);
// If the title contains a name starting with ng, e.g. "ngController", then add the module
// name
// without the ng to the title text, e.g. "controller".
function extractTitleWords(title) {
var match = /ng([A-Z]\w*)/.exec(title);
if (match) {
title = title + ' ' + match[1].toLowerCase();
}
return title;
}
function extractWords(text, words, keywordMap) {
var tokens = text.toLowerCase().split(/[.\s,`'"#]+/mg);
tokens.forEach(function(token) {
var match = token.match(KEYWORD_REGEX);
if (match) {
var key = match[1];
if (!keywordMap[key]) {
keywordMap[key] = true;
words.push(key);
}
}
});
}
// We are only interested in docs that live in the right area
const filteredDocs = docs.filter(function(doc) { return !docTypesToIgnore[doc.docType]; });
filteredDocs.forEach(function(doc) {
var words = [];
var keywordMap = Object.assign({}, ignoreWordsMap);
var members = [];
var membersMap = {};
// Search each top level property of the document for search terms
Object.keys(doc).forEach(function(key) {
const value = doc[key];
if (isString(value) && !propertiesToIgnore[key]) {
extractWords(value, words, keywordMap);
}
if (key === 'methods' || key === 'properties' || key === 'events') {
value.forEach(function(member) { extractWords(member.name, members, membersMap); });
}
});
doc.searchTerms = {
titleWords: extractTitleWords(doc.name),
keywords: words.sort().join(' '),
members: members.sort().join(' ')
};
});
var searchData =
filteredDocs.filter(function(page) { return page.searchTerms; }).map(function(page) {
return Object.assign(
{path: page.path, title: page.name, type: page.docType}, page.searchTerms);
});
docs.push({
docType: 'json-doc',
id: 'search-data-json',
template: 'json-doc.template.json',
path: this.outputFolder + '/search-data.json',
outputPath: this.outputFolder + '/search-data.json',
data: searchData
});
}
};
};
function isString(value) {
return typeof value == 'string';
}
function convertToMap(collection) {
const obj = {};
collection.forEach(key => { obj[key] = true; });
return obj;
}

View File

@ -0,0 +1,49 @@
module.exports = function generateNavigationDoc() {
return {
$runAfter: ['extra-docs-added'],
$runBefore: ['rendering-docs'],
outputFolder: '',
$validate: {outputFolder: {presence: true}},
$process: function(docs) {
var modulesDoc = {
docType: 'data-module',
value: {api: {sections: []}, guide: {pages: []}},
path: this.outputFolder + '/navigation',
outputPath: this.outputFolder + '/navigation.ts',
serviceName: 'NAVIGATION'
};
docs.forEach(function(doc) {
if (doc.docType === 'module') {
var moduleNavItem =
{path: doc.path, partial: doc.outputPath, name: doc.id, type: 'module', pages: []};
modulesDoc.value.api.sections.push(moduleNavItem);
doc.exports.forEach(function(exportDoc) {
if (!exportDoc.internal) {
var exportNavItem = {
path: exportDoc.path,
partial: exportDoc.outputPath,
name: exportDoc.name,
type: exportDoc.docType
};
moduleNavItem.pages.push(exportNavItem);
}
});
}
});
docs.forEach(function(doc) {
if (doc.docType === 'guide') {
console.log('guide', doc.name);
var guideDoc = {path: doc.path, partial: doc.outputPath, name: doc.name, type: 'guide'};
modulesDoc.value.guide.pages.push(guideDoc);
}
});
docs.push(modulesDoc);
}
};
};

View File

@ -0,0 +1,62 @@
var _ = require('lodash');
/**
* @dgProcessor
* @description
*
*/
module.exports = function matchUpDirectiveDecoratorsProcessor(aliasMap) {
return {
$runAfter: ['ids-computed', 'paths-computed'],
$runBefore: ['rendering-docs'],
decoratorMappings: {'Inputs': 'inputs', 'Outputs': 'outputs'},
$process: function(docs) {
var decoratorMappings = this.decoratorMappings;
_.forEach(docs, function(doc) {
if (doc.docType === 'directive') {
doc.selector = doc.directiveOptions.selector;
for (decoratorName in decoratorMappings) {
var propertyName = decoratorMappings[decoratorName];
doc[propertyName] =
getDecoratorValues(doc.directiveOptions[propertyName], decoratorName, doc.members);
}
}
});
}
};
};
function getDecoratorValues(classDecoratorValues, memberDecoratorName, members) {
var optionMap = {};
var decoratorValues = {};
// Parse the class decorator
_.forEach(classDecoratorValues, function(option) {
// Options are of the form: "propName : bindingName" (bindingName is optional)
var optionPair = option.split(':');
var propertyName = optionPair.shift().trim();
var bindingName = (optionPair.shift() || '').trim() || propertyName;
decoratorValues[propertyName] = {propertyName: propertyName, bindingName: bindingName};
});
_.forEach(members, function(member) {
_.forEach(member.decorators, function(decorator) {
if (decorator.name === memberDecoratorName) {
decoratorValues[member.name] = {
propertyName: member.name,
bindingName: decorator.arguments[0] || member.name
};
}
});
if (decoratorValues[member.name]) {
decoratorValues[member.name].memberDoc = member;
}
});
if (Object.keys(decoratorValues).length) {
return decoratorValues;
}
}

View File

@ -0,0 +1,97 @@
var _ = require('lodash');
module.exports = function mergeDecoratorDocs() {
return {
$runAfter: ['processing-docs'],
$runBefore: ['docs-processed'],
docsToMergeInfo: [
{nameTemplate: _.template('${name}Decorator'), decoratorProperty: 'decoratorInterfaceDoc'}, {
nameTemplate: _.template('${name}Metadata'),
decoratorProperty: 'metadataDoc',
useFields: ['howToUse', 'whatItDoes']
},
{nameTemplate: _.template('${name}MetadataType'), decoratorProperty: 'metadataInterfaceDoc'},
{
nameTemplate: _.template('${name}MetadataFactory'),
decoratorProperty: 'metadataFactoryDoc'
}
],
$process: function(docs) {
var docsToMergeInfo = this.docsToMergeInfo;
var docsToMerge = Object.create(null);
docs.forEach(function(doc) {
// find all the decorators, signified by a call to `makeDecorator(metadata)`
var makeDecorator = getMakeDecoratorCall(doc);
if (makeDecorator) {
doc.docType = 'decorator';
// get the type of the decorator metadata
doc.decoratorType = makeDecorator.arguments[0].text;
// clear the symbol type named (e.g. ComponentMetadataFactory) since it is not needed
doc.symbolTypeName = undefined;
// keep track of the docs that need to be merged into this decorator doc
docsToMergeInfo.forEach(function(info) {
docsToMerge[info.nameTemplate({name: doc.name})] = {
decoratorDoc: doc,
property: info.decoratorProperty
};
});
}
});
// merge the metadata docs into the decorator docs
docs = docs.filter(function(doc) {
if (docsToMerge[doc.name]) {
var decoratorDoc = docsToMerge[doc.name].decoratorDoc;
var property = docsToMerge[doc.name].property;
var useFields = docsToMerge[doc.name].useFields;
// attach this document to its decorator
decoratorDoc[property] = doc;
// Copy over fields from the merged doc if specified
if (useFields) {
useFields.forEach(function(field) { decoratorDoc[field] = doc[field]; });
}
// remove doc from its module doc's exports
doc.moduleDoc.exports =
doc.moduleDoc.exports.filter(function(exportDoc) { return exportDoc !== doc; });
// remove from the overall list of docs to be rendered
return false;
}
return true;
});
}
};
};
function getMakeDecoratorCall(doc, type) {
var makeDecoratorFnName = 'make' + (type || '') + 'Decorator';
var initializer = doc.exportSymbol && doc.exportSymbol.valueDeclaration &&
doc.exportSymbol.valueDeclaration.initializer;
if (initializer) {
// There appear to be two forms of initializer:
// export var Injectable: InjectableFactory =
// <InjectableFactory>makeDecorator(InjectableMetadata);
// and
// export var RouteConfig: (configs: RouteDefinition[]) => ClassDecorator =
// makeDecorator(RouteConfigAnnotation);
// In the first case, the type assertion `<InjectableFactory>` causes the AST to contain an
// extra level of expression
// to hold the new type of the expression.
if (initializer.expression && initializer.expression.expression) {
initializer = initializer.expression;
}
if (initializer.expression && initializer.expression.text === makeDecoratorFnName) {
return initializer;
}
}
}

View File

@ -0,0 +1,61 @@
var testPackage = require('../../helpers/test-package');
var Dgeni = require('dgeni');
describe('mergeDecoratorDocs processor', function() {
var dgeni, injector, processor, decoratorDoc, otherDoc;
beforeEach(function() {
dgeni = new Dgeni([testPackage('angular.io-package')]);
injector = dgeni.configureInjector();
processor = injector.get('mergeDecoratorDocs');
decoratorDoc = {
name: 'X',
docType: 'var',
exportSymbol: {
valueDeclaration: {
initializer: {expression: {text: 'makeDecorator'}, arguments: [{text: 'XMetadata'}]}
}
}
};
decoratorDocWithTypeAssertion = {
name: 'Y',
docType: 'var',
exportSymbol: {
valueDeclaration: {
initializer: {
expression: {
type: {},
expression: {text: 'makeDecorator'},
arguments: [{text: 'YMetadata'}]
}
}
}
}
};
otherDoc = {
name: 'Y',
docType: 'var',
exportSymbol: {
valueDeclaration:
{initializer: {expression: {text: 'otherCall'}, arguments: [{text: 'param1'}]}}
}
};
});
it('should change the docType of only the docs that are initialied by a call to makeDecorator',
function() {
processor.$process([decoratorDoc, decoratorDocWithTypeAssertion, otherDoc]);
expect(decoratorDoc.docType).toEqual('decorator');
expect(decoratorDocWithTypeAssertion.docType).toEqual('decorator');
expect(otherDoc.docType).toEqual('var');
});
it('should extract the "type" of the decorator meta data', function() {
processor.$process([decoratorDoc, decoratorDocWithTypeAssertion, otherDoc]);
expect(decoratorDoc.decoratorType).toEqual('XMetadata');
expect(decoratorDocWithTypeAssertion.decoratorType).toEqual('YMetadata');
});
});