Merge remote-tracking branch 'origin/master'
# Conflicts: # README.md
This commit is contained in:
commit
e140256358
|
@ -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:
|
||||
|
|
156
gulpfile.js
156
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
<b><a href="/docs/js/latest/api/">Angular 2 API Preview (JavaScript)</a></b>
|
||||
[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")
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -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 = [];
|
||||
|
|
|
@ -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"
|
|
@ -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
|
|
@ -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'))
|
||||
;
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = function arrayFromIterable(iterable) {
|
||||
const arr = [];
|
||||
for (let e of iterable) arr.push(e);
|
||||
return arr;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = function logFactory() {
|
||||
var winston = require('winston');
|
||||
winston.cli();
|
||||
winston.level = 'info';
|
||||
return winston;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
});
|
|
@ -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-example language="dart">${code}\n</code-example>`;
|
||||
}
|
||||
|
||||
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 <ol></ol>. 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
|
||||
//- <base> 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;
|
||||
}
|
Loading…
Reference in New Issue