Compare commits
58 Commits
2.3.0-beta
...
2.2.2
Author | SHA1 | Date | |
---|---|---|---|
11ed8f56ab | |||
a49acbf027 | |||
8e41910429 | |||
a4ab14bf74 | |||
ea4fc9b421 | |||
0956acee58 | |||
2ca67e1674 | |||
472666fc2b | |||
462316b0f1 | |||
96c2b2cc25 | |||
3d407fc010 | |||
64bd672e3a | |||
ef38676091 | |||
38be2b81c6 | |||
39a71eb0ec | |||
2fe6fb1163 | |||
b5afe51b26 | |||
170525a225 | |||
0c98f45105 | |||
e7025c9423 | |||
8f295287a2 | |||
030facc66a | |||
45af8f6752 | |||
33a79028be | |||
09226d96f8 | |||
6c3166e6e4 | |||
8df328b15a | |||
115f18fa06 | |||
511cd4d182 | |||
87d5d49530 | |||
933caacad3 | |||
efe9c4f35c | |||
5b0f9e2f51 | |||
462879887a | |||
dae0d0fd66 | |||
c7f750dd5a | |||
73de925551 | |||
547c22029a | |||
364642d58c | |||
7b67badc43 | |||
dc1662a447 | |||
b5f433626b | |||
dabaf858d9 | |||
bbc3c9ce0e | |||
1dcf1f484e | |||
583d2833db | |||
f502a768d3 | |||
16303ac487 | |||
6cdc3b5c12 | |||
5c46c493f2 | |||
e02c18049d | |||
e0ce5458a2 | |||
6a5ba0ec81 | |||
828c0d24eb | |||
22536442d6 | |||
845ea235ee | |||
21a4de999b | |||
82b34838bf |
13
CHANGELOG.md
13
CHANGELOG.md
@ -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)
|
||||
|
||||
|
2
build.sh
2
build.sh
@ -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
|
||||
|
@ -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/**',
|
||||
|
@ -22,7 +22,6 @@
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
function fk(index: number, b: number, c: number, d: number): [number, number] {
|
||||
if (index < 20) {
|
||||
return [(b & c) | (~b & d), 0x5a827999];
|
||||
}
|
||||
return hex.toLowerCase();
|
||||
|
||||
if (index < 40) {
|
||||
return [b ^ c ^ d, 0x6ed9eba1];
|
||||
}
|
||||
|
||||
if (index < 60) {
|
||||
return [(b & c) | (b & d) | (c & d), 0x8f1bbcdc];
|
||||
}
|
||||
|
||||
return [b ^ c ^ d, 0xca62c1d6];
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
return decimal.split('').reverse().join('');
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
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 rol32(a: number, count: number): number {
|
||||
return (a << count) | (a >>> (32 - count));
|
||||
}
|
@ -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);
|
||||
|
@ -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); }
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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); }
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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 (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 _LoadVisitor().parse(result.rootNodes, messageBundle);
|
||||
|
||||
if (errors.length) {
|
||||
throw new Error(`xliff parse errors:\n${errors.join('\n')}`);
|
||||
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[] = [];
|
||||
|
||||
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')}`);
|
||||
}
|
||||
|
||||
digest(message: i18n.Message): string { return digest(message); }
|
||||
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 {
|
||||
this._msgId = msgId.value;
|
||||
}
|
||||
ml.visitAll(this, element.children, null);
|
||||
if (this._unitMlNodes) {
|
||||
this._mlNodesByMsgId[id] = this._unitMlNodes;
|
||||
} else {
|
||||
this._addError(element, `Message ${id} misses a translation`);
|
||||
}
|
||||
}
|
||||
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 ''; }
|
||||
|
||||
visitExpansion(expansion: ml.Expansion, context: any): any {
|
||||
throw new Error('unreachable code');
|
||||
}
|
||||
|
||||
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "id" attribute`);
|
||||
} else {
|
||||
this._addError(el, `Unexpected tag`);
|
||||
visitExpansionCase(expansionCase: ml.ExpansionCase, 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) {}
|
||||
|
||||
private _addError(node: ml.Node, message: string): void {
|
||||
this._errors.push(new I18nError(node.sourceSpan, message));
|
||||
|
@ -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);
|
||||
}
|
@ -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[] = [];
|
||||
|
||||
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')}`);
|
||||
}
|
||||
|
||||
digest(message: i18n.Message): string { return digest(message); }
|
||||
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));
|
||||
|
@ -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; }
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -6,12 +6,13 @@
|
||||
* 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',
|
||||
it('should work on emnpty strings',
|
||||
() => { expect(sha1('')).toEqual('da39a3ee5e6b4b0d3255bfef95601890afd80709'); });
|
||||
|
||||
it('should returns the sha1 of "hello world"',
|
||||
@ -54,53 +55,4 @@ export function main(): void {
|
||||
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');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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><i></ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex></i></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><b></ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex></b></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><i></ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex></i></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><b></ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex></b></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><h1></ex></ph>Markers in html comments<ph name="CLOSE_HEADING_LEVEL1"><ex></h1></ex></ph>
|
||||
<ph name="START_TAG_DIV"><ex><div></ex></ph><ph name="CLOSE_TAG_DIV"><ex></div></ex></ph>
|
||||
<ph name="START_TAG_DIV_1"><ex><div></ex></ph><ph name="ICU"/><ph name="CLOSE_TAG_DIV"><ex></div></ex></ph>
|
||||
</msg>
|
||||
<msg id="1491627405349178954">it <ph name="START_BOLD_TEXT"><ex><b></ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> work</msg>
|
||||
</messagebundle>
|
||||
`;
|
||||
<msg id="93a30c67d4e6c9b37aecfe2ac0f2b5d366d7b520">it <ph name="START_BOLD_TEXT"><ex><b></ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> work</msg>
|
||||
</messagebundle>`;
|
||||
|
@ -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[] {
|
||||
|
@ -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 {
|
||||
|
@ -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`)));
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -43,10 +43,10 @@ export function main(): void {
|
||||
<!ELEMENT ex (#PCDATA)>
|
||||
]>
|
||||
<messagebundle>
|
||||
<msg id="7056919470098446707">translatable element <ph name="START_BOLD_TEXT"><ex><b></ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"/></msg>
|
||||
<msg id="2981514368455622387">{VAR_PLURAL, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex></p></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><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>} } } }</msg>
|
||||
<msg id="ec1d033f2436133c14ab038286c4f5df4697484a">translatable element <ph name="START_BOLD_TEXT"><ex><b></ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"/></msg>
|
||||
<msg id="e2ccf3d131b15f54aa1fcf1314b1ca77c14bfcc2">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex></p></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><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></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/);
|
||||
});
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -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', () => {
|
||||
it('should throw when a placeholder is not present in the source message', () => {
|
||||
const HTML = `<div i18n>bar</div>`;
|
||||
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<translationbundle>
|
||||
<translation id="28a86c8a00ae573b2bac698d6609316dc7b4a226"><ph name="UNKNOWN"/></translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(() => {
|
||||
loadAsText(HTML, XTB);
|
||||
}).toThrowError(/The placeholder "UNKNOWN" does not exists in the source message/);
|
||||
});
|
||||
});
|
||||
|
||||
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(() => {
|
||||
loadAsMap(XTB);
|
||||
loadAsText('', 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>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(() => {
|
||||
loadAsMap(XTB);
|
||||
}).toThrowError(/Duplicated translations for msg 1186013544048295927/);
|
||||
});
|
||||
|
||||
it('should throw when trying to save an xtb file',
|
||||
() => { expect(() => { serializer.write([]); }).toThrowError(/Unsupported/); });
|
||||
|
||||
});
|
||||
() => { expect(() => { serializer.write({}); }).toThrowError(/Unsupported/); });
|
||||
});
|
||||
}
|
@ -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"/);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -22,8 +22,5 @@
|
||||
"files": [
|
||||
"index.ts",
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -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';
|
||||
|
@ -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) {
|
||||
|
@ -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 */;
|
||||
|
@ -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!');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,6 @@
|
||||
"../../system.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,6 @@
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
@ -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"
|
||||
}
|
||||
}
|
@ -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()]
|
||||
}
|
@ -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(); }
|
||||
}
|
@ -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>
|
||||
};
|
@ -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));
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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;
|
||||
}
|
@ -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); }
|
||||
}
|
@ -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;
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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}))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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
@ -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;
|
||||
}
|
||||
}
|
@ -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', '&', '>', '<', 'ι'); });
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -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>';
|
||||
}
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
@ -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(''); }
|
||||
});
|
@ -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;
|
||||
}
|
@ -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'); });
|
||||
});
|
@ -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;
|
||||
`
|
||||
}
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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', '&', '>', '<', 'ι'); });
|
||||
|
||||
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);
|
||||
}
|
@ -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"
|
||||
]
|
||||
}
|
@ -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)) {
|
||||
|
@ -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()]]
|
||||
}];
|
@ -24,7 +24,6 @@
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,6 @@
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
@ -79,7 +78,8 @@ export function routerNgProbeToken() {
|
||||
* * `forRoot` creates a module that contains all the directives, the given routes, and the router
|
||||
* 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}
|
||||
];
|
||||
}
|
||||
|
@ -27,7 +27,6 @@
|
||||
"index.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,6 @@
|
||||
"../../../node_modules/zone.js/dist/zone.js.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
3728
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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'
|
||||
|
@ -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';
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
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.
|
||||
tsc.emit(programWithCodegen);
|
||||
|
||||
if (diagnostics) {
|
||||
(ts as any).performance.forEachMeasure(
|
||||
(name: string, duration: number) => { console.error(`TS ${name}: ${duration}ms`); });
|
||||
const metadataWriter = new MetadataWriterHost(host, newProgram, ngOptions);
|
||||
tsc.emit(metadataWriter, newProgram);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
@ -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));
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
12
tools/public_api_guard/core/index.d.ts
vendored
12
tools/public_api_guard/core/index.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
4
tools/public_api_guard/router/index.d.ts
vendored
4
tools/public_api_guard/router/index.d.ts
vendored
@ -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);
|
||||
|
Reference in New Issue
Block a user