From 27e03591dd7e3067eb06f49a7c4c8a40ff5172b6 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 4 Dec 2014 14:02:03 +0000 Subject: [PATCH] chore(docs): initial dgeni docs generation Closes #261 --- .gitignore | 3 + docs/app/css/app.css | 405 ++++++++++++++++++ docs/app/index.html | 51 +++ docs/app/js/app.js | 47 ++ docs/bower.json | 19 + docs/dgeni-package/index.js | 138 ++++++ docs/dgeni-package/mocks/mockPackage.js | 10 + .../processors/generateDocsFromComments.js | 34 ++ .../processors/generateNavigationDoc.js | 66 +++ .../processors/processClassDocs.js | 42 ++ .../processors/processModuleDocs.js | 46 ++ docs/dgeni-package/readers/atScript.js | 30 ++ docs/dgeni-package/readers/atScript.spec.js | 55 +++ docs/dgeni-package/readers/ngdoc.js | 32 ++ docs/dgeni-package/readers/ngdoc.spec.js | 45 ++ .../services/ExportTreeVisitor.js | 109 +++++ docs/dgeni-package/services/atParser.js | 97 +++++ docs/dgeni-package/services/atParser.spec.js | 80 ++++ .../dgeni-package/services/getJSDocComment.js | 28 ++ .../services/getJSDocComment.spec.js | 67 +++ .../templates/class.template.html | 14 + .../templates/common.template.html | 9 + .../templates/data-module.template.js | 3 + .../templates/guide.template.html | 5 + .../templates/layout/base.template.html | 1 + .../templates/module.template.html | 16 + gulpfile.js | 55 +++ package.json | 9 +- 28 files changed, 1515 insertions(+), 1 deletion(-) create mode 100644 docs/app/css/app.css create mode 100644 docs/app/index.html create mode 100644 docs/app/js/app.js create mode 100644 docs/bower.json create mode 100644 docs/dgeni-package/index.js create mode 100644 docs/dgeni-package/mocks/mockPackage.js create mode 100644 docs/dgeni-package/processors/generateDocsFromComments.js create mode 100644 docs/dgeni-package/processors/generateNavigationDoc.js create mode 100644 docs/dgeni-package/processors/processClassDocs.js create mode 100644 docs/dgeni-package/processors/processModuleDocs.js create mode 100644 docs/dgeni-package/readers/atScript.js create mode 100644 docs/dgeni-package/readers/atScript.spec.js create mode 100644 docs/dgeni-package/readers/ngdoc.js create mode 100644 docs/dgeni-package/readers/ngdoc.spec.js create mode 100644 docs/dgeni-package/services/ExportTreeVisitor.js create mode 100644 docs/dgeni-package/services/atParser.js create mode 100644 docs/dgeni-package/services/atParser.spec.js create mode 100644 docs/dgeni-package/services/getJSDocComment.js create mode 100644 docs/dgeni-package/services/getJSDocComment.spec.js create mode 100644 docs/dgeni-package/templates/class.template.html create mode 100644 docs/dgeni-package/templates/common.template.html create mode 100644 docs/dgeni-package/templates/data-module.template.js create mode 100644 docs/dgeni-package/templates/guide.template.html create mode 100644 docs/dgeni-package/templates/layout/base.template.html create mode 100644 docs/dgeni-package/templates/module.template.html diff --git a/.gitignore b/.gitignore index 42e9772f40..54f9e418ba 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ packages pubspec.lock .c9 .idea/ + + +docs/bower_components/ \ No newline at end of file diff --git a/docs/app/css/app.css b/docs/app/css/app.css new file mode 100644 index 0000000000..3f34fde18d --- /dev/null +++ b/docs/app/css/app.css @@ -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; +} diff --git a/docs/app/index.html b/docs/app/index.html new file mode 100644 index 0000000000..0e1aa7e9df --- /dev/null +++ b/docs/app/index.html @@ -0,0 +1,51 @@ + + + + Angular 2 Docs + + + + + + + + + + + + + + + + + +

Angular V2

+
+ +
+ + +

Navigation

+
+

{{ area.name }}

+ + +

{{section.name}}

+ +
+
+
+
+ + + + + + +
+ + \ No newline at end of file diff --git a/docs/app/js/app.js b/docs/app/js/app.js new file mode 100644 index 0000000000..c5dd7d3115 --- /dev/null +++ b/docs/app/js/app.js @@ -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); } + ); +}]); diff --git a/docs/bower.json b/docs/bower.json new file mode 100644 index 0000000000..0fc69be89f --- /dev/null +++ b/docs/bower.json @@ -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" + } +} diff --git a/docs/dgeni-package/index.js b/docs/dgeni-package/index.js new file mode 100644 index 0000000000..c3afc48861 --- /dev/null +++ b/docs/dgeni-package/index.js @@ -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' + }); +}); \ No newline at end of file diff --git a/docs/dgeni-package/mocks/mockPackage.js b/docs/dgeni-package/mocks/mockPackage.js new file mode 100644 index 0000000000..61d0df228a --- /dev/null +++ b/docs/dgeni-package/mocks/mockPackage.js @@ -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); }); + +}; diff --git a/docs/dgeni-package/processors/generateDocsFromComments.js b/docs/dgeni-package/processors/generateDocsFromComments.js new file mode 100644 index 0000000000..5d8b682c66 --- /dev/null +++ b/docs/dgeni-package/processors/generateDocsFromComments.js @@ -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); + } + }; +}; \ No newline at end of file diff --git a/docs/dgeni-package/processors/generateNavigationDoc.js b/docs/dgeni-package/processors/generateNavigationDoc.js new file mode 100644 index 0000000000..5cc8a7bdbe --- /dev/null +++ b/docs/dgeni-package/processors/generateNavigationDoc.js @@ -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); + } + }; +}; \ No newline at end of file diff --git a/docs/dgeni-package/processors/processClassDocs.js b/docs/dgeni-package/processors/processClassDocs.js new file mode 100644 index 0000000000..fd0f17f1ab --- /dev/null +++ b/docs/dgeni-package/processors/processClassDocs.js @@ -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); + } + }; +}; diff --git a/docs/dgeni-package/processors/processModuleDocs.js b/docs/dgeni-package/processors/processModuleDocs.js new file mode 100644 index 0000000000..52db81b056 --- /dev/null +++ b/docs/dgeni-package/processors/processModuleDocs.js @@ -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); + } + }; +}; \ No newline at end of file diff --git a/docs/dgeni-package/readers/atScript.js b/docs/dgeni-package/readers/atScript.js new file mode 100644 index 0000000000..b6e3f1cef6 --- /dev/null +++ b/docs/dgeni-package/readers/atScript.js @@ -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; + + +}; diff --git a/docs/dgeni-package/readers/atScript.spec.js b/docs/dgeni-package/readers/atScript.spec.js new file mode 100644 index 0000000000..b524fc01f1 --- /dev/null +++ b/docs/dgeni-package/readers/atScript.spec.js @@ -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'); + }); + +}); \ No newline at end of file diff --git a/docs/dgeni-package/readers/ngdoc.js b/docs/dgeni-package/readers/ngdoc.js new file mode 100644 index 0000000000..a1e6a7e4d5 --- /dev/null +++ b/docs/dgeni-package/readers/ngdoc.js @@ -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; +}; \ No newline at end of file diff --git a/docs/dgeni-package/readers/ngdoc.spec.js b/docs/dgeni-package/readers/ngdoc.spec.js new file mode 100644 index 0000000000..658663c9fb --- /dev/null +++ b/docs/dgeni-package/readers/ngdoc.spec.js @@ -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 + }]); + }); + }); +}); + diff --git a/docs/dgeni-package/services/ExportTreeVisitor.js b/docs/dgeni-package/services/ExportTreeVisitor.js new file mode 100644 index 0000000000..b8750ce8ca --- /dev/null +++ b/docs/dgeni-package/services/ExportTreeVisitor.js @@ -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; +}; \ No newline at end of file diff --git a/docs/dgeni-package/services/atParser.js b/docs/dgeni-package/services/atParser.js new file mode 100644 index 0000000000..31ece78ee6 --- /dev/null +++ b/docs/dgeni-package/services/atParser.js @@ -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); + } +}; \ No newline at end of file diff --git a/docs/dgeni-package/services/atParser.spec.js b/docs/dgeni-package/services/atParser.spec.js new file mode 100644 index 0000000000..7c4e1ad265 --- /dev/null +++ b/docs/dgeni-package/services/atParser.spec.js @@ -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' + + '*/' + ); + }); +}); \ No newline at end of file diff --git a/docs/dgeni-package/services/getJSDocComment.js b/docs/dgeni-package/services/getJSDocComment.js new file mode 100644 index 0000000000..0ebc69d90a --- /dev/null +++ b/docs/dgeni-package/services/getJSDocComment.js @@ -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; + }; +}; \ No newline at end of file diff --git a/docs/dgeni-package/services/getJSDocComment.spec.js b/docs/dgeni-package/services/getJSDocComment.spec.js new file mode 100644 index 0000000000..210238cc67 --- /dev/null +++ b/docs/dgeni-package/services/getJSDocComment.spec.js @@ -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' + ); + }); +}); \ No newline at end of file diff --git a/docs/dgeni-package/templates/class.template.html b/docs/dgeni-package/templates/class.template.html new file mode 100644 index 0000000000..8dace000b8 --- /dev/null +++ b/docs/dgeni-package/templates/class.template.html @@ -0,0 +1,14 @@ +{% extends 'layout/base.template.html' %} + +{% block body %} +

{$ doc.name $} class

+

exported from {$ doc.moduleDoc.id $}

+

{$ doc.description | marked $}

+ +

Members

+{% for member in doc.members %} +

{$ member.name $}

+

{$ member.description | marked $}

+{% endfor %} + +{% endblock %} \ No newline at end of file diff --git a/docs/dgeni-package/templates/common.template.html b/docs/dgeni-package/templates/common.template.html new file mode 100644 index 0000000000..dc10750a80 --- /dev/null +++ b/docs/dgeni-package/templates/common.template.html @@ -0,0 +1,9 @@ +{% extends 'layout/base.template.html' %} + +{% block body %} +

{$ doc.id $}

+

({$ doc.docType $})

+
+{$ doc.description | marked $} +
+{% endblock %} \ No newline at end of file diff --git a/docs/dgeni-package/templates/data-module.template.js b/docs/dgeni-package/templates/data-module.template.js new file mode 100644 index 0000000000..498cdad8cf --- /dev/null +++ b/docs/dgeni-package/templates/data-module.template.js @@ -0,0 +1,3 @@ +angular.module('{$ doc.moduleName $}', []) + +.value('{$ doc.serviceName $}', {$ doc.value | json $}); \ No newline at end of file diff --git a/docs/dgeni-package/templates/guide.template.html b/docs/dgeni-package/templates/guide.template.html new file mode 100644 index 0000000000..3ee289dbc8 --- /dev/null +++ b/docs/dgeni-package/templates/guide.template.html @@ -0,0 +1,5 @@ +{% extends 'layout/base.template.html' %} + +{% block body %} +{$ doc.description | marked $} +{% endblock %} \ No newline at end of file diff --git a/docs/dgeni-package/templates/layout/base.template.html b/docs/dgeni-package/templates/layout/base.template.html new file mode 100644 index 0000000000..16a0d9dc96 --- /dev/null +++ b/docs/dgeni-package/templates/layout/base.template.html @@ -0,0 +1 @@ +{% block body %}{% endblock %} \ No newline at end of file diff --git a/docs/dgeni-package/templates/module.template.html b/docs/dgeni-package/templates/module.template.html new file mode 100644 index 0000000000..2000a4ab7d --- /dev/null +++ b/docs/dgeni-package/templates/module.template.html @@ -0,0 +1,16 @@ +{% extends 'layout/base.template.html' %} + +{% block body %} +

{$ doc.id $} module

+ +

{$ doc.description | marked $}

+ +{% if doc.exports.length %} +

Exports

+ +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 7c2cdc9bd9..8e55be5f3e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -399,3 +399,58 @@ gulp.task('build', function(done) { gulp.task('analyze', function(done) { 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' + })); +}); diff --git a/package.json b/package.json index 143a284783..9c29df5a8a 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,18 @@ "which": "~1" }, "devDependencies": { + "bower": "^1.3.12", + "canonical-path": "0.0.2", + "dgeni": "^0.4.1", + "dgeni-packages": "^0.10.7", "gulp": "^3.8.8", "gulp-changed": "^1.0.0", "gulp-ejs": "^0.3.1", + "gulp-jasmine": "^1.0.1", "gulp-load-plugins": "^0.7.1", "gulp-rename": "^1.2.0", - "gulp-shell": "^0.2.10" + "gulp-shell": "^0.2.10", + "gulp-webserver": "^0.8.7", + "lodash": "^2.4.1" } }