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 6f784956fb..a7bc1101b2 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', + // On TRAVIS? Skip building the Dart API docs for now. + ...(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,40 @@ 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'); + gutil.log('build-dart-cheatsheet - NOT IMPLEMENTED YET'); + // return buildApiDocsForDart(); +}); + +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 +643,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 +765,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) { @@ -1087,8 +1166,8 @@ 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; } @@ -1099,6 +1178,48 @@ function buildApiDocs(targetLanguage) { } } + +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; + } +} + function buildShredMaps(shouldWrite) { var options = { devguideExamplesDir: _devguideShredOptions.examplesDir, @@ -1270,8 +1391,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/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 6e301a0b71..3a27ed9d2b 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 #{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/_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/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/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; + }