Compare commits

..

58 Commits

Author SHA1 Message Date
11ed8f56ab docs(changelog): add changelog for 2.2.2 2016-11-22 14:36:49 -08:00
a49acbf027 chore(release): cut the 2.2.2 release 2016-11-22 14:33:08 -08:00
8e41910429 fix(build): update versions of umd bundles (#13038)
Fixes #13037
2016-11-22 14:26:20 -08:00
a4ab14bf74 test(upgrade): remove setTimeout from lifecycle hook tests (#13027)
* test(upgrade): remove unnecessary NO_ERRORS_SCHEMA

* test(upgrade): remove `setTimeout` from lifecycle hook tests

Closes #13019
2016-11-22 14:26:20 -08:00
ea4fc9b421 fix(animations): blend in all previously transitioned styles into next animation if interrupted (#13014)
Closes #13013
Closes #13014
2016-11-22 14:26:20 -08:00
0956acee58 fix(closure): quote date pattern aliases (#13012)
Quota the pattern aliases to prevent closure renaming. These are quoted in DatePipe and also need to be quoted here.
2016-11-22 14:26:20 -08:00
2ca67e1674 refactor(compiler): allow control of StaticSymbol lifetime (#12986) 2016-11-22 14:26:20 -08:00
472666fc2b refactor(ngUpgrade): Small cleanup with Testability API and resumeBootstrap (#12926)
* With non-static ngUpgrade apps, callbacks to `whenStable` were being invoked with the wrong
  context
* With non-static ngUpgrade apps, `resumeBootstrap` was being run outside the NgZone
* Remove redundent `whenStableContext` variable

Neither of the first two problems were actually causing bugs (as far as I know), but they *might*
have caused problems in the future.

Inspired by https://github.com/angular/angular/pull/12910, but for non-static apps.
2016-11-22 14:26:20 -08:00
462316b0f1 fix(upgrade): call ng1 lifecycle hooks (#12875) 2016-11-22 14:26:20 -08:00
96c2b2cc25 fix(changelog): replace beta.1 with beta.0 (#12961) 2016-11-22 14:26:19 -08:00
3d407fc010 refactor(compiler): further minor fixes 2016-11-22 14:26:19 -08:00
64bd672e3a refactor(compiler): Reintroduce ReflectorHost and move Extractor into @angular/compiler 2016-11-22 14:26:19 -08:00
ef38676091 refactor(comiler): various cleanups 2016-11-22 14:26:19 -08:00
38be2b81c6 refactor(compiler): renames
- `NgHost` to `CompilerHost`
- `AotCompilerHost.resolveFileToImport` to `AotCompilerHost.fileNameToModuleName`
- `AotCompilerHoset.resolveImportToFile` to `AotCompilerHost.moduleNameToFileName`
2016-11-22 14:26:19 -08:00
39a71eb0ec refactor(tsc-wrapped): collect all exported functions and classes and bump metadata version from 1 to 2
This is needed to resolve symbols without `.d.ts` files.
This bumps the version of the metadata from 1 to 2.
This adds logic into `ng_host.ts` to automatically upgrade
version 1 to version 2 metadata by adding the exported symbols
from the `.d.ts` file.
2016-11-22 14:26:19 -08:00
2fe6fb1163 fix(compiler): fix versions of @angular/tsc-wrapped 2016-11-22 14:26:19 -08:00
b5afe51b26 refactor(compiler): move symbol extraction to AotCompiler 2016-11-22 14:26:19 -08:00
170525a225 refactor(compiler): add createAotCompiler factory
Also adds 2 more methods to the `AotCompilerHost`:
- `loadResource`
- `resolveFileToImport`
2016-11-22 14:26:19 -08:00
0c98f45105 refactor(compiler): remove asset: urls
These urls were just relicts from Dart.
2016-11-22 14:26:19 -08:00
e7025c9423 refactor(compiler): move findDeclaration into the StaticReflector
Previously, this was part of the `AotCompilerHost`.
The `AotCompilerHost` is now also greatly simplified.
2016-11-22 14:26:18 -08:00
8f295287a2 refactor(compiler): move static_reflector into @angular/compiler and rename files
- `src/runtime_compiler.ts` -> `src/jit/compiler.ts`
- `src/compiler.ts` -> `src/jit/compiler_factory.ts`
- `src/offline_compiler` -> `src/aot/compiler.ts`

Part of #12867
2016-11-22 14:26:18 -08:00
030facc66a chore(build): update package.json versions during build (#12957) 2016-11-22 14:26:18 -08:00
45af8f6752 fix(ci): pin version of npm on CircleCI (#12954) 2016-11-22 14:26:18 -08:00
33a79028be fix(benchmarks): use sanitized style values (#12943) 2016-11-22 14:26:18 -08:00
09226d96f8 fix(router): support redirects to named outlets
Closes #12740, #9921
2016-11-22 14:26:18 -08:00
6c3166e6e4 chore(release): cut the 2.3.0-beta.0 realse and add change log 2016-11-22 14:26:18 -08:00
8df328b15a fix(router): add a banner file for the router (#12919) 2016-11-22 14:26:17 -08:00
115f18fa06 fix(router): removes a peer dependency from router to upgrade 2016-11-22 14:26:16 -08:00
511cd4d182 fix(router): add a banner file for the router (#12919) 2016-11-22 14:26:14 -08:00
87d5d49530 fix(router): removes a peer dependency from router to upgrade 2016-11-22 14:26:14 -08:00
933caacad3 chore(release): cut angular 2.2.1 2016-11-16 16:38:28 -08:00
efe9c4f35c fix(tools): fix error when running test.sh (#12927) 2016-11-16 16:26:44 -08:00
5b0f9e2f51 refactor(compiler): allows synchronous retrieving of metadata (#12908)
Allows non-normalized metadata to be retrieved synchronously.

Related to #7482
2016-11-16 16:26:44 -08:00
462879887a fix(core): support ngTemplateOutlet in production mode (#12921)
Fixes #12911
2016-11-16 16:26:44 -08:00
dae0d0fd66 docs(upgrade/static): improve API docs with examples
Closes #12717
2016-11-16 16:26:44 -08:00
c7f750dd5a chore(public_api): remove Angular 1 types from upgrade/static API 2016-11-16 16:26:44 -08:00
73de925551 chore(examples): add upgrade/static example 2016-11-16 16:26:44 -08:00
547c22029a chore(examples): support upgrade/static examples 2016-11-16 16:26:44 -08:00
364642d58c fix(router): add a banner file for the router (#12919) 2016-11-16 16:26:44 -08:00
7b67badc43 fix(platform_browser): fix disableDebugTools() (#12918) 2016-11-16 16:26:44 -08:00
dc1662a447 fix(ngUpgrade): make AoT ngUpgrade work with the testability API and resumeBootstrap() (#12910) 2016-11-16 16:26:43 -08:00
b5f433626b build(build.sh): echo before building examples 2016-11-16 16:26:43 -08:00
dabaf858d9 fix(router): should not create a route state if navigation is canceled (#12868)
Closes #12776
2016-11-16 16:26:43 -08:00
bbc3c9ce0e refactor(forms): remove facade (#12558) 2016-11-16 16:26:43 -08:00
1dcf1f484e fix(router): removes a peer dependency from router to upgrade 2016-11-16 16:26:43 -08:00
583d2833db fix(animations): only pass in same typed players as previous players into web-animations (#12907)
Closes #12907
2016-11-16 16:26:43 -08:00
f502a768d3 chore(router): remove @angular/upgrade peer dep (#12896) 2016-11-16 16:26:43 -08:00
16303ac487 refactor(http): remove all facade methods from http module (#12870) 2016-11-16 16:26:43 -08:00
6cdc3b5c12 fix(tsickle): support ctorParams in function closure (#12876)
See https://github.com/angular/tsickle/issues/261 for context.
2016-11-16 16:26:43 -08:00
5c46c493f2 fix(animations): retain styling when transition destinations are changed (#12208)
Closes #9661
Closes #12208
2016-11-16 16:26:43 -08:00
e02c18049d fix(select): allow for null values in HTML select options bound with ngValue
closes #12829
2016-11-16 16:26:43 -08:00
e0ce5458a2 fix: allow for null values in HTML select options bound with ngValue
This corrects the case of <option [ngValue]="null"> binding a string like "{0: null}" to the model instead of an actual null object.

Closes #10349
2016-11-16 16:26:43 -08:00
6a5ba0ec81 fix: allow for null values in HTML select options bound with ngValue
This corrects the case of <option [ngValue]="null"> binding a string like "{0: null}" to the model instead of an actual null object.

Closes #10349
2016-11-16 16:26:42 -08:00
828c0d24eb refactor(core): remove dead code (#12871) 2016-11-16 16:26:42 -08:00
22536442d6 refactor(core): remove ListWrapper from i18n 2016-11-16 16:26:42 -08:00
845ea235ee fix(http): return request url if it cannot be retrieved from response
closes #12837
2016-11-16 16:26:42 -08:00
21a4de999b fix(http): correctly handle response body for 204 status code
closes #12830
fixes #12393
2016-11-16 16:26:42 -08:00
82b34838bf refactor(xhr_backend): remove facade 2016-11-16 16:26:42 -08:00
99 changed files with 5831 additions and 10755 deletions

View File

@ -1,16 +1,3 @@
<a name="2.3.0-beta.1"></a>
# [2.3.0-beta.1](https://github.com/angular/angular/compare/2.3.0-beta.0...2.3.0-beta.1) (2016-11-22)
### Bug Fixes
Note: The 2.3.0-beta.1 release also contains all the changes present in the 2.2.2 release.
### Features
* **language-service:** add services to support editors ([#12987](https://github.com/angular/angular/issues/12987)) ([519a324](https://github.com/angular/angular/commit/519a324))
* **tools:** allow disabling annotation lowering ([c1a62e2](https://github.com/angular/angular/commit/c1a62e2))
<a name="2.2.2"></a>
## [2.2.2](https://github.com/angular/angular/compare/2.2.1...2.2.2) (2016-11-22)

View File

@ -17,7 +17,6 @@ PACKAGES=(core
upgrade
router
compiler-cli
language-service
benchpress)
BUILD_ALL=true
BUNDLE=true
@ -176,6 +175,7 @@ do
mv ${UMD_ES5_PATH}.tmp ${UMD_ES5_PATH}
$UGLIFYJS -c --screw-ie8 --comments -o ${UMD_ES5_MIN_PATH} ${UMD_ES5_PATH}
if [[ -e rollup-testing.config.js ]]; then
echo "====== Rollup ${PACKAGE} testing"
../../../node_modules/.bin/rollup -c rollup-testing.config.js

View File

@ -52,7 +52,6 @@ module.exports = function(config) {
'dist/all/@angular/compiler-cli/**',
'dist/all/@angular/compiler/test/aot/**',
'dist/all/@angular/benchpress/**',
'dist/all/@angular/language-service/**',
'dist/all/angular1_router.js',
'dist/all/@angular/platform-browser/testing/e2e_util.js',
'dist/examples/**/e2e_test/**',

View File

@ -22,7 +22,6 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE translationbundle [<!ELEMENT translationbundle (translation)*>
<!ATTLIST translationbundle lang CDATA #REQUIRED>
<!ELEMENT translation (#PCDATA|ph)*>
<!ATTLIST translation id CDATA #REQUIRED>
<!ELEMENT ph EMPTY>
<!ATTLIST ph name CDATA #REQUIRED>
]>
<translationbundle>
<translation id="76e1eccb1b772fa9f294ef9c146ea6d0efa8a2d4">käännä teksti</translation>
<translation id="65cc4ab3b4c438e07c89be2b677d08369fb62da2">tervetuloa</translation>
<translation id="63a85808f03b8181e36a952e0fa38202c2304862">other-3rdP-component</translation>
</translationbundle>

View File

@ -34,9 +34,9 @@ const EXPECTED_XMB = `<?xml version="1.0" encoding="UTF-8" ?>
<!ELEMENT ex (#PCDATA)>
]>
<messagebundle>
<msg id="3772663375917578720">other-3rdP-component</msg>
<msg id="8136548302122759730" desc="desc" meaning="meaning">translate me</msg>
<msg id="3492007542396725315">Welcome</msg>
<msg id="63a85808f03b8181e36a952e0fa38202c2304862">other-3rdP-component</msg>
<msg id="76e1eccb1b772fa9f294ef9c146ea6d0efa8a2d4" desc="desc" meaning="meaning">translate me</msg>
<msg id="65cc4ab3b4c438e07c89be2b677d08369fb62da2">Welcome</msg>
</messagebundle>
`;
@ -79,4 +79,5 @@ describe('template i18n extraction output', () => {
const xlf = fs.readFileSync(xlfOutput, {encoding: 'utf-8'});
expect(xlf).toEqual(EXPECTED_XLIFF);
});
});

View File

@ -41,8 +41,9 @@ function extract(
case 'xliff':
case 'xlf':
default:
const htmlParser = new compiler.I18NHtmlParser(new compiler.HtmlParser());
ext = 'xlf';
serializer = new compiler.Xliff();
serializer = new compiler.Xliff(htmlParser, compiler.DEFAULT_INTERPOLATION_CONFIG);
break;
}

View File

@ -8,16 +8,10 @@
import * as i18n from './i18n_ast';
export function digest(message: i18n.Message): string {
export function digestMessage(message: i18n.Message): string {
return sha1(serializeNodes(message.nodes).join('') + `[${message.meaning}]`);
}
export function decimalDigest(message: i18n.Message): string {
const visitor = new _SerializerIgnoreIcuExpVisitor();
const parts = message.nodes.map(a => a.visit(visitor, null));
return computeMsgId(parts.join(''), message.meaning);
}
/**
* Serialize the i18n ast to something xml-like in order to generate an UID.
*
@ -45,7 +39,7 @@ class _SerializerVisitor implements i18n.Visitor {
}
visitPlaceholder(ph: i18n.Placeholder, context: any): any {
return ph.value ? `<ph name="${ph.name}">${ph.value}</ph>` : `<ph name="${ph.name}"/>`;
return `<ph name="${ph.name}">${ph.value}</ph>`;
}
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
@ -59,21 +53,6 @@ export function serializeNodes(nodes: i18n.Node[]): string[] {
return nodes.map(a => a.visit(serializerVisitor, null));
}
/**
* Serialize the i18n ast to something xml-like in order to generate an UID.
*
* Ignore the ICU expressions so that message IDs stays identical if only the expression changes.
*
* @internal
*/
class _SerializerIgnoreIcuExpVisitor extends _SerializerVisitor {
visitIcu(icu: i18n.Icu, context: any): any {
let strCases = Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
// Do not take the expression into account
return `{${icu.type}, ${strCases.join(', ')}}`;
}
}
/**
* Compute the SHA1 of the given string
*
@ -84,7 +63,7 @@ class _SerializerIgnoreIcuExpVisitor extends _SerializerVisitor {
*/
export function sha1(str: string): string {
const utf8 = utf8Encode(str);
const words32 = stringToWords32(utf8, Endian.Big);
const words32 = stringToWords32(utf8);
const len = utf8.length * 8;
const w = new Array(80);
@ -111,99 +90,15 @@ export function sha1(str: string): string {
[a, b, c, d, e] = [add32(a, h0), add32(b, h1), add32(c, h2), add32(d, h3), add32(e, h4)];
}
return byteStringToHexString(words32ToByteString([a, b, c, d, e]));
}
const sha1 = words32ToString([a, b, c, d, e]);
function fk(index: number, b: number, c: number, d: number): [number, number] {
if (index < 20) {
return [(b & c) | (~b & d), 0x5a827999];
let hex: string = '';
for (let i = 0; i < sha1.length; i++) {
const b = sha1.charCodeAt(i);
hex += (b >>> 4 & 0x0f).toString(16) + (b & 0x0f).toString(16);
}
if (index < 40) {
return [b ^ c ^ d, 0x6ed9eba1];
}
if (index < 60) {
return [(b & c) | (b & d) | (c & d), 0x8f1bbcdc];
}
return [b ^ c ^ d, 0xca62c1d6];
}
/**
* Compute the fingerprint of the given string
*
* The output is 64 bit number encoded as a decimal string
*
* based on:
* https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/GoogleJsMessageIdGenerator.java
*/
export function fingerprint(str: string): [number, number] {
const utf8 = utf8Encode(str);
let [hi, lo] = [hash32(utf8, 0), hash32(utf8, 102072)];
if (hi == 0 && (lo == 0 || lo == 1)) {
hi = hi ^ 0x130f9bef;
lo = lo ^ -0x6b5f56d8;
}
return [hi, lo];
}
export function computeMsgId(msg: string, meaning: string): string {
let [hi, lo] = fingerprint(msg);
if (meaning) {
const [him, lom] = fingerprint(meaning);
[hi, lo] = add64(rol64([hi, lo], 1), [him, lom]);
}
return byteStringToDecString(words32ToByteString([hi & 0x7fffffff, lo]));
}
function hash32(str: string, c: number): number {
let [a, b] = [0x9e3779b9, 0x9e3779b9];
let i: number;
const len = str.length;
for (i = 0; i + 12 <= len; i += 12) {
a = add32(a, wordAt(str, i, Endian.Little));
b = add32(b, wordAt(str, i + 4, Endian.Little));
c = add32(c, wordAt(str, i + 8, Endian.Little));
[a, b, c] = mix([a, b, c]);
}
a = add32(a, wordAt(str, i, Endian.Little));
b = add32(b, wordAt(str, i + 4, Endian.Little));
// the first byte of c is reserved for the length
c = add32(c, len);
c = add32(c, wordAt(str, i + 8, Endian.Little) << 8);
return mix([a, b, c])[2];
}
// clang-format off
function mix([a, b, c]: [number, number, number]): [number, number, number] {
a = sub32(a, b); a = sub32(a, c); a ^= c >>> 13;
b = sub32(b, c); b = sub32(b, a); b ^= a << 8;
c = sub32(c, a); c = sub32(c, b); c ^= b >>> 13;
a = sub32(a, b); a = sub32(a, c); a ^= c >>> 12;
b = sub32(b, c); b = sub32(b, a); b ^= a << 16;
c = sub32(c, a); c = sub32(c, b); c ^= b >>> 5;
a = sub32(a, b); a = sub32(a, c); a ^= c >>> 3;
b = sub32(b, c); b = sub32(b, a); b ^= a << 10;
c = sub32(c, a); c = sub32(c, b); c ^= b >>> 15;
return [a, b, c];
}
// clang-format on
// Utils
enum Endian {
Little,
Big,
return hex.toLowerCase();
}
function utf8Encode(str: string): string {
@ -236,9 +131,10 @@ function decodeSurrogatePairs(str: string, index: number): number {
}
const high = str.charCodeAt(index);
let low: number;
if (high >= 0xd800 && high <= 0xdfff && str.length > index + 1) {
const low = byteAt(str, index + 1);
low = str.charCodeAt(index + 1);
if (low >= 0xdc00 && low <= 0xdfff) {
return (high - 0xd800) * 0x400 + low - 0xdc00 + 0x10000;
}
@ -247,126 +143,50 @@ function decodeSurrogatePairs(str: string, index: number): number {
return high;
}
function add32(a: number, b: number): number {
return add32to64(a, b)[1];
}
function add32to64(a: number, b: number): [number, number] {
const low = (a & 0xffff) + (b & 0xffff);
const high = (a >>> 16) + (b >>> 16) + (low >>> 16);
return [high >>> 16, (high << 16) | (low & 0xffff)];
}
function add64([ah, al]: [number, number], [bh, bl]: [number, number]): [number, number] {
const [carry, l] = add32to64(al, bl);
const h = add32(add32(ah, bh), carry);
return [h, l];
}
function sub32(a: number, b: number): number {
const low = (a & 0xffff) - (b & 0xffff);
const high = (a >> 16) - (b >> 16) + (low >> 16);
return (high << 16) | (low & 0xffff);
}
// Rotate a 32b number left `count` position
function rol32(a: number, count: number): number {
return (a << count) | (a >>> (32 - count));
}
// Rotate a 64b number left `count` position
function rol64([hi, lo]: [number, number], count: number): [number, number] {
const h = (hi << count) | (lo >>> (32 - count));
const l = (lo << count) | (hi >>> (32 - count));
return [h, l];
}
function stringToWords32(str: string, endian: Endian): number[] {
const words32 = Array((str.length + 3) >>> 2);
function stringToWords32(str: string): number[] {
const words32 = Array(str.length >>> 2);
for (let i = 0; i < words32.length; i++) {
words32[i] = wordAt(str, i * 4, endian);
words32[i] = 0;
}
for (let i = 0; i < str.length; i++) {
words32[i >>> 2] |= (str.charCodeAt(i) & 0xff) << 8 * (3 - i & 0x3);
}
return words32;
}
function byteAt(str: string, index: number): number {
return index >= str.length ? 0 : str.charCodeAt(index) & 0xff;
}
function wordAt(str: string, index: number, endian: Endian): number {
let word = 0;
if (endian === Endian.Big) {
for (let i = 0; i < 4; i++) {
word += byteAt(str, index + i) << (24 - 8 * i);
}
} else {
for (let i = 0; i < 4; i++) {
word += byteAt(str, index + i) << 8 * i;
}
}
return word;
}
function words32ToByteString(words32: number[]): string {
return words32.reduce((str, word) => str + word32ToByteString(word), '');
}
function word32ToByteString(word: number): string {
function words32ToString(words32: number[]): string {
let str = '';
for (let i = 0; i < 4; i++) {
str += String.fromCharCode((word >>> 8 * (3 - i)) & 0xff);
for (let i = 0; i < words32.length * 4; i++) {
str += String.fromCharCode((words32[i >>> 2] >>> 8 * (3 - i & 0x3)) & 0xff);
}
return str;
}
function byteStringToHexString(str: string): string {
let hex: string = '';
for (let i = 0; i < str.length; i++) {
const b = byteAt(str, i);
hex += (b >>> 4).toString(16) + (b & 0x0f).toString(16);
}
return hex.toLowerCase();
}
// based on http://www.danvk.org/hex2dec.html (JS can not handle more than 56b)
function byteStringToDecString(str: string): string {
let decimal = '';
let toThePower = '1';
for (let i = str.length - 1; i >= 0; i--) {
decimal = addBigInt(decimal, numberTimesBigInt(byteAt(str, i), toThePower));
toThePower = numberTimesBigInt(256, toThePower);
function fk(index: number, b: number, c: number, d: number): [number, number] {
if (index < 20) {
return [(b & c) | (~b & d), 0x5a827999];
}
return decimal.split('').reverse().join('');
}
// x and y decimal, lowest significant digit first
function addBigInt(x: string, y: string): string {
let sum = '';
const len = Math.max(x.length, y.length);
for (let i = 0, carry = 0; i < len || carry; i++) {
const tmpSum = carry + +(x[i] || 0) + +(y[i] || 0);
if (tmpSum >= 10) {
carry = 1;
sum += tmpSum - 10;
} else {
carry = 0;
sum += tmpSum;
}
if (index < 40) {
return [b ^ c ^ d, 0x6ed9eba1];
}
return sum;
if (index < 60) {
return [(b & c) | (b & d) | (c & d), 0x8f1bbcdc];
}
return [b ^ c ^ d, 0xca62c1d6];
}
function numberTimesBigInt(num: number, b: string): string {
let product = '';
let bToThePower = b;
for (; num !== 0; num = num >>> 1) {
if (num & 1) product = addBigInt(product, bToThePower);
bToThePower = addBigInt(bToThePower, bToThePower);
}
return product;
function add32(a: number, b: number): number {
const low = (a & 0xffff) + (b & 0xffff);
const high = (a >> 16) + (b >> 16) + (low >> 16);
return (high << 16) | (low & 0xffff);
}
function rol32(a: number, count: number): number {
return (a << count) | (a >>> (32 - count));
}

View File

@ -10,6 +10,7 @@ import * as html from '../ml_parser/ast';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseTreeResult} from '../ml_parser/parser';
import {digestMessage} from './digest';
import * as i18n from './i18n_ast';
import {createI18nMessageFactory} from './i18n_parser';
import {I18nError} from './parse_util';
@ -213,8 +214,8 @@ class _Visitor implements html.Visitor {
// Extract only top level nodes with the (implicit) "i18n" attribute if not in a block or an ICU
// message
const i18nAttr = _getI18nAttr(el);
const isImplicit = this._implicitTags.some(tag => el.name === tag) && !this._inIcu &&
!this._isInTranslatableSection;
const isImplicit = this._implicitTags.some((tag: string): boolean => el.name === tag) &&
!this._inIcu && !this._isInTranslatableSection;
const isTopLevelImplicit = !wasInImplicitNode && isImplicit;
this._inImplicitNode = this._inImplicitNode || isImplicit;
@ -347,14 +348,14 @@ class _Visitor implements html.Visitor {
// no-op when called in extraction mode (returns [])
private _translateMessage(el: html.Node, message: i18n.Message): html.Node[] {
if (message && this._mode === _VisitorMode.Merge) {
const nodes = this._translations.get(message);
const id = digestMessage(message);
const nodes = this._translations.get(id);
if (nodes) {
return nodes;
}
this._reportError(
el, `Translation unavailable for message id="${this._translations.digest(message)}"`);
this._reportError(el, `Translation unavailable for message id="${id}"`);
}
return [];
@ -383,20 +384,19 @@ class _Visitor implements html.Visitor {
if (attr.value && attr.value != '' && i18nAttributeMeanings.hasOwnProperty(attr.name)) {
const meaning = i18nAttributeMeanings[attr.name];
const message: i18n.Message = this._createI18nMessage([attr], meaning, '');
const nodes = this._translations.get(message);
const id = digestMessage(message);
const nodes = this._translations.get(id);
if (nodes) {
if (nodes[0] instanceof html.Text) {
const value = (nodes[0] as html.Text).value;
translatedAttributes.push(new html.Attribute(attr.name, value, attr.sourceSpan));
} else {
this._reportError(
el,
`Unexpected translation for attribute "${attr.name}" (id="${this._translations.digest(message)}")`);
el, `Unexpected translation for attribute "${attr.name}" (id="${id}")`);
}
} else {
this._reportError(
el,
`Translation unavailable for attribute "${attr.name}" (id="${this._translations.digest(message)}")`);
el, `Translation unavailable for attribute "${attr.name}" (id="${id}")`);
}
} else {
translatedAttributes.push(attr);

View File

@ -12,20 +12,18 @@ export class Message {
/**
* @param nodes message AST
* @param placeholders maps placeholder names to static content
* @param placeholderToMessage maps placeholder names to messages (used for nested ICU messages)
* @param placeholderToMsgIds maps placeholder names to translatable message IDs (used for ICU
* messages)
* @param meaning
* @param description
*/
constructor(
public nodes: Node[], public placeholders: {[phName: string]: string},
public placeholderToMessage: {[phName: string]: Message}, public meaning: string,
public nodes: Node[], public placeholders: {[name: string]: string},
public placeholderToMsgIds: {[name: string]: string}, public meaning: string,
public description: string) {}
}
export interface Node {
sourceSpan: ParseSourceSpan;
visit(visitor: Visitor, context?: any): any;
}
export interface Node { visit(visitor: Visitor, context?: any): any; }
export class Text implements Node {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
@ -33,7 +31,6 @@ export class Text implements Node {
visit(visitor: Visitor, context?: any): any { return visitor.visitText(this, context); }
}
// TODO(vicb): do we really need this node (vs an array) ?
export class Container implements Node {
constructor(public children: Node[], public sourceSpan: ParseSourceSpan) {}
@ -41,7 +38,6 @@ export class Container implements Node {
}
export class Icu implements Node {
public expressionPlaceholder: string;
constructor(
public expression: string, public type: string, public cases: {[k: string]: Node},
public sourceSpan: ParseSourceSpan) {}
@ -59,13 +55,13 @@ export class TagPlaceholder implements Node {
}
export class Placeholder implements Node {
constructor(public value: string, public name: string, public sourceSpan: ParseSourceSpan) {}
constructor(public value: string, public name: string = '', public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitPlaceholder(this, context); }
}
export class IcuPlaceholder implements Node {
constructor(public value: Icu, public name: string, public sourceSpan: ParseSourceSpan) {}
constructor(public value: Icu, public name: string = '', public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitIcuPlaceholder(this, context); }
}

View File

@ -11,6 +11,7 @@ import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/in
import {ParseTreeResult} from '../ml_parser/parser';
import {mergeTranslations} from './extractor_merger';
import {MessageBundle} from './message_bundle';
import {Serializer} from './serializers/serializer';
import {Xliff} from './serializers/xliff';
import {Xmb} from './serializers/xmb';
@ -40,29 +41,32 @@ export class I18NHtmlParser implements HtmlParser {
}
// TODO(vicb): add support for implicit tags / attributes
const messageBundle = new MessageBundle(this._htmlParser, [], {});
const errors = messageBundle.updateFromTemplate(source, url, interpolationConfig);
if (parseResult.errors.length) {
return new ParseTreeResult(parseResult.rootNodes, parseResult.errors);
if (errors && errors.length) {
return new ParseTreeResult(parseResult.rootNodes, parseResult.errors.concat(errors));
}
const serializer = this._createSerializer();
const translationBundle = TranslationBundle.load(this._translations, url, serializer);
const serializer = this._createSerializer(interpolationConfig);
const translationBundle =
TranslationBundle.load(this._translations, url, messageBundle, serializer);
return mergeTranslations(parseResult.rootNodes, translationBundle, interpolationConfig, [], {});
}
private _createSerializer(): Serializer {
private _createSerializer(interpolationConfig: InterpolationConfig): Serializer {
const format = (this._translationsFormat || 'xlf').toLowerCase();
switch (format) {
case 'xmb':
return new Xmb();
case 'xtb':
return new Xtb();
return new Xtb(this._htmlParser, interpolationConfig);
case 'xliff':
case 'xlf':
default:
return new Xliff();
return new Xliff(this._htmlParser, interpolationConfig);
}
}
}

View File

@ -12,6 +12,7 @@ import * as html from '../ml_parser/ast';
import {getHtmlTagDefinition} from '../ml_parser/html_tags';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseSourceSpan} from '../parse_util';
import {digestMessage} from './digest';
import * as i18n from './i18n_ast';
import {PlaceholderRegistry} from './serializers/placeholder';
@ -33,8 +34,8 @@ class _I18nVisitor implements html.Visitor {
private _isIcu: boolean;
private _icuDepth: number;
private _placeholderRegistry: PlaceholderRegistry;
private _placeholderToContent: {[phName: string]: string};
private _placeholderToMessage: {[phName: string]: i18n.Message};
private _placeholderToContent: {[name: string]: string};
private _placeholderToIds: {[name: string]: string};
constructor(
private _expressionParser: ExpressionParser,
@ -45,12 +46,12 @@ class _I18nVisitor implements html.Visitor {
this._icuDepth = 0;
this._placeholderRegistry = new PlaceholderRegistry();
this._placeholderToContent = {};
this._placeholderToMessage = {};
this._placeholderToIds = {};
const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {});
return new i18n.Message(
i18nodes, this._placeholderToContent, this._placeholderToMessage, meaning, description);
i18nodes, this._placeholderToContent, this._placeholderToIds, meaning, description);
}
visitElement(el: html.Element, context: any): i18n.Node {
@ -98,13 +99,7 @@ class _I18nVisitor implements html.Visitor {
this._icuDepth--;
if (this._isIcu || this._icuDepth > 0) {
// Returns an ICU node when:
// - the message (vs a part of the message) is an ICU message, or
// - the ICU message is nested.
const expPh = this._placeholderRegistry.getUniquePlaceholder(`VAR_${icu.type}`);
i18nIcu.expressionPlaceholder = expPh;
this._placeholderToContent[expPh] = icu.switchValue;
// If the message (vs a part of the message) is an ICU message returns it
return i18nIcu;
}
@ -115,7 +110,7 @@ class _I18nVisitor implements html.Visitor {
// TODO(vicb): add a html.Node -> i18n.Message cache to avoid having to re-create the msg
const phName = this._placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
const visitor = new _I18nVisitor(this._expressionParser, this._interpolationConfig);
this._placeholderToMessage[phName] = visitor.toI18nMessage([icu], '', '');
this._placeholderToIds[phName] = digestMessage(visitor.toI18nMessage([icu], '', ''));
return new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
}

View File

@ -10,6 +10,7 @@ import {HtmlParser} from '../ml_parser/html_parser';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseError} from '../parse_util';
import {digestMessage} from './digest';
import {extractMessages} from './extractor_merger';
import {Message} from './i18n_ast';
import {Serializer} from './serializers/serializer';
@ -18,7 +19,7 @@ import {Serializer} from './serializers/serializer';
* A container for message extracted from the templates.
*/
export class MessageBundle {
private _messages: Message[] = [];
private _messageMap: {[id: string]: Message} = {};
constructor(
private _htmlParser: HtmlParser, private _implicitTags: string[],
@ -39,10 +40,11 @@ export class MessageBundle {
return i18nParserResult.errors;
}
this._messages.push(...i18nParserResult.messages);
i18nParserResult.messages.forEach(
(message) => { this._messageMap[digestMessage(message)] = message; });
}
getMessages(): Message[] { return this._messages; }
getMessageMap(): {[id: string]: Message} { return this._messageMap; }
write(serializer: Serializer): string { return serializer.write(this._messages); }
write(serializer: Serializer): string { return serializer.write(this._messageMap); }
}

View File

@ -40,9 +40,7 @@ const TAG_TO_PLACEHOLDER_NAMES: {[k: string]: string} = {
};
/**
* Creates unique names for placeholder with different content.
*
* Returns the same placeholder name when the content is identical.
* Creates unique names for placeholder with different content
*
* @internal
*/
@ -95,10 +93,6 @@ export class PlaceholderRegistry {
return uniqueName;
}
getUniquePlaceholder(name: string): string {
return this._generateUniqueName(name.toUpperCase());
}
// Generate a hash for a tag - does not take attribute order into account
private _hashTag(tag: string, attrs: {[k: string]: string}, isVoid: boolean): string {
const start = `<${tag}`;
@ -111,8 +105,18 @@ export class PlaceholderRegistry {
private _hashClosingTag(tag: string): string { return this._hashTag(`/${tag}`, {}, false); }
private _generateUniqueName(base: string): string {
const next = this._placeHolderNameCounts[base];
this._placeHolderNameCounts[base] = next ? next + 1 : 1;
return next ? `${base}_${next}` : base;
let name = base;
let next = this._placeHolderNameCounts[name];
if (!next) {
next = 1;
} else {
name += `_${next}`;
next++;
}
this._placeHolderNameCounts[base] = next;
return name;
}
}

View File

@ -6,12 +6,36 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as html from '../../ml_parser/ast';
import * as i18n from '../i18n_ast';
import {MessageBundle} from '../message_bundle';
export interface Serializer {
write(messages: i18n.Message[]): string;
write(messageMap: {[id: string]: i18n.Message}): string;
load(content: string, url: string): {[msgId: string]: i18n.Node[]};
digest(message: i18n.Message): string;
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: html.Node[]};
}
// Generate a map of placeholder to content indexed by message ids
export function extractPlaceholders(messageBundle: MessageBundle) {
const messageMap = messageBundle.getMessageMap();
const placeholders: {[id: string]: {[name: string]: string}} = {};
Object.keys(messageMap).forEach(msgId => {
placeholders[msgId] = messageMap[msgId].placeholders;
});
return placeholders;
}
// Generate a map of placeholder to message ids indexed by message ids
export function extractPlaceholderToIds(messageBundle: MessageBundle) {
const messageMap = messageBundle.getMessageMap();
const placeholderToIds: {[id: string]: {[name: string]: string}} = {};
Object.keys(messageMap).forEach(msgId => {
placeholderToIds[msgId] = messageMap[msgId].placeholderToMsgIds;
});
return placeholderToIds;
}

View File

@ -7,12 +7,15 @@
*/
import * as ml from '../../ml_parser/ast';
import {HtmlParser} from '../../ml_parser/html_parser';
import {InterpolationConfig} from '../../ml_parser/interpolation_config';
import {XmlParser} from '../../ml_parser/xml_parser';
import {digest} from '../digest';
import {ParseError} from '../../parse_util';
import * as i18n from '../i18n_ast';
import {MessageBundle} from '../message_bundle';
import {I18nError} from '../parse_util';
import {Serializer} from './serializer';
import {Serializer, extractPlaceholderToIds, extractPlaceholders} from './serializer';
import * as xml from './xml_helper';
const _VERSION = '1.2';
@ -20,7 +23,6 @@ const _XMLNS = 'urn:oasis:names:tc:xliff:document:1.2';
// TODO(vicb): make this a param (s/_/-/)
const _SOURCE_LANG = 'en';
const _PLACEHOLDER_TAG = 'x';
const _SOURCE_TAG = 'source';
const _TARGET_TAG = 'target';
const _UNIT_TAG = 'trans-unit';
@ -28,19 +30,17 @@ const _UNIT_TAG = 'trans-unit';
// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html
// http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
export class Xliff implements Serializer {
write(messages: i18n.Message[]): string {
constructor(private _htmlParser: HtmlParser, private _interpolationConfig: InterpolationConfig) {}
write(messageMap: {[id: string]: i18n.Message}): string {
const visitor = new _WriteVisitor();
const visited: {[id: string]: boolean} = {};
const transUnits: xml.Node[] = [];
messages.forEach(message => {
const id = this.digest(message);
Object.keys(messageMap).forEach((id) => {
const message = messageMap[id];
// deduplicate messages
if (visited[id]) return;
visited[id] = true;
const transUnit = new xml.Tag(_UNIT_TAG, {id, datatype: 'html'});
const transUnit = new xml.Tag(_UNIT_TAG, {id: id, datatype: 'html'});
transUnit.children.push(
new xml.CR(8), new xml.Tag(_SOURCE_TAG, {}, visitor.serialize(message.nodes)),
new xml.CR(8), new xml.Tag(_TARGET_TAG));
@ -75,28 +75,38 @@ export class Xliff implements Serializer {
]);
}
load(content: string, url: string): {[msgId: string]: i18n.Node[]} {
// xliff to xml nodes
const xliffParser = new XliffParser();
const {mlNodesByMsgId, errors} = xliffParser.parse(content, url);
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: ml.Node[]} {
// Parse the xtb file into xml nodes
const result = new XmlParser().parse(content, url);
// xml nodes to i18n nodes
const i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {};
const converter = new XmlToI18n();
Object.keys(mlNodesByMsgId).forEach(msgId => {
const {i18nNodes, errors: e} = converter.convert(mlNodesByMsgId[msgId]);
errors.push(...e);
i18nNodesByMsgId[msgId] = i18nNodes;
});
if (errors.length) {
throw new Error(`xliff parse errors:\n${errors.join('\n')}`);
if (result.errors.length) {
throw new Error(`xtb parse errors:\n${result.errors.join('\n')}`);
}
return i18nNodesByMsgId;
}
// Replace the placeholders, messages are now string
const {messages, errors} = new _LoadVisitor().parse(result.rootNodes, messageBundle);
digest(message: i18n.Message): string { return digest(message); }
if (errors.length) {
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
}
// Convert the string messages to html ast
// TODO(vicb): map error message back to the original message in xtb
const messageMap: {[id: string]: ml.Node[]} = {};
const parseErrors: ParseError[] = [];
Object.keys(messages).forEach((id) => {
const res = this._htmlParser.parse(messages[id], url, true, this._interpolationConfig);
parseErrors.push(...res.errors);
messageMap[id] = res.rootNodes;
});
if (parseErrors.length) {
throw new Error(`xtb parse errors:\n${parseErrors.join('\n')}`);
}
return messageMap;
}
}
class _WriteVisitor implements i18n.Visitor {
@ -156,46 +166,75 @@ class _WriteVisitor implements i18n.Visitor {
}
// TODO(vicb): add error management (structure)
// Extract messages as xml nodes from the xliff file
class XliffParser implements ml.Visitor {
private _unitMlNodes: ml.Node[];
// TODO(vicb): factorize (xtb) ?
class _LoadVisitor implements ml.Visitor {
private _messageNodes: [string, ml.Node[]][];
private _translatedMessages: {[id: string]: string};
private _msgId: string;
private _target: ml.Node[];
private _errors: I18nError[];
private _mlNodesByMsgId: {[msgId: string]: ml.Node[]};
private _placeholders: {[name: string]: string};
private _placeholderToIds: {[name: string]: string};
parse(xliff: string, url: string) {
this._unitMlNodes = [];
this._mlNodesByMsgId = {};
parse(nodes: ml.Node[], messageBundle: MessageBundle):
{messages: {[k: string]: string}, errors: I18nError[]} {
this._messageNodes = [];
this._translatedMessages = {};
this._msgId = '';
this._target = [];
this._errors = [];
const xml = new XmlParser().parse(xliff, url, false);
// Find all messages
ml.visitAll(this, nodes, null);
this._errors = xml.errors;
ml.visitAll(this, xml.rootNodes, null);
const messageMap = messageBundle.getMessageMap();
const placeholders = extractPlaceholders(messageBundle);
const placeholderToIds = extractPlaceholderToIds(messageBundle);
return {
mlNodesByMsgId: this._mlNodesByMsgId,
errors: this._errors,
};
this._messageNodes
.filter(message => {
// Remove any messages that is not present in the source message bundle.
return messageMap.hasOwnProperty(message[0]);
})
.sort((a, b) => {
// Because there could be no ICU placeholders inside an ICU message,
// we do not need to take into account the `placeholderToMsgIds` of the referenced
// messages, those would always be empty
// TODO(vicb): overkill - create 2 buckets and [...woDeps, ...wDeps].process()
if (Object.keys(messageMap[a[0]].placeholderToMsgIds).length == 0) {
return -1;
}
if (Object.keys(messageMap[b[0]].placeholderToMsgIds).length == 0) {
return 1;
}
return 0;
})
.forEach(message => {
const id = message[0];
this._placeholders = placeholders[id] || {};
this._placeholderToIds = placeholderToIds[id] || {};
// TODO(vicb): make sure there is no `_TRANSLATIONS_TAG` nor `_TRANSLATION_TAG`
this._translatedMessages[id] = ml.visitAll(this, message[1]).join('');
});
return {messages: this._translatedMessages, errors: this._errors};
}
visitElement(element: ml.Element, context: any): any {
switch (element.name) {
case _UNIT_TAG:
this._unitMlNodes = null;
const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!idAttr) {
this._target = null;
const msgId = element.attrs.find((attr) => attr.name === 'id');
if (!msgId) {
this._addError(element, `<${_UNIT_TAG}> misses the "id" attribute`);
} else {
const id = idAttr.value;
if (this._mlNodesByMsgId.hasOwnProperty(id)) {
this._addError(element, `Duplicated translations for msg ${id}`);
} else {
ml.visitAll(this, element.children, null);
if (this._unitMlNodes) {
this._mlNodesByMsgId[id] = this._unitMlNodes;
} else {
this._addError(element, `Message ${id} misses a translation`);
}
}
this._msgId = msgId.value;
}
ml.visitAll(this, element.children, null);
if (this._msgId !== null) {
this._messageNodes.push([this._msgId, this._target]);
}
break;
@ -204,65 +243,48 @@ class XliffParser implements ml.Visitor {
break;
case _TARGET_TAG:
this._unitMlNodes = element.children;
this._target = element.children;
break;
case _PLACEHOLDER_TAG:
const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!idAttr) {
this._addError(element, `<${_PLACEHOLDER_TAG}> misses the "id" attribute`);
} else {
const id = idAttr.value;
if (this._placeholders.hasOwnProperty(id)) {
return this._placeholders[id];
}
if (this._placeholderToIds.hasOwnProperty(id) &&
this._translatedMessages.hasOwnProperty(this._placeholderToIds[id])) {
return this._translatedMessages[this._placeholderToIds[id]];
}
// TODO(vicb): better error message for when
// !this._translatedMessages.hasOwnProperty(this._placeholderToIds[id])
this._addError(element, `The placeholder "${id}" does not exists in the source message`);
}
break;
default:
// TODO(vicb): assert file structure, xliff version
// For now only recurse on unhandled nodes
ml.visitAll(this, element.children, null);
}
}
visitAttribute(attribute: ml.Attribute, context: any): any {}
visitText(text: ml.Text, context: any): any {}
visitComment(comment: ml.Comment, context: any): any {}
visitExpansion(expansion: ml.Expansion, context: any): any {}
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
}
}
// Convert ml nodes (xliff syntax) to i18n nodes
class XmlToI18n implements ml.Visitor {
private _errors: I18nError[];
convert(nodes: ml.Node[]) {
this._errors = [];
return {
i18nNodes: ml.visitAll(this, nodes),
errors: this._errors,
};
visitAttribute(attribute: ml.Attribute, context: any): any {
throw new Error('unreachable code');
}
visitText(text: ml.Text, context: any) { return new i18n.Text(text.value, text.sourceSpan); }
visitText(text: ml.Text, context: any): any { return text.value; }
visitElement(el: ml.Element, context: any): i18n.Placeholder {
if (el.name === _PLACEHOLDER_TAG) {
const nameAttr = el.attrs.find((attr) => attr.name === 'id');
if (nameAttr) {
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan);
}
visitComment(comment: ml.Comment, context: any): any { return ''; }
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "id" attribute`);
} else {
this._addError(el, `Unexpected tag`);
}
visitExpansion(expansion: ml.Expansion, context: any): any {
throw new Error('unreachable code');
}
visitExpansion(icu: ml.Expansion, context: any) {}
visitExpansionCase(icuCase: ml.ExpansionCase, context: any): any {}
visitComment(comment: ml.Comment, context: any) {}
visitAttribute(attribute: ml.Attribute, context: any) {}
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {
throw new Error('unreachable code');
}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));

View File

@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {decimalDigest} from '../digest';
import * as html from '../../ml_parser/ast';
import * as i18n from '../i18n_ast';
import {MessageBundle} from '../message_bundle';
import {Serializer} from './serializer';
import * as xml from './xml_helper';
@ -38,18 +39,12 @@ const _DOCTYPE = `<!ELEMENT messagebundle (msg)*>
<!ELEMENT ex (#PCDATA)>`;
export class Xmb implements Serializer {
write(messages: i18n.Message[]): string {
write(messageMap: {[k: string]: i18n.Message}): string {
const visitor = new _Visitor();
const visited: {[id: string]: boolean} = {};
let rootNode = new xml.Tag(_MESSAGES_TAG);
messages.forEach(message => {
const id = this.digest(message);
// deduplicate messages
if (visited[id]) return;
visited[id] = true;
const rootNode = new xml.Tag(_MESSAGES_TAG);
Object.keys(messageMap).forEach((id) => {
const message = messageMap[id];
const attrs: {[k: string]: string} = {id};
if (message.description) {
@ -76,11 +71,9 @@ export class Xmb implements Serializer {
]);
}
load(content: string, url: string): {[msgId: string]: i18n.Node[]} {
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: html.Node[]} {
throw new Error('Unsupported');
}
digest(message: i18n.Message): string { return digest(message); }
}
class _Visitor implements i18n.Visitor {
@ -93,7 +86,7 @@ class _Visitor implements i18n.Visitor {
}
visitIcu(icu: i18n.Icu, context?: any): xml.Node[] {
const nodes = [new xml.Text(`{${icu.expressionPlaceholder}, ${icu.type}, `)];
const nodes = [new xml.Text(`{${icu.expression}, ${icu.type}, `)];
Object.keys(icu.cases).forEach((c: string) => {
nodes.push(new xml.Text(`${c} {`), ...icu.cases[c].visit(this), new xml.Text(`} `));
@ -130,7 +123,3 @@ class _Visitor implements i18n.Visitor {
return [].concat(...nodes.map(node => node.visit(this)));
}
}
export function digest(message: i18n.Message): string {
return decimalDigest(message);
}

View File

@ -7,63 +7,112 @@
*/
import * as ml from '../../ml_parser/ast';
import {HtmlParser} from '../../ml_parser/html_parser';
import {InterpolationConfig} from '../../ml_parser/interpolation_config';
import {XmlParser} from '../../ml_parser/xml_parser';
import {ParseError} from '../../parse_util';
import * as i18n from '../i18n_ast';
import {MessageBundle} from '../message_bundle';
import {I18nError} from '../parse_util';
import {Serializer} from './serializer';
import {digest} from './xmb';
import {Serializer, extractPlaceholderToIds, extractPlaceholders} from './serializer';
const _TRANSLATIONS_TAG = 'translationbundle';
const _TRANSLATION_TAG = 'translation';
const _PLACEHOLDER_TAG = 'ph';
export class Xtb implements Serializer {
write(messages: i18n.Message[]): string { throw new Error('Unsupported'); }
constructor(private _htmlParser: HtmlParser, private _interpolationConfig: InterpolationConfig) {}
load(content: string, url: string): {[msgId: string]: i18n.Node[]} {
// xtb to xml nodes
const xtbParser = new XtbParser();
const {mlNodesByMsgId, errors} = xtbParser.parse(content, url);
write(messageMap: {[id: string]: i18n.Message}): string { throw new Error('Unsupported'); }
// xml nodes to i18n nodes
const i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {};
const converter = new XmlToI18n();
Object.keys(mlNodesByMsgId).forEach(msgId => {
const {i18nNodes, errors: e} = converter.convert(mlNodesByMsgId[msgId]);
errors.push(...e);
i18nNodesByMsgId[msgId] = i18nNodes;
});
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: ml.Node[]} {
// Parse the xtb file into xml nodes
const result = new XmlParser().parse(content, url);
if (result.errors.length) {
throw new Error(`xtb parse errors:\n${result.errors.join('\n')}`);
}
// Replace the placeholders, messages are now string
const {messages, errors} = new _Visitor().parse(result.rootNodes, messageBundle);
if (errors.length) {
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
}
return i18nNodesByMsgId;
}
// Convert the string messages to html ast
// TODO(vicb): map error message back to the original message in xtb
const messageMap: {[id: string]: ml.Node[]} = {};
const parseErrors: ParseError[] = [];
digest(message: i18n.Message): string { return digest(message); }
Object.keys(messages).forEach((id) => {
const res = this._htmlParser.parse(messages[id], url, true, this._interpolationConfig);
parseErrors.push(...res.errors);
messageMap[id] = res.rootNodes;
});
if (parseErrors.length) {
throw new Error(`xtb parse errors:\n${parseErrors.join('\n')}`);
}
return messageMap;
}
}
// Extract messages as xml nodes from the xtb file
class XtbParser implements ml.Visitor {
class _Visitor implements ml.Visitor {
private _messageNodes: [string, ml.Node[]][];
private _translatedMessages: {[id: string]: string};
private _bundleDepth: number;
private _translationDepth: number;
private _errors: I18nError[];
private _mlNodesByMsgId: {[msgId: string]: ml.Node[]};
private _placeholders: {[name: string]: string};
private _placeholderToIds: {[name: string]: string};
parse(xtb: string, url: string) {
parse(nodes: ml.Node[], messageBundle: MessageBundle):
{messages: {[k: string]: string}, errors: I18nError[]} {
this._messageNodes = [];
this._translatedMessages = {};
this._bundleDepth = 0;
this._mlNodesByMsgId = {};
this._translationDepth = 0;
this._errors = [];
const xml = new XmlParser().parse(xtb, url, true);
// Find all messages
ml.visitAll(this, nodes, null);
this._errors = xml.errors;
ml.visitAll(this, xml.rootNodes);
const messageMap = messageBundle.getMessageMap();
const placeholders = extractPlaceholders(messageBundle);
const placeholderToIds = extractPlaceholderToIds(messageBundle);
return {
mlNodesByMsgId: this._mlNodesByMsgId,
errors: this._errors,
};
this._messageNodes
.filter(message => {
// Remove any messages that is not present in the source message bundle.
return messageMap.hasOwnProperty(message[0]);
})
.sort((a, b) => {
// Because there could be no ICU placeholders inside an ICU message,
// we do not need to take into account the `placeholderToMsgIds` of the referenced
// messages, those would always be empty
// TODO(vicb): overkill - create 2 buckets and [...woDeps, ...wDeps].process()
if (Object.keys(messageMap[a[0]].placeholderToMsgIds).length == 0) {
return -1;
}
if (Object.keys(messageMap[b[0]].placeholderToMsgIds).length == 0) {
return 1;
}
return 0;
})
.forEach(message => {
const id = message[0];
this._placeholders = placeholders[id] || {};
this._placeholderToIds = placeholderToIds[id] || {};
// TODO(vicb): make sure there is no `_TRANSLATIONS_TAG` nor `_TRANSLATION_TAG`
this._translatedMessages[id] = ml.visitAll(this, message[1]).join('');
});
return {messages: this._translatedMessages, errors: this._errors};
}
visitElement(element: ml.Element, context: any): any {
@ -78,16 +127,40 @@ class XtbParser implements ml.Visitor {
break;
case _TRANSLATION_TAG:
this._translationDepth++;
if (this._translationDepth > 1) {
this._addError(element, `<${_TRANSLATION_TAG}> elements can not be nested`);
}
const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!idAttr) {
this._addError(element, `<${_TRANSLATION_TAG}> misses the "id" attribute`);
} else {
const id = idAttr.value;
if (this._mlNodesByMsgId.hasOwnProperty(id)) {
this._addError(element, `Duplicated translations for msg ${id}`);
} else {
this._mlNodesByMsgId[id] = element.children;
// ICU placeholders are reference to other messages.
// The referenced message might not have been decoded yet.
// We need to have all messages available to make sure deps are decoded first.
// TODO(vicb): report an error on duplicate id
this._messageNodes.push([idAttr.value, element.children]);
}
this._translationDepth--;
break;
case _PLACEHOLDER_TAG:
const nameAttr = element.attrs.find((attr) => attr.name === 'name');
if (!nameAttr) {
this._addError(element, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
} else {
const name = nameAttr.value;
if (this._placeholders.hasOwnProperty(name)) {
return this._placeholders[name];
}
if (this._placeholderToIds.hasOwnProperty(name) &&
this._translatedMessages.hasOwnProperty(this._placeholderToIds[name])) {
return this._translatedMessages[this._placeholderToIds[name]];
}
// TODO(vicb): better error message for when
// !this._translatedMessages.hasOwnProperty(this._placeholderToIds[name])
this._addError(
element, `The placeholder "${name}" does not exists in the source message`);
}
break;
@ -96,68 +169,23 @@ class XtbParser implements ml.Visitor {
}
}
visitAttribute(attribute: ml.Attribute, context: any): any {}
visitAttribute(attribute: ml.Attribute, context: any): any {
throw new Error('unreachable code');
}
visitText(text: ml.Text, context: any): any {}
visitText(text: ml.Text, context: any): any { return text.value; }
visitComment(comment: ml.Comment, context: any): any {}
visitComment(comment: ml.Comment, context: any): any { return ''; }
visitExpansion(expansion: ml.Expansion, context: any): any {}
visitExpansion(expansion: ml.Expansion, context: any): any {
const strCases = expansion.cases.map(c => c.visit(this, null));
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
}
}
// Convert ml nodes (xtb syntax) to i18n nodes
class XmlToI18n implements ml.Visitor {
private _errors: I18nError[];
convert(nodes: ml.Node[]) {
this._errors = [];
return {
i18nNodes: ml.visitAll(this, nodes),
errors: this._errors,
};
}
visitText(text: ml.Text, context: any) { return new i18n.Text(text.value, text.sourceSpan); }
visitExpansion(icu: ml.Expansion, context: any) {
const caseMap: {[value: string]: i18n.Node} = {};
ml.visitAll(this, icu.cases).forEach(c => {
caseMap[c.value] = new i18n.Container(c.nodes, icu.sourceSpan);
});
return new i18n.Icu(icu.switchValue, icu.type, caseMap, icu.sourceSpan);
}
visitExpansionCase(icuCase: ml.ExpansionCase, context: any): any {
return {
value: icuCase.value,
nodes: ml.visitAll(this, icuCase.expression),
};
}
visitElement(el: ml.Element, context: any): i18n.Placeholder {
if (el.name === _PLACEHOLDER_TAG) {
const nameAttr = el.attrs.find((attr) => attr.name === 'name');
if (nameAttr) {
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan);
}
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
} else {
this._addError(el, `Unexpected tag`);
}
}
visitComment(comment: ml.Comment, context: any) {}
visitAttribute(attribute: ml.Attribute, context: any) {}
return `{${expansion.switchValue}, ${expansion.type}, strCases.join(' ')}`;
}
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {
return `${expansionCase.value} {${ml.visitAll(this, expansionCase.expression, null)}}`;
}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));

View File

@ -7,120 +7,22 @@
*/
import * as html from '../ml_parser/ast';
import {HtmlParser} from '../ml_parser/html_parser';
import * as i18n from './i18n_ast';
import {I18nError} from './parse_util';
import {MessageBundle} from './message_bundle';
import {Serializer} from './serializers/serializer';
/**
* A container for translated messages
*/
export class TranslationBundle {
private _i18nToHtml: I18nToHtmlVisitor;
constructor(private _messageMap: {[id: string]: html.Node[]} = {}) {}
constructor(
private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {},
public digest: (m: i18n.Message) => string) {
this._i18nToHtml = new I18nToHtmlVisitor(_i18nNodesByMsgId, digest);
static load(content: string, url: string, messageBundle: MessageBundle, serializer: Serializer):
TranslationBundle {
return new TranslationBundle(serializer.load(content, url, messageBundle));
}
static load(content: string, url: string, serializer: Serializer): TranslationBundle {
const i18nNodesByMsgId = serializer.load(content, url);
const digestFn = (m: i18n.Message) => serializer.digest(m);
return new TranslationBundle(i18nNodesByMsgId, digestFn);
}
get(id: string): html.Node[] { return this._messageMap[id]; }
get(srcMsg: i18n.Message): html.Node[] {
const html = this._i18nToHtml.convert(srcMsg);
if (html.errors.length) {
throw new Error(html.errors.join('\n'));
}
return html.nodes;
}
has(srcMsg: i18n.Message): boolean { return this.digest(srcMsg) in this._i18nNodesByMsgId; }
}
class I18nToHtmlVisitor implements i18n.Visitor {
private _srcMsg: i18n.Message;
private _srcMsgStack: i18n.Message[] = [];
private _errors: I18nError[] = [];
constructor(
private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {},
private _digest: (m: i18n.Message) => string) {}
convert(srcMsg: i18n.Message): {nodes: html.Node[], errors: I18nError[]} {
this._srcMsgStack.length = 0;
this._errors.length = 0;
// i18n to text
const text = this._convertToText(srcMsg);
// text to html
const url = srcMsg.nodes[0].sourceSpan.start.file.url;
const html = new HtmlParser().parse(text, url, true);
return {
nodes: html.rootNodes,
errors: [...this._errors, ...html.errors],
};
}
visitText(text: i18n.Text, context?: any): string { return text.value; }
visitContainer(container: i18n.Container, context?: any): any {
return container.children.map(n => n.visit(this)).join('');
}
visitIcu(icu: i18n.Icu, context?: any): any {
const cases = Object.keys(icu.cases).map(k => `${k} {${icu.cases[k].visit(this)}}`);
// TODO(vicb): Once all format switch to using expression placeholders
// we should throw when the placeholder is not in the source message
const exp = this._srcMsg.placeholders.hasOwnProperty(icu.expression) ?
this._srcMsg.placeholders[icu.expression] :
icu.expression;
return `{${exp}, ${icu.type}, ${cases.join(' ')}}`;
}
visitPlaceholder(ph: i18n.Placeholder, context?: any): string {
const phName = ph.name;
if (this._srcMsg.placeholders.hasOwnProperty(phName)) {
return this._srcMsg.placeholders[phName];
}
if (this._srcMsg.placeholderToMessage.hasOwnProperty(phName)) {
return this._convertToText(this._srcMsg.placeholderToMessage[phName]);
}
this._addError(ph, `Unknown placeholder`);
return '';
}
visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): any { throw 'unreachable code'; }
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { throw 'unreachable code'; }
private _convertToText(srcMsg: i18n.Message): string {
const digest = this._digest(srcMsg);
if (this._i18nNodesByMsgId.hasOwnProperty(digest)) {
this._srcMsgStack.push(this._srcMsg);
this._srcMsg = srcMsg;
const nodes = this._i18nNodesByMsgId[digest];
const text = nodes.map(node => node.visit(this)).join('');
this._srcMsg = this._srcMsgStack.pop();
return text;
}
this._addError(srcMsg.nodes[0], `Missing translation for message ${digest}`);
return '';
}
private _addError(el: i18n.Node, msg: string) {
this._errors.push(new I18nError(el.sourceSpan, msg));
}
has(id: string): boolean { return id in this._messageMap; }
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {isPrimitive, isStrictStringMap} from './facade/lang';
import {isBlank, isPrimitive, isStrictStringMap} from './facade/lang';
export const MODULE_SUFFIX = '';
@ -48,7 +48,7 @@ export function visitValue(value: any, visitor: ValueVisitor, context: any): any
return visitor.visitStringMap(<{[key: string]: any}>value, context);
}
if (value == null || isPrimitive(value)) {
if (isBlank(value) || isPrimitive(value)) {
return visitor.visitPrimitive(value, context);
}

View File

@ -6,101 +6,53 @@
* found in the LICENSE file at https://angular.io/license
*/
import {computeMsgId, sha1} from '../../src/i18n/digest';
import {describe, expect, it} from '@angular/core/testing/testing_internal';
import {sha1} from '../../src/i18n/digest';
export function main(): void {
describe('digest', () => {
describe('sha1', () => {
it('should work on empty strings',
() => { expect(sha1('')).toEqual('da39a3ee5e6b4b0d3255bfef95601890afd80709'); });
describe('sha1', () => {
it('should work on emnpty strings',
() => { expect(sha1('')).toEqual('da39a3ee5e6b4b0d3255bfef95601890afd80709'); });
it('should returns the sha1 of "hello world"',
() => { expect(sha1('abc')).toEqual('a9993e364706816aba3e25717850c26c9cd0d89d'); });
it('should returns the sha1 of "hello world"',
() => { expect(sha1('abc')).toEqual('a9993e364706816aba3e25717850c26c9cd0d89d'); });
it('should returns the sha1 of unicode strings',
() => { expect(sha1('你好,世界')).toEqual('3becb03b015ed48050611c8d7afe4b88f70d5a20'); });
it('should returns the sha1 of unicode strings',
() => { expect(sha1('你好,世界')).toEqual('3becb03b015ed48050611c8d7afe4b88f70d5a20'); });
it('should support arbitrary string size', () => {
// node.js reference code:
//
// var crypto = require('crypto');
//
// function sha1(string) {
// var shasum = crypto.createHash('sha1');
// shasum.update(string, 'utf8');
// return shasum.digest('hex', 'utf8');
// }
//
// var prefix = `你好,世界`;
// var result = sha1(prefix);
// for (var size = prefix.length; size < 5000; size += 101) {
// result = prefix + sha1(result);
// while (result.length < size) {
// result += result;
// }
// result = result.slice(-size);
// }
//
// console.log(sha1(result));
const prefix = `你好,世界`;
let result = sha1(prefix);
for (let size = prefix.length; size < 5000; size += 101) {
result = prefix + sha1(result);
while (result.length < size) {
result += result;
}
result = result.slice(-size);
it('should support arbitrary string size', () => {
// node.js reference code:
//
// var crypto = require('crypto');
//
// function sha1(string) {
// var shasum = crypto.createHash('sha1');
// shasum.update(string, 'utf8');
// return shasum.digest('hex', 'utf8');
// }
//
// var prefix = `你好,世界`;
// var result = sha1(prefix);
// for (var size = prefix.length; size < 5000; size += 101) {
// result = prefix + sha1(result);
// while (result.length < size) {
// result += result;
// }
// result = result.slice(-size);
// }
//
// console.log(sha1(result));
const prefix = `你好,世界`;
let result = sha1(prefix);
for (let size = prefix.length; size < 5000; size += 101) {
result = prefix + sha1(result);
while (result.length < size) {
result += result;
}
expect(sha1(result)).toEqual('24c2dae5c1ac6f604dbe670a60290d7ce6320b45');
});
});
describe('decimal fingerprint', () => {
it('should work on well known inputs w/o meaning', () => {
const fixtures: {[msg: string]: string} = {
' Spaced Out ': '3976450302996657536',
'Last Name': '4407559560004943843',
'First Name': '6028371114637047813',
'View': '2509141182388535183',
'START_BOLDNUMEND_BOLD of START_BOLDmillionsEND_BOLD': '29997634073898638',
'The customer\'s credit card was authorized for AMOUNT and passed all risk checks.':
'6836487644149622036',
'Hello world!': '3022994926184248873',
'Jalape\u00f1o': '8054366208386598941',
'The set of SET_NAME is {XXX, ...}.': '135956960462609535',
'NAME took a trip to DESTINATION.': '768490705511913603',
'by AUTHOR (YEAR)': '7036633296476174078',
'': '4416290763660062288',
};
Object.keys(fixtures).forEach(
msg => { expect(computeMsgId(msg, '')).toEqual(fixtures[msg]); });
});
it('should work on well known inputs with meaning', () => {
const fixtures: {[msg: string]: [string, string]} = {
'7790835225175622807': ['Last Name', 'Gmail UI'],
'1809086297585054940': ['First Name', 'Gmail UI'],
'3993998469942805487': ['View', 'Gmail UI'],
};
Object.keys(fixtures).forEach(
id => { expect(computeMsgId(fixtures[id][0], fixtures[id][1])).toEqual(id); });
});
it('should support arbitrary string size', () => {
const prefix = `你好,世界`;
let result = computeMsgId(prefix, '');
for (let size = prefix.length; size < 5000; size += 101) {
result = prefix + computeMsgId(result, '');
while (result.length < size) {
result += result;
}
result = result.slice(-size);
}
expect(computeMsgId(result, '')).toEqual('2122606631351252558');
});
result = result.slice(-size);
}
expect(sha1(result)).toEqual('24c2dae5c1ac6f604dbe670a60290d7ce6320b45');
});
});
}

View File

@ -6,13 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {DEFAULT_INTERPOLATION_CONFIG, HtmlParser} from '@angular/compiler';
import {describe, expect, it} from '@angular/core/testing/testing_internal';
import {digest, serializeNodes as serializeI18nNodes} from '../../src/i18n/digest';
import {digestMessage, serializeNodes as serializeI18nNodes} from '../../src/i18n/digest';
import {extractMessages, mergeTranslations} from '../../src/i18n/extractor_merger';
import * as i18n from '../../src/i18n/i18n_ast';
import {TranslationBundle} from '../../src/i18n/translation_bundle';
import * as html from '../../src/ml_parser/ast';
import {HtmlParser} from '../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
import {serializeNodes as serializeHtmlNodes} from '../ml_parser/ast_serializer_spec';
export function main() {
@ -92,10 +94,9 @@ export function main() {
],
[
[
'text', '<ph tag name="START_PARAGRAPH">html, <ph tag' +
' name="START_BOLD_TEXT">nested</ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">',
'<ph icu name="ICU">{count, plural, =0 {[<ph tag' +
' name="START_TAG_SPAN">html</ph name="CLOSE_TAG_SPAN">]}}</ph>',
'text',
'<ph tag name="START_PARAGRAPH">html, <ph tag name="START_BOLD_TEXT">nested</ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">',
'<ph icu name="ICU">{count, plural, =0 {[<ph tag name="START_TAG_SPAN">html</ph name="CLOSE_TAG_SPAN">]}}</ph>',
'[<ph name="INTERPOLATION">interp</ph>]'
],
'', ''
@ -189,8 +190,9 @@ export function main() {
it('should extract from attributes in translatable elements', () => {
expect(extract('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>')).toEqual([
[
['<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph' +
' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'],
[
'<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'
],
'', ''
],
[['msg'], 'm', 'd'],
@ -202,8 +204,9 @@ export function main() {
.toEqual([
[['msg'], 'm', 'd'],
[
['<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph' +
' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'],
[
'<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'
],
'', ''
],
]);
@ -217,8 +220,7 @@ export function main() {
[['msg'], 'm', 'd'],
[
[
'{count, plural, =0 {[<ph tag name="START_PARAGRAPH"><ph tag' +
' name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">]}}'
'{count, plural, =0 {[<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">]}}'
],
'', ''
],
@ -349,9 +351,7 @@ export function main() {
const HTML = `before<!-- i18n --><p>foo</p><span><i>bar</i></span><!-- /i18n -->after`;
expect(fakeTranslate(HTML))
.toEqual(
'before**[ph tag name="START_PARAGRAPH">foo[/ph name="CLOSE_PARAGRAPH">[ph tag' +
' name="START_TAG_SPAN">[ph tag name="START_ITALIC_TEXT">bar[/ph' +
' name="CLOSE_ITALIC_TEXT">[/ph name="CLOSE_TAG_SPAN">**after');
'before**<ph tag name="START_PARAGRAPH">foo</ph name="CLOSE_PARAGRAPH"><ph tag name="START_TAG_SPAN"><ph tag name="START_ITALIC_TEXT">bar</ph name="CLOSE_ITALIC_TEXT"></ph name="CLOSE_TAG_SPAN">**after');
});
it('should merge nested blocks', () => {
@ -359,9 +359,7 @@ export function main() {
`<div>before<!-- i18n --><p>foo</p><span><i>bar</i></span><!-- /i18n -->after</div>`;
expect(fakeTranslate(HTML))
.toEqual(
'<div>before**[ph tag name="START_PARAGRAPH">foo[/ph name="CLOSE_PARAGRAPH">[ph' +
' tag name="START_TAG_SPAN">[ph tag name="START_ITALIC_TEXT">bar[/ph' +
' name="CLOSE_ITALIC_TEXT">[/ph name="CLOSE_TAG_SPAN">**after</div>');
'<div>before**<ph tag name="START_PARAGRAPH">foo</ph name="CLOSE_PARAGRAPH"><ph tag name="START_TAG_SPAN"><ph tag name="START_ITALIC_TEXT">bar</ph name="CLOSE_ITALIC_TEXT"></ph name="CLOSE_TAG_SPAN">**after</div>');
});
});
@ -402,15 +400,15 @@ function fakeTranslate(
extractMessages(htmlNodes, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs)
.messages;
const i18nMsgMap: {[id: string]: i18n.Node[]} = {};
const i18nMsgMap: {[id: string]: html.Node[]} = {};
messages.forEach(message => {
const id = digest(message);
const text = serializeI18nNodes(message.nodes).join('').replace(/</g, '[');
i18nMsgMap[id] = [new i18n.Text(`**${text}**`, null)];
const id = digestMessage(message);
const text = serializeI18nNodes(message.nodes).join('');
i18nMsgMap[id] = [new html.Text(`**${text}**`, null)];
});
const translations = new TranslationBundle(i18nMsgMap, digest);
const translations = new TranslationBundle(i18nMsgMap);
const translatedNodes =
mergeTranslations(

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {digest} from '@angular/compiler/src/i18n/digest';
import {extractMessages} from '@angular/compiler/src/i18n/extractor_merger';
import {Message} from '@angular/compiler/src/i18n/i18n_ast';
import {describe, expect, it} from '@angular/core/testing/testing_internal';
import {serializeNodes} from '../../src/i18n/digest';
import {HtmlParser} from '../../src/ml_parser/html_parser';
@ -272,14 +272,11 @@ export function main() {
[['{count, plural, =1 {[1]}}'], '', ''],
]);
expect(_humanizePlaceholders(html)).toEqual([
'',
'VAR_PLURAL=count',
'VAR_PLURAL=count',
'VAR_PLURAL=count',
]);
// ICU message placeholders are reference to translations.
// As such they have no static content but refs to message ids.
expect(_humanizePlaceholders(html)).toEqual(['', '', '', '']);
expect(_humanizePlaceholdersToMessage(html)).toEqual([
expect(_humanizePlaceholdersToIds(html)).toEqual([
'ICU=f0f76923009914f1b05f41042a5c7231b9496504, ICU_1=73693d1f78d0fc882f0bcbce4cb31a0aa1995cfe',
'',
'',
@ -311,13 +308,13 @@ function _humanizePlaceholders(
// clang-format on
}
function _humanizePlaceholdersToMessage(
function _humanizePlaceholdersToIds(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): string[] {
// clang-format off
// https://github.com/angular/clang-format/issues/35
return _extractMessages(html, implicitTags, implicitAttrs).map(
msg => Object.keys(msg.placeholderToMessage).map(k => `${k}=${digest(msg.placeholderToMessage[k])}`).join(', '));
msg => Object.keys(msg.placeholderToMsgIds).map(k => `${k}=${msg.placeholderToMsgIds[k]}`).join(', '));
// clang-format on
}

View File

@ -43,9 +43,6 @@ export function main() {
expectHtml(el, '#i18n-2').toBe('<div id="i18n-2"><p>imbriqué</p></div>');
expectHtml(el, '#i18n-3')
.toBe('<div id="i18n-3"><p><i>avec des espaces réservés</i></p></div>');
expectHtml(el, '#i18n-3b')
.toBe(
'<div id="i18n-3b"><p><i class="preserved-on-placeholders">avec des espaces réservés</i></p></div>');
expectHtml(el, '#i18n-4')
.toBe('<p id="i18n-4" title="sur des balises non traductibles"></p>');
expectHtml(el, '#i18n-5').toBe('<p id="i18n-5" title="sur des balises traductibles"></p>');
@ -69,10 +66,8 @@ export function main() {
expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('beaucoup');
cmp.sex = 'm';
cmp.sexB = 'f';
tb.detectChanges();
expect(el.query(By.css('#i18n-8')).nativeElement).toHaveText('homme');
expect(el.query(By.css('#i18n-8b')).nativeElement).toHaveText('femme');
cmp.sex = 'f';
tb.detectChanges();
expect(el.query(By.css('#i18n-8')).nativeElement).toHaveText('femme');
@ -111,7 +106,6 @@ function expectHtml(el: DebugElement, cssSelector: string): any {
<div id="i18n-2"><p i18n="different meaning|">nested</p></div>
<div id="i18n-3"><p i18n><i>with placeholders</i></p></div>
<div id="i18n-3b"><p i18n><i class="preserved-on-placeholders">with placeholders</i></p></div>
<div>
<p id="i18n-4" i18n-title title="on not translatable node"></p>
@ -123,10 +117,7 @@ function expectHtml(el: DebugElement, cssSelector: string): any {
<div i18n id="i18n-7">{count, plural, =0 {zero} =1 {one} =2 {two} other {<b>many</b>}}</div>
<div i18n id="i18n-8">
{sex, select, m {male} f {female}}
</div>
<div i18n id="i18n-8b">
{sexB, select, m {male} f {female}}
{sex, sex, m {male} f {female}}
</div>
<div i18n id="i18n-9">{{ "count = " + count }}</div>
@ -144,9 +135,8 @@ function expectHtml(el: DebugElement, cssSelector: string): any {
`
})
class I18nComponent {
count: number;
sex: string;
sexB: string;
count: number = 0;
sex: string = 'm';
}
class FrLocalization extends NgLocalization {
@ -163,52 +153,51 @@ class FrLocalization extends NgLocalization {
const XTB = `
<translationbundle>
<translation id="615790887472569365">attributs i18n sur les balises</translation>
<translation id="3707494640264351337">imbriqué</translation>
<translation id="5539162898278769904">imbriqué</translation>
<translation id="3780349238193953556"><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation>
<translation id="5525133077318024839">sur des balises non traductibles</translation>
<translation id="8670732454866344690">sur des balises traductibles</translation>
<translation id="4593805537723189714">{VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {<ph name="START_BOLD_TEXT"/>beaucoup<ph name="CLOSE_BOLD_TEXT"/>}}</translation>
<translation id="1746565782635215"><ph name="ICU"/></translation>
<translation id="5868084092545682515">{VAR_SELECT, select, m {homme} f {femme}}</translation>
<translation id="4851788426695310455"><ph name="INTERPOLATION"/></translation>
<translation id="9013357158046221374">sexe = <ph name="INTERPOLATION"/></translation>
<translation id="8324617391167353662"><ph name="CUSTOM_NAME"/></translation>
<translation id="7685649297917455806">dans une section traductible</translation>
<translation id="2387287228265107305">
<translation id="3cb04208df1c2f62553ed48e75939cf7107f9dad">attributs i18n sur les balises</translation>
<translation id="52895b1221effb3f3585b689f049d2784d714952">imbriqué</translation>
<translation id="88d5f22050a9df477ee5646153558b3a4862d47e">imbriqué</translation>
<translation id="34fec9cc62e28e8aa6ffb306fa8569ef0a8087fe"><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation>
<translation id="1fe4616cce80a57c7707bac1c97054aa8e244a67">sur des balises non traductibles</translation>
<translation id="67162b5af5f15fd0eb6480c88688dafdf952b93a">sur des balises traductibles</translation>
<translation id="dc5536bb9e0e07291c185a0d306601a2ecd4813f">{count, plural, =0 {zero} =1 {un} =2 {deux} other {<ph name="START_BOLD_TEXT"/>beaucoup<ph name="CLOSE_BOLD_TEXT"/>}}</translation>
<translation id="018efa03821ca41e27611e4a584736810d56ed8a"><ph name="ICU"/></translation>
<translation id="fd3186ad2a9aa801fe072ddb16ca34cd98ae93da">{sex, sex, m {homme} f {femme}}</translation>
<translation id="d9879678f727b244bc7c7e20f22b63d98cb14890"><ph name="INTERPOLATION"/></translation>
<translation id="50dac33dc6fc0578884baac79d875785ed77c928">sexe = <ph name="INTERPOLATION"/></translation>
<translation id="a46f833b1fe6ca49e8b97c18f4b7ea0b930c9383"><ph name="CUSTOM_NAME"/></translation>
<translation id="2ec983b4893bcd5b24af33bebe3ecba63868453c">dans une section traductible</translation>
<translation id="eee74a5be8a75881a4785905bd8302a71f7d9f75">
<ph name="START_HEADING_LEVEL1"/>Balises dans les commentaires html<ph name="CLOSE_HEADING_LEVEL1"/>
<ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/>
<ph name="START_TAG_DIV_1"/><ph name="ICU"/><ph name="CLOSE_TAG_DIV"></ph>
</translation>
<translation id="1491627405349178954">ca <ph name="START_BOLD_TEXT"/>devrait<ph name="CLOSE_BOLD_TEXT"/> marcher</translation>
<translation id="93a30c67d4e6c9b37aecfe2ac0f2b5d366d7b520">ca <ph name="START_BOLD_TEXT"/>devrait<ph name="CLOSE_BOLD_TEXT"/> marcher</translation>
</translationbundle>`;
// unused, for reference only
// can be generated from xmb_spec as follow:
// `fit('extract xmb', () => { console.log(toXmb(HTML)); });`
// `iit('extract xmb', () => { console.log(toXmb(HTML)); });`
const XMB = `
<messagebundle>
<msg id="615790887472569365">i18n attribute on tags</msg>
<msg id="3707494640264351337">nested</msg>
<msg id="5539162898278769904" meaning="different meaning">nested</msg>
<msg id="3780349238193953556"><ph name="START_ITALIC_TEXT"><ex>&lt;i&gt;</ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex>&lt;/i&gt;</ex></ph></msg>
<msg id="5525133077318024839">on not translatable node</msg>
<msg id="8670732454866344690">on translatable node</msg>
<msg id="4593805537723189714">{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</msg>
<msg id="1746565782635215">
<msg id="3cb04208df1c2f62553ed48e75939cf7107f9dad">i18n attribute on tags</msg>
<msg id="52895b1221effb3f3585b689f049d2784d714952">nested</msg>
<msg id="88d5f22050a9df477ee5646153558b3a4862d47e" meaning="different meaning">nested</msg>
<msg id="34fec9cc62e28e8aa6ffb306fa8569ef0a8087fe"><ph name="START_ITALIC_TEXT"><ex>&lt;i&gt;</ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex>&lt;/i&gt;</ex></ph></msg>
<msg id="1fe4616cce80a57c7707bac1c97054aa8e244a67">on not translatable node</msg>
<msg id="67162b5af5f15fd0eb6480c88688dafdf952b93a">on translatable node</msg>
<msg id="dc5536bb9e0e07291c185a0d306601a2ecd4813f">{count, plural, =0 {zero}=1 {one}=2 {two}other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>}}</msg>
<msg id="018efa03821ca41e27611e4a584736810d56ed8a">
<ph name="ICU"/>
</msg>
<msg id="5868084092545682515">{VAR_SELECT, select, m {male} f {female} }</msg>
<msg id="4851788426695310455"><ph name="INTERPOLATION"/></msg>
<msg id="9013357158046221374">sex = <ph name="INTERPOLATION"/></msg>
<msg id="8324617391167353662"><ph name="CUSTOM_NAME"/></msg>
<msg id="7685649297917455806">in a translatable section</msg>
<msg id="2387287228265107305">
<msg id="fd3186ad2a9aa801fe072ddb16ca34cd98ae93da">{sex, sex, m {male}f {female}}</msg>
<msg id="d9879678f727b244bc7c7e20f22b63d98cb14890"><ph name="INTERPOLATION"/></msg>
<msg id="50dac33dc6fc0578884baac79d875785ed77c928">sex = <ph name="INTERPOLATION"/></msg>
<msg id="a46f833b1fe6ca49e8b97c18f4b7ea0b930c9383"><ph name="CUSTOM_NAME"/></msg>
<msg id="2ec983b4893bcd5b24af33bebe3ecba63868453c">in a translatable section</msg>
<msg id="eee74a5be8a75881a4785905bd8302a71f7d9f75">
<ph name="START_HEADING_LEVEL1"><ex>&lt;h1&gt;</ex></ph>Markers in html comments<ph name="CLOSE_HEADING_LEVEL1"><ex>&lt;/h1&gt;</ex></ph>
<ph name="START_TAG_DIV"><ex>&lt;div&gt;</ex></ph><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph>
<ph name="START_TAG_DIV_1"><ex>&lt;div&gt;</ex></ph><ph name="ICU"/><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph>
</msg>
<msg id="1491627405349178954">it <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> work</msg>
</messagebundle>
`;
<msg id="93a30c67d4e6c9b37aecfe2ac0f2b5d366d7b520">it <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> work</msg>
</messagebundle>`;

View File

@ -6,10 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as i18n from '@angular/compiler/src/i18n/i18n_ast';
import {Serializer} from '@angular/compiler/src/i18n/serializers/serializer';
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {serializeNodes} from '../../src/i18n/digest';
import * as i18n from '../../src/i18n/i18n_ast';
import {MessageBundle} from '../../src/i18n/message_bundle';
import {Serializer} from '../../src/i18n/serializers/serializer';
import {HtmlParser} from '../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
@ -24,18 +26,17 @@ export function main(): void {
messages.updateFromTemplate(
'<p i18n="m|d">Translate Me</p>', 'url', DEFAULT_INTERPOLATION_CONFIG);
expect(humanizeMessages(messages)).toEqual([
'Translate Me (m|d)',
'2e791a68a3324ecdd29e252198638dafacec46e9=Translate Me',
]);
});
it('should extract the all messages and duplicates', () => {
it('should extract the same message with different meaning in different entries', () => {
messages.updateFromTemplate(
'<p i18n="m|d">Translate Me</p><p i18n>Translate Me</p><p i18n>Translate Me</p>', 'url',
'<p i18n="m|d">Translate Me</p><p i18n>Translate Me</p>', 'url',
DEFAULT_INTERPOLATION_CONFIG);
expect(humanizeMessages(messages)).toEqual([
'Translate Me (m|d)',
'Translate Me (|)',
'Translate Me (|)',
'2e791a68a3324ecdd29e252198638dafacec46e9=Translate Me',
'8ca133f957845af1b1868da1b339180d1f519644=Translate Me',
]);
});
});
@ -43,14 +44,13 @@ export function main(): void {
}
class _TestSerializer implements Serializer {
write(messages: i18n.Message[]): string {
return messages.map(msg => `${serializeNodes(msg.nodes)} (${msg.meaning}|${msg.description})`)
write(messageMap: {[id: string]: i18n.Message}): string {
return Object.keys(messageMap)
.map(id => `${id}=${serializeNodes(messageMap[id].nodes)}`)
.join('//');
}
load(content: string, url: string): {} { return null; }
digest(msg: i18n.Message): string { return 'unused'; }
load(content: string, url: string, placeholders: {}): {} { return null; }
}
function humanizeMessages(catalog: MessageBundle): string[] {

View File

@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {PlaceholderRegistry} from '../../../src/i18n/serializers/placeholder';
export function main(): void {

View File

@ -6,13 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {escapeRegExp} from '@angular/core/src/facade/lang';
import {serializeNodes} from '../../../src/i18n/digest';
import {Xliff} from '@angular/compiler/src/i18n/serializers/xliff';
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {MessageBundle} from '../../../src/i18n/message_bundle';
import {Xliff} from '../../../src/i18n/serializers/xliff';
import {HtmlParser} from '../../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config';
import {serializeNodes} from '../../ml_parser/ast_serializer_spec';
const HTML = `
<p i18n-title title="translatable attribute">not translatable</p>
@ -78,7 +77,8 @@ const LOAD_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
`;
export function main(): void {
const serializer = new Xliff();
let serializer: Xliff;
let htmlParser: HtmlParser;
function toXliff(html: string): string {
const catalog = new MessageBundle(new HtmlParser, [], {});
@ -86,130 +86,39 @@ export function main(): void {
return catalog.write(serializer);
}
function loadAsMap(xliff: string): {[id: string]: string} {
const i18nNodesByMsgId = serializer.load(xliff, 'url');
const msgMap: {[id: string]: string} = {};
Object.keys(i18nNodesByMsgId)
.forEach(id => msgMap[id] = serializeNodes(i18nNodesByMsgId[id]).join(''));
function loadAsText(template: string, xliff: string): {[id: string]: string} {
const messageBundle = new MessageBundle(htmlParser, [], {});
messageBundle.updateFromTemplate(template, 'url', DEFAULT_INTERPOLATION_CONFIG);
return msgMap;
const asAst = serializer.load(xliff, 'url', messageBundle);
const asText: {[id: string]: string} = {};
Object.keys(asAst).forEach(id => { asText[id] = serializeNodes(asAst[id]).join(''); });
return asText;
}
describe('XLIFF serializer', () => {
beforeEach(() => {
htmlParser = new HtmlParser();
serializer = new Xliff(htmlParser, DEFAULT_INTERPOLATION_CONFIG);
});
describe('write', () => {
it('should write a valid xliff file', () => { expect(toXliff(HTML)).toEqual(WRITE_XLIFF); });
});
describe('load', () => {
it('should load XLIFF files', () => {
expect(loadAsMap(LOAD_XLIFF)).toEqual({
expect(loadAsText(HTML, LOAD_XLIFF)).toEqual({
'983775b9a51ce14b036be72d4cfd65d68d64e231': 'etubirtta elbatalsnart',
'ec1d033f2436133c14ab038286c4f5df4697484a':
'<ph name="INTERPOLATION"/> footnemele elbatalsnart <ph name="START_BOLD_TEXT"/>sredlohecalp htiw<ph name="CLOSE_BOLD_TEXT"/>',
'{{ interpolation}} footnemele elbatalsnart <b>sredlohecalp htiw</b>',
'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': 'oof',
'd7fa2d59aaedcaa5309f13028c59af8c85b8c49d':
'<ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/><ph name="TAG_IMG"/><ph name="LINE_BREAK"/>',
'd7fa2d59aaedcaa5309f13028c59af8c85b8c49d': '<div></div><img/><br/>',
});
});
describe('structure errors', () => {
it('should throw when a trans-unit has no translation', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="missingtarget">
<source/>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(/Message missingtarget misses a translation/);
});
it('should throw when a trans-unit has no id attribute', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit datatype="html">
<source/>
<target/>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(/<trans-unit> misses the "id" attribute/);
});
it('should throw on duplicate trans-unit id', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="deadbeef">
<source/>
<target/>
</trans-unit>
<trans-unit id="deadbeef">
<source/>
<target/>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(/Duplicated translations for msg deadbeef/);
});
});
describe('message errors', () => {
it('should throw on unknown message tags', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="deadbeef" datatype="html">
<source/>
<target><b>msg should contain only ph tags</b></target>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => { loadAsMap(XLIFF); })
.toThrowError(
new RegExp(escapeRegExp(`[ERROR ->]<b>msg should contain only ph tags</b>`)));
});
it('should throw when a placeholder misses an id attribute', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="deadbeef" datatype="html">
<source/>
<target><x/></target>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(new RegExp(escapeRegExp(`<x> misses the "id" attribute`)));
});
});
});
});
}

View File

@ -43,10 +43,10 @@ export function main(): void {
<!ELEMENT ex (#PCDATA)>
]>
<messagebundle>
<msg id="7056919470098446707">translatable element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></msg>
<msg id="2981514368455622387">{VAR_PLURAL, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} }</msg>
<msg id="7999024498831672133" desc="d" meaning="m">foo</msg>
<msg id="2015957479576096115">{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} } } }</msg>
<msg id="ec1d033f2436133c14ab038286c4f5df4697484a">translatable element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></msg>
<msg id="e2ccf3d131b15f54aa1fcf1314b1ca77c14bfcc2">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} }</msg>
<msg id="db3e0a6a5a96481f60aec61d98c3eecddef5ac23" desc="d" meaning="m">foo</msg>
<msg id="0e16a673a5a7a135c9f7b957ec2c5c6f6ee6e2c4">{ count, plural, =0 {{ sex, select, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} } } }</msg>
</messagebundle>
`;
@ -55,7 +55,7 @@ export function main(): void {
it('should throw when trying to load an xmb file', () => {
expect(() => {
const serializer = new Xmb();
serializer.load(XMB, 'url');
serializer.load(XMB, 'url', null);
}).toThrowError(/Unsupported/);
});
});

View File

@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {describe, expect, it} from '@angular/core/testing/testing_internal';
import * as xml from '../../../src/i18n/serializers/xml_helper';
export function main(): void {

View File

@ -8,24 +8,37 @@
import {escapeRegExp} from '@angular/core/src/facade/lang';
import {serializeNodes} from '../../../src/i18n/digest';
import {MessageBundle} from '../../../src/i18n/message_bundle';
import {Xtb} from '../../../src/i18n/serializers/xtb';
import {HtmlParser} from '../../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config';
import {serializeNodes} from '../../ml_parser/ast_serializer_spec';
export function main(): void {
describe('XTB serializer', () => {
const serializer = new Xtb();
let serializer: Xtb;
let htmlParser: HtmlParser;
function loadAsMap(xtb: string): {[id: string]: string} {
const i18nNodesByMsgId = serializer.load(xtb, 'url');
const msgMap: {[id: string]: string} = {};
Object.keys(i18nNodesByMsgId).forEach(id => {
msgMap[id] = serializeNodes(i18nNodesByMsgId[id]).join('');
});
return msgMap;
function loadAsText(template: string, xtb: string): {[id: string]: string} {
const messageBundle = new MessageBundle(htmlParser, [], {});
messageBundle.updateFromTemplate(template, 'url', DEFAULT_INTERPOLATION_CONFIG);
const asAst = serializer.load(xtb, 'url', messageBundle);
const asText: {[id: string]: string} = {};
Object.keys(asAst).forEach(id => { asText[id] = serializeNodes(asAst[id]).join(''); });
return asText;
}
beforeEach(() => {
htmlParser = new HtmlParser();
serializer = new Xtb(htmlParser, DEFAULT_INTERPOLATION_CONFIG);
});
describe('load', () => {
it('should load XTB files with a doctype', () => {
const HTML = `<div i18n>bar</div>`;
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE translationbundle [<!ELEMENT translationbundle (translation)*>
<!ATTLIST translationbundle lang CDATA #REQUIRED>
@ -37,66 +50,75 @@ export function main(): void {
<!ATTLIST ph name CDATA #REQUIRED>
]>
<translationbundle>
<translation id="8841459487341224498">rab</translation>
<translation id="28a86c8a00ae573b2bac698d6609316dc7b4a226">rab</translation>
</translationbundle>`;
expect(loadAsMap(XTB)).toEqual({'8841459487341224498': 'rab'});
expect(loadAsText(HTML, XTB)).toEqual({'28a86c8a00ae573b2bac698d6609316dc7b4a226': 'rab'});
});
it('should load XTB files without placeholders', () => {
const HTML = `<div i18n>bar</div>`;
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<translationbundle>
<translation id="8841459487341224498">rab</translation>
<translation id="28a86c8a00ae573b2bac698d6609316dc7b4a226">rab</translation>
</translationbundle>`;
expect(loadAsMap(XTB)).toEqual({'8841459487341224498': 'rab'});
expect(loadAsText(HTML, XTB)).toEqual({'28a86c8a00ae573b2bac698d6609316dc7b4a226': 'rab'});
});
it('should load XTB files with placeholders', () => {
const HTML = `<div i18n><p>bar</p></div>`;
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<translationbundle>
<translation id="8877975308926375834"><ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/></translation>
<translation id="7de4d8ff1e42b7b31da6204074818236a9a5317f"><ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/></translation>
</translationbundle>`;
expect(loadAsMap(XTB)).toEqual({
'8877975308926375834': '<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>'
expect(loadAsText(HTML, XTB)).toEqual({
'7de4d8ff1e42b7b31da6204074818236a9a5317f': '<p>rab</p>'
});
});
it('should replace ICU placeholders with their translations', () => {
const HTML = `<div i18n>-{ count, plural, =0 {<p>bar</p>}}-</div>`;
const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
<translationbundle>
<translation id="7717087045075616176">*<ph name="ICU"/>*</translation>
<translation id="5115002811911870583">{VAR_PLURAL, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
<translation id="eb404e202fed4846e25e7d9ac1fcb719fe4da257">*<ph name="ICU"/>*</translation>
<translation id="fc92b9b781194a02ab773129c8c5a7fc0735efd7">{ count, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
</translationbundle>`;
expect(loadAsMap(XTB)).toEqual({
'7717087045075616176': `*<ph name="ICU"/>*`,
'5115002811911870583':
`{VAR_PLURAL, plural, =1 {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}`,
expect(loadAsText(HTML, XTB)).toEqual({
'eb404e202fed4846e25e7d9ac1fcb719fe4da257': `*{ count, plural, =1 {<p>rab</p>}}*`,
'fc92b9b781194a02ab773129c8c5a7fc0735efd7': `{ count, plural, =1 {<p>rab</p>}}`,
});
});
it('should load complex XTB files', () => {
const HTML = `
<div i18n>foo <b>bar</b> {{ a + b }}</div>
<div i18n>{ count, plural, =0 {<p>bar</p>}}</div>
<div i18n="m|d">foo</div>
<div i18n>{ count, plural, =0 {{ sex, select, other {<p>bar</p>}} }}</div>`;
const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
<translationbundle>
<translation id="8281795707202401639"><ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof</translation>
<translation id="5115002811911870583">{VAR_PLURAL, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
<translation id="130772889486467622">oof</translation>
<translation id="4739316421648347533">{VAR_PLURAL, plural, =1 {{VAR_GENDER, gender, male {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}} }}</translation>
<translation id="7103b4b13b616270a0044efade97d8b4f96f2ca6"><ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof</translation>
<translation id="fc92b9b781194a02ab773129c8c5a7fc0735efd7">{ count, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
<translation id="db3e0a6a5a96481f60aec61d98c3eecddef5ac23">oof</translation>
<translation id="8fb569d3dd83e92eff2551b24f5290d3035ce61b">{ count, plural, =1 {{ sex, select, other {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}} }}</translation>
</translationbundle>`;
expect(loadAsMap(XTB)).toEqual({
'8281795707202401639':
`<ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof`,
'5115002811911870583':
`{VAR_PLURAL, plural, =1 {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}`,
'130772889486467622': `oof`,
'4739316421648347533':
`{VAR_PLURAL, plural, =1 {[{VAR_GENDER, gender, male {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}, ]}}`,
expect(loadAsText(HTML, XTB)).toEqual({
'7103b4b13b616270a0044efade97d8b4f96f2ca6': `{{ a + b }}<b>rab</b> oof`,
'fc92b9b781194a02ab773129c8c5a7fc0735efd7': `{ count, plural, =1 {<p>rab</p>}}`,
'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': `oof`,
'8fb569d3dd83e92eff2551b24f5290d3035ce61b':
`{ count, plural, =1 {{ sex, select, other {<p>rab</p>}} }}`,
});
});
});
describe('errors', () => {
@ -105,7 +127,7 @@ export function main(): void {
'<translationbundle><translationbundle></translationbundle></translationbundle>';
expect(() => {
loadAsMap(XTB);
loadAsText('', XTB);
}).toThrowError(/<translationbundle> elements can not be nested/);
});
@ -114,49 +136,58 @@ export function main(): void {
<translation></translation>
</translationbundle>`;
expect(() => { loadAsMap(XTB); }).toThrowError(/<translation> misses the "id" attribute/);
expect(() => {
loadAsText('', XTB);
}).toThrowError(/<translation> misses the "id" attribute/);
});
it('should throw when a placeholder has no name attribute', () => {
const HTML = '<div i18n>give me a message</div>';
const XTB = `<translationbundle>
<translation id="1186013544048295927"><ph /></translation>
<translation id="8de97c6a35252d9409dcaca0b8171c952740b28c"><ph /></translation>
</translationbundle>`;
expect(() => { loadAsMap(XTB); }).toThrowError(/<ph> misses the "name" attribute/);
expect(() => { loadAsText(HTML, XTB); }).toThrowError(/<ph> misses the "name" attribute/);
});
it('should throw on unknown xtb tags', () => {
const XTB = `<what></what>`;
it('should throw when a placeholder is not present in the source message', () => {
const HTML = `<div i18n>bar</div>`;
expect(() => {
loadAsMap(XTB);
}).toThrowError(new RegExp(escapeRegExp(`Unexpected tag ("[ERROR ->]<what></what>")`)));
});
it('should throw on unknown message tags', () => {
const XTB = `<translationbundle>
<translation id="1186013544048295927"><b>msg should contain only ph tags</b></translation>
</translationbundle>`;
expect(() => { loadAsMap(XTB); })
.toThrowError(
new RegExp(escapeRegExp(`[ERROR ->]<b>msg should contain only ph tags</b>`)));
});
it('should throw on duplicate message id', () => {
const XTB = `<translationbundle>
<translation id="1186013544048295927">msg1</translation>
<translation id="1186013544048295927">msg2</translation>
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<translationbundle>
<translation id="28a86c8a00ae573b2bac698d6609316dc7b4a226"><ph name="UNKNOWN"/></translation>
</translationbundle>`;
expect(() => {
loadAsMap(XTB);
}).toThrowError(/Duplicated translations for msg 1186013544048295927/);
loadAsText(HTML, XTB);
}).toThrowError(/The placeholder "UNKNOWN" does not exists in the source message/);
});
});
it('should throw when trying to save an xtb file',
() => { expect(() => { serializer.write([]); }).toThrowError(/Unsupported/); });
it('should throw when the translation results in invalid html', () => {
const HTML = `<div i18n><p>bar</p></div>`;
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<translationbundle>
<translation id="7de4d8ff1e42b7b31da6204074818236a9a5317f">rab<ph name="CLOSE_PARAGRAPH"/></translation>
</translationbundle>`;
expect(() => {
loadAsText(HTML, XTB);
}).toThrowError(/xtb parse errors:\nUnexpected closing tag "p"/);
});
it('should throw on unknown tags', () => {
const XTB = `<what></what>`;
expect(() => {
loadAsText('', XTB);
}).toThrowError(new RegExp(escapeRegExp(`Unexpected tag ("[ERROR ->]<what></what>")`)));
});
it('should throw when trying to save an xtb file',
() => { expect(() => { serializer.write({}); }).toThrowError(/Unsupported/); });
});
}

View File

@ -1,110 +0,0 @@
/**
* @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
*/
import * as i18n from '../../src/i18n/i18n_ast';
import {TranslationBundle} from '../../src/i18n/translation_bundle';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
import {serializeNodes} from '../ml_parser/ast_serializer_spec';
export function main(): void {
describe('TranslationBundle', () => {
const file = new ParseSourceFile('content', 'url');
const location = new ParseLocation(file, 0, 0, 0);
const span = new ParseSourceSpan(location, null);
const srcNode = new i18n.Text('src', span);
it('should translate a plain message', () => {
const msgMap = {foo: [new i18n.Text('bar', null)]};
const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
expect(serializeNodes(tb.get(msg))).toEqual(['bar']);
});
it('should translate a message with placeholder', () => {
const msgMap = {
foo: [
new i18n.Text('bar', null),
new i18n.Placeholder('', 'ph1', null),
]
};
const phMap = {
ph1: '*phContent*',
};
const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd');
expect(serializeNodes(tb.get(msg))).toEqual(['bar*phContent*']);
});
it('should translate a message with placeholder referencing messages', () => {
const msgMap = {
foo: [
new i18n.Text('--', null),
new i18n.Placeholder('', 'ph1', null),
new i18n.Text('++', null),
],
ref: [
new i18n.Text('*refMsg*', null),
],
};
const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd');
let count = 0;
const digest = (_: any) => count++ ? 'ref' : 'foo';
const tb = new TranslationBundle(msgMap, digest);
expect(serializeNodes(tb.get(msg))).toEqual(['--*refMsg*++']);
});
describe('errors', () => {
it('should report unknown placeholders', () => {
const msgMap = {
foo: [
new i18n.Text('bar', null),
new i18n.Placeholder('', 'ph1', span),
]
};
const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
expect(() => tb.get(msg)).toThrowError(/Unknown placeholder/);
});
it('should report missing translation', () => {
const tb = new TranslationBundle({}, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
expect(() => tb.get(msg)).toThrowError(/Missing translation for message foo/);
});
it('should report missing referenced message', () => {
const msgMap = {
foo: [new i18n.Placeholder('', 'ph1', span)],
};
const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd');
let count = 0;
const digest = (_: any) => count++ ? 'ref' : 'foo';
const tb = new TranslationBundle(msgMap, digest);
expect(() => tb.get(msg)).toThrowError(/Missing translation for message ref/);
});
it('should report invalid translated html', () => {
const msgMap = {
foo: [
new i18n.Text('text', null),
new i18n.Placeholder('', 'ph1', null),
]
};
const phMap = {
ph1: '</b>',
};
const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd');
expect(() => tb.get(msg)).toThrowError(/Unexpected closing tag "b"/);
});
});
});
}

View File

@ -22,8 +22,5 @@
"files": [
"index.ts",
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true
}
]
}

View File

@ -21,8 +21,6 @@ import {CompilerFactory, CompilerOptions} from './linker/compiler';
import {ComponentFactory, ComponentRef} from './linker/component_factory';
import {ComponentFactoryResolver} from './linker/component_factory_resolver';
import {NgModuleFactory, NgModuleInjector, NgModuleRef} from './linker/ng_module_factory';
import {AppView} from './linker/view';
import {ViewRef, ViewRef_} from './linker/view_ref';
import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile';
import {Testability, TestabilityRegistry} from './testability/testability';
import {Type} from './type';
@ -62,15 +60,6 @@ export function isDevMode(): boolean {
return _devMode;
}
/**
* A token for third-party components that can register themselves with NgProbe.
*
* @experimental
*/
export class NgProbeToken {
constructor(public name: string, public token: any) {}
}
/**
* Creates a platform.
* Platforms have to be eagerly created via this function.
@ -389,23 +378,6 @@ export abstract class ApplicationRef {
* Get a list of components registered to this application.
*/
get components(): ComponentRef<any>[] { return <ComponentRef<any>[]>unimplemented(); };
/**
* Attaches a view so that it will be dirty checked.
* The view will be automatically detached when it is destroyed.
* This will throw if the view is already attached to a ViewContainer.
*/
attachView(view: ViewRef): void { unimplemented(); }
/**
* Detaches a view from dirty checking again.
*/
detachView(view: ViewRef): void { unimplemented(); }
/**
* Returns the number of attached views.
*/
get viewCount() { return unimplemented(); }
}
@Injectable()
@ -416,7 +388,7 @@ export class ApplicationRef_ extends ApplicationRef {
private _bootstrapListeners: Function[] = [];
private _rootComponents: ComponentRef<any>[] = [];
private _rootComponentTypes: Type<any>[] = [];
private _views: AppView<any>[] = [];
private _changeDetectorRefs: ChangeDetectorRef[] = [];
private _runningTick: boolean = false;
private _enforceNoNewChanges: boolean = false;
@ -434,16 +406,12 @@ export class ApplicationRef_ extends ApplicationRef {
{next: () => { this._zone.run(() => { this.tick(); }); }});
}
attachView(viewRef: ViewRef): void {
const view = (viewRef as ViewRef_<any>).internalView;
this._views.push(view);
view.attachToAppRef(this);
registerChangeDetector(changeDetector: ChangeDetectorRef): void {
this._changeDetectorRefs.push(changeDetector);
}
detachView(viewRef: ViewRef): void {
const view = (viewRef as ViewRef_<any>).internalView;
ListWrapper.remove(this._views, view);
view.detach();
unregisterChangeDetector(changeDetector: ChangeDetectorRef): void {
ListWrapper.remove(this._changeDetectorRefs, changeDetector);
}
bootstrap<C>(componentOrFactory: ComponentFactory<C>|Type<C>): ComponentRef<C> {
@ -474,8 +442,9 @@ export class ApplicationRef_ extends ApplicationRef {
return compRef;
}
private _loadComponent(componentRef: ComponentRef<any>): void {
this.attachView(componentRef.hostView);
/** @internal */
_loadComponent(componentRef: ComponentRef<any>): void {
this._changeDetectorRefs.push(componentRef.changeDetectorRef);
this.tick();
this._rootComponents.push(componentRef);
// Get the listeners lazily to prevent DI cycles.
@ -485,8 +454,12 @@ export class ApplicationRef_ extends ApplicationRef {
listeners.forEach((listener) => listener(componentRef));
}
private _unloadComponent(componentRef: ComponentRef<any>): void {
this.detachView(componentRef.hostView);
/** @internal */
_unloadComponent(componentRef: ComponentRef<any>): void {
if (this._rootComponents.indexOf(componentRef) == -1) {
return;
}
this.unregisterChangeDetector(componentRef.changeDetectorRef);
ListWrapper.remove(this._rootComponents, componentRef);
}
@ -498,9 +471,9 @@ export class ApplicationRef_ extends ApplicationRef {
const scope = ApplicationRef_._tickScope();
try {
this._runningTick = true;
this._views.forEach((view) => view.ref.detectChanges());
this._changeDetectorRefs.forEach((detector) => detector.detectChanges());
if (this._enforceNoNewChanges) {
this._views.forEach((view) => view.ref.checkNoChanges());
this._changeDetectorRefs.forEach((detector) => detector.checkNoChanges());
}
} finally {
this._runningTick = false;
@ -510,11 +483,9 @@ export class ApplicationRef_ extends ApplicationRef {
ngOnDestroy() {
// TODO(alxhub): Dispose of the NgZone.
this._views.slice().forEach((view) => view.destroy());
this._rootComponents.slice().forEach((component) => component.destroy());
}
get viewCount() { return this._views.length; }
get componentTypes(): Type<any>[] { return this._rootComponentTypes; }
get components(): ComponentRef<any>[] { return this._rootComponents; }

View File

@ -14,7 +14,7 @@
export * from './metadata';
export * from './util';
export * from './di';
export {createPlatform, assertPlatform, destroyPlatform, getPlatform, PlatformRef, ApplicationRef, enableProdMode, isDevMode, createPlatformFactory, NgProbeToken} from './application_ref';
export {createPlatform, assertPlatform, destroyPlatform, getPlatform, PlatformRef, ApplicationRef, enableProdMode, isDevMode, createPlatformFactory} from './application_ref';
export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, APP_BOOTSTRAP_LISTENER} from './application_tokens';
export {APP_INITIALIZER, ApplicationInitStatus} from './application_init';
export * from './zone';

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ApplicationRef} from '../application_ref';
import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection';
import {Injector, THROW_IF_NOT_FOUND} from '../di/injector';
import {ListWrapper} from '../facade/collection';
@ -42,10 +41,7 @@ export abstract class AppView<T> {
lastRootNode: any;
allNodes: any[];
disposables: Function[];
viewContainer: ViewContainer;
// This will be set if a view is directly attached to an ApplicationRef
// and not to a view container.
appRef: ApplicationRef;
viewContainer: ViewContainer = null;
numberOfChecks: number = 0;
@ -142,12 +138,10 @@ export abstract class AppView<T> {
injector(nodeIndex: number): Injector { return new ElementInjector(this, nodeIndex); }
detachAndDestroy() {
if (this.viewContainer) {
this.viewContainer.detachView(this.viewContainer.nestedViews.indexOf(this));
} else if (this.appRef) {
this.appRef.detachView(this.ref);
} else if (this._hasExternalHostElement) {
if (this._hasExternalHostElement) {
this.detach();
} else if (isPresent(this.viewContainer)) {
this.viewContainer.detachView(this.viewContainer.nestedViews.indexOf(this));
}
this.destroy();
}
@ -202,7 +196,6 @@ export abstract class AppView<T> {
projectedViews.splice(index, 1);
}
}
this.appRef = null;
this.viewContainer = null;
this.dirtyParentQueriesInternal();
}
@ -215,18 +208,7 @@ export abstract class AppView<T> {
}
}
attachToAppRef(appRef: ApplicationRef) {
if (this.viewContainer) {
throw new Error('This view is already attached to a ViewContainer!');
}
this.appRef = appRef;
this.dirtyParentQueriesInternal();
}
attachAfter(viewContainer: ViewContainer, prevView: AppView<any>) {
if (this.appRef) {
throw new Error('This view is already attached directly to the ApplicationRef!');
}
this._renderAttach(viewContainer, prevView);
this.viewContainer = viewContainer;
if (this.declaredViewContainer && this.declaredViewContainer !== viewContainer) {

View File

@ -17,7 +17,7 @@ import {AppView} from './view';
/**
* @stable
*/
export abstract class ViewRef extends ChangeDetectorRef {
export abstract class ViewRef {
get destroyed(): boolean { return <boolean>unimplemented(); }
abstract onDestroy(callback: Function): any /** TODO #9100 */;

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, PlatformRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, PlatformRef, Type} from '@angular/core';
import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref';
import {ErrorHandler} from '@angular/core/src/error_handler';
import {ComponentRef} from '@angular/core/src/linker/component_factory';
@ -16,7 +16,9 @@ import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
import {expect} from '@angular/platform-browser/testing/matchers';
import {ServerModule} from '@angular/platform-server';
import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';
import {TestBed, async, inject, withModule} from '../testing';
import {SpyChangeDetectorRef} from './spies';
@Component({selector: 'comp', template: 'hello'})
class SomeComponent {
@ -72,16 +74,13 @@ export function main() {
beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); });
it('should throw when reentering tick', inject([ApplicationRef], (ref: ApplicationRef_) => {
const view = jasmine.createSpyObj('view', ['detach', 'attachToAppRef']);
const viewRef = jasmine.createSpyObj('viewRef', ['detectChanges']);
viewRef.internalView = view;
view.ref = viewRef;
const cdRef = <any>new SpyChangeDetectorRef();
try {
ref.attachView(viewRef);
viewRef.detectChanges.and.callFake(() => ref.tick());
ref.registerChangeDetector(cdRef);
cdRef.spy('detectChanges').and.callFake(() => ref.tick());
expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively');
} finally {
ref.detachView(viewRef);
ref.unregisterChangeDetector(cdRef);
}
}));
@ -262,84 +261,6 @@ export function main() {
});
}));
});
describe('attachView / detachView', () => {
@Component({template: '{{name}}'})
class MyComp {
name = 'Initial';
}
@Component({template: '<ng-container #vc></ng-container>'})
class ContainerComp {
@ViewChild('vc', {read: ViewContainerRef})
vc: ViewContainerRef;
}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComp, ContainerComp],
providers: [{provide: ComponentFixtureNoNgZone, useValue: true}]
});
});
it('should dirty check attached views', () => {
const comp = TestBed.createComponent(MyComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
expect(appRef.viewCount).toBe(0);
appRef.tick();
expect(comp.nativeElement).toHaveText('');
appRef.attachView(comp.componentRef.hostView);
appRef.tick();
expect(appRef.viewCount).toBe(1);
expect(comp.nativeElement).toHaveText('Initial');
});
it('should not dirty check detached views', () => {
const comp = TestBed.createComponent(MyComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
appRef.attachView(comp.componentRef.hostView);
appRef.tick();
expect(comp.nativeElement).toHaveText('Initial');
appRef.detachView(comp.componentRef.hostView);
comp.componentInstance.name = 'New';
appRef.tick();
expect(appRef.viewCount).toBe(0);
expect(comp.nativeElement).toHaveText('Initial');
});
it('should detach attached views if they are destroyed', () => {
const comp = TestBed.createComponent(MyComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
appRef.attachView(comp.componentRef.hostView);
comp.destroy();
expect(appRef.viewCount).toBe(0);
});
it('should not allow to attach a view to both, a view container and the ApplicationRef',
() => {
const comp = TestBed.createComponent(MyComp);
const hostView = comp.componentRef.hostView;
const containerComp = TestBed.createComponent(ContainerComp);
containerComp.detectChanges();
const vc = containerComp.componentInstance.vc;
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
vc.insert(hostView);
expect(() => appRef.attachView(hostView))
.toThrowError('This view is already attached to a ViewContainer!');
vc.detach(0);
appRef.attachView(hostView);
expect(() => vc.insert(hostView))
.toThrowError('This view is already attached directly to the ApplicationRef!');
});
});
});
}

View File

@ -23,7 +23,6 @@
"../../system.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -27,7 +27,6 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -24,7 +24,6 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -1,22 +0,0 @@
/**
* @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
* Entry point for all public APIs of the language service package.
*/
import * as ts from 'typescript';
import {LanguageServicePlugin} from './src/ts_plugin';
export {createLanguageService} from './src/language_service';
export {Completion, Completions, Declaration, Declarations, Definition, Diagnostic, Diagnostics, Hover, HoverTextSection, LanguageService, LanguageServiceHost, Location, Span, TemplateSource, TemplateSources} from './src/types';
export {TypeScriptServiceHost, createLanguageServiceFromTypescript} from './src/typescript_host';
export default LanguageServicePlugin;

View File

@ -1,14 +0,0 @@
{
"name": "@angular/language-service",
"version": "0.0.0-PLACEHOLDER",
"description": "Angular 2 - language services",
"main": "bundles/language-service.umd.js",
"module": "index.js",
"typings": "index.d.ts",
"author": "angular",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/angular/angular.git"
}
}

View File

@ -1,81 +0,0 @@
/**
* @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
*/
import commonjs from 'rollup-plugin-commonjs';
import * as path from 'path';
var m = /^\@angular\/((\w|\-)+)(\/(\w|\d|\/|\-)+)?$/;
var location = normalize('../../../dist/packages-dist') + '/';
var rxjsLocation = normalize('../../../node_modules/rxjs');
var esm = 'esm/';
var locations = {
'tsc-wrapped': normalize('../../../dist/tools/@angular') + '/',
};
var esm_suffixes = {};
function normalize(fileName) {
return path.resolve(__dirname, fileName);
}
function resolve(id, from) {
// console.log('Resolve id:', id, 'from', from)
if (id == '@angular/tsc-wrapped') {
// Hack to restrict the import to not include the index of @angular/tsc-wrapped so we don't
// rollup tsickle.
return locations['tsc-wrapped'] + 'tsc-wrapped/src/collector.js';
}
var match = m.exec(id);
if (match) {
var packageName = match[1];
var esm_suffix = esm_suffixes[packageName] || '';
var loc = locations[packageName] || location;
var r = loc + esm_suffix + packageName + (match[3] || '/index') + '.js';
// console.log('** ANGULAR MAPPED **: ', r);
return r;
}
if (id && id.startsWith('rxjs/')) {
const resolved = `${rxjsLocation}${id.replace('rxjs', '')}.js`;
return resolved;
}
}
var banner = `
var $deferred, $resolved, $provided;
function $getModule(name) { return $provided[name] || require(name); }
function define(modules, cb) { $deferred = { modules: modules, cb: cb }; }
module.exports = function(provided) {
if ($resolved) return $resolved;
var result = {};
$provided = Object.assign({}, provided || {}, { exports: result });
$deferred.cb.apply(this, $deferred.modules.map($getModule));
$resolved = result;
return result;
}
`;
export default {
entry: '../../../dist/packages-dist/language-service/index.js',
dest: '../../../dist/packages-dist/language-service/bundles/language-service.umd.js',
format: 'amd',
moduleName: 'ng.language_service',
exports: 'named',
external: [
'fs',
'path',
'typescript',
],
globals: {
'typescript': 'ts',
'path': 'path',
'fs': 'fs',
},
banner: banner,
plugins: [{resolveId: resolve}, commonjs()]
}

View File

@ -1,29 +0,0 @@
/**
* @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 class AstPath<T> {
constructor(private path: T[]) {}
get empty(): boolean { return !this.path || !this.path.length; }
get head(): T|undefined { return this.path[0]; }
get tail(): T|undefined { return this.path[this.path.length - 1]; }
parentOf(node: T): T|undefined { return this.path[this.path.indexOf(node) - 1]; }
childOf(node: T): T|undefined { return this.path[this.path.indexOf(node) + 1]; }
first<N extends T>(ctor: {new (...args: any[]): N}): N|undefined {
for (let i = this.path.length - 1; i >= 0; i--) {
let item = this.path[i];
if (item instanceof ctor) return <N>item;
}
}
push(node: T) { this.path.push(node); }
pop(): T { return this.path.pop(); }
}

View File

@ -1,53 +0,0 @@
/**
* @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
*/
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary} from '@angular/compiler';
import {Parser} from '@angular/compiler/src/expression_parser/parser';
import {Node as HtmlAst} from '@angular/compiler/src/ml_parser/ast';
import {ParseError} from '@angular/compiler/src/parse_util';
import {CssSelector} from '@angular/compiler/src/selector';
import {TemplateAst} from '@angular/compiler/src/template_parser/template_ast';
import {Diagnostic, TemplateSource} from './types';
export interface AstResult {
htmlAst?: HtmlAst[];
templateAst?: TemplateAst[];
directive?: CompileDirectiveMetadata;
directives?: CompileDirectiveSummary[];
pipes?: CompilePipeSummary[];
parseErrors?: ParseError[];
expressionParser?: Parser;
errors?: Diagnostic[];
}
export interface TemplateInfo {
position?: number;
fileName?: string;
template: TemplateSource;
htmlAst: HtmlAst[];
directive: CompileDirectiveMetadata;
directives: CompileDirectiveSummary[];
pipes: CompilePipeSummary[];
templateAst: TemplateAst[];
expressionParser: Parser;
}
export interface AttrInfo {
name: string;
input?: boolean;
output?: boolean;
template?: boolean;
fromHtml?: boolean;
}
export type SelectorInfo = {
selectors: CssSelector[],
map: Map<CssSelector, CompileDirectiveSummary>
};

View File

@ -1,495 +0,0 @@
/**
* @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
*/
import {AST, ImplicitReceiver, ParseSpan, PropertyRead} from '@angular/compiler/src/expression_parser/ast';
import {Attribute, Element, Node as HtmlAst, Text} from '@angular/compiler/src/ml_parser/ast';
import {getHtmlTagDefinition} from '@angular/compiler/src/ml_parser/html_tags';
import {NAMED_ENTITIES, TagContentType, splitNsName} from '@angular/compiler/src/ml_parser/tags';
import {CssSelector, SelectorMatcher} from '@angular/compiler/src/selector';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast';
import {AstResult, AttrInfo, SelectorInfo, TemplateInfo} from './common';
import {getExpressionCompletions, getExpressionScope} from './expressions';
import {attributeNames, elementNames, eventNames, propertyNames} from './html_info';
import {HtmlAstPath} from './html_path';
import {NullTemplateVisitor, TemplateAstChildVisitor, TemplateAstPath} from './template_path';
import {BuiltinType, Completion, Completions, Span, Symbol, SymbolDeclaration, SymbolTable, TemplateSource} from './types';
import {flatten, getSelectors, hasTemplateReference, inSpan, removeSuffix, spanOf, uniqueByName} from './utils';
const TEMPLATE_ATTR_PREFIX = '*';
const hiddenHtmlElements = {
html: true,
script: true,
noscript: true,
base: true,
body: true,
title: true,
head: true,
link: true,
};
export function getTemplateCompletions(templateInfo: TemplateInfo): Completions {
let result: Completions = undefined;
let {htmlAst, templateAst, template} = templateInfo;
// The templateNode starts at the delimiter character so we add 1 to skip it.
let templatePosition = templateInfo.position - template.span.start;
let path = new HtmlAstPath(htmlAst, templatePosition);
let mostSpecific = path.tail;
if (path.empty) {
result = elementCompletions(templateInfo, path);
} else {
let astPosition = templatePosition - mostSpecific.sourceSpan.start.offset;
mostSpecific.visit(
{
visitElement(ast) {
let startTagSpan = spanOf(ast.sourceSpan);
let tagLen = ast.name.length;
if (templatePosition <=
startTagSpan.start + tagLen + 1 /* 1 for the opening angle bracked */) {
// If we are in the tag then return the element completions.
result = elementCompletions(templateInfo, path);
} else if (templatePosition < startTagSpan.end) {
// We are in the attribute section of the element (but not in an attribute).
// Return the attribute completions.
result = attributeCompletions(templateInfo, path);
}
},
visitAttribute(ast) {
if (!ast.valueSpan || !inSpan(templatePosition, spanOf(ast.valueSpan))) {
// We are in the name of an attribute. Show attribute completions.
result = attributeCompletions(templateInfo, path);
} else if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
result = attributeValueCompletions(templateInfo, templatePosition, ast);
}
},
visitText(ast) {
// Check if we are in a entity.
result = entityCompletions(getSourceText(template, spanOf(ast)), astPosition);
if (result) return result;
result = interpolationCompletions(templateInfo, templatePosition);
if (result) return result;
let element = path.first(Element);
if (element) {
let definition = getHtmlTagDefinition(element.name);
if (definition.contentType === TagContentType.PARSABLE_DATA) {
result = voidElementAttributeCompletions(templateInfo, path);
if (!result) {
// If the element can hold content Show element completions.
result = elementCompletions(templateInfo, path);
}
}
} else {
// If no element container, implies parsable data so show elements.
result = voidElementAttributeCompletions(templateInfo, path);
if (!result) {
result = elementCompletions(templateInfo, path);
}
}
},
visitComment(ast) {},
visitExpansion(ast) {},
visitExpansionCase(ast) {}
},
null);
}
return result;
}
function attributeCompletions(info: TemplateInfo, path: HtmlAstPath): Completions {
let item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail);
if (item instanceof Element) {
return attributeCompletionsForElement(info, item.name, item);
}
return undefined;
}
function attributeCompletionsForElement(
info: TemplateInfo, elementName: string, element?: Element): Completions {
const attributes = getAttributeInfosForElement(info, elementName, element);
// Map all the attributes to a completion
return attributes.map<Completion>(attr => ({
kind: attr.fromHtml ? 'html attribute' : 'attribute',
name: nameOfAttr(attr),
sort: attr.name
}));
}
function getAttributeInfosForElement(
info: TemplateInfo, elementName: string, element?: Element): AttrInfo[] {
let attributes: AttrInfo[] = [];
// Add html attributes
let htmlAttributes = attributeNames(elementName) || [];
if (htmlAttributes) {
attributes.push(...htmlAttributes.map<AttrInfo>(name => ({name, fromHtml: true})));
}
// Add html properties
let htmlProperties = propertyNames(elementName);
if (htmlProperties) {
attributes.push(...htmlProperties.map<AttrInfo>(name => ({name, input: true})));
}
// Add html events
let htmlEvents = eventNames(elementName);
if (htmlEvents) {
attributes.push(...htmlEvents.map<AttrInfo>(name => ({name, output: true})));
}
let {selectors, map: selectorMap} = getSelectors(info);
if (selectors && selectors.length) {
// All the attributes that are selectable should be shown.
const applicableSelectors =
selectors.filter(selector => !selector.element || selector.element == elementName);
const selectorAndAttributeNames =
applicableSelectors.map(selector => ({selector, attrs: selector.attrs.filter(a => !!a)}));
let attrs = flatten(selectorAndAttributeNames.map<AttrInfo[]>(selectorAndAttr => {
const directive = selectorMap.get(selectorAndAttr.selector);
const result = selectorAndAttr.attrs.map<AttrInfo>(
name => ({name, input: name in directive.inputs, output: name in directive.outputs}));
return result;
}));
// Add template attribute if a directive contains a template reference
selectorAndAttributeNames.forEach(selectorAndAttr => {
const selector = selectorAndAttr.selector;
const directive = selectorMap.get(selector);
if (directive && hasTemplateReference(directive.type) && selector.attrs.length &&
selector.attrs[0]) {
attrs.push({name: selector.attrs[0], template: true});
}
});
// All input and output properties of the matching directives should be added.
let elementSelector = element ?
createElementCssSelector(element) :
createElementCssSelector(new Element(elementName, [], [], undefined, undefined, undefined));
let matcher = new SelectorMatcher();
matcher.addSelectables(selectors);
matcher.match(elementSelector, selector => {
let directive = selectorMap.get(selector);
if (directive) {
attrs.push(...Object.keys(directive.inputs).map(name => ({name, input: true})));
attrs.push(...Object.keys(directive.outputs).map(name => ({name, output: true})));
}
});
// If a name shows up twice, fold it into a single value.
attrs = foldAttrs(attrs);
// Now expand them back out to ensure that input/output shows up as well as input and
// output.
attributes.push(...flatten(attrs.map(expandedAttr)));
}
return attributes;
}
function attributeValueCompletions(
info: TemplateInfo, position: number, attr: Attribute): Completions {
const path = new TemplateAstPath(info.templateAst, position);
const mostSpecific = path.tail;
if (mostSpecific) {
const visitor =
new ExpressionVisitor(info, position, attr, () => getExpressionScope(info, path, false));
mostSpecific.visit(visitor, null);
if (!visitor.result || !visitor.result.length) {
// Try allwoing widening the path
const widerPath = new TemplateAstPath(info.templateAst, position, /* allowWidening */ true);
if (widerPath.tail) {
const widerVisitor = new ExpressionVisitor(
info, position, attr, () => getExpressionScope(info, widerPath, false));
widerPath.tail.visit(widerVisitor, null);
return widerVisitor.result;
}
}
return visitor.result;
}
}
function elementCompletions(info: TemplateInfo, path: HtmlAstPath): Completions {
let htmlNames = elementNames().filter(name => !(name in hiddenHtmlElements));
// Collect the elements referenced by the selectors
let directiveElements =
getSelectors(info).selectors.map(selector => selector.element).filter(name => !!name);
let components =
directiveElements.map<Completion>(name => ({kind: 'component', name: name, sort: name}));
let htmlElements = htmlNames.map<Completion>(name => ({kind: 'element', name: name, sort: name}));
// Return components and html elements
return uniqueByName(htmlElements.concat(components));
}
function entityCompletions(value: string, position: number): Completions {
// Look for entity completions
const re = /&[A-Za-z]*;?(?!\d)/g;
let found: RegExpExecArray|null;
let result: Completions;
while (found = re.exec(value)) {
let len = found[0].length;
if (position >= found.index && position < (found.index + len)) {
result = Object.keys(NAMED_ENTITIES)
.map<Completion>(name => ({kind: 'entity', name: `&${name};`, sort: name}));
break;
}
}
return result;
}
function interpolationCompletions(info: TemplateInfo, position: number): Completions {
// Look for an interpolation in at the position.
const templatePath = new TemplateAstPath(info.templateAst, position);
const mostSpecific = templatePath.tail;
if (mostSpecific) {
let visitor = new ExpressionVisitor(
info, position, undefined, () => getExpressionScope(info, templatePath, false));
mostSpecific.visit(visitor, null);
return uniqueByName(visitor.result);
}
}
// There is a special case of HTML where text that contains a unclosed tag is treated as
// text. For exaple '<h1> Some <a text </h1>' produces a text nodes inside of the H1
// element "Some <a text". We, however, want to treat this as if the user was requesting
// the attributes of an "a" element, not requesting completion in the a text element. This
// code checks for this case and returns element completions if it is detected or undefined
// if it is not.
function voidElementAttributeCompletions(info: TemplateInfo, path: HtmlAstPath): Completions {
let tail = path.tail;
if (tail instanceof Text) {
let match = tail.value.match(/<(\w(\w|\d|-)*:)?(\w(\w|\d|-)*)\s/);
// The position must be after the match, otherwise we are still in a place where elements
// are expected (such as `<|a` or `<a|`; we only want attributes for `<a |` or after).
if (match && path.position >= match.index + match[0].length + tail.sourceSpan.start.offset) {
return attributeCompletionsForElement(info, match[3]);
}
}
}
class ExpressionVisitor extends NullTemplateVisitor {
result: Completions;
constructor(
private info: TemplateInfo, private position: number, private attr?: Attribute,
private getExpressionScope?: () => SymbolTable) {
super();
if (!getExpressionScope) {
this.getExpressionScope = () => info.template.members;
}
}
visitDirectiveProperty(ast: BoundDirectivePropertyAst): void {
this.attributeValueCompletions(ast.value);
}
visitElementProperty(ast: BoundElementPropertyAst): void {
this.attributeValueCompletions(ast.value);
}
visitEvent(ast: BoundEventAst): void { this.attributeValueCompletions(ast.handler); }
visitElement(ast: ElementAst): void {
if (this.attr && getSelectors(this.info) && this.attr.name.startsWith(TEMPLATE_ATTR_PREFIX)) {
// The value is a template expression but the expression AST was not produced when the
// TemplateAst was produce so
// do that now.
const key = this.attr.name.substr(TEMPLATE_ATTR_PREFIX.length);
// Find the selector
const selectorInfo = getSelectors(this.info);
const selectors = selectorInfo.selectors;
const selector =
selectors.filter(s => s.attrs.some((attr, i) => i % 2 == 0 && attr == key))[0];
const templateBindingResult =
this.info.expressionParser.parseTemplateBindings(key, this.attr.value, null);
// find the template binding that contains the position
const valueRelativePosition = this.position - this.attr.valueSpan.start.offset - 1;
const bindings = templateBindingResult.templateBindings;
const binding =
bindings.find(
binding => inSpan(valueRelativePosition, binding.span, /* exclusive */ true)) ||
bindings.find(binding => inSpan(valueRelativePosition, binding.span));
const keyCompletions = () => {
let keys: string[] = [];
if (selector) {
const attrNames = selector.attrs.filter((_, i) => i % 2 == 0);
keys = attrNames.filter(name => name.startsWith(key) && name != key)
.map(name => lowerName(name.substr(key.length)));
}
keys.push('let');
this.result = keys.map(key => <Completion>{kind: 'key', name: key, sort: key});
};
if (!binding || (binding.key == key && !binding.expression)) {
// We are in the root binding. We should return `let` and keys that are left in the
// selector.
keyCompletions();
} else if (binding.keyIsVar) {
const equalLocation = this.attr.value.indexOf('=');
this.result = [];
if (equalLocation >= 0 && valueRelativePosition >= equalLocation) {
// We are after the '=' in a let clause. The valid values here are the members of the
// template reference's type parameter.
const directiveMetadata = selectorInfo.map.get(selector);
const contextTable =
this.info.template.query.getTemplateContext(directiveMetadata.type.reference);
if (contextTable) {
this.result = this.symbolsToCompletions(contextTable.values());
}
} else if (binding.key && valueRelativePosition <= (binding.key.length - key.length)) {
keyCompletions();
}
} else {
// If the position is in the expression or after the key or there is no key, return the
// expression completions
if ((binding.expression && inSpan(valueRelativePosition, binding.expression.ast.span)) ||
(binding.key &&
valueRelativePosition > binding.span.start + (binding.key.length - key.length)) ||
!binding.key) {
const span = new ParseSpan(0, this.attr.value.length);
this.attributeValueCompletions(
binding.expression ? binding.expression.ast :
new PropertyRead(span, new ImplicitReceiver(span), ''),
valueRelativePosition);
} else {
keyCompletions();
}
}
}
}
visitBoundText(ast: BoundTextAst) {
const expressionPosition = this.position - ast.sourceSpan.start.offset;
if (inSpan(expressionPosition, ast.value.span)) {
const completions = getExpressionCompletions(
this.getExpressionScope(), ast.value, expressionPosition, this.info.template.query);
if (completions) {
this.result = this.symbolsToCompletions(completions);
}
}
}
private attributeValueCompletions(value: AST, position?: number) {
const symbols = getExpressionCompletions(
this.getExpressionScope(), value, position == null ? this.attributeValuePosition : position,
this.info.template.query);
if (symbols) {
this.result = this.symbolsToCompletions(symbols);
}
}
private symbolsToCompletions(symbols: Symbol[]): Completions {
return symbols.filter(s => !s.name.startsWith('__') && s.public)
.map(symbol => <Completion>{kind: symbol.kind, name: symbol.name, sort: symbol.name});
}
private get attributeValuePosition() {
return this.position - this.attr.valueSpan.start.offset - 1;
}
}
function getSourceText(template: TemplateSource, span: Span): string {
return template.source.substring(span.start, span.end);
}
function nameOfAttr(attr: AttrInfo): string {
let name = attr.name;
if (attr.output) {
name = removeSuffix(name, 'Events');
name = removeSuffix(name, 'Changed');
}
let result = [name];
if (attr.input) {
result.unshift('[');
result.push(']');
}
if (attr.output) {
result.unshift('(');
result.push(')');
}
if (attr.template) {
result.unshift('*');
}
return result.join('');
}
const templateAttr = /^(\w+:)?(template$|^\*)/;
function createElementCssSelector(element: Element): CssSelector {
const cssSelector = new CssSelector();
let elNameNoNs = splitNsName(element.name)[1];
cssSelector.setElement(elNameNoNs);
for (let attr of element.attrs) {
if (!attr.name.match(templateAttr)) {
let [_, attrNameNoNs] = splitNsName(attr.name);
cssSelector.addAttribute(attrNameNoNs, attr.value);
if (attr.name.toLowerCase() == 'class') {
const classes = attr.value.split(/s+/g);
classes.forEach(className => cssSelector.addClassName(className));
}
}
}
return cssSelector;
}
function foldAttrs(attrs: AttrInfo[]): AttrInfo[] {
let inputOutput = new Map<string, AttrInfo>();
let templates = new Map<string, AttrInfo>();
let result: AttrInfo[] = [];
attrs.forEach(attr => {
if (attr.fromHtml) {
return attr;
}
if (attr.template) {
let duplicate = templates.get(attr.name);
if (!duplicate) {
result.push({name: attr.name, template: true});
templates.set(attr.name, attr);
}
}
if (attr.input || attr.output) {
let duplicate = inputOutput.get(attr.name);
if (duplicate) {
duplicate.input = duplicate.input || attr.input;
duplicate.output = duplicate.output || attr.output;
} else {
let cloneAttr: AttrInfo = {name: attr.name};
if (attr.input) cloneAttr.input = true;
if (attr.output) cloneAttr.output = true;
result.push(cloneAttr);
inputOutput.set(attr.name, cloneAttr);
}
}
});
return result;
}
function expandedAttr(attr: AttrInfo): AttrInfo[] {
if (attr.input && attr.output) {
return [
attr, {name: attr.name, input: true, output: false},
{name: attr.name, input: false, output: true}
];
}
return [attr];
}
function lowerName(name: string): string {
return name && (name[0].toLowerCase() + name.substr(1));
}

View File

@ -1,16 +0,0 @@
/**
* @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
*/
import {TemplateInfo} from './common';
import {locateSymbol} from './locate_symbol';
import {Definition} from './types';
export function getDefinition(info: TemplateInfo): Definition {
const result = locateSymbol(info);
return result && result.symbol.definition;
}

View File

@ -1,250 +0,0 @@
/**
* @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
*/
import {CompileDirectiveMetadata, CompileDirectiveSummary, StaticSymbol} from '@angular/compiler';
import {NgAnalyzedModules} from '@angular/compiler/src/aot/compiler';
import {AST} from '@angular/compiler/src/expression_parser/ast';
import {Attribute} from '@angular/compiler/src/ml_parser/ast';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast';
import {AstResult, SelectorInfo, TemplateInfo} from './common';
import {getExpressionDiagnostics, getExpressionScope} from './expressions';
import {HtmlAstPath} from './html_path';
import {NullTemplateVisitor, TemplateAstChildVisitor, TemplateAstPath} from './template_path';
import {Declaration, Declarations, Diagnostic, DiagnosticKind, Diagnostics, Span, SymbolTable, TemplateSource} from './types';
import {getSelectors, hasTemplateReference, offsetSpan, spanOf} from './utils';
export interface AstProvider {
getTemplateAst(template: TemplateSource, fileName: string): AstResult;
}
export function getTemplateDiagnostics(
fileName: string, astProvider: AstProvider, templates: TemplateSource[]): Diagnostics {
const results: Diagnostics = [];
for (const template of templates) {
const ast = astProvider.getTemplateAst(template, fileName);
if (ast) {
if (ast.parseErrors && ast.parseErrors.length) {
results.push(...ast.parseErrors.map<Diagnostic>(
e => ({
kind: DiagnosticKind.Error,
span: offsetSpan(spanOf(e.span), template.span.start),
message: e.msg
})));
} else if (ast.templateAst) {
const expressionDiagnostics = getTemplateExpressionDiagnostics(template, ast);
results.push(...expressionDiagnostics);
}
if (ast.errors) {
results.push(...ast.errors.map<Diagnostic>(
e => ({kind: e.kind, span: e.span || template.span, message: e.message})));
}
}
}
return results;
}
export function getDeclarationDiagnostics(
declarations: Declarations, modules: NgAnalyzedModules): Diagnostics {
const results: Diagnostics = [];
let directives: Set<StaticSymbol>|undefined = undefined;
for (const declaration of declarations) {
let report = (message: string) => {
results.push(
<Diagnostic>{kind: DiagnosticKind.Error, span: declaration.declarationSpan, message});
};
if (declaration.error) {
report(declaration.error);
}
if (declaration.metadata) {
if (declaration.metadata.isComponent) {
if (!modules.ngModuleByPipeOrDirective.has(declaration.type)) {
report(
`Component '${declaration.type.name}' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration`);
}
if (declaration.metadata.template.template == null &&
!declaration.metadata.template.templateUrl) {
report(`Component ${declaration.type.name} must have a template or templateUrl`);
}
} else {
if (!directives) {
directives = new Set();
modules.ngModules.forEach(module => {
module.declaredDirectives.forEach(
directive => { directives.add(directive.reference); });
});
}
if (!directives.has(declaration.type)) {
report(
`Directive '${declaration.type.name}' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration`);
}
}
}
}
return results;
}
function getTemplateExpressionDiagnostics(
template: TemplateSource, astResult: AstResult): Diagnostics {
const info: TemplateInfo = {
template,
htmlAst: astResult.htmlAst,
directive: astResult.directive,
directives: astResult.directives,
pipes: astResult.pipes,
templateAst: astResult.templateAst,
expressionParser: astResult.expressionParser
};
const visitor = new ExpressionDiagnosticsVisitor(
info, (path: TemplateAstPath, includeEvent: boolean) =>
getExpressionScope(info, path, includeEvent));
templateVisitAll(visitor, astResult.templateAst);
return visitor.diagnostics;
}
class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor {
private path: TemplateAstPath;
private directiveSummary: CompileDirectiveSummary;
diagnostics: Diagnostics = [];
constructor(
private info: TemplateInfo,
private getExpressionScope: (path: TemplateAstPath, includeEvent: boolean) => SymbolTable) {
super();
this.path = new TemplateAstPath([], 0);
}
visitDirective(ast: DirectiveAst, context: any): any {
// Override the default child visitor to ignore the host properties of a directive.
if (ast.inputs && ast.inputs.length) {
templateVisitAll(this, ast.inputs, context);
}
}
visitBoundText(ast: BoundTextAst): void {
this.push(ast);
this.diagnoseExpression(ast.value, ast.sourceSpan.start.offset, false);
this.pop();
}
visitDirectiveProperty(ast: BoundDirectivePropertyAst): void {
this.push(ast);
this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false);
this.pop();
}
visitElementProperty(ast: BoundElementPropertyAst): void {
this.push(ast);
this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false);
this.pop();
}
visitEvent(ast: BoundEventAst): void {
this.push(ast);
this.diagnoseExpression(ast.handler, this.attributeValueLocation(ast), true);
this.pop();
}
visitVariable(ast: VariableAst): void {
const directive = this.directiveSummary;
if (directive && ast.value) {
const context = this.info.template.query.getTemplateContext(directive.type.reference);
if (!context.has(ast.value)) {
if (ast.value === '$implicit') {
this.reportError(
'The template context does not have an implicit value', spanOf(ast.sourceSpan));
} else {
this.reportError(
`The template context does not defined a member called '${ast.value}'`,
spanOf(ast.sourceSpan));
}
}
}
}
visitElement(ast: ElementAst, context: any): void {
this.push(ast);
super.visitElement(ast, context);
this.pop();
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
const previousDirectiveSummary = this.directiveSummary;
this.push(ast);
// Find directive that refernces this template
this.directiveSummary =
ast.directives.map(d => d.directive).find(d => hasTemplateReference(d.type));
// Process children
super.visitEmbeddedTemplate(ast, context);
this.pop();
this.directiveSummary = previousDirectiveSummary;
}
private attributeValueLocation(ast: TemplateAst) {
const path = new HtmlAstPath(this.info.htmlAst, ast.sourceSpan.start.offset);
const last = path.tail;
if (last instanceof Attribute && last.valueSpan) {
// Add 1 for the quote.
return last.valueSpan.start.offset + 1;
}
return ast.sourceSpan.start.offset;
}
private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) {
const scope = this.getExpressionScope(this.path, includeEvent);
this.diagnostics.push(
...getExpressionDiagnostics(scope, ast, this.info.template.query)
.map(d => ({
span: offsetSpan(d.ast.span, offset + this.info.template.span.start),
kind: d.kind,
message: d.message
})));
}
private push(ast: TemplateAst) { this.path.push(ast); }
private pop() { this.path.pop(); }
private _selectors: SelectorInfo;
private selectors(): SelectorInfo {
let result = this._selectors;
if (!result) {
this._selectors = result = getSelectors(this.info);
}
return result;
}
private findElement(position: number): Element {
const htmlPath = new HtmlAstPath(this.info.htmlAst, position);
if (htmlPath.tail instanceof Element) {
return htmlPath.tail;
}
}
private reportError(message: string, span: Span) {
this.diagnostics.push({
span: offsetSpan(span, this.info.template.span.start),
kind: DiagnosticKind.Error, message
});
}
private reportWarning(message: string, span: Span) {
this.diagnostics.push({
span: offsetSpan(span, this.info.template.span.start),
kind: DiagnosticKind.Warning, message
});
}
}

View File

@ -1,770 +0,0 @@
/**
* @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
*/
import {StaticSymbol} from '@angular/compiler';
import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler/src/expression_parser/ast';
import {ElementAst, EmbeddedTemplateAst, ReferenceAst, TemplateAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast';
import {AstPath as AstPathBase} from './ast_path';
import {TemplateInfo} from './common';
import {TemplateAstChildVisitor, TemplateAstPath} from './template_path';
import {BuiltinType, CompletionKind, Definition, DiagnosticKind, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './types';
import {inSpan, spanOf} from './utils';
export function getExpressionDiagnostics(
scope: SymbolTable, ast: AST, query: SymbolQuery): TypeDiagnostic[] {
const analyzer = new AstType(scope, query);
analyzer.getDiagnostics(ast);
return analyzer.diagnostics;
}
export function getExpressionCompletions(
scope: SymbolTable, ast: AST, position: number, query: SymbolQuery): Symbol[] {
const path = new AstPath(ast, position);
if (path.empty) return undefined;
const tail = path.tail;
let result: SymbolTable|undefined = scope;
function getType(ast: AST): Symbol { return new AstType(scope, query).getType(ast); }
// If the completion request is in a not in a pipe or property access then the global scope
// (that is the scope of the implicit receiver) is the right scope as the user is typing the
// beginning of an expression.
tail.visit({
visitBinary(ast) {},
visitChain(ast) {},
visitConditional(ast) {},
visitFunctionCall(ast) {},
visitImplicitReceiver(ast) {},
visitInterpolation(ast) { result = undefined; },
visitKeyedRead(ast) {},
visitKeyedWrite(ast) {},
visitLiteralArray(ast) {},
visitLiteralMap(ast) {},
visitLiteralPrimitive(ast) {},
visitMethodCall(ast) {},
visitPipe(ast) {
if (position >= ast.exp.span.end &&
(!ast.args || !ast.args.length || position < (<AST>ast.args[0]).span.start)) {
// We are in a position a pipe name is expected.
result = query.getPipes();
}
},
visitPrefixNot(ast) {},
visitPropertyRead(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
visitPropertyWrite(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
visitQuote(ast) {
// For a quote, return the members of any (if there are any).
result = query.getBuiltinType(BuiltinType.Any).members();
},
visitSafeMethodCall(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
visitSafePropertyRead(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
});
return result && result.values();
}
export function getExpressionSymbol(
scope: SymbolTable, ast: AST, position: number,
query: SymbolQuery): {symbol: Symbol, span: Span} {
const path = new AstPath(ast, position, /* excludeEmpty */ true);
if (path.empty) return undefined;
const tail = path.tail;
function getType(ast: AST): Symbol { return new AstType(scope, query).getType(ast); }
let symbol: Symbol = undefined;
let span: Span = undefined;
// If the completion request is in a not in a pipe or property access then the global scope
// (that is the scope of the implicit receiver) is the right scope as the user is typing the
// beginning of an expression.
tail.visit({
visitBinary(ast) {},
visitChain(ast) {},
visitConditional(ast) {},
visitFunctionCall(ast) {},
visitImplicitReceiver(ast) {},
visitInterpolation(ast) {},
visitKeyedRead(ast) {},
visitKeyedWrite(ast) {},
visitLiteralArray(ast) {},
visitLiteralMap(ast) {},
visitLiteralPrimitive(ast) {},
visitMethodCall(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = ast.span;
},
visitPipe(ast) {
if (position >= ast.exp.span.end &&
(!ast.args || !ast.args.length || position < (<AST>ast.args[0]).span.start)) {
// We are in a position a pipe name is expected.
const pipes = query.getPipes();
if (pipes) {
symbol = pipes.get(ast.name);
span = ast.span;
}
}
},
visitPrefixNot(ast) {},
visitPropertyRead(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = ast.span;
},
visitPropertyWrite(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = ast.span;
},
visitQuote(ast) {},
visitSafeMethodCall(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = ast.span;
},
visitSafePropertyRead(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = ast.span;
},
});
if (symbol && span) {
return {symbol, span};
}
}
interface ExpressionVisitor extends AstVisitor {
visit?(ast: AST, context?: any): any;
}
// Consider moving to expression_parser/ast
class NullVisitor implements ExpressionVisitor {
visitBinary(ast: Binary): void {}
visitChain(ast: Chain): void {}
visitConditional(ast: Conditional): void {}
visitFunctionCall(ast: FunctionCall): void {}
visitImplicitReceiver(ast: ImplicitReceiver): void {}
visitInterpolation(ast: Interpolation): void {}
visitKeyedRead(ast: KeyedRead): void {}
visitKeyedWrite(ast: KeyedWrite): void {}
visitLiteralArray(ast: LiteralArray): void {}
visitLiteralMap(ast: LiteralMap): void {}
visitLiteralPrimitive(ast: LiteralPrimitive): void {}
visitMethodCall(ast: MethodCall): void {}
visitPipe(ast: BindingPipe): void {}
visitPrefixNot(ast: PrefixNot): void {}
visitPropertyRead(ast: PropertyRead): void {}
visitPropertyWrite(ast: PropertyWrite): void {}
visitQuote(ast: Quote): void {}
visitSafeMethodCall(ast: SafeMethodCall): void {}
visitSafePropertyRead(ast: SafePropertyRead): void {}
}
export class TypeDiagnostic {
constructor(public kind: DiagnosticKind, public message: string, public ast: AST) {}
}
// AstType calculatetype of the ast given AST element.
class AstType implements ExpressionVisitor {
public diagnostics: TypeDiagnostic[];
constructor(private scope: SymbolTable, private query: SymbolQuery) {}
getType(ast: AST): Symbol { return ast.visit(this); }
getDiagnostics(ast: AST): TypeDiagnostic[] {
this.diagnostics = [];
ast.visit(this);
return this.diagnostics;
}
visitBinary(ast: Binary): Symbol {
// Treat undefined and null as other.
function normalize(kind: BuiltinType): BuiltinType {
switch (kind) {
case BuiltinType.Undefined:
case BuiltinType.Null:
return BuiltinType.Other;
}
return kind;
}
const leftType = this.getType(ast.left);
const rightType = this.getType(ast.right);
const leftKind = normalize(this.query.getTypeKind(leftType));
const rightKind = normalize(this.query.getTypeKind(rightType));
// The following swtich implements operator typing similar to the
// type production tables in the TypeScript specification.
const operKind = leftKind << 8 | rightKind;
switch (ast.operation) {
case '*':
case '/':
case '%':
case '-':
case '<<':
case '>>':
case '>>>':
case '&':
case '^':
case '|':
switch (operKind) {
case BuiltinType.Any << 8 | BuiltinType.Any:
case BuiltinType.Number << 8 | BuiltinType.Any:
case BuiltinType.Any << 8 | BuiltinType.Number:
case BuiltinType.Number << 8 | BuiltinType.Number:
return this.query.getBuiltinType(BuiltinType.Number);
default:
let errorAst = ast.left;
switch (leftKind) {
case BuiltinType.Any:
case BuiltinType.Number:
errorAst = ast.right;
break;
}
return this.reportError('Expected a numeric type', errorAst);
}
case '+':
switch (operKind) {
case BuiltinType.Any << 8 | BuiltinType.Any:
case BuiltinType.Any << 8 | BuiltinType.Boolean:
case BuiltinType.Any << 8 | BuiltinType.Number:
case BuiltinType.Any << 8 | BuiltinType.Other:
case BuiltinType.Boolean << 8 | BuiltinType.Any:
case BuiltinType.Number << 8 | BuiltinType.Any:
case BuiltinType.Other << 8 | BuiltinType.Any:
return this.anyType;
case BuiltinType.Any << 8 | BuiltinType.String:
case BuiltinType.Boolean << 8 | BuiltinType.String:
case BuiltinType.Number << 8 | BuiltinType.String:
case BuiltinType.String << 8 | BuiltinType.Any:
case BuiltinType.String << 8 | BuiltinType.Boolean:
case BuiltinType.String << 8 | BuiltinType.Number:
case BuiltinType.String << 8 | BuiltinType.String:
case BuiltinType.String << 8 | BuiltinType.Other:
case BuiltinType.Other << 8 | BuiltinType.String:
return this.query.getBuiltinType(BuiltinType.String);
case BuiltinType.Number << 8 | BuiltinType.Number:
return this.query.getBuiltinType(BuiltinType.Number);
case BuiltinType.Boolean << 8 | BuiltinType.Number:
case BuiltinType.Other << 8 | BuiltinType.Number:
return this.reportError('Expected a number type', ast.left);
case BuiltinType.Number << 8 | BuiltinType.Boolean:
case BuiltinType.Number << 8 | BuiltinType.Other:
return this.reportError('Expected a number type', ast.right);
default:
return this.reportError('Expected operands to be a string or number type', ast);
}
case '>':
case '<':
case '<=':
case '>=':
case '==':
case '!=':
case '===':
case '!==':
switch (operKind) {
case BuiltinType.Any << 8 | BuiltinType.Any:
case BuiltinType.Any << 8 | BuiltinType.Boolean:
case BuiltinType.Any << 8 | BuiltinType.Number:
case BuiltinType.Any << 8 | BuiltinType.String:
case BuiltinType.Any << 8 | BuiltinType.Other:
case BuiltinType.Boolean << 8 | BuiltinType.Any:
case BuiltinType.Boolean << 8 | BuiltinType.Boolean:
case BuiltinType.Number << 8 | BuiltinType.Any:
case BuiltinType.Number << 8 | BuiltinType.Number:
case BuiltinType.String << 8 | BuiltinType.Any:
case BuiltinType.String << 8 | BuiltinType.String:
case BuiltinType.Other << 8 | BuiltinType.Any:
case BuiltinType.Other << 8 | BuiltinType.Other:
return this.query.getBuiltinType(BuiltinType.Boolean);
default:
return this.reportError('Expected the operants to be of similar type or any', ast);
}
case '&&':
return rightType;
case '||':
return this.query.getTypeUnion(leftType, rightType);
}
return this.reportError(`Unrecognized operator ${ast.operation}`, ast);
}
visitChain(ast: Chain) {
if (this.diagnostics) {
// If we are producing diagnostics, visit the children
visitChildren(ast, this);
}
// The type of a chain is always undefined.
return this.query.getBuiltinType(BuiltinType.Undefined);
}
visitConditional(ast: Conditional) {
// The type of a conditional is the union of the true and false conditions.
return this.query.getTypeUnion(this.getType(ast.trueExp), this.getType(ast.falseExp));
}
visitFunctionCall(ast: FunctionCall) {
// The type of a function call is the return type of the selected signature.
// The signature is selected based on the types of the arguments. Angular doesn't
// support contextual typing of arguments so this is simpler than TypeScript's
// version.
const args = ast.args.map(arg => this.getType(arg));
const target = this.getType(ast.target);
if (!target || !target.callable) return this.reportError('Call target is not callable', ast);
const signature = target.selectSignature(args);
if (signature) return signature.result;
// TODO: Consider a better error message here.
return this.reportError('Unable no compatible signature found for call', ast);
}
visitImplicitReceiver(ast: ImplicitReceiver): Symbol {
const _this = this;
// Return a pseudo-symbol for the implicit receiver.
// The members of the implicit receiver are what is defined by the
// scope passed into this class.
return {
name: '$implict',
kind: 'component',
language: 'ng-template',
type: undefined,
container: undefined,
callable: false,
public: true,
definition: undefined,
members(): SymbolTable{return _this.scope;},
signatures(): Signature[]{return [];},
selectSignature(types): Signature | undefined{return undefined;},
indexed(argument): Symbol | undefined{return undefined;}
};
}
visitInterpolation(ast: Interpolation): Symbol {
// If we are producing diagnostics, visit the children.
if (this.diagnostics) {
visitChildren(ast, this);
}
return this.undefinedType;
}
visitKeyedRead(ast: KeyedRead): Symbol {
const targetType = this.getType(ast.obj);
const keyType = this.getType(ast.key);
const result = targetType.indexed(keyType);
return result || this.anyType;
}
visitKeyedWrite(ast: KeyedWrite): Symbol {
// The write of a type is the type of the value being written.
return this.getType(ast.value);
}
visitLiteralArray(ast: LiteralArray): Symbol {
// A type literal is an array type of the union of the elements
return this.query.getArrayType(
this.query.getTypeUnion(...ast.expressions.map(element => this.getType(element))));
}
visitLiteralMap(ast: LiteralMap): Symbol {
// If we are producing diagnostics, visit the children
if (this.diagnostics) {
visitChildren(ast, this);
}
// TODO: Return a composite type.
return this.anyType;
}
visitLiteralPrimitive(ast: LiteralPrimitive) {
// The type of a literal primitive depends on the value of the literal.
switch (ast.value) {
case true:
case false:
return this.query.getBuiltinType(BuiltinType.Boolean);
case null:
return this.query.getBuiltinType(BuiltinType.Null);
default:
switch (typeof ast.value) {
case 'string':
return this.query.getBuiltinType(BuiltinType.String);
case 'number':
return this.query.getBuiltinType(BuiltinType.Number);
default:
return this.reportError('Unrecognized primitive', ast);
}
}
}
visitMethodCall(ast: MethodCall) {
return this.resolveMethodCall(this.getType(ast.receiver), ast);
}
visitPipe(ast: BindingPipe) {
// The type of a pipe node is the return type of the pipe's transform method. The table returned
// by getPipes() is expected to contain symbols with the corresponding transform method type.
const pipe = this.query.getPipes().get(ast.name);
if (!pipe) return this.reportError(`No pipe by the name ${pipe.name} found`, ast);
const expType = this.getType(ast.exp);
const signature =
pipe.selectSignature([expType].concat(ast.args.map(arg => this.getType(arg))));
if (!signature) return this.reportError('Unable to resolve signature for pipe invocation', ast);
return signature.result;
}
visitPrefixNot(ast: PrefixNot) {
// The type of a prefix ! is always boolean.
return this.query.getBuiltinType(BuiltinType.Boolean);
}
visitPropertyRead(ast: PropertyRead) {
return this.resolvePropertyRead(this.getType(ast.receiver), ast);
}
visitPropertyWrite(ast: PropertyWrite) {
// The type of a write is the type of the value being written.
return this.getType(ast.value);
}
visitQuote(ast: Quote) {
// The type of a quoted expression is any.
return this.query.getBuiltinType(BuiltinType.Any);
}
visitSafeMethodCall(ast: SafeMethodCall) {
return this.resolveMethodCall(this.query.getNonNullableType(this.getType(ast.receiver)), ast);
}
visitSafePropertyRead(ast: SafePropertyRead) {
return this.resolvePropertyRead(this.query.getNonNullableType(this.getType(ast.receiver)), ast);
}
private _anyType: Symbol;
private get anyType(): Symbol {
let result = this._anyType;
if (!result) {
result = this._anyType = this.query.getBuiltinType(BuiltinType.Any);
}
return result;
}
private _undefinedType: Symbol;
private get undefinedType(): Symbol {
let result = this._undefinedType;
if (!result) {
result = this._undefinedType = this.query.getBuiltinType(BuiltinType.Undefined);
}
return result;
}
private resolveMethodCall(receiverType: Symbol, ast: SafeMethodCall|MethodCall) {
if (this.isAny(receiverType)) {
return this.anyType;
}
// The type of a method is the selected methods result type.
const method = receiverType.members().get(ast.name);
if (!method) return this.reportError(`Unknown method ${ast.name}`, ast);
if (!method.type.callable) return this.reportError(`Member ${ast.name} is not callable`, ast);
const signature = method.type.selectSignature(ast.args.map(arg => this.getType(arg)));
if (!signature)
return this.reportError(`Unable to resolve signature for call of method ${ast.name}`, ast);
return signature.result;
}
private resolvePropertyRead(receiverType: Symbol, ast: SafePropertyRead|PropertyRead) {
if (this.isAny(receiverType)) {
return this.anyType;
}
// The type of a property read is the seelcted member's type.
const member = receiverType.members().get(ast.name);
if (!member) {
let receiverInfo = receiverType.name;
if (receiverInfo == '$implict') {
receiverInfo =
'The component declaration, template variable declarations, and element references do';
} else {
receiverInfo = `'${receiverInfo}' does`;
}
return this.reportError(
`Identifier '${ast.name}' is not defined. ${receiverInfo} not contain such a member`,
ast);
}
if (!member.public) {
let receiverInfo = receiverType.name;
if (receiverInfo == '$implict') {
receiverInfo = 'the component';
} else {
receiverInfo = `'${receiverInfo}'`;
}
this.reportWarning(
`Identifier '${ast.name}' refers to a private member of ${receiverInfo}`, ast);
}
return member.type;
}
private reportError(message: string, ast: AST): Symbol {
if (this.diagnostics) {
this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Error, message, ast));
}
return this.anyType;
}
private reportWarning(message: string, ast: AST): Symbol {
if (this.diagnostics) {
this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Warning, message, ast));
}
return this.anyType;
}
private isAny(symbol: Symbol): boolean {
return !symbol || this.query.getTypeKind(symbol) == BuiltinType.Any ||
(symbol.type && this.isAny(symbol.type));
}
}
class AstPath extends AstPathBase<AST> {
constructor(ast: AST, public position: number, excludeEmpty: boolean = false) {
super(new AstPathVisitor(position, excludeEmpty).buildPath(ast).path);
}
}
class AstPathVisitor extends NullVisitor {
public path: AST[] = [];
constructor(private position: number, private excludeEmpty: boolean) { super(); }
visit(ast: AST) {
if ((!this.excludeEmpty || ast.span.start < ast.span.end) && inSpan(this.position, ast.span)) {
this.path.push(ast);
visitChildren(ast, this);
}
}
buildPath(ast: AST): AstPathVisitor {
// We never care about the ASTWithSource node and its visit() method calls its ast's visit so
// the visit() method above would never see it.
if (ast instanceof ASTWithSource) {
ast = ast.ast;
}
this.visit(ast);
return this;
}
}
// TODO: Consider moving to expression_parser/ast
function visitChildren(ast: AST, visitor: ExpressionVisitor) {
function visit(ast: AST) { visitor.visit && visitor.visit(ast) || ast.visit(visitor); }
function visitAll<T extends AST>(asts: T[]) { asts.forEach(visit); }
ast.visit({
visitBinary(ast) {
visit(ast.left);
visit(ast.right);
},
visitChain(ast) { visitAll(ast.expressions); },
visitConditional(ast) {
visit(ast.condition);
visit(ast.trueExp);
visit(ast.falseExp);
},
visitFunctionCall(ast) {
visit(ast.target);
visitAll(ast.args);
},
visitImplicitReceiver(ast) {},
visitInterpolation(ast) { visitAll(ast.expressions); },
visitKeyedRead(ast) {
visit(ast.obj);
visit(ast.key);
},
visitKeyedWrite(ast) {
visit(ast.obj);
visit(ast.key);
visit(ast.obj);
},
visitLiteralArray(ast) { visitAll(ast.expressions); },
visitLiteralMap(ast) {},
visitLiteralPrimitive(ast) {},
visitMethodCall(ast) {
visit(ast.receiver);
visitAll(ast.args);
},
visitPipe(ast) {
visit(ast.exp);
visitAll(ast.args);
},
visitPrefixNot(ast) { visit(ast.expression); },
visitPropertyRead(ast) { visit(ast.receiver); },
visitPropertyWrite(ast) {
visit(ast.receiver);
visit(ast.value);
},
visitQuote(ast) {},
visitSafeMethodCall(ast) {
visit(ast.receiver);
visitAll(ast.args);
},
visitSafePropertyRead(ast) { visit(ast.receiver); },
});
}
export function getExpressionScope(
info: TemplateInfo, path: TemplateAstPath, includeEvent: boolean): SymbolTable {
let result = info.template.members;
const references = getReferences(info);
const variables = getVarDeclarations(info, path);
const events = getEventDeclaration(info, path, includeEvent);
if (references.length || variables.length || events.length) {
const referenceTable = info.template.query.createSymbolTable(references);
const variableTable = info.template.query.createSymbolTable(variables);
const eventsTable = info.template.query.createSymbolTable(events);
result =
info.template.query.mergeSymbolTable([result, referenceTable, variableTable, eventsTable]);
}
return result;
}
function getEventDeclaration(info: TemplateInfo, path: TemplateAstPath, includeEvent?: boolean) {
let result: SymbolDeclaration[] = [];
if (includeEvent) {
// TODO: Determine the type of the event parameter based on the Observable<T> or EventEmitter<T>
// of the event.
result = [{
name: '$event',
kind: 'variable',
type: info.template.query.getBuiltinType(BuiltinType.Any)
}];
}
return result;
}
function getReferences(info: TemplateInfo): SymbolDeclaration[] {
const result: SymbolDeclaration[] = [];
function processReferences(references: ReferenceAst[]) {
for (const reference of references) {
let type: Symbol;
if (reference.value) {
type = info.template.query.getTypeSymbol(reference.value.reference);
}
result.push({
name: reference.name,
kind: 'reference',
type: type || info.template.query.getBuiltinType(BuiltinType.Any),
get definition() { return getDefintionOf(info, reference); }
});
}
}
const visitor = new class extends TemplateAstChildVisitor {
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
super.visitEmbeddedTemplate(ast, context);
processReferences(ast.references);
}
visitElement(ast: ElementAst, context: any): any {
super.visitElement(ast, context);
processReferences(ast.references);
}
};
templateVisitAll(visitor, info.templateAst);
return result;
}
function getVarDeclarations(info: TemplateInfo, path: TemplateAstPath): SymbolDeclaration[] {
const result: SymbolDeclaration[] = [];
let current = path.tail;
while (current) {
if (current instanceof EmbeddedTemplateAst) {
for (const variable of current.variables) {
const name = variable.name;
// Find the first directive with a context.
const context =
current.directives
.map(d => info.template.query.getTemplateContext(d.directive.type.reference))
.find(c => !!c);
// Determine the type of the context field referenced by variable.value.
let type: Symbol;
if (context) {
const value = context.get(variable.value);
if (value) {
type = value.type;
if (info.template.query.getTypeKind(type) === BuiltinType.Any) {
// The any type is not very useful here. For special cases, such as ngFor, we can do
// better.
type = refinedVariableType(type, info, current);
}
}
}
if (!type) {
type = info.template.query.getBuiltinType(BuiltinType.Any);
}
result.push({
name,
kind: 'variable', type, get definition() { return getDefintionOf(info, variable); }
});
}
}
current = path.parentOf(current);
}
return result;
}
function refinedVariableType(
type: Symbol, info: TemplateInfo, templateElement: EmbeddedTemplateAst): Symbol {
// Special case the ngFor directive
const ngForDirective = templateElement.directives.find(d => d.directive.type.name == 'NgFor');
if (ngForDirective) {
const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf');
if (ngForOfBinding) {
const bindingType =
new AstType(info.template.members, info.template.query).getType(ngForOfBinding.value);
if (bindingType) {
return info.template.query.getElementType(bindingType);
}
}
}
// We can't do better, just return the original type.
return type;
}
function getDefintionOf(info: TemplateInfo, ast: TemplateAst): Definition {
if (info.fileName) {
const templateOffset = info.template.span.start;
return [{
fileName: info.fileName,
span: {
start: ast.sourceSpan.start.offset + templateOffset,
end: ast.sourceSpan.end.offset + templateOffset
}
}];
}
}

View File

@ -1,28 +0,0 @@
/**
* @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
*/
import {TemplateInfo} from './common';
import {locateSymbol} from './locate_symbol';
import {Hover, HoverTextSection, Symbol} from './types';
export function getHover(info: TemplateInfo): Hover {
const result = locateSymbol(info);
if (result) {
return {text: hoverTextOf(result.symbol), span: result.span};
}
}
function hoverTextOf(symbol: Symbol): HoverTextSection[] {
const result: HoverTextSection[] =
[{text: symbol.kind}, {text: ' '}, {text: symbol.name, language: symbol.language}];
const container = symbol.container;
if (container) {
result.push({text: ' of '}, {text: container.name, language: container.language});
}
return result;
}

View File

@ -1,462 +0,0 @@
/**
* @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
*/
// Information about the HTML DOM elements
// This section defines the HTML elements and attribute surface of HTML 4
// which is derived from https://www.w3.org/TR/html4/strict.dtd
type attrType = string | string[];
type hash<T> = {
[name: string]: T
};
const values: attrType[] = [
'ID',
'CDATA',
'NAME',
['ltr', 'rtl'],
['rect', 'circle', 'poly', 'default'],
'NUMBER',
['nohref'],
['ismap'],
['declare'],
['DATA', 'REF', 'OBJECT'],
['GET', 'POST'],
'IDREF',
['TEXT', 'PASSWORD', 'CHECKBOX', 'RADIO', 'SUBMIT', 'RESET', 'FILE', 'HIDDEN', 'IMAGE', 'BUTTON'],
['checked'],
['disabled'],
['readonly'],
['multiple'],
['selected'],
['button', 'submit', 'reset'],
['void', 'above', 'below', 'hsides', 'lhs', 'rhs', 'vsides', 'box', 'border'],
['none', 'groups', 'rows', 'cols', 'all'],
['left', 'center', 'right', 'justify', 'char'],
['top', 'middle', 'bottom', 'baseline'],
'IDREFS',
['row', 'col', 'rowgroup', 'colgroup'],
['defer']
];
const groups: hash<number>[] = [
{id: 0},
{
onclick: 1,
ondblclick: 1,
onmousedown: 1,
onmouseup: 1,
onmouseover: 1,
onmousemove: 1,
onmouseout: 1,
onkeypress: 1,
onkeydown: 1,
onkeyup: 1
},
{lang: 2, dir: 3},
{onload: 1, onunload: 1},
{name: 1},
{href: 1},
{type: 1},
{alt: 1},
{tabindex: 5},
{media: 1},
{nohref: 6},
{usemap: 1},
{src: 1},
{onfocus: 1, onblur: 1},
{charset: 1},
{declare: 8, classid: 1, codebase: 1, data: 1, codetype: 1, archive: 1, standby: 1},
{title: 1},
{value: 1},
{cite: 1},
{datetime: 1},
{accept: 1},
{shape: 4, coords: 1},
{ for: 11
},
{action: 1, method: 10, enctype: 1, onsubmit: 1, onreset: 1, 'accept-charset': 1},
{valuetype: 9},
{longdesc: 1},
{width: 1},
{disabled: 14},
{readonly: 15, onselect: 1},
{accesskey: 1},
{size: 5, multiple: 16},
{onchange: 1},
{label: 1},
{selected: 17},
{type: 12, checked: 13, size: 1, maxlength: 5},
{rows: 5, cols: 5},
{type: 18},
{height: 1},
{summary: 1, border: 1, frame: 19, rules: 20, cellspacing: 1, cellpadding: 1, datapagesize: 1},
{align: 21, char: 1, charoff: 1, valign: 22},
{span: 5},
{abbr: 1, axis: 1, headers: 23, scope: 24, rowspan: 5, colspan: 5},
{profile: 1},
{'http-equiv': 2, name: 2, content: 1, scheme: 1},
{class: 1, style: 1},
{hreflang: 2, rel: 1, rev: 1},
{ismap: 7},
{ defer: 25, event: 1, for : 1 }
];
const elements: {[name: string]: number[]} = {
TT: [0, 1, 2, 16, 44],
I: [0, 1, 2, 16, 44],
B: [0, 1, 2, 16, 44],
BIG: [0, 1, 2, 16, 44],
SMALL: [0, 1, 2, 16, 44],
EM: [0, 1, 2, 16, 44],
STRONG: [0, 1, 2, 16, 44],
DFN: [0, 1, 2, 16, 44],
CODE: [0, 1, 2, 16, 44],
SAMP: [0, 1, 2, 16, 44],
KBD: [0, 1, 2, 16, 44],
VAR: [0, 1, 2, 16, 44],
CITE: [0, 1, 2, 16, 44],
ABBR: [0, 1, 2, 16, 44],
ACRONYM: [0, 1, 2, 16, 44],
SUB: [0, 1, 2, 16, 44],
SUP: [0, 1, 2, 16, 44],
SPAN: [0, 1, 2, 16, 44],
BDO: [0, 2, 16, 44],
BR: [0, 16, 44],
BODY: [0, 1, 2, 3, 16, 44],
ADDRESS: [0, 1, 2, 16, 44],
DIV: [0, 1, 2, 16, 44],
A: [0, 1, 2, 4, 5, 6, 8, 13, 14, 16, 21, 29, 44, 45],
MAP: [0, 1, 2, 4, 16, 44],
AREA: [0, 1, 2, 5, 7, 8, 10, 13, 16, 21, 29, 44],
LINK: [0, 1, 2, 5, 6, 9, 14, 16, 44, 45],
IMG: [0, 1, 2, 4, 7, 11, 12, 16, 25, 26, 37, 44, 46],
OBJECT: [0, 1, 2, 4, 6, 8, 11, 15, 16, 26, 37, 44],
PARAM: [0, 4, 6, 17, 24],
HR: [0, 1, 2, 16, 44],
P: [0, 1, 2, 16, 44],
H1: [0, 1, 2, 16, 44],
H2: [0, 1, 2, 16, 44],
H3: [0, 1, 2, 16, 44],
H4: [0, 1, 2, 16, 44],
H5: [0, 1, 2, 16, 44],
H6: [0, 1, 2, 16, 44],
PRE: [0, 1, 2, 16, 44],
Q: [0, 1, 2, 16, 18, 44],
BLOCKQUOTE: [0, 1, 2, 16, 18, 44],
INS: [0, 1, 2, 16, 18, 19, 44],
DEL: [0, 1, 2, 16, 18, 19, 44],
DL: [0, 1, 2, 16, 44],
DT: [0, 1, 2, 16, 44],
DD: [0, 1, 2, 16, 44],
OL: [0, 1, 2, 16, 44],
UL: [0, 1, 2, 16, 44],
LI: [0, 1, 2, 16, 44],
FORM: [0, 1, 2, 4, 16, 20, 23, 44],
LABEL: [0, 1, 2, 13, 16, 22, 29, 44],
INPUT: [0, 1, 2, 4, 7, 8, 11, 12, 13, 16, 17, 20, 27, 28, 29, 31, 34, 44, 46],
SELECT: [0, 1, 2, 4, 8, 13, 16, 27, 30, 31, 44],
OPTGROUP: [0, 1, 2, 16, 27, 32, 44],
OPTION: [0, 1, 2, 16, 17, 27, 32, 33, 44],
TEXTAREA: [0, 1, 2, 4, 8, 13, 16, 27, 28, 29, 31, 35, 44],
FIELDSET: [0, 1, 2, 16, 44],
LEGEND: [0, 1, 2, 16, 29, 44],
BUTTON: [0, 1, 2, 4, 8, 13, 16, 17, 27, 29, 36, 44],
TABLE: [0, 1, 2, 16, 26, 38, 44],
CAPTION: [0, 1, 2, 16, 44],
COLGROUP: [0, 1, 2, 16, 26, 39, 40, 44],
COL: [0, 1, 2, 16, 26, 39, 40, 44],
THEAD: [0, 1, 2, 16, 39, 44],
TBODY: [0, 1, 2, 16, 39, 44],
TFOOT: [0, 1, 2, 16, 39, 44],
TR: [0, 1, 2, 16, 39, 44],
TH: [0, 1, 2, 16, 39, 41, 44],
TD: [0, 1, 2, 16, 39, 41, 44],
HEAD: [2, 42],
TITLE: [2],
BASE: [5],
META: [2, 43],
STYLE: [2, 6, 9, 16],
SCRIPT: [6, 12, 14, 47],
NOSCRIPT: [0, 1, 2, 16, 44],
HTML: [2]
};
const defaultAttributes = [0, 1, 2, 4];
export function elementNames(): string[] {
return Object.keys(elements).sort().map(v => v.toLowerCase());
}
function compose(indexes: number[] | undefined): hash<attrType> {
const result: hash<attrType> = {};
if (indexes) {
for (let index of indexes) {
const group = groups[index];
for (let name in group)
if (group.hasOwnProperty(name)) result[name] = values[group[name]];
}
}
return result;
}
export function attributeNames(element: string): string[] {
return Object.keys(compose(elements[element.toUpperCase()] || defaultAttributes)).sort();
}
export function attributeType(element: string, attribute: string): string|string[]|undefined {
return compose(elements[element.toUpperCase()] || defaultAttributes)[attribute.toLowerCase()];
}
// This section is describes the DOM property surface of a DOM element and is dervided from
// from the SCHEMA strings from the security context information. SCHEMA is copied here because
// it would be an unnecessary risk to allow this array to be imported from the security context
// schema registry.
const SCHEMA:
string[] =
[
'[Element]|textContent,%classList,className,id,innerHTML,*beforecopy,*beforecut,*beforepaste,*copy,*cut,*paste,*search,*selectstart,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerHTML,#scrollLeft,#scrollTop',
'[HTMLElement]^[Element]|accessKey,contentEditable,dir,!draggable,!hidden,innerText,lang,*abort,*beforecopy,*beforecut,*beforepaste,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*message,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*mozfullscreenchange,*mozfullscreenerror,*mozpointerlockchange,*mozpointerlockerror,*paste,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*search,*seeked,*seeking,*select,*selectstart,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,*webglcontextcreationerror,*webglcontextlost,*webglcontextrestored,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate',
'abbr,address,article,aside,b,bdi,bdo,cite,code,dd,dfn,dt,em,figcaption,figure,footer,header,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,section,small,strong,sub,sup,u,var,wbr^[HTMLElement]|accessKey,contentEditable,dir,!draggable,!hidden,innerText,lang,*abort,*beforecopy,*beforecut,*beforepaste,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*message,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*mozfullscreenchange,*mozfullscreenerror,*mozpointerlockchange,*mozpointerlockerror,*paste,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*search,*seeked,*seeking,*select,*selectstart,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,*webglcontextcreationerror,*webglcontextlost,*webglcontextrestored,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate',
'media^[HTMLElement]|!autoplay,!controls,%crossOrigin,#currentTime,!defaultMuted,#defaultPlaybackRate,!disableRemotePlayback,!loop,!muted,*encrypted,#playbackRate,preload,src,%srcObject,#volume',
':svg:^[HTMLElement]|*abort,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*cuechange,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*seeked,*seeking,*select,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,%style,#tabIndex',
':svg:graphics^:svg:|',
':svg:animation^:svg:|*begin,*end,*repeat',
':svg:geometry^:svg:|',
':svg:componentTransferFunction^:svg:|',
':svg:gradient^:svg:|',
':svg:textContent^:svg:graphics|',
':svg:textPositioning^:svg:textContent|',
'a^[HTMLElement]|charset,coords,download,hash,host,hostname,href,hreflang,name,password,pathname,ping,port,protocol,referrerPolicy,rel,rev,search,shape,target,text,type,username',
'area^[HTMLElement]|alt,coords,hash,host,hostname,href,!noHref,password,pathname,ping,port,protocol,referrerPolicy,search,shape,target,username',
'audio^media|',
'br^[HTMLElement]|clear',
'base^[HTMLElement]|href,target',
'body^[HTMLElement]|aLink,background,bgColor,link,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,text,vLink',
'button^[HTMLElement]|!autofocus,!disabled,formAction,formEnctype,formMethod,!formNoValidate,formTarget,name,type,value',
'canvas^[HTMLElement]|#height,#width',
'content^[HTMLElement]|select',
'dl^[HTMLElement]|!compact',
'datalist^[HTMLElement]|',
'details^[HTMLElement]|!open',
'dialog^[HTMLElement]|!open,returnValue',
'dir^[HTMLElement]|!compact',
'div^[HTMLElement]|align',
'embed^[HTMLElement]|align,height,name,src,type,width',
'fieldset^[HTMLElement]|!disabled,name',
'font^[HTMLElement]|color,face,size',
'form^[HTMLElement]|acceptCharset,action,autocomplete,encoding,enctype,method,name,!noValidate,target',
'frame^[HTMLElement]|frameBorder,longDesc,marginHeight,marginWidth,name,!noResize,scrolling,src',
'frameset^[HTMLElement]|cols,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,rows',
'hr^[HTMLElement]|align,color,!noShade,size,width',
'head^[HTMLElement]|',
'h1,h2,h3,h4,h5,h6^[HTMLElement]|align',
'html^[HTMLElement]|version',
'iframe^[HTMLElement]|align,!allowFullscreen,frameBorder,height,longDesc,marginHeight,marginWidth,name,referrerPolicy,%sandbox,scrolling,src,srcdoc,width',
'img^[HTMLElement]|align,alt,border,%crossOrigin,#height,#hspace,!isMap,longDesc,lowsrc,name,referrerPolicy,sizes,src,srcset,useMap,#vspace,#width',
'input^[HTMLElement]|accept,align,alt,autocapitalize,autocomplete,!autofocus,!checked,!defaultChecked,defaultValue,dirName,!disabled,%files,formAction,formEnctype,formMethod,!formNoValidate,formTarget,#height,!incremental,!indeterminate,max,#maxLength,min,#minLength,!multiple,name,pattern,placeholder,!readOnly,!required,selectionDirection,#selectionEnd,#selectionStart,#size,src,step,type,useMap,value,%valueAsDate,#valueAsNumber,#width',
'keygen^[HTMLElement]|!autofocus,challenge,!disabled,keytype,name',
'li^[HTMLElement]|type,#value',
'label^[HTMLElement]|htmlFor',
'legend^[HTMLElement]|align',
'link^[HTMLElement]|as,charset,%crossOrigin,!disabled,href,hreflang,integrity,media,rel,%relList,rev,%sizes,target,type',
'map^[HTMLElement]|name',
'marquee^[HTMLElement]|behavior,bgColor,direction,height,#hspace,#loop,#scrollAmount,#scrollDelay,!trueSpeed,#vspace,width',
'menu^[HTMLElement]|!compact',
'meta^[HTMLElement]|content,httpEquiv,name,scheme',
'meter^[HTMLElement]|#high,#low,#max,#min,#optimum,#value',
'ins,del^[HTMLElement]|cite,dateTime',
'ol^[HTMLElement]|!compact,!reversed,#start,type',
'object^[HTMLElement]|align,archive,border,code,codeBase,codeType,data,!declare,height,#hspace,name,standby,type,useMap,#vspace,width',
'optgroup^[HTMLElement]|!disabled,label',
'option^[HTMLElement]|!defaultSelected,!disabled,label,!selected,text,value',
'output^[HTMLElement]|defaultValue,%htmlFor,name,value',
'p^[HTMLElement]|align',
'param^[HTMLElement]|name,type,value,valueType',
'picture^[HTMLElement]|',
'pre^[HTMLElement]|#width',
'progress^[HTMLElement]|#max,#value',
'q,blockquote,cite^[HTMLElement]|',
'script^[HTMLElement]|!async,charset,%crossOrigin,!defer,event,htmlFor,integrity,src,text,type',
'select^[HTMLElement]|!autofocus,!disabled,#length,!multiple,name,!required,#selectedIndex,#size,value',
'shadow^[HTMLElement]|',
'source^[HTMLElement]|media,sizes,src,srcset,type',
'span^[HTMLElement]|',
'style^[HTMLElement]|!disabled,media,type',
'caption^[HTMLElement]|align',
'th,td^[HTMLElement]|abbr,align,axis,bgColor,ch,chOff,#colSpan,headers,height,!noWrap,#rowSpan,scope,vAlign,width',
'col,colgroup^[HTMLElement]|align,ch,chOff,#span,vAlign,width',
'table^[HTMLElement]|align,bgColor,border,%caption,cellPadding,cellSpacing,frame,rules,summary,%tFoot,%tHead,width',
'tr^[HTMLElement]|align,bgColor,ch,chOff,vAlign',
'tfoot,thead,tbody^[HTMLElement]|align,ch,chOff,vAlign',
'template^[HTMLElement]|',
'textarea^[HTMLElement]|autocapitalize,!autofocus,#cols,defaultValue,dirName,!disabled,#maxLength,#minLength,name,placeholder,!readOnly,!required,#rows,selectionDirection,#selectionEnd,#selectionStart,value,wrap',
'title^[HTMLElement]|text',
'track^[HTMLElement]|!default,kind,label,src,srclang',
'ul^[HTMLElement]|!compact,type',
'unknown^[HTMLElement]|',
'video^media|#height,poster,#width',
':svg:a^:svg:graphics|',
':svg:animate^:svg:animation|',
':svg:animateMotion^:svg:animation|',
':svg:animateTransform^:svg:animation|',
':svg:circle^:svg:geometry|',
':svg:clipPath^:svg:graphics|',
':svg:cursor^:svg:|',
':svg:defs^:svg:graphics|',
':svg:desc^:svg:|',
':svg:discard^:svg:|',
':svg:ellipse^:svg:geometry|',
':svg:feBlend^:svg:|',
':svg:feColorMatrix^:svg:|',
':svg:feComponentTransfer^:svg:|',
':svg:feComposite^:svg:|',
':svg:feConvolveMatrix^:svg:|',
':svg:feDiffuseLighting^:svg:|',
':svg:feDisplacementMap^:svg:|',
':svg:feDistantLight^:svg:|',
':svg:feDropShadow^:svg:|',
':svg:feFlood^:svg:|',
':svg:feFuncA^:svg:componentTransferFunction|',
':svg:feFuncB^:svg:componentTransferFunction|',
':svg:feFuncG^:svg:componentTransferFunction|',
':svg:feFuncR^:svg:componentTransferFunction|',
':svg:feGaussianBlur^:svg:|',
':svg:feImage^:svg:|',
':svg:feMerge^:svg:|',
':svg:feMergeNode^:svg:|',
':svg:feMorphology^:svg:|',
':svg:feOffset^:svg:|',
':svg:fePointLight^:svg:|',
':svg:feSpecularLighting^:svg:|',
':svg:feSpotLight^:svg:|',
':svg:feTile^:svg:|',
':svg:feTurbulence^:svg:|',
':svg:filter^:svg:|',
':svg:foreignObject^:svg:graphics|',
':svg:g^:svg:graphics|',
':svg:image^:svg:graphics|',
':svg:line^:svg:geometry|',
':svg:linearGradient^:svg:gradient|',
':svg:mpath^:svg:|',
':svg:marker^:svg:|',
':svg:mask^:svg:|',
':svg:metadata^:svg:|',
':svg:path^:svg:geometry|',
':svg:pattern^:svg:|',
':svg:polygon^:svg:geometry|',
':svg:polyline^:svg:geometry|',
':svg:radialGradient^:svg:gradient|',
':svg:rect^:svg:geometry|',
':svg:svg^:svg:graphics|#currentScale,#zoomAndPan',
':svg:script^:svg:|type',
':svg:set^:svg:animation|',
':svg:stop^:svg:|',
':svg:style^:svg:|!disabled,media,title,type',
':svg:switch^:svg:graphics|',
':svg:symbol^:svg:|',
':svg:tspan^:svg:textPositioning|',
':svg:text^:svg:textPositioning|',
':svg:textPath^:svg:textContent|',
':svg:title^:svg:|',
':svg:use^:svg:graphics|',
':svg:view^:svg:|#zoomAndPan',
'data^[HTMLElement]|value',
'menuitem^[HTMLElement]|type,label,icon,!disabled,!checked,radiogroup,!default',
'summary^[HTMLElement]|',
'time^[HTMLElement]|dateTime',
];
const attrToPropMap: {[name: string]: string} = <any>{
'class': 'className',
'formaction': 'formAction',
'innerHtml': 'innerHTML',
'readonly': 'readOnly',
'tabindex': 'tabIndex'
};
const EVENT = 'event';
const BOOLEAN = 'boolean';
const NUMBER = 'number';
const STRING = 'string';
const OBJECT = 'object';
export class SchemaInformation {
schema = <{[element: string]: {[property: string]: string}}>{};
constructor() {
SCHEMA.forEach(encodedType => {
const parts = encodedType.split('|');
const properties = parts[1].split(',');
const typeParts = (parts[0] + '^').split('^');
const typeName = typeParts[0];
const type = <{[property: string]: string}>{};
typeName.split(',').forEach(tag => this.schema[tag.toLowerCase()] = type);
const superName = typeParts[1];
const superType = superName && this.schema[superName.toLowerCase()];
if (superType) {
for (const key in superType) {
type[key] = superType[key];
}
}
properties.forEach((property: string) => {
if (property == '') {
} else if (property.startsWith('*')) {
type[property.substring(1)] = EVENT;
} else if (property.startsWith('!')) {
type[property.substring(1)] = BOOLEAN;
} else if (property.startsWith('#')) {
type[property.substring(1)] = NUMBER;
} else if (property.startsWith('%')) {
type[property.substring(1)] = OBJECT;
} else {
type[property] = STRING;
}
});
});
}
allKnownElements(): string[] { return Object.keys(this.schema); }
eventsOf(elementName: string): string[] {
const elementType = this.schema[elementName.toLowerCase()] || {};
return Object.keys(elementType).filter(property => elementType[property] === EVENT);
}
propertiesOf(elementName: string): string[] {
const elementType = this.schema[elementName.toLowerCase()] || {};
return Object.keys(elementType).filter(property => elementType[property] !== EVENT);
}
typeOf(elementName: string, property: string): string {
return (this.schema[elementName.toLowerCase()] || {})[property];
}
private static _instance: SchemaInformation;
static get instance(): SchemaInformation {
let result = SchemaInformation._instance;
if (!result) {
result = SchemaInformation._instance = new SchemaInformation();
}
return result;
}
}
export function eventNames(elementName: string): string[] {
return SchemaInformation.instance.eventsOf(elementName);
}
export function propertyNames(elementName: string): string[] {
return SchemaInformation.instance.propertiesOf(elementName);
}
export function propertyType(elementName: string, propertyName: string): string {
return SchemaInformation.instance.typeOf(elementName, propertyName);
}

View File

@ -1,72 +0,0 @@
/**
* @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
*/
import {Attribute, Comment, Element, Expansion, ExpansionCase, Node, Text, Visitor, visitAll} from '@angular/compiler/src/ml_parser/ast';
import {AstPath} from './ast_path';
import {inSpan, spanOf} from './utils';
export class HtmlAstPath extends AstPath<Node> {
constructor(ast: Node[], public position: number) { super(buildPath(ast, position)); }
}
function buildPath(ast: Node[], position: number): Node[] {
let visitor = new HtmlAstPathBuilder(position);
visitAll(visitor, ast);
return visitor.getPath();
}
export class ChildVisitor implements Visitor {
constructor(private visitor?: Visitor) {}
visitElement(ast: Element, context: any): any {
this.visitChildren(context, visit => {
visit(ast.attrs);
visit(ast.children);
});
}
visitAttribute(ast: Attribute, context: any): any {}
visitText(ast: Text, context: any): any {}
visitComment(ast: Comment, context: any): any {}
visitExpansion(ast: Expansion, context: any): any {
return this.visitChildren(context, visit => { visit(ast.cases); });
}
visitExpansionCase(ast: ExpansionCase, context: any): any {}
private visitChildren<T extends Node>(
context: any, cb: (visit: (<V extends Node>(children: V[]|undefined) => void)) => void) {
const visitor = this.visitor || this;
let results: any[][] = [];
function visit<T extends Node>(children: T[] | undefined) {
if (children) results.push(visitAll(visitor, children, context));
}
cb(visit);
return [].concat.apply([], results);
}
}
class HtmlAstPathBuilder extends ChildVisitor {
private path: Node[] = [];
constructor(private position: number) { super(); }
visit(ast: Node, context: any): any {
let span = spanOf(ast);
if (inSpan(this.position, span)) {
this.path.push(ast);
} else {
// Returning a value here will result in the children being skipped.
return true;
}
}
getPath(): Node[] { return this.path; }
}

View File

@ -1,186 +0,0 @@
/**
* @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
*/
import {NgAnalyzedModules} from '@angular/compiler/src/aot/compiler';
import {CompileNgModuleMetadata} from '@angular/compiler/src/compile_metadata';
import {Lexer} from '@angular/compiler/src/expression_parser/lexer';
import {Parser} from '@angular/compiler/src/expression_parser/parser';
import {I18NHtmlParser} from '@angular/compiler/src/i18n/i18n_html_parser';
import {CompileMetadataResolver} from '@angular/compiler/src/metadata_resolver';
import {HtmlParser} from '@angular/compiler/src/ml_parser/html_parser';
import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry';
import {TemplateParser} from '@angular/compiler/src/template_parser/template_parser';
import {AstResult, AttrInfo, TemplateInfo} from './common';
import {getTemplateCompletions} from './completions';
import {getDefinition} from './definitions';
import {getDeclarationDiagnostics, getTemplateDiagnostics} from './diagnostics';
import {getHover} from './hover';
import {Completion, CompletionKind, Completions, Declaration, Declarations, Definition, Diagnostic, DiagnosticKind, Diagnostics, Hover, LanguageService, LanguageServiceHost, Location, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable, TemplateSource, TemplateSources} from './types';
/**
* Create an instance of an Angular `LanguageService`.
*
* @experimental
*/
export function createLanguageService(host: LanguageServiceHost): LanguageService {
return new LanguageServiceImpl(host);
}
class LanguageServiceImpl implements LanguageService {
constructor(private host: LanguageServiceHost) {}
private get metadataResolver(): CompileMetadataResolver { return this.host.resolver; }
getTemplateReferences(): string[] { return this.host.getTemplateReferences(); }
getDiagnostics(fileName: string): Diagnostics {
let results: Diagnostics = [];
let templates = this.host.getTemplates(fileName);
if (templates && templates.length) {
results.push(...getTemplateDiagnostics(fileName, this, templates));
}
let declarations = this.host.getDeclarations(fileName);
if (declarations && declarations.length) {
const summary = this.host.getAnalyzedModules();
results.push(...getDeclarationDiagnostics(declarations, summary));
}
return uniqueBySpan(results);
}
getPipesAt(fileName: string, position: number): Pipes {
let templateInfo = this.getTemplateAstAtPosition(fileName, position);
if (templateInfo) {
return templateInfo.pipes.map(
pipeInfo => ({name: pipeInfo.name, symbol: pipeInfo.type.reference}));
}
}
getCompletionsAt(fileName: string, position: number): Completions {
let templateInfo = this.getTemplateAstAtPosition(fileName, position);
if (templateInfo) {
return getTemplateCompletions(templateInfo);
}
}
getDefinitionAt(fileName: string, position: number): Definition {
let templateInfo = this.getTemplateAstAtPosition(fileName, position);
if (templateInfo) {
return getDefinition(templateInfo);
}
}
getHoverAt(fileName: string, position: number): Hover {
let templateInfo = this.getTemplateAstAtPosition(fileName, position);
if (templateInfo) {
return getHover(templateInfo);
}
}
private getTemplateAstAtPosition(fileName: string, position: number): TemplateInfo {
let template = this.host.getTemplateAt(fileName, position);
if (template) {
let astResult = this.getTemplateAst(template, fileName);
if (astResult && astResult.htmlAst && astResult.templateAst)
return {
position,
fileName,
template,
htmlAst: astResult.htmlAst,
directive: astResult.directive,
directives: astResult.directives,
pipes: astResult.pipes,
templateAst: astResult.templateAst,
expressionParser: astResult.expressionParser
};
}
return undefined;
}
getTemplateAst(template: TemplateSource, contextFile: string): AstResult {
let result: AstResult;
try {
const directive =
this.metadataResolver.getNonNormalizedDirectiveMetadata(template.type as any);
if (directive) {
const rawHtmlParser = new HtmlParser();
const htmlParser = new I18NHtmlParser(rawHtmlParser);
const expressionParser = new Parser(new Lexer());
const parser = new TemplateParser(
expressionParser, new DomElementSchemaRegistry(), htmlParser, null, []);
const htmlResult = htmlParser.parse(template.source, '');
const analyzedModules = this.host.getAnalyzedModules();
let errors: Diagnostic[] = undefined;
let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(template.type);
if (!ngModule) {
// Reported by the the declaration diagnostics.
ngModule = findSuitableDefaultModule(analyzedModules);
}
if (ngModule) {
const directives = ngModule.transitiveModule.directives.map(
d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference).toSummary());
const pipes = ngModule.transitiveModule.pipes.map(
p => this.host.resolver.getOrLoadPipeMetadata(p.reference).toSummary());
const schemas = ngModule.schemas;
const parseResult = parser.tryParseHtml(
htmlResult, directive, template.source, directives, pipes, schemas, '');
result = {
htmlAst: htmlResult.rootNodes,
templateAst: parseResult.templateAst, directive, directives, pipes,
parseErrors: parseResult.errors, expressionParser, errors
};
}
}
} catch (e) {
let span = template.span;
if (e.fileName == contextFile) {
span = template.query.getSpanAt(e.line, e.column) || span;
}
result = {errors: [{kind: DiagnosticKind.Error, message: e.message, span}]};
}
return result;
}
}
function uniqueBySpan < T extends {
span: Span;
}
> (elements: T[] | undefined): T[]|undefined {
if (elements) {
const result: T[] = [];
const map = new Map<number, Set<number>>();
for (const element of elements) {
let span = element.span;
let set = map.get(span.start);
if (!set) {
set = new Set();
map.set(span.start, set);
}
if (!set.has(span.end)) {
set.add(span.end);
result.push(element);
}
}
return result;
}
}
function findSuitableDefaultModule(modules: NgAnalyzedModules): CompileNgModuleMetadata {
let result: CompileNgModuleMetadata;
let resultSize = 0;
for (const module of modules.ngModules) {
const moduleSize = module.transitiveModule.directives.length;
if (moduleSize > resultSize) {
result = module;
resultSize = moduleSize;
}
}
return result;
}

View File

@ -1,192 +0,0 @@
/**
* @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
*/
import {AST} from '@angular/compiler/src/expression_parser/ast';
import {Attribute} from '@angular/compiler/src/ml_parser/ast';
import {BoundDirectivePropertyAst, BoundEventAst, ElementAst, TemplateAst} from '@angular/compiler/src/template_parser/template_ast';
import {TemplateInfo} from './common';
import {getExpressionScope, getExpressionSymbol} from './expressions';
import {HtmlAstPath} from './html_path';
import {TemplateAstPath} from './template_path';
import {Definition, Location, Span, Symbol, SymbolTable} from './types';
import {inSpan, offsetSpan, spanOf} from './utils';
export interface SymbolInfo {
symbol: Symbol;
span: Span;
}
export function locateSymbol(info: TemplateInfo): SymbolInfo {
const templatePosition = info.position - info.template.span.start;
const path = new TemplateAstPath(info.templateAst, templatePosition);
if (path.tail) {
let symbol: Symbol = undefined;
let span: Span = undefined;
const attributeValueSymbol = (ast: AST, inEvent: boolean = false): boolean => {
const attribute = findAttribute(info);
if (attribute) {
if (inSpan(templatePosition, spanOf(attribute.valueSpan))) {
const scope = getExpressionScope(info, path, inEvent);
const expressionOffset = attribute.valueSpan.start.offset + 1;
const result = getExpressionSymbol(
scope, ast, templatePosition - expressionOffset, info.template.query);
if (result) {
symbol = result.symbol;
span = offsetSpan(result.span, expressionOffset);
}
return true;
}
}
return false;
};
path.tail.visit(
{
visitNgContent(ast) {},
visitEmbeddedTemplate(ast) {},
visitElement(ast) {
const component = ast.directives.find(d => d.directive.isComponent);
if (component) {
symbol = info.template.query.getTypeSymbol(component.directive.type.reference);
symbol = symbol && new OverrideKindSymbol(symbol, 'component');
span = spanOf(ast);
} else {
// Find a directive that matches the element name
const directive =
ast.directives.find(d => d.directive.selector.indexOf(ast.name) >= 0);
if (directive) {
symbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
symbol = symbol && new OverrideKindSymbol(symbol, 'directive');
span = spanOf(ast);
}
}
},
visitReference(ast) {
symbol = info.template.query.getTypeSymbol(ast.value.reference);
span = spanOf(ast);
},
visitVariable(ast) {},
visitEvent(ast) {
if (!attributeValueSymbol(ast.handler, /* inEvent */ true)) {
symbol = findOutputBinding(info, path, ast);
symbol = symbol && new OverrideKindSymbol(symbol, 'event');
span = spanOf(ast);
}
},
visitElementProperty(ast) { attributeValueSymbol(ast.value); },
visitAttr(ast) {},
visitBoundText(ast) {
const expressionPosition = templatePosition - ast.sourceSpan.start.offset;
if (inSpan(expressionPosition, ast.value.span)) {
const scope = getExpressionScope(info, path, /* includeEvent */ false);
const result =
getExpressionSymbol(scope, ast.value, expressionPosition, info.template.query);
if (result) {
symbol = result.symbol;
span = offsetSpan(result.span, ast.sourceSpan.start.offset);
}
}
},
visitText(ast) {},
visitDirective(ast) {
symbol = info.template.query.getTypeSymbol(ast.directive.type.reference);
span = spanOf(ast);
},
visitDirectiveProperty(ast) {
if (!attributeValueSymbol(ast.value)) {
symbol = findInputBinding(info, path, ast);
span = spanOf(ast);
}
}
},
null);
if (symbol && span) {
return {symbol, span: offsetSpan(span, info.template.span.start)};
}
}
}
function findAttribute(info: TemplateInfo): Attribute {
const templatePosition = info.position - info.template.span.start;
const path = new HtmlAstPath(info.htmlAst, templatePosition);
return path.first(Attribute);
}
function findInputBinding(
info: TemplateInfo, path: TemplateAstPath, binding: BoundDirectivePropertyAst): Symbol {
const element = path.first(ElementAst);
if (element) {
for (const directive of element.directives) {
const invertedInput = invertMap(directive.directive.inputs);
const fieldName = invertedInput[binding.templateName];
if (fieldName) {
const classSymbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
if (classSymbol) {
return classSymbol.members().get(fieldName);
}
}
}
}
}
function findOutputBinding(
info: TemplateInfo, path: TemplateAstPath, binding: BoundEventAst): Symbol {
const element = path.first(ElementAst);
if (element) {
for (const directive of element.directives) {
const invertedOutputs = invertMap(directive.directive.outputs);
const fieldName = invertedOutputs[binding.name];
if (fieldName) {
const classSymbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
if (classSymbol) {
return classSymbol.members().get(fieldName);
}
}
}
}
}
function invertMap(obj: {[name: string]: string}): {[name: string]: string} {
const result: {[name: string]: string} = {};
for (const name of Object.keys(obj)) {
const v = obj[name];
result[v] = name;
}
return result;
}
/**
* Wrap a symbol and change its kind to component.
*/
class OverrideKindSymbol implements Symbol {
constructor(private sym: Symbol, private kindOverride: string) {}
get name(): string { return this.sym.name; }
get kind(): string { return this.kindOverride; }
get language(): string { return this.sym.language; }
get type(): Symbol|undefined { return this.sym.type; }
get container(): Symbol|undefined { return this.sym.container; }
get public(): boolean { return this.sym.public; }
get callable(): boolean { return this.sym.callable; }
get definition(): Definition { return this.sym.definition; }
members() { return this.sym.members(); }
signatures() { return this.sym.signatures(); }
selectSignature(types: Symbol[]) { return this.sym.selectSignature(types); }
indexed(argument: Symbol) { return this.sym.indexed(argument); }
}

View File

@ -1,158 +0,0 @@
/**
* @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
*/
import {StaticReflectorHost, StaticSymbol} from '@angular/compiler';
import {MetadataCollector} from '@angular/tsc-wrapped/src/collector';
import {ModuleMetadata} from '@angular/tsc-wrapped/src/schema';
import * as path from 'path';
import * as ts from 'typescript';
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const DTS = /\.d\.ts$/;
let serialNumber = 0;
class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost {
private forceExists: string[] = [];
constructor(private host: ts.LanguageServiceHost) {
if (host.directoryExists)
this.directoryExists = directoryName => this.host.directoryExists(directoryName);
}
fileExists(fileName: string): boolean {
return !!this.host.getScriptSnapshot(fileName) || this.forceExists.indexOf(fileName) >= 0;
}
readFile(fileName: string): string {
let snapshot = this.host.getScriptSnapshot(fileName);
if (snapshot) {
return snapshot.getText(0, snapshot.getLength());
}
}
directoryExists: (directoryName: string) => boolean;
forceExist(fileName: string): void { this.forceExists.push(fileName); }
}
export class ReflectorHost implements StaticReflectorHost {
private metadataCollector: MetadataCollector;
private moduleResolverHost: ReflectorModuleModuleResolutionHost;
private _typeChecker: ts.TypeChecker;
private metadataCache = new Map<string, MetadataCacheEntry>();
constructor(
private getProgram: () => ts.Program, private serviceHost: ts.LanguageServiceHost,
private options: ts.CompilerOptions, private basePath: string) {
this.moduleResolverHost = new ReflectorModuleModuleResolutionHost(serviceHost);
this.metadataCollector = new MetadataCollector();
}
getCanonicalFileName(fileName: string): string { return fileName; }
private get program() { return this.getProgram(); }
public moduleNameToFileName(moduleName: string, containingFile: string) {
if (!containingFile || !containingFile.length) {
if (moduleName.indexOf('.') === 0) {
throw new Error('Resolution of relative paths requires a containing file.');
}
// Any containing file gives the same result for absolute imports
containingFile = this.getCanonicalFileName(path.join(this.basePath, 'index.ts'));
}
moduleName = moduleName.replace(EXT, '');
const resolved =
ts.resolveModuleName(moduleName, containingFile, this.options, this.moduleResolverHost)
.resolvedModule;
return resolved ? resolved.resolvedFileName : null;
};
/**
* We want a moduleId that will appear in import statements in the generated code.
* These need to be in a form that system.js can load, so absolute file paths don't work.
* Relativize the paths by checking candidate prefixes of the absolute path, to see if
* they are resolvable by the moduleResolution strategy from the CompilerHost.
*/
fileNameToModuleName(importedFile: string, containingFile: string) {
// TODO(tbosch): if a file does not yet exist (because we compile it later),
// we still need to create it so that the `resolve` method works!
if (!this.moduleResolverHost.fileExists(importedFile)) {
this.moduleResolverHost.forceExist(importedFile);
}
const parts = importedFile.replace(EXT, '').split(path.sep).filter(p => !!p);
for (let index = parts.length - 1; index >= 0; index--) {
let candidate = parts.slice(index, parts.length).join(path.sep);
if (this.moduleNameToFileName('.' + path.sep + candidate, containingFile) === importedFile) {
return `./${candidate}`;
}
if (this.moduleNameToFileName(candidate, containingFile) === importedFile) {
return candidate;
}
}
throw new Error(
`Unable to find any resolvable import for ${importedFile} relative to ${containingFile}`);
}
private get typeChecker(): ts.TypeChecker {
let result = this._typeChecker;
if (!result) {
result = this._typeChecker = this.program.getTypeChecker();
}
return result;
}
private typeCache = new Map<string, StaticSymbol>();
// TODO(alexeagle): take a statictype
getMetadataFor(filePath: string): ModuleMetadata[] {
if (!this.moduleResolverHost.fileExists(filePath)) {
throw new Error(`No such file '${filePath}'`);
}
if (DTS.test(filePath)) {
const metadataPath = filePath.replace(DTS, '.metadata.json');
if (this.moduleResolverHost.fileExists(metadataPath)) {
return this.readMetadata(metadataPath);
}
}
let sf = this.program.getSourceFile(filePath);
if (!sf) {
throw new Error(`Source file ${filePath} not present in program.`);
}
const entry = this.metadataCache.get(sf.path);
const version = this.serviceHost.getScriptVersion(sf.path);
if (entry && entry.version == version) {
if (!entry.content) return undefined;
return [entry.content];
}
const metadata = this.metadataCollector.getMetadata(sf);
this.metadataCache.set(sf.path, {version, content: metadata});
if (metadata) return [metadata];
}
readMetadata(filePath: string) {
try {
const text = this.moduleResolverHost.readFile(filePath);
const result = JSON.parse(text);
if (!Array.isArray(result)) return [result];
return result;
} catch (e) {
console.error(`Failed to read JSON file ${filePath}`);
throw e;
}
}
}
interface MetadataCacheEntry {
version: string;
content: ModuleMetadata;
}

View File

@ -1,151 +0,0 @@
/**
* @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
*/
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast';
import {AstPath} from './ast_path';
import {inSpan, isNarrower, spanOf} from './utils';
export class TemplateAstPath extends AstPath<TemplateAst> {
constructor(ast: TemplateAst[], public position: number, allowWidening: boolean = false) {
super(buildTemplatePath(ast, position, allowWidening));
}
}
function buildTemplatePath(
ast: TemplateAst[], position: number, allowWidening: boolean = false): TemplateAst[] {
const visitor = new TemplateAstPathBuilder(position, allowWidening);
templateVisitAll(visitor, ast);
return visitor.getPath();
}
export class NullTemplateVisitor implements TemplateAstVisitor {
visitNgContent(ast: NgContentAst): void {}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst): void {}
visitElement(ast: ElementAst): void {}
visitReference(ast: ReferenceAst): void {}
visitVariable(ast: VariableAst): void {}
visitEvent(ast: BoundEventAst): void {}
visitElementProperty(ast: BoundElementPropertyAst): void {}
visitAttr(ast: AttrAst): void {}
visitBoundText(ast: BoundTextAst): void {}
visitText(ast: TextAst): void {}
visitDirective(ast: DirectiveAst): void {}
visitDirectiveProperty(ast: BoundDirectivePropertyAst): void {}
}
export class TemplateAstChildVisitor implements TemplateAstVisitor {
constructor(private visitor?: TemplateAstVisitor) {}
// Nodes with children
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
return this.visitChildren(context, visit => {
visit(ast.attrs);
visit(ast.references);
visit(ast.variables);
visit(ast.directives);
visit(ast.providers);
visit(ast.children);
});
}
visitElement(ast: ElementAst, context: any): any {
return this.visitChildren(context, visit => {
visit(ast.attrs);
visit(ast.inputs);
visit(ast.outputs);
visit(ast.references);
visit(ast.directives);
visit(ast.providers);
visit(ast.children);
});
}
visitDirective(ast: DirectiveAst, context: any): any {
return this.visitChildren(context, visit => {
visit(ast.inputs);
visit(ast.hostProperties);
visit(ast.hostEvents);
});
}
// Terminal nodes
visitNgContent(ast: NgContentAst, context: any): any {}
visitReference(ast: ReferenceAst, context: any): any {}
visitVariable(ast: VariableAst, context: any): any {}
visitEvent(ast: BoundEventAst, context: any): any {}
visitElementProperty(ast: BoundElementPropertyAst, context: any): any {}
visitAttr(ast: AttrAst, context: any): any {}
visitBoundText(ast: BoundTextAst, context: any): any {}
visitText(ast: TextAst, context: any): any {}
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {}
protected visitChildren<T extends TemplateAst>(
context: any,
cb: (visit: (<V extends TemplateAst>(children: V[]|undefined) => void)) => void) {
const visitor = this.visitor || this;
let results: any[][] = [];
function visit<T extends TemplateAst>(children: T[] | undefined) {
if (children && children.length) results.push(templateVisitAll(visitor, children, context));
}
cb(visit);
return [].concat.apply([], results);
}
}
class TemplateAstPathBuilder extends TemplateAstChildVisitor {
private path: TemplateAst[] = [];
constructor(private position: number, private allowWidening: boolean) { super(); }
visit(ast: TemplateAst, context: any): any {
let span = spanOf(ast);
if (inSpan(this.position, span)) {
const len = this.path.length;
if (!len || this.allowWidening || isNarrower(span, spanOf(this.path[len - 1]))) {
this.path.push(ast);
}
} else {
// Returning a value here will result in the children being skipped.
return true;
}
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
return this.visitChildren(context, visit => {
// Ignore reference, variable and providers
visit(ast.attrs);
visit(ast.directives);
visit(ast.children);
});
}
visitElement(ast: ElementAst, context: any): any {
return this.visitChildren(context, visit => {
// Ingnore providers
visit(ast.attrs);
visit(ast.inputs);
visit(ast.outputs);
visit(ast.references);
visit(ast.directives);
visit(ast.children);
});
}
visitDirective(ast: DirectiveAst, context: any): any {
// Ignore the host properties of a directive
const result = this.visitChildren(context, visit => { visit(ast.inputs); });
// We never care about the diretive itself, just its inputs.
if (this.path[this.path.length - 1] == ast) {
this.path.pop();
}
return result;
}
getPath() { return this.path; }
}

View File

@ -1,77 +0,0 @@
/**
* @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
*/
import * as ts from 'typescript';
import {createLanguageService} from './language_service';
import {LanguageService, LanguageServiceHost} from './types';
import {TypeScriptServiceHost} from './typescript_host';
/** A plugin to TypeScript's langauge service that provide language services for
* templates in string literals.
*
* @experimental
*/
export class LanguageServicePlugin {
private ts: typeof ts;
private serviceHost: TypeScriptServiceHost;
private service: LanguageService;
private host: ts.LanguageServiceHost;
static 'extension-kind' = 'language-service';
constructor(config: {
ts: typeof ts; host: ts.LanguageServiceHost; service: ts.LanguageService;
registry?: ts.DocumentRegistry, args?: any
}) {
this.ts = config.ts;
this.host = config.host;
this.serviceHost = new TypeScriptServiceHost(this.ts, config.host, config.service);
this.service = createLanguageService(this.serviceHost);
this.serviceHost.setSite(this.service);
}
/**
* Augment the diagnostics reported by TypeScript with errors from the templates in string
* literals.
*/
getSemanticDiagnosticsFilter(fileName: string, previous: ts.Diagnostic[]): ts.Diagnostic[] {
let errors = this.service.getDiagnostics(fileName);
if (errors && errors.length) {
let file = this.serviceHost.getSourceFile(fileName);
for (const error of errors) {
previous.push({
file,
start: error.span.start,
length: error.span.end - error.span.start,
messageText: error.message,
category: this.ts.DiagnosticCategory.Error,
code: 0
});
}
}
return previous;
}
/**
* Get completions for angular templates if one is at the given position.
*/
getCompletionsAtPosition(fileName: string, position: number): ts.CompletionInfo {
let result = this.service.getCompletionsAt(fileName, position);
if (result) {
return {
isMemberCompletion: false,
isNewIdentifierLocation: false,
entries: result.map<ts.CompletionEntry>(
entry =>
({name: entry.name, kind: entry.kind, kindModifiers: '', sortText: entry.sort}))
};
}
}
}

View File

@ -1,706 +0,0 @@
/**
* @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
*/
import {CompileDirectiveMetadata, StaticSymbol} from '@angular/compiler';
import {NgAnalyzedModules} from '@angular/compiler/src/aot/compiler';
import {CompileMetadataResolver} from '@angular/compiler/src/metadata_resolver';
/**
* The range of a span of text in a source file.
*
* @experimental
*/
export interface Span {
/**
* The first code-point of the span as an offset relative to the beginning of the source assuming
* a UTF-16 encoding.
*/
start: number;
/**
* The first code-point after the span as an offset relative to the beginning of the source
* assuming a UTF-16 encoding.
*/
end: number;
}
/**
* The information `LanguageService` needs from the `LanguageServiceHost` to describe the content of
* a template and the
* langauge context the template is in.
*
* A host interface; see `LanguageSeriviceHost`.
*
* @experimental
*/
export interface TemplateSource {
/**
* The source of the template.
*/
readonly source: string;
/**
* The version of the source. As files are modified the version should change. That is, if the
* `LanguageSerivce` requesting
* template infomration for a source file and that file has changed since the last time the host
* was asked for the file then
* this version string should be different. No assumptions are made about the format of this
* string.
*
* The version can change more often than the source but should not change less often.
*/
readonly version: string;
/**
* The span of the template within the source file.
*/
readonly span: Span;
/**
* A static symbol for the template's component.
*/
readonly type: StaticSymbol;
/**
* The `SymbolTable` for the members of the component.
*/
readonly members: SymbolTable;
/**
* A `SymbolQuery` for the context of the template.
*/
readonly query: SymbolQuery;
}
/**
* A sequence of template sources.
*
* A host type; see `LanguageSeriviceHost`.
*
* @experimental
*/
export type TemplateSources = TemplateSource[] /* | undefined */;
/**
* Information about the component declarations.
*
* A file might contain a declaration without a template because the file contains only
* templateUrl references. However, the compoennt declaration might contain errors that
* need to be reported such as the template string is missing or the component is not
* declared in a module. These error should be reported on the declaration, not the
* template.
*
* A host type; see `LanguageSeriviceHost`.
*
* @experimental
*/
export interface Declaration {
/**
* The static symbol of the compponent being declared.
*/
readonly type: StaticSymbol;
/**
* The span of the declaration annotation reference (e.g. the 'Component' or 'Directive'
* reference).
*/
readonly declarationSpan: Span;
/**
* Reference to the compiler directive metadata for the declaration.
*/
readonly metadata?: CompileDirectiveMetadata;
/**
* Error reported trying to get the metadata.
*/
readonly error?: string;
}
/**
* A sequence of declarations.
*
* A host type; see `LanguageSeriviceHost`.
*
* @experimental
*/
export type Declarations = Declaration[];
/**
* An enumeration of basic types.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export enum BuiltinType {
/**
* The type is a type that can hold any other type.
*/
Any,
/**
* The type of a string literal.
*/
String,
/**
* The type of a numeric literal.
*/
Number,
/**
* The type of the `true` and `false` literals.
*/
Boolean,
/**
* The type of the `undefined` literal.
*/
Undefined,
/**
* the type of the `null` literal.
*/
Null,
/**
* Not a built-in type.
*/
Other
}
/**
* A symbol describing a language element that can be referenced by expressions
* in an Angular template.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export interface Symbol {
/**
* The name of the symbol as it would be referenced in an Angular expression.
*/
readonly name: string;
/**
* The kind of completion this symbol should generate if included.
*/
readonly kind: string;
/**
* The language of the source that defines the symbol. (e.g. typescript for TypeScript,
* ng-template for an Angular template, etc.)
*/
readonly language: string;
/**
* A symbol representing type of the symbol.
*/
readonly type: Symbol /* | undefined */;
/**
* A symbol for the container of this symbol. For example, if this is a method, the container
* is the class or interface of the method. If no container is appropriate, undefined is
* returned.
*/
readonly container: Symbol /* | undefined */;
/**
* The symbol is public in the container.
*/
readonly public: boolean;
/**
* `true` if the symbol can be the target of a call.
*/
readonly callable: boolean;
/**
* The location of the definition of the symbol
*/
readonly definition: Definition;
/**
* A table of the members of the symbol; that is, the members that can appear
* after a `.` in an Angular expression.
*
*/
members(): SymbolTable;
/**
* The list of overloaded signatures that can be used if the symbol is the
* target of a call.
*/
signatures(): Signature[];
/**
* Return which signature of returned by `signatures()` would be used selected
* given the `types` supplied. If no signature would match, this method should
* return `undefined`.
*/
selectSignature(types: Symbol[]): Signature /* | undefined */;
/**
* Return the type of the expression if this symbol is indexed by `argument`.
* If the symbol cannot be indexed, this method should return `undefined`.
*/
indexed(argument: Symbol): Symbol /* | undefined */;
}
/**
* A table of `Symbol`s accessible by name.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export interface SymbolTable {
/**
* The number of symbols in the table.
*/
readonly size: number;
/**
* Get the symbol corresponding to `key` or `undefined` if there is no symbol in the
* table by the name `key`.
*/
get(key: string): Symbol /* | undefined */;
/**
* Returns `true` if the table contains a `Symbol` with the name `key`.
*/
has(key: string): boolean;
/**
* Returns all the `Symbol`s in the table. The order should be, but is not required to be,
* in declaration order.
*/
values(): Symbol[];
}
/**
* A description of a function or method signature.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export interface Signature {
/**
* The arguments of the signture. The order of `argumetnts.symbols()` must be in the order
* of argument declaration.
*/
readonly arguments: SymbolTable;
/**
* The symbol of the signature result type.
*/
readonly result: Symbol;
}
/**
* Describes the language context in which an Angular expression is evaluated.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export interface SymbolQuery {
/**
* Return the built-in type this symbol represents or Other if it is not a built-in type.
*/
getTypeKind(symbol: Symbol): BuiltinType;
/**
* Return a symbol representing the given built-in type.
*/
getBuiltinType(kind: BuiltinType): Symbol;
/**
* Return the symbol for a type that represents the union of all the types given. Any value
* of one of the types given should be assignable to the returned type. If no one type can
* be constructed then this should be the Any type.
*/
getTypeUnion(...types: Symbol[]): Symbol;
/**
* Return a symbol for an array type that has the `type` as its element type.
*/
getArrayType(type: Symbol): Symbol;
/**
* Return element type symbol for an array type if the `type` is an array type. Otherwise return
* undefined.
*/
getElementType(type: Symbol): Symbol /* | undefined */;
/**
* Return a type that is the non-nullable version of the given type. If `type` is already
* non-nullable, return `type`.
*/
getNonNullableType(type: Symbol): Symbol;
/**
* Return a symbol table for the pipes that are in scope.
*/
getPipes(): SymbolTable;
/**
* Return the type symbol for the given static symbol.
*/
getTypeSymbol(type: StaticSymbol): Symbol;
/**
* Return the members that are in the context of a type's template reference.
*/
getTemplateContext(type: StaticSymbol): SymbolTable;
/**
* Produce a symbol table with the given symbols. Used to produce a symbol table
* for use with mergeSymbolTables().
*/
createSymbolTable(symbols: SymbolDeclaration[]): SymbolTable;
/**
* Produce a merged symbol table. If the symbol tables contain duplicate entries
* the entries of the latter symbol tables will obscure the entries in the prior
* symbol tables.
*
* The symbol tables passed to this routine MUST be produces by the same instance
* of SymbolQuery that is being called.
*/
mergeSymbolTable(symbolTables: SymbolTable[]): SymbolTable;
/**
* Return the span of the narrowest non-token node at the given location.
*/
getSpanAt(line: number, column: number): Span /* | undefined */;
}
/**
* The host for a `LanguageService`. This provides all the `LanguageSerivce` requires to respond to
* the `LanguageService` requests.
*
* This interface describes the requirements of the `LanguageService` on its host.
*
* The host interface is host language agnostic.
*
* Adding optional member to this interface or any interface that is described as a
* `LanguageSerivceHost`
* interface is not considered a breaking change as defined by SemVer. Removing a method or changing
* a
* member from required to optional will also not be considered a breaking change.
*
* If a member is deprecated it will be changed to optional in a minor release before it is removed
* in
* a major release.
*
* Adding a required member or changing a method's parameters, is considered a breaking change and
* will
* only be done when breaking changes are allowed. When possible, a new optional member will be
* added and
* the old member will be deprecated. The new member will then be made required in and the old
* member will
* be removed only when breaking chnages are allowed.
*
* While an interface is marked as experimental breaking-changes will be allowed between minor
* releases.
* After an interface is marked as stable breaking-changes will only be allowed between major
* releases.
* No breaking changes are allowed between patch releases.
*
* @experimental
*/
export interface LanguageServiceHost {
/**
* The resolver to use to find compiler metadata.
*/
readonly resolver: CompileMetadataResolver;
/**
* Returns the template information for templates in `fileName` at the given location. If
* `fileName`
* refers to a template file then the `position` should be ignored. If the `position` is not in a
* template literal string then this method should return `undefined`.
*/
getTemplateAt(fileName: string, position: number): TemplateSource /* |undefined */;
/**
* Return the template source information for all templates in `fileName` or for `fileName` if it
* is
* a template file.
*/
getTemplates(fileName: string): TemplateSources;
/**
* Returns the Angular declarations in the given file.
*/
getDeclarations(fileName: string): Declarations;
/**
* Return a summary of all Angular modules in the project.
*/
getAnalyzedModules(): NgAnalyzedModules;
/**
* Return a list all the template files referenced by the project.
*/
getTemplateReferences(): string[];
}
/**
* The kinds of completions generated by the language service.
*
* A 'LanguageService' interface.
*
* @experimental
*/
export type CompletionKind = 'attribute' | 'html attribute' | 'component' | 'element' | 'entity' |
'key' | 'method' | 'pipe' | 'property' | 'type' | 'reference' | 'variable';
/**
* An item of the completion result to be displayed by an editor.
*
* A `LanguageService` interface.
*
* @experimental
*/
export interface Completion {
/**
* The kind of comletion.
*/
kind: CompletionKind;
/**
* The name of the completion to be displayed
*/
name: string;
/**
* The key to use to sort the completions for display.
*/
sort: string;
}
/**
* A sequence of completions.
*
* @experimental
*/
export type Completions = Completion[] /* | undefined */;
/**
* A file and span.
*/
export interface Location {
fileName: string;
span: Span;
}
/**
* A defnition location(s).
*/
export type Definition = Location[] /* | undefined */;
/**
* The kind of diagnostic message.
*
* @experimental
*/
export enum DiagnosticKind {
Error,
Warning,
}
/**
* An template diagnostic message to display.
*
* @experimental
*/
export interface Diagnostic {
/**
* The kind of diagnostic message
*/
kind: DiagnosticKind;
/**
* The source span that should be highlighted.
*/
span: Span;
/**
* The text of the diagnostic message to display.
*/
message: string;
}
/**
* A sequence of diagnostic message.
*
* @experimental
*/
export type Diagnostics = Diagnostic[];
/**
* Information about the pipes that are available for use in a template.
*
* A `LanguageService` interface.
*
* @experimental
*/
export interface PipeInfo {
/**
* The name of the pipe.
*/
name: string;
/**
* The static symbol for the pipe's constructor.
*/
symbol: StaticSymbol;
}
/**
* A sequence of pipe information.
*
* @experimental
*/
export type Pipes = PipeInfo[] /* | undefined */;
/**
* Describes a symbol to type binding used to build a symbol table.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export interface SymbolDeclaration {
/**
* The name of the symbol in table.
*/
readonly name: string;
/**
* The kind of symbol to declare.
*/
readonly kind: CompletionKind;
/**
* Type of the symbol. The type symbol should refer to a symbol for a type.
*/
readonly type: Symbol;
/**
* The definion of the symbol if one exists.
*/
readonly definition?: Definition;
}
/**
* A section of hover text. If the text is code then langauge should be provided.
* Otherwise the text is assumed to be Markdown text that will be sanitized.
*/
export interface HoverTextSection {
/**
* Source code or markdown text describing the symbol a the hover location.
*/
readonly text: string;
/**
* The langauge of the source if `text` is a souce code fragment.
*/
readonly language?: string;
}
/**
* Hover infomration for a symbol at the hover location.
*/
export interface Hover {
/**
* The hover text to display for the symbol at the hover location. If the text includes
* source code, the section will specify which langauge it should be interpreted as.
*/
readonly text: HoverTextSection[];
/**
* The span of source the hover covers.
*/
readonly span: Span;
}
/**
* An instance of an Angular language service created by `createLanguageService()`.
*
* The language service returns information about Angular templates that are included in a project
* as
* defined by the `LanguageServiceHost`.
*
* When a method expects a `fileName` this file can either be source file in the project that
* contains
* a template in a string literal or a template file referenced by the project returned by
* `getTemplateReference()`. All other files will cause the method to return `undefined`.
*
* If a method takes a `position`, it is the offset of the UTF-16 code-point relative to the
* beginning
* of the file reference by `fileName`.
*
* This interface and all interfaces and types marked as `LanguageSerivce` types, describe a
* particlar
* implementation of the Angular language service and is not intented to be implemented. Adding
* members
* to the interface will not be considered a breaking change as defined by SemVer.
*
* Removing a member or making a member optional, changing a method parameters, or changing a
* member's
* type will all be considered a breaking change.
*
* While an interface is marked as experimental breaking-changes will be allowed between minor
* releases.
* After an interface is marked as stable breaking-changes will only be allowed between major
* releases.
* No breaking changes are allowed between patch releases.
*
* @experimental
*/
export interface LanguageService {
/**
* Returns a list of all the external templates referenced by the project.
*/
getTemplateReferences(): string[] /* | undefined */;
/**
* Returns a list of all error for all templates in the given file.
*/
getDiagnostics(fileName: string): Diagnostics /* | undefined */;
/**
* Return the completions at the given position.
*/
getCompletionsAt(fileName: string, position: number): Completions /* | undefined */;
/**
* Return the definition location for the symbol at position.
*/
getDefinitionAt(fileName: string, position: number): Definition /* | undefined */;
/**
* Return the hover information for the symbol at position.
*/
getHoverAt(fileName: string, position: number): Hover /* | undefined */;
/**
* Return the pipes that are available at the given position.
*/
getPipesAt(fileName: string, position: number): Pipes /* | undefined */;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,99 +0,0 @@
/**
* @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
*/
import {CompileDirectiveSummary, CompileTypeMetadata} from '@angular/compiler';
import {ParseSourceSpan} from '@angular/compiler/src/parse_util';
import {CssSelector, SelectorMatcher} from '@angular/compiler/src/selector';
import {SelectorInfo, TemplateInfo} from './common';
import {Span} from './types';
export interface SpanHolder {
sourceSpan: ParseSourceSpan;
endSourceSpan?: ParseSourceSpan;
children?: SpanHolder[];
}
export function isParseSourceSpan(value: any): value is ParseSourceSpan {
return value && !!value.start;
}
export function spanOf(span?: SpanHolder | ParseSourceSpan): Span {
if (!span) return undefined;
if (isParseSourceSpan(span)) {
return {start: span.start.offset, end: span.end.offset};
} else {
if (span.endSourceSpan) {
return {start: span.sourceSpan.start.offset, end: span.endSourceSpan.end.offset};
} else if (span.children && span.children.length) {
return {
start: span.sourceSpan.start.offset,
end: spanOf(span.children[span.children.length - 1]).end
};
}
return {start: span.sourceSpan.start.offset, end: span.sourceSpan.end.offset};
}
}
export function inSpan(position: number, span?: Span, exclusive?: boolean): boolean {
return span && exclusive ? position >= span.start && position < span.end :
position >= span.start && position <= span.end;
}
export function offsetSpan(span: Span, amount: number): Span {
return {start: span.start + amount, end: span.end + amount};
}
export function isNarrower(spanA: Span, spanB: Span): boolean {
return spanA.start >= spanB.start && spanA.end <= spanB.end;
}
export function hasTemplateReference(type: CompileTypeMetadata): boolean {
if (type.diDeps) {
for (let diDep of type.diDeps) {
if (diDep.token.identifier && diDep.token.identifier.name == 'TemplateRef') return true;
}
}
return false;
}
export function getSelectors(info: TemplateInfo): SelectorInfo {
const map = new Map<CssSelector, CompileDirectiveSummary>();
const selectors = flatten(info.directives.map(directive => {
const selectors = CssSelector.parse(directive.selector);
selectors.forEach(selector => map.set(selector, directive));
return selectors;
}));
return {selectors, map};
}
export function flatten<T>(a: T[][]) {
return (<T[]>[]).concat(...a);
}
export function removeSuffix(value: string, suffix: string) {
if (value.endsWith(suffix)) return value.substring(0, value.length - suffix.length);
return value;
}
export function uniqueByName < T extends {
name: string;
}
> (elements: T[] | undefined): T[]|undefined {
if (elements) {
const result: T[] = [];
const set = new Set<string>();
for (const element of elements) {
if (!set.has(element.name)) {
set.add(element.name);
result.push(element);
}
}
return result;
}
}

View File

@ -1,236 +0,0 @@
/**
* @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
*/
import 'reflect-metadata';
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Completions, Diagnostic, Diagnostics} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost, includeDiagnostic, noDiagnostics} from './test_utils';
describe('completions', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to get entity completions',
() => { contains('/app/test.ng', 'entity-amp', '&amp;', '&gt;', '&lt;', '&iota;'); });
it('should be able to return html elements', () => {
let htmlTags = ['h1', 'h2', 'div', 'span'];
let locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
for (let location of locations) {
contains('/app/test.ng', location, ...htmlTags);
}
});
it('should be able to return element diretives',
() => { contains('/app/test.ng', 'empty', 'my-app'); });
it('should be able to return h1 attributes',
() => { contains('/app/test.ng', 'h1-after-space', 'id', 'dir', 'lang', 'onclick'); });
it('should be able to find common angular attributes',
() => { contains('/app/test.ng', 'div-attributes', '(click)', '[ngClass]'); });
it('should be able to get completions in some random garbage', () => {
const fileName = '/app/test.ng';
mockHost.override(fileName, ' > {{tle<\n {{retl ><bel/beled}}di>\n la</b </d &a ');
expect(() => ngService.getCompletionsAt(fileName, 31)).not.toThrow();
mockHost.override(fileName, undefined);
});
it('should be able to infer the type of a ngForOf', () => {
addCode(
`
interface Person {
name: string,
street: string
}
@Component({template: '<div *ngFor="let person of people">{{person.~{name}name}}</div'})
export class MyComponent {
people: Person[]
}`,
() => { contains('/app/app.component.ts', 'name', 'name', 'street'); });
});
it('should be able to infer the type of a ngForOf with an async pipe', () => {
addCode(
`
interface Person {
name: string,
street: string
}
@Component({template: '<div *ngFor="let person of people | async">{{person.~{name}name}}</div'})
export class MyComponent {
people: Promise<Person[]>;
}`,
() => { contains('/app/app.component.ts', 'name', 'name', 'street'); });
});
it('should be able to complete every character in the file', () => {
const fileName = '/app/test.ng';
expect(() => {
let chance = 0.05;
let requests = 0;
function tryCompletionsAt(position: number) {
try {
if (Math.random() < chance) {
ngService.getCompletionsAt(fileName, position);
requests++;
}
} catch (e) {
// Emit enough diagnostic information to reproduce the error.
console.log(
`Position: ${position}\nContent: "${mockHost.getFileContent(fileName)}"\nStack:\n${e.stack}`);
throw e;
}
}
try {
const originalContent = mockHost.getFileContent(fileName);
// For each character in the file, add it to the file and request a completion after it.
for (let index = 0, len = originalContent.length; index < len; index++) {
const content = originalContent.substr(0, index);
mockHost.override(fileName, content);
tryCompletionsAt(index);
}
// For the complete file, try to get a completion at every character.
mockHost.override(fileName, originalContent);
for (let index = 0, len = originalContent.length; index < len; index++) {
tryCompletionsAt(index);
}
// Delete random characters in the file until we get an empty file.
let content = originalContent;
while (content.length > 0) {
const deleteIndex = Math.floor(Math.random() * content.length);
content = content.slice(0, deleteIndex - 1) + content.slice(deleteIndex + 1);
mockHost.override(fileName, content);
const requestIndex = Math.floor(Math.random() * content.length);
tryCompletionsAt(requestIndex);
}
// Build up the string from zero asking for a completion after every char
buildUp(originalContent, (text, position) => {
mockHost.override(fileName, text);
tryCompletionsAt(position);
});
} finally {
mockHost.override(fileName, undefined);
}
}).not.toThrow();
});
describe('with regression tests', () => {
it('should not crash with an incomplete component', () => {
expect(() => {
const code = `
@Component({
template: '~{inside-template}'
})
export class MyComponent {
}`;
addCode(code, fileName => { contains(fileName, 'inside-template', 'h1'); });
}).not.toThrow();
});
it('should hot crash with an incomplete class', () => {
expect(() => {
addCode('\nexport class', fileName => { ngHost.updateAnalyzedModules(); });
}).not.toThrow();
});
});
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
function contains(fileName: string, locationMarker: string, ...names: string[]) {
let location = mockHost.getMarkerLocations(fileName)[locationMarker];
if (location == null) {
throw new Error(`No marker ${locationMarker} found.`);
}
expectEntries(locationMarker, ngService.getCompletionsAt(fileName, location), ...names);
}
});
function expectEntries(locationMarker: string, completions: Completions, ...names: string[]) {
let entries: {[name: string]: boolean} = {};
if (!completions) {
throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`);
}
if (!completions.length) {
throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} an empty result provided`);
} else {
for (let entry of completions) {
entries[entry.name] = true;
}
let missing = names.filter(name => !entries[name]);
if (missing.length) {
throw new Error(
`Expected result from ${locationMarker} to include at least one of the following, ${missing.join(', ')}, in the list of entries ${completions.map(entry => entry.name).join(', ')}`);
}
}
}
function buildUp(originalText: string, cb: (text: string, position: number) => void) {
let count = originalText.length;
let inString: boolean[] = (new Array(count)).fill(false);
let unused: number[] = (new Array(count)).fill(1).map((v, i) => i);
function getText() {
return new Array(count)
.fill(1)
.map((v, i) => i)
.filter(i => inString[i])
.map(i => originalText[i])
.join('');
}
function randomUnusedIndex() { return Math.floor(Math.random() * unused.length); }
while (unused.length > 0) {
let unusedIndex = randomUnusedIndex();
let index = unused[unusedIndex];
if (index == null) throw new Error('Internal test buildup error');
if (inString[index]) throw new Error('Internal test buildup error');
inString[index] = true;
unused.splice(unusedIndex, 1);
let text = getText();
let position =
inString.filter((_, i) => i <= index).map(v => v ? 1 : 0).reduce((p, v) => p + v, 0);
cb(text, position);
}
}

View File

@ -1,169 +0,0 @@
/**
* @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
*/
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Completions, Diagnostic, Diagnostics, Span} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost,} from './test_utils';
describe('definitions', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to find field in an interpolation', () => {
localReference(
` @Component({template: '{{«name»}}'}) export class MyComponent { «∆name∆: string;» }`);
});
it('should be able to find a field in a attribute reference', () => {
localReference(
` @Component({template: '<input [(ngModel)]="«name»">'}) export class MyComponent { «∆name∆: string;» }`);
});
it('should be able to find a method from a call', () => {
localReference(
` @Component({template: '<div (click)="«myClick»();"></div>'}) export class MyComponent { «∆myClick∆() { }»}`);
});
it('should be able to find a field reference in an *ngIf', () => {
localReference(
` @Component({template: '<div *ngIf="«include»"></div>'}) export class MyComponent { «∆include∆ = true;»}`);
});
it('should be able to find a reference to a component', () => {
reference(
'parsing-cases.ts',
` @Component({template: '<«test-comp»></test-comp>'}) export class MyComponent { }`);
});
it('should be able to find an event provider', () => {
reference(
'/app/parsing-cases.ts', 'test',
` @Component({template: '<test-comp («test»)="myHandler()"></div>'}) export class MyComponent { myHandler() {} }`);
});
it('should be able to find an input provider', () => {
reference(
'/app/parsing-cases.ts', 'tcName',
` @Component({template: '<test-comp [«tcName»]="name"></div>'}) export class MyComponent { name = 'my name'; }`);
});
it('should be able to find a pipe', () => {
reference(
'async_pipe.d.ts',
` @Component({template: '<div *ngIf="input | «async»"></div>'}) export class MyComponent { input: EventEmitter; }`);
});
function localReference(code: string) {
addCode(code, fileName => {
const refResult = mockHost.getReferenceMarkers(fileName);
for (const name in refResult.references) {
const references = refResult.references[name];
const definitions = refResult.definitions[name];
expect(definitions).toBeDefined(); // If this fails the test data is wrong.
for (const reference of references) {
const definition = ngService.getDefinitionAt(fileName, reference.start);
if (definition) {
definition.forEach(d => expect(d.fileName).toEqual(fileName));
const match = matchingSpan(definition.map(d => d.span), definitions);
if (!match) {
throw new Error(
`Expected one of ${stringifySpans(definition.map(d => d.span))} to match one of ${stringifySpans(definitions)}`);
}
} else {
throw new Error('Expected a definition');
}
}
}
});
}
function reference(referencedFile: string, code: string): void;
function reference(referencedFile: string, span: Span, code: string): void;
function reference(referencedFile: string, definition: string, code: string): void;
function reference(referencedFile: string, p1?: any, p2?: any): void {
const code: string = p2 ? p2 : p1;
const definition: string = p2 ? p1 : undefined;
let span: Span = p2 && p1.start != null ? p1 : undefined;
if (definition && !span) {
const referencedFileMarkers = mockHost.getReferenceMarkers(referencedFile);
expect(referencedFileMarkers).toBeDefined(); // If this fails the test data is wrong.
const spans = referencedFileMarkers.definitions[definition];
expect(spans).toBeDefined(); // If this fails the test data is wrong.
span = spans[0];
}
addCode(code, fileName => {
const refResult = mockHost.getReferenceMarkers(fileName);
let tests = 0;
for (const name in refResult.references) {
const references = refResult.references[name];
expect(reference).toBeDefined(); // If this fails the test data is wrong.
for (const reference of references) {
tests++;
const definition = ngService.getDefinitionAt(fileName, reference.start);
if (definition) {
definition.forEach(d => {
if (d.fileName.indexOf(referencedFile) < 0) {
throw new Error(
`Expected reference to file ${referencedFile}, received ${d.fileName}`);
}
if (span) {
expect(d.span).toEqual(span);
}
});
} else {
throw new Error('Expected a definition');
}
}
}
if (!tests) {
throw new Error('Expected at least one reference (test data error)');
}
});
}
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
});
function matchingSpan(aSpans: Span[], bSpans: Span[]): Span {
for (const a of aSpans) {
for (const b of bSpans) {
if (a.start == b.start && a.end == b.end) {
return a;
}
}
}
}
function stringifySpan(span: Span) {
return span ? `(${span.start}-${span.end})` : '<undefined>';
}
function stringifySpans(spans: Span[]) {
return spans ? `[${spans.map(stringifySpan).join(', ')}]` : '<empty>';
}

View File

@ -1,136 +0,0 @@
/**
* @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
*/
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Completions, Diagnostic, Diagnostics} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost, includeDiagnostic, noDiagnostics} from './test_utils';
describe('diagnostics', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be no diagnostics for test.ng',
() => { expect(ngService.getDiagnostics('/app/test.ng')).toEqual([]); });
describe('for semantic errors', () => {
const fileName = '/app/test.ng';
function diagnostics(template: string): Diagnostics {
try {
mockHost.override(fileName, template);
return ngService.getDiagnostics(fileName);
} finally {
mockHost.override(fileName, undefined);
}
}
function accept(template: string) { noDiagnostics(diagnostics(template)); }
function reject(template: string, message: string): void;
function reject(template: string, message: string, at: string): void;
function reject(template: string, message: string, location: string): void;
function reject(template: string, message: string, location: string, len: number): void;
function reject(template: string, message: string, at?: number | string, len?: number): void {
if (typeof at == 'string') {
len = at.length;
at = template.indexOf(at);
}
includeDiagnostic(diagnostics(template), message, at, len);
}
describe('with $event', () => {
it('should accept an event',
() => { accept('<div (click)="myClick($event)">Click me!</div>'); });
it('should reject it when not in an event binding', () => {
reject('<div [tabIndex]="$event"></div>', '\'$event\' is not defined', '$event');
});
});
});
describe('with regression tests', () => {
it('should not crash with a incomplete *ngFor', () => {
expect(() => {
const code =
'\n@Component({template: \'<div *ngFor></div> ~{after-div}\'}) export class MyComponent {}';
addCode(code, fileName => { ngService.getDiagnostics(fileName); });
}).not.toThrow();
});
it('should report a component not in a module', () => {
const code = '\n@Component({template: \'<div></div>\'}) export class MyComponent {}';
addCode(code, (fileName, content) => {
const diagnostics = ngService.getDiagnostics(fileName);
const offset = content.lastIndexOf('@Component') + 1;
const len = 'Component'.length;
includeDiagnostic(
diagnostics, 'Component \'MyComponent\' is not included in a module', offset, len);
});
});
it('should not report an error for a form\'s host directives', () => {
const code = '\n@Component({template: \'<form></form>\'}) export class MyComponent {}';
addCode(code, (fileName, content) => {
const diagnostics = ngService.getDiagnostics(fileName);
onlyModuleDiagnostics(diagnostics);
});
});
it('should not throw getting diagnostics for an index expression', () => {
const code =
` @Component({template: '<a *ngIf="(auth.isAdmin | async) || (event.leads && event.leads[(auth.uid | async)])"></a>'}) export class MyComponent {}`;
addCode(
code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
});
it('should not throw using a directive with no value', () => {
const code =
` @Component({template: '<form><input [(ngModel)]="name" required /></form>'}) export class MyComponent { name = 'some name'; }`;
addCode(
code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
});
it('should report an error for invalid metadata', () => {
const code =
` @Component({template: '', provider: [{provide: 'foo', useFactor: () => 'foo' }]}) export class MyComponent { name = 'some name'; }`;
addCode(code, (fileName, content) => {
const diagnostics = ngService.getDiagnostics(fileName);
includeDiagnostic(
diagnostics, 'Function calls are not supported.', '() => \'foo\'', content);
});
});
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
function onlyModuleDiagnostics(diagnostics: Diagnostics) {
// Expect only the 'MyComponent' diagnostic
expect(diagnostics.length).toBe(1);
expect(diagnostics[0].message.indexOf('MyComponent') >= 0).toBeTruthy();
}
});
});

View File

@ -1,105 +0,0 @@
/**
* @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
*/
import 'reflect-metadata';
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Hover, HoverTextSection} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost, includeDiagnostic, noDiagnostics} from './test_utils';
describe('hover', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to find field in an interpolation', () => {
hover(
` @Component({template: '{{«name»}}'}) export class MyComponent { name: string; }`,
'property name of MyComponent');
});
it('should be able to find a field in a attribute reference', () => {
hover(
` @Component({template: '<input [(ngModel)]="«name»">'}) export class MyComponent { name: string; }`,
'property name of MyComponent');
});
it('should be able to find a method from a call', () => {
hover(
` @Component({template: '<div (click)="«∆myClick∆()»;"></div>'}) export class MyComponent { myClick() { }}`,
'method myClick of MyComponent');
});
it('should be able to find a field reference in an *ngIf', () => {
hover(
` @Component({template: '<div *ngIf="«include»"></div>'}) export class MyComponent { include = true;}`,
'property include of MyComponent');
});
it('should be able to find a reference to a component', () => {
hover(
` @Component({template: '«<∆test∆-comp></test-comp>»'}) export class MyComponent { }`,
'component TestComponent');
});
it('should be able to find an event provider', () => {
hover(
` @Component({template: '<test-comp «(∆test∆)="myHandler()"»></div>'}) export class MyComponent { myHandler() {} }`,
'event testEvent of TestComponent');
});
it('should be able to find an input provider', () => {
hover(
` @Component({template: '<test-comp «[∆tcName∆]="name"»></div>'}) export class MyComponent { name = 'my name'; }`,
'property name of TestComponent');
});
function hover(code: string, hoverText: string) {
addCode(code, fileName => {
let tests = 0;
const markers = mockHost.getReferenceMarkers(fileName);
const keys = Object.keys(markers.references).concat(Object.keys(markers.definitions));
for (const referenceName of keys) {
const references = (markers.references[referenceName] ||
[]).concat(markers.definitions[referenceName] || []);
for (const reference of references) {
tests++;
const hover = ngService.getHoverAt(fileName, reference.start);
if (!hover) throw new Error(`Expected a hover at location ${reference.start}`);
expect(hover.span).toEqual(reference);
expect(toText(hover)).toEqual(hoverText);
}
}
expect(tests).toBeGreaterThan(0); // If this fails the test is wrong.
});
}
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
function toText(hover: Hover): string { return hover.text.map(h => h.text).join(''); }
});

View File

@ -1,52 +0,0 @@
/**
* @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
*/
import {DomElementSchemaRegistry} from '@angular/compiler';
import {SchemaInformation} from '../src/html_info';
describe('html_info', () => {
const domRegistry = new DomElementSchemaRegistry();
it('should have the same elements as the dom registry', () => {
// If this test fails, replace the SCHEMA constant in html_info with the one
// from dom_element_schema_registry and also verify the code to interpret
// the schema is the same.
const domElements = domRegistry.allKnownElementNames();
const infoElements = SchemaInformation.instance.allKnownElements();
const uniqueToDom = uniqueElements(infoElements, domElements);
const uniqueToInfo = uniqueElements(domElements, infoElements);
expect(uniqueToDom).toEqual([]);
expect(uniqueToInfo).toEqual([]);
});
it('should have at least a sub-set of properties', () => {
const elements = SchemaInformation.instance.allKnownElements();
for (const element of elements) {
for (const prop of SchemaInformation.instance.propertiesOf(element)) {
expect(domRegistry.hasProperty(element, prop, []));
}
}
});
});
function uniqueElements<T>(a: T[], b: T[]): T[] {
const s = new Set<T>();
for (const aItem of a) {
s.add(aItem);
}
const result: T[] = [];
const reported = new Set<T>();
for (const bItem of b) {
if (!s.has(bItem) && !reported.has(bItem)) {
reported.add(bItem);
result.push(bItem);
}
}
return result;
}

View File

@ -1,32 +0,0 @@
/**
* @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
*/
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Completions, Diagnostic, Diagnostics} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost} from './test_utils';
describe('references', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to get template references',
() => { expect(() => ngService.getTemplateReferences()).not.toThrow(); });
it('should be able to determine that test.ng is a template reference',
() => { expect(ngService.getTemplateReferences()).toContain('/app/test.ng'); });
});

View File

@ -1,231 +0,0 @@
/**
* @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
*/
import {MockData} from './test_utils';
export const toh = {
app: {
'app.component.ts': `import { Component } from '@angular/core';
export class Hero {
id: number;
name: string;
}
@Component({
selector: 'my-app',
template: \`~{empty}
<~{start-tag}h~{start-tag-after-h}1~{start-tag-h1} ~{h1-after-space}>~{h1-content} {{~{sub-start}title~{sub-end}}}</h1>
~{after-h1}<h2>{{~{h2-hero}hero.~{h2-name}name}} details!</h2>
<div><label>id: </label>{{~{label-hero}hero.~{label-id}id}}</div>
<div ~{div-attributes}>
<label>name: </label>
</div>
&~{entity-amp}amp;
\`
})
export class AppComponent {
title = 'Tour of Heroes';
hero: Hero = {
id: 1,
name: 'Windstorm'
};
private internal: string;
}`,
'main.ts': `
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { CaseIncompleteOpen, CaseMissingClosing, CaseUnknown, Pipes, TemplateReference, NoValueAttribute,
AttributeBinding, StringModel,PropertyBinding, EventBinding, TwoWayBinding, EmptyInterpolation,
ForOfEmpty, ForLetIEqual, ForOfLetEmpty, ForUsingComponent, References, TestComponent} from './parsing-cases';
import { WrongFieldReference, WrongSubFieldReference, PrivateReference, ExpectNumericType, LowercasePipe } from './expression-cases';
import { UnknownPeople, UnknownEven, UnknownTrackBy } from './ng-for-cases';
import { ShowIf } from './ng-if-cases';
@NgModule({
imports: [CommonModule, FormsModule],
declarations: [AppComponent, CaseIncompleteOpen, CaseMissingClosing, CaseUnknown, Pipes, TemplateReference, NoValueAttribute,
AttributeBinding, StringModel, PropertyBinding, EventBinding, TwoWayBinding, EmptyInterpolation, ForOfEmpty, ForOfLetEmpty,
ForLetIEqual, ForUsingComponent, References, TestComponent, WrongFieldReference, WrongSubFieldReference, PrivateReference,
ExpectNumericType, UnknownPeople, UnknownEven, UnknownTrackBy, ShowIf, LowercasePipe]
})
export class AppModule {}
declare function bootstrap(v: any): void;
bootstrap(AppComponent);
`,
'parsing-cases.ts': `
import {Component, Directive, Input, Output, EventEmitter} from '@angular/core';
import {Hero} from './app.component';
@Component({template: '<h1>Some <~{incomplete-open-lt}a~{incomplete-open-a} ~{incomplete-open-attr} text</h1>'})
export class CaseIncompleteOpen {}
@Component({template: '<h1>Some <a> ~{missing-closing} text</h1>'})
export class CaseMissingClosing {}
@Component({template: '<h1>Some <unknown ~{unknown-element}> text</h1>'})
export class CaseUnknown {}
@Component({template: '<h1>{{data | ~{before-pipe}lowe~{in-pipe}rcase~{after-pipe} }}'})
export class Pipes {
data = 'Some string';
}
@Component({template: '<h1 h~{no-value-attribute}></h1>'})
export class NoValueAttribute {}
@Component({template: '<h1 model="~{attribute-binding-model}test"></h1>'})
export class AttributeBinding {
test: string;
}
@Component({template: '<h1 [model]="~{property-binding-model}test"></h1>'})
export class PropertyBinding {
test: string;
}
@Component({template: '<h1 (model)="~{event-binding-model}modelChanged()"></h1>'})
export class EventBinding {
test: string;
modelChanged() {}
}
@Component({template: '<h1 [(model)]="~{two-way-binding-model}test"></h1>'})
export class TwoWayBinding {
test: string;
}
@Directive({selector: '[string-model]'})
export class StringModel {
@Input() model: string;
@Output() modelChanged: EventEmitter<string>;
}
interface Person {
name: string;
age: number
}
@Component({template: '<div *ngFor="~{for-empty}"></div>'})
export class ForOfEmpty {}
@Component({template: '<div *ngFor="let ~{for-let-empty}"></div>'})
export class ForOfLetEmpty {}
@Component({template: '<div *ngFor="let i = ~{for-let-i-equal}"></div>'})
export class ForLetIEqual {}
@Component({template: '<div *ngFor="~{for-let}let ~{for-person}person ~{for-of}of ~{for-people}people"> <span>Name: {{~{for-interp-person}person.~{for-interp-name}name}}</span><span>Age: {{person.~{for-interp-age}age}}</span></div>'})
export class ForUsingComponent {
people: Person[];
}
@Component({template: '<div #div> <test-comp #test1> {{~{test-comp-content}}} {{test1.~{test-comp-after-test}name}} {{div.~{test-comp-after-div}.innerText}} </test-comp> </div> <test-comp #test2></test-comp>'})
export class References {}
@Component({selector: 'test-comp', template: '<div>Testing: {{name}}</div>'})
export class TestComponent {
«@Input('∆tcName∆') name = 'test';»
«@Output('∆test∆') testEvent = new EventEmitter();»
}
@Component({templateUrl: 'test.ng'})
export class TemplateReference {
title = 'Some title';
hero: Hero = {
id: 1,
name: 'Windstorm'
};
myClick(event: any) {
}
}
@Component({template: '{{~{empty-interpolation}}}'})
export class EmptyInterpolation {
title = 'Some title';
subTitle = 'Some sub title';
}
`,
'expression-cases.ts': `
import {Component} from '@angular/core';
export interface Person {
name: string;
age: number;
}
@Component({template: '{{~{foo}foo~{foo-end}}}'})
export class WrongFieldReference {
bar = 'bar';
}
@Component({template: '{{~{nam}person.nam~{nam-end}}}'})
export class WrongSubFieldReference {
person: Person = { name: 'Bob', age: 23 };
}
@Component({template: '{{~{myField}myField~{myField-end}}}'})
export class PrivateReference {
private myField = 'My Field';
}
@Component({template: '{{~{mod}"a" ~{mod-end}% 2}}'})
export class ExpectNumericType {}
@Component({template: '{{ (name | lowercase).~{string-pipe}substring }}'})
export class LowercasePipe {
name: string;
}
`,
'ng-for-cases.ts': `
import {Component} from '@angular/core';
export interface Person {
name: string;
age: number;
}
@Component({template: '<div *ngFor="let person of ~{people_1}people_1~{people_1-end}"> <span>{{person.name}}</span> </div>'})
export class UnknownPeople {}
@Component({template: '<div ~{even_1}*ngFor="let person of people; let e = even_1"~{even_1-end}><span>{{person.name}}</span> </div>'})
export class UnknownEven {
people: Person[];
}
@Component({template: '<div *ngFor="let person of people; trackBy ~{trackBy_1}trackBy_1~{trackBy_1-end}"><span>{{person.name}}</span> </div>'})
export class UnknownTrackBy {
people: Person[];
}
`,
'ng-if-cases.ts': `
import {Component} from '@angular/core';
@Component({template: '<div ~{implicit}*ngIf="show; let l"~{implicit-end}>Showing now!</div>'})
export class ShowIf {
show = false;
}
`,
'test.ng': `~{empty}
<~{start-tag}h~{start-tag-after-h}1~{start-tag-h1} ~{h1-after-space}>~{h1-content} {{~{sub-start}title~{sub-end}}}</h1>
~{after-h1}<h2>{{~{h2-hero}hero.~{h2-name}name}} details!</h2>
<div><label>id: </label>{{~{label-hero}hero.~{label-id}id}}</div>
<div ~{div-attributes}>
<label>name: </label>
</div>
&~{entity-amp}amp;
`
}
};

View File

@ -1,320 +0,0 @@
/**
* @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
*/
/// <reference path="../../../../node_modules/@types/node/index.d.ts" />
/// <reference path="../../../../node_modules/@types/jasmine/index.d.ts" />
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {Diagnostic, Diagnostics, Span} from '../src/types';
export type MockData = string | MockDirectory;
export type MockDirectory = {
[name: string]: MockData | undefined;
}
const angularts = /@angular\/(\w|\/|-)+\.tsx?$/;
const rxjsts = /rxjs\/(\w|\/)+\.tsx?$/;
const rxjsmetadata = /rxjs\/(\w|\/)+\.metadata\.json?$/;
const tsxfile = /\.tsx$/;
/* The missing cache does two things. First it improves performance of the
tests as it reduces the number of OS calls made during testing. Also it
improves debugging experience as fewer exceptions are raised allow you
to use stopping on all exceptions. */
const missingCache = new Map<string, boolean>();
const cacheUsed = new Set<string>();
const reportedMissing = new Set<string>();
/**
* The cache is valid if all the returned entries are empty.
*/
export function validateCache(): {exists: string[], unused: string[], reported: string[]} {
const exists: string[] = [];
const unused: string[] = [];
for (const fileName of iterableToArray(missingCache.keys())) {
if (fs.existsSync(fileName)) {
exists.push(fileName);
}
if (!cacheUsed.has(fileName)) {
unused.push(fileName);
}
}
return {exists, unused, reported: iterableToArray(reportedMissing.keys())};
}
missingCache.set('/node_modules/@angular/core.d.ts', true);
missingCache.set('/node_modules/@angular/common.d.ts', true);
missingCache.set('/node_modules/@angular/forms.d.ts', true);
missingCache.set('/node_modules/@angular/core/src/di/provider.metadata.json', true);
missingCache.set(
'/node_modules/@angular/core/src/change_detection/pipe_transform.metadata.json', true);
missingCache.set('/node_modules/@angular/core/src/reflection/types.metadata.json', true);
missingCache.set(
'/node_modules/@angular/core/src/reflection/platform_reflection_capabilities.metadata.json',
true);
missingCache.set('/node_modules/@angular/forms/src/directives/form_interface.metadata.json', true);
export class MockTypescriptHost implements ts.LanguageServiceHost {
private angularPath: string;
private nodeModulesPath: string;
private scriptVersion = new Map<string, number>();
private overrides = new Map<string, string>();
private projectVersion = 0;
constructor(private scriptNames: string[], private data: MockData) {
let angularIndex = module.filename.indexOf('@angular');
if (angularIndex >= 0)
this.angularPath =
module.filename.substr(0, angularIndex).replace('/all/', '/packages-dist/');
let distIndex = module.filename.indexOf('/dist/all');
if (distIndex >= 0)
this.nodeModulesPath = path.join(module.filename.substr(0, distIndex), 'node_modules');
}
override(fileName: string, content: string) {
this.scriptVersion.set(fileName, (this.scriptVersion.get(fileName) || 0) + 1);
if (fileName.endsWith('.ts')) {
this.projectVersion++;
}
if (content) {
this.overrides.set(fileName, content);
} else {
this.overrides.delete(fileName);
}
}
getCompilationSettings(): ts.CompilerOptions {
return {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
emitDecoratorMetadata: true,
experimentalDecorators: true,
removeComments: false,
noImplicitAny: false,
lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
};
}
getProjectVersion(): string { return this.projectVersion.toString(); }
getScriptFileNames(): string[] { return this.scriptNames; }
getScriptVersion(fileName: string): string {
return (this.scriptVersion.get(fileName) || 0).toString();
}
getScriptSnapshot(fileName: string): ts.IScriptSnapshot {
const content = this.getFileContent(fileName);
if (content) return ts.ScriptSnapshot.fromString(content);
return undefined;
}
getCurrentDirectory(): string { return '/'; }
getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
directoryExists(directoryName: string): boolean {
let effectiveName = this.getEffectiveName(directoryName);
if (effectiveName === directoryName)
return directoryExists(directoryName, this.data);
else
return fs.existsSync(effectiveName);
}
getMarkerLocations(fileName: string): {[name: string]: number}|undefined {
let content = this.getRawFileContent(fileName);
if (content) {
return getLocationMarkers(content);
}
}
getReferenceMarkers(fileName: string): ReferenceResult {
let content = this.getRawFileContent(fileName);
if (content) {
return getReferenceMarkers(content);
}
}
getFileContent(fileName: string): string {
const content = this.getRawFileContent(fileName);
if (content) return removeReferenceMarkers(removeLocationMarkers(content));
}
private getRawFileContent(fileName: string): string {
if (this.overrides.has(fileName)) {
return this.overrides.get(fileName);
}
let basename = path.basename(fileName);
if (/^lib.*\.d\.ts$/.test(basename)) {
let libPath = ts.getDefaultLibFilePath(this.getCompilationSettings());
return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8');
} else {
if (missingCache.has(fileName)) {
cacheUsed.add(fileName);
return undefined;
}
let effectiveName = this.getEffectiveName(fileName);
if (effectiveName === fileName)
return open(fileName, this.data);
else if (
!fileName.match(angularts) && !fileName.match(rxjsts) && !fileName.match(rxjsmetadata) &&
!fileName.match(tsxfile)) {
if (fs.existsSync(effectiveName)) {
return fs.readFileSync(effectiveName, 'utf8');
} else {
missingCache.set(fileName, true);
reportedMissing.add(fileName);
cacheUsed.add(fileName);
}
}
}
}
private getEffectiveName(name: string): string {
const node_modules = 'node_modules';
const at_angular = '/@angular';
if (name.startsWith('/' + node_modules)) {
if (this.nodeModulesPath && !name.startsWith('/' + node_modules + at_angular)) {
let result = path.join(this.nodeModulesPath, name.substr(node_modules.length + 1));
if (!name.match(rxjsts))
if (fs.existsSync(result)) {
return result;
}
}
if (this.angularPath && name.startsWith('/' + node_modules + at_angular)) {
return path.join(
this.angularPath, name.substr(node_modules.length + at_angular.length + 1));
}
}
return name;
}
}
function iterableToArray<T>(iterator: IterableIterator<T>) {
const result: T[] = [];
while (true) {
const next = iterator.next();
if (next.done) break;
result.push(next.value);
}
return result;
}
function find(fileName: string, data: MockData): MockData|undefined {
let names = fileName.split('/');
if (names.length && !names[0].length) names.shift();
let current = data;
for (let name of names) {
if (typeof current === 'string')
return undefined;
else
current = (<MockDirectory>current)[name];
if (!current) return undefined;
}
return current;
}
function open(fileName: string, data: MockData): string|undefined {
let result = find(fileName, data);
if (typeof result === 'string') {
return result;
}
return undefined;
}
function directoryExists(dirname: string, data: MockData): boolean {
let result = find(dirname, data);
return result && typeof result !== 'string';
}
const locationMarker = /\~\{(\w+(-\w+)*)\}/g;
function removeLocationMarkers(value: string): string {
return value.replace(locationMarker, '');
}
function getLocationMarkers(value: string): {[name: string]: number} {
value = removeReferenceMarkers(value);
let result: {[name: string]: number} = {};
let adjustment = 0;
value.replace(locationMarker, (match: string, name: string, _: any, index: number): string => {
result[name] = index - adjustment;
adjustment += match.length;
return '';
});
return result;
}
const referenceMarker = /«(((\w|\-)+)|([^∆]*∆(\w+)∆.[^»]*))»/g;
const definitionMarkerGroup = 1;
const nameMarkerGroup = 2;
export type ReferenceMarkers = {
[name: string]: Span[]
};
export interface ReferenceResult {
text: string;
definitions: ReferenceMarkers;
references: ReferenceMarkers;
}
function getReferenceMarkers(value: string): ReferenceResult {
const references: ReferenceMarkers = {};
const definitions: ReferenceMarkers = {};
value = removeLocationMarkers(value);
let adjustment = 0;
const text = value.replace(
referenceMarker, (match: string, text: string, reference: string, _: string,
definition: string, definitionName: string, index: number): string => {
const result = reference ? text : text.replace(/∆/g, '');
const span: Span = {start: index - adjustment, end: index - adjustment + result.length};
const markers = reference ? references : definitions;
const name = reference || definitionName;
(markers[name] = (markers[name] || [])).push(span);
adjustment += match.length - result.length;
return result;
});
return {text, definitions, references};
}
function removeReferenceMarkers(value: string): string {
return value.replace(referenceMarker, (match, text) => text.replace(/∆/g, ''));
}
export function noDiagnostics(diagnostics: Diagnostics) {
if (diagnostics && diagnostics.length) {
throw new Error(`Unexpected diagnostics: \n ${diagnostics.map(d => d.message).join('\n ')}`);
}
}
export function includeDiagnostic(
diagnostics: Diagnostics, message: string, text?: string, len?: string): void;
export function includeDiagnostic(
diagnostics: Diagnostics, message: string, at?: number, len?: number): void;
export function includeDiagnostic(diagnostics: Diagnostics, message: string, p1?: any, p2?: any) {
expect(diagnostics).toBeDefined();
if (diagnostics) {
const diagnostic = diagnostics.find(d => d.message.indexOf(message) >= 0) as Diagnostic;
expect(diagnostic).toBeDefined();
if (diagnostic && p1 != null) {
const at = typeof p1 === 'number' ? p1 : p2.indexOf(p1);
const len = typeof p2 === 'number' ? p2 : p1.length;
expect(diagnostic.span.start).toEqual(at);
if (len != null) {
expect(diagnostic.span.end - diagnostic.span.start).toEqual(len);
}
}
}
}

View File

@ -1,264 +0,0 @@
/**
* @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
*/
import 'reflect-metadata';
import * as ts from 'typescript';
import {LanguageServicePlugin} from '../src/ts_plugin';
import {toh} from './test_data';
import {MockTypescriptHost} from './test_utils';
describe('plugin', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
it('should not report errors on tour of heroes', () => {
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
for (let source of program.getSourceFiles()) {
expectNoDiagnostics(service.getSyntacticDiagnostics(source.fileName));
expectNoDiagnostics(service.getSemanticDiagnostics(source.fileName));
}
});
let plugin =
new LanguageServicePlugin({ts: ts, host: mockHost, service, registry: documentRegistry});
it('should not report template errors on tour of heroes', () => {
for (let source of program.getSourceFiles()) {
// Ignore all 'cases.ts' files as they intentionally contain errors.
if (!source.fileName.endsWith('cases.ts')) {
expectNoDiagnostics(plugin.getSemanticDiagnosticsFilter(source.fileName, []));
}
}
});
it('should be able to get entity completions',
() => { contains('app/app.component.ts', 'entity-amp', '&amp;', '&gt;', '&lt;', '&iota;'); });
it('should be able to return html elements', () => {
let htmlTags = ['h1', 'h2', 'div', 'span'];
let locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
for (let location of locations) {
contains('app/app.component.ts', location, ...htmlTags);
}
});
it('should be able to return element diretives',
() => { contains('app/app.component.ts', 'empty', 'my-app'); });
it('should be able to return h1 attributes',
() => { contains('app/app.component.ts', 'h1-after-space', 'id', 'dir', 'lang', 'onclick'); });
it('should be able to find common angular attributes', () => {
contains('app/app.component.ts', 'div-attributes', '(click)', '[ngClass]', '*ngIf', '*ngFor');
});
it('should be able to returned attribute names with an incompete attribute',
() => { contains('app/parsing-cases.ts', 'no-value-attribute', 'id', 'dir', 'lang'); });
it('should be able to return attributes of an incomplete element', () => {
contains('app/parsing-cases.ts', 'incomplete-open-lt', 'a');
contains('app/parsing-cases.ts', 'incomplete-open-a', 'a');
contains('app/parsing-cases.ts', 'incomplete-open-attr', 'id', 'dir', 'lang');
});
it('should be able to return completions with a missing closing tag',
() => { contains('app/parsing-cases.ts', 'missing-closing', 'h1', 'h2'); });
it('should be able to return common attributes of in an unknown tag',
() => { contains('app/parsing-cases.ts', 'unknown-element', 'id', 'dir', 'lang'); });
it('should be able to get the completions at the beginning of an interpolation',
() => { contains('app/app.component.ts', 'h2-hero', 'hero', 'title'); });
it('should not include private members of the of a class',
() => { contains('app/app.component.ts', 'h2-hero', '-internal'); });
it('should be able to get the completions at the end of an interpolation',
() => { contains('app/app.component.ts', 'sub-end', 'hero', 'title'); });
it('should be able to get the completions in a property read',
() => { contains('app/app.component.ts', 'h2-name', 'name', 'id'); });
it('should be able to get a list of pipe values', () => {
contains('app/parsing-cases.ts', 'before-pipe', 'lowercase', 'uppercase');
contains('app/parsing-cases.ts', 'in-pipe', 'lowercase', 'uppercase');
contains('app/parsing-cases.ts', 'after-pipe', 'lowercase', 'uppercase');
});
it('should be able get completions in an empty interpolation',
() => { contains('app/parsing-cases.ts', 'empty-interpolation', 'title', 'subTitle'); });
describe('with attributes', () => {
it('should be able to complete property value',
() => { contains('app/parsing-cases.ts', 'property-binding-model', 'test'); });
it('should be able to complete an event',
() => { contains('app/parsing-cases.ts', 'event-binding-model', 'modelChanged'); });
it('should be able to complete a two-way binding',
() => { contains('app/parsing-cases.ts', 'two-way-binding-model', 'test'); });
});
describe('with a *ngFor', () => {
it('should include a let for empty attribute',
() => { contains('app/parsing-cases.ts', 'for-empty', 'let'); });
it('should not suggest any entries if in the name part of a let',
() => { expectEmpty('app/parsing-cases.ts', 'for-let-empty'); });
it('should suggest NgForRow members for let initialization expression', () => {
contains(
'app/parsing-cases.ts', 'for-let-i-equal', 'index', 'count', 'first', 'last', 'even',
'odd');
});
it('should include a let', () => { contains('app/parsing-cases.ts', 'for-let', 'let'); });
it('should include an "of"', () => { contains('app/parsing-cases.ts', 'for-of', 'of'); });
it('should include field reference',
() => { contains('app/parsing-cases.ts', 'for-people', 'people'); });
it('should include person in the let scope',
() => { contains('app/parsing-cases.ts', 'for-interp-person', 'person'); });
// TODO: Enable when we can infer the element type of the ngFor
// it('should include determine person\'s type as Person', () => {
// contains('app/parsing-cases.ts', 'for-interp-name', 'name', 'age');
// contains('app/parsing-cases.ts', 'for-interp-age', 'name', 'age');
// });
});
describe('for pipes', () => {
it('should be able to resolve lowercase',
() => { contains('app/expression-cases.ts', 'string-pipe', 'substring'); });
});
describe('with references', () => {
it('should list references',
() => { contains('app/parsing-cases.ts', 'test-comp-content', 'test1', 'test2', 'div'); });
it('should reference the component',
() => { contains('app/parsing-cases.ts', 'test-comp-after-test', 'name'); });
// TODO: Enable when we have a flag that indicates the project targets the DOM
// it('should refernce the element if no component', () => {
// contains('app/parsing-cases.ts', 'test-comp-after-div', 'innerText');
// });
});
describe('for semantic errors', () => {
it('should report access to an unknown field', () => {
expectSemanticError(
'app/expression-cases.ts', 'foo',
'Identifier \'foo\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
});
it('should report access to an unknown sub-field', () => {
expectSemanticError(
'app/expression-cases.ts', 'nam',
'Identifier \'nam\' is not defined. \'Person\' does not contain such a member');
});
it('should report access to a private member', () => {
expectSemanticError(
'app/expression-cases.ts', 'myField',
'Identifier \'myField\' refers to a private member of the component');
});
it('should report numeric operator erros',
() => { expectSemanticError('app/expression-cases.ts', 'mod', 'Expected a numeric type'); });
describe('in ngFor', () => {
function expectError(locationMarker: string, message: string) {
expectSemanticError('app/ng-for-cases.ts', locationMarker, message);
}
it('should report an unknown field', () => {
expectError(
'people_1',
'Identifier \'people_1\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
});
it('should report an unknown context reference', () => {
expectError('even_1', 'The template context does not defined a member called \'even_1\'');
});
it('should report an unknown value in a key expression', () => {
expectError(
'trackBy_1',
'Identifier \'trackBy_1\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
});
});
describe('in ngIf', () => {
function expectError(locationMarker: string, message: string) {
expectSemanticError('app/ng-if-cases.ts', locationMarker, message);
}
it('should report an implicit context reference', () => {
expectError('implicit', 'The template context does not have an implicit value');
});
});
});
function getMarkerLocation(fileName: string, locationMarker: string): number {
const location = mockHost.getMarkerLocations(fileName)[locationMarker];
if (location == null) {
throw new Error(`No marker ${locationMarker} found.`);
}
return location;
}
function contains(fileName: string, locationMarker: string, ...names: string[]) {
const location = getMarkerLocation(fileName, locationMarker);
expectEntries(locationMarker, plugin.getCompletionsAtPosition(fileName, location), ...names);
}
function expectEmpty(fileName: string, locationMarker: string) {
const location = getMarkerLocation(fileName, locationMarker);
expect(plugin.getCompletionsAtPosition(fileName, location).entries).toEqual([]);
}
function expectSemanticError(fileName: string, locationMarker: string, message: string) {
const start = getMarkerLocation(fileName, locationMarker);
const end = getMarkerLocation(fileName, locationMarker + '-end');
const errors = plugin.getSemanticDiagnosticsFilter(fileName, []);
for (const error of errors) {
if (error.messageText.toString().indexOf(message) >= 0) {
expect(error.start).toEqual(start);
expect(error.length).toEqual(end - start);
return;
}
}
throw new Error(
`Expected error messages to contain ${message}, in messages:\n ${errors.map(e => e.messageText.toString()).join(',\n ')}`);
}
});
function expectEntries(locationMarker: string, info: ts.CompletionInfo, ...names: string[]) {
let entries: {[name: string]: boolean} = {};
if (!info) {
throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`);
} else {
for (let entry of info.entries) {
entries[entry.name] = true;
}
let shouldContains = names.filter(name => !name.startsWith('-'));
let shouldNotContain = names.filter(name => name.startsWith('-'));
let missing = shouldContains.filter(name => !entries[name]);
let present = shouldNotContain.map(name => name.substr(1)).filter(name => entries[name]);
if (missing.length) {
throw new Error(
`Expected result from ${locationMarker} to include at least one of the following, ${missing.join(', ')}, in the list of entries ${info.entries.map(entry => entry.name).join(', ')}`);
}
if (present.length) {
throw new Error(
`Unexpected member${present.length > 1 ? 's': ''} included in result: ${present.join(', ')}`);
}
}
}
function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
for (const diagnostic of diagnostics) {
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
if (diagnostic.start) {
let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
} else {
console.log(`${message}`);
}
}
expect(diagnostics.length).toBe(0);
}

View File

@ -1,36 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
"stripInternal": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"module": "es2015",
"moduleResolution": "node",
"outDir": "../../../dist/packages-dist/language-service",
"paths": {
"@angular/core": ["../../../dist/packages-dist/core"],
"@angular/core/testing": ["../../../dist/packages-dist/core/testing"],
"@angular/core/testing/*": ["../../../dist/packages-dist/core/testing/*"],
"@angular/common": ["../../../dist/packages-dist/common"],
"@angular/compiler": ["../../../dist/packages-dist/compiler"],
"@angular/compiler/*": ["../../../dist/packages-dist/compiler/*"],
"@angular/platform-server": ["../../../dist/packages-dist/platform-server"],
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"],
"@angular/tsc-wrapped": ["../../../dist/tools/@angular/tsc-wrapped"],
"@angular/tsc-wrapped/*": ["../../../dist/tools/@angular/tsc-wrapped/*"]
},
"rootDir": ".",
"sourceMap": true,
"inlineSources": true,
"target": "es5",
"skipLibCheck": true,
"lib": ["es2015", "dom"]
},
"files": [
"index.ts",
"../../../node_modules/zone.js/dist/zone.js.d.ts",
"../../../node_modules/@types/node/index.d.ts",
"../../../node_modules/@types/jasmine/index.d.ts"
]
}

View File

@ -303,7 +303,6 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
importIntoDoc(node: Node): any { return document.importNode(this.templateAwareRoot(node), true); }
adoptNode(node: Node): any { return document.adoptNode(node); }
getHref(el: Element): string { return (<any>el).href; }
getEventKey(event: any): string {
let key = event.key;
if (isBlank(key)) {

View File

@ -6,16 +6,17 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as core from '@angular/core';
import {ApplicationRef, DebugNode, NgZone, Optional, Provider, RootRenderer, getDebugNode, isDevMode} from '@angular/core';
import {StringMapWrapper} from '../../facade/collection';
import {DebugDomRootRenderer} from '../../private_import_core';
import {getDOM} from '../dom_adapter';
import {DomRootRenderer} from '../dom_renderer';
const CORE_TOKENS = {
'ApplicationRef': core.ApplicationRef,
'NgZone': core.NgZone,
'ApplicationRef': ApplicationRef,
'NgZone': NgZone
};
const INSPECT_GLOBAL_NAME = 'ng.probe';
@ -26,27 +27,26 @@ const CORE_TOKENS_GLOBAL_NAME = 'ng.coreTokens';
* null if the given native element does not have an Angular view associated
* with it.
*/
export function inspectNativeElement(element: any): core.DebugNode {
return core.getDebugNode(element);
export function inspectNativeElement(element: any /** TODO #9100 */): DebugNode {
return getDebugNode(element);
}
/**
* Deprecated. Use the one from '@angular/core'.
* @deprecated
* @experimental
*/
export class NgProbeToken {
constructor(public name: string, public token: any) {}
constructor(private name: string, private token: any) {}
}
export function _createConditionalRootRenderer(
rootRenderer: any, extraTokens: NgProbeToken[], coreTokens: core.NgProbeToken[]) {
return core.isDevMode() ?
_createRootRenderer(rootRenderer, (extraTokens || []).concat(coreTokens || [])) :
rootRenderer;
rootRenderer: any /** TODO #9100 */, extraTokens: NgProbeToken[]) {
if (isDevMode()) {
return _createRootRenderer(rootRenderer, extraTokens);
}
return rootRenderer;
}
function _createRootRenderer(rootRenderer: any, extraTokens: NgProbeToken[]) {
function _createRootRenderer(rootRenderer: any /** TODO #9100 */, extraTokens: NgProbeToken[]) {
getDOM().setGlobalVar(INSPECT_GLOBAL_NAME, inspectNativeElement);
getDOM().setGlobalVar(
CORE_TOKENS_GLOBAL_NAME,
@ -61,11 +61,14 @@ function _ngProbeTokensToMap(tokens: NgProbeToken[]): {[name: string]: any} {
/**
* Providers which support debugging Angular applications (e.g. via `ng.probe`).
*/
export const ELEMENT_PROBE_PROVIDERS: core.Provider[] = [{
provide: core.RootRenderer,
export const ELEMENT_PROBE_PROVIDERS: Provider[] = [{
provide: RootRenderer,
useFactory: _createConditionalRootRenderer,
deps: [
DomRootRenderer, [NgProbeToken, new core.Optional()],
[core.NgProbeToken, new core.Optional()]
]
deps: [DomRootRenderer, [NgProbeToken, new Optional()]]
}];
export const ELEMENT_PROBE_PROVIDERS_PROD_MODE: any[] = [{
provide: RootRenderer,
useFactory: _createRootRenderer,
deps: [DomRootRenderer, [NgProbeToken, new Optional()]]
}];

View File

@ -24,7 +24,6 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -27,7 +27,6 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -24,7 +24,6 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -89,13 +89,11 @@ import {UrlTree} from '../url_tree';
*/
@Directive({selector: ':not(a)[routerLink]'})
export class RouterLink {
private commands: any[] = [];
@Input() queryParams: {[k: string]: any};
@Input() fragment: string;
@Input() preserveQueryParams: boolean;
@Input() preserveFragment: boolean;
@Input() skipLocationChange: boolean;
@Input() replaceUrl: boolean;
private commands: any[] = [];
constructor(
private router: Router, private route: ActivatedRoute,
@ -122,9 +120,7 @@ export class RouterLink {
queryParams: this.queryParams,
fragment: this.fragment,
preserveQueryParams: toBool(this.preserveQueryParams),
preserveFragment: toBool(this.preserveFragment),
skipLocationChange: toBool(this.skipLocationChange),
replaceUrl: toBool(this.replaceUrl),
preserveFragment: toBool(this.preserveFragment)
});
}
}
@ -142,14 +138,12 @@ export class RouterLink {
@Directive({selector: 'a[routerLink]'})
export class RouterLinkWithHref implements OnChanges, OnDestroy {
@Input() target: string;
private commands: any[] = [];
@Input() queryParams: {[k: string]: any};
@Input() fragment: string;
@Input() routerLinkOptions: {preserveQueryParams: boolean, preserveFragment: boolean};
@Input() preserveQueryParams: boolean;
@Input() preserveFragment: boolean;
@Input() skipLocationChange: boolean;
@Input() replaceUrl: boolean;
private commands: any[] = [];
private subscription: Subscription;
// the url displayed on the anchor element.
@ -201,9 +195,7 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
queryParams: this.queryParams,
fragment: this.fragment,
preserveQueryParams: toBool(this.preserveQueryParams),
preserveFragment: toBool(this.preserveFragment),
skipLocationChange: toBool(this.skipLocationChange),
replaceUrl: toBool(this.replaceUrl),
preserveFragment: toBool(this.preserveFragment)
});
}
}

View File

@ -7,7 +7,7 @@
*/
import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, ApplicationRef, Compiler, ComponentRef, Inject, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, OpaqueToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, ApplicationRef, Compiler, ComponentRef, Inject, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, OpaqueToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
import {Route, Routes} from './config';
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
import {RouterLinkActive} from './directives/router_link_active';
@ -40,10 +40,17 @@ export const ROUTER_CONFIGURATION = new OpaqueToken('ROUTER_CONFIGURATION');
*/
export const ROUTER_FORROOT_GUARD = new OpaqueToken('ROUTER_FORROOT_GUARD');
const pathLocationStrategy = {
provide: LocationStrategy,
useClass: PathLocationStrategy
};
const hashLocationStrategy = {
provide: LocationStrategy,
useClass: HashLocationStrategy
};
export const ROUTER_PROVIDERS: Provider[] = [
Location,
{provide: UrlSerializer, useClass: DefaultUrlSerializer},
{
Location, {provide: UrlSerializer, useClass: DefaultUrlSerializer}, {
provide: Router,
useFactory: setupRouter,
deps: [
@ -51,19 +58,11 @@ export const ROUTER_PROVIDERS: Provider[] = [
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()]
]
},
RouterOutletMap,
{provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
{provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader},
RouterPreloader,
NoPreloading,
PreloadAllModules,
{provide: ROUTER_CONFIGURATION, useValue: {enableTracing: false}},
RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
{provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader}, RouterPreloader, NoPreloading,
PreloadAllModules, {provide: ROUTER_CONFIGURATION, useValue: {enableTracing: false}}
];
export function routerNgProbeToken() {
return new NgProbeToken('Router', Router);
}
/**
* @whatItDoes Adds router directives and providers.
*
@ -77,9 +76,10 @@ export function routerNgProbeToken() {
* `RouterModule.forChild`.
*
* * `forRoot` creates a module that contains all the directives, the given routes, and the router
* service itself.
* service itself.
* * `forChild` creates a module that contains all the directives and the given routes, but does not
* include the router service.
* include
* the router service.
*
* When registered at the root, the module should be used as follows
*
@ -134,15 +134,12 @@ export class RouterModule {
return {
ngModule: RouterModule,
providers: [
ROUTER_PROVIDERS,
provideRoutes(routes),
{
ROUTER_PROVIDERS, provideRoutes(routes), {
provide: ROUTER_FORROOT_GUARD,
useFactory: provideForRootGuard,
deps: [[Router, new Optional(), new SkipSelf()]]
},
{provide: ROUTER_CONFIGURATION, useValue: config ? config : {}},
{
{provide: ROUTER_CONFIGURATION, useValue: config ? config : {}}, {
provide: LocationStrategy,
useFactory: provideLocationStrategy,
deps: [
@ -154,9 +151,8 @@ export class RouterModule {
useExisting: config && config.preloadingStrategy ? config.preloadingStrategy :
NoPreloading
},
{provide: NgProbeToken, multi: true, useFactory: routerNgProbeToken},
provideRouterInitializer(),
],
provideRouterInitializer()
]
};
}
@ -200,7 +196,7 @@ export function provideForRootGuard(router: Router): any {
export function provideRoutes(routes: Routes): any {
return [
{provide: ANALYZE_FOR_ENTRY_COMPONENTS, multi: true, useValue: routes},
{provide: ROUTES, multi: true, useValue: routes},
{provide: ROUTES, multi: true, useValue: routes}
];
}
@ -301,6 +297,6 @@ export function provideRouterInitializer() {
useFactory: initialRouterNavigation,
deps: [Router, ApplicationRef, RouterPreloader, ROUTER_CONFIGURATION]
},
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER},
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER}
];
}

View File

@ -27,7 +27,6 @@
"index.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -27,7 +27,6 @@
"../../../node_modules/zone.js/dist/zone.js.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true
}
}

View File

@ -154,8 +154,7 @@ class AppModuleInjector extends import0.NgModuleInjector<import1.AppModule> {
get _RootRenderer_20(): any {
if ((this.__RootRenderer_20 == (null as any))) {
(this.__RootRenderer_20 = import23._createConditionalRootRenderer(
this._DomRootRenderer_19, this.parent.get(import23.NgProbeToken, (null as any)),
this.parent.get(import8.NgProbeToken, (null as any))));
this._DomRootRenderer_19, this.parent.get(import23.NgProbeToken, (null as any))));
}
return this.__RootRenderer_20;
}

View File

@ -154,8 +154,7 @@ class AppModuleInjector extends import0.NgModuleInjector<import1.AppModule> {
get _RootRenderer_20(): any {
if ((this.__RootRenderer_20 == (null as any))) {
(this.__RootRenderer_20 = import23._createConditionalRootRenderer(
this._DomRootRenderer_19, this.parent.get(import23.NgProbeToken, (null as any)),
this.parent.get(import8.NgProbeToken, (null as any))));
this._DomRootRenderer_19, this.parent.get(import23.NgProbeToken, (null as any))));
}
return this.__RootRenderer_20;
}

View File

@ -13,8 +13,7 @@
"selenium-webdriver": ["../node_modules/@types/selenium-webdriver/index.d.ts"],
"rxjs/*": ["../node_modules/rxjs/*"],
"@angular/*": ["./@angular/*"],
"@angular/tsc-wrapped": ["../dist/tools/@angular/tsc-wrapped"],
"@angular/tsc-wrapped/*": ["../dist/tools/@angular/tsc-wrapped/*"]
"@angular/tsc-wrapped": ["../dist/tools/@angular/tsc-wrapped"]
},
"rootDir": ".",
"inlineSourceMap": true,

File diff suppressed because it is too large Load Diff

3728
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "2.3.0-beta.1",
"version": "2.2.2",
"private": true,
"branchPattern": "2.0.*",
"description": "Angular 2 - a web framework for modern web apps",
@ -71,14 +71,13 @@
"react": "^0.14.0",
"rewire": "^2.3.3",
"rollup": "^0.26.3",
"rollup-plugin-commonjs": "^5.0.5",
"selenium-webdriver": "^2.53.3",
"semver": "^5.1.0",
"source-map": "^0.3.0",
"source-map-support": "^0.4.2",
"systemjs": "0.18.10",
"ts-api-guardian": "0.1.4",
"tsickle": "^0.2.1",
"tsickle": "^0.1.7",
"tslint": "^3.15.1",
"typescript": "^2.0.2",
"universal-analytics": "^0.3.9",

View File

@ -17,6 +17,5 @@ node dist/tools/@angular/tsc-wrapped/src/main -p modules/@angular/core/tsconfig-
node dist/tools/@angular/tsc-wrapped/src/main -p modules/@angular/common/tsconfig-build.json
node dist/tools/@angular/tsc-wrapped/src/main -p modules/@angular/platform-browser/tsconfig-build.json
node dist/tools/@angular/tsc-wrapped/src/main -p modules/@angular/router/tsconfig-build.json
node dist/tools/@angular/tsc-wrapped/src/main -p modules/@angular/forms/tsconfig-build.json
echo 'travis_fold:end:BUILD'

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
export {DecoratorDownlevelCompilerHost, MetadataWriterHost} from './src/compiler_host';
export {MetadataWriterHost, TsickleHost} from './src/compiler_host';
export {CodegenExtension, main} from './src/main';
export {default as AngularCompilerOptions} from './src/options';

View File

@ -1,6 +1,6 @@
{
"name": "@angular/tsc-wrapped",
"version": "0.4.1",
"version": "0.4.0",
"description": "Wraps the tsc CLI, allowing extensions.",
"homepage": "https://github.com/angular/angular/tree/master/tools/tsc-wrapped",
"bugs": "https://github.com/angular/angular/issues",
@ -11,7 +11,7 @@
"license": "MIT",
"repository": {"type":"git","url":"https://github.com/angular/angular.git"},
"dependencies": {
"tsickle": "^0.2"
"tsickle": "^0.1.7"
},
"peerDependencies": {
"typescript": "^2.0.2"

View File

@ -7,20 +7,12 @@
*/
import {writeFileSync} from 'fs';
import * as tsickle from 'tsickle';
import {convertDecorators} from 'tsickle';
import * as ts from 'typescript';
import NgOptions from './options';
import {MetadataCollector} from './collector';
export function formatDiagnostics(d: ts.Diagnostic[]): string {
const host: ts.FormatDiagnosticsHost = {
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
getNewLine: () => ts.sys.newLine,
getCanonicalFileName: (f: string) => f
};
return ts.formatDiagnostics(d, host);
}
/**
* Implementation of CompilerHost that forwards all methods to another instance.
@ -49,16 +41,15 @@ export abstract class DelegatingHost implements ts.CompilerHost {
directoryExists = (directoryName: string) => this.delegate.directoryExists(directoryName);
}
export class DecoratorDownlevelCompilerHost extends DelegatingHost {
private ANNOTATION_SUPPORT = `
export class TsickleHost extends DelegatingHost {
// Additional diagnostics gathered by pre- and post-emit transformations.
public diagnostics: ts.Diagnostic[] = [];
private TSICKLE_SUPPORT = `
interface DecoratorInvocation {
type: Function;
args?: any[];
}
`;
/** Error messages produced by tsickle, if any. */
public diagnostics: ts.Diagnostic[] = [];
constructor(delegate: ts.CompilerHost, private program: ts.Program) { super(delegate); }
getSourceFile =
@ -67,12 +58,12 @@ interface DecoratorInvocation {
let newContent = originalContent;
if (!/\.d\.ts$/.test(fileName)) {
try {
const converted = tsickle.convertDecorators(
const converted = convertDecorators(
this.program.getTypeChecker(), this.program.getSourceFile(fileName));
if (converted.diagnostics) {
this.diagnostics.push(...converted.diagnostics);
}
newContent = converted.output + this.ANNOTATION_SUPPORT;
newContent = converted.output + this.TSICKLE_SUPPORT;
} catch (e) {
console.error('Cannot convertDecorators on file', fileName);
throw e;
@ -82,35 +73,14 @@ interface DecoratorInvocation {
};
}
export class TsickleCompilerHost extends DelegatingHost {
/** Error messages produced by tsickle, if any. */
public diagnostics: ts.Diagnostic[] = [];
constructor(
delegate: ts.CompilerHost, private oldProgram: ts.Program, private options: NgOptions) {
super(delegate);
}
getSourceFile =
(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) => {
let sourceFile = this.oldProgram.getSourceFile(fileName);
let isDefinitions = /\.d\.ts$/.test(fileName);
// Don't tsickle-process any d.ts that isn't a compilation target;
// this means we don't process e.g. lib.d.ts.
if (isDefinitions) return sourceFile;
let {output, externs, diagnostics} =
tsickle.annotate(this.oldProgram, sourceFile, {untyped: true});
this.diagnostics = diagnostics;
return ts.createSourceFile(fileName, output, languageVersion, true);
};
}
const IGNORED_FILES = /\.ngfactory\.js$|\.css\.js$|\.css\.shim\.js$/;
export class MetadataWriterHost extends DelegatingHost {
private metadataCollector = new MetadataCollector();
constructor(delegate: ts.CompilerHost, private ngOptions: NgOptions) { super(delegate); }
constructor(
delegate: ts.CompilerHost, private program: ts.Program, private ngOptions: NgOptions) {
super(delegate);
}
private writeMetadata(emitFilePath: string, sourceFile: ts.SourceFile) {
// TODO: replace with DTS filePath when https://github.com/Microsoft/TypeScript/pull/8412 is

View File

@ -13,7 +13,7 @@ import * as ts from 'typescript';
import {check, tsc} from './tsc';
import NgOptions from './options';
import {MetadataWriterHost, DecoratorDownlevelCompilerHost, TsickleCompilerHost} from './compiler_host';
import {MetadataWriterHost, TsickleHost} from './compiler_host';
import {CliOptions} from './cli_options';
export type CodegenExtension =
@ -34,10 +34,6 @@ export function main(
// read the configuration options from wherever you store them
const {parsed, ngOptions} = tsc.readConfiguration(project, basePath);
ngOptions.basePath = basePath;
const createProgram = (host: ts.CompilerHost, oldProgram?: ts.Program) =>
ts.createProgram(parsed.fileNames, parsed.options, host, oldProgram);
const diagnostics = (parsed.options as any).diagnostics;
if (diagnostics) (ts as any).performance.enable();
const host = ts.createCompilerHost(parsed.options, true);
@ -46,60 +42,30 @@ export function main(
// todo(misko): remove once facade symlinks are removed
host.realpath = (path) => path;
const program = createProgram(host);
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
const errors = program.getOptionsDiagnostics();
check(errors);
if (ngOptions.skipTemplateCodegen || !codegen) {
codegen = () => Promise.resolve(null);
}
if (diagnostics) console.time('NG codegen');
return codegen(ngOptions, cliOptions, program, host).then(() => {
if (diagnostics) console.timeEnd('NG codegen');
let definitionsHost = host;
if (!ngOptions.skipMetadataEmit) {
definitionsHost = new MetadataWriterHost(host, ngOptions);
}
// Create a new program since codegen files were created after making the old program
let programWithCodegen = createProgram(definitionsHost, program);
tsc.typeCheck(host, programWithCodegen);
const newProgram = ts.createProgram(parsed.fileNames, parsed.options, host, program);
tsc.typeCheck(host, newProgram);
let preprocessHost = host;
let programForJsEmit = programWithCodegen;
// Emit *.js with Decorators lowered to Annotations, and also *.js.map
const tsicklePreProcessor = new TsickleHost(host, newProgram);
tsc.emit(tsicklePreProcessor, newProgram);
if (ngOptions.annotationsAs !== 'decorators') {
if (diagnostics) console.time('NG downlevel');
const downlevelHost = new DecoratorDownlevelCompilerHost(preprocessHost, programForJsEmit);
// A program can be re-used only once; save the programWithCodegen to be reused by
// metadataWriter
programForJsEmit = createProgram(downlevelHost);
check(downlevelHost.diagnostics);
preprocessHost = downlevelHost;
if (diagnostics) console.timeEnd('NG downlevel');
}
if (ngOptions.annotateForClosureCompiler) {
if (diagnostics) console.time('NG JSDoc');
const tsickleHost = new TsickleCompilerHost(preprocessHost, programForJsEmit, ngOptions);
programForJsEmit = createProgram(tsickleHost);
check(tsickleHost.diagnostics);
if (diagnostics) console.timeEnd('NG JSDoc');
}
// Emit *.js and *.js.map
tsc.emit(programForJsEmit);
// Emit *.d.ts and maybe *.metadata.json
// Not in the same emit pass with above, because tsickle erases
// decorators which we want to read or document.
// Do this emit second since TypeScript will create missing directories for us
// in the standard emit.
tsc.emit(programWithCodegen);
if (diagnostics) {
(ts as any).performance.forEachMeasure(
(name: string, duration: number) => { console.error(`TS ${name}: ${duration}ms`); });
if (!ngOptions.skipMetadataEmit) {
// Emit *.metadata.json and *.d.ts
// Not in the same emit pass with above, because tsickle erases
// decorators which we want to read or document.
// Do this emit second since TypeScript will create missing directories for us
// in the standard emit.
const metadataWriter = new MetadataWriterHost(host, newProgram, ngOptions);
tsc.emit(metadataWriter, newProgram);
}
});
} catch (e) {

View File

@ -9,43 +9,28 @@
import * as ts from 'typescript';
interface Options extends ts.CompilerOptions {
// Absolute path to a directory where generated file structure is written.
// If unspecified, generated files will be written alongside sources.
genDir?: string;
// Absolute path to a directory where generated file structure is written
genDir: string;
// Path to the directory containing the tsconfig.json file.
basePath?: string;
basePath: string;
// Don't produce .metadata.json files (they don't work for bundled emit with --out)
skipMetadataEmit?: boolean;
skipMetadataEmit: boolean;
// Produce an error if the metadata written for a class would produce an error if used.
strictMetadataEmit?: boolean;
strictMetadataEmit: boolean;
// Don't produce .ngfactory.ts or .css.shim.ts files
skipTemplateCodegen?: boolean;
skipTemplateCodegen: boolean;
// Whether to generate code for library code.
// If true, produce .ngfactory.ts and .css.shim.ts files for .d.ts inputs.
// Default is true.
generateCodeForLibraries?: boolean;
// Insert JSDoc type annotations needed by Closure Compiler
annotateForClosureCompiler?: boolean;
// Modify how angular annotations are emitted to improve tree-shaking.
// Default is static fields.
// decorators: Leave the Decorators in-place. This makes compilation faster.
// TypeScript will emit calls to the __decorate helper.
// `--emitDecoratorMetadata` can be used for runtime reflection.
// However, the resulting code will not properly tree-shake.
// static fields: Replace decorators with a static field in the class.
// Allows advanced tree-shakers like Closure Compiler to remove
// unused classes.
annotationsAs?: 'decorators'|'static fields';
// Print extra information while running the compiler
trace?: boolean;
trace: boolean;
// Whether to embed debug information in the compiled templates
debug?: boolean;

View File

@ -11,6 +11,7 @@ import * as path from 'path';
import * as ts from 'typescript';
import AngularCompilerOptions from './options';
import {TsickleHost} from './compiler_host';
/**
* Our interface to the TypeScript standard compiler.
@ -21,7 +22,7 @@ export interface CompilerInterface {
readConfiguration(project: string, basePath: string):
{parsed: ts.ParsedCommandLine, ngOptions: AngularCompilerOptions};
typeCheck(compilerHost: ts.CompilerHost, program: ts.Program): void;
emit(program: ts.Program): number;
emit(compilerHost: ts.CompilerHost, program: ts.Program): number;
}
const DEBUG = false;
@ -51,26 +52,6 @@ export function check(diags: ts.Diagnostic[]) {
}
}
export function validateAngularCompilerOptions(options: AngularCompilerOptions): ts.Diagnostic[] {
if (options.annotationsAs) {
switch (options.annotationsAs) {
case 'decorators':
case 'static fields':
break;
default:
return [{
file: null,
start: null,
length: null,
messageText:
'Angular compiler options "annotationsAs" only supports "static fields" and "decorators"',
category: ts.DiagnosticCategory.Error,
code: 0
}];
}
}
}
export class Tsc implements CompilerInterface {
public ngOptions: AngularCompilerOptions;
public parsed: ts.ParsedCommandLine;
@ -115,8 +96,6 @@ export class Tsc implements CompilerInterface {
for (const key of Object.keys(this.parsed.options)) {
this.ngOptions[key] = this.parsed.options[key];
}
check(validateAngularCompilerOptions(this.ngOptions));
return {parsed: this.parsed, ngOptions: this.ngOptions};
}
@ -133,11 +112,15 @@ export class Tsc implements CompilerInterface {
check(diagnostics);
}
emit(program: ts.Program): number {
emit(compilerHost: TsickleHost, oldProgram: ts.Program): number {
// Create a new program since the host may be different from the old program.
const program = ts.createProgram(this.parsed.fileNames, this.parsed.options, compilerHost);
debug('Emitting outputs...');
const emitResult = program.emit();
const diagnostics: ts.Diagnostic[] = [];
diagnostics.push(...emitResult.diagnostics);
check(compilerHost.diagnostics);
return emitResult.emitSkipped ? 1 : 0;
}
}

View File

@ -1,51 +0,0 @@
/**
* @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
*/
import * as ts from 'typescript';
import NgOptions from '../src/options';
import {formatDiagnostics, TsickleCompilerHost} from '../src/compiler_host';
import {writeTempFile} from './test_support';
describe('Compiler Host', () => {
function makeProgram(fileName: string, source: string): [ts.Program, ts.CompilerHost, NgOptions] {
let fn = writeTempFile(fileName, source);
let opts: NgOptions = {
target: ts.ScriptTarget.ES5,
types: [],
genDir: '/tmp',
basePath: '/tmp',
noEmit: true,
};
// TsickleCompilerHost wants a ts.Program, which is the result of
// parsing and typechecking the code before tsickle processing.
// So we must create and run the entire stack of CompilerHost.
let host = ts.createCompilerHost(opts);
let program = ts.createProgram([fn], opts, host);
// To get types resolved, you must first call getPreEmitDiagnostics.
let diags = formatDiagnostics(ts.getPreEmitDiagnostics(program));
expect(diags).toEqual('');
return [program, host, opts];
}
it('inserts JSDoc annotations', () => {
const [program, host, opts] = makeProgram('foo.ts', 'let x: number = 123');
const tsickleHost = new TsickleCompilerHost(host, program, opts);
const f = tsickleHost.getSourceFile(program.getRootFileNames()[0], ts.ScriptTarget.ES5);
expect(f.text).toContain('/** @type {?} */');
});
it('reports diagnostics about existing JSDoc', () => {
const [program, host, opts] =
makeProgram('error.ts', '/** @param {string} x*/ function f(x: string){};');
const tsickleHost = new TsickleCompilerHost(host, program, opts);
const f = tsickleHost.getSourceFile(program.getRootFileNames()[0], ts.ScriptTarget.ES5);
expect(formatDiagnostics(tsickleHost.diagnostics)).toContain('redundant with TypeScript types');
});
});

View File

@ -1,209 +0,0 @@
/**
* @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
*/
import * as fs from 'fs';
import * as path from 'path';
import {main} from '../src/main';
import {makeTempDir} from './test_support';
describe('tsc-wrapped', () => {
let basePath: string;
let write: (fileName: string, content: string) => void;
beforeEach(() => {
basePath = makeTempDir();
write = (fileName: string, content: string) => {
fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'});
};
write('decorators.ts', '/** @Annotation */ export var Component: Function;');
write('dep.ts', `
export const A = 1;
export const B = 2;
`);
write('test.ts', `
import {Component} from './decorators';
export * from './dep';
@Component({})
export class Comp {
/**
* Comment that is
* multiple lines
*/
method(x: string): void {}
}
`);
});
function readOut(ext: string) {
return fs.readFileSync(path.join(basePath, 'built', `test.${ext}`), {encoding: 'utf-8'});
}
it('should report error if project not found', () => {
main('not-exist', null as any)
.then(() => fail('should report error'))
.catch(e => expect(e.message).toContain('ENOENT'));
});
it('should pre-process sources', (done) => {
write('tsconfig.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"types": [],
"outDir": "built",
"declaration": true,
"module": "es2015"
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true
},
"files": ["test.ts"]
}`);
main(basePath, {basePath})
.then(() => {
const out = readOut('js');
// No helpers since decorators were lowered
expect(out).not.toContain('__decorate');
// Expand `export *`
expect(out).toContain('export { A, B }');
// Annotated for Closure compiler
expect(out).toContain('* @param {?} x');
// Comments should stay multi-line
expect(out).not.toContain('Comment that is multiple lines');
// Decorator is now an annotation
expect(out).toMatch(/Comp.decorators = \[\s+\{ type: Component/);
const decl = readOut('d.ts');
expect(decl).toContain('declare class Comp');
const metadata = readOut('metadata.json');
expect(metadata).toContain('"Comp":{"__symbolic":"class"');
done();
})
.catch(e => done.fail(e));
});
it('should allow all options disabled', (done) => {
write('tsconfig.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"types": [],
"outDir": "built",
"declaration": false,
"module": "es2015"
},
"angularCompilerOptions": {
"annotateForClosureCompiler": false,
"annotationsAs": "decorators",
"skipMetadataEmit": true,
"skipTemplateCodegen": true
},
"files": ["test.ts"]
}`);
main(basePath, {basePath})
.then(() => {
const out = readOut('js');
// TypeScript's decorator emit
expect(out).toContain('__decorate');
// Not annotated for Closure compiler
expect(out).not.toContain('* @param {?} x');
expect(() => fs.accessSync(path.join(basePath, 'built', 'test.metadata.json'))).toThrow();
expect(() => fs.accessSync(path.join(basePath, 'built', 'test.d.ts'))).toThrow();
done();
})
.catch(e => done.fail(e));
});
it('should allow JSDoc annotations without decorator downleveling', (done) => {
write('tsconfig.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"types": [],
"outDir": "built",
"declaration": true
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"annotationsAs": "decorators"
},
"files": ["test.ts"]
}`);
main(basePath, {basePath}).then(() => done()).catch(e => done.fail(e));
});
xit('should run quickly (performance baseline)', (done) => {
for (let i = 0; i < 1000; i++) {
write(`input${i}.ts`, `
import {Component} from './decorators';
@Component({})
export class Input${i} {
private __brand: string;
}
`);
}
write('tsconfig.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"types": [],
"outDir": "built",
"declaration": true,
"diagnostics": true
},
"angularCompilerOptions": {
"annotateForClosureCompiler": false,
"annotationsAs": "decorators",
"skipMetadataEmit": true
},
"include": ["input*.ts"]
}`);
console.time('BASELINE');
main(basePath, {basePath})
.then(() => {
console.timeEnd('BASELINE');
done();
})
.catch(e => done.fail(e));
});
xit('should run quickly (performance test)', (done) => {
for (let i = 0; i < 1000; i++) {
write(`input${i}.ts`, `
import {Component} from './decorators';
@Component({})
export class Input${i} {
private __brand: string;
}
`);
}
write('tsconfig.json', `{
"compilerOptions": {
"experimentalDecorators": true,
"types": [],
"outDir": "built",
"declaration": true,
"diagnostics": true,
"skipLibCheck": true
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true
},
"include": ["input*.ts"]
}`);
console.time('TSICKLE');
main(basePath, {basePath})
.then(() => {
console.timeEnd('TSICKLE');
done();
})
.catch(e => done.fail(e));
});
});

View File

@ -1,28 +0,0 @@
/**
* @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
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const tmpdir = process.env.TEST_TMPDIR || os.tmpdir();
export function writeTempFile(name: string, contents: string): string {
// TEST_TMPDIR is set by bazel.
const id = (Math.random() * 1000000).toFixed(0);
const fn = path.join(tmpdir, `tmp.${id}.${name}`);
fs.writeFileSync(fn, contents);
return fn;
}
export function makeTempDir(): string {
const id = (Math.random() * 1000000).toFixed(0);
const dir = path.join(tmpdir, `tmp.${id}`);
fs.mkdirSync(dir);
return dir;
}

View File

@ -150,10 +150,7 @@ export declare class ApplicationModule {
export declare abstract class ApplicationRef {
componentTypes: Type<any>[];
components: ComponentRef<any>[];
viewCount: any;
attachView(view: ViewRef): void;
abstract bootstrap<C>(componentFactory: ComponentFactory<C> | Type<C>): ComponentRef<C>;
detachView(view: ViewRef): void;
abstract tick(): void;
}
@ -602,13 +599,6 @@ export declare abstract class NgModuleRef<T> {
abstract onDestroy(callback: () => void): void;
}
/** @experimental */
export declare class NgProbeToken {
name: string;
token: any;
constructor(name: string, token: any);
}
/** @experimental */
export declare class NgZone {
hasPendingMacrotasks: boolean;
@ -1000,7 +990,7 @@ export declare enum ViewEncapsulation {
}
/** @stable */
export declare abstract class ViewRef extends ChangeDetectorRef {
export declare abstract class ViewRef {
destroyed: boolean;
abstract onDestroy(callback: Function): any;
}

View File

@ -58,10 +58,8 @@ export declare class HammerGestureConfig {
buildHammer(element: HTMLElement): HammerInstance;
}
/** @deprecated */
/** @experimental */
export declare class NgProbeToken {
name: string;
token: any;
constructor(name: string, token: any);
}

View File

@ -232,9 +232,7 @@ export declare class RouterLink {
queryParams: {
[k: string]: any;
};
replaceUrl: boolean;
routerLink: any[] | string;
skipLocationChange: boolean;
urlTree: UrlTree;
constructor(router: Router, route: ActivatedRoute, locationStrategy: LocationStrategy);
onClick(): boolean;
@ -264,13 +262,11 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
queryParams: {
[k: string]: any;
};
replaceUrl: boolean;
routerLink: any[] | string;
routerLinkOptions: {
preserveQueryParams: boolean;
preserveFragment: boolean;
};
skipLocationChange: boolean;
target: string;
urlTree: UrlTree;
constructor(router: Router, route: ActivatedRoute, locationStrategy: LocationStrategy);