chore(docs): initial dgeni docs generation

Closes #261
This commit is contained in:
Peter Bacon Darwin 2014-12-04 14:02:03 +00:00
parent 983c00c495
commit 27e03591dd
28 changed files with 1515 additions and 1 deletions

3
.gitignore vendored
View File

@ -19,3 +19,6 @@ packages
pubspec.lock pubspec.lock
.c9 .c9
.idea/ .idea/
docs/bower_components/

405
docs/app/css/app.css Normal file
View File

@ -0,0 +1,405 @@
.hide { display: none !important; }
body {
overflow: hidden;
max-width: 100%;
max-height: 100%;
font-size: 14px;
}
a {
color: #3f51b5;
}
table {
margin-bottom: 20px;
max-width: 100%;
width: 100%;
border-spacing: 0;
border-collapse: collapse;
background-color: transparent;
}
td,
th {
padding: $baseline-grid ($baseline-grid * 2);
border-top: 1px solid #ddd;
vertical-align: top;
}
th {
border-bottom: 2px solid #ddd;
vertical-align: bottom;
}
pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
}
pre > code.highlight {
padding: 10px;
font-weight: 400;
-webkit-user-select: initial;
-moz-user-select: initial;
-ms-user-select: initial;
user-select: initial;
}
pre, code {
margin: 0;
padding: 0;
overflow-wrap: break-word;
font-family: monospace, serif;
}
pre > code.highlight {
padding: 10px;
font-weight: 400;
}
code {
font-size: 14px;
background: #f6f6f6;
}
code.highlight {
display: block;
overflow-wrap: break-word;
}
code:not(.highlight) {
color: #4285f4;
margin-left: 1px;
margin-right: 1px;
}
.md-sidenav-inner {
background: #fff;
}
.layout-content,
.doc-content {
max-width: 864px;
margin: auto;
}
.layout-label {
width: 120px;
}
.layout-content code.highlight {
margin-bottom: 15px;
}
.menu-item {
background: none;
border-width: 0;
cursor: pointer;
display: block;
color: #333;
font-size: inherit;
line-height: 40px;
max-height: 40px;
opacity: 1;
margin: 0;
outline: none;
padding: 0px 28px;
position: relative;
text-align: left;
text-decoration: none;
width: 100%;
z-index: 1;
-webkit-transition: 0.45s cubic-bezier(0.35, 0, 0.25, 1);
-webkit-transition-property: max-height, background-color, opacity;
-moz-transition: 0.45s cubic-bezier(0.35, 0, 0.25, 1);
-moz-transition-property: max-height, background-color, opacity;
transition: 0.45s cubic-bezier(0.35, 0, 0.25, 1);
transition-property: max-height, background-color, opacity;
}
.menu-item.ng-hide {
max-height: 0;
opacity: 0;
}
.menu-item:hover {
color: #999;
}
.menu-item:focus {
font-weight: bold;
}
.menu-item.menu-title {
color: #888;
font-size: 14px;
padding-left: 16px;
text-align: left;
text-transform: uppercase;
transition: color 0.35s cubic-bezier(0.35, 0, 0.25, 1);
}
.menu-item.menu-title:hover,
.menu-item.menu-title.active {
color: #1976d2;
}
.menu-icon {
background: none;
border: none;
}
.app-toolbar .md-toolbar-tools h3 {
-webkit-margin-before: 0;
-webkit-margin-after: 0;
}
.demo-container {
border-radius: 4px;
margin-top: 16px;
-webkit-transition: 0.02s padding cubic-bezier(0.35, 0, 0.25, 1);
transition: 0.02s padding cubic-bezier(0.35, 0, 0.25, 1);
position: relative;
padding-bottom: 0;
}
.demo-source-tabs {
z-index: 1;
-webkit-transition: all 0.45s cubic-bezier(0.35, 0, 0.25, 1);
transition: all 0.45s cubic-bezier(0.35, 0, 0.25, 1);
max-height: 448px;
min-height: 448px;
background: #fff;
overflow: hidden;
}
md-tabs.demo-source-tabs md-tab,
md-tabs.demo-source-tabs .md-header {
background-color: #444444 !important;
}
md-tabs.demo-source-tabs md-tab-label {
color: #ccc !important;
}
md-tabs.demo-source-tabs .active md-tab-label {
color: #fff !important;
}
.demo-source-tabs.ng-hide {
max-height: 0px;
min-height: 0px;
}
.demo-source-tabs {
position: relative;
width: 100%;
z-index: 0;
}
.demo-content {
position: relative;
overflow:hidden;
min-height: 448px;
display: -webkit-box;
display: -webkit-flex;
display: -moz-box;
display: -moz-flex;
display: -ms-flexbox;
display: flex;
}
.small-demo .demo-source-tabs:not(.ng-hide) {
min-height: 224px;
max-height: 224px;
}
.small-demo .demo-content {
min-height: 128px;
}
.demo-content > * {
-webkit-box-flex: 1;
-webkit-flex: 1;
-moz-box-flex: 1;
-moz-flex: 1;
-ms-flex: 1;
flex: 1;
}
.demo-content > div[layout-fill] {
min-height: 448px;
}
.small-demo .demo-content > div[layout-fill] {
min-height: 224px;
}
.small-demo .demo-toolbar,
.small-demo .md-toolbar-tools {
min-height: 48px;
max-height: 48px;
font-size: 20px;
}
.show-source md-toolbar.demo-toolbar {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.36);
}
.demo-toolbar .md-button {
color: #616161;
}
md-toolbar.demo-toolbar,
.demo-source-tabs md-tab,
.demo-source-tabs .tabs-header {
background: #E0E0E0 !important;
color: #616161;
}
md-toolbar.demo-toolbar md-tab-label {
color: #99E4EE
}
md-toolbar.demo-toolbar .md-button:hover,
md-toolbar.demo-toolbar .md-button:focus,
md-toolbar.demo-toolbar .md-button.active {
background: rgba(0,0,0,0.1);
}
md-toolbar.demo-toolbar .md-button {
-webkit-transition: all 0.3s linear;
-moz-transition: all 0.3s linear;
transition: all 0.3s linear;
}
.demo-source-container {
display: block;
border: 1px solid #ddd;
background-color: #f6f6f6;
height: 400px;
}
.demo-source-content {
height: 400px;
}
.demo-source-content,
.demo-source-content pre,
.demo-source-content code {
background: #f6f6f6;
font-family: monospace;
}
.demo-source-content pre {
max-width: 100%;
overflow-wrap: break-word;
}
.show-source div[demo-include] {
border-top: #ddd solid 2px;
}
.menu-separator-icon {
margin: 0;
}
.menu-module-name {
opacity: 0.6;
font-size: 18px;
}
/************
* DOCS
************/
.api-options-bar .md-button {
margin: 4px;
padding: 4px;
}
.api-options-bar .md-button:hover,
.api-options-bar .md-button:focus {
background: rgba(0, 0, 0, 0.2);
}
.api-options-bar.with-icon md-icon {
position: absolute;
top: -3px;
left: 2px;
}
.api-options-bar.with-icon .md-button span {
margin-left: 22px;
}
.api-params-item {
min-height: 72px;
border-bottom: 1px solid #ddd;
}
.api-params-label {
margin-right: 8px;
text-align: center;
margin-top: 14px;
-webkit-align-self: flex-start;
-moz-align-self: flex-start;
-ms-flex-item-align: start;
align-self: flex-start;
}
.api-params-title {
color: #888;
}
code.api-type {
font-weight: bold;
}
ul {
margin: 0;
}
ul li {
margin-top: 3px;
list-style-position: inside;
}
ul li:first-child {
margin-top: 0;
}
.layout-title {
color: #999999;
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
}
.api-params-content ul {
padding-left: 4px;
}
ul.methods > li {
margin-bottom: 48px;
}
ul.methods .method-function-syntax {
font-weight: normal;
font-size: 20px;
margin: 0;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
}
ul.methods li h3 {
/* border-bottom: 1px solid #eee; */
}
@media (max-width: 600px) {
ul.methods > li {
padding-left: 0;
border-left: none;
list-style: default;
}
ul.methods .method-function-syntax {
font-size: 14px;
}
}
.version {
padding-left: 10px;
text-decoration: underline;
font-size: 0.95em;
}
.demo-source-container pre,
.demo-source-container code {
min-height: 100%;
}
md-content.demo-source-container > hljs > pre > code.highlight {
position : absolute;
top : 0px;
left: 0px;
right: 0px;
}
.extraPad {
padding-left:32px !important;
padding-right:32px !important;
}

51
docs/app/index.html Normal file
View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<title>Angular 2 Docs</title>
<base href="/">
<link rel="stylesheet" type="text/css" href="/lib/angular-material/angular-material.css">
<link rel="stylesheet" type="text/css" href="/css/app.css">
<script src="/lib/hammerjs/hammer.js"></script>
<script src="/lib/angular/angular.js"></script>
<script src="/lib/angular-animate/angular-animate.js"></script>
<script src="/lib/angular-aria/angular-aria.js"></script>
<script src="/lib/angular-material/angular-material.js"></script>
<script src="/js/navigation-modules.js"></script>
<script src="/js/navigation-guides.js"></script>
<script src="/js/app.js"></script>
</head>
<body ng-app="app" ng-controller="NavController as nav" layout="column">
<md-toolbar md-scroll-shrink>
<h1 class="md-toolbar-tools">Angular V2</h1>
</md-toolbar>
<section layout="row">
<md-content>
<h2>Navigation</h2>
<section ng-repeat="area in nav.areas">
<h3>{{ area.name }}</h3>
<md-list>
<md-item ng-repeat="section in area.sections">
<h3><a href="{{section.path}}">{{section.name}}</a></h3>
<ul>
<li ng-repeat="page in section.pages">
<a href="{{page.path}}">{{ page.name }}</a>
</li>
</ul>
</md-item>
</md-list>
</section>
</md-content>
<md-content class="md-padding">
<ng-include src="nav.currentPage.partial"></ng-include>
</md-content>
</section>
</body>
</html>

47
docs/app/js/app.js Normal file
View File

@ -0,0 +1,47 @@
angular.module('app', ['ngMaterial', 'navigation-modules', 'navigation-guides'])
.config(function($locationProvider) {
$locationProvider.html5Mode(true);
})
.controller('NavController', ['$scope', '$location', 'MODULES', 'GUIDES',
function($scope, $location, MODULES, GUIDES) {
var that = this;
this.areas = [
{ name: 'Guides', sections: [ { pages: GUIDES.pages } ] },
{ name: 'Modules', sections: MODULES.sections }
];
this.updateCurrentPage = function(path) {
path = path.replace(/^\//, '');
console.log('path', path);
this.currentPage = null;
this.areas.forEach(function(area) {
area.sections.forEach(function(section) {
// Short-circuit out if the page has been found
if ( that.currentPage ) {
return;
}
if (section.path === path) {
console.log('found!');
that.currentPage = section;
} else {
section.pages.forEach(function(page) {
if (page.path === path) {
that.currentPage = page;
}
});
}
});
});
};
$scope.$watch(
function getLocationPath() { return $location.path(); },
function handleLocationPathChange(path) { that.updateCurrentPage(path); }
);
}]);

19
docs/bower.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "angular-docs",
"main": "index.js",
"version": "0.0.0",
"homepage": "https://github.com/angular/angular",
"authors": [],
"license": "Apache-2.0",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"angular-material": "~0.6.0"
}
}

138
docs/dgeni-package/index.js Normal file
View File

@ -0,0 +1,138 @@
var Package = require('dgeni').Package;
var jsdocPackage = require('dgeni-packages/jsdoc');
var nunjucksPackage = require('dgeni-packages/nunjucks');
var path = require('canonical-path');
var PARTIAL_PATH = 'partials';
var MODULES_DOCS_PATH = PARTIAL_PATH + '/modules';
var GUIDES_PATH = PARTIAL_PATH + '/guides';
// Define the dgeni package for generating the docs
module.exports = new Package('angular', [jsdocPackage, nunjucksPackage])
// Register the services and file readers
.factory(require('./services/atParser'))
.factory(require('./services/getJSDocComment'))
.factory(require('./services/ExportTreeVisitor'))
.factory(require('./readers/atScript'))
.factory(require('./readers/ngdoc'))
// Register the processors
.processor(require('./processors/generateDocsFromComments'))
.processor(require('./processors/processModuleDocs'))
.processor(require('./processors/processClassDocs'))
.processor(require('./processors/generateNavigationDoc'))
// Configure the log service
.config(function(log) {
log.level = 'info';
})
// Configure file reading
.config(function(readFilesProcessor, atScriptFileReader, ngdocFileReader) {
readFilesProcessor.fileReaders = [atScriptFileReader, ngdocFileReader];
readFilesProcessor.basePath = path.resolve(__dirname, '../..');
readFilesProcessor.sourceFiles = [
{ include: 'modules/*/src/**/*.js', basePath: 'modules' },
{ include: 'modules/*/docs/**/*.md', basePath: 'modules' },
{ include: 'docs/content/**/*.md', basePath: 'docs/content' }
];
})
// Configure file writing
.config(function(writeFilesProcessor) {
writeFilesProcessor.outputFolder = 'build/docs';
})
// Configure rendering
.config(function(templateFinder, templateEngine) {
// Nunjucks and Angular conflict in their template bindings so change Nunjucks
templateEngine.config.tags = {
variableStart: '{$',
variableEnd: '$}'
};
templateFinder.templateFolders
.unshift(path.resolve(__dirname, 'templates'));
templateFinder.templatePatterns = [
'${ doc.template }',
'${ doc.id }.${ doc.docType }.template.html',
'${ doc.id }.template.html',
'${ doc.docType }.template.html',
'common.template.html'
];
})
// Configure ids and paths
.config(function(computeIdsProcessor, computePathsProcessor) {
computeIdsProcessor.idTemplates.push({
docTypes: [
'class',
'function',
'NAMED_EXPORT',
'VARIABLE_STATEMENT'
],
idTemplate: '${moduleDoc.id}.${name}',
getAliases: function(doc) { return [doc.id]; }
});
computeIdsProcessor.idTemplates.push({
docTypes: ['member'],
idTemplate: '${classDoc.id}.${name}',
getAliases: function(doc) { return [doc.id]; }
});
computeIdsProcessor.idTemplates.push({
docTypes: ['guide'],
getId: function(doc) {
return doc.fileInfo.relativePath
// path should be relative to `modules` folder
.replace(/.*\/?modules\//, '')
// path should not include `/docs/`
.replace(/\/docs\//, '/')
// path should not have a suffix
.replace(/\.\w*$/, '');
},
getAliases: function(doc) { return [doc.id]; }
});
computePathsProcessor.pathTemplates.push({
docTypes: ['module'],
pathTemplate: '${id}',
outputPathTemplate: MODULES_DOCS_PATH + '/${id}/index.html'
});
computePathsProcessor.pathTemplates.push({
docTypes: [
'class',
'function',
'NAMED_EXPORT',
'VARIABLE_STATEMENT'
],
pathTemplate: '${moduleDoc.path}/${name}',
outputPathTemplate: MODULES_DOCS_PATH + '/${path}/index.html'
});
computePathsProcessor.pathTemplates.push({
docTypes: ['member'],
pathTemplate: '${classDoc.path}/${name}',
getOutputPath: function() {} // These docs are not written to their own file, instead they are part of their class doc
});
computePathsProcessor.pathTemplates.push({
docTypes: ['guide'],
pathTemplate: '${id}',
outputPathTemplate: GUIDES_PATH + '/${id}.html'
});
});

View File

@ -0,0 +1,10 @@
var Package = require('dgeni').Package;
module.exports = function mockPackage() {
return new Package('mockPackage', [require('../')])
// provide a mock log service
.factory('log', function() { return require('dgeni/lib/mocks/log')(false); });
};

View File

@ -0,0 +1,34 @@
var _ = require('lodash');
module.exports = function generateDocsFromComments(log) {
return {
$runAfter: ['files-read'],
$runBefore: ['parsing-tags'],
$process: function(docs) {
var commentDocs = [];
docs = _.filter(docs, function(doc) {
if (doc.docType !== 'atScriptFile') {
return true;
} else {
_.forEach(doc.fileInfo.comments, function(comment) {
// we need to check for `/**` at the start of the comment to find all the jsdoc style comments
comment.range.toString().replace(/^\/\*\*([\w\W]*)\*\/$/g, function(match, commentBody) {
// Create a doc from this comment
commentDocs.push({
fileInfo: doc.fileInfo,
startingLine: comment.range.start.line,
endingLine: comment.range.end.line,
content: commentBody,
codeTree: comment.treeAfter,
docType: 'atScriptDoc'
});
});
});
}
});
return docs.concat(commentDocs);
}
};
};

View File

@ -0,0 +1,66 @@
var _ = require('lodash');
module.exports = function generateNavigationDoc() {
return {
$runAfter: ['docs-processed'],
$runBefore: ['rendering-docs'],
$process: function(docs) {
var modulesDoc = {
value: { sections: [] },
moduleName: 'navigation-modules',
serviceName: 'MODULES',
template: 'data-module.template.js',
outputPath: 'js/navigation-modules.js'
};
_.forEach(docs, function(doc) {
if ( doc.docType === 'module' ) {
var moduleNavItem = {
path: doc.path,
partial: doc.outputPath,
name: doc.id,
type: 'module',
pages: []
};
modulesDoc.value.sections.push(moduleNavItem);
_.forEach(doc.exports, function(exportDoc) {
var exportNavItem = {
path: exportDoc.path,
partial: exportDoc.outputPath,
name: exportDoc.name,
type: exportDoc.docType
};
moduleNavItem.pages.push(exportNavItem);
});
}
});
docs.push(modulesDoc);
var guidesDoc = {
value: { pages: [] },
moduleName: 'navigation-guides',
serviceName: 'GUIDES',
template: 'data-module.template.js',
outputPath: 'js/navigation-guides.js'
};
_.forEach(docs, function(doc) {
if ( doc.docType === 'guide' ) {
var guideDoc = {
path: doc.path,
partial: doc.outputPath,
name: doc.id,
type: 'guide'
};
guidesDoc.value.pages.push(guideDoc)
}
});
docs.push(guidesDoc);
}
};
};

View File

@ -0,0 +1,42 @@
var _ = require('lodash');
module.exports = function processClassDocs(log, getJSDocComment) {
return {
$runAfter: ['processModuleDocs'],
$runBefore: ['parsing-tags', 'generateDocsFromComments'],
$process: function(docs) {
var memberDocs = [];
_.forEach(docs, function(classDoc) {
if ( classDoc.docType === 'class' ) {
classDoc.members = [];
// Create a new doc for each member of the class
_.forEach(classDoc.elements, function(memberDoc) {
classDoc.members.push(memberDoc);
memberDocs.push(memberDoc);
memberDoc.docType = 'member';
memberDoc.classDoc = classDoc;
memberDoc.name = memberDoc.name.literalToken.value;
if (memberDoc.commentBefore ) {
// If this export has a comment, remove it from the list of
// comments collected in the module
var index = classDoc.moduleDoc.comments.indexOf(memberDoc.commentBefore);
if ( index !== -1 ) {
classDoc.moduleDoc.comments.splice(index, 1);
}
_.assign(memberDoc, getJSDocComment(memberDoc.commentBefore));
}
});
}
});
return docs.concat(memberDocs);
}
};
};

View File

@ -0,0 +1,46 @@
var _ = require('lodash');
module.exports = function processModuleDocs(log, ExportTreeVisitor, getJSDocComment) {
return {
$runAfter: ['files-read'],
$runBefore: ['parsing-tags', 'generateDocsFromComments'],
$process: function(docs) {
var exportDocs = [];
_.forEach(docs, function(doc) {
if ( doc.docType === 'module' ) {
log.debug('processing', doc.moduleTree.moduleName);
doc.exports = [];
if ( doc.moduleTree.visit ) {
var visitor = new ExportTreeVisitor();
visitor.visit(doc.moduleTree);
_.forEach(visitor.exports, function(exportDoc) {
doc.exports.push(exportDoc);
exportDocs.push(exportDoc);
exportDoc.moduleDoc = doc;
if (exportDoc.comment) {
// If this export has a comment, remove it from the list of
// comments collected in the module
var index = doc.comments.indexOf(exportDoc.comment);
if ( index !== -1 ) {
doc.comments.splice(index, 1);
}
_.assign(exportDoc, getJSDocComment(exportDoc.comment));
}
});
}
}
});
return docs.concat(exportDocs);
}
};
};

View File

@ -0,0 +1,30 @@
var path = require('canonical-path');
/**
* @dgService atScriptFileReader
* @description
* This file reader will create a simple doc for each
* file including a code AST of the AtScript in the file.
*/
module.exports = function atScriptFileReader(log, atParser) {
var reader = {
name: 'atScriptFileReader',
defaultPattern: /\.js$/,
getDocs: function(fileInfo) {
var moduleDoc = atParser.parseModule(fileInfo);
moduleDoc.docType = 'module';
moduleDoc.id = moduleDoc.moduleTree.moduleName;
moduleDoc.aliases = [moduleDoc.id];
// Readers return a collection of docs read from the file
// but in this read there is only one document (module) to return
return [moduleDoc];
}
};
return reader;
};

View File

@ -0,0 +1,55 @@
var mockPackage = require('../mocks/mockPackage');
var Dgeni = require('dgeni');
describe('atScript file reader', function() {
var dgeni, injector, reader;
var fileContent =
'import {CONST} from "facade/lang";\n' +
'\n' +
'/**\n' +
'* A parameter annotation that creates a synchronous eager dependency.\n' +
'*\n' +
'* class AComponent {\n' +
'* constructor(@Inject("aServiceToken") aService) {}\n' +
'* }\n' +
'*\n' +
'*/\n' +
'export class Inject {\n' +
'token;\n' +
'@CONST()\n' +
'constructor(token) {\n' +
'this.token = token;\n' +
'}\n' +
'}';
beforeEach(function() {
dgeni = new Dgeni([mockPackage()]);
injector = dgeni.configureInjector();
reader = injector.get('atScriptFileReader');
});
it('should provide a default pattern', function() {
expect(reader.defaultPattern).toEqual(/\.js$/);
});
it('should parse the file using the atParser and return a single doc', function() {
var atParser = injector.get('atParser');
spyOn(atParser, 'parseModule').and.callThrough();
var docs = reader.getDocs({
content: fileContent,
relativePath: 'di/src/annotations.js'
});
expect(atParser.parseModule).toHaveBeenCalled();
expect(docs.length).toEqual(1);
expect(docs[0].docType).toEqual('module');
});
});

View File

@ -0,0 +1,32 @@
var path = require('canonical-path');
/**
* @dgService ngdocFileReader
* @description
* This file reader will pull the contents from a text file (by default .ngdoc)
*
* The doc will initially have the form:
* ```
* {
* content: 'the content of the file',
* startingLine: 1
* }
* ```
*/
module.exports = function ngdocFileReader() {
var reader = {
name: 'ngdocFileReader',
defaultPattern: /\.md$/,
getDocs: function(fileInfo) {
// We return a single element array because ngdoc files only contain one document
return [{
docType: 'guide',
content: fileInfo.content,
startingLine: 1
}];
}
};
return reader;
};

View File

@ -0,0 +1,45 @@
var ngdocFileReaderFactory = require('./ngdoc');
var path = require('canonical-path');
describe('ngdocFileReader', function() {
var fileReader;
var createFileInfo = function(file, content, basePath) {
return {
fileReader: fileReader.name,
filePath: file,
baseName: path.basename(file, path.extname(file)),
extension: path.extname(file).replace(/^\./, ''),
basePath: basePath,
relativePath: path.relative(basePath, file),
content: content
};
};
beforeEach(function() {
fileReader = ngdocFileReaderFactory();
});
describe('defaultPattern', function() {
it('should match .md files', function() {
expect(fileReader.defaultPattern.test('abc.md')).toBeTruthy();
expect(fileReader.defaultPattern.test('abc.js')).toBeFalsy();
});
});
describe('getDocs', function() {
it('should return an object containing info about the file and its contents', function() {
var fileInfo = createFileInfo('project/path/modules/someModule/foo/docs/subfolder/bar.ngdoc', 'A load of content', 'project/path');
expect(fileReader.getDocs(fileInfo)).toEqual([{
docType: 'guide',
content: 'A load of content',
startingLine: 1
}]);
});
});
});

View File

@ -0,0 +1,109 @@
var traceur = require('traceur/src/node/traceur.js');
var ParseTreeVisitor = System.get("traceur@0.0.74/src/syntax/ParseTreeVisitor").ParseTreeVisitor;
module.exports = function ExportTreeVisitor(log) {
function ExportTreeVisitorImpl() {
ParseTreeVisitor.call(this);
}
ExportTreeVisitorImpl.prototype = {
__proto__: ParseTreeVisitor.prototype,
visitExportDeclaration: function(tree) {
// We are entering an export declaration - create an object to track it
this.currentExport = {
comment: tree.commentBefore,
location: tree.location
};
log.silly('enter', tree.type, tree.commentBefore ? 'has comment' : '');
ParseTreeVisitor.prototype.visitExportDeclaration.call(this, tree);
log.silly('exit', this.currentExport);
// We are exiting the export declaration - store the export object
this.exports.push(this.currentExport);
this.currentExport = null;
},
visitVariableStatement: function(tree) {
if ( this.currentExport ) {
this.updateExport(tree);
this.currentExport.name = "VARIABLE_STATEMENT";
}
},
visitVariableDeclaration: function(tree) {
if ( this.currentExport ) {
this.updateExport(tree);
this.currentExport.name = tree.lvalue;
this.currentExport.variableDeclaration = tree;
}
},
visitFunctionDeclaration: function(tree) {
if ( this.currentExport ) {
this.updateExport(tree);
this.currentExport.name = tree.name.identifierToken.value;
this.currentExport.functionKind = tree.functionKind;
this.currentExport.parameters = tree.parameterList.parameters;
this.currentExport.typeAnnotation = tree.typeAnnotation;
this.currentExport.annotations = tree.annotations;
this.currentExport.docType = 'function';
log.silly(tree.type, tree.commentBefore ? 'has comment' : '');
}
},
visitClassDeclaration: function(tree) {
if ( this.currentExport ) {
this.updateExport(tree);
this.currentExport.name = tree.name.identifierToken.value;
this.currentExport.superClass = tree.superClass;
this.currentExport.annotations = tree.annotations;
this.currentExport.elements = tree.elements;
this.currentExport.docType = 'class';
}
},
visitAsyncFunctionDeclaration: function(tree) {
if ( this.currentExport ) {
this.updateExport(tree);
}
},
visitExportDefault: function(tree) {
if ( this.currentExport ) {
this.updateExport(tree);
this.currentExport.name = 'DEFAULT';
this.currentExport.defaultExport = tree;
// Default exports are either classes, functions or expressions
// So we let the super class continue down...
ParseTreeVisitor.prototype.visitExportDefault.call(this, tree);
}
},
visitNamedExport: function(tree) {
if ( this.currentExport ) {
this.updateExport(tree);
this.currentExport.namedExport = tree;
this.currentExport.name = 'NAMED_EXPORT';
// TODO: work out this bit!!
// We need to cope with any export specifiers in the named export
}
},
// TODO - if the export is an expression, find the thing that is being
// exported and use it and its comments for docs
updateExport: function(tree) {
this.currentExport.comment = this.currentExport.comment || tree.commentBefore;
this.currentExport.docType = tree.type;
},
visit: function(tree) {
this.exports = [];
ParseTreeVisitor.prototype.visit.call(this, tree);
}
};
return ExportTreeVisitorImpl;
};

View File

@ -0,0 +1,97 @@
var traceur = require('traceur/src/node/traceur.js');
var ParseTreeVisitor = System.get(System.map.traceur + '/src/syntax/ParseTreeVisitor').ParseTreeVisitor;
var file2modulename = require('../../../file2modulename');
/**
* Wrapper around traceur that can parse the contents of a file
*/
module.exports = function atParser(log) {
var service = {
/**
* The options to pass to traceur
*/
traceurOptions: {
annotations: true, // parse annotations
types: true, // parse types
memberVariables: true, // parse class fields
commentCallback: true // handle comments
},
/**
* Parse a module AST from the contents of a file.
* @param {Object} fileInfo information about the file to parse
* @return { { moduleTree: Object, comments: Array } } An object containing the parsed module
* AST and an array of all the comments found in the file
*/
parseModule: parseModule
};
return service;
// Parse the contents of the file using traceur
function parseModule(fileInfo) {
var moduleName = file2modulename(fileInfo.relativePath);
var sourceFile = new traceur.syntax.SourceFile(moduleName, fileInfo.content);
var parser = new traceur.syntax.Parser(sourceFile);
var comments = [];
var moduleTree;
// Configure the parser
parser.handleComment = function(range) {
comments.push({ range: range });
};
traceur.options.setFromObject(service.traceurOptions);
try {
// Parse the file as a module, attaching the comments
moduleTree = parser.parseModule();
attachComments(moduleTree, comments);
} catch(ex) {
// HACK: sometime traceur crashes for various reasons including
// Not Yet Implemented (NYI)!
log.error(ex.stack);
moduleTree = {};
}
log.debug(moduleName);
moduleTree.moduleName = moduleName;
// We return the module AST but also a collection of all the comments
// since it can be helpful to iterate through them without having to
// traverse the AST again
return {
moduleTree: moduleTree,
comments: comments
};
}
// attach the comments to their nearest code tree
function attachComments(tree, comments) {
var visitor = new ParseTreeVisitor();
var index = 0;
var currentComment = comments[index];
if (currentComment) log.silly('comment: ' + currentComment.range.start.line + ' - ' + currentComment.range.end.line);
// Really we ought to subclass ParseTreeVisitor but this is fiddly in ES5 so
// it is easier to simply override the prototype's method on the instance
visitor.visitAny = function(tree) {
if (tree && tree.location && tree.location.start && currentComment) {
if (currentComment.range.end.offset < tree.location.start.offset) {
log.silly('tree: ' + tree.constructor.name + ' - ' + tree.location.start.line);
tree.commentBefore = currentComment;
currentComment.treeAfter = tree;
index++;
currentComment = comments[index];
if (currentComment) log.silly('comment: ' + currentComment.range.start.line + ' - ' + currentComment.range.end.line);
}
}
return ParseTreeVisitor.prototype.visitAny.call(this, tree);
};
// Visit every node of the tree using our custom method
visitor.visit(tree);
}
};

View File

@ -0,0 +1,80 @@
var mockPackage = require('../mocks/mockPackage');
var Dgeni = require('dgeni');
describe('atParser service', function() {
var dgeni, injector, parser;
var fileContent =
'import {CONST} from "facade/lang";\n' +
'\n' +
'/**\n' +
'* A parameter annotation that creates a synchronous eager dependency.\n' +
'*\n' +
'* class AComponent {\n' +
'* constructor(@Inject("aServiceToken") aService) {}\n' +
'* }\n' +
'*\n' +
'*/\n' +
'export class Inject {\n' +
'token;\n' +
'@CONST()\n' +
'constructor(token) {\n' +
'this.token = token;\n' +
'}\n' +
'}';
beforeEach(function() {
dgeni = new Dgeni([mockPackage()]);
injector = dgeni.configureInjector();
parser = injector.get('atParser');
});
it('should extract the comments from the file', function() {
var result = parser.parseModule({
content: fileContent,
relativePath: 'di/src/annotations.js'
});
expect(result.comments[0].range.toString()).toEqual(
'/**\n' +
'* A parameter annotation that creates a synchronous eager dependency.\n' +
'*\n' +
'* class AComponent {\n' +
'* constructor(@Inject("aServiceToken") aService) {}\n' +
'* }\n' +
'*\n' +
'*/'
);
});
it('should extract a module AST from the file', function() {
var result = parser.parseModule({
content: fileContent,
relativePath: 'di/src/annotations.js'
});
expect(result.moduleTree.moduleName).toEqual('di/annotations');
expect(result.moduleTree.scriptItemList[0].type).toEqual('IMPORT_DECLARATION');
expect(result.moduleTree.scriptItemList[1].type).toEqual('EXPORT_DECLARATION');
});
it('should attach comments to their following AST', function() {
var result = parser.parseModule({
content: fileContent,
relativePath: 'di/src/annotations.js'
});
expect(result.moduleTree.scriptItemList[1].commentBefore.range.toString()).toEqual(
'/**\n' +
'* A parameter annotation that creates a synchronous eager dependency.\n' +
'*\n' +
'* class AComponent {\n' +
'* constructor(@Inject("aServiceToken") aService) {}\n' +
'* }\n' +
'*\n' +
'*/'
);
});
});

View File

@ -0,0 +1,28 @@
var LEADING_STAR = /^[^\S\r\n]*\*[^\S\n\r]?/gm;
/**
* Extact comment info from a comment object
* @param {Object} comment object to process
* @return { {startingLine, endingLine, content, codeTree}= } a comment info object
* or undefined if the comment is not a jsdoc style comment
*/
module.exports = function getJSDocComment() {
return function(comment) {
var commentInfo;
// we need to check for `/**` at the start of the comment to find all the jsdoc style comments
comment.range.toString().replace(/^\/\*\*([\w\W]*)\*\/$/g, function(match, commentBody) {
commentBody = commentBody.replace(LEADING_STAR, '').trim();
commentInfo = {
startingLine: comment.range.start.line,
endingLine: comment.range.end.line,
content: commentBody,
codeTree: comment.treeAfter
};
});
return commentInfo;
};
};

View File

@ -0,0 +1,67 @@
var mockPackage = require('../mocks/mockPackage');
var Dgeni = require('dgeni');
describe('getJSDocComment service', function() {
var dgeni, injector, getJSDocComment;
function createComment(commentString, start, end, codeTree) {
return {
range: {
toString: function() { return commentString; },
start: { line: start },
end: { line: end },
},
treeAfter: codeTree
};
}
beforeEach(function() {
dgeni = new Dgeni([mockPackage()]);
injector = dgeni.configureInjector();
getJSDocComment = injector.get('getJSDocComment');
});
it('should only return an object if the comment starts with /** and ends with */', function() {
var result = getJSDocComment(createComment('/** this is a jsdoc comment */'));
expect(result).toBeDefined();
result = getJSDocComment(createComment('/* this is a normal comment */'));
expect(result).toBeUndefined();
result = getJSDocComment(createComment('this is not a valid comment */'));
expect(result).toBeUndefined();
result = getJSDocComment(createComment('nor is this'));
expect(result).toBeUndefined();
result = getJSDocComment(createComment('/* or even this'));
expect(result).toBeUndefined();
result = getJSDocComment(createComment('/** and this'));
expect(result).toBeUndefined();
});
it('should return a result that contains info about the comment', function() {
var codeTree = {};
var result = getJSDocComment(createComment('/** this is a comment */', 10, 20, codeTree));
expect(result.startingLine).toEqual(10);
expect(result.endingLine).toEqual(20);
expect(result.codeTree).toBe(codeTree);
});
it('should strip off leading stars from each line', function() {
var result = getJSDocComment(createComment(
'/** this is a jsdoc comment */\n' +
' *\n' +
' * some content\n' +
' */'
));
expect(result.content).toEqual(
'this is a jsdoc comment */\n' +
'\n' +
'some content'
);
});
});

View File

@ -0,0 +1,14 @@
{% extends 'layout/base.template.html' %}
{% block body %}
<h1>{$ doc.name $} <span class="type">class</span></h1>
<p class="module">exported from <a href="/{$ doc.moduleDoc.path $}">{$ doc.moduleDoc.id $}</a></p>
<p>{$ doc.description | marked $}</p>
<h2>Members</h2>
{% for member in doc.members %}
<h3>{$ member.name $}</h3>
<p>{$ member.description | marked $}</p>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends 'layout/base.template.html' %}
{% block body %}
<h1>{$ doc.id $}</h1>
<h2>({$ doc.docType $})</h2>
<div>
{$ doc.description | marked $}
</div>
{% endblock %}

View File

@ -0,0 +1,3 @@
angular.module('{$ doc.moduleName $}', [])
.value('{$ doc.serviceName $}', {$ doc.value | json $});

View File

@ -0,0 +1,5 @@
{% extends 'layout/base.template.html' %}
{% block body %}
{$ doc.description | marked $}
{% endblock %}

View File

@ -0,0 +1 @@
{% block body %}{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends 'layout/base.template.html' %}
{% block body %}
<h1 class="id">{$ doc.id $} <span class="type">module</span></h1>
<p>{$ doc.description | marked $}</p>
{% if doc.exports.length %}
<h2>Exports</h2>
<ul>
{%- for exportDoc in doc.exports %}
<li><a href="/{$ exportDoc.path $}">{$ exportDoc.name $} {$ exportDoc.docType $}</a></li>
{%- endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@ -399,3 +399,58 @@ gulp.task('build', function(done) {
gulp.task('analyze', function(done) { gulp.task('analyze', function(done) {
runSequence('analyze/analyzer.dart'); runSequence('analyze/analyzer.dart');
}); });
// --------------
// doc generation
var Dgeni = require('dgeni');
gulp.task('docs/dgeni', function() {
try {
var dgeni = new Dgeni([require('./docs/dgeni-package')]);
return dgeni.generate();
} catch(x) {
console.log(x.stack);
throw x;
}
});
var bower = require('bower');
gulp.task('docs/bower', function() {
var bowerTask = bower.commands.install(undefined, undefined, { cwd: 'docs' });
bowerTask.on('log', function (result) {
console.log('bower:', result.id, result.data.endpoint.name);
});
bowerTask.on('error', function(error) {
console.log(error);
});
return bowerTask;
});
gulp.task('docs/assets', ['docs/bower'], function() {
return gulp.src('docs/bower_components/**/*')
.pipe(gulp.dest('build/docs/lib'));
});
gulp.task('docs/app', function() {
return gulp.src('docs/app/**/*')
.pipe(gulp.dest('build/docs'));
});
gulp.task('docs', ['docs/assets', 'docs/app', 'docs/dgeni']);
gulp.task('docs-watch', function() {
return gulp.watch('docs/app/**/*', ['docs-app']);
});
var jasmine = require('gulp-jasmine');
gulp.task('docs/test', function () {
return gulp.src('docs/**/*.spec.js')
.pipe(jasmine());
});
var webserver = require('gulp-webserver');
gulp.task('docs/serve', function() {
gulp.src('build/docs/')
.pipe(webserver({
fallback: 'index.html'
}));
});

View File

@ -28,11 +28,18 @@
"which": "~1" "which": "~1"
}, },
"devDependencies": { "devDependencies": {
"bower": "^1.3.12",
"canonical-path": "0.0.2",
"dgeni": "^0.4.1",
"dgeni-packages": "^0.10.7",
"gulp": "^3.8.8", "gulp": "^3.8.8",
"gulp-changed": "^1.0.0", "gulp-changed": "^1.0.0",
"gulp-ejs": "^0.3.1", "gulp-ejs": "^0.3.1",
"gulp-jasmine": "^1.0.1",
"gulp-load-plugins": "^0.7.1", "gulp-load-plugins": "^0.7.1",
"gulp-rename": "^1.2.0", "gulp-rename": "^1.2.0",
"gulp-shell": "^0.2.10" "gulp-shell": "^0.2.10",
"gulp-webserver": "^0.8.7",
"lodash": "^2.4.1"
} }
} }