diff --git a/modules/@angular/docs/cheatsheet/bootstrapping.md b/docs/content/cheatsheet/bootstrapping.md similarity index 100% rename from modules/@angular/docs/cheatsheet/bootstrapping.md rename to docs/content/cheatsheet/bootstrapping.md diff --git a/modules/@angular/docs/cheatsheet/built-in-directives.md b/docs/content/cheatsheet/built-in-directives.md similarity index 100% rename from modules/@angular/docs/cheatsheet/built-in-directives.md rename to docs/content/cheatsheet/built-in-directives.md diff --git a/modules/@angular/docs/cheatsheet/class-decorators.md b/docs/content/cheatsheet/class-decorators.md similarity index 100% rename from modules/@angular/docs/cheatsheet/class-decorators.md rename to docs/content/cheatsheet/class-decorators.md diff --git a/modules/@angular/docs/cheatsheet/component-configuration.md b/docs/content/cheatsheet/component-configuration.md similarity index 100% rename from modules/@angular/docs/cheatsheet/component-configuration.md rename to docs/content/cheatsheet/component-configuration.md diff --git a/modules/@angular/docs/cheatsheet/dependency-injection.md b/docs/content/cheatsheet/dependency-injection.md similarity index 100% rename from modules/@angular/docs/cheatsheet/dependency-injection.md rename to docs/content/cheatsheet/dependency-injection.md diff --git a/modules/@angular/docs/cheatsheet/directive-and-component-decorators.md b/docs/content/cheatsheet/directive-and-component-decorators.md similarity index 100% rename from modules/@angular/docs/cheatsheet/directive-and-component-decorators.md rename to docs/content/cheatsheet/directive-and-component-decorators.md diff --git a/modules/@angular/docs/cheatsheet/directive-configuration.md b/docs/content/cheatsheet/directive-configuration.md similarity index 100% rename from modules/@angular/docs/cheatsheet/directive-configuration.md rename to docs/content/cheatsheet/directive-configuration.md diff --git a/modules/@angular/docs/cheatsheet/forms.md b/docs/content/cheatsheet/forms.md similarity index 100% rename from modules/@angular/docs/cheatsheet/forms.md rename to docs/content/cheatsheet/forms.md diff --git a/modules/@angular/docs/cheatsheet/lifecycle hooks.md b/docs/content/cheatsheet/lifecycle hooks.md similarity index 100% rename from modules/@angular/docs/cheatsheet/lifecycle hooks.md rename to docs/content/cheatsheet/lifecycle hooks.md diff --git a/modules/@angular/docs/cheatsheet/ngmodules.md b/docs/content/cheatsheet/ngmodules.md similarity index 100% rename from modules/@angular/docs/cheatsheet/ngmodules.md rename to docs/content/cheatsheet/ngmodules.md diff --git a/modules/@angular/docs/cheatsheet/routing.md b/docs/content/cheatsheet/routing.md similarity index 100% rename from modules/@angular/docs/cheatsheet/routing.md rename to docs/content/cheatsheet/routing.md diff --git a/modules/@angular/docs/cheatsheet/template-syntax.md b/docs/content/cheatsheet/template-syntax.md similarity index 100% rename from modules/@angular/docs/cheatsheet/template-syntax.md rename to docs/content/cheatsheet/template-syntax.md diff --git a/docs/src/app/app.component.ts b/docs/src/app/app.component.ts new file mode 100644 index 0000000000000..f5a791e144b44 --- /dev/null +++ b/docs/src/app/app.component.ts @@ -0,0 +1,44 @@ +/* +Copyright 2016 Google Inc. All Rights Reserved. +Use of this source code is governed by an MIT-style license that +can be found in the LICENSE file at http://angular.io/license +*/ + +import {Component, OnInit, NgZone } from '@angular/core'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/switchMap'; +import {QueryResults, SearchWorkerClient} from './search-worker-client'; + + +@Component({ + selector: 'my-app', + template: ` +
class {$ doc.name $} {
+ {% if doc.statics.length %}
+ static + +{$ member.name | indent(6, false) | trim $}
+ ++ {$ params.paramList(member.parameters) | indent(8, false) | trim $}{$ params.returnType(member.returnType) $} +
+ {% endif %}{% endfor %} + {% endif %} + {% if doc.constructorDoc.name %} +++ +{$ doc.constructorDoc.name $}
+ ++ {$ params.paramList(doc.constructorDoc.parameters) | indent(8, false) | trim $} +
+ {% endif %} + {% if doc.members.length %} ++ {% for member in doc.members %}{% if not member.internal %} ++ ++ {% endif %}{% endfor %} + {% endif %} +{$ member.name | indent(6, false) | trim $}
+ +{$ params.paramList(member.parameters) | indent(8, false) | trim $}{$ params.returnType(member.returnType) $}
++
+ +{% block additional %} +{% endblock %} + +}
++++Class Description
++ {%- if doc.description.length > 2 %} + {$ doc.description | trimBlankLines | marked $} + {% endif %} ++ +{%- if doc.decorators.length %} +{% block annotations %} ++++Annotations
++ {%- for decorator in doc.decorators %} ++{% endblock %} +{% endif %} + +{%- if doc.constructorDoc and not doc.constructorDoc.internal %} +++ {%- if not decorator.notYetDocumented %} + {$ decorator.description | indentForMarkdown(8) | trimBlankLines | marked $} + {% endif %} + {% endfor %} ++ @{$ decorator.name $}{$ params.paramList(decorator.arguments) | indent(10, false) $} +
++++Constructor
++ +++ {%- if not doc.constructorDoc.notYetDocumented %} + {$ doc.constructorDoc.description | replace('### Example', '') | replace('## Example', '') | replace('# Example', '') | trimBlankLines | marked $} + {% endif %} +{% endif %} + +{% if doc.statics.length %} ++ {$ doc.constructorDoc.name $}{$ params.paramList(doc.constructorDoc.parameters) | indent(8, false) | trim $} +
++++Static Members
++ {% for member in doc.statics %}{% if not member.internal %} + +++ {%- if not member.notYetDocumented %} + {$ member.description | replace('### Example', '') | replace('## Example', '') | replace('# Example', '') | trimBlankLines | marked $} + {% endif %} + + {% if not loop.last %} ++ {$ member.name $}{$ params.paramList(member.parameters) | indent(8, false) | trim $}{$ params.returnType(member.returnType) $} +
+
+ {% endif %} + + {% endif %}{% endfor %} +{% endif %} + +{% if doc.members.length %} ++++Class Details
++ {% for member in doc.members %}{% if not member.internal %} + +++ {%- if not member.notYetDocumented %} + {$ member.description | replace('### Example', '') | replace('## Example', '') | replace('# Example', '') | trimBlankLines | marked $} + {% endif -%} + + {% if not loop.last %} ++ {$ member.name $}{$ params.paramList(member.parameters) | indent(8, false) | trim $}{$ params.returnType(member.returnType) $} +
+
+ {% endif %} + {% endif %}{% endfor %} +{% endif %} + ++ exported from {@link {$ doc.moduleDoc.id $} {$doc.moduleDoc.id $} }, + defined in {$ github.githubViewLink(doc, versionInfo) $} +
+{% endblock %} diff --git a/docs/templates/const.template.html b/docs/templates/const.template.html new file mode 100644 index 0000000000000..a208c7ca4f201 --- /dev/null +++ b/docs/templates/const.template.html @@ -0,0 +1 @@ +{% extends 'var.template.html' -%} \ No newline at end of file diff --git a/docs/templates/data-module.template.js b/docs/templates/data-module.template.js new file mode 100644 index 0000000000000..146dd9ea8bacf --- /dev/null +++ b/docs/templates/data-module.template.js @@ -0,0 +1 @@ +export const {$ doc.serviceName $} = {$ doc.value | json $}; \ No newline at end of file diff --git a/docs/templates/decorator.template.html b/docs/templates/decorator.template.html new file mode 100644 index 0000000000000..d90a10fd37d8b --- /dev/null +++ b/docs/templates/decorator.template.html @@ -0,0 +1,44 @@ +{% import "lib/githubLinks.html" as github -%} +{% import "lib/paramList.html" as params -%} +{% extends 'layout/base.template.html' %} + +{% block body %} + + +{% include "layout/_what-it-does.html" %} + +{% include "layout/_security-notes.html" %} + +{% include "layout/_deprecated-notes.html" %} + +{% include "layout/_how-to-use.html" %} + ++++{% if doc.metadataDoc and doc.metadataDoc.members.length %} +Description
++ {%- if not doc.notYetDocumented %}{$ doc.description | trimBlankLines | marked $}{% endif %} +++++Metadata Properties
++ {% for metadata in doc.metadataDoc.members %}{% if not metadata.internal %} + +++ {%- if not metadata.notYetDocumented %}{$ metadata.description | replace('### Example', '') | replace('## Example', '') | replace('# Example', '') | trimBlankLines | marked $}{% endif -%} + {% if not loop.last %}+ {$ metadata.name $}{$ params.paramList(metadata.parameters) | indent(8, false) | trim $}{$ params.returnType(metadata.returnType) $} +
+
{% endif %} + {% endif %}{% endfor %} +{% endif %} + ++ exported from {@link {$ doc.moduleDoc.id $} {$doc.moduleDoc.id $} } defined in {$ github.githubViewLink(doc, versionInfo) $} +
+{% endblock %} diff --git a/docs/templates/directive.template.html b/docs/templates/directive.template.html new file mode 100644 index 0000000000000..aed19ca17a8d8 --- /dev/null +++ b/docs/templates/directive.template.html @@ -0,0 +1,62 @@ +{% import "lib/githubLinks.html" as github -%} +{% import "lib/paramList.html" as params -%} +{% extends 'class.template.html' -%} + +{% block annotations %} +{% endblock %} + +{% block additional -%} + +{%- if doc.directiveOptions.selector.split(',').length %} +++Selectors
++ {% for selector in doc.directiveOptions.selector.split(',') %} ++
+ {% endfor %} +{% endif %} + +{% if doc.outputs %} +{$ selector $}
++++Outputs
++ {% for binding, property in doc.outputs %} ++{% endif %} + +{% if doc.inputs %} +++ {% endfor %} +{$ property.bindingName $} bound to
+{$ property.memberDoc.classDoc.name $}.{$ property.propertyName $}
+ {$ property.memberDoc.description | trimBlankLines | marked $} +++Inputs
++ {% for binding, property in doc.inputs %} ++{$ property.bindingName $} bound to
+{$ property.memberDoc.classDoc.name $}.{$ property.propertyName $}
+ {$ property.memberDoc.description | trimBlankLines | marked $} + {% endfor %} +{% endif %} + +{%- if doc.directiveOptions.exportAs %} ++++Exported as
+++
+{% endif %} +{% endblock %} diff --git a/docs/templates/enum.template.html b/docs/templates/enum.template.html new file mode 100644 index 0000000000000..9c59159b296ae --- /dev/null +++ b/docs/templates/enum.template.html @@ -0,0 +1 @@ +{% extends 'class.template.html' -%} \ No newline at end of file diff --git a/docs/templates/example-region.template.html b/docs/templates/example-region.template.html new file mode 100644 index 0000000000000..d6dcfa830c6ea --- /dev/null +++ b/docs/templates/example-region.template.html @@ -0,0 +1,7 @@ +{$ doc.directiveOptions.exportAs $}
++{% marked %} +``` +{$ doc.contents $} +``` +{% endmarked %} +\ No newline at end of file diff --git a/docs/templates/function.template.html b/docs/templates/function.template.html new file mode 100644 index 0000000000000..555b4e0e95c24 --- /dev/null +++ b/docs/templates/function.template.html @@ -0,0 +1,31 @@ +{% import "lib/githubLinks.html" as github -%} +{% import "lib/paramList.html" as params -%} +{% extends 'layout/base.template.html' -%} + +{% block body %} + + +{% include "layout/_what-it-does.html" %} + +{% include "layout/_security-notes.html" %} + +{% include "layout/_deprecated-notes.html" %} + +{% include "layout/_how-to-use.html" %} + ++++Class Export
++++ {%- if not doc.notYetDocumented %}{$ doc.description | trimBlankLines | marked $}{% endif %} + ++ export {$ doc.name $}{$ params.paramList(doc.parameters) | indent(8, true) | trim $}{$ params.returnType(doc.returnType) $} +
++ exported from {@link {$ doc.moduleDoc.id $} {$doc.moduleDoc.id $} } defined in {$ github.githubViewLink(doc, versionInfo) $} +
+{% endblock %} \ No newline at end of file diff --git a/docs/templates/interface.template.html b/docs/templates/interface.template.html new file mode 100644 index 0000000000000..ff413cb599864 --- /dev/null +++ b/docs/templates/interface.template.html @@ -0,0 +1,72 @@ +{% import "lib/githubLinks.html" as github -%} +{% import "lib/paramList.html" as params -%} +{% extends 'layout/base.template.html' -%} + +{% block body %} + + +{% include "layout/_what-it-does.html" %} + +{% include "layout/_security-notes.html" %} + +{% include "layout/_deprecated-notes.html" %} + +{% include "layout/_how-to-use.html" %} + ++++Interface Overview
++interface {$ doc.name $} {
+ + {% if doc.members.length %} ++ {% for member in doc.members %}{% if not member.internal %} ++ ++ {% endif %}{% endfor %} + {% endif %} +{$ member.name | indent(6, false) | trim $}
+{$ params.paramList(member.parameters) | indent(8, false) | trim $}{$ params.returnType(member.returnType) $}
+ ++
+ +{% block additional %} +{% endblock %} + +}
++++Interface Description
++ {%- if doc.description.length > 2 %}{$ doc.description | trimBlankLines | marked $}{% endif %} ++{% if doc.members.length %} +++{% endif %} + +++Interface Details
++ {% for member in doc.members %}{% if not member.internal %} + ++ {% endif %}{% endfor %} +++ {%- if not member.notYetDocumented %}{$ member.description | replace('### Example', '') | replace('## Example', '') | replace('# Example', '') | trimBlankLines | marked $}{% endif -%} + {% if not loop.last %}+ {$ member.name $}{$ params.paramList(member.parameters) | indent(8, false) | trim $}{$ params.returnType(member.returnType) $} +
+
{% endif %} ++ exported from {@link {$ doc.moduleDoc.id $} {$doc.moduleDoc.id $} }, + defined in {$ github.githubViewLink(doc, versionInfo) $} +
+{% endblock %} diff --git a/docs/templates/json-doc.template.json b/docs/templates/json-doc.template.json new file mode 100644 index 0000000000000..3e7f096354147 --- /dev/null +++ b/docs/templates/json-doc.template.json @@ -0,0 +1 @@ +{$ doc.data | json $} \ No newline at end of file diff --git a/docs/templates/layout/_deprecated-notes.html b/docs/templates/layout/_deprecated-notes.html new file mode 100644 index 0000000000000..eb196f37c47d2 --- /dev/null +++ b/docs/templates/layout/_deprecated-notes.html @@ -0,0 +1,11 @@ +{% if doc.showDeprecatedNotes %} +++{% endif %} \ No newline at end of file diff --git a/docs/templates/layout/_how-to-use.html b/docs/templates/layout/_how-to-use.html new file mode 100644 index 0000000000000..2fb251a0d6cb9 --- /dev/null +++ b/docs/templates/layout/_how-to-use.html @@ -0,0 +1,10 @@ +{%- if doc.howToUse %} +++Deprecation Notes
++ {%- if doc.deprecated %}{$ doc.deprecated | marked $} + {% else %}Not yet documented{% endif %} ++++{% endif %} \ No newline at end of file diff --git a/docs/templates/layout/_ng-module.html b/docs/templates/layout/_ng-module.html new file mode 100644 index 0000000000000..151c9d5a86f94 --- /dev/null +++ b/docs/templates/layout/_ng-module.html @@ -0,0 +1,8 @@ +++How to use
++ {$ doc.howToUse | marked $} +++\ No newline at end of file diff --git a/docs/templates/layout/_security-notes.html b/docs/templates/layout/_security-notes.html new file mode 100644 index 0000000000000..f66ce66cc4e61 --- /dev/null +++ b/docs/templates/layout/_security-notes.html @@ -0,0 +1,10 @@ +{% if doc.showSecurityNotes and doc.security %} +++NgModule
++ {$ doc.ngModule $} ++++{% endif %} \ No newline at end of file diff --git a/docs/templates/layout/_what-it-does.html b/docs/templates/layout/_what-it-does.html new file mode 100644 index 0000000000000..14b800004be1c --- /dev/null +++ b/docs/templates/layout/_what-it-does.html @@ -0,0 +1,10 @@ +{%- if doc.whatItDoes %} +++Security Risk
++ {$ doc.security | marked $} ++++{% endif %} \ No newline at end of file diff --git a/docs/templates/layout/base.template.html b/docs/templates/layout/base.template.html new file mode 100644 index 0000000000000..16a0d9dc96f07 --- /dev/null +++ b/docs/templates/layout/base.template.html @@ -0,0 +1 @@ +{% block body %}{% endblock %} \ No newline at end of file diff --git a/docs/templates/let.template.html b/docs/templates/let.template.html new file mode 100644 index 0000000000000..a208c7ca4f201 --- /dev/null +++ b/docs/templates/let.template.html @@ -0,0 +1 @@ +{% extends 'var.template.html' -%} \ No newline at end of file diff --git a/docs/templates/lib/githubLinks.html b/docs/templates/lib/githubLinks.html new file mode 100644 index 0000000000000..53d5132765083 --- /dev/null +++ b/docs/templates/lib/githubLinks.html @@ -0,0 +1,7 @@ +{% macro githubHref(doc, versionInfo) -%} +https://github.com/{$ versionInfo.gitRepoInfo.owner $}/{$ versionInfo.gitRepoInfo.repo $}/tree/{$ versionInfo.currentVersion.isSnapshot and versionInfo.currentVersion.SHA or versionInfo.currentVersion.raw $}/modules/{$ doc.fileInfo.projectRelativePath $}#L{$ doc.location.start.line+1 $}-L{$ doc.location.end.line+1 $} +{%- endmacro %} + +{% macro githubViewLink(doc, versionInfo) -%} + {$ doc.fileInfo.projectRelativePath $} +{%- endmacro %} diff --git a/docs/templates/lib/paramList.html b/docs/templates/lib/paramList.html new file mode 100644 index 0000000000000..24ba12c080b72 --- /dev/null +++ b/docs/templates/lib/paramList.html @@ -0,0 +1,12 @@ +{% macro paramList(params) -%} + {%- if params -%} + ({%- for param in params -%} + {$ param | escape $}{% if not loop.last %}, {% endif %} + {%- endfor %}) + {%- endif %} +{%- endmacro -%} + + +{% macro returnType(returnType) -%} + {%- if returnType %} : {$ returnType | escape $}{% endif -%} +{%- endmacro -%} diff --git a/docs/templates/module.template.html b/docs/templates/module.template.html new file mode 100644 index 0000000000000..4c632fb454787 --- /dev/null +++ b/docs/templates/module.template.html @@ -0,0 +1,14 @@ +{% import "lib/githubLinks.html" as github -%} +{% extends 'layout/base.template.html' -%} + +{% block body -%} +++What it does
++ {$ doc.whatItDoes | marked $} ++defined in {$ github.githubViewLink(doc, versionInfo) $}
++ {% for page in doc.childPages -%} +
+{% endblock %} diff --git a/docs/templates/overview-dump.template.html b/docs/templates/overview-dump.template.html new file mode 100644 index 0000000000000..86e1b72e0e324 --- /dev/null +++ b/docs/templates/overview-dump.template.html @@ -0,0 +1,74 @@ +{% import "lib/githubLinks.html" as github -%} +{% import "lib/paramList.html" as params -%} + + + +- + + {$ page.title $} +
+ {% endfor %} ++ + + + + + Module Overview
+ +{% for module in doc.modules %} + ++
+ + {% for export in module.exports %} +{$ module.id $}{%- if module.public %} (public){% endif %}
++ +
+ {%- if export.constructorDoc %} +{$ export.docType $} {$ export.name $}
+ ++ +
+ {% endif -%} + {%- for member in export.members %} +{$ export.constructorDoc.name $}{$ params.paramList(export.constructorDoc.params) $}
+ ++ +
+ {% endfor %} + + {% endfor %} + +{% endfor %} + + + diff --git a/docs/templates/pipe.template.html b/docs/templates/pipe.template.html new file mode 100644 index 0000000000000..d328b8578d19a --- /dev/null +++ b/docs/templates/pipe.template.html @@ -0,0 +1,30 @@ +{% import "lib/githubLinks.html" as github -%} +{% import "lib/paramList.html" as params -%} +{% extends 'layout/base.template.html' -%} + +{% block body %} + + +{% include "layout/_what-it-does.html" %} + +{% include "layout/_security-notes.html" %} + +{% include "layout/_deprecated-notes.html" %} + +{% include "layout/_how-to-use.html" %} + +{% include "layout/_ng-module.html" %} + +{$ member.name $}{$ params.paramList(member.params) $}
+ +++++Description
++ {%- if doc.description.length > 2 %}{$ doc.description | trimBlankLines | marked $}{% endif %} +++ exported from {@link {$ doc.moduleDoc.id $} {$doc.moduleDoc.id $} } + defined in {$ github.githubViewLink(doc, versionInfo) $} +
+{% endblock %} diff --git a/docs/templates/type-alias.template.html b/docs/templates/type-alias.template.html new file mode 100644 index 0000000000000..8f4b225223e94 --- /dev/null +++ b/docs/templates/type-alias.template.html @@ -0,0 +1 @@ +{% extends 'interface.template.html' %} diff --git a/docs/templates/var.template.html b/docs/templates/var.template.html new file mode 100644 index 0000000000000..99037091e6595 --- /dev/null +++ b/docs/templates/var.template.html @@ -0,0 +1,33 @@ +{% import "lib/githubLinks.html" as github -%} +{% import "lib/paramList.html" as params -%} +{% extends 'layout/base.template.html' %} + +{% block body %} + + +{% include "layout/_what-it-does.html" %} + +{% include "layout/_security-notes.html" %} + +{% include "layout/_deprecated-notes.html" %} + +{% include "layout/_how-to-use.html" %} + +++++Variable Export
+
++++ {%- if not doc.notYetDocumented %}{$ doc.description | trimBlankLines | marked $}{% endif %} ++ export {$ doc.name $} +
++ exported from {@link {$ doc.moduleDoc.id $} {$doc.moduleDoc.id $} } + defined in {$ github.githubViewLink(doc, versionInfo) $} +
+{% endblock %} diff --git a/gulpfile.js b/gulpfile.js index 9ca76727502cd..0517a0c14a30e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -252,6 +252,24 @@ gulp.task('changelog', () => { .pipe(gulp.dest('./')); }); +gulp.task('docs', ['doc-gen', 'docs-app']); +gulp.task('doc-gen', () => { + const Dgeni = require('dgeni'); + const angularDocsPackage = require(path.resolve(__dirname, 'tools/docs/angular.io-package')); + const dgeni = new Dgeni([angularDocsPackage]); + return dgeni.generate(); +}); +gulp.task('docs-app', () => { gulp.src('docs/src/**/*').pipe(gulp.dest('dist/docs')); }); + +gulp.task('docs-test', ['doc-gen-test', 'docs-app-test']); +gulp.task('doc-gen-test', () => { + const execSync = require('child_process').execSync; + execSync( + 'node dist/tools/cjs-jasmine/index-tools ../../tools/docs/**/*.spec.js', + {stdio: ['inherit', 'inherit', 'inherit']}); +}); +gulp.task('docs-app-test', () => {}); + function tsc(projectPath, done) { const childProcess = require('child_process'); diff --git a/tools/docs/README.md b/tools/docs/README.md new file mode 100644 index 0000000000000..2200fb8c90fe8 --- /dev/null +++ b/tools/docs/README.md @@ -0,0 +1,27 @@ +# Documentation Generation + +The dgeni tool is used to generate the documentation from the source files held in this repository. +The documentation generation is configured by a dgeni package defined in `docs/angular.io-package/index.js`. +This package, in turn requires a number of other packages, some are defined locally in the `docs` folder, +such as `docs/cheatsheet-package` and `docs/content-package`, etc. And some are brought in from the +`dgeni-packages` node modules, such as `jsdoc` and `nunjucks`. + +## Generating the docs + +To generate the documentation simply run `gulp docs` from the command line. + +## Testing the dgeni packages + +The local packages have unit tests that you can execute by running `gulp docs-test` from the command line. + +## What does it generate? + +The output from dgeni is written to files in the `dist/docs` folder. + +Notably this includes a partial HTML file for each "page" of the documentation, such as API pages and guides. +It also includes JavaScript files that contain metadata about the documentation such as navigation data and +keywords for building a search index. + +## Viewing the docs + +You can view the dummy demo app using a simple HTTP server hosting `dist/docs/index.html` diff --git a/tools/docs/angular.io-package/ignore.words b/tools/docs/angular.io-package/ignore.words new file mode 100644 index 0000000000000..82b9f2fc3fc5d --- /dev/null +++ b/tools/docs/angular.io-package/ignore.words @@ -0,0 +1,701 @@ +a +able +about +above +abst +accordance +according +accordingly +across +act +actually +added +adj +adopted +affected +affecting +affects +after +afterwards +again +against +ah +all +almost +alone +along +already +also +although +always +am +among +amongst +an +and +announce +another +any +anybody +anyhow +anymore +anyone +anything +anyway +anyways +anywhere +apparently +approximately +are +aren +arent +arise +around +as +aside +ask +asking +at +auth +available +away +awfully +b +back +be +became +because +become +becomes +becoming +been +before +beforehand +begin +beginning +beginnings +begins +behind +being +believe +below +beside +besides +between +beyond +biol +both +brief +briefly +but +by +c +ca +came +can +cannot +can't +cant +cause +causes +certain +certainly +co +com +come +comes +contain +containing +contains +could +couldnt +d +date +did +didn't +didnt +different +do +does +doesn't +doesnt +doing +done +don't +dont +down +downwards +due +during +e +each +ed +edu +effect +eg +eight +eighty +either +else +elsewhere +end +ending +enough +especially +et +et-al +etc +even +ever +every +everybody +everyone +everything +everywhere +ex +except +f +far +few +ff +fifth +first +five +fix +followed +following +follows +for +former +formerly +forth +found +four +from +further +furthermore +g +gave +get +gets +getting +give +given +gives +giving +go +goes +gone +got +gotten +h +had +happens +hardly +has +hasn't +hasnt +have +haven't +havent +having +he +hed +hence +her +here +hereafter +hereby +herein +heres +hereupon +hers +herself +hes +hi +hid +him +himself +his +hither +home +how +howbeit +however +hundred +i +id +ie +if +i'll +ill +im +immediate +immediately +importance +important +in +inc +indeed +index +information +instead +into +invention +inward +is +isn't +isnt +it +itd +it'll +itll +its +itself +i've +ive +j +just +k +keep +keeps +kept +keys +kg +km +know +known +knows +l +largely +last +lately +later +latter +latterly +least +less +lest +let +lets +like +liked +likely +line +little +'ll +'ll +look +looking +looks +ltd +m +made +mainly +make +makes +many +may +maybe +me +mean +means +meantime +meanwhile +merely +mg +might +million +miss +ml +more +moreover +most +mostly +mr +mrs +much +mug +must +my +myself +n +na +name +namely +nay +nd +near +nearly +necessarily +necessary +need +needs +neither +never +nevertheless +new +next +nine +ninety +no +nobody +non +none +nonetheless +noone +nor +normally +nos +not +noted +nothing +now +nowhere +o +obtain +obtained +obviously +of +off +often +oh +ok +okay +old +omitted +on +once +one +ones +only +onto +or +ord +other +others +otherwise +ought +our +ours +ourselves +out +outside +over +overall +owing +own +p +page +pages +part +particular +particularly +past +per +perhaps +placed +please +plus +poorly +possible +possibly +potentially +pp +predominantly +present +previously +primarily +probably +promptly +proud +provides +put +q +que +quickly +quite +qv +r +ran +rather +rd +re +readily +really +recent +recently +ref +refs +regarding +regardless +regards +related +relatively +research +respectively +resulted +resulting +results +right +run +s +said +same +saw +say +saying +says +sec +section +see +seeing +seem +seemed +seeming +seems +seen +self +selves +sent +seven +several +shall +she +shed +she'll +shell +shes +should +shouldn't +shouldnt +show +showed +shown +showns +shows +significant +significantly +similar +similarly +since +six +slightly +so +some +somebody +somehow +someone +somethan +something +sometime +sometimes +somewhat +somewhere +soon +sorry +specifically +specified +specify +specifying +state +states +still +stop +strongly +sub +substantially +successfully +such +sufficiently +suggest +sup +sure +t +take +taken +taking +tell +tends +th +than +thank +thanks +thanx +that +that'll +thatll +thats +that've +thatve +the +their +theirs +them +themselves +then +thence +there +thereafter +thereby +thered +therefore +therein +there'll +therell +thereof +therere +theres +thereto +thereupon +there've +thereve +these +they +theyd +they'll +theyll +theyre +they've +theyve +think +this +those +thou +though +thoughh +thousand +throug +through +throughout +thru +thus +til +tip +to +together +too +took +toward +towards +tried +tries +truly +try +trying +ts +twice +two +u +un +under +unfortunately +unless +unlike +unlikely +until +unto +up +upon +ups +us +use +used +useful +usefully +usefulness +uses +using +usually +v +value +various +'ve +'ve +very +via +viz +vol +vols +vs +w +want +wants +was +wasn't +wasnt +way +we +wed +welcome +we'll +well +went +were +weren't +werent +we've +weve +what +whatever +what'll +whatll +whats +when +whence +whenever +where +whereafter +whereas +whereby +wherein +wheres +whereupon +wherever +whether +which +while +whim +whither +who +whod +whoever +whole +who'll +wholl +whom +whomever +whos +whose +why +widely +will +willing +wish +with +within +without +won't +wont +words +would +wouldn't +wouldnt +www +x +y +yes +yet +you +youd +you'll +youll +your +youre +yours +yourself +yourselves +you've +youve +z +zero diff --git a/tools/docs/angular.io-package/index.js b/tools/docs/angular.io-package/index.js new file mode 100644 index 0000000000000..c2e18f78ccf28 --- /dev/null +++ b/tools/docs/angular.io-package/index.js @@ -0,0 +1,223 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +const path = require('path'); +const fs = require('fs'); +const Package = require('dgeni').Package; + +const jsdocPackage = require('dgeni-packages/jsdoc'); +const nunjucksPackage = require('dgeni-packages/nunjucks'); +const typescriptPackage = require('dgeni-packages/typescript'); +const gitPackage = require('dgeni-packages/git'); +const linksPackage = require('../links-package'); +const examplesPackage = require('../examples-package'); +const targetPackage = require('../target-package'); +const cheatsheetPackage = require('../cheatsheet-package'); + +const PROJECT_ROOT = path.resolve(__dirname, '../../..'); +const API_SOURCE_PATH = path.resolve(PROJECT_ROOT, 'modules'); +const CONTENTS_PATH = path.resolve(PROJECT_ROOT, 'docs/content'); +const TEMPLATES_PATH = path.resolve(PROJECT_ROOT, 'docs/templates'); + +module.exports = + new Package( + 'angular.io', + [ + jsdocPackage, nunjucksPackage, typescriptPackage, linksPackage, examplesPackage, + gitPackage, targetPackage, cheatsheetPackage + ]) + + // Register the processors + .processor(require('./processors/convertPrivateClassesToInterfaces')) + .processor(require('./processors/generateNavigationDoc')) + .processor(require('./processors/generateKeywords')) + .processor(require('./processors/extractTitleFromGuides')) + .processor(require('./processors/createOverviewDump')) + .processor(require('./processors/checkUnbalancedBackTicks')) + .processor(require('./processors/addNotYetDocumentedProperty')) + .processor(require('./processors/mergeDecoratorDocs')) + .processor(require('./processors/extractDecoratedClasses')) + .processor(require('./processors/matchUpDirectiveDecorators')) + .processor(require('./processors/filterMemberDocs')) + + .config(function(checkAnchorLinksProcessor, log) { + // TODO: re-enable + checkAnchorLinksProcessor.$enabled = false; + }) + + // Where do we get the source files? + .config(function( + readTypeScriptModules, readFilesProcessor, collectExamples, generateKeywordsProcessor) { + + // API files are typescript + readTypeScriptModules.basePath = API_SOURCE_PATH; + readTypeScriptModules.ignoreExportsMatching = [/^_/]; + readTypeScriptModules.hidePrivateMembers = true; + readTypeScriptModules.sourceFiles = [ + '@angular/common/index.ts', + '@angular/common/testing/index.ts', + '@angular/core/index.ts', + '@angular/core/testing/index.ts', + '@angular/forms/index.ts', + '@angular/http/index.ts', + '@angular/http/testing/index.ts', + '@angular/platform-browser/index.ts', + '@angular/platform-browser/testing/index.ts', + '@angular/platform-browser-dynamic/index.ts', + '@angular/platform-browser-dynamic/testing/index.ts', + '@angular/platform-server/index.ts', + '@angular/platform-server/testing/index.ts', + '@angular/platform-webworker/index.ts', + '@angular/platform-webworker-dynamic/index.ts', + '@angular/router/index.ts', + '@angular/router/testing/index.ts', + '@angular/upgrade/index.ts', + '@angular/upgrade/static.ts', + ]; + + readFilesProcessor.basePath = PROJECT_ROOT; + readFilesProcessor.sourceFiles = [ + {basePath: CONTENTS_PATH, include: CONTENTS_PATH + '/cheatsheet/*.md'}, { + basePath: API_SOURCE_PATH, + include: API_SOURCE_PATH + '/@angular/examples/**/*', + fileReader: 'exampleFileReader' + } + ]; + + collectExamples.exampleFolders = ['@angular/examples']; + + generateKeywordsProcessor.ignoreWordsFile = 'tools/docs/angular.io-package/ignore.words'; + }) + + + + // Where do we write the output files? + .config(function(writeFilesProcessor) { writeFilesProcessor.outputFolder = 'dist/docs'; }) + + + // Target environments + .config(function(targetEnvironments) { + const ALLOWED_LANGUAGES = ['ts', 'js', 'dart']; + const TARGET_LANGUAGE = 'ts'; + + ALLOWED_LANGUAGES.forEach(target => targetEnvironments.addAllowed(target)); + targetEnvironments.activate(TARGET_LANGUAGE); + + // TODO: we may need to do something with `linkDocsInlineTagDef` + }) + + + // Configure jsdoc-style tag parsing + .config(function(parseTagsProcessor, getInjectables) { + // Load up all the tag definitions in the tag-defs folder + parseTagsProcessor.tagDefinitions = + parseTagsProcessor.tagDefinitions.concat(getInjectables(requireFolder('./tag-defs'))); + + // We actually don't want to parse param docs in this package as we are getting the data + // out using TS + // TODO: rewire the param docs to the params extracted from TS + parseTagsProcessor.tagDefinitions.forEach(function(tagDef) { + if (tagDef.name === 'param') { + tagDef.docProperty = 'paramData'; + tagDef.transforms = []; + } + }); + }) + + + + // Configure nunjucks rendering of docs via templates + .config(function( + renderDocsProcessor, versionInfo, templateFinder, templateEngine, getInjectables) { + + // Where to find the templates for the doc rendering + templateFinder.templateFolders = [TEMPLATES_PATH]; + // templateFinder.templateFolders.unshift(TEMPLATES_PATH); + + // Standard patterns for matching docs to templates + templateFinder.templatePatterns = [ + '${ doc.template }', '${ doc.id }.${ doc.docType }.template.html', + '${ doc.id }.template.html', '${ doc.docType }.template.html', + '${ doc.id }.${ doc.docType }.template.js', '${ doc.id }.template.js', + '${ doc.docType }.template.js', '${ doc.id }.${ doc.docType }.template.json', + '${ doc.id }.template.json', '${ doc.docType }.template.json', 'common.template.html' + ]; + + // Nunjucks and Angular conflict in their template bindings so change Nunjucks + templateEngine.config.tags = {variableStart: '{$', variableEnd: '$}'}; + + templateEngine.filters = + templateEngine.filters.concat(getInjectables(requireFolder('./rendering'))); + + // Add the version data to the renderer, for use in things like github links + renderDocsProcessor.extraData.versionInfo = versionInfo; + + // helpers are made available to the nunjucks templates + renderDocsProcessor.helpers.relativePath = function(from, to) { + return path.relative(from, to); + }; + }) + + + + // We are going to be relaxed about ambigous links + .config(function(getLinkInfo) { + getLinkInfo.useFirstAmbiguousLink = false; + // TODO: I think we don't need this for Igor's shell app + // getLinkInfo.relativeLinks = true; + }) + + + + .config(function( + computeIdsProcessor, computePathsProcessor, EXPORT_DOC_TYPES, generateNavigationDoc, + generateKeywordsProcessor) { + + const API_SEGMENT = 'api'; + const GUIDE_SEGMENT = 'guide'; + const APP_SEGMENT = 'app'; + + generateNavigationDoc.outputFolder = APP_SEGMENT; + generateKeywordsProcessor.outputFolder = APP_SEGMENT; + + // Replace any path templates inherited from other packages + // (we want full and transparent control) + computePathsProcessor.pathTemplates = [ + { + docTypes: ['module'], + getPath: function computeModulePath(doc) { + doc.moduleFolder = + doc.id.replace(/^@angular\//, API_SEGMENT + '/').replace(/\/index$/, ''); + return doc.moduleFolder; + }, + outputPathTemplate: '${moduleFolder}/index.html' + }, + { + docTypes: EXPORT_DOC_TYPES.concat(['decorator', 'directive', 'pipe']), + pathTemplate: '${moduleDoc.moduleFolder}/${name}', + outputPathTemplate: '${moduleDoc.moduleFolder}/${name}.html', + }, + { + docTypes: ['api-list-data', 'api-list-audit'], + pathTemplate: APP_SEGMENT + '/${docType}.json', + outputPathTemplate: '${path}' + }, + { + docTypes: ['cheatsheet-data'], + pathTemplate: GUIDE_SEGMENT + '/cheatsheet.json', + outputPathTemplate: '${path}' + }, + {docTypes: ['example-region'], getOutputPath: function() {}} + ]; + }); + +function requireFolder(folderPath) { + const absolutePath = path.resolve(__dirname, folderPath); + return fs.readdirSync(absolutePath) + .filter(p => !/[._]spec\.js$/.test(p)) // ignore spec files + .map(p => require(path.resolve(absolutePath, p))); +} \ No newline at end of file diff --git a/tools/docs/angular.io-package/mocks/importedSrc.ts b/tools/docs/angular.io-package/mocks/importedSrc.ts new file mode 100644 index 0000000000000..a3f9d5d19b0d8 --- /dev/null +++ b/tools/docs/angular.io-package/mocks/importedSrc.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export const x = 100; \ No newline at end of file diff --git a/tools/docs/angular.io-package/mocks/testSrc.ts b/tools/docs/angular.io-package/mocks/testSrc.ts new file mode 100644 index 0000000000000..1b6863cefbdb2 --- /dev/null +++ b/tools/docs/angular.io-package/mocks/testSrc.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * @module + * @description + * This is the module description + */ + +export * from './importedSrc'; + +/** + * This is some random other comment + */ + +/** + * This is MyClass + */ +export class MyClass { + message: String; + + /** + * Create a new MyClass + * @param {String} name The name to say hello to + */ + constructor(name) { this.message = 'hello ' + name; } + + /** + * Return a greeting message + */ + greet() { return this.message; } +} + +/** + * An exported function + */ +export const myFn = (val: number) => val * 2; diff --git a/tools/docs/angular.io-package/processors/addNotYetDocumentedProperty.js b/tools/docs/angular.io-package/processors/addNotYetDocumentedProperty.js new file mode 100644 index 0000000000000..f6ab7ae7c3973 --- /dev/null +++ b/tools/docs/angular.io-package/processors/addNotYetDocumentedProperty.js @@ -0,0 +1,37 @@ +module.exports = function addNotYetDocumentedProperty(EXPORT_DOC_TYPES, log, createDocMessage) { + return { + $runAfter: ['tags-parsed'], + $runBefore: ['rendering-docs'], + $process: function(docs) { + docs.forEach(function(doc) { + + if (EXPORT_DOC_TYPES.indexOf(doc.docType) === -1) return; + + // NotYetDocumented means that no top level comments and no member level comments + doc.notYetDocumented = notYetDocumented(doc); + + if (doc.constructorDoc) { + doc.constructorDoc.notYetDocumented = notYetDocumented(doc.constructorDoc); + doc.notYetDocumented = doc.notYetDocumented && doc.constructorDoc.notYetDocumented; + } + + if (doc.members) { + doc.members.forEach(function(member) { + member.notYetDocumented = notYetDocumented(member); + doc.notYetDocumented = doc.notYetDocumented && member.notYetDocumented; + }); + } + + if (doc.notYetDocumented) { + log.debug(createDocMessage('Not yet documented', doc)); + } + }); + + return docs; + } + }; +}; + +function notYetDocumented(doc) { + return !doc.noDescription && doc.description.trim().length == 0; +} diff --git a/tools/docs/angular.io-package/processors/addNotYetDocumentedProperty.spec.js b/tools/docs/angular.io-package/processors/addNotYetDocumentedProperty.spec.js new file mode 100644 index 0000000000000..a3ecf30e4adf3 --- /dev/null +++ b/tools/docs/angular.io-package/processors/addNotYetDocumentedProperty.spec.js @@ -0,0 +1,148 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); + +describe('addNotYetDocumentedProperty', function() { + var dgeni, injector, processor, log; + + beforeEach(function() { + dgeni = new Dgeni([testPackage('angular.io-package')]); + injector = dgeni.configureInjector(); + processor = injector.get('addNotYetDocumentedProperty'); + log = injector.get('log'); + }); + + it('should mark export docs with no description as "not yet documented"', function() { + var a, b, c, d, a1, b1, c1, d1; + var docs = [ + a = {id: 'a', docType: 'interface', description: 'some content'}, + b = {id: 'b', docType: 'class', description: 'some content'}, + c = {id: 'c', docType: 'var', description: 'some content'}, + d = {id: 'd', docType: 'function', description: 'some content'}, + a1 = {id: 'a1', docType: 'interface', description: ''}, + b1 = {id: 'b1', docType: 'class', description: ''}, + c1 = {id: 'c1', docType: 'var', description: ''}, + d1 = {id: 'd1', docType: 'function', description: ''} + ]; + + processor.$process(docs); + + expect(a.notYetDocumented).toBeFalsy(); + expect(b.notYetDocumented).toBeFalsy(); + expect(c.notYetDocumented).toBeFalsy(); + expect(d.notYetDocumented).toBeFalsy(); + + expect(a1.notYetDocumented).toBeTruthy(); + expect(b1.notYetDocumented).toBeTruthy(); + expect(c1.notYetDocumented).toBeTruthy(); + expect(d1.notYetDocumented).toBeTruthy(); + }); + + it('should mark member docs with no description as "not yet documented"', function() { + var a, a1, a2, b, b1, b2, c, c1, c2; + var docs = [ + a = { + id: 'a', + docType: 'interface', + description: 'some content', + members: [a1 = {id: 'a1', description: 'some content'}, a2 = {id: 'a2', description: ''}] + }, + b = { + id: 'b', + docType: 'class', + description: '', + members: [b1 = {id: 'b1', description: 'some content'}, b2 = {id: 'b2', description: ''}] + }, + c = { + id: 'c', + docType: 'class', + description: '', + members: [c1 = {id: 'c1', description: ''}, c2 = {id: 'c2', description: ''}] + }, + ]; + + processor.$process(docs); + + expect(a.notYetDocumented).toBeFalsy(); + expect(b.notYetDocumented).toBeFalsy(); + expect(c.notYetDocumented).toBeTruthy(); + + expect(a1.notYetDocumented).toBeFalsy(); + expect(a2.notYetDocumented).toBeTruthy(); + expect(b1.notYetDocumented).toBeFalsy(); + expect(b2.notYetDocumented).toBeTruthy(); + expect(c1.notYetDocumented).toBeTruthy(); + expect(c2.notYetDocumented).toBeTruthy(); + }); + + + it('should mark constructor doc with no description as "not yet documented"', function() { + var a, a1, b, b1; + var docs = [ + a = { + id: 'a', + docType: 'interface', + description: '', + constructorDoc: a1 = {id: 'a1', description: 'some content'} + }, + b = { + id: 'b', + docType: 'interface', + description: '', + constructorDoc: b1 = {id: 'b1', description: ''} + } + ]; + + processor.$process(docs); + + expect(a.notYetDocumented).toBeFalsy(); + expect(b.notYetDocumented).toBeTruthy(); + + expect(a1.notYetDocumented).toBeFalsy(); + expect(b1.notYetDocumented).toBeTruthy(); + }); + + + it('should not mark documents explicity tagged as `@noDescription`', function() { + var a, a1, a2, b, b1, b2, c, c1, c2; + var docs = [ + a = { + id: 'a', + docType: 'interface', + description: 'some content', + members: [ + a1 = {id: 'a1', description: 'some content'}, + a2 = {id: 'a2', description: '', noDescription: true} + ] + }, + b = { + id: 'b', + docType: 'class', + description: '', + members: [ + b1 = {id: 'b1', description: 'some content'}, + b2 = {id: 'b2', description: '', noDescription: true} + ] + }, + c = { + id: 'c', + docType: 'class', + description: '', + noDescription: true, + members: [c1 = {id: 'c1', description: ''}, c2 = {id: 'c2', description: ''}] + }, + ]; + + processor.$process(docs); + + expect(a.notYetDocumented).toBeFalsy(); + expect(b.notYetDocumented).toBeFalsy(); + expect(c.notYetDocumented).toBeFalsy(); + + expect(a1.notYetDocumented).toBeFalsy(); + expect(a2.notYetDocumented).toBeFalsy(); + expect(b1.notYetDocumented).toBeFalsy(); + expect(b2.notYetDocumented).toBeFalsy(); + expect(c1.notYetDocumented).toBeTruthy(); + expect(c2.notYetDocumented).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/tools/docs/angular.io-package/processors/checkUnbalancedBackTicks.js b/tools/docs/angular.io-package/processors/checkUnbalancedBackTicks.js new file mode 100644 index 0000000000000..f79b1ba9617a3 --- /dev/null +++ b/tools/docs/angular.io-package/processors/checkUnbalancedBackTicks.js @@ -0,0 +1,33 @@ +var _ = require('lodash'); + +/** + * @dgProcessor checkUnbalancedBackTicks + * @description + * Searches the rendered content for an odd number of (```) backticks, + * which would indicate an unbalanced pair and potentially a typo in the + * source content. + */ +module.exports = function checkUnbalancedBackTicks(log, createDocMessage) { + + var BACKTICK_REGEX = /^ *```/gm; + + return { + // $runAfter: ['checkAnchorLinksProcessor'], + $runAfter: ['inlineTagProcessor'], + $runBefore: ['writeFilesProcessor'], + $process: function(docs) { + _.forEach(docs, function(doc) { + if (doc.renderedContent) { + var matches = doc.renderedContent.match(BACKTICK_REGEX); + if (matches && matches.length % 2 !== 0) { + doc.unbalancedBackTicks = true; + log.warn(createDocMessage( + 'checkUnbalancedBackTicks processor: unbalanced backticks found in rendered content', + doc)); + log.warn(doc.renderedContent); + } + } + }); + } + }; +}; \ No newline at end of file diff --git a/tools/docs/angular.io-package/processors/checkUnbalancedBackTicks.spec.js b/tools/docs/angular.io-package/processors/checkUnbalancedBackTicks.spec.js new file mode 100644 index 0000000000000..b96c3929dcd31 --- /dev/null +++ b/tools/docs/angular.io-package/processors/checkUnbalancedBackTicks.spec.js @@ -0,0 +1,30 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); +var path = require('canonical-path'); + +describe('checkUnbalancedBackTicks', function() { + var dgeni, injector, processor, log; + + beforeEach(function() { + dgeni = new Dgeni([testPackage('angular.io-package')]); + injector = dgeni.configureInjector(); + processor = injector.get('checkUnbalancedBackTicks'); + log = injector.get('log'); + }); + + it('should warn if there are an odd number of back ticks in the rendered content', function() { + var docs = [{ + renderedContent: '```\n' + + 'code block\n' + + '```\n' + + '```\n' + + 'code block with missing closing back ticks\n' + }]; + + processor.$process(docs); + + expect(log.warn).toHaveBeenCalledWith( + 'checkUnbalancedBackTicks processor: unbalanced backticks found in rendered content - doc'); + expect(docs[0].unbalancedBackTicks).toBe(true); + }); +}); \ No newline at end of file diff --git a/tools/docs/angular.io-package/processors/convertPrivateClassesToInterfaces.js b/tools/docs/angular.io-package/processors/convertPrivateClassesToInterfaces.js new file mode 100644 index 0000000000000..83ccf6a3c3617 --- /dev/null +++ b/tools/docs/angular.io-package/processors/convertPrivateClassesToInterfaces.js @@ -0,0 +1,11 @@ +module.exports = function convertPrivateClassesToInterfacesProcessor( + convertPrivateClassesToInterfaces) { + return { + $runAfter: ['processing-docs'], + $runBefore: ['docs-processed'], + $process: function(docs) { + convertPrivateClassesToInterfaces(docs, false); + return docs; + } + }; +}; \ No newline at end of file diff --git a/tools/docs/angular.io-package/processors/createOverviewDump.js b/tools/docs/angular.io-package/processors/createOverviewDump.js new file mode 100644 index 0000000000000..6032e94f64621 --- /dev/null +++ b/tools/docs/angular.io-package/processors/createOverviewDump.js @@ -0,0 +1,24 @@ +var _ = require('lodash'); + +module.exports = function createOverviewDump() { + + return { + $runAfter: ['processing-docs'], + $runBefore: ['docs-processed'], + $process: function(docs) { + var overviewDoc = { + id: 'overview-dump', + aliases: ['overview-dump'], + path: 'overview-dump', + outputPath: 'overview-dump.html', + modules: [] + }; + _.forEach(docs, function(doc) { + if (doc.docType === 'module') { + overviewDoc.modules.push(doc); + } + }); + docs.push(overviewDoc); + } + }; +}; \ No newline at end of file diff --git a/tools/docs/angular.io-package/processors/extractDecoratedClasses.js b/tools/docs/angular.io-package/processors/extractDecoratedClasses.js new file mode 100644 index 0000000000000..de643aa5a5cda --- /dev/null +++ b/tools/docs/angular.io-package/processors/extractDecoratedClasses.js @@ -0,0 +1,29 @@ +var _ = require('lodash'); + +module.exports = function extractDecoratedClassesProcessor(EXPORT_DOC_TYPES) { + + // Add the "directive" docType into those that can be exported from a module + EXPORT_DOC_TYPES.push('directive', 'pipe'); + + return { + $runAfter: ['processing-docs'], + $runBefore: ['docs-processed'], + decoratorTypes: ['Directive', 'Component', 'Pipe'], + $process: function(docs) { + var decoratorTypes = this.decoratorTypes; + + _.forEach(docs, function(doc) { + + _.forEach(doc.decorators, function(decorator) { + + if (decoratorTypes.indexOf(decorator.name) !== -1) { + doc.docType = decorator.name.toLowerCase(); + doc[doc.docType + 'Options'] = decorator.argumentInfo[0]; + } + }); + }); + + return docs; + } + }; +}; diff --git a/tools/docs/angular.io-package/processors/extractDecoratedClasses.spec.js b/tools/docs/angular.io-package/processors/extractDecoratedClasses.spec.js new file mode 100644 index 0000000000000..a323945017316 --- /dev/null +++ b/tools/docs/angular.io-package/processors/extractDecoratedClasses.spec.js @@ -0,0 +1,48 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); + +describe('extractDecoratedClasses processor', function() { + var dgeni, injector, processor; + + beforeEach(function() { + dgeni = new Dgeni([testPackage('angular.io-package')]); + injector = dgeni.configureInjector(); + processor = injector.get('extractDecoratedClassesProcessor'); + }); + + it('should extract specified decorator arguments', function() { + var doc1 = { + id: '@angular/common/ngFor', + name: 'ngFor', + docType: 'class', + decorators: [{ + name: 'Directive', + arguments: ['{selector: \'[ng-for][ng-for-of]\', properties: [\'ngForOf\']}'], + argumentInfo: [{selector: '[ng-for][ng-for-of]', properties: ['ngForOf']}] + }] + }; + var doc2 = { + id: '@angular/core/DecimalPipe', + name: 'DecimalPipe', + docType: 'class', + decorators: + [{name: 'Pipe', arguments: ['{name: \'number\'}'], argumentInfo: [{name: 'number'}]}] + }; + + processor.$process([doc1, doc2]); + + expect(doc1).toEqual(jasmine.objectContaining({ + id: '@angular/common/ngFor', + name: 'ngFor', + docType: 'directive', + directiveOptions: {selector: '[ng-for][ng-for-of]', properties: ['ngForOf']} + })); + + expect(doc2).toEqual(jasmine.objectContaining({ + id: '@angular/core/DecimalPipe', + name: 'DecimalPipe', + docType: 'pipe', + pipeOptions: {name: 'number'} + })); + }); +}); \ No newline at end of file diff --git a/tools/docs/angular.io-package/processors/extractTitleFromGuides.js b/tools/docs/angular.io-package/processors/extractTitleFromGuides.js new file mode 100644 index 0000000000000..1a4e6c3806dde --- /dev/null +++ b/tools/docs/angular.io-package/processors/extractTitleFromGuides.js @@ -0,0 +1,24 @@ +var _ = require('lodash'); + +module.exports = function extractTitleFromGuides() { + + return { + $runAfter: ['processing-docs'], + $runBefore: ['docs-processed'], + $process: function(docs) { + _(docs).forEach(function(doc) { + if (doc.docType === 'guide') { + doc.name = doc.name || getNameFromHeading(doc.description); + } + }); + } + }; +}; + + +function getNameFromHeading(text) { + var match = /^\s*#\s*(.*)/.exec(text); + if (match) { + return match[1]; + } +} \ No newline at end of file diff --git a/tools/docs/angular.io-package/processors/filterMemberDocs.js b/tools/docs/angular.io-package/processors/filterMemberDocs.js new file mode 100644 index 0000000000000..906a9c1213f27 --- /dev/null +++ b/tools/docs/angular.io-package/processors/filterMemberDocs.js @@ -0,0 +1,7 @@ +module.exports = function filterMemberDocs() { + return { + $runAfter: ['extra-docs-added'], $runBefore: ['computing-paths'], $process: function(docs) { + return docs.filter(function(doc) { return doc.docType !== 'member'; }); + } + } +}; \ No newline at end of file diff --git a/tools/docs/angular.io-package/processors/generateKeywords.js b/tools/docs/angular.io-package/processors/generateKeywords.js new file mode 100644 index 0000000000000..d2a22b6e9c6d5 --- /dev/null +++ b/tools/docs/angular.io-package/processors/generateKeywords.js @@ -0,0 +1,139 @@ +'use strict'; + +var fs = require('fs'); +var path = require('canonical-path'); + +/** + * @dgProcessor generateKeywordsProcessor + * @description + * This processor extracts all the keywords from each document and creates + * a new document that will be rendered as a JavaScript file containing all + * this data. + */ +module.exports = function generateKeywordsProcessor(log, readFilesProcessor) { + return { + ignoreWordsFile: undefined, + propertiesToIgnore: [], + docTypesToIgnore: [], + outputFolder: '', + $validate: { + ignoreWordsFile: {}, + docTypesToIgnore: {}, + propertiesToIgnore: {}, + outputFolder: {presence: true} + }, + $runAfter: ['paths-computed'], + $runBefore: ['rendering-docs'], + $process: function(docs) { + + // Keywords to ignore + var wordsToIgnore = []; + var propertiesToIgnore; + var docTypesToIgnore; + + // Keywords start with "ng:" or one of $, _ or a letter + var KEYWORD_REGEX = /^((ng:|[$_a-z])[\w\-_]+)/; + + // Load up the keywords to ignore, if specified in the config + if (this.ignoreWordsFile) { + var ignoreWordsPath = path.resolve(readFilesProcessor.basePath, this.ignoreWordsFile); + wordsToIgnore = fs.readFileSync(ignoreWordsPath, 'utf8').toString().split(/[,\s\n\r]+/gm); + + log.debug('Loaded ignore words from "' + ignoreWordsPath + '"'); + log.silly(wordsToIgnore); + } + + propertiesToIgnore = convertToMap(this.propertiesToIgnore); + log.debug('Properties to ignore', propertiesToIgnore); + docTypesToIgnore = convertToMap(this.docTypesToIgnore); + log.debug('Doc types to ignore', docTypesToIgnore); + + var ignoreWordsMap = convertToMap(wordsToIgnore); + + // If the title contains a name starting with ng, e.g. "ngController", then add the module + // name + // without the ng to the title text, e.g. "controller". + function extractTitleWords(title) { + var match = /ng([A-Z]\w*)/.exec(title); + if (match) { + title = title + ' ' + match[1].toLowerCase(); + } + return title; + } + + function extractWords(text, words, keywordMap) { + var tokens = text.toLowerCase().split(/[.\s,`'"#]+/mg); + tokens.forEach(function(token) { + var match = token.match(KEYWORD_REGEX); + if (match) { + var key = match[1]; + if (!keywordMap[key]) { + keywordMap[key] = true; + words.push(key); + } + } + }); + } + + + // We are only interested in docs that live in the right area + const filteredDocs = docs.filter(function(doc) { return !docTypesToIgnore[doc.docType]; }); + + filteredDocs.forEach(function(doc) { + + + var words = []; + var keywordMap = Object.assign({}, ignoreWordsMap); + var members = []; + var membersMap = {}; + + // Search each top level property of the document for search terms + Object.keys(doc).forEach(function(key) { + const value = doc[key]; + + if (isString(value) && !propertiesToIgnore[key]) { + extractWords(value, words, keywordMap); + } + + if (key === 'methods' || key === 'properties' || key === 'events') { + value.forEach(function(member) { extractWords(member.name, members, membersMap); }); + } + }); + + + doc.searchTerms = { + titleWords: extractTitleWords(doc.name), + keywords: words.sort().join(' '), + members: members.sort().join(' ') + }; + + }); + + var searchData = + filteredDocs.filter(function(page) { return page.searchTerms; }).map(function(page) { + return Object.assign( + {path: page.path, title: page.name, type: page.docType}, page.searchTerms); + }); + + docs.push({ + docType: 'json-doc', + id: 'search-data-json', + template: 'json-doc.template.json', + path: this.outputFolder + '/search-data.json', + outputPath: this.outputFolder + '/search-data.json', + data: searchData + }); + } + }; +}; + + +function isString(value) { + return typeof value == 'string'; +} + +function convertToMap(collection) { + const obj = {}; + collection.forEach(key => { obj[key] = true; }); + return obj; +} \ No newline at end of file diff --git a/tools/docs/angular.io-package/processors/generateNavigationDoc.js b/tools/docs/angular.io-package/processors/generateNavigationDoc.js new file mode 100644 index 0000000000000..d01405f41ab81 --- /dev/null +++ b/tools/docs/angular.io-package/processors/generateNavigationDoc.js @@ -0,0 +1,49 @@ +module.exports = function generateNavigationDoc() { + + return { + $runAfter: ['extra-docs-added'], + $runBefore: ['rendering-docs'], + outputFolder: '', + $validate: {outputFolder: {presence: true}}, + $process: function(docs) { + var modulesDoc = { + docType: 'data-module', + value: {api: {sections: []}, guide: {pages: []}}, + path: this.outputFolder + '/navigation', + outputPath: this.outputFolder + '/navigation.ts', + serviceName: 'NAVIGATION' + }; + + docs.forEach(function(doc) { + if (doc.docType === 'module') { + var moduleNavItem = + {path: doc.path, partial: doc.outputPath, name: doc.id, type: 'module', pages: []}; + + modulesDoc.value.api.sections.push(moduleNavItem); + + doc.exports.forEach(function(exportDoc) { + if (!exportDoc.internal) { + var exportNavItem = { + path: exportDoc.path, + partial: exportDoc.outputPath, + name: exportDoc.name, + type: exportDoc.docType + }; + moduleNavItem.pages.push(exportNavItem); + } + }); + } + }); + + docs.forEach(function(doc) { + if (doc.docType === 'guide') { + console.log('guide', doc.name); + var guideDoc = {path: doc.path, partial: doc.outputPath, name: doc.name, type: 'guide'}; + modulesDoc.value.guide.pages.push(guideDoc); + } + }); + + docs.push(modulesDoc); + } + }; +}; diff --git a/tools/docs/angular.io-package/processors/matchUpDirectiveDecorators.js b/tools/docs/angular.io-package/processors/matchUpDirectiveDecorators.js new file mode 100644 index 0000000000000..b2564749cf202 --- /dev/null +++ b/tools/docs/angular.io-package/processors/matchUpDirectiveDecorators.js @@ -0,0 +1,62 @@ +var _ = require('lodash'); + +/** + * @dgProcessor + * @description + * + */ +module.exports = function matchUpDirectiveDecoratorsProcessor(aliasMap) { + + return { + $runAfter: ['ids-computed', 'paths-computed'], + $runBefore: ['rendering-docs'], + decoratorMappings: {'Inputs': 'inputs', 'Outputs': 'outputs'}, + $process: function(docs) { + var decoratorMappings = this.decoratorMappings; + _.forEach(docs, function(doc) { + if (doc.docType === 'directive') { + doc.selector = doc.directiveOptions.selector; + + for (decoratorName in decoratorMappings) { + var propertyName = decoratorMappings[decoratorName]; + doc[propertyName] = + getDecoratorValues(doc.directiveOptions[propertyName], decoratorName, doc.members); + } + } + }); + } + }; +}; + +function getDecoratorValues(classDecoratorValues, memberDecoratorName, members) { + var optionMap = {}; + var decoratorValues = {}; + + // Parse the class decorator + _.forEach(classDecoratorValues, function(option) { + // Options are of the form: "propName : bindingName" (bindingName is optional) + var optionPair = option.split(':'); + var propertyName = optionPair.shift().trim(); + var bindingName = (optionPair.shift() || '').trim() || propertyName; + + decoratorValues[propertyName] = {propertyName: propertyName, bindingName: bindingName}; + }); + + _.forEach(members, function(member) { + _.forEach(member.decorators, function(decorator) { + if (decorator.name === memberDecoratorName) { + decoratorValues[member.name] = { + propertyName: member.name, + bindingName: decorator.arguments[0] || member.name + }; + } + }); + if (decoratorValues[member.name]) { + decoratorValues[member.name].memberDoc = member; + } + }); + + if (Object.keys(decoratorValues).length) { + return decoratorValues; + } +} \ No newline at end of file diff --git a/tools/docs/angular.io-package/processors/mergeDecoratorDocs.js b/tools/docs/angular.io-package/processors/mergeDecoratorDocs.js new file mode 100644 index 0000000000000..bd7cfcd8f4530 --- /dev/null +++ b/tools/docs/angular.io-package/processors/mergeDecoratorDocs.js @@ -0,0 +1,97 @@ +var _ = require('lodash'); + +module.exports = function mergeDecoratorDocs() { + return { + $runAfter: ['processing-docs'], + $runBefore: ['docs-processed'], + docsToMergeInfo: [ + {nameTemplate: _.template('${name}Decorator'), decoratorProperty: 'decoratorInterfaceDoc'}, { + nameTemplate: _.template('${name}Metadata'), + decoratorProperty: 'metadataDoc', + useFields: ['howToUse', 'whatItDoes'] + }, + {nameTemplate: _.template('${name}MetadataType'), decoratorProperty: 'metadataInterfaceDoc'}, + { + nameTemplate: _.template('${name}MetadataFactory'), + decoratorProperty: 'metadataFactoryDoc' + } + ], + $process: function(docs) { + + var docsToMergeInfo = this.docsToMergeInfo; + var docsToMerge = Object.create(null); + + docs.forEach(function(doc) { + + // find all the decorators, signified by a call to `makeDecorator(metadata)` + var makeDecorator = getMakeDecoratorCall(doc); + if (makeDecorator) { + doc.docType = 'decorator'; + // get the type of the decorator metadata + doc.decoratorType = makeDecorator.arguments[0].text; + // clear the symbol type named (e.g. ComponentMetadataFactory) since it is not needed + doc.symbolTypeName = undefined; + + // keep track of the docs that need to be merged into this decorator doc + docsToMergeInfo.forEach(function(info) { + docsToMerge[info.nameTemplate({name: doc.name})] = { + decoratorDoc: doc, + property: info.decoratorProperty + }; + }); + } + }); + + // merge the metadata docs into the decorator docs + docs = docs.filter(function(doc) { + if (docsToMerge[doc.name]) { + var decoratorDoc = docsToMerge[doc.name].decoratorDoc; + var property = docsToMerge[doc.name].property; + var useFields = docsToMerge[doc.name].useFields; + + // attach this document to its decorator + decoratorDoc[property] = doc; + + // Copy over fields from the merged doc if specified + if (useFields) { + useFields.forEach(function(field) { decoratorDoc[field] = doc[field]; }); + } + + // remove doc from its module doc's exports + doc.moduleDoc.exports = + doc.moduleDoc.exports.filter(function(exportDoc) { return exportDoc !== doc; }); + + + // remove from the overall list of docs to be rendered + return false; + } + return true; + }); + } + }; +}; + +function getMakeDecoratorCall(doc, type) { + var makeDecoratorFnName = 'make' + (type || '') + 'Decorator'; + + var initializer = doc.exportSymbol && doc.exportSymbol.valueDeclaration && + doc.exportSymbol.valueDeclaration.initializer; + + if (initializer) { + // There appear to be two forms of initializer: + // export var Injectable: InjectableFactory = + //makeDecorator(InjectableMetadata); + // and + // export var RouteConfig: (configs: RouteDefinition[]) => ClassDecorator = + // makeDecorator(RouteConfigAnnotation); + // In the first case, the type assertion ` ` causes the AST to contain an + // extra level of expression + // to hold the new type of the expression. + if (initializer.expression && initializer.expression.expression) { + initializer = initializer.expression; + } + if (initializer.expression && initializer.expression.text === makeDecoratorFnName) { + return initializer; + } + } +} \ No newline at end of file diff --git a/tools/docs/angular.io-package/processors/mergeDecoratorDocs.spec.js b/tools/docs/angular.io-package/processors/mergeDecoratorDocs.spec.js new file mode 100644 index 0000000000000..4819f98ad6a35 --- /dev/null +++ b/tools/docs/angular.io-package/processors/mergeDecoratorDocs.spec.js @@ -0,0 +1,61 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); + +describe('mergeDecoratorDocs processor', function() { + var dgeni, injector, processor, decoratorDoc, otherDoc; + + beforeEach(function() { + dgeni = new Dgeni([testPackage('angular.io-package')]); + injector = dgeni.configureInjector(); + processor = injector.get('mergeDecoratorDocs'); + + decoratorDoc = { + name: 'X', + docType: 'var', + exportSymbol: { + valueDeclaration: { + initializer: {expression: {text: 'makeDecorator'}, arguments: [{text: 'XMetadata'}]} + } + } + }; + + decoratorDocWithTypeAssertion = { + name: 'Y', + docType: 'var', + exportSymbol: { + valueDeclaration: { + initializer: { + expression: { + type: {}, + expression: {text: 'makeDecorator'}, + arguments: [{text: 'YMetadata'}] + } + } + } + } + }; + otherDoc = { + name: 'Y', + docType: 'var', + exportSymbol: { + valueDeclaration: + {initializer: {expression: {text: 'otherCall'}, arguments: [{text: 'param1'}]}} + } + }; + }); + + + it('should change the docType of only the docs that are initialied by a call to makeDecorator', + function() { + processor.$process([decoratorDoc, decoratorDocWithTypeAssertion, otherDoc]); + expect(decoratorDoc.docType).toEqual('decorator'); + expect(decoratorDocWithTypeAssertion.docType).toEqual('decorator'); + expect(otherDoc.docType).toEqual('var'); + }); + + it('should extract the "type" of the decorator meta data', function() { + processor.$process([decoratorDoc, decoratorDocWithTypeAssertion, otherDoc]); + expect(decoratorDoc.decoratorType).toEqual('XMetadata'); + expect(decoratorDocWithTypeAssertion.decoratorType).toEqual('YMetadata'); + }); +}); \ No newline at end of file diff --git a/tools/docs/angular.io-package/rendering/indentForMarkdown.js b/tools/docs/angular.io-package/rendering/indentForMarkdown.js new file mode 100644 index 0000000000000..e0fff8afb6a9f --- /dev/null +++ b/tools/docs/angular.io-package/rendering/indentForMarkdown.js @@ -0,0 +1,62 @@ +module.exports = function(encodeCodeBlock) { + // var MIXIN_PATTERN = /\S*\+\S*\(.*/; + return { + name: 'indentForMarkdown', + process: function(str, width) { + if (str == null || str.length === 0) { + return ''; + } + width = width || 4; + + var lines = str.split('\n'); + var newLines = []; + var sp = spaces(width); + var spMixin = spaces(width - 2); + var isAfterMarkdownTag = true; + lines.forEach(function(line) { + // indent lines that match mixin pattern by 2 less than specified width + if (line.indexOf('{@example') >= 0) { + if (isAfterMarkdownTag) { + // happens if example follows example + if (newLines.length > 0) { + newLines.pop(); + } else { + // wierd case - first expression in str is an @example + // in this case the :marked appear above the str passed in, + // so we need to put 'something' into the markdown tag. + newLines.push(sp + '.'); // '.' is a dummy char + } + } + newLines.push(spMixin + line); + // after a mixin line we need to reenter markdown. + newLines.push(spMixin + ':marked'); + isAfterMarkdownTag = true; + } else { + if ((!isAfterMarkdownTag) || (line.trim().length > 0)) { + newLines.push(sp + line); + isAfterMarkdownTag = false; + } + } + }); + if (isAfterMarkdownTag) { + if (newLines.length > 0) { + // if last line is a markdown tag remove it. + newLines.pop(); + } + } + // force character to be a newLine. + if (newLines.length > 0) newLines.push(''); + var res = newLines.join('\n'); + return res; + } + }; + + function spaces(n) { + var str = ''; + for (var i = 0; i < n; i++) { + str += ' '; + } + return str; + }; + +}; \ No newline at end of file diff --git a/tools/docs/angular.io-package/rendering/toId.js b/tools/docs/angular.io-package/rendering/toId.js new file mode 100644 index 0000000000000..007172eea6088 --- /dev/null +++ b/tools/docs/angular.io-package/rendering/toId.js @@ -0,0 +1,6 @@ +module.exports = function toId() { + return { + name: 'toId', + process: function(str) { return str.replace(/[^(a-z)(A-Z)(0-9)._-]/g, '-'); } + }; +}; \ No newline at end of file diff --git a/tools/docs/angular.io-package/rendering/toId.spec.js b/tools/docs/angular.io-package/rendering/toId.spec.js new file mode 100644 index 0000000000000..2a6e984a02c31 --- /dev/null +++ b/tools/docs/angular.io-package/rendering/toId.spec.js @@ -0,0 +1,14 @@ +var factory = require('./toId'); + +describe('toId filter', function() { + var filter; + + beforeEach(function() { filter = factory(); }); + + it('should be called "toId"', function() { expect(filter.name).toEqual('toId'); }); + + it('should convert a string to make it appropriate for use as an HTML id', function() { + expect(filter.process('This is a big string with €bad#characaters¢\nAnd even NewLines')) + .toEqual('This-is-a-big-string-with--bad-characaters--And-even-NewLines'); + }); +}); \ No newline at end of file diff --git a/tools/docs/angular.io-package/rendering/trimBlankLines.js b/tools/docs/angular.io-package/rendering/trimBlankLines.js new file mode 100644 index 0000000000000..37615955c60d1 --- /dev/null +++ b/tools/docs/angular.io-package/rendering/trimBlankLines.js @@ -0,0 +1,15 @@ +module.exports = function() { + return { + name: 'trimBlankLines', + process: function(str) { + var lines = str.split(/\r?\n/); + while (lines.length && (lines[0].trim() === '')) { + lines.shift(); + } + while (lines.length && (lines[lines.length - 1].trim() === '')) { + lines.pop(); + } + return lines.join('\n'); + } + }; +}; \ No newline at end of file diff --git a/tools/docs/angular.io-package/rendering/trimBlankLines.spec.js b/tools/docs/angular.io-package/rendering/trimBlankLines.spec.js new file mode 100644 index 0000000000000..77a0c8e01d3b3 --- /dev/null +++ b/tools/docs/angular.io-package/rendering/trimBlankLines.spec.js @@ -0,0 +1,15 @@ +var factory = require('./trimBlankLines'); + +describe('trimBlankLines filter', function() { + var filter; + + beforeEach(function() { filter = factory(); }); + + it('should be called "trimBlankLines"', + function() { expect(filter.name).toEqual('trimBlankLines'); }); + + it('should remove empty lines from the start and end of the string', function() { + expect(filter.process('\n \n\nsome text\n \nmore text\n \n')) + .toEqual('some text\n \nmore text'); + }); +}); \ No newline at end of file diff --git a/tools/docs/angular.io-package/tag-defs/Annotation.js b/tools/docs/angular.io-package/tag-defs/Annotation.js new file mode 100644 index 0000000000000..4e9c8714cbcc3 --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/Annotation.js @@ -0,0 +1,4 @@ +// A ts2dart compiler annotation that can be ignored in API docs. +module.exports = function() { + return {name: 'Annotation', ignore: true}; +}; diff --git a/tools/docs/angular.io-package/tag-defs/deprecated.js b/tools/docs/angular.io-package/tag-defs/deprecated.js new file mode 100644 index 0000000000000..7a984c75ac005 --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/deprecated.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'deprecated'}; +}; diff --git a/tools/docs/angular.io-package/tag-defs/docsNotRequired.js b/tools/docs/angular.io-package/tag-defs/docsNotRequired.js new file mode 100644 index 0000000000000..8bc386388c754 --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/docsNotRequired.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'docsNotRequired'}; +}; diff --git a/tools/docs/angular.io-package/tag-defs/experimental.js b/tools/docs/angular.io-package/tag-defs/experimental.js new file mode 100644 index 0000000000000..4f679b5c14202 --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/experimental.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'experimental'}; +}; diff --git a/tools/docs/angular.io-package/tag-defs/howToUse.js b/tools/docs/angular.io-package/tag-defs/howToUse.js new file mode 100644 index 0000000000000..f6073bb87ab55 --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/howToUse.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'howToUse'}; +}; diff --git a/tools/docs/angular.io-package/tag-defs/internal.js b/tools/docs/angular.io-package/tag-defs/internal.js new file mode 100644 index 0000000000000..422b6b147a86c --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/internal.js @@ -0,0 +1,5 @@ +module.exports = function() { + return { + name: 'internal', transforms: function() { return true; } + } +}; diff --git a/tools/docs/angular.io-package/tag-defs/ngModule.js b/tools/docs/angular.io-package/tag-defs/ngModule.js new file mode 100644 index 0000000000000..a80c7df32a911 --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/ngModule.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'ngModule'}; +}; diff --git a/tools/docs/angular.io-package/tag-defs/no-description.js b/tools/docs/angular.io-package/tag-defs/no-description.js new file mode 100644 index 0000000000000..9a105df8dba76 --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/no-description.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'noDescription', transforms: function() { return true; }}; +}; \ No newline at end of file diff --git a/tools/docs/angular.io-package/tag-defs/security.js b/tools/docs/angular.io-package/tag-defs/security.js new file mode 100644 index 0000000000000..1351061d996a8 --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/security.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'security'}; +}; diff --git a/tools/docs/angular.io-package/tag-defs/stable.js b/tools/docs/angular.io-package/tag-defs/stable.js new file mode 100644 index 0000000000000..08c5077d74009 --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/stable.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'stable'}; +}; diff --git a/tools/docs/angular.io-package/tag-defs/syntax.js b/tools/docs/angular.io-package/tag-defs/syntax.js new file mode 100644 index 0000000000000..45c5a53cf92dc --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/syntax.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'syntax'}; +}; \ No newline at end of file diff --git a/tools/docs/angular.io-package/tag-defs/ts2dart_const.js b/tools/docs/angular.io-package/tag-defs/ts2dart_const.js new file mode 100644 index 0000000000000..68c43bb23bc19 --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/ts2dart_const.js @@ -0,0 +1,4 @@ +// A ts2dart compiler annotation that can be ignored in API docs. +module.exports = function() { + return {name: 'ts2dart_const', ignore: true}; +}; diff --git a/tools/docs/angular.io-package/tag-defs/whatItDoes.js b/tools/docs/angular.io-package/tag-defs/whatItDoes.js new file mode 100644 index 0000000000000..f11bb62a18f70 --- /dev/null +++ b/tools/docs/angular.io-package/tag-defs/whatItDoes.js @@ -0,0 +1,3 @@ +module.exports = function() { + return {name: 'whatItDoes'}; +}; diff --git a/tools/docs/cheatsheet-package/index.js b/tools/docs/cheatsheet-package/index.js new file mode 100644 index 0000000000000..d657cde1d99d4 --- /dev/null +++ b/tools/docs/cheatsheet-package/index.js @@ -0,0 +1,16 @@ +var Package = require('dgeni').Package; + +module.exports = new Package( + 'cheatsheet', + [ + require('../content-package'), require('../target-package'), + require('dgeni-packages/git'), require('dgeni-packages/nunjucks') + ]) + + .factory(require('./services/cheatsheetItemParser')) + .processor(require('./processors/createCheatsheetDoc')) + + .config(function(parseTagsProcessor, getInjectables) { + parseTagsProcessor.tagDefinitions = parseTagsProcessor.tagDefinitions.concat( + getInjectables(require('./tag-defs'))); + }); diff --git a/tools/docs/cheatsheet-package/processors/createCheatsheetDoc.js b/tools/docs/cheatsheet-package/processors/createCheatsheetDoc.js new file mode 100644 index 0000000000000..2a717f7cedc18 --- /dev/null +++ b/tools/docs/cheatsheet-package/processors/createCheatsheetDoc.js @@ -0,0 +1,48 @@ +var _ = require('lodash'); + +module.exports = function createCheatsheetDoc( + createDocMessage, renderMarkdown, versionInfo, targetEnvironments) { + return { + $runAfter: ['processing-docs'], + $runBefore: ['docs-processed'], + $process: function(docs) { + + var currentEnvironment = targetEnvironments.isActive('ts') && 'TypeScript' || + targetEnvironments.isActive('js') && 'JavaScript' || + targetEnvironments.isActive('dart') && 'Dart'; + + var cheatsheetDoc = { + id: 'cheatsheet', + aliases: ['cheatsheet'], + docType: 'cheatsheet-data', + sections: [], + version: versionInfo, + currentEnvironment: currentEnvironment + }; + + docs = docs.filter(function(doc) { + if (doc.docType === 'cheatsheet-section') { + var section = _.pick(doc, ['name', 'description', 'items', 'index']); + + // Let's make sure that the descriptions are rendered as markdown + section.description = renderMarkdown(section.description); + section.items.forEach(function(item) { + item.description = renderMarkdown(item.description); + }); + + + cheatsheetDoc.sections.push(section); + return false; + } + return true; + }); + + // Sort the sections by their index + cheatsheetDoc.sections.sort(function(a, b) { return a.index - b.index; }); + + docs.push(cheatsheetDoc); + + return docs; + } + }; +}; \ No newline at end of file diff --git a/tools/docs/cheatsheet-package/services/cheatsheetItemParser.js b/tools/docs/cheatsheet-package/services/cheatsheetItemParser.js new file mode 100644 index 0000000000000..03d176be525a8 --- /dev/null +++ b/tools/docs/cheatsheet-package/services/cheatsheetItemParser.js @@ -0,0 +1,125 @@ +/** + * @dgService + * @description + * Parse the text from a cheatsheetItem tag into a cheatsheet item object + * The text must contain a syntax block followed by zero or more bold matchers and finally a + * description + * The syntax block and bold matchers must be wrapped in backticks and be separated by pipes. + * For example + * + * ``` + * ` + * ... + * ... + * ... + *`|`[ng-switch]`|`[ng-switch-when]`|`ng-switch-when`|`ng-switch-default` + * Conditionally swaps the contents of the div by selecting one of the embedded templates based on + * the current value of conditionExpression. + * ``` + * + * will be parsed into + * + * ``` + * { + * syntax: '\n'+ + * ' ...\n'+ + * ' ...\n'+ + * ' ...\n'+ + * '', + * bold: ['[ng-switch]', '[ng-switch-when]', 'ng-switch-when', 'ng-switch-default'], + * description: 'Conditionally swaps the contents of the div by selecting one of the embedded + * templates based on the current value of conditionExpression.' + * } + * ``` + */ +module.exports = + function cheatsheetItemParser(targetEnvironments) { + return function(text) { + var fields = getFields(text, ['syntax', 'description']); + + var item = {syntax: '', bold: [], description: ''}; + + fields.forEach(function(field) { + if (!field.languages || targetEnvironments.someActive(field.languages)) { + switch (field.name) { + case 'syntax': + parseSyntax(field.value.trim()); + break; + case 'description': + item.description = field.value.trim(); + break; + } + } + }); + + return item; + + function parseSyntax(text) { + var index = 0; + + if (text.charAt(index) !== '`') throw new Error('item syntax must start with a backtick'); + + var start = index + 1; + index = text.indexOf('`', start); + if (index === -1) throw new Error('item syntax must end with a backtick'); + item.syntax = text.substring(start, index); + start = index + 1; + + // skip to next pipe + while (index < text.length && text.charAt(index) !== '|') index += 1; + + while (text.charAt(start) === '|') { + start += 1; + + // skip whitespace + while (start < text.length && /\s/.test(text.charAt(start))) start++; + + if (text.charAt(start) !== '`') throw new Error('bold matcher must start with a backtick'); + + start += 1; + index = text.indexOf('`', start); + if (index === -1) throw new Error('bold matcher must end with a backtick'); + item.bold.push(text.substring(start, index)); + start = index + 1; + } + + if (start !== text.length) { + throw new Error( + 'syntax field must only contain a syntax code block and zero or more bold ' + + 'matcher code blocks, delimited by pipes.\n' + + 'Instead it was "' + text + '"'); + } + } + }; +} + + +function getFields(text, fieldNames) { + var FIELD_START = /^([^:(]+)\(?([^)]+)?\)?:$/; + var lines = text.split('\n'); + var fields = []; + var field, line; + while (lines.length) { + line = lines.shift(); + var match = FIELD_START.exec(line); + if (match && fieldNames.indexOf(match[1]) !== -1) { + // start new field + if (field) { + fields.push(field); + } + field = {name: match[1], languages: (match[2] && match[2].split(' ')), value: ''}; + } else { + if (!field) + throw new Error( + 'item must start with one of the following field specifiers:\n' + + fieldNames.map(function(field) { return field + ':'; }).join('\n') + '\n' + + 'but instead it contained: "' + text + '"'); + field.value += line + '\n'; + } + } + if (field) { + fields.push(field); + } + + return fields; +} \ No newline at end of file diff --git a/tools/docs/cheatsheet-package/services/cheatsheetItemParser.spec.js b/tools/docs/cheatsheet-package/services/cheatsheetItemParser.spec.js new file mode 100644 index 0000000000000..38b60d2dcf94f --- /dev/null +++ b/tools/docs/cheatsheet-package/services/cheatsheetItemParser.spec.js @@ -0,0 +1,75 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); + +describe('cheatsheetItemParser', function() { + var dgeni, injector, cheatsheetItemParser; + + beforeEach(function() { + dgeni = new Dgeni([testPackage('cheatsheet-package')]); + injector = dgeni.configureInjector(); + cheatsheetItemParser = injector.get('cheatsheetItemParser'); + var targetEnvironments = injector.get('targetEnvironments'); + targetEnvironments.addAllowed('js'); + targetEnvironments.addAllowed('ts', true); + }); + + describe('no language targets', function() { + it('should extract the syntax', function() { + expect(cheatsheetItemParser('syntax:\n`abc`')) + .toEqual({syntax: 'abc', bold: [], description: ''}); + }); + + it('should extract the bolds', function() { + expect(cheatsheetItemParser('syntax:\n`abc`|`bold1`|`bold2`')) + .toEqual({syntax: 'abc', bold: ['bold1', 'bold2'], description: ''}); + }); + + it('should extract the description', function() { + expect(cheatsheetItemParser('syntax:\n`abc`|`bold1`|`bold2`\ndescription:\nsome description')) + .toEqual({syntax: 'abc', bold: ['bold1', 'bold2'], description: 'some description'}); + }); + + it('should allow bold to be optional', function() { + expect(cheatsheetItemParser('syntax:\n`abc`\ndescription:\nsome description')) + .toEqual({syntax: 'abc', bold: [], description: 'some description'}); + }); + + it('should allow whitespace between the parts', function() { + expect(cheatsheetItemParser( + 'syntax:\n`abc`| `bold1`| `bold2`\ndescription:\n\nsome description')) + .toEqual({syntax: 'abc', bold: ['bold1', 'bold2'], description: 'some description'}); + }); + }); + + describe('with language targets', function() { + it('should extract the active language', function() { + expect(cheatsheetItemParser( + 'syntax(ts):\n`abc`|`bold1`|`bold2`\ndescription(ts):\nsome description')) + .toEqual({syntax: 'abc', bold: ['bold1', 'bold2'], description: 'some description'}); + }); + + it('should ignore the non-active language', function() { + expect(cheatsheetItemParser( + 'syntax(js):\n`abc`|`bold1`|`bold2`\ndescription(js):\nsome description')) + .toEqual({syntax: '', bold: [], description: ''}); + }); + + it('should select the active language and ignore non-active language', function() { + expect(cheatsheetItemParser( + 'syntax(js):\n`JS`|`boldJS``\n' + + 'syntax(ts):\n`TS`|`boldTS`\n' + + 'description(js):\nJS description\n' + + 'description(ts):\nTS description')) + .toEqual({syntax: 'TS', bold: ['boldTS'], description: 'TS description'}); + }); + + it('should error if a language target is used that is not allowed', function() { + expect(function() { + cheatsheetItemParser( + 'syntax(dart):\n`abc`|`bold1`|`bold2`\ndescription(ts):\nsome description'); + }) + .toThrowError( + 'Error accessing target "dart". It is not in the list of allowed targets: js,ts'); + }); + }); +}); \ No newline at end of file diff --git a/tools/docs/cheatsheet-package/tag-defs/cheatsheet-index.js b/tools/docs/cheatsheet-package/tag-defs/cheatsheet-index.js new file mode 100644 index 0000000000000..6a43b09e2b962 --- /dev/null +++ b/tools/docs/cheatsheet-package/tag-defs/cheatsheet-index.js @@ -0,0 +1,14 @@ +module.exports = function(createDocMessage) { + return { + name: 'cheatsheetIndex', + docProperty: 'index', + transforms: function(doc, tag, value) { + try { + return parseInt(value, 10); + } catch (x) { + throw new Error( + createDocMessage('"@' + tag.tagName + '" must be followed by a number', doc)); + } + } + }; +}; \ No newline at end of file diff --git a/tools/docs/cheatsheet-package/tag-defs/cheatsheet-item.js b/tools/docs/cheatsheet-package/tag-defs/cheatsheet-item.js new file mode 100644 index 0000000000000..4f293903fcabd --- /dev/null +++ b/tools/docs/cheatsheet-package/tag-defs/cheatsheet-item.js @@ -0,0 +1,15 @@ +module.exports = function(createDocMessage, cheatsheetItemParser) { + return { + name: 'cheatsheetItem', + multi: true, + docProperty: 'items', + transforms: function(doc, tag, value) { + try { + return cheatsheetItemParser(value); + } catch (x) { + throw new Error(createDocMessage( + '"@' + tag.tagName + '" tag has an invalid format - ' + x.message, doc)); + } + } + }; +}; \ No newline at end of file diff --git a/tools/docs/cheatsheet-package/tag-defs/cheatsheet-section.js b/tools/docs/cheatsheet-package/tag-defs/cheatsheet-section.js new file mode 100644 index 0000000000000..62399a5ccae67 --- /dev/null +++ b/tools/docs/cheatsheet-package/tag-defs/cheatsheet-section.js @@ -0,0 +1,10 @@ +module.exports = function() { + return { + name: 'cheatsheetSection', + docProperty: 'docType', + transforms: function(doc, tag, value) { + doc.name = value ? value.trim() : ''; + return 'cheatsheet-section'; + } + }; +}; diff --git a/tools/docs/cheatsheet-package/tag-defs/index.js b/tools/docs/cheatsheet-package/tag-defs/index.js new file mode 100644 index 0000000000000..cde6ecd667d32 --- /dev/null +++ b/tools/docs/cheatsheet-package/tag-defs/index.js @@ -0,0 +1,2 @@ +module.exports = + [require('./cheatsheet-section'), require('./cheatsheet-index'), require('./cheatsheet-item')]; \ No newline at end of file diff --git a/tools/docs/content-package/index.js b/tools/docs/content-package/index.js new file mode 100644 index 0000000000000..c59a98c5e1707 --- /dev/null +++ b/tools/docs/content-package/index.js @@ -0,0 +1,35 @@ +var Package = require('dgeni').Package; +var jsdocPackage = require('dgeni-packages/jsdoc'); +var linksPackage = require('../links-package'); +var path = require('canonical-path'); +var fs = require('fs'); + +// Define the dgeni package for generating the docs +module.exports = new Package('content', [jsdocPackage, linksPackage]) + + // Register the services and file readers + .factory(require('./readers/content')) + + // Configure file reading + .config(function(readFilesProcessor, contentFileReader) { + readFilesProcessor.fileReaders.push(contentFileReader); + }) + + // Configure ids and paths + .config(function(computeIdsProcessor, computePathsProcessor) { + + computeIdsProcessor.idTemplates.push({ + docTypes: ['content'], + getId: function(doc) { + return doc.fileInfo + .relativePath + // path should be relative to `modules` folder + .replace(/.*\/?modules\//, '') + // path should not include `/docs/` + .replace(/\/docs\//, '/') + // path should not have a suffix + .replace(/\.\w*$/, ''); + }, + getAliases: function(doc) { return [doc.id]; } + }); + }); diff --git a/tools/docs/content-package/readers/content.js b/tools/docs/content-package/readers/content.js new file mode 100644 index 0000000000000..92aac21c68928 --- /dev/null +++ b/tools/docs/content-package/readers/content.js @@ -0,0 +1,26 @@ +var path = require('canonical-path'); + +/** + * @dgService + * @description + * This file reader will pull the contents from a text file (by default .md) + * + * The doc will initially have the form: + * ``` + * { + * content: 'the content of the file', + * startingLine: 1 + * } + * ``` + */ +module.exports = function contentFileReader() { + return { + name: 'contentFileReader', + defaultPattern: /\.md$/, + getDocs: function(fileInfo) { + + // We return a single element array because content files only contain one document + return [{docType: 'guide', content: fileInfo.content, startingLine: 1}]; + } + }; +}; \ No newline at end of file diff --git a/tools/docs/content-package/readers/content.spec.js b/tools/docs/content-package/readers/content.spec.js new file mode 100644 index 0000000000000..53d8fc856d7e3 --- /dev/null +++ b/tools/docs/content-package/readers/content.spec.js @@ -0,0 +1,44 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); +var path = require('canonical-path'); + +describe('contentFileReader', function() { + var dgeni, injector, fileReader; + + beforeEach(function() { + dgeni = new Dgeni([testPackage('content-package', true)]); + injector = dgeni.configureInjector(); + fileReader = injector.get('contentFileReader'); + }); + + var createFileInfo = function(file, content, basePath) { + return { + fileReader: fileReader.name, + filePath: file, + baseName: path.basename(file, path.extname(file)), + extension: path.extname(file).replace(/^\./, ''), + basePath: basePath, + relativePath: path.relative(basePath, file), + content: content + }; + }; + + describe('defaultPattern', function() { + it('should match .md files', function() { + expect(fileReader.defaultPattern.test('abc.md')).toBeTruthy(); + expect(fileReader.defaultPattern.test('abc.js')).toBeFalsy(); + }); + }); + + + describe('getDocs', function() { + it('should return an object containing info about the file and its contents', function() { + var fileInfo = createFileInfo( + 'project/path/modules/someModule/foo/docs/subfolder/bar.ngdoc', 'A load of content', + 'project/path'); + expect(fileReader.getDocs(fileInfo)).toEqual([ + {docType: 'guide', content: 'A load of content', startingLine: 1} + ]); + }); + }); +}); diff --git a/tools/docs/eslintrc.js b/tools/docs/eslintrc.js new file mode 100644 index 0000000000000..a48ebde2a733f --- /dev/null +++ b/tools/docs/eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + 'globals': {'describe': true, 'beforeEach': true, 'it': true, 'expect': true}, + 'env': {'node': true}, + 'extends': 'eslint:recommended', + 'rules': { + 'indent': ['error', 2], + 'linebreak-style': ['error', 'unix'], + 'quotes': ['error', 'single'], + 'semi': ['error', 'always'] + } +}; \ No newline at end of file diff --git a/tools/docs/examples-package/file-readers/example-reader.js b/tools/docs/examples-package/file-readers/example-reader.js new file mode 100644 index 0000000000000..536afd7a05c38 --- /dev/null +++ b/tools/docs/examples-package/file-readers/example-reader.js @@ -0,0 +1,14 @@ +/** + * The point of this reader is to tag all the files that are going to be used as examples in the + * documentation. + * Later on we can extract the regions, via "shredding"; and we can also construct runnable examples + * for passing to plunker and the like. + */ +module.exports = function exampleFileReader(log) { + return { + name: 'exampleFileReader', + getDocs: function(fileInfo) { + return [{docType: 'example-file', content: fileInfo.content, startingLine: 1}]; + } + }; +}; diff --git a/tools/docs/examples-package/index.js b/tools/docs/examples-package/index.js new file mode 100644 index 0000000000000..f4bd01a25470d --- /dev/null +++ b/tools/docs/examples-package/index.js @@ -0,0 +1,29 @@ +var Package = require('dgeni').Package; +var jsdocPackage = require('dgeni-packages/jsdoc'); + +module.exports = + new Package('examples', [jsdocPackage]) + + .factory(require('./inline-tag-defs/example')) + // .factory(require('./inline-tag-defs/exampleTabs')) + .factory(require('./services/parseArgString')) + .factory(require('./services/getExampleFilename')) + .factory(require('./services/example-map')) + .factory(require('./file-readers/example-reader')) + .factory(require('./services/region-parser')) + + .processor(require('./processors/collect-examples')) + + .config(function(readFilesProcessor, exampleFileReader) { + readFilesProcessor.fileReaders.push(exampleFileReader); + }) + + .config(function(inlineTagProcessor, exampleInlineTagDef) { + inlineTagProcessor.inlineTagDefinitions.push(exampleInlineTagDef); + // inlineTagProcessor.inlineTagDefinitions.push(exampleTabsInlineTagDef); + }) + + .config(function(computePathsProcessor) { + computePathsProcessor.pathTemplates.push( + {docTypes: ['example-region'], getPath: function() {}, getOutputPath: function() {}}); + }); diff --git a/tools/docs/examples-package/inline-tag-defs/example.js b/tools/docs/examples-package/inline-tag-defs/example.js new file mode 100644 index 0000000000000..a86f0d744c5c8 --- /dev/null +++ b/tools/docs/examples-package/inline-tag-defs/example.js @@ -0,0 +1,57 @@ +var path = require('canonical-path'); +var fs = require('fs'); +var entities = require('entities'); + +/** + * @dgService exampleInlineTagDef + * @description + * Process inline example tags (of the form {@example relativePath region -title='some title' + * -stylePattern='{some style pattern}' }), + * replacing them with code from a shredded file + * Examples: + * {@example core/application_spec.ts hello-app -title='Sample component' } + * {@example core/application_spec.ts -region=hello-app -title='Sample component' } + * @kind function + */ +module.exports = function exampleInlineTagDef( + parseArgString, exampleMap, getExampleFilename, createDocMessage, log, collectExamples) { + return { + name: 'example', + description: + 'Process inline example tags (of the form {@example some/uri Some Title}), replacing them with HTML anchors', + + + handler: function(doc, tagName, tagDescription) { + const EXAMPLES_FOLDER = collectExamples.exampleFolders[0]; + + var tagArgs = parseArgString(entities.decodeHTML(tagDescription)); + + var unnamedArgs = tagArgs._; + var relativePath = unnamedArgs[0]; + var regionName = tagArgs.region || (unnamedArgs.length > 1 ? unnamedArgs[1] : null); + var title = tagArgs.title || (unnamedArgs.length > 2 ? unnamedArgs[2] : null); + var stylePattern = tagArgs.stylePattern; // TODO: not yet implemented here + + var exampleFile = exampleMap[EXAMPLES_FOLDER][relativePath]; + if (!exampleFile) { + log.error( + createDocMessage('Missing example file... relativePath: "' + relativePath + '".', doc)); + log.error( + 'Example files available are:', Object.keys(exampleMap[EXAMPLES_FOLDER]).join('\n')); + return ''; + } + + var sourceCode = exampleFile.regions[regionName]; + if (!sourceCode) { + log.error(createDocMessage( + 'Missing example region... relativePath: "' + relativePath + '", region: "' + + regionName + '".', + doc)); + log.error('Regions available are:', Object.keys[exampleFile.regions]); + return ''; + } + + return sourceCode.renderedContent; + } + }; +}; diff --git a/tools/docs/examples-package/inline-tag-defs/exampleTabs.js b/tools/docs/examples-package/inline-tag-defs/exampleTabs.js new file mode 100644 index 0000000000000..a141b83bcde7f --- /dev/null +++ b/tools/docs/examples-package/inline-tag-defs/exampleTabs.js @@ -0,0 +1,61 @@ +var path = require('canonical-path'); +var fs = require('fs'); + +/** + * @dgService exampleTabsInlineTagDef + * @description + * Process inline example tags (of the form {@example relativePath region -title='some title' + * -stylePattern='{some style pattern}' }), + * replacing them with a jade makeExample mixin call. + * Examples: + * {@exampleTabs core/application_spec.ts,core/application_spec.ts "hello-app,hello-app2" + * -titles="Hello app1, Hello app2" } + * {@exampleTabs core/application_spec.ts,core/application_spec.ts regions="hello-app,hello-app2" + * -titles="Hello app1, Hello app2" } + * @kind function + */ +module.exports = function exampleTabsInlineTagDef( + getLinkInfo, parseArgString, createDocMessage, log) { + return { + name: 'exampleTabs', + description: + 'Process inline example tags (of the form {@example some/uri Some Title}), replacing them with HTML anchors', + handler: function(doc, tagName, tagDescription) { + + var tagArgs = parseArgString(tagDescription); + var unnamedArgs = tagArgs._; + var relativePaths = unnamedArgs[0].split(','); + var regions = tagArgs.regions || (unnamedArgs.length > 1 ? unnamedArgs[1] : null); + var titles = tagArgs.titles || (unnamedArgs.length > 2 ? unnamedArgs[2] : null); + if (regions) { + regions = regions.split(','); + } + + // TODO: not yet implemented here + var stylePatterns = tagArgs.stylePattern; + + var mixinPaths = relativePaths.map(function(relativePath, ix) { + var fragFileName = getApiFragmentFileName(relativePath, regions && regions[ix]); + if (!fs.existsSync(fragFileName)) { + // TODO: log.warn(createDocMessage('Invalid example (unable to locate fragment file: ' + + // quote(fragFileName) + ")", doc)); + } + return path.join('_api', relativePath); + }); + + var comma = ', ' + var pathsArg = quote(mixinPaths.join(',')); + var regionsArg = regions ? quote(regions.join(',')) : 'null'; + var titlesArg = titles ? quote(titles) : 'null'; + var res = ['+makeTabs(', pathsArg, comma, regionsArg, comma, titlesArg, ')'].join(''); + return res; + } + + }; +}; + +function quote(str) { + if (str == null || str.length === 0) return str; + str = str.replace('\'', '\'\''); + return '\'' + str + '\''; +} diff --git a/tools/docs/examples-package/processors/collect-examples.js b/tools/docs/examples-package/processors/collect-examples.js new file mode 100644 index 0000000000000..d883b772f91ee --- /dev/null +++ b/tools/docs/examples-package/processors/collect-examples.js @@ -0,0 +1,67 @@ +const {mapObject} = require('../utils'); + +module.exports = function collectExamples(exampleMap, regionParser, log, createDocMessage) { + return { + $runAfter: ['files-read'], + $runBefore: ['parsing-tags'], + $validate: {exampleFolders: {presence: true}}, + $process: function(docs) { + const exampleFolders = this.exampleFolders; + const regionDocs = []; + docs = docs.filter((doc) => { + if (doc.docType === 'example-file') { + try { + // find the first matching folder + exampleFolders.some((folder) => { + if (doc.fileInfo.relativePath.indexOf(folder) === 0) { + const relativePath = + doc.fileInfo.relativePath.substr(folder.length).replace(/^\//, ''); + exampleMap[folder] = exampleMap[folder] || {}; + exampleMap[folder][relativePath] = doc; + + const parsedRegions = regionParser(doc.content, doc.fileInfo.extension); + + log.debug( + 'found example file', folder, relativePath, Object.keys(parsedRegions.regions)); + + doc.renderedContent = parsedRegions.contents; + + // Map each region into a doc that can be put through the rendering pipeline + doc.regions = mapObject(parsedRegions.regions, (regionName, regionContents) => { + const regionDoc = + createRegionDoc(folder, relativePath, regionName, regionContents); + regionDocs.push(regionDoc); + return regionDoc; + }); + + return true; + } + }); + + return false; + + } catch (e) { + throw new Error(createDocMessage(e.message, doc, e)); + } + } else { + return true; + } + }); + + return docs.concat(regionDocs); + } + }; +}; + +function createRegionDoc(folder, relativePath, regionName, regionContents) { + const path = folder + '/' + relativePath; + const id = path + '#' + regionName + return { + docType: 'example-region', + path: path, + name: regionName, + id: id, + aliases: [id], + contents: regionContents + }; +} \ No newline at end of file diff --git a/tools/docs/examples-package/processors/collect-examples.spec.js b/tools/docs/examples-package/processors/collect-examples.spec.js new file mode 100644 index 0000000000000..e2fc5bffee672 --- /dev/null +++ b/tools/docs/examples-package/processors/collect-examples.spec.js @@ -0,0 +1,193 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); +var path = require('path'); + +describe('collectExampleRegions processor', () => { + var injector, processor, exampleMap, regionParser; + + beforeEach(function() { + + regionParser = jasmine.createSpy('regionParser').and.callFake(function(contents, extension) { + return { contents: 'PARSED:' + contents, regions: {dummy: extension} } + }); + + const dgeni = + new Dgeni([testPackage('examples-package', true).factory('regionParser', function() { + return regionParser; + })]); + + injector = dgeni.configureInjector(); + exampleMap = injector.get('exampleMap'); + processor = injector.get('collectExamples'); + + processor.exampleFolders = ['examples-1', 'examples-2']; + }); + + it('should identify example files that are in the exampleFolders', () => { + const docs = [ + createDoc('A', 'examples-1/x/app.js'), createDoc('B', 'examples-1/y/index.html'), + createDoc('C', 'examples-2/s/app.js'), createDoc('D', 'examples-2/t/style.css'), + createDoc('E', 'other/b/c.js') + ]; + + processor.$process(docs); + + expect(exampleMap['examples-1']['x/app.js']).toBeDefined(); + expect(exampleMap['examples-1']['y/index.html']).toBeDefined(); + expect(exampleMap['examples-2']['s/app.js']).toBeDefined(); + expect(exampleMap['examples-2']['t/style.css']).toBeDefined(); + + expect(exampleMap['other']).toBeUndefined(); + }); + + it('should remove example files from the docs collection', () => { + const docs = [ + createDoc('Example A', 'examples-1/x/app.js'), + createDoc('Example B', 'examples-1/y/index.html'), + createDoc('Other doc 1', 'examples-2/t/style.css', 'content'), + createDoc('Example C', 'examples-2/s/app.js'), + createDoc('Other doc 2', 'other/b/c.js', 'content') + ]; + + const processedDocs = processor.$process(docs); + + expect(processedDocs.filter(doc => doc.docType === 'example-file')).toEqual([]); + }); + + it('should not remove docs from the docs collection that are not example files', () => { + const docs = [ + createDoc('Example A', 'examples-1/x/app.js'), + createDoc('Example B', 'examples-1/y/index.html'), + createDoc('Other doc 1', 'examples-2/t/style.css', 'content'), + createDoc('Example C', 'examples-2/s/app.js'), + createDoc('Other doc 2', 'other/b/c.js', 'content') + ]; + + const processedDocs = processor.$process(docs); + + expect(processedDocs.filter(doc => doc.docType !== 'example-file')) + .toEqual(jasmine.objectContaining([ + createDoc('Other doc 1', 'examples-2/t/style.css', 'content'), + createDoc('Other doc 2', 'other/b/c.js', 'content') + ])); + }); + + it('should call `regionParser` from with the content and file extension of each example doc', + () => { + const docs = [ + createDoc('Example A', 'examples-1/x/app.js'), + createDoc('Example B', 'examples-1/y/index.html'), + createDoc('Other doc 1', 'examples-2/t/style.css', 'content'), + createDoc('Example C', 'examples-2/s/app.js'), + createDoc('Other doc 2', 'other/b/c.js', 'content') + ]; + + const processedDocs = processor.$process(docs); + + expect(regionParser).toHaveBeenCalledTimes(3); + expect(regionParser).toHaveBeenCalledWith('Example A', 'js'); + expect(regionParser).toHaveBeenCalledWith('Example B', 'html'); + expect(regionParser).toHaveBeenCalledWith('Example C', 'js'); + }); + + + it('should attach parsed content as renderedContent to the example file docs', () => { + const docs = [ + createDoc('A', 'examples-1/x/app.js'), + createDoc('B', 'examples-1/y/index.html'), + createDoc('C', 'examples-2/s/app.js'), + createDoc('D', 'examples-2/t/style.css'), + ]; + + processor.$process(docs); + + expect(exampleMap['examples-1']['x/app.js'].renderedContent).toEqual('PARSED:A'); + expect(exampleMap['examples-1']['y/index.html'].renderedContent).toEqual('PARSED:B'); + expect(exampleMap['examples-2']['s/app.js'].renderedContent).toEqual('PARSED:C'); + expect(exampleMap['examples-2']['t/style.css'].renderedContent).toEqual('PARSED:D'); + + }); + + it('should create region docs for each region in the example file docs', () => { + const docs = [ + createDoc('/* #docregion X */\nA', 'examples-1/x/app.js'), + createDoc('\nB', 'examples-1/y/index.html'), + createDoc('/* #docregion Z */\nC', 'examples-2/t/style.css'), + ]; + + const newDocs = processor.$process(docs); + + expect(newDocs.length).toEqual(3); + expect(newDocs).toEqual([ + jasmine.objectContaining({ + docType: 'example-region', + name: 'dummy', + id: 'examples-1/x/app.js#dummy', + contents: 'js' + }), + jasmine.objectContaining({ + docType: 'example-region', + name: 'dummy', + id: 'examples-1/y/index.html#dummy', + contents: 'html' + }), + jasmine.objectContaining({ + docType: 'example-region', + name: 'dummy', + id: 'examples-2/t/style.css#dummy', + contents: 'css' + }) + ]); + }); + + it('should attach region docs to the example file docs', () => { + const docs = [ + createDoc('/* #docregion X */\nA', 'examples-1/x/app.js'), + createDoc('\nB', 'examples-1/y/index.html'), + createDoc('/* #docregion Z */\nC', 'examples-2/t/style.css'), + ]; + + processor.$process(docs); + + expect(exampleMap['examples-1']['x/app.js'].regions).toEqual({ + dummy: { + docType: 'example-region', + path: 'examples-1/x/app.js', + name: 'dummy', + id: 'examples-1/x/app.js#dummy', + aliases: ['examples-1/x/app.js#dummy'], + contents: 'js' + } + }); + expect(exampleMap['examples-1']['y/index.html'].regions).toEqual({ + dummy: { + docType: 'example-region', + path: 'examples-1/y/index.html', + name: 'dummy', + id: 'examples-1/y/index.html#dummy', + aliases: ['examples-1/y/index.html#dummy'], + contents: 'html' + } + }); + expect(exampleMap['examples-2']['t/style.css'].regions).toEqual({ + dummy: { + docType: 'example-region', + path: 'examples-2/t/style.css', + name: 'dummy', + id: 'examples-2/t/style.css#dummy', + aliases: ['examples-2/t/style.css#dummy'], + contents: 'css' + } + }); + }); +}); + + +function createDoc(content, relativePath, docType) { + return { + fileInfo: {relativePath: relativePath, extension: path.extname(relativePath).substr(1)}, + content: content, + docType: docType || 'example-file', + startingLine: 1 + }; +} \ No newline at end of file diff --git a/tools/docs/examples-package/services/example-map.js b/tools/docs/examples-package/services/example-map.js new file mode 100644 index 0000000000000..86e8614d65aaa --- /dev/null +++ b/tools/docs/examples-package/services/example-map.js @@ -0,0 +1,3 @@ +module.exports = function exampleMap() { + return {}; +}; diff --git a/tools/docs/examples-package/services/getExampleFilename.js b/tools/docs/examples-package/services/getExampleFilename.js new file mode 100644 index 0000000000000..35500d7399462 --- /dev/null +++ b/tools/docs/examples-package/services/getExampleFilename.js @@ -0,0 +1,9 @@ +module.exports = function getExampleFilename() { + + function getExampleFilenameImpl(relativePath) { + return getExampleFilenameImpl.examplesFolder + relativePath; + } + + getExampleFilenameImpl.examplesFolder = '@angular/examples/'; + return getExampleFilenameImpl; +}; diff --git a/tools/docs/examples-package/services/parseArgString.js b/tools/docs/examples-package/services/parseArgString.js new file mode 100644 index 0000000000000..b19ce46be6298 --- /dev/null +++ b/tools/docs/examples-package/services/parseArgString.js @@ -0,0 +1,55 @@ +/** + * @dgService parseArgString + * @description + * processes an arg string in 'almost' the same fashion that the command processor does + * and returns an args object in yargs format. + * @kind function + * @param {String} str The arg string to process + * @return {Object} The args parsed into a yargs format. + */ + +module.exports = function parseArgString() { + + return function parseArgStringImpl(str) { + // regex from npm string-argv + //[^\s'"] Match if not a space ' or " + + //+|['] or Match ' + //([^']*) Match anything that is not ' + //['] Close match if ' + + //+|["] or Match " + //([^"]*) Match anything that is not " + //["] Close match if " + var rx = /[^\s'"]+|[']([^']*?)[']|["]([^"]*?)["]/gi; + var value = str; + var unnammedArgs = []; + var args = {_: unnammedArgs}; + var match, key; + do { + // Each call to exec returns the next regex match as an array + match = rx.exec(value); + if (match !== null) { + // Index 1 in the array is the captured group if it exists + // Index 0 is the matched text, which we use if no captured group exists + var arg = match[2] ? match[2] : (match[1] ? match[1] : match[0]); + if (key) { + args[key] = arg; + key = null; + } else { + if (arg.substr(arg.length - 1) === '=') { + key = arg.substr(0, arg.length - 1); + // remove leading '-' if it exists. + if (key.substr(0, 1) == '-') { + key = key.substr(1); + } + } else { + unnammedArgs.push(arg) + key = null; + } + } + } + } while (match !== null); + return args; + } +} \ No newline at end of file diff --git a/tools/docs/examples-package/services/region-matchers/block-c.js b/tools/docs/examples-package/services/region-matchers/block-c.js new file mode 100644 index 0000000000000..d7464a6460c7b --- /dev/null +++ b/tools/docs/examples-package/services/region-matchers/block-c.js @@ -0,0 +1,7 @@ +// These kind of comments are used CSS and other languages that do not support inline comments +module.exports = { + regionStartMatcher: /^\s*\/\*\s*#docregion\s*(.*)\s*\*\/\s*$/, + regionEndMatcher: /^\s*\/\*\s*#enddocregion\s*(.*)\s*\*\/\s*$/, + plasterMatcher: /^\s*\/\*\s*#docplaster\s*(.*)\s*\*\/\s*$/, + createPlasterComment: plaster => `/* ${plaster} */` +}; \ No newline at end of file diff --git a/tools/docs/examples-package/services/region-matchers/block-c.spec.js b/tools/docs/examples-package/services/region-matchers/block-c.spec.js new file mode 100644 index 0000000000000..68d24e454225b --- /dev/null +++ b/tools/docs/examples-package/services/region-matchers/block-c.spec.js @@ -0,0 +1,55 @@ +const matcher = require('./block-c'); + +describe('block-c region-matcher', () => { + it('should match start annotations', () => { + let matches; + + matches = matcher.regionStartMatcher.exec('/* #docregion A b c */'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c '); + + matches = matcher.regionStartMatcher.exec('/*#docregion A b c*/'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionStartMatcher.exec('/* #docregion */'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should match end annotations', () => { + let matches; + + matches = matcher.regionEndMatcher.exec('/* #enddocregion A b c */'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c '); + + matches = matcher.regionEndMatcher.exec('/*#enddocregion A b c*/'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionEndMatcher.exec('/* #enddocregion */'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should match plaster annotations', () => { + let matches; + + matches = matcher.plasterMatcher.exec('/* #docplaster A b c */'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c '); + + matches = matcher.plasterMatcher.exec('/*#docplaster A b c*/'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.plasterMatcher.exec('/* #docplaster */'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should create a plaster comment', () => { + expect(matcher.createPlasterComment('... elided ...')).toEqual('/* ... elided ... */'); + }); +}); \ No newline at end of file diff --git a/tools/docs/examples-package/services/region-matchers/html.js b/tools/docs/examples-package/services/region-matchers/html.js new file mode 100644 index 0000000000000..9305f2162f77d --- /dev/null +++ b/tools/docs/examples-package/services/region-matchers/html.js @@ -0,0 +1,7 @@ +// These kind of comments are used in HTML +module.exports = { + regionStartMatcher: /^\s*\s*$/, + regionEndMatcher: /^\s*\s*$/, + plasterMatcher: /^\s*\s*$/, + createPlasterComment: plaster => `` +}; diff --git a/tools/docs/examples-package/services/region-matchers/html.spec.js b/tools/docs/examples-package/services/region-matchers/html.spec.js new file mode 100644 index 0000000000000..7a3cbf006b156 --- /dev/null +++ b/tools/docs/examples-package/services/region-matchers/html.spec.js @@ -0,0 +1,55 @@ +const matcher = require('./html'); + +describe('html region-matcher', () => { + it('should match start annotations', () => { + let matches; + + matches = matcher.regionStartMatcher.exec(''); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c '); + + matches = matcher.regionStartMatcher.exec(''); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionStartMatcher.exec(''); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should match end annotations', () => { + let matches; + + matches = matcher.regionEndMatcher.exec(''); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c '); + + matches = matcher.regionEndMatcher.exec(''); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionEndMatcher.exec(''); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should match plaster annotations', () => { + let matches; + + matches = matcher.plasterMatcher.exec(''); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c '); + + matches = matcher.plasterMatcher.exec(''); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.plasterMatcher.exec(''); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should create a plaster comment', () => { + expect(matcher.createPlasterComment('... elided ...')).toEqual(''); + }); +}); \ No newline at end of file diff --git a/tools/docs/examples-package/services/region-matchers/inline-c-only.js b/tools/docs/examples-package/services/region-matchers/inline-c-only.js new file mode 100644 index 0000000000000..90b1d8d9bade5 --- /dev/null +++ b/tools/docs/examples-package/services/region-matchers/inline-c-only.js @@ -0,0 +1,7 @@ +// These kind of comments are used in languages that do not support block comments, such as Jade +module.exports = { + regionStartMatcher: /^\s*\/\/\s*#docregion\s*(.*)\s*$/, + regionEndMatcher: /^\s*\/\/\s*#enddocregion\s*(.*)\s*$/, + plasterMatcher: /^\s*\/\/\s*#docplaster\s*(.*)\s*$/, + createPlasterComment: plaster => `// ${plaster}` +}; diff --git a/tools/docs/examples-package/services/region-matchers/inline-c-only.spec.js b/tools/docs/examples-package/services/region-matchers/inline-c-only.spec.js new file mode 100644 index 0000000000000..b3a8cb694ab06 --- /dev/null +++ b/tools/docs/examples-package/services/region-matchers/inline-c-only.spec.js @@ -0,0 +1,55 @@ +const matcher = require('./inline-c-only'); + +describe('inline-c-only region-matcher', () => { + it('should match start annotations', () => { + let matches; + + matches = matcher.regionStartMatcher.exec('// #docregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionStartMatcher.exec('//#docregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionStartMatcher.exec('// #docregion'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should match end annotations', () => { + let matches; + + matches = matcher.regionEndMatcher.exec('// #enddocregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionEndMatcher.exec('//#enddocregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionEndMatcher.exec('// #enddocregion'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should match plaster annotations', () => { + let matches; + + matches = matcher.plasterMatcher.exec('// #docplaster A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.plasterMatcher.exec('//#docplaster A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.plasterMatcher.exec('// #docplaster'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should create a plaster comment', () => { + expect(matcher.createPlasterComment('... elided ...')).toEqual('// ... elided ...'); + }); +}); \ No newline at end of file diff --git a/tools/docs/examples-package/services/region-matchers/inline-c.js b/tools/docs/examples-package/services/region-matchers/inline-c.js new file mode 100644 index 0000000000000..2c4dc870b36e1 --- /dev/null +++ b/tools/docs/examples-package/services/region-matchers/inline-c.js @@ -0,0 +1,7 @@ +// This comment type is used in C like languages such as JS, TS, Dart, etc +module.exports = { + regionStartMatcher: /^\s*\/\/\s*#docregion\s*(.*)\s*$/, + regionEndMatcher: /^\s*\/\/\s*#enddocregion\s*(.*)\s*$/, + plasterMatcher: /^\s*\/\/\s*#docplaster\s*(.*)\s*$/, + createPlasterComment: plaster => `/* ${plaster} */` +}; diff --git a/tools/docs/examples-package/services/region-matchers/inline-c.spec.js b/tools/docs/examples-package/services/region-matchers/inline-c.spec.js new file mode 100644 index 0000000000000..e148537fe77b4 --- /dev/null +++ b/tools/docs/examples-package/services/region-matchers/inline-c.spec.js @@ -0,0 +1,55 @@ +const matcher = require('./inline-c'); + +describe('inline-c region-matcher', () => { + it('should match start annotations', () => { + let matches; + + matches = matcher.regionStartMatcher.exec('// #docregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionStartMatcher.exec('//#docregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionStartMatcher.exec('// #docregion'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should match end annotations', () => { + let matches; + + matches = matcher.regionEndMatcher.exec('// #enddocregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionEndMatcher.exec('//#enddocregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionEndMatcher.exec('// #enddocregion'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should match plaster annotations', () => { + let matches; + + matches = matcher.plasterMatcher.exec('// #docplaster A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.plasterMatcher.exec('//#docplaster A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.plasterMatcher.exec('// #docplaster'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should create a plaster comment', () => { + expect(matcher.createPlasterComment('... elided ...')).toEqual('/* ... elided ... */'); + }); +}); \ No newline at end of file diff --git a/tools/docs/examples-package/services/region-matchers/inline-hash.js b/tools/docs/examples-package/services/region-matchers/inline-hash.js new file mode 100644 index 0000000000000..1dc975fe3fbc5 --- /dev/null +++ b/tools/docs/examples-package/services/region-matchers/inline-hash.js @@ -0,0 +1,7 @@ +// These type of comments are used in hash comment based languages such as bash and Yaml +module.exports = { + regionStartMatcher: /^\s*#\s*#docregion\s*(.*)\s*$/, + regionEndMatcher: /^\s*#\s*#enddocregion\s*(.*)\s*$/, + plasterMatcher: /^\s*#\s*#docplaster\s*(.*)\s*$/, + createPlasterComment: plaster => `# ${plaster}` +}; diff --git a/tools/docs/examples-package/services/region-matchers/inline-hash.spec.js b/tools/docs/examples-package/services/region-matchers/inline-hash.spec.js new file mode 100644 index 0000000000000..e9b294a1a3403 --- /dev/null +++ b/tools/docs/examples-package/services/region-matchers/inline-hash.spec.js @@ -0,0 +1,54 @@ +const matcher = require('./inline-hash'); + +describe('inline-hash region-matcher', () => { + it('should match start annotations', () => { + let matches; + + matches = matcher.regionStartMatcher.exec('# #docregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionStartMatcher.exec('##docregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionStartMatcher.exec('# #docregion'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should match end annotations', () => { + let matches; + + matches = matcher.regionEndMatcher.exec('# #enddocregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionEndMatcher.exec('##enddocregion A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.regionEndMatcher.exec('# #enddocregion'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should match plaster annotations', () => { + let matches; + + matches = matcher.plasterMatcher.exec('# #docplaster A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.plasterMatcher.exec('##docplaster A b c'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual('A b c'); + + matches = matcher.plasterMatcher.exec('# #docplaster'); + expect(matches).not.toBeNull(); + expect(matches[1]).toEqual(''); + }); + + it('should create a plaster comment', + () => { expect(matcher.createPlasterComment('... elided ...')).toEqual('# ... elided ...'); }); +}); \ No newline at end of file diff --git a/tools/docs/examples-package/services/region-parser.js b/tools/docs/examples-package/services/region-parser.js new file mode 100644 index 0000000000000..69d98e4919b6e --- /dev/null +++ b/tools/docs/examples-package/services/region-parser.js @@ -0,0 +1,112 @@ +const blockC = require('./region-matchers/block-c'); +const html = require('./region-matchers/html'); +const inlineC = require('./region-matchers/inline-c'); +const inlineCOnly = require('./region-matchers/inline-c-only'); +const inlineHash = require('./region-matchers/inline-hash'); +const NO_NAME_REGION = ''; +const DEFAULT_PLASTER = '. . .'; +const {mapObject} = require('../utils'); + +module.exports = function regionParser() { + return regionParserImpl; +}; + +regionParserImpl.regionMatchers = { + ts: inlineC, + js: inlineC, + es6: inlineC, + dart: inlineC, + html: html, + css: blockC, + yaml: inlineHash, + jade: inlineCOnly +}; + +/** + * @param contents string + * @param fileType string + * @returns {contents: string, regions: {[regionName: string]: string}} + */ +function regionParserImpl(contents, fileType) { + const regionMatcher = regionParserImpl.regionMatchers[fileType]; + const openRegions = []; + const regionMap = {}; + + if (regionMatcher) { + let plaster = regionMatcher.createPlasterComment(DEFAULT_PLASTER); + const lines = contents.split(/\r?\n/).filter((line, index) => { + const startRegion = line.match(regionMatcher.regionStartMatcher); + const endRegion = line.match(regionMatcher.regionEndMatcher); + const updatePlaster = line.match(regionMatcher.plasterMatcher); + + // start region processing + if (startRegion) { + // open up the specified region + const regionName = getRegionName(startRegion[1]); + const region = regionMap[regionName]; + if (region) { + if (region.open) { + throw new RegionParserError( + `Tried to open a region, named "${regionName}", that is already open`, index); + } + region.open = true; + region.lines.push(plaster); + } else { + regionMap[regionName] = {lines: [], open: true}; + } + openRegions.push(regionName); + + // end region processing + } else if (endRegion) { + if (openRegions.length === 0) { + throw new RegionParserError('Tried to close a region when none are open', index); + } + // close down the specified region (or most recent if no name is given) + const regionName = getRegionName(endRegion[1]) || openRegions[openRegions.length - 1]; + const region = regionMap[regionName]; + if (!region || !region.open) { + throw new RegionParserError( + `Tried to close a region, named "${regionName}", that is not open`, index); + } + region.open = false; + removeLast(openRegions, regionName); + + // doc plaster processing + } else if (updatePlaster) { + plaster = regionMatcher.createPlasterComment(updatePlaster[1].trim()); + + // simple line of content processing + } else { + openRegions.forEach(regionName => regionMap[regionName].lines.push(line)); + // do not filter out this line from the content + return true; + } + + // this line contained an annotation so let's filter it out + return false; + }); + return { + contents: lines.join('\n'), + regions: mapObject(regionMap, (regionName, region) => region.lines.join('\n')) + }; + } else { + return {contents, regions: {}}; + } +} + +function getRegionName(input) { + return input.trim(); +} + +function removeLast(array, item) { + const index = array.lastIndexOf(item); + array.splice(index, 1); +} + +function RegionParserError(message, lineNum) { + this.message = `regionParser: ${message} (at line ${lineNum}).`; + this.lineNum = lineNum; + this.stack = (new Error()).stack; +} +RegionParserError.prototype = Object.create(Error.prototype); +RegionParserError.prototype.constructor = RegionParserError; \ No newline at end of file diff --git a/tools/docs/examples-package/services/region-parser.spec.js b/tools/docs/examples-package/services/region-parser.spec.js new file mode 100644 index 0000000000000..21bd5be351f24 --- /dev/null +++ b/tools/docs/examples-package/services/region-parser.spec.js @@ -0,0 +1,143 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); + +const testRegionMatcher = { + regionStartMatcher: /^\s*\/\*\s*#docregion\s+(.*)\s*\*\/\s*$/, + regionEndMatcher: /^\s*\/\*\s*#enddocregion\s+(.*)\s*\*\/\s*$/, + plasterMatcher: /^\s*\/\*\s*#docplaster\s+(.*)\s*\*\/\s*$/, + createPlasterComment: plaster => `/* ${plaster} */` +}; + +describe('regionParser service', () => { + var dgeni, injector, regionParser; + + beforeEach(function() { + dgeni = new Dgeni([testPackage('examples-package', true)]); + injector = dgeni.configureInjector(); + regionParser = injector.get('regionParser'); + regionParser.regionMatchers = {'test-type': testRegionMatcher}; + }); + + it('should return just the contents if there is no region-matcher for the file type', () => { + const output = regionParser('some contents', 'unknown'); + expect(output).toEqual({contents: 'some contents', regions: {}}); + }); + + it('should return just the contents if there is a region-matcher but no regions', () => { + const output = regionParser('some contents', 'test-type'); + expect(output).toEqual({contents: 'some contents', regions: {}}); + }); + + it('should remove start region annotations from the contents', () => { + const output = regionParser( + t('/* #docregion */', 'abc', '/* #docregion X */', 'def', '/* #docregion Y */', 'ghi'), + 'test-type'); + expect(output.contents).toEqual(t('abc', 'def', 'ghi')); + }); + + it('should remove end region annotations from the contents', () => { + const output = regionParser( + t('/* #docregion */', 'abc', '/* #docregion X */', 'def', '/* #enddocregion X */', + '/* #docregion Y */', 'ghi', '/* #enddocregion Y */', '/* #enddocregion */'), + 'test-type'); + expect(output.contents).toEqual(t('abc', 'def', 'ghi')); + }); + + + it('should remove doc plaster annotations from the contents', () => { + const output = + regionParser(t('/* #docplaster ... elided ... */', 'abc', 'def', 'ghi'), 'test-type'); + expect(output.contents).toEqual(t('abc', 'def', 'ghi')); + }); + + it('should capture the rest of the contents for a region with no end region annotation', () => { + const output = regionParser( + t('/* #docregion */', 'abc', '/* #docregion X */', 'def', '/* #docregion Y */', 'ghi'), + 'test-type'); + expect(output.regions['']).toEqual(t('abc', 'def', 'ghi')); + expect(output.regions['X']).toEqual(t('def', 'ghi')); + expect(output.regions['Y']).toEqual(t('ghi')); + }); + + + it('should capture the contents for a region up to the end region annotation', () => { + const output = regionParser( + t('/* #docregion */', 'abc', '/* #enddocregion */', '/* #docregion X */', 'def', + '/* #enddocregion X */', '/* #docregion Y */', 'ghi', '/* #enddocregion Y */'), + 'test-type'); + expect(output.regions['']).toEqual(t('abc')); + expect(output.regions['X']).toEqual(t('def')); + expect(output.regions['Y']).toEqual(t('ghi')); + }); + + it('should close the most recently opened region if there is no region name', () => { + const output = regionParser( + t('/* #docregion X*/', 'abc', '/* #docregion Y */', 'def', '/* #enddocregion */', 'ghi', + '/* #enddocregion */'), + 'test-type'); + expect(output.regions['X']).toEqual(t('abc', 'def', 'ghi')); + expect(output.regions['Y']).toEqual(t('def')); + }); + + it('should handle overlapping regions', () => { + const output = regionParser( + t('/* #docregion X*/', 'abc', '/* #docregion Y */', 'def', '/* #enddocregion X */', 'ghi', + '/* #enddocregion Y */'), + 'test-type'); + expect(output.regions['X']).toEqual(t('abc', 'def')); + expect(output.regions['Y']).toEqual(t('def', 'ghi')); + }); + + it('should error if we attempt to open an already open region', () => { + expect(() => regionParser(t('/* #docregion */', 'abc', '/* #docregion */', 'def'), 'test-type')) + .toThrowError( + 'regionParser: Tried to open a region, named "", that is already open (at line 2).'); + + expect( + () => + regionParser(t('/* #docregion X */', 'abc', '/* #docregion X */', 'def'), 'test-type')) + .toThrowError( + 'regionParser: Tried to open a region, named "X", that is already open (at line 2).'); + }); + + it('should error if we attempt to close an already closed region', () => { + expect(() => regionParser(t('abc', '/* #enddocregion */', 'def'), 'test-type')) + .toThrowError('regionParser: Tried to close a region when none are open (at line 1).'); + + expect( + () => + regionParser(t('/* #docregion */', 'abc', '/* #enddocregion X */', 'def'), 'test-type')) + .toThrowError( + 'regionParser: Tried to close a region, named "X", that is not open (at line 2).'); + }); + + it('should handle whitespace in region names on single annotation', () => { + const output = + regionParser(t('/* #docregion A B*/', 'abc', '/* #docregion A C */', 'def'), 'test-type'); + expect(output.regions['A B']).toEqual(t('abc', 'def')); + expect(output.regions['A C']).toEqual(t('def')); + }); + + it('should join multiple regions with the default plaster string (". . .")', () => { + const output = regionParser( + t('/* #docregion */', 'abc', '/* #enddocregion */', 'def', '/* #docregion */', 'ghi', + '/* #enddocregion */'), + 'test-type'); + expect(output.regions['']).toEqual(t('abc', '/* . . . */', 'ghi')); + }); + + + it('should join multiple regions with the current plaster string', () => { + const output = regionParser( + t('/* #docregion */', 'abc', '/* #enddocregion */', 'def', '/* #docregion */', 'ghi', + '/* #enddocregion */', '/* #docplaster ... elided ... */', '/* #docregion A */', 'jkl', + '/* #enddocregion A */', 'mno', '/* #docregion A */', 'pqr', '/* #enddocregion A */'), + 'test-type'); + expect(output.regions['']).toEqual(t('abc', '/* . . . */', 'ghi')); + expect(output.regions['A']).toEqual(t('jkl', '/* ... elided ... */', 'pqr')); + }); +}); + +function t() { + return Array.prototype.join.call(arguments, '\n'); +} diff --git a/tools/docs/examples-package/utils.js b/tools/docs/examples-package/utils.js new file mode 100644 index 0000000000000..d0580917a0eb8 --- /dev/null +++ b/tools/docs/examples-package/utils.js @@ -0,0 +1,7 @@ +module.exports = { + mapObject(obj, mapper) { + const mappedObj = {}; + Object.keys(obj).forEach(key => { mappedObj[key] = mapper(key, obj[key]); }); + return mappedObj; + } +}; \ No newline at end of file diff --git a/tools/docs/helpers/test-package.js b/tools/docs/helpers/test-package.js new file mode 100644 index 0000000000000..c2c02bccdc171 --- /dev/null +++ b/tools/docs/helpers/test-package.js @@ -0,0 +1,23 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +const Package = require('dgeni').Package; + +module.exports = function testPackage(packageName, mockTemplateEngine) { + + const pkg = new Package('mock_' + packageName, [require('../' + packageName)]); + + // provide a mock log service + pkg.factory('log', function() { return require('dgeni/lib/mocks/log')(false); }); + + if (mockTemplateEngine) { + pkg.factory('templateEngine', function() { return {}; }); + } + + return pkg; +}; diff --git a/tools/docs/links-package/index.js b/tools/docs/links-package/index.js new file mode 100644 index 0000000000000..1fab11d2ae355 --- /dev/null +++ b/tools/docs/links-package/index.js @@ -0,0 +1,22 @@ +var Package = require('dgeni').Package; +var jsdocPackage = require('dgeni-packages/jsdoc'); + +module.exports = + new Package('links', [jsdocPackage]) + + .factory(require('./inline-tag-defs/link')) + .factory(require('./services/getAliases')) + .factory(require('./services/getDocFromAlias')) + .factory(require('./services/getLinkInfo')) + .factory(require('./services/moduleScopeLinkDisambiguator')) + .factory(require('./services/deprecatedDocsLinkDisambiguator')) + + .config(function(inlineTagProcessor, linkInlineTagDef) { + inlineTagProcessor.inlineTagDefinitions.push(linkInlineTagDef); + }) + + .config(function( + getLinkInfo, moduleScopeLinkDisambiguator, deprecatedDocsLinkDisambiguator) { + getLinkInfo.disambiguators.push(moduleScopeLinkDisambiguator); + getLinkInfo.disambiguators.push(deprecatedDocsLinkDisambiguator); + }); diff --git a/tools/docs/links-package/inline-tag-defs/link.js b/tools/docs/links-package/inline-tag-defs/link.js new file mode 100644 index 0000000000000..2b24828bad805 --- /dev/null +++ b/tools/docs/links-package/inline-tag-defs/link.js @@ -0,0 +1,35 @@ +var INLINE_LINK = /(\S+)(?:\s+([\s\S]+))?/; + +/** + * @dgService linkInlineTagDef + * @description + * Process inline link tags (of the form {@link some/uri Some Title}), replacing them with HTML anchors + * @kind function + * @param {Object} url The url to match + * @param {Function} docs error message + * @return {String} The html link information + * + * @property {boolean} relativeLinks Whether we expect the links to be relative to the originating doc + */ +module.exports = function linkInlineTagDef(getLinkInfo, createDocMessage, log) { + return { + name: 'link', + aliases: ['linkDocs'], + description: + 'Process inline link tags (of the form {@link some/uri Some Title}), replacing them with HTML anchors', + handler: function(doc, tagName, tagDescription) { + + // Parse out the uri and title + return tagDescription.replace(INLINE_LINK, function(match, uri, title) { + + var linkInfo = getLinkInfo(uri, title, doc); + + if (!linkInfo.valid) { + log.warn(createDocMessage(linkInfo.error, doc)); + } + + return '' + linkInfo.title + ''; + }); + } + }; +}; \ No newline at end of file diff --git a/tools/docs/links-package/services/deprecatedDocsLinkDisambiguator.js b/tools/docs/links-package/services/deprecatedDocsLinkDisambiguator.js new file mode 100644 index 0000000000000..662d3e53e200a --- /dev/null +++ b/tools/docs/links-package/services/deprecatedDocsLinkDisambiguator.js @@ -0,0 +1,12 @@ +var _ = require('lodash'); + +module.exports = function deprecatedDocsLinkDisambiguator() { + return function(url, title, currentDoc, docs) { + if (docs.length != 2) return docs; + + var filteredDocs = _.filter( + docs, function(doc) { return !doc.fileInfo.relativePath.match(/\/(\w+)-deprecated\//); }); + + return filteredDocs.length > 0 ? filteredDocs : docs; + }; +}; diff --git a/tools/docs/links-package/services/getAliases.js b/tools/docs/links-package/services/getAliases.js new file mode 100644 index 0000000000000..1f21a6c80f793 --- /dev/null +++ b/tools/docs/links-package/services/getAliases.js @@ -0,0 +1,71 @@ + +function parseCodeName(codeName) { + var parts = []; + var currentPart; + + codeName.split('.').forEach(function(part) { + var subParts = part.split(':'); + + var name = subParts.pop(); + var modifier = subParts.pop(); + + if (!modifier && currentPart) { + currentPart.name += '.' + name; + } else { + currentPart = {name: name, modifier: modifier}; + parts.push(currentPart); + } + }); + return parts; +} + +/** + * @dgService getAliases + * @description + * Get a list of all the aliases that can be made from the doc + * @param {Object} doc A doc from which to extract aliases + * @return {Array} A collection of aliases + */ +module.exports = function getAliases() { + + return function(doc) { + + var codeNameParts = parseCodeName(doc.id); + + var methodName; + var aliases = []; + // Add the last part to the list of aliases + var part = codeNameParts.pop(); + + // If the name contains a # then it is a member and that should be included in the aliases + if (part.name.indexOf('#') !== -1) { + methodName = part.name.split('#')[1]; + } + // Add the part name and modifier, if provided + aliases.push(part.name); + if (part.modifier) { + aliases.push(part.modifier + ':' + part.name); + } + + // Continue popping off the parts of the codeName and work forward collecting up each alias + aliases = codeNameParts.reduceRight(function(aliases, part) { + + // Add this part to each of the aliases we have so far + aliases.forEach(function(name) { + // Add the part name and modifier, if provided + aliases.push(part.name + '.' + name); + if (part.modifier) { + aliases.push(part.modifier + ':' + part.name + '.' + name); + } + }); + + return aliases; + }, aliases); + + if (methodName) { + aliases.push(methodName); + } + + return aliases; + }; +}; \ No newline at end of file diff --git a/tools/docs/links-package/services/getAliases.spec.js b/tools/docs/links-package/services/getAliases.spec.js new file mode 100644 index 0000000000000..e3f57c7599943 --- /dev/null +++ b/tools/docs/links-package/services/getAliases.spec.js @@ -0,0 +1,14 @@ +var getAliasesFactory = require('./getAliases'); + +describe('getAliases', function() { + + it('should extract all the parts from a code name', function() { + + var getAliases = getAliasesFactory(); + + expect(getAliases({id: 'module:ng.service:$http#get'})).toEqual([ + '$http#get', 'service:$http#get', 'ng.$http#get', 'module:ng.$http#get', + 'ng.service:$http#get', 'module:ng.service:$http#get', 'get' + ]); + }); +}); diff --git a/tools/docs/links-package/services/getDocFromAlias.js b/tools/docs/links-package/services/getDocFromAlias.js new file mode 100644 index 0000000000000..4ae5b6f89b9d5 --- /dev/null +++ b/tools/docs/links-package/services/getDocFromAlias.js @@ -0,0 +1,31 @@ +var _ = require('lodash'); + +/** + * @dgService getDocFromAlias + * @description Get an array of docs that match this alias, relative to the originating doc. + */ +module.exports = function getDocFromAlias(aliasMap, log) { + + return function getDocFromAlias(alias, originatingDoc) { + var docs = aliasMap.getDocs(alias); + + // If there is more than one item with this name then try to filter them by the originatingDoc's + // area + if (docs.length > 1 && originatingDoc && originatingDoc.area) { + docs = _.filter(docs, function(doc) { return doc.area === originatingDoc.area; }); + } + + // If filtering by area left us with none then let's start again + if (docs.length === 0) { + docs = aliasMap.getDocs(alias); + } + + // If there is more than one item with this name then try to filter them by the originatingDoc's + // module + if (docs.length > 1 && originatingDoc && originatingDoc.module) { + docs = _.filter(docs, function(doc) { return doc.module === originatingDoc.module; }); + } + + return docs; + }; +}; \ No newline at end of file diff --git a/tools/docs/links-package/services/getDocFromAlias.spec.js b/tools/docs/links-package/services/getDocFromAlias.spec.js new file mode 100644 index 0000000000000..c5b24a47f296d --- /dev/null +++ b/tools/docs/links-package/services/getDocFromAlias.spec.js @@ -0,0 +1,48 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); + +var getDocFromAlias, aliasMap; + +describe('getDocFromAlias', function() { + beforeEach(function() { + var dgeni = new Dgeni([testPackage('links-package', true)]); + var injector = dgeni.configureInjector(); + aliasMap = injector.get('aliasMap'); + getDocFromAlias = injector.get('getDocFromAlias'); + }); + + it('should return an array of docs that match the alias', function() { + var doc1 = {aliases: ['a', 'b', 'c']}; + var doc2 = {aliases: ['a', 'b']}; + var doc3 = {aliases: ['a']}; + aliasMap.addDoc(doc1); + aliasMap.addDoc(doc2); + aliasMap.addDoc(doc3); + + expect(getDocFromAlias('a')).toEqual([doc1, doc2, doc3]); + expect(getDocFromAlias('b')).toEqual([doc1, doc2]); + expect(getDocFromAlias('c')).toEqual([doc1]); + }); + + it('should return docs that match the alias and originating doc\'s area', function() { + var doc1 = {aliases: ['a'], area: 'api'}; + var doc2 = {aliases: ['a'], area: 'api'}; + var doc3 = {aliases: ['a'], area: 'other'}; + aliasMap.addDoc(doc1); + aliasMap.addDoc(doc2); + aliasMap.addDoc(doc3); + + expect(getDocFromAlias('a', {area: 'api'})).toEqual([doc1, doc2]); + }); + + it('should return docs that match the alias and originating doc\'s area and module', function() { + var doc1 = {aliases: ['a'], area: 'api', module: 'ng'}; + var doc2 = {aliases: ['a'], area: 'api', module: 'ngMock'}; + var doc3 = {aliases: ['a'], area: 'other', module: 'ng'}; + aliasMap.addDoc(doc1); + aliasMap.addDoc(doc2); + aliasMap.addDoc(doc3); + + expect(getDocFromAlias('a', {area: 'api', module: 'ng'})).toEqual([doc1]); + }); +}); \ No newline at end of file diff --git a/tools/docs/links-package/services/getLinkInfo.js b/tools/docs/links-package/services/getLinkInfo.js new file mode 100644 index 0000000000000..cc3aaeaf77353 --- /dev/null +++ b/tools/docs/links-package/services/getLinkInfo.js @@ -0,0 +1,78 @@ +var path = require('canonical-path'); + +/** + * @dgService getLinkInfo + * @description + * Get link information to a document that matches the given url + * @kind function + * @param {String} url The url to match + * @param {String} title An optional title to return in the link information + * @return {Object} The link information + * + * @property {boolean} relativeLinks Whether we expect the links to be relative to the originating doc + * @property {array1) { + linkInfo.valid = false; + linkInfo.errorType = 'ambiguous'; + linkInfo.error = 'Ambiguous link: "' + url + '".\n' + docs.reduce(function(msg, doc) { + return msg + '\n "' + doc.id + '" (' + doc.docType + ') : (' + doc.path + ' / ' + + doc.fileInfo.relativePath + ')'; + }, 'Matching docs: '); + + } else if (docs.length >= 1) { + linkInfo.url = docs[0].path; + linkInfo.title = title || encodeCodeBlock(docs[0].name, true); + linkInfo.type = 'doc'; + + if (getLinkInfoImpl.relativeLinks && currentDoc && currentDoc.path) { + var currentFolder = path.dirname(currentDoc.path); + var docFolder = path.dirname(linkInfo.url); + var relativeFolder = + path.relative(path.join('/', currentFolder), path.join('/', docFolder)); + linkInfo.url = path.join(relativeFolder, path.basename(linkInfo.url)); + log.debug(currentDoc.path, docs[0].path, linkInfo.url); + } + + } else if (url.indexOf('#') > 0) { + var pathAndHash = url.split('#'); + linkInfo = getLinkInfoImpl(pathAndHash[0], title, currentDoc); + linkInfo.url = linkInfo.url + '#' + pathAndHash[1]; + return linkInfo; + + } else if (url.indexOf('/') === -1 && url.indexOf('#') !== 0) { + linkInfo.valid = false; + linkInfo.errorType = 'missing'; + linkInfo.error = 'Invalid link (does not match any doc): "' + url + '"'; + + } else { + linkInfo.title = + title || ((url.indexOf('#') === 0) ? url.substring(1) : path.basename(url, '.html')); + } + + return linkInfo; + }; + +}; \ No newline at end of file diff --git a/tools/docs/links-package/services/moduleScopeLinkDisambiguator.js b/tools/docs/links-package/services/moduleScopeLinkDisambiguator.js new file mode 100644 index 0000000000000..08435b96eef88 --- /dev/null +++ b/tools/docs/links-package/services/moduleScopeLinkDisambiguator.js @@ -0,0 +1,15 @@ +var _ = require('lodash'); + +module.exports = function moduleScopeLinkDisambiguator() { + return function(url, title, currentDoc, docs) { + if (docs.length > 1) { + // filter out target docs that are not in the same module as the source doc + var filteredDocs = + _.filter(docs, function(doc) { return doc.moduleDoc === currentDoc.moduleDoc; }); + // if all target docs are in a different module then just return the full collection of + // ambiguous docs + return filteredDocs.length > 0 ? filteredDocs : docs; + } + return docs; + }; +}; diff --git a/tools/docs/target-package/index.js b/tools/docs/target-package/index.js new file mode 100644 index 0000000000000..edfb9c8d9a439 --- /dev/null +++ b/tools/docs/target-package/index.js @@ -0,0 +1,10 @@ +var Package = require('dgeni').Package; + +module.exports = new Package('target', [require('dgeni-packages/jsdoc')]) + + .factory(require('./services/targetEnvironments')) + .factory(require('./inline-tag-defs/target')) + + .config(function(inlineTagProcessor, targetInlineTagDef) { + inlineTagProcessor.inlineTagDefinitions.push(targetInlineTagDef); + }); diff --git a/tools/docs/target-package/inline-tag-defs/target.js b/tools/docs/target-package/inline-tag-defs/target.js new file mode 100644 index 0000000000000..bab4913018f4d --- /dev/null +++ b/tools/docs/target-package/inline-tag-defs/target.js @@ -0,0 +1,33 @@ +/** + * @dgService + * @description + * Process inline `target` block tags + * (of the form `{@target environment1 environment2}...{@endtarget}`), + * filtering out the blocks that do not match the active `targetEnvironments`. + */ +module.exports = function targetInlineTagDef(targetEnvironments, log, createDocMessage) { + return { + name: 'target', + end: 'endtarget', + handler: function(doc, tagName, tagDescription) { + var targets = tagDescription && tagDescription.tag.split(' '); + var hasTargets = targets && targets.length; + + try { + // Return the contents of this block if any of the following is true: + // * it has no targets + // * there are no targets stored in the targetEnvironments service + // * the block's targets overlap with the active targets in the targetEnvironments service + if (!hasTargets || !targetEnvironments.hasActive() || + targetEnvironments.someActive(targets)) { + return tagDescription.content; + } + } catch (x) { + log.error(createDocMessage('Error processing target inline tag def - ' + x.message, doc)); + } + + // Otherwise return an empty string + return ''; + } + }; +}; \ No newline at end of file diff --git a/tools/docs/target-package/inline-tag-defs/target.spec.js b/tools/docs/target-package/inline-tag-defs/target.spec.js new file mode 100644 index 0000000000000..e01bf14ace12c --- /dev/null +++ b/tools/docs/target-package/inline-tag-defs/target.spec.js @@ -0,0 +1,40 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); + +describe('target inline-tag-def', function() { + var dgeni, injector, targetInlineTagDef; + + beforeEach(function() { + dgeni = new Dgeni([testPackage('target-package', true)]); + injector = dgeni.configureInjector(); + targetInlineTagDef = injector.get('targetInlineTagDef'); + }); + + + it('should filter out content that does not match the targetEnvironments', function() { + + var doc = {}; + + var targetEnvironments = injector.get('targetEnvironments'); + targetEnvironments.addAllowed('js', true); + targetEnvironments.addAllowed('es6', true); + targetEnvironments.addAllowed('ts', false); + + var result = targetInlineTagDef.handler(doc, 'target', {tag: 'es6 ts', content: 'abc'}); + expect(result).toEqual('abc'); + + result = targetInlineTagDef.handler(doc, 'target', {tag: 'ts', content: 'xyz'}); + expect(result).toEqual(''); + }); + + + it('should not filter anything if there are no doc nor global target environments', function() { + var doc = {}; + + var result = targetInlineTagDef.handler(doc, 'target', {tag: 'es6 ts', content: 'abc'}); + expect(result).toEqual('abc'); + + result = targetInlineTagDef.handler(doc, 'target', {tag: 'ts', content: 'xyz'}); + expect(result).toEqual('xyz'); + }); +}); \ No newline at end of file diff --git a/tools/docs/target-package/services/targetEnvironments.js b/tools/docs/target-package/services/targetEnvironments.js new file mode 100644 index 0000000000000..23e1719503ee5 --- /dev/null +++ b/tools/docs/target-package/services/targetEnvironments.js @@ -0,0 +1,51 @@ +module.exports = function targetEnvironments() { + var _targets = Object.create(null); + var _activeCount = 0; + + var checkAllowed = function(target) { + if (!(target in _targets)) { + throw new Error( + 'Error accessing target "' + target + '". It is not in the list of allowed targets: ' + + Object.keys(_targets)); + } + }; + + var updateActiveCount = function() { + _activeCount = 0; + for (target in _targets) { + if (_targets[target]) _activeCount++; + } + }; + + return { + addAllowed: function(target, isActive) { + _targets[target] = !!isActive; + updateActiveCount(); + }, + removeAllowed: function(target) { + delete _targets[target]; + updateActiveCount(); + }, + activate: function(target) { + checkAllowed(target); + _targets[target] = true; + updateActiveCount(); + }, + deactivate: function(target) { + checkAllowed(target); + _targets[target] = false; + updateActiveCount(); + }, + isActive: function(target) { + checkAllowed(target); + return _targets[target]; + }, + hasActive: function() { return _activeCount > 0; }, + someActive: function(targets) { + for (var i = 0, ii = targets.length; i < ii; i++) { + if (this.isActive(targets[i])) return true; + } + return false; + } + }; +}; \ No newline at end of file diff --git a/tools/docs/target-package/services/targetEnvironments.spec.js b/tools/docs/target-package/services/targetEnvironments.spec.js new file mode 100644 index 0000000000000..3f7d7bdfa1cd8 --- /dev/null +++ b/tools/docs/target-package/services/targetEnvironments.spec.js @@ -0,0 +1,108 @@ +var testPackage = require('../../helpers/test-package'); +var Dgeni = require('dgeni'); + +describe('target inline-tag-def', function() { + var dgeni, injector, te; + + beforeEach(function() { + dgeni = new Dgeni([testPackage('target-package', true)]); + injector = dgeni.configureInjector(); + te = injector.get('targetEnvironments'); + }); + + describe('addAllowed', function() { + it('should store the target and whether it is active', function() { + te.addAllowed('a', true); + te.addAllowed('b', false); + te.addAllowed('c'); + expect(te.isActive('a')).toBe(true); + expect(te.isActive('b')).toBe(false); + expect(te.isActive('c')).toBe(false); + }); + }); + + describe('removeAllowed', function() { + it('should disallow the target', function() { + te.addAllowed('a'); + te.addAllowed('b'); + te.removeAllowed('b'); + expect(te.isActive('a')).toBe(false); + expect(function() { + te.isActive('b'); + }).toThrowError('Error accessing target "b". It is not in the list of allowed targets: a'); + }); + }); + + describe('activate', function() { + it('should active an already allowed target', function() { + te.addAllowed('a', true); + te.addAllowed('b', false); + te.addAllowed('c'); + + te.activate('a'); + te.activate('b'); + te.activate('c'); + expect(te.isActive('a')).toBe(true); + expect(te.isActive('b')).toBe(true); + expect(te.isActive('c')).toBe(true); + }); + }); + + describe('deactivate', function() { + it('should deactive an already allowed target', function() { + te.addAllowed('a', true); + te.addAllowed('b', false); + te.addAllowed('c'); + + te.deactivate('a'); + te.deactivate('b'); + te.deactivate('c'); + expect(te.isActive('a')).toBe(false); + expect(te.isActive('b')).toBe(false); + expect(te.isActive('c')).toBe(false); + }); + }); + + describe('isActive', function() { + it('should return true if the item is allowed and active', function() { + te.addAllowed('a', true); + te.addAllowed('b', false); + + expect(te.isActive('a')).toBe(true); + expect(te.isActive('b')).toBe(false); + }); + }); + + describe('hasActive', function() { + it('should return true if there are any active targets', function() { + te.addAllowed('a', true); + te.addAllowed('b', false); + expect(te.hasActive()).toBe(true); + + te.deactivate('a'); + expect(te.hasActive()).toBe(false); + + te.activate('b'); + expect(te.hasActive()).toBe(true); + }); + }); + + describe('someActive', function() { + it('should return true if the array of targets passed are all allowed and at least on is active', + function() { + te.addAllowed('a', true); + te.addAllowed('b', false); + te.addAllowed('c'); + + expect(te.someActive(['a'])).toBe(true); + expect(te.someActive(['b'])).toBe(false); + expect(te.someActive(['a', 'b'])).toBe(true); + expect(te.someActive(['b', 'c'])).toBe(false); + expect(te.someActive([])).toBe(false); + + expect(function() { te.someActive('d'); }) + .toThrowError( + 'Error accessing target "d". It is not in the list of allowed targets: a,b,c'); + }); + }); +}); diff --git a/tools/tsconfig.json b/tools/tsconfig.json index 8914ce783a8eb..f16b62ee1da36 100644 --- a/tools/tsconfig.json +++ b/tools/tsconfig.json @@ -21,6 +21,7 @@ "exclude": [ "node_modules", "typings-test", - "public_api_guard" + "public_api_guard", + "docs" ] }