diff --git a/.travis.yml b/.travis.yml index c6cadbafa5..ef7145f388 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,13 +15,13 @@ env: - TASK=lint - TASK="run-e2e-tests --fast" SCRIPT=examples-install.sh - TASK="run-e2e-tests --fast" SCRIPT=examples-install-preview.sh - - TASK=harp-compile SCRIPT=deploy-install.sh - - TASK=harp-compile SCRIPT=deploy-install-preview.sh + - TASK=build-compile SCRIPT=deploy-install.sh + - TASK=build-compile SCRIPT=deploy-install-preview.sh matrix: fast_finish: true allow_failures: - env: "TASK=\"run-e2e-tests --fast\" SCRIPT=examples-install-preview.sh" - - env: "TASK=harp-compile SCRIPT=deploy-install-preview.sh" + - env: "TASK=build-compile SCRIPT=deploy-install-preview.sh" before_install: - npm install -g gulp --no-optional install: diff --git a/gulpfile.js b/gulpfile.js index f589774508..38df29993d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -30,6 +30,7 @@ var tslint = require('gulp-tslint'); // 2. Think about using spawn instead of exec in case of long error messages. var TOOLS_PATH = './tools'; +var ANGULAR_IO_PROJECT_PATH = path.resolve('.'); var ANGULAR_PROJECT_PATH = '../angular'; var PUBLIC_PATH = './public'; var TEMP_PATH = './_temp'; @@ -63,12 +64,21 @@ var _devguideShredJadeOptions = { }; var _apiShredOptions = { + lang: 'ts', examplesDir: path.join(ANGULAR_PROJECT_PATH, 'modules/@angular/examples'), fragmentsDir: path.join(DOCS_PATH, '_fragments/_api'), zipDir: path.join(RESOURCES_PATH, 'zips/api'), logLevel: _dgeniLogLevel }; +var _apiShredOptionsForDart = { + lang: 'dart', + examplesDir: path.resolve(ngPathFor('dart'), 'examples'), + fragmentsDir: path.join(DOCS_PATH, '_fragments/_api'), + zipDir: path.join(RESOURCES_PATH, 'zips/api'), + logLevel: _dgeniLogLevel +}; + var _excludePatterns = ['**/node_modules/**', '**/typings/**', '**/packages/**']; var _excludeMatchers = _excludePatterns.map(function(excludePattern){ @@ -96,6 +106,14 @@ var _exampleProtractorBoilerplateFiles = [ var _exampleConfigFilename = 'example-config.json'; +var lang, langs; +function configLangs(langOption) { + lang = (langOption || 'all').toLowerCase(); + if (lang === 'all') { lang = '(ts|js|dart)'; } + langs = lang.match(/\w+/g); // the languages in `lang` as an array +} +configLangs(argv.lang); + function isDartPath(path) { // Testing via indexOf() for now. If we need to match only paths with folders // named 'dart' vs 'dart*' then try: path.match('/dart(/|$)') != null; @@ -131,6 +149,7 @@ gulp.task('run-e2e-tests', runE2e); * all means (ts|js|dart) */ function runE2e() { + if (!argv.lang) configLangs('ts|js'); // Exclude dart by default var promise; if (argv.fast) { // fast; skip all setup @@ -183,8 +202,6 @@ function runE2e() { // each app/spec collection sequentially. function findAndRunE2eTests(filter, outputFile) { // create an output file with header. - var lang = (argv.lang || '(ts|js)').toLowerCase(); - if (lang === 'all') { lang = '(ts|js|dart)'; } var startTime = new Date().getTime(); var header = `Doc Sample Protractor Results for ${lang} on ${new Date().toLocaleString()}\n`; header += argv.fast ? @@ -528,7 +545,9 @@ gulp.task('build-docs', ['build-devguide-docs', 'build-api-docs', 'build-plunker // Stop zipping examples Feb 28, 2016 //gulp.task('build-docs', ['build-devguide-docs', 'build-api-docs', 'build-plunkers', '_zip-examples']); -gulp.task('build-api-docs', ['build-js-api-docs', 'build-ts-api-docs', 'build-dart-cheatsheet']); +gulp.task('build-api-docs', ['build-js-api-docs', 'build-ts-api-docs', 'build-dart-cheatsheet'] + // On TRAVIS? Skip building the Dart API docs for now. + .concat(process.env.TRAVIS ? [] : ['build-dart-api-docs'])); gulp.task('build-devguide-docs', ['_shred-devguide-examples', '_shred-devguide-shared-jade'], function() { return buildShredMaps(true); @@ -542,12 +561,42 @@ gulp.task('build-js-api-docs', ['_shred-api-examples'], function() { return buildApiDocs('js'); }); +gulp.task('build-dart-api-docs', ['_shred-api-examples', 'dartdoc'], function() { + // TODO(chalin): also build build-dart-cheatsheet + return buildApiDocsForDart(); +}); + gulp.task('build-plunkers', ['_copy-example-boilerplate'], function() { return plunkerBuilder.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH, { errFn: gutil.log }); }); -gulp.task('build-dart-cheatsheet', [], function() { - return buildApiDocs('dart'); +gulp.task('build-dart-cheatsheet', ['build-ts-api-docs'], function() { + gutil.log('build-dart-cheatsheet - NOT IMPLEMENTED YET - copying TS cheatsheet data'); + const src = './public/docs/ts/latest/guide/cheatsheet.json'; + fs.copy(src, './public/docs/dart/latest/guide/cheatsheet.json', {clobber: true}, + (err) => { if(err) throw err }); +}); + +gulp.task('dartdoc', ['pub upgrade'], function() { + const ngRepoPath = ngPathFor('dart'); + if (argv.fast && fs.existsSync(path.resolve(ngRepoPath, 'doc'))) { + gutil.log('Skipping dartdoc: --fast flag enabled and "doc" dir exists'); + return true; + } + checkAngularProjectPath(ngRepoPath); + const dartdoc = spawnExt('dartdoc', ['--output', 'doc/api', '--add-crossdart'], { cwd: ngRepoPath}); + return dartdoc.promise; +}); + +gulp.task('pub upgrade', [], function() { + const ngRepoPath = ngPathFor('dart'); + if (argv.fast && fs.existsSync(path.resolve(ngRepoPath, 'packages'))) { + gutil.log('Skipping pub upgrade: --fast flag enabled and "packages" dir exists'); + return true; + } + checkAngularProjectPath(ngRepoPath); + const pubUpgrade = spawnExt('pub', ['upgrade'], { cwd: ngRepoPath}); + return pubUpgrade.promise; }); gulp.task('git-changed-examples', ['_shred-devguide-examples'], function(){ @@ -596,10 +645,35 @@ gulp.task('git-changed-examples', ['_shred-devguide-examples'], function(){ }); }); -gulp.task('harp-compile', ['build-docs'], function() { +gulp.task('harp-compile', [], function() { + return harpCompile() +}); + +gulp.task('serve', [], function() { + // Harp will serve files from workspace. + const cmd = 'npm run harp -- server .'; + gutil.log('Launching harp server (over project files)'); + gutil.log(` > ${cmd}`); + gutil.log('Note: issuing this command directly from the command line will show harp comiple warnings.'); + return execPromise(cmd); +}); + +gulp.task('serve-www', [], function() { + // Serve generated site. + return execPromise('npm run live-server ./www'); +}); + +gulp.task('build-compile', ['build-docs'], function() { return harpCompile(); }); +gulp.task('check-serve', ['build-docs'], function() { + return harpCompile().then(function() { + gutil.log('Launching live-server over ./www'); + return execPromise('npm run live-server ./www'); + }); +}); + gulp.task('check-deploy', ['build-docs'], function() { return harpCompile().then(function() { gutil.log('compile ok'); @@ -693,8 +767,15 @@ gulp.task('_shred-clean-devguide', function(cb) { }); gulp.task('_shred-api-examples', ['_shred-clean-api'], function() { - checkAngularProjectPath(); - return docShredder.shred(_apiShredOptions); + const promises = []; + gutil.log('Shredding API examples for languages: ' + langs.join(', ')); + langs.forEach((lang) => { + if (lang === 'js') return; // JS is handled via TS. + checkAngularProjectPath(ngPathFor(lang)); + const options = lang == 'dart' ? _apiShredOptionsForDart : _apiShredOptions; + promises.push(docShredder.shred(options)); + }); + return Q.all(promises); }); gulp.task('_shred-clean-api', function(cb) { @@ -1089,15 +1170,51 @@ function buildApiDocs(targetLanguage) { var dgeni = new Dgeni([package]); return dgeni.generate(); } catch(err) { - gutil.log(err); - gutil.log(err.stack); + console.error(err); + console.error(err.stack); throw err; } +} - function copyApiDocsToJsFolder() { - // Make a copy of the JS API docs to the TS folder - return gulp.src([path.join(DOCS_PATH, 'ts/latest/api/**/*.*'), '!' + path.join(DOCS_PATH, 'ts/latest/api/index.jade')]) - .pipe(gulp.dest('./public/docs/js/latest/api')); + +function buildApiDocsForDart() { + const apiDir = 'api'; + const vers = 'latest'; + const dab = require('./tools/dart-api-builder/dab')(ANGULAR_IO_PROJECT_PATH); + const log = dab.log; + + log.level = _dgeniLogLevel; + const dabInfo = dab.dartPkgConfigInfo; + dabInfo.ngIoDartApiDocPath = path.join(DOCS_PATH, 'dart', vers, apiDir); + dabInfo.ngDartDocPath = path.join(ngPathFor('dart'), 'doc', apiDir); + // Exclude API entries for developer/internal libraries. Also exclude entries for + // the top-level catch all "angular2" library (otherwise every entry appears twice). + dabInfo.excludeLibRegExp = new RegExp(/^(?!angular2)|\.testing|_|codegen|^angular2$/); + + try { + checkAngularProjectPath('dart'); + var destPath = dabInfo.ngIoDartApiDocPath; + var sourceDirs = fs.readdirSync(dabInfo.ngDartDocPath) + .filter((name) => !name.match(/^index/)) + .map((name) => path.join(dabInfo.ngDartDocPath, name)); + log.info(`Building Dart API pages for ${sourceDirs.length} libraries`); + + return copyFiles(sourceDirs, [destPath]).then(() => { + log.debug('Finished copying', sourceDirs.length, 'directories from', dabInfo.ngDartDocPath, 'to', destPath); + + const apiEntries = dab.loadApiDataAndSaveToApiListFile(); + const tmpDocsPath = path.resolve(path.join(process.env.HOME, 'tmp/docs.json')); + if (argv.dumpDocsJson) fs.writeFileSync(tmpDocsPath, JSON.stringify(apiEntries, null, 2)); + dab.createApiDataAndJadeFiles(apiEntries); + + }).catch((err) => { + console.log(err); + }); + + } catch(err) { + console.error(err); + console.error(err.stack); + throw err; } } @@ -1272,8 +1389,13 @@ function execCommands(cmds, options, cb) { }); } -function checkAngularProjectPath() { - if (!fs.existsSync(ANGULAR_PROJECT_PATH)) { - throw new Error('API related tasks require the angular2 repo to be at ' + path.resolve(ANGULAR_PROJECT_PATH)); +function ngPathFor(lang) { + return ANGULAR_PROJECT_PATH + (lang === 'dart' ? '-dart' : ''); +} + +function checkAngularProjectPath(lang) { + var ngPath = path.resolve(ngPathFor(lang || 'ts')); + if (!fs.existsSync(ngPath)) { + throw new Error('API related tasks require the angular2 repo to be at ' + ngPath); } } diff --git a/harp.json b/harp.json index 8af12262c8..c173f53512 100644 --- a/harp.json +++ b/harp.json @@ -301,6 +301,13 @@ "bio": "Rob is a Developer Advocate on the Angular team at Google. He's the Angular team's resident reactive programming geek and founded the Reactive Extensions for Angular project, ngrx.", "type": "Google" }, + "vikram": { + "name": "Vikram Subramanian", + "picture": "/resources/images/bios/vikram.jpg", + "twitter": "vikerman", + "bio": "Vikram is a Software Engineer on the Angular team focused on Engineering Productivity. That means he makes sure people on the team can move fast and not break things. Vikram enjoys doing Yoga and going on walks with his daughter.", + "type": "Google" + }, "maxsills": { "name": "Max Sills", "picture": "/resources/images/bios/max-sills.jpg", diff --git a/package.json b/package.json index 44d1d2da1a..4a582d893a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "broken-link-checker": "0.7.1", "browser-sync": "^2.9.3", "canonical-path": "0.0.2", + "cheerio": "^0.20.0", "cross-spawn": "^4.0.0", "codelyzer": "0.0.22", "del": "^2.2.0", diff --git a/public/_includes/_hero.jade b/public/_includes/_hero.jade index e029825873..d2ce8d2639 100644 --- a/public/_includes/_hero.jade +++ b/public/_includes/_hero.jade @@ -1,11 +1,11 @@ -// Refer to jade.template.html and addJadeDataDocsProcessor to figure out where the context of this jade file originates +// template: public/_includes/_hero +//- Refer to jade.template.html and addJadeDataDocsProcessor to figure out where the context of this jade file originates - var textFormat = ''; - var headerTitle = title + (typeof varType !== 'undefined' ? (': ' + varType) : ''); - var capitalize = function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } - var useBadges = docType || stability; - -// renamer :: String -> String -// Renames `Let` and `Var` into `Const` +//- renamer :: String -> String +//- Renames `Let` and `Var` into `Const` - var renamer = function renamer(docType) { - return (docType === 'Let' || docType === 'Var') ? 'Const' : docType - } @@ -13,7 +13,7 @@ if current.path[4] && current.path[3] == 'api' - var textFormat = 'is-standard-case' -header(class="hero background-sky") +header(class="hero background-sky", style=fixHeroCss ? "height:auto" : "") div(class="inner-header") h1(class="hero-title text-display-1 translated-cn #{textFormat}") #{headerTitle} if useBadges @@ -33,5 +33,7 @@ header(class="hero background-sky") if subtitle h2.hero-subtitle.text-subhead #{subtitle} + else if current.path[3] == 'api' && current.path[1] == 'dart' + block breadcrumbs else if current.path[0] == "docs" != partial("_version-dropdown") diff --git a/public/docs/_examples/toh-6/ts/app/hero.service.ts b/public/docs/_examples/toh-6/ts/app/hero.service.ts index a2344d84e6..8abbcc2778 100644 --- a/public/docs/_examples/toh-6/ts/app/hero.service.ts +++ b/public/docs/_examples/toh-6/ts/app/hero.service.ts @@ -53,7 +53,7 @@ export class HeroService { let url = `${this.heroesUrl}/${hero.id}`; return this.http - .delete(url, headers) + .delete(url, {headers: headers}) .toPromise() .catch(this.handleError); } diff --git a/public/docs/_layout-dart-api.jade b/public/docs/_layout-dart-api.jade new file mode 100644 index 0000000000..d8e77353b6 --- /dev/null +++ b/public/docs/_layout-dart-api.jade @@ -0,0 +1,40 @@ +//- WARNING: _layout.jade and _layout-dart-api.jade should match in terms of content +//- except that one uses Harp partial/yield and the other uses Jade extends/include. +doctype +html(lang="en" ng-app="angularIOApp" itemscope itemtype="http://schema.org/Framework") + // template: public/docs/_layout-dart-api + head + include ../_includes/_head-include + block head-extra + + block var-def + body(class="l-offset-nav l-offset-side-nav" ng-controller="AppCtrl as appCtrl") + include ../_includes/_main-nav + if current.path[2] + include _includes/_side-nav + include ../_includes/_hero + include ../_includes/_banner + + if current.path[3] == 'api' + if current.path[4] == 'index' + block main-content + else + article(class="l-content-small grid-fluid docs-content") + block main-content + else if current.path.indexOf('cheatsheet') > 0 + block main-content + else + if current.path[3] == 'index' || current.path[3] == 'styleguide' + article(class="l-content-small grid-fluid docs-content") + block main-content + else + article(class="l-content-small grid-fluid docs-content") + div(class="c10") + .showcase + .showcase-content + block main-content + if (current.path[3] == 'guide' || current.path[3] == 'tutorial') && current.path[4] + include ../_includes/_next-item + + include ../_includes/_footer + include ../_includes/_scripts-include diff --git a/public/docs/_layout.jade b/public/docs/_layout.jade index e277180fa0..936c478fee 100644 --- a/public/docs/_layout.jade +++ b/public/docs/_layout.jade @@ -1,8 +1,13 @@ +//- WARNING: _layout.jade and _layout-dart-api.jade should match in terms of content +//- except that one uses Harp partial/yield and the other uses Jade extends/include. doctype html(lang="en" ng-app="angularIOApp" itemscope itemtype="http://schema.org/Framework") + // template: public/docs/_layout head != partial("../_includes/_head-include") + block head-extra + //- body(class="l-offset-nav l-offset-side-nav" ng-controller="AppCtrl as appCtrl") != partial("../_includes/_main-nav") if current.path[2] diff --git a/public/docs/dart/latest/api/index.jade b/public/docs/dart/latest/api/index.jade index c0b081114d..6954ca79c5 100644 --- a/public/docs/dart/latest/api/index.jade +++ b/public/docs/dart/latest/api/index.jade @@ -1,10 +1,13 @@ -.l-main-section - h2 Beta +:marked + > **WARNING:** API documentation is preliminary and subject to change. - p. - The proposed Angular 2 API does not yet have Dart-specific documentation. - However, because the Dart and JavaScript APIs are generated from the same source, - you might find the JavaScript API docs helpful: + > **Known issues:** Although this generated API reference displays Dart + APIs, individual pages sometimes describe TypeScript APIs accompanied with + TypeScript code. The angular.io issue tracker contains [all known + issues][api-issues]; if you notice others, please [report + them][new-issue]. Thanks! - p.text-center - Angular 2 API Preview (JavaScript) + [new-issue]: https://github.com/angular/angular.io/issues/new?labels=dart,api&title=%5BDart%5D%5BAPI%5D%20 + [api-issues]: https://github.com/angular/angular.io/issues?q=label%3Aapi+label%3Adart + +api-list(src="api-list.json" lang="dart") diff --git a/public/resources/images/bios/vikram.jpg b/public/resources/images/bios/vikram.jpg new file mode 100644 index 0000000000..ffc64a9913 Binary files /dev/null and b/public/resources/images/bios/vikram.jpg differ diff --git a/public/resources/js/directives/api-list.js b/public/resources/js/directives/api-list.js index 7be1c88d6d..97c4afa980 100644 --- a/public/resources/js/directives/api-list.js +++ b/public/resources/js/directives/api-list.js @@ -26,6 +26,8 @@ angularIO.directive('apiList', function () { controller: function($scope, $attrs, $http, $location) { var $ctrl = this; + var isForDart = $attrs.lang === 'dart'; + $ctrl.apiTypes = [ { cssClass: 'stable', title: 'Stable', matches: ['stable']}, { cssClass: 'directive', title: 'Directive', matches: ['directive'] }, @@ -37,6 +39,9 @@ angularIO.directive('apiList', function () { { cssClass: 'const', title: 'Const', matches: ['var', 'let', 'const'] } ]; + if (isForDart) $ctrl.apiTypes = $ctrl.apiTypes.filter((t) => + !t.cssClass.match(/^(stable|directive|decorator|interface|enum)$/)); + $ctrl.apiFilter = getApiFilterFromLocation(); $ctrl.apiType = getApiTypeFromLocation(); $ctrl.groupedSections = []; diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000000..cc95b6726b --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +cd `dirname $0`/.. + +if [[ "$(node --version)" < "v5" ]]; then + echo "ERROR: bad version of node detected. If you have nvm installed, type:" + echo " nvm use" + echo "Aborting installation." + exit 1; +else + echo "Node version: $(node --version)" +fi + +echo "Installing main packages ..." +npm install --no-optional + +echo "Patching ..." +source ./scripts/patch.sh + +if [ "$TRAVIS" != "true" ]; then + echo "Rebuilding node-sass, just in case ..." + npm rebuild node-sass; +fi + +echo "Installing packages for examples ..." +source ./scripts/examples-install.sh +set +x + +echo "Installation done" \ No newline at end of file diff --git a/scripts/patch.sh b/scripts/patch.sh new file mode 100755 index 0000000000..87876b20b5 --- /dev/null +++ b/scripts/patch.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +TARGET=node_modules/terraform/lib/helpers/raw.js + +# Around line 282 change from/to: +# var namespace = sourcePath.split(".")[0].split("/") +# var namespace = sourcePath.split('.').slice(0, -1).join('.').split('/') + +if [ -e "$TARGET" ]; then + perl -i.bak -pe 's/^(\s+var namespace.*split\("."\))\[0\]/\1.slice(0, -1).join(".")/' "$TARGET" + echo "Patched '$TARGET'." +else + echo "Nothing to patch. Can't find file '$TARGET'." + exit 1; +fi diff --git a/tools/api-builder/dart-package/index.js b/tools/api-builder/dart-package/index.js new file mode 100644 index 0000000000..6b9d825d22 --- /dev/null +++ b/tools/api-builder/dart-package/index.js @@ -0,0 +1,18 @@ +'use strict'; + +var Package = require('dgeni').Package; +var path = require('canonical-path'); + +module.exports = new Package('dart', []) + + .factory(require('./services/apiListDataFileService')) + .factory(require('./services/arrayFromIterable')) + .factory(require('./services/dartPkgConfigInfo')) + .factory(require('./services/logFactory')) + .factory(require('./services/preprocessDartDocData')) + + // Register the processors + .processor(require('./processors/loadDartDocData')) + // .processor(require('./processors/createApiListData')) + // .processor(require('./processors/loadDartDocHtml')) + ; diff --git a/tools/api-builder/dart-package/processors/loadDartDocData.js b/tools/api-builder/dart-package/processors/loadDartDocData.js new file mode 100644 index 0000000000..af42f68eec --- /dev/null +++ b/tools/api-builder/dart-package/processors/loadDartDocData.js @@ -0,0 +1,21 @@ +'use strict'; + +const path = require('canonical-path'); + +module.exports = function loadDartDocDataProcessor(log, dartPkgConfigInfo, preprocessDartDocData) { + return { + // $runAfter: ['reading-docs'], + // $runBefore: ['docs-read'], + + $process: function (docs) { + if (docs.length != 0) log.error('Expected docs array to be nonempty.'); + + const dataFilePath = path.resolve(dartPkgConfigInfo.ngDartDocPath, 'index.json'); + const dartDocData = require(dataFilePath); + log.info('Loaded', dartDocData.length, 'dartdoc api entries from', dataFilePath); + + preprocessDartDocData.preprocess(dartDocData); + docs.push(...dartDocData); + } + }; +}; diff --git a/tools/api-builder/dart-package/processors/loadDartDocHtml.js b/tools/api-builder/dart-package/processors/loadDartDocHtml.js new file mode 100644 index 0000000000..94fbc203d3 --- /dev/null +++ b/tools/api-builder/dart-package/processors/loadDartDocHtml.js @@ -0,0 +1,45 @@ +'use strict'; + +var path = require('canonical-path'); +var fs = require("q-io/fs"); +var q = require('q'); +var cheerio = require('cheerio'); + +// Original sample file by @petebacondarwin +// Not currently used, but keeping it for now, +// until we completely rule out use of dgeni. + +module.exports = function loadDartDocHtmlProcessor(log, dartPkgConfigInfo) { + return { + $runAfter: ['loadDartDocDataProcessor'], + // $runBefore: ['docs-read'], + + $process: function (docs) { + var ngIoDartApiDocPath = dartPkgConfigInfo.ngIoDartApiDocPath; + + // Return a promise sync we are async in here + return q.all(docs.map(function (doc) { + if (doc.kind.match(/-dart-api$/)) return; + + // Load up the HTML and extract the contents of the body + var htmlPath = path.resolve(ngIoDartApiDocPath, doc.href); + + return fs.exists(htmlPath).then(function (exists) { + + if (!exists) { + log.debug('missing html ' + htmlPath); + return; + } + + return fs.read().then(function (html) { + log.info('Reading ' + htmlPath) + var $ = cheerio.load(html); + doc.htmlContent = $('body').contents().html(); + }); + + }); + + })); + } + } +}; diff --git a/tools/api-builder/dart-package/services/apiListDataFileService.js b/tools/api-builder/dart-package/services/apiListDataFileService.js new file mode 100644 index 0000000000..afc6be6e36 --- /dev/null +++ b/tools/api-builder/dart-package/services/apiListDataFileService.js @@ -0,0 +1,76 @@ +'use strict'; + +const assert = require('assert-plus'); +const fs = require('fs-extra'); +const path = require('canonical-path'); +const Array_from = require('./arrayFromIterable'); + +module.exports = function apiListDataFileService(log, dartPkgConfigInfo) { + + const _self = { + + mainDataFileName: 'api-list.json', + mainDataFilePath: null, + + libToEntryMap: null, + containerToEntryMap: null, + numExcludedEntries: 0, + + createDataAndSaveToFile: function (dartDocDataWithExtraProps) { + const libToEntryMap = _self.libToEntryMap = new Map(); + const containerToEntryMap = _self.containerToEntryMap = new Map(); + const re = dartPkgConfigInfo.excludeLibRegExp; + + // Populate the two maps from dartDocDataWithExtraProps. + dartDocDataWithExtraProps.forEach((e) => { + // Skip non-preprocessed entries. + if (!e.kind) return true; + + // Exclude non-public APIs. + if (e.libName.match(re)) { _self.numExcludedEntries++; return true; } + + let key; + if (e.kind.startsWith('entry')) { + // Store library entry info in lib map. + key = e.libName; + assert.equal(key, e.enclosedByQualifiedName, e); + _set(libToEntryMap, key, e); + } else if (e.enclosedBy) { + assert.notEqual(e.type, 'library'); + key = e.enclosedByQualifiedName; + } else { + assert.equal(e.type, 'library'); + // Add library "index" page to the library's entries in the general container map, + // but not the lib map which is used to create the main API page index. + key = e.libName; + _set(containerToEntryMap, key, e); + // Add the library as an entry to the Angular2 package container: + key = ''; + } + _set(containerToEntryMap, key, e); + }); + log.info('Excluded', _self.numExcludedEntries, 'library entries (regexp match).'); + + // Write the library map out as the top-level data file. + _self.mainDataFilePath = path.resolve(path.join(dartPkgConfigInfo.ngIoDartApiDocPath, _self.mainDataFileName)); + + // The data file needs to be a map of lib names to an array of entries + const fileData = Object.create(null); + for (let name of Array_from(libToEntryMap.keys()).sort()) { + fileData[name] = Array_from(libToEntryMap.get(name).values()); + } + fs.writeFileSync(_self.mainDataFilePath, JSON.stringify(fileData, null, 2)); + log.info('Wrote', Object.keys(fileData).length, 'library entries to', _self.mainDataFilePath); + return fileData; + }, + + } + return _self; +}; + +// Adds e to the map of m[key]. +function _set(m, key, e) { + if (!m.has(key)) m.set(key, new Map()); + const entryMap = m.get(key); + entryMap.set(e.name, e); +} \ No newline at end of file diff --git a/tools/api-builder/dart-package/services/arrayFromIterable.js b/tools/api-builder/dart-package/services/arrayFromIterable.js new file mode 100644 index 0000000000..e38890737f --- /dev/null +++ b/tools/api-builder/dart-package/services/arrayFromIterable.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = function arrayFromIterable(iterable) { + const arr = []; + for (let e of iterable) arr.push(e); + return arr; +}; diff --git a/tools/api-builder/dart-package/services/dartPkgConfigInfo.js b/tools/api-builder/dart-package/services/dartPkgConfigInfo.js new file mode 100644 index 0000000000..05117df847 --- /dev/null +++ b/tools/api-builder/dart-package/services/dartPkgConfigInfo.js @@ -0,0 +1,13 @@ +'use strict'; + +/** + * @return {Object} The Dart package config information + */ +module.exports = function dartPkgConfigInfo() { + const _self = { + ngIoDartApiDocPath: 'ngIoDartApiDocPath is uninitialized', + ngDartDocPath: 'ngDartDocPath is uninitialized', + excludeLibRegExp: null, + }; + return _self; +}; \ No newline at end of file diff --git a/tools/api-builder/dart-package/services/logFactory.js b/tools/api-builder/dart-package/services/logFactory.js new file mode 100644 index 0000000000..84b7fa4b93 --- /dev/null +++ b/tools/api-builder/dart-package/services/logFactory.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function logFactory() { + var winston = require('winston'); + winston.cli(); + winston.level = 'info'; + return winston; +}; \ No newline at end of file diff --git a/tools/api-builder/dart-package/services/preprocessDartDocData.js b/tools/api-builder/dart-package/services/preprocessDartDocData.js new file mode 100644 index 0000000000..9ecc0dcfc0 --- /dev/null +++ b/tools/api-builder/dart-package/services/preprocessDartDocData.js @@ -0,0 +1,81 @@ +'use strict'; + +const assert = require('assert-plus'); +const path = require('canonical-path'); +const fs = require('fs-extra'); + +module.exports = function preprocessDartDocData(log, dartPkgConfigInfo) { + + const _self = { + + entryMap: null, + + preprocess: function (dartDocData) { + // List of API entities + let entryMap = _self.entryMap = new Map(); // used to remove duplicates + let numDuplicates = 0; + + dartDocData + .forEach((e) => { + if (entryMap.has(e.href)) { + log.debug('Dartdoc preprocessor: duplicate entry for', e.href); + numDuplicates++; + return true; + } + // Sample entry (note that enclosedBy is optional): + // { + // "name": "Pipe", + // "qualifiedName": "angular2.core.Pipe", + // "href": "angular2.core/Pipe-class.html", + // "type": "class", + // "enclosedBy": { + // "name": "angular2.core", + // "type": "library" + // } + // } + + // Save original type property since it will be overridden. + e.origDartDocType = e.type; + const name = e.name; + const qualifiedName = e.qualifiedName; + const matches = e.href.match(/-([a-z]+)\.html/); + let type = matches ? (e.typeFromHref = matches[1]) : e.type; + // Conform to TS type names for now. + if (type === 'constant') type = 'let'; + + let libName; + e.enclosedByQualifiedName = path.dirname(e.href); + if (e.enclosedBy && e.enclosedBy.type === 'library') { + e.kind = 'entry-dart-api'; + libName = e.enclosedBy.name; + assert.equal(libName, e.enclosedByQualifiedName, e.kind); + } else if (e.origDartDocType === 'library') { + e.kind = 'library-dart-api'; + libName = e.name; + e.enclosedByQualifiedName = ''; // Dart libraries can only be at the top level. + } else { + e.kind = 'subentry-dart-api'; + libName = e.enclosedByQualifiedName.split('/')[0]; + assert.equal(path.join(libName, e.enclosedBy.name), e.enclosedByQualifiedName, e); + } + e.docType = type; + e.libName = libName; + e.path = e.href; + e.title = name; + e.layout = false; // To prevent public/docs/_layout.jade from be applied to Dart API pages + // Also set above: + // e.kind: one of {library,entry,subentry}-dart-api + // e.enclosedByQualifiedName + // e.origDartDocType + // e.typeFromHref + Object.freeze(e); + entryMap.set(e.path, e); + log.silly('Preproc API entity =', JSON.stringify(e, null, 2)); + }); + // There shouldn't be duplicates (hence the warning), but there are: + // https://github.com/dart-lang/dartdoc/issues/1197 + if (numDuplicates) log.warn('Number of duplicate dartdoc entries', numDuplicates); + }, + }; + return _self; +}; \ No newline at end of file diff --git a/tools/api-builder/dart-package/test.js b/tools/api-builder/dart-package/test.js new file mode 100644 index 0000000000..22ffb7f73d --- /dev/null +++ b/tools/api-builder/dart-package/test.js @@ -0,0 +1,26 @@ +'use strict'; + +// This file is likely outdated. +// To run, cd to this dir and +// node test.js + +const path = require('canonical-path'); +const Dgeni = require('dgeni'); +const dartPkg = require(path.resolve('.')); + +const ANGULAR_IO_PROJECT_PATH = '../../..'; +const DOCS_PATH = path.join(ANGULAR_IO_PROJECT_PATH, 'public/docs'); +const apiDocPath = path.join(DOCS_PATH, 'dart/latest/api'); + +dartPkg.config(function (dartPkgConfigInfo) { + dartPkgConfigInfo.ngIoDartApiDocPath = apiDocPath; + dartPkgConfigInfo.ngDartDocPath = path.join(ANGULAR_IO_PROJECT_PATH, '../ngdart/doc/api'); +}); + +const dgeni = new Dgeni([dartPkg]); + +dgeni.generate().catch(function (err) { + console.log(err); + console.log(err.stack); + throw err; +}); diff --git a/tools/dart-api-builder/dab.js b/tools/dart-api-builder/dab.js new file mode 100644 index 0000000000..b314719f02 --- /dev/null +++ b/tools/dart-api-builder/dab.js @@ -0,0 +1,215 @@ +'use strict'; + +const assert = require('assert-plus'); +const cheerio = require('cheerio'); +const Encoder = require('node-html-encoder').Encoder; +const fs = require('fs-extra'); +const path = require('canonical-path'); + +module.exports = function dabFactory(ngIoProjPath) { + const encoder = new Encoder('entity'); + + // Get the functionality we need from the dgeni package by the same name. + const dartApiBuilderDgeniProjPath = 'tools/api-builder/dart-package'; + const dab = require(path.resolve(ngIoProjPath, dartApiBuilderDgeniProjPath)).module; + + const log = dab.logFactory[1](); + const dartPkgConfigInfo = dab.dartPkgConfigInfo[1](); + const preprocessDartDocData = dab.preprocessDartDocData[1](log, dartPkgConfigInfo); + const loadDartDocDataProcessor = dab.loadDartDocDataProcessor[1](log, dartPkgConfigInfo, preprocessDartDocData); + const apiListDataFileService = dab.apiListDataFileService[1](log, dartPkgConfigInfo); + const Array_from = dab.arrayFromIterable[1]; + + // Load API data, then create and save 'api-list.json'. + function loadApiDataAndSaveToApiListFile() { + const docs = []; + loadDartDocDataProcessor.$process(docs); + log.debug('Number of Dart API entries loaded:', docs.length); + var libMap = apiListDataFileService.createDataAndSaveToFile(docs); + for (let name in libMap) { + log.debug(' ', name, 'has', libMap[name].length, 'top-level entries'); + } + return docs; + } + + // Create and save the container's '_data.json' file. + function _createDirData(containerName, destDirPath, entries) { + const entryNames = Array_from(entries.keys()).sort(); + const dataMap = Object.create(null); + entryNames.map((n) => { + const e = entries.get(n); + assert.object(e, `entry named ${n}`); + dataMap[path.basename(e.path, '.html')] = e; + }); + const dataFilePath = path.resolve(destDirPath, '_data.json'); + fs.writeFile(dataFilePath, JSON.stringify(dataMap, null, 2)); + log.info(containerName, 'wrote', Object.keys(dataMap).length, 'entries to', dataFilePath); + } + + function _insertExampleFragments(enclosedByName, eltId, $, div) { + const fragDir = path.join(dartPkgConfigInfo.ngIoDartApiDocPath, '../../../_fragments/_api'); + const exList = div.find('p:contains("{@example")'); + exList.each((i, elt) => { + const text = $(elt).text(); + log.debug(`Found example: ${enclosedByName} ${eltId}`, text); + const matches = text.match(/{@example\s+([^\s]+)(\s+region=[\'\"]?(\w+)[\'\"]?)?\s*}/); + if (!matches) { + log.warn(enclosedByName, eltId, 'has an invalidly formed @example tag:', text); + return true; + } + const exRelPath = matches[1]; + const region = matches[3]; + + const dir = path.dirname(exRelPath) + const extn = path.extname(exRelPath); + const baseName = path.basename(exRelPath, extn); + const fileNameNoExt = baseName + (region ? `-${region}` : '') + const exFragPath = path.resolve(fragDir, dir, `${fileNameNoExt}${extn}.md`); + if (!fs.existsSync(exFragPath)) { + log.warn('Fragment not found:', exFragPath); + return true; + } + $(elt).empty(); + const md = fs.readFileSync(exFragPath, 'utf8'); + const codeElt = _extractAndWrapInCodeTags(md); + $(elt).html(codeElt); + log.silly('Fragment code in html:', $(elt).html()); + }); + } + + function _extractAndWrapInCodeTags(md) { + const lines = md.split('\n'); + // Drop first and last lines that are the code markdown tripple ticks (and last \n): + lines.shift(); lines.pop(); lines.pop(); + const code = lines.map((line) => encoder.htmlEncode(line)).join('\n'); + // TS uses format="linenums"; removing that for now. + return `${code}\n`; + } + + function _createEntryJadeFile(e, destDirPath) { + const htmlPagePath = path.resolve(dartPkgConfigInfo.ngDartDocPath, e.path); + if (!fs.existsSync(htmlPagePath)) { + log.warn('Entry', e.name, ': expected to find file but didn\'t', htmlPagePath); + return; + } + const html = fs.readFileSync(htmlPagePath, 'utf8'); + log.debug('Reading (and then deleting)', html.length, 'chars from', htmlPagePath); + const $ = cheerio.load(html); + const div = $('div.body.container'); + $('div.sidebar-offcanvas-left').remove(); + const baseNameNoExtn = path.basename(e.path, '.html'); + _insertExampleFragments(e.enclosedByQualifiedName, baseNameNoExtn, $, div); + + const outFileNoExtn = path.join(destDirPath, baseNameNoExtn); + const depth = path.dirname(e.path).split('/').length; + assert(depth === 1 || depth == 2, 'depth ' + depth); + const jadeFilePath = path.resolve(outFileNoExtn + '.jade'); + const breadcrumbs = $('header > nav ol.breadcrumbs'); + fs.writeFileSync(jadeFilePath, apiEntryJadeTemplate(depth, breadcrumbs, div)); + // In case harp cached the .html version, remove it since it will be generated. + try { + fs.unlinkSync(path.resolve(outFileNoExtn + '.html')); + } catch (err) { + if (e.enclosedBy && e.enclosedBy.type === 'class' && + e.enclosedBy.name.toLowerCase() === e.name.toLowerCase()) { + // Do nothing since this is a known bug with dartdoc: + // https://github.com/dart-lang/dartdoc/issues/1196 + } else { + console.error(err); + console.error(`Output path: ${destDirPath}`); + console.error(`Entity: ${e}`); + console.error(err.stack); + throw err; + } + } + log.debug(' ', e.enclosedByQualifiedName, 'entry', e.name, 'wrote to ', jadeFilePath); + } + + function _createJadeFiles(containerName, destDirPath, entries) { + let numApiPagesWritten = 0; + for (let name of entries.keys()) { + _createEntryJadeFile(entries.get(name), destDirPath); + numApiPagesWritten++ + } + log.info(containerName, 'created', numApiPagesWritten, 'Jade entry files.'); + return numApiPagesWritten; + } + + function createApiDataAndJadeFiles(docs) { + let numApiPagesWritten = 0; + let map = apiListDataFileService.containerToEntryMap; + for (let name of map.keys()) { + if (!name) continue; // skip package-level + let destDirPath = path.resolve(dartPkgConfigInfo.ngIoDartApiDocPath, name); + let entries; + if (!fs.existsSync(destDirPath)) { + log.error(`Dartdoc API folder not found:`, destDirPath); + } else if ((entries = map.get(name)).size > 0) { + _createDirData(name, destDirPath, entries); + numApiPagesWritten += _createJadeFiles(name, destDirPath, entries); + } + } + return numApiPagesWritten; + } + + const _self = { + Array_from: Array_from, + apiEntryJadeTemplate: apiEntryJadeTemplate, + apiListDataFileService: apiListDataFileService, + loadApiDataAndSaveToApiListFile: loadApiDataAndSaveToApiListFile, + createApiDataAndJadeFiles: createApiDataAndJadeFiles, + dartPkgConfigInfo: dartPkgConfigInfo, + loadDartDocDataProcessor: loadDartDocDataProcessor, + log: log, + preprocessDartDocData: preprocessDartDocData, + }; + Object.freeze(_self); + return _self; +}; + +function _indentedEltHtml($elt, i, filterFnOpt) { + let lines = $elt.html().split('\n'); + if (filterFnOpt) lines = lines.filter(filterFnOpt); + const indent = ' '.substring(0,i); + return lines.map((line) => `${indent}| ${line}`).join('\n'); +} + +function apiEntryJadeTemplate(baseHrefDepth, $breadcrumbs, $mainDiv) { + const baseHref = path.join(...Array(baseHrefDepth).fill('..')); + // TODO/investigate: for some reason $breadcrumbs.html() is missing the
    . We add it back in the template below. + const breadcrumbs = _indentedEltHtml($breadcrumbs, 6, (line) => !line.match(/^\s*$/)); + const mainDivHtml = _indentedEltHtml($mainDiv, 4); + // WARNING: since the following is Jade, indentation is significant. + const result = ` +extends ${baseHref}/../../../_layout-dart-api + +include ${baseHref}/../_util-fns + +block var-def + //- FIXME: a CSS expert needs to figure out why the header CSS needs to be patched for Dart. + //- This enables the patch: + - var fixHeroCss = 1; + +block head-extra + // generated Dart API page template: head-extra + //- is required because all the links in dartdoc generated pages are "pseudo-absolute" + base(href="${baseHref}") + link(rel='stylesheet' href='https://fonts.googleapis.com/css?family=Source+Code+Pro|Roboto:500,400italic,300,400' type='text/css') + link(rel="stylesheet" href="static-assets/prettify.css") + link(rel="stylesheet" href="static-assets/css/bootstrap.min.css") + link(rel="stylesheet" href="static-assets/styles.css") + +block breadcrumbs + // generated Dart API page template: breadcrumbs + nav.dropdown + ol.breadcrumbs.gt-separated.hidden-xs +${breadcrumbs} + +block main-content + // generated Dart API page template: main-content: start + div.dart-api-entry-main +${mainDivHtml} + // generated Dart API page template: main-content: end +`; + return result; + }