repackaging: all the file moves

This commit is contained in:
Igor Minar
2016-04-28 08:02:15 -07:00
committed by Misko Hevery
parent 4fe0f1fa65
commit 505da6c0a8
739 changed files with 0 additions and 52 deletions

View File

@ -0,0 +1,37 @@
/**
* @module
* @description
* Starting point to import all compiler APIs.
*/
export {
PLATFORM_DIRECTIVES,
PLATFORM_PIPES,
COMPILER_PROVIDERS,
TEMPLATE_TRANSFORMS,
CompilerConfig,
RenderTypes,
UrlResolver,
DEFAULT_PACKAGE_URL_PROVIDER,
createOfflineCompileUrlResolver,
XHR,
ViewResolver,
DirectiveResolver,
PipeResolver,
SourceModule,
NormalizedComponentWithViewDirectives,
OfflineCompiler,
CompileMetadataWithIdentifier,
CompileMetadataWithType,
CompileIdentifierMetadata,
CompileDiDependencyMetadata,
CompileProviderMetadata,
CompileFactoryMetadata,
CompileTokenMetadata,
CompileTypeMetadata,
CompileQueryMetadata,
CompileTemplateMetadata,
CompileDirectiveMetadata,
CompilePipeMetadata
} from 'angular2/src/compiler/compiler';
export * from 'angular2/src/compiler/template_ast';

View File

@ -0,0 +1,3 @@
library angular2.core.util.asserions;
void assertArrayOfStrings(String identifier, Object value) {}

View File

@ -0,0 +1,16 @@
import {isArray, isString, isBlank, assertionsEnabled} from '../facade/lang';
import {BaseException} from '../facade/exceptions';
export function assertArrayOfStrings(identifier: string, value: any) {
if (!assertionsEnabled() || isBlank(value)) {
return;
}
if (!isArray(value)) {
throw new BaseException(`Expected '${identifier}' to be an array of strings.`);
}
for (var i = 0; i < value.length; i += 1) {
if (!isString(value[i])) {
throw new BaseException(`Expected '${identifier}' to be an array of strings.`);
}
}
}

View File

@ -0,0 +1,64 @@
export const $EOF = /*@ts2dart_const*/ 0;
export const $TAB = /*@ts2dart_const*/ 9;
export const $LF = /*@ts2dart_const*/ 10;
export const $VTAB = /*@ts2dart_const*/ 11;
export const $FF = /*@ts2dart_const*/ 12;
export const $CR = /*@ts2dart_const*/ 13;
export const $SPACE = /*@ts2dart_const*/ 32;
export const $BANG = /*@ts2dart_const*/ 33;
export const $DQ = /*@ts2dart_const*/ 34;
export const $HASH = /*@ts2dart_const*/ 35;
export const $$ = /*@ts2dart_const*/ 36;
export const $PERCENT = /*@ts2dart_const*/ 37;
export const $AMPERSAND = /*@ts2dart_const*/ 38;
export const $SQ = /*@ts2dart_const*/ 39;
export const $LPAREN = /*@ts2dart_const*/ 40;
export const $RPAREN = /*@ts2dart_const*/ 41;
export const $STAR = /*@ts2dart_const*/ 42;
export const $PLUS = /*@ts2dart_const*/ 43;
export const $COMMA = /*@ts2dart_const*/ 44;
export const $MINUS = /*@ts2dart_const*/ 45;
export const $PERIOD = /*@ts2dart_const*/ 46;
export const $SLASH = /*@ts2dart_const*/ 47;
export const $COLON = /*@ts2dart_const*/ 58;
export const $SEMICOLON = /*@ts2dart_const*/ 59;
export const $LT = /*@ts2dart_const*/ 60;
export const $EQ = /*@ts2dart_const*/ 61;
export const $GT = /*@ts2dart_const*/ 62;
export const $QUESTION = /*@ts2dart_const*/ 63;
export const $0 = /*@ts2dart_const*/ 48;
export const $9 = /*@ts2dart_const*/ 57;
export const $A = /*@ts2dart_const*/ 65;
export const $E = /*@ts2dart_const*/ 69;
export const $Z = /*@ts2dart_const*/ 90;
export const $LBRACKET = /*@ts2dart_const*/ 91;
export const $BACKSLASH = /*@ts2dart_const*/ 92;
export const $RBRACKET = /*@ts2dart_const*/ 93;
export const $CARET = /*@ts2dart_const*/ 94;
export const $_ = /*@ts2dart_const*/ 95;
export const $a = /*@ts2dart_const*/ 97;
export const $e = /*@ts2dart_const*/ 101;
export const $f = /*@ts2dart_const*/ 102;
export const $n = /*@ts2dart_const*/ 110;
export const $r = /*@ts2dart_const*/ 114;
export const $t = /*@ts2dart_const*/ 116;
export const $u = /*@ts2dart_const*/ 117;
export const $v = /*@ts2dart_const*/ 118;
export const $z = /*@ts2dart_const*/ 122;
export const $LBRACE = /*@ts2dart_const*/ 123;
export const $BAR = /*@ts2dart_const*/ 124;
export const $RBRACE = /*@ts2dart_const*/ 125;
export const $NBSP = /*@ts2dart_const*/ 160;
export const $PIPE = /*@ts2dart_const*/ 124;
export const $TILDA = /*@ts2dart_const*/ 126;
export const $AT = /*@ts2dart_const*/ 64;
export function isWhitespace(code: number): boolean {
return (code >= $TAB && code <= $SPACE) || (code == $NBSP);
}

View File

@ -0,0 +1,801 @@
import {
isPresent,
isBlank,
isNumber,
isBoolean,
normalizeBool,
normalizeBlank,
serializeEnum,
Type,
isString,
RegExpWrapper,
StringWrapper,
isArray
} from 'angular2/src/facade/lang';
import {unimplemented, BaseException} from 'angular2/src/facade/exceptions';
import {
StringMapWrapper,
MapWrapper,
SetWrapper,
ListWrapper
} from 'angular2/src/facade/collection';
import {
ChangeDetectionStrategy,
CHANGE_DETECTION_STRATEGY_VALUES
} from 'angular2/src/core/change_detection/change_detection';
import {ViewEncapsulation, VIEW_ENCAPSULATION_VALUES} from 'angular2/src/core/metadata/view';
import {CssSelector} from 'angular2/src/compiler/selector';
import {splitAtColon, sanitizeIdentifier} from './util';
import {LifecycleHooks, LIFECYCLE_HOOKS_VALUES} from 'angular2/src/core/metadata/lifecycle_hooks';
import {getUrlScheme} from './url_resolver';
// group 1: "property" from "[property]"
// group 2: "event" from "(event)"
var HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))$/g;
export abstract class CompileMetadataWithIdentifier {
abstract toJson(): {[key: string]: any};
get identifier(): CompileIdentifierMetadata { return <CompileIdentifierMetadata>unimplemented(); }
}
export abstract class CompileMetadataWithType extends CompileMetadataWithIdentifier {
abstract toJson(): {[key: string]: any};
get type(): CompileTypeMetadata { return <CompileTypeMetadata>unimplemented(); }
get identifier(): CompileIdentifierMetadata { return <CompileIdentifierMetadata>unimplemented(); }
}
export function metadataFromJson(data: {[key: string]: any}): any {
return _COMPILE_METADATA_FROM_JSON[data['class']](data);
}
export class CompileIdentifierMetadata implements CompileMetadataWithIdentifier {
runtime: any;
name: string;
prefix: string;
moduleUrl: string;
value: any;
constructor(
{runtime, name, moduleUrl, prefix, value}:
{runtime?: any, name?: string, moduleUrl?: string, prefix?: string, value?: any} = {}) {
this.runtime = runtime;
this.name = name;
this.prefix = prefix;
this.moduleUrl = moduleUrl;
this.value = value;
}
static fromJson(data: {[key: string]: any}): CompileIdentifierMetadata {
let value = isArray(data['value']) ? _arrayFromJson(data['value'], metadataFromJson) :
_objFromJson(data['value'], metadataFromJson);
return new CompileIdentifierMetadata(
{name: data['name'], prefix: data['prefix'], moduleUrl: data['moduleUrl'], value: value});
}
toJson(): {[key: string]: any} {
let value = isArray(this.value) ? _arrayToJson(this.value) : _objToJson(this.value);
return {
// Note: Runtime type can't be serialized...
'class': 'Identifier',
'name': this.name,
'moduleUrl': this.moduleUrl,
'prefix': this.prefix,
'value': value
};
}
get identifier(): CompileIdentifierMetadata { return this; }
}
export class CompileDiDependencyMetadata {
isAttribute: boolean;
isSelf: boolean;
isHost: boolean;
isSkipSelf: boolean;
isOptional: boolean;
isValue: boolean;
query: CompileQueryMetadata;
viewQuery: CompileQueryMetadata;
token: CompileTokenMetadata;
value: any;
constructor({isAttribute, isSelf, isHost, isSkipSelf, isOptional, isValue, query, viewQuery,
token, value}: {
isAttribute?: boolean,
isSelf?: boolean,
isHost?: boolean,
isSkipSelf?: boolean,
isOptional?: boolean,
isValue?: boolean,
query?: CompileQueryMetadata,
viewQuery?: CompileQueryMetadata,
token?: CompileTokenMetadata,
value?: any
} = {}) {
this.isAttribute = normalizeBool(isAttribute);
this.isSelf = normalizeBool(isSelf);
this.isHost = normalizeBool(isHost);
this.isSkipSelf = normalizeBool(isSkipSelf);
this.isOptional = normalizeBool(isOptional);
this.isValue = normalizeBool(isValue);
this.query = query;
this.viewQuery = viewQuery;
this.token = token;
this.value = value;
}
static fromJson(data: {[key: string]: any}): CompileDiDependencyMetadata {
return new CompileDiDependencyMetadata({
token: _objFromJson(data['token'], CompileTokenMetadata.fromJson),
query: _objFromJson(data['query'], CompileQueryMetadata.fromJson),
viewQuery: _objFromJson(data['viewQuery'], CompileQueryMetadata.fromJson),
value: data['value'],
isAttribute: data['isAttribute'],
isSelf: data['isSelf'],
isHost: data['isHost'],
isSkipSelf: data['isSkipSelf'],
isOptional: data['isOptional'],
isValue: data['isValue']
});
}
toJson(): {[key: string]: any} {
return {
'token': _objToJson(this.token),
'query': _objToJson(this.query),
'viewQuery': _objToJson(this.viewQuery),
'value': this.value,
'isAttribute': this.isAttribute,
'isSelf': this.isSelf,
'isHost': this.isHost,
'isSkipSelf': this.isSkipSelf,
'isOptional': this.isOptional,
'isValue': this.isValue
};
}
}
export class CompileProviderMetadata {
token: CompileTokenMetadata;
useClass: CompileTypeMetadata;
useValue: any;
useExisting: CompileTokenMetadata;
useFactory: CompileFactoryMetadata;
deps: CompileDiDependencyMetadata[];
multi: boolean;
constructor({token, useClass, useValue, useExisting, useFactory, deps, multi}: {
token?: CompileTokenMetadata,
useClass?: CompileTypeMetadata,
useValue?: any,
useExisting?: CompileTokenMetadata,
useFactory?: CompileFactoryMetadata,
deps?: CompileDiDependencyMetadata[],
multi?: boolean
}) {
this.token = token;
this.useClass = useClass;
this.useValue = useValue;
this.useExisting = useExisting;
this.useFactory = useFactory;
this.deps = normalizeBlank(deps);
this.multi = normalizeBool(multi);
}
static fromJson(data: {[key: string]: any}): CompileProviderMetadata {
return new CompileProviderMetadata({
token: _objFromJson(data['token'], CompileTokenMetadata.fromJson),
useClass: _objFromJson(data['useClass'], CompileTypeMetadata.fromJson),
useExisting: _objFromJson(data['useExisting'], CompileTokenMetadata.fromJson),
useValue: _objFromJson(data['useValue'], CompileIdentifierMetadata.fromJson),
useFactory: _objFromJson(data['useFactory'], CompileFactoryMetadata.fromJson),
multi: data['multi'],
deps: _arrayFromJson(data['deps'], CompileDiDependencyMetadata.fromJson)
});
}
toJson(): {[key: string]: any} {
return {
// Note: Runtime type can't be serialized...
'class': 'Provider',
'token': _objToJson(this.token),
'useClass': _objToJson(this.useClass),
'useExisting': _objToJson(this.useExisting),
'useValue': _objToJson(this.useValue),
'useFactory': _objToJson(this.useFactory),
'multi': this.multi,
'deps': _arrayToJson(this.deps)
};
}
}
export class CompileFactoryMetadata implements CompileIdentifierMetadata,
CompileMetadataWithIdentifier {
runtime: Function;
name: string;
prefix: string;
moduleUrl: string;
value: any;
diDeps: CompileDiDependencyMetadata[];
constructor({runtime, name, moduleUrl, prefix, diDeps, value}: {
runtime?: Function,
name?: string,
prefix?: string,
moduleUrl?: string,
value?: boolean,
diDeps?: CompileDiDependencyMetadata[]
}) {
this.runtime = runtime;
this.name = name;
this.prefix = prefix;
this.moduleUrl = moduleUrl;
this.diDeps = _normalizeArray(diDeps);
this.value = value;
}
get identifier(): CompileIdentifierMetadata { return this; }
static fromJson(data: {[key: string]: any}): CompileFactoryMetadata {
return new CompileFactoryMetadata({
name: data['name'],
prefix: data['prefix'],
moduleUrl: data['moduleUrl'],
value: data['value'],
diDeps: _arrayFromJson(data['diDeps'], CompileDiDependencyMetadata.fromJson)
});
}
toJson(): {[key: string]: any} {
return {
'class': 'Factory',
'name': this.name,
'prefix': this.prefix,
'moduleUrl': this.moduleUrl,
'value': this.value,
'diDeps': _arrayToJson(this.diDeps)
};
}
}
export class CompileTokenMetadata implements CompileMetadataWithIdentifier {
value: any;
identifier: CompileIdentifierMetadata;
identifierIsInstance: boolean;
constructor({value, identifier, identifierIsInstance}: {
value?: any,
identifier?: CompileIdentifierMetadata,
identifierIsInstance?: boolean
}) {
this.value = value;
this.identifier = identifier;
this.identifierIsInstance = normalizeBool(identifierIsInstance);
}
static fromJson(data: {[key: string]: any}): CompileTokenMetadata {
return new CompileTokenMetadata({
value: data['value'],
identifier: _objFromJson(data['identifier'], CompileIdentifierMetadata.fromJson),
identifierIsInstance: data['identifierIsInstance']
});
}
toJson(): {[key: string]: any} {
return {
'value': this.value,
'identifier': _objToJson(this.identifier),
'identifierIsInstance': this.identifierIsInstance
};
}
get runtimeCacheKey(): any {
if (isPresent(this.identifier)) {
return this.identifier.runtime;
} else {
return this.value;
}
}
get assetCacheKey(): any {
if (isPresent(this.identifier)) {
return isPresent(this.identifier.moduleUrl) &&
isPresent(getUrlScheme(this.identifier.moduleUrl)) ?
`${this.identifier.name}|${this.identifier.moduleUrl}|${this.identifierIsInstance}` :
null;
} else {
return this.value;
}
}
equalsTo(token2: CompileTokenMetadata): boolean {
var rk = this.runtimeCacheKey;
var ak = this.assetCacheKey;
return (isPresent(rk) && rk == token2.runtimeCacheKey) ||
(isPresent(ak) && ak == token2.assetCacheKey);
}
get name(): string {
return isPresent(this.value) ? sanitizeIdentifier(this.value) : this.identifier.name;
}
}
export class CompileTokenMap<VALUE> {
private _valueMap = new Map<any, VALUE>();
private _values: VALUE[] = [];
add(token: CompileTokenMetadata, value: VALUE) {
var existing = this.get(token);
if (isPresent(existing)) {
throw new BaseException(`Can only add to a TokenMap! Token: ${token.name}`);
}
this._values.push(value);
var rk = token.runtimeCacheKey;
if (isPresent(rk)) {
this._valueMap.set(rk, value);
}
var ak = token.assetCacheKey;
if (isPresent(ak)) {
this._valueMap.set(ak, value);
}
}
get(token: CompileTokenMetadata): VALUE {
var rk = token.runtimeCacheKey;
var ak = token.assetCacheKey;
var result;
if (isPresent(rk)) {
result = this._valueMap.get(rk);
}
if (isBlank(result) && isPresent(ak)) {
result = this._valueMap.get(ak);
}
return result;
}
values(): VALUE[] { return this._values; }
get size(): number { return this._values.length; }
}
/**
* Metadata regarding compilation of a type.
*/
export class CompileTypeMetadata implements CompileIdentifierMetadata, CompileMetadataWithType {
runtime: Type;
name: string;
prefix: string;
moduleUrl: string;
isHost: boolean;
value: any;
diDeps: CompileDiDependencyMetadata[];
constructor({runtime, name, moduleUrl, prefix, isHost, value, diDeps}: {
runtime?: Type,
name?: string,
moduleUrl?: string,
prefix?: string,
isHost?: boolean,
value?: any,
diDeps?: CompileDiDependencyMetadata[]
} = {}) {
this.runtime = runtime;
this.name = name;
this.moduleUrl = moduleUrl;
this.prefix = prefix;
this.isHost = normalizeBool(isHost);
this.value = value;
this.diDeps = _normalizeArray(diDeps);
}
static fromJson(data: {[key: string]: any}): CompileTypeMetadata {
return new CompileTypeMetadata({
name: data['name'],
moduleUrl: data['moduleUrl'],
prefix: data['prefix'],
isHost: data['isHost'],
value: data['value'],
diDeps: _arrayFromJson(data['diDeps'], CompileDiDependencyMetadata.fromJson)
});
}
get identifier(): CompileIdentifierMetadata { return this; }
get type(): CompileTypeMetadata { return this; }
toJson(): {[key: string]: any} {
return {
// Note: Runtime type can't be serialized...
'class': 'Type',
'name': this.name,
'moduleUrl': this.moduleUrl,
'prefix': this.prefix,
'isHost': this.isHost,
'value': this.value,
'diDeps': _arrayToJson(this.diDeps)
};
}
}
export class CompileQueryMetadata {
selectors: Array<CompileTokenMetadata>;
descendants: boolean;
first: boolean;
propertyName: string;
read: CompileTokenMetadata;
constructor({selectors, descendants, first, propertyName, read}: {
selectors?: Array<CompileTokenMetadata>,
descendants?: boolean,
first?: boolean,
propertyName?: string,
read?: CompileTokenMetadata
} = {}) {
this.selectors = selectors;
this.descendants = normalizeBool(descendants);
this.first = normalizeBool(first);
this.propertyName = propertyName;
this.read = read;
}
static fromJson(data: {[key: string]: any}): CompileQueryMetadata {
return new CompileQueryMetadata({
selectors: _arrayFromJson(data['selectors'], CompileTokenMetadata.fromJson),
descendants: data['descendants'],
first: data['first'],
propertyName: data['propertyName'],
read: _objFromJson(data['read'], CompileTokenMetadata.fromJson)
});
}
toJson(): {[key: string]: any} {
return {
'selectors': _arrayToJson(this.selectors),
'descendants': this.descendants,
'first': this.first,
'propertyName': this.propertyName,
'read': _objToJson(this.read)
};
}
}
/**
* Metadata regarding compilation of a template.
*/
export class CompileTemplateMetadata {
encapsulation: ViewEncapsulation;
template: string;
templateUrl: string;
styles: string[];
styleUrls: string[];
ngContentSelectors: string[];
baseUrl: string;
constructor({encapsulation, template, templateUrl, styles, styleUrls, ngContentSelectors,
baseUrl}: {
encapsulation?: ViewEncapsulation,
template?: string,
templateUrl?: string,
styles?: string[],
styleUrls?: string[],
ngContentSelectors?: string[],
baseUrl?: string
} = {}) {
this.encapsulation = isPresent(encapsulation) ? encapsulation : ViewEncapsulation.Emulated;
this.template = template;
this.templateUrl = templateUrl;
this.styles = isPresent(styles) ? styles : [];
this.styleUrls = isPresent(styleUrls) ? styleUrls : [];
this.ngContentSelectors = isPresent(ngContentSelectors) ? ngContentSelectors : [];
this.baseUrl = baseUrl;
}
static fromJson(data: {[key: string]: any}): CompileTemplateMetadata {
return new CompileTemplateMetadata({
encapsulation: isPresent(data['encapsulation']) ?
VIEW_ENCAPSULATION_VALUES[data['encapsulation']] :
data['encapsulation'],
template: data['template'],
templateUrl: data['templateUrl'],
styles: data['styles'],
styleUrls: data['styleUrls'],
ngContentSelectors: data['ngContentSelectors'],
baseUrl: data['baseUrl']
});
}
toJson(): {[key: string]: any} {
return {
'encapsulation':
isPresent(this.encapsulation) ? serializeEnum(this.encapsulation) : this.encapsulation,
'template': this.template,
'templateUrl': this.templateUrl,
'styles': this.styles,
'styleUrls': this.styleUrls,
'ngContentSelectors': this.ngContentSelectors,
'baseUrl': this.baseUrl
};
}
}
/**
* Metadata regarding compilation of a directive.
*/
export class CompileDirectiveMetadata implements CompileMetadataWithType {
static create({type, isComponent, selector, exportAs, changeDetection, inputs, outputs, host,
lifecycleHooks, providers, viewProviders, queries, viewQueries, template}: {
type?: CompileTypeMetadata,
isComponent?: boolean,
selector?: string,
exportAs?: string,
changeDetection?: ChangeDetectionStrategy,
inputs?: string[],
outputs?: string[],
host?: {[key: string]: string},
lifecycleHooks?: LifecycleHooks[],
providers?:
Array<CompileProviderMetadata | CompileTypeMetadata | CompileIdentifierMetadata | any[]>,
viewProviders?:
Array<CompileProviderMetadata | CompileTypeMetadata | CompileIdentifierMetadata | any[]>,
queries?: CompileQueryMetadata[],
viewQueries?: CompileQueryMetadata[],
template?: CompileTemplateMetadata
} = {}): CompileDirectiveMetadata {
var hostListeners: {[key: string]: string} = {};
var hostProperties: {[key: string]: string} = {};
var hostAttributes: {[key: string]: string} = {};
if (isPresent(host)) {
StringMapWrapper.forEach(host, (value: string, key: string) => {
var matches = RegExpWrapper.firstMatch(HOST_REG_EXP, key);
if (isBlank(matches)) {
hostAttributes[key] = value;
} else if (isPresent(matches[1])) {
hostProperties[matches[1]] = value;
} else if (isPresent(matches[2])) {
hostListeners[matches[2]] = value;
}
});
}
var inputsMap: {[key: string]: string} = {};
if (isPresent(inputs)) {
inputs.forEach((bindConfig: string) => {
// canonical syntax: `dirProp: elProp`
// if there is no `:`, use dirProp = elProp
var parts = splitAtColon(bindConfig, [bindConfig, bindConfig]);
inputsMap[parts[0]] = parts[1];
});
}
var outputsMap: {[key: string]: string} = {};
if (isPresent(outputs)) {
outputs.forEach((bindConfig: string) => {
// canonical syntax: `dirProp: elProp`
// if there is no `:`, use dirProp = elProp
var parts = splitAtColon(bindConfig, [bindConfig, bindConfig]);
outputsMap[parts[0]] = parts[1];
});
}
return new CompileDirectiveMetadata({
type: type,
isComponent: normalizeBool(isComponent),
selector: selector,
exportAs: exportAs,
changeDetection: changeDetection,
inputs: inputsMap,
outputs: outputsMap,
hostListeners: hostListeners,
hostProperties: hostProperties,
hostAttributes: hostAttributes,
lifecycleHooks: isPresent(lifecycleHooks) ? lifecycleHooks : [],
providers: providers,
viewProviders: viewProviders,
queries: queries,
viewQueries: viewQueries,
template: template
});
}
type: CompileTypeMetadata;
isComponent: boolean;
selector: string;
exportAs: string;
changeDetection: ChangeDetectionStrategy;
inputs: {[key: string]: string};
outputs: {[key: string]: string};
hostListeners: {[key: string]: string};
hostProperties: {[key: string]: string};
hostAttributes: {[key: string]: string};
lifecycleHooks: LifecycleHooks[];
providers: CompileProviderMetadata[];
viewProviders: CompileProviderMetadata[];
queries: CompileQueryMetadata[];
viewQueries: CompileQueryMetadata[];
template: CompileTemplateMetadata;
constructor({type, isComponent, selector, exportAs, changeDetection, inputs, outputs,
hostListeners, hostProperties, hostAttributes, lifecycleHooks, providers,
viewProviders, queries, viewQueries, template}: {
type?: CompileTypeMetadata,
isComponent?: boolean,
selector?: string,
exportAs?: string,
changeDetection?: ChangeDetectionStrategy,
inputs?: {[key: string]: string},
outputs?: {[key: string]: string},
hostListeners?: {[key: string]: string},
hostProperties?: {[key: string]: string},
hostAttributes?: {[key: string]: string},
lifecycleHooks?: LifecycleHooks[],
providers?:
Array<CompileProviderMetadata | CompileTypeMetadata | CompileIdentifierMetadata | any[]>,
viewProviders?:
Array<CompileProviderMetadata | CompileTypeMetadata | CompileIdentifierMetadata | any[]>,
queries?: CompileQueryMetadata[],
viewQueries?: CompileQueryMetadata[],
template?: CompileTemplateMetadata
} = {}) {
this.type = type;
this.isComponent = isComponent;
this.selector = selector;
this.exportAs = exportAs;
this.changeDetection = changeDetection;
this.inputs = inputs;
this.outputs = outputs;
this.hostListeners = hostListeners;
this.hostProperties = hostProperties;
this.hostAttributes = hostAttributes;
this.lifecycleHooks = _normalizeArray(lifecycleHooks);
this.providers = _normalizeArray(providers);
this.viewProviders = _normalizeArray(viewProviders);
this.queries = _normalizeArray(queries);
this.viewQueries = _normalizeArray(viewQueries);
this.template = template;
}
get identifier(): CompileIdentifierMetadata { return this.type; }
static fromJson(data: {[key: string]: any}): CompileDirectiveMetadata {
return new CompileDirectiveMetadata({
isComponent: data['isComponent'],
selector: data['selector'],
exportAs: data['exportAs'],
type: isPresent(data['type']) ? CompileTypeMetadata.fromJson(data['type']) : data['type'],
changeDetection: isPresent(data['changeDetection']) ?
CHANGE_DETECTION_STRATEGY_VALUES[data['changeDetection']] :
data['changeDetection'],
inputs: data['inputs'],
outputs: data['outputs'],
hostListeners: data['hostListeners'],
hostProperties: data['hostProperties'],
hostAttributes: data['hostAttributes'],
lifecycleHooks:
(<any[]>data['lifecycleHooks']).map(hookValue => LIFECYCLE_HOOKS_VALUES[hookValue]),
template: isPresent(data['template']) ? CompileTemplateMetadata.fromJson(data['template']) :
data['template'],
providers: _arrayFromJson(data['providers'], metadataFromJson),
viewProviders: _arrayFromJson(data['viewProviders'], metadataFromJson),
queries: _arrayFromJson(data['queries'], CompileQueryMetadata.fromJson),
viewQueries: _arrayFromJson(data['viewQueries'], CompileQueryMetadata.fromJson)
});
}
toJson(): {[key: string]: any} {
return {
'class': 'Directive',
'isComponent': this.isComponent,
'selector': this.selector,
'exportAs': this.exportAs,
'type': isPresent(this.type) ? this.type.toJson() : this.type,
'changeDetection': isPresent(this.changeDetection) ? serializeEnum(this.changeDetection) :
this.changeDetection,
'inputs': this.inputs,
'outputs': this.outputs,
'hostListeners': this.hostListeners,
'hostProperties': this.hostProperties,
'hostAttributes': this.hostAttributes,
'lifecycleHooks': this.lifecycleHooks.map(hook => serializeEnum(hook)),
'template': isPresent(this.template) ? this.template.toJson() : this.template,
'providers': _arrayToJson(this.providers),
'viewProviders': _arrayToJson(this.viewProviders),
'queries': _arrayToJson(this.queries),
'viewQueries': _arrayToJson(this.viewQueries)
};
}
}
/**
* Construct {@link CompileDirectiveMetadata} from {@link ComponentTypeMetadata} and a selector.
*/
export function createHostComponentMeta(componentType: CompileTypeMetadata,
componentSelector: string): CompileDirectiveMetadata {
var template = CssSelector.parse(componentSelector)[0].getMatchingElementTemplate();
return CompileDirectiveMetadata.create({
type: new CompileTypeMetadata({
runtime: Object,
name: `${componentType.name}_Host`,
moduleUrl: componentType.moduleUrl,
isHost: true
}),
template: new CompileTemplateMetadata(
{template: template, templateUrl: '', styles: [], styleUrls: [], ngContentSelectors: []}),
changeDetection: ChangeDetectionStrategy.Default,
inputs: [],
outputs: [],
host: {},
lifecycleHooks: [],
isComponent: true,
selector: '*',
providers: [],
viewProviders: [],
queries: [],
viewQueries: []
});
}
export class CompilePipeMetadata implements CompileMetadataWithType {
type: CompileTypeMetadata;
name: string;
pure: boolean;
lifecycleHooks: LifecycleHooks[];
constructor({type, name, pure, lifecycleHooks}: {
type?: CompileTypeMetadata,
name?: string,
pure?: boolean,
lifecycleHooks?: LifecycleHooks[]
} = {}) {
this.type = type;
this.name = name;
this.pure = normalizeBool(pure);
this.lifecycleHooks = _normalizeArray(lifecycleHooks);
}
get identifier(): CompileIdentifierMetadata { return this.type; }
static fromJson(data: {[key: string]: any}): CompilePipeMetadata {
return new CompilePipeMetadata({
type: isPresent(data['type']) ? CompileTypeMetadata.fromJson(data['type']) : data['type'],
name: data['name'],
pure: data['pure']
});
}
toJson(): {[key: string]: any} {
return {
'class': 'Pipe',
'type': isPresent(this.type) ? this.type.toJson() : null,
'name': this.name,
'pure': this.pure
};
}
}
var _COMPILE_METADATA_FROM_JSON = {
'Directive': CompileDirectiveMetadata.fromJson,
'Pipe': CompilePipeMetadata.fromJson,
'Type': CompileTypeMetadata.fromJson,
'Provider': CompileProviderMetadata.fromJson,
'Identifier': CompileIdentifierMetadata.fromJson,
'Factory': CompileFactoryMetadata.fromJson
};
function _arrayFromJson(obj: any[], fn: (a: {[key: string]: any}) => any): any {
return isBlank(obj) ? null : obj.map(o => _objFromJson(o, fn));
}
function _arrayToJson(obj: any[]): string | {[key: string]: any} {
return isBlank(obj) ? null : obj.map(_objToJson);
}
function _objFromJson(obj: any, fn: (a: {[key: string]: any}) => any): any {
if (isArray(obj)) return _arrayFromJson(obj, fn);
if (isString(obj) || isBlank(obj) || isBoolean(obj) || isNumber(obj)) return obj;
return fn(obj);
}
function _objToJson(obj: any): string | {[key: string]: any} {
if (isArray(obj)) return _arrayToJson(obj);
if (isString(obj) || isBlank(obj) || isBoolean(obj) || isNumber(obj)) return obj;
return obj.toJson();
}
function _normalizeArray(obj: any[]): any[] {
return isPresent(obj) ? obj : [];
}

View File

@ -0,0 +1,62 @@
export {PLATFORM_DIRECTIVES, PLATFORM_PIPES} from 'angular2/src/core/platform_directives_and_pipes';
export * from 'angular2/src/compiler/template_ast';
export {TEMPLATE_TRANSFORMS} from 'angular2/src/compiler/template_parser';
export {CompilerConfig, RenderTypes} from './config';
export * from './compile_metadata';
export * from './offline_compiler';
export {RuntimeCompiler} from './runtime_compiler';
export * from 'angular2/src/compiler/url_resolver';
export * from 'angular2/src/compiler/xhr';
export {ViewResolver} from './view_resolver';
export {DirectiveResolver} from './directive_resolver';
export {PipeResolver} from './pipe_resolver';
import {assertionsEnabled, Type} from 'angular2/src/facade/lang';
import {TemplateParser} from 'angular2/src/compiler/template_parser';
import {HtmlParser} from 'angular2/src/compiler/html_parser';
import {DirectiveNormalizer} from 'angular2/src/compiler/directive_normalizer';
import {CompileMetadataResolver} from 'angular2/src/compiler/metadata_resolver';
import {StyleCompiler} from 'angular2/src/compiler/style_compiler';
import {ViewCompiler} from 'angular2/src/compiler/view_compiler/view_compiler';
import {CompilerConfig} from './config';
import {ComponentResolver} from 'angular2/src/core/linker/component_resolver';
import {RuntimeCompiler} from 'angular2/src/compiler/runtime_compiler';
import {ElementSchemaRegistry} from 'angular2/src/compiler/schema/element_schema_registry';
import {DomElementSchemaRegistry} from 'angular2/src/compiler/schema/dom_element_schema_registry';
import {UrlResolver, DEFAULT_PACKAGE_URL_PROVIDER} from 'angular2/src/compiler/url_resolver';
import {Parser} from './expression_parser/parser';
import {Lexer} from './expression_parser/lexer';
import {ViewResolver} from './view_resolver';
import {DirectiveResolver} from './directive_resolver';
import {PipeResolver} from './pipe_resolver';
function _createCompilerConfig() {
return new CompilerConfig(assertionsEnabled(), false, true);
}
/**
* A set of providers that provide `RuntimeCompiler` and its dependencies to use for
* template compilation.
*/
export const COMPILER_PROVIDERS: Array<any | Type | {[k: string]: any} | any[]> =
/*@ts2dart_const*/[
Lexer,
Parser,
HtmlParser,
TemplateParser,
DirectiveNormalizer,
CompileMetadataResolver,
DEFAULT_PACKAGE_URL_PROVIDER,
StyleCompiler,
ViewCompiler,
/*@ts2dart_Provider*/ {provide: CompilerConfig, useFactory: _createCompilerConfig, deps: []},
RuntimeCompiler,
/*@ts2dart_Provider*/ {provide: ComponentResolver, useExisting: RuntimeCompiler},
DomElementSchemaRegistry,
/*@ts2dart_Provider*/ {provide: ElementSchemaRegistry, useExisting: DomElementSchemaRegistry},
UrlResolver,
ViewResolver,
DirectiveResolver,
PipeResolver
];

View File

@ -0,0 +1,38 @@
import {isBlank} from 'angular2/src/facade/lang';
import {unimplemented} from 'angular2/src/facade/exceptions';
import {Identifiers} from './identifiers';
import {CompileIdentifierMetadata} from './compile_metadata';
export class CompilerConfig {
public renderTypes: RenderTypes;
constructor(public genDebugInfo: boolean, public logBindingUpdate: boolean,
public useJit: boolean, renderTypes: RenderTypes = null) {
if (isBlank(renderTypes)) {
renderTypes = new DefaultRenderTypes();
}
this.renderTypes = renderTypes;
}
}
/**
* Types used for the renderer.
* Can be replaced to specialize the generated output to a specific renderer
* to help tree shaking.
*/
export abstract class RenderTypes {
get renderer(): CompileIdentifierMetadata { return unimplemented(); }
get renderText(): CompileIdentifierMetadata { return unimplemented(); }
get renderElement(): CompileIdentifierMetadata { return unimplemented(); }
get renderComment(): CompileIdentifierMetadata { return unimplemented(); }
get renderNode(): CompileIdentifierMetadata { return unimplemented(); }
get renderEvent(): CompileIdentifierMetadata { return unimplemented(); }
}
export class DefaultRenderTypes implements RenderTypes {
renderer = Identifiers.Renderer;
renderText = null;
renderElement = null;
renderComment = null;
renderNode = null;
renderEvent = null;
}

View File

@ -0,0 +1,756 @@
import {NumberWrapper, StringWrapper, isPresent, resolveEnumToken} from "angular2/src/facade/lang";
import {BaseException} from 'angular2/src/facade/exceptions';
import {
isWhitespace,
$EOF,
$HASH,
$TILDA,
$CARET,
$PERCENT,
$$,
$_,
$COLON,
$SQ,
$DQ,
$EQ,
$SLASH,
$BACKSLASH,
$PERIOD,
$STAR,
$PLUS,
$LPAREN,
$RPAREN,
$LBRACE,
$RBRACE,
$LBRACKET,
$RBRACKET,
$PIPE,
$COMMA,
$SEMICOLON,
$MINUS,
$BANG,
$QUESTION,
$AT,
$AMPERSAND,
$GT,
$a,
$A,
$z,
$Z,
$0,
$9,
$FF,
$CR,
$LF,
$VTAB
} from "angular2/src/compiler/chars";
export {
$EOF,
$AT,
$RBRACE,
$LBRACE,
$LBRACKET,
$RBRACKET,
$LPAREN,
$RPAREN,
$COMMA,
$COLON,
$SEMICOLON,
isWhitespace
} from "angular2/src/compiler/chars";
export enum CssTokenType {
EOF,
String,
Comment,
Identifier,
Number,
IdentifierOrNumber,
AtKeyword,
Character,
Whitespace,
Invalid
}
export enum CssLexerMode {
ALL,
ALL_TRACK_WS,
SELECTOR,
PSEUDO_SELECTOR,
ATTRIBUTE_SELECTOR,
AT_RULE_QUERY,
MEDIA_QUERY,
BLOCK,
KEYFRAME_BLOCK,
STYLE_BLOCK,
STYLE_VALUE,
STYLE_VALUE_FUNCTION,
STYLE_CALC_FUNCTION
}
export class LexedCssResult {
constructor(public error: CssScannerError, public token: CssToken) {}
}
export function generateErrorMessage(input: string, message: string, errorValue: string,
index: number, row: number, column: number): string {
return `${message} at column ${row}:${column} in expression [` +
findProblemCode(input, errorValue, index, column) + ']';
}
export function findProblemCode(input: string, errorValue: string, index: number,
column: number): string {
var endOfProblemLine = index;
var current = charCode(input, index);
while (current > 0 && !isNewline(current)) {
current = charCode(input, ++endOfProblemLine);
}
var choppedString = input.substring(0, endOfProblemLine);
var pointerPadding = "";
for (var i = 0; i < column; i++) {
pointerPadding += " ";
}
var pointerString = "";
for (var i = 0; i < errorValue.length; i++) {
pointerString += "^";
}
return choppedString + "\n" + pointerPadding + pointerString + "\n";
}
export class CssToken {
numValue: number;
constructor(public index: number, public column: number, public line: number,
public type: CssTokenType, public strValue: string) {
this.numValue = charCode(strValue, 0);
}
}
export class CssLexer {
scan(text: string, trackComments: boolean = false): CssScanner {
return new CssScanner(text, trackComments);
}
}
export class CssScannerError extends BaseException {
public rawMessage: string;
public message: string;
constructor(public token: CssToken, message) {
super('Css Parse Error: ' + message);
this.rawMessage = message;
}
toString(): string { return this.message; }
}
function _trackWhitespace(mode: CssLexerMode) {
switch (mode) {
case CssLexerMode.SELECTOR:
case CssLexerMode.ALL_TRACK_WS:
case CssLexerMode.STYLE_VALUE:
return true;
default:
return false;
}
}
export class CssScanner {
peek: number;
peekPeek: number;
length: number = 0;
index: number = -1;
column: number = -1;
line: number = 0;
/** @internal */
_currentMode: CssLexerMode = CssLexerMode.BLOCK;
/** @internal */
_currentError: CssScannerError = null;
constructor(public input: string, private _trackComments: boolean = false) {
this.length = this.input.length;
this.peekPeek = this.peekAt(0);
this.advance();
}
getMode(): CssLexerMode { return this._currentMode; }
setMode(mode: CssLexerMode) {
if (this._currentMode != mode) {
if (_trackWhitespace(this._currentMode)) {
this.consumeWhitespace();
}
this._currentMode = mode;
}
}
advance(): void {
if (isNewline(this.peek)) {
this.column = 0;
this.line++;
} else {
this.column++;
}
this.index++;
this.peek = this.peekPeek;
this.peekPeek = this.peekAt(this.index + 1);
}
peekAt(index: number): number {
return index >= this.length ? $EOF : StringWrapper.charCodeAt(this.input, index);
}
consumeEmptyStatements(): void {
this.consumeWhitespace();
while (this.peek == $SEMICOLON) {
this.advance();
this.consumeWhitespace();
}
}
consumeWhitespace(): void {
while (isWhitespace(this.peek) || isNewline(this.peek)) {
this.advance();
if (!this._trackComments && isCommentStart(this.peek, this.peekPeek)) {
this.advance(); // /
this.advance(); // *
while (!isCommentEnd(this.peek, this.peekPeek)) {
if (this.peek == $EOF) {
this.error('Unterminated comment');
}
this.advance();
}
this.advance(); // *
this.advance(); // /
}
}
}
consume(type: CssTokenType, value: string = null): LexedCssResult {
var mode = this._currentMode;
this.setMode(CssLexerMode.ALL);
var previousIndex = this.index;
var previousLine = this.line;
var previousColumn = this.column;
var output = this.scan();
// just incase the inner scan method returned an error
if (isPresent(output.error)) {
this.setMode(mode);
return output;
}
var next = output.token;
if (!isPresent(next)) {
next = new CssToken(0, 0, 0, CssTokenType.EOF, "end of file");
}
var isMatchingType;
if (type == CssTokenType.IdentifierOrNumber) {
// TODO (matsko): implement array traversal for lookup here
isMatchingType = next.type == CssTokenType.Number || next.type == CssTokenType.Identifier;
} else {
isMatchingType = next.type == type;
}
// before throwing the error we need to bring back the former
// mode so that the parser can recover...
this.setMode(mode);
var error = null;
if (!isMatchingType || (isPresent(value) && value != next.strValue)) {
var errorMessage = resolveEnumToken(CssTokenType, next.type) + " does not match expected " +
resolveEnumToken(CssTokenType, type) + " value";
if (isPresent(value)) {
errorMessage += ' ("' + next.strValue + '" should match "' + value + '")';
}
error = new CssScannerError(
next, generateErrorMessage(this.input, errorMessage, next.strValue, previousIndex,
previousLine, previousColumn));
}
return new LexedCssResult(error, next);
}
scan(): LexedCssResult {
var trackWS = _trackWhitespace(this._currentMode);
if (this.index == 0 && !trackWS) { // first scan
this.consumeWhitespace();
}
var token = this._scan();
if (token == null) return null;
var error = this._currentError;
this._currentError = null;
if (!trackWS) {
this.consumeWhitespace();
}
return new LexedCssResult(error, token);
}
/** @internal */
_scan(): CssToken {
var peek = this.peek;
var peekPeek = this.peekPeek;
if (peek == $EOF) return null;
if (isCommentStart(peek, peekPeek)) {
// even if comments are not tracked we still lex the
// comment so we can move the pointer forward
var commentToken = this.scanComment();
if (this._trackComments) {
return commentToken;
}
}
if (_trackWhitespace(this._currentMode) && (isWhitespace(peek) || isNewline(peek))) {
return this.scanWhitespace();
}
peek = this.peek;
peekPeek = this.peekPeek;
if (peek == $EOF) return null;
if (isStringStart(peek, peekPeek)) {
return this.scanString();
}
// something like url(cool)
if (this._currentMode == CssLexerMode.STYLE_VALUE_FUNCTION) {
return this.scanCssValueFunction();
}
var isModifier = peek == $PLUS || peek == $MINUS;
var digitA = isModifier ? false : isDigit(peek);
var digitB = isDigit(peekPeek);
if (digitA || (isModifier && (peekPeek == $PERIOD || digitB)) || (peek == $PERIOD && digitB)) {
return this.scanNumber();
}
if (peek == $AT) {
return this.scanAtExpression();
}
if (isIdentifierStart(peek, peekPeek)) {
return this.scanIdentifier();
}
if (isValidCssCharacter(peek, this._currentMode)) {
return this.scanCharacter();
}
return this.error(`Unexpected character [${StringWrapper.fromCharCode(peek)}]`);
}
scanComment(): CssToken {
if (this.assertCondition(isCommentStart(this.peek, this.peekPeek),
"Expected comment start value")) {
return null;
}
var start = this.index;
var startingColumn = this.column;
var startingLine = this.line;
this.advance(); // /
this.advance(); // *
while (!isCommentEnd(this.peek, this.peekPeek)) {
if (this.peek == $EOF) {
this.error('Unterminated comment');
}
this.advance();
}
this.advance(); // *
this.advance(); // /
var str = this.input.substring(start, this.index);
return new CssToken(start, startingColumn, startingLine, CssTokenType.Comment, str);
}
scanWhitespace(): CssToken {
var start = this.index;
var startingColumn = this.column;
var startingLine = this.line;
while (isWhitespace(this.peek) && this.peek != $EOF) {
this.advance();
}
var str = this.input.substring(start, this.index);
return new CssToken(start, startingColumn, startingLine, CssTokenType.Whitespace, str);
}
scanString(): CssToken {
if (this.assertCondition(isStringStart(this.peek, this.peekPeek),
"Unexpected non-string starting value")) {
return null;
}
var target = this.peek;
var start = this.index;
var startingColumn = this.column;
var startingLine = this.line;
var previous = target;
this.advance();
while (!isCharMatch(target, previous, this.peek)) {
if (this.peek == $EOF || isNewline(this.peek)) {
this.error('Unterminated quote');
}
previous = this.peek;
this.advance();
}
if (this.assertCondition(this.peek == target, "Unterminated quote")) {
return null;
}
this.advance();
var str = this.input.substring(start, this.index);
return new CssToken(start, startingColumn, startingLine, CssTokenType.String, str);
}
scanNumber(): CssToken {
var start = this.index;
var startingColumn = this.column;
if (this.peek == $PLUS || this.peek == $MINUS) {
this.advance();
}
var periodUsed = false;
while (isDigit(this.peek) || this.peek == $PERIOD) {
if (this.peek == $PERIOD) {
if (periodUsed) {
this.error('Unexpected use of a second period value');
}
periodUsed = true;
}
this.advance();
}
var strValue = this.input.substring(start, this.index);
return new CssToken(start, startingColumn, this.line, CssTokenType.Number, strValue);
}
scanIdentifier(): CssToken {
if (this.assertCondition(isIdentifierStart(this.peek, this.peekPeek),
'Expected identifier starting value')) {
return null;
}
var start = this.index;
var startingColumn = this.column;
while (isIdentifierPart(this.peek)) {
this.advance();
}
var strValue = this.input.substring(start, this.index);
return new CssToken(start, startingColumn, this.line, CssTokenType.Identifier, strValue);
}
scanCssValueFunction(): CssToken {
var start = this.index;
var startingColumn = this.column;
while (this.peek != $EOF && this.peek != $RPAREN) {
this.advance();
}
var strValue = this.input.substring(start, this.index);
return new CssToken(start, startingColumn, this.line, CssTokenType.Identifier, strValue);
}
scanCharacter(): CssToken {
var start = this.index;
var startingColumn = this.column;
if (this.assertCondition(isValidCssCharacter(this.peek, this._currentMode),
charStr(this.peek) + ' is not a valid CSS character')) {
return null;
}
var c = this.input.substring(start, start + 1);
this.advance();
return new CssToken(start, startingColumn, this.line, CssTokenType.Character, c);
}
scanAtExpression(): CssToken {
if (this.assertCondition(this.peek == $AT, 'Expected @ value')) {
return null;
}
var start = this.index;
var startingColumn = this.column;
this.advance();
if (isIdentifierStart(this.peek, this.peekPeek)) {
var ident = this.scanIdentifier();
var strValue = '@' + ident.strValue;
return new CssToken(start, startingColumn, this.line, CssTokenType.AtKeyword, strValue);
} else {
return this.scanCharacter();
}
}
assertCondition(status: boolean, errorMessage: string): boolean {
if (!status) {
this.error(errorMessage);
return true;
}
return false;
}
error(message: string, errorTokenValue: string = null, doNotAdvance: boolean = false): CssToken {
var index: number = this.index;
var column: number = this.column;
var line: number = this.line;
errorTokenValue =
isPresent(errorTokenValue) ? errorTokenValue : StringWrapper.fromCharCode(this.peek);
var invalidToken = new CssToken(index, column, line, CssTokenType.Invalid, errorTokenValue);
var errorMessage =
generateErrorMessage(this.input, message, errorTokenValue, index, line, column);
if (!doNotAdvance) {
this.advance();
}
this._currentError = new CssScannerError(invalidToken, errorMessage);
return invalidToken;
}
}
function isAtKeyword(current: CssToken, next: CssToken): boolean {
return current.numValue == $AT && next.type == CssTokenType.Identifier;
}
function isCharMatch(target: number, previous: number, code: number): boolean {
return code == target && previous != $BACKSLASH;
}
function isDigit(code: number): boolean {
return $0 <= code && code <= $9;
}
function isCommentStart(code: number, next: number): boolean {
return code == $SLASH && next == $STAR;
}
function isCommentEnd(code: number, next: number): boolean {
return code == $STAR && next == $SLASH;
}
function isStringStart(code: number, next: number): boolean {
var target = code;
if (target == $BACKSLASH) {
target = next;
}
return target == $DQ || target == $SQ;
}
function isIdentifierStart(code: number, next: number): boolean {
var target = code;
if (target == $MINUS) {
target = next;
}
return ($a <= target && target <= $z) || ($A <= target && target <= $Z) || target == $BACKSLASH ||
target == $MINUS || target == $_;
}
function isIdentifierPart(target: number): boolean {
return ($a <= target && target <= $z) || ($A <= target && target <= $Z) || target == $BACKSLASH ||
target == $MINUS || target == $_ || isDigit(target);
}
function isValidPseudoSelectorCharacter(code: number): boolean {
switch (code) {
case $LPAREN:
case $RPAREN:
return true;
default:
return false;
}
}
function isValidKeyframeBlockCharacter(code: number): boolean {
return code == $PERCENT;
}
function isValidAttributeSelectorCharacter(code: number): boolean {
// value^*|$~=something
switch (code) {
case $$:
case $PIPE:
case $CARET:
case $TILDA:
case $STAR:
case $EQ:
return true;
default:
return false;
}
}
function isValidSelectorCharacter(code: number): boolean {
// selector [ key = value ]
// IDENT C IDENT C IDENT C
// #id, .class, *+~>
// tag:PSEUDO
switch (code) {
case $HASH:
case $PERIOD:
case $TILDA:
case $STAR:
case $PLUS:
case $GT:
case $COLON:
case $PIPE:
case $COMMA:
return true;
default:
return false;
}
}
function isValidStyleBlockCharacter(code: number): boolean {
// key:value;
// key:calc(something ... )
switch (code) {
case $HASH:
case $SEMICOLON:
case $COLON:
case $PERCENT:
case $SLASH:
case $BACKSLASH:
case $BANG:
case $PERIOD:
case $LPAREN:
case $RPAREN:
return true;
default:
return false;
}
}
function isValidMediaQueryRuleCharacter(code: number): boolean {
// (min-width: 7.5em) and (orientation: landscape)
switch (code) {
case $LPAREN:
case $RPAREN:
case $COLON:
case $PERCENT:
case $PERIOD:
return true;
default:
return false;
}
}
function isValidAtRuleCharacter(code: number): boolean {
// @document url(http://www.w3.org/page?something=on#hash),
switch (code) {
case $LPAREN:
case $RPAREN:
case $COLON:
case $PERCENT:
case $PERIOD:
case $SLASH:
case $BACKSLASH:
case $HASH:
case $EQ:
case $QUESTION:
case $AMPERSAND:
case $STAR:
case $COMMA:
case $MINUS:
case $PLUS:
return true;
default:
return false;
}
}
function isValidStyleFunctionCharacter(code: number): boolean {
switch (code) {
case $PERIOD:
case $MINUS:
case $PLUS:
case $STAR:
case $SLASH:
case $LPAREN:
case $RPAREN:
case $COMMA:
return true;
default:
return false;
}
}
function isValidBlockCharacter(code: number): boolean {
// @something { }
// IDENT
return code == $AT;
}
function isValidCssCharacter(code: number, mode: CssLexerMode): boolean {
switch (mode) {
case CssLexerMode.ALL:
case CssLexerMode.ALL_TRACK_WS:
return true;
case CssLexerMode.SELECTOR:
return isValidSelectorCharacter(code);
case CssLexerMode.PSEUDO_SELECTOR:
return isValidPseudoSelectorCharacter(code);
case CssLexerMode.ATTRIBUTE_SELECTOR:
return isValidAttributeSelectorCharacter(code);
case CssLexerMode.MEDIA_QUERY:
return isValidMediaQueryRuleCharacter(code);
case CssLexerMode.AT_RULE_QUERY:
return isValidAtRuleCharacter(code);
case CssLexerMode.KEYFRAME_BLOCK:
return isValidKeyframeBlockCharacter(code);
case CssLexerMode.STYLE_BLOCK:
case CssLexerMode.STYLE_VALUE:
return isValidStyleBlockCharacter(code);
case CssLexerMode.STYLE_CALC_FUNCTION:
return isValidStyleFunctionCharacter(code);
case CssLexerMode.BLOCK:
return isValidBlockCharacter(code);
default:
return false;
}
}
function charCode(input, index): number {
return index >= input.length ? $EOF : StringWrapper.charCodeAt(input, index);
}
function charStr(code: number): string {
return StringWrapper.fromCharCode(code);
}
export function isNewline(code): boolean {
switch (code) {
case $FF:
case $CR:
case $LF:
case $VTAB:
return true;
default:
return false;
}
}

View File

@ -0,0 +1,740 @@
import {
ParseSourceSpan,
ParseSourceFile,
ParseLocation,
ParseError
} from "angular2/src/compiler/parse_util";
import {
bitWiseOr,
bitWiseAnd,
NumberWrapper,
StringWrapper,
isPresent
} from "angular2/src/facade/lang";
import {
CssLexerMode,
CssToken,
CssTokenType,
CssScanner,
CssScannerError,
generateErrorMessage,
$AT,
$EOF,
$RBRACE,
$LBRACE,
$LBRACKET,
$RBRACKET,
$LPAREN,
$RPAREN,
$COMMA,
$COLON,
$SEMICOLON,
isNewline
} from "angular2/src/compiler/css/lexer";
export {CssToken} from "angular2/src/compiler/css/lexer";
export enum BlockType {
Import,
Charset,
Namespace,
Supports,
Keyframes,
MediaQuery,
Selector,
FontFace,
Page,
Document,
Viewport,
Unsupported
}
const EOF_DELIM = 1;
const RBRACE_DELIM = 2;
const LBRACE_DELIM = 4;
const COMMA_DELIM = 8;
const COLON_DELIM = 16;
const SEMICOLON_DELIM = 32;
const NEWLINE_DELIM = 64;
const RPAREN_DELIM = 128;
function mergeTokens(tokens: CssToken[], separator: string = ""): CssToken {
var mainToken = tokens[0];
var str = mainToken.strValue;
for (var i = 1; i < tokens.length; i++) {
str += separator + tokens[i].strValue;
}
return new CssToken(mainToken.index, mainToken.column, mainToken.line, mainToken.type, str);
}
function getDelimFromToken(token: CssToken): number {
return getDelimFromCharacter(token.numValue);
}
function getDelimFromCharacter(code: number): number {
switch (code) {
case $EOF:
return EOF_DELIM;
case $COMMA:
return COMMA_DELIM;
case $COLON:
return COLON_DELIM;
case $SEMICOLON:
return SEMICOLON_DELIM;
case $RBRACE:
return RBRACE_DELIM;
case $LBRACE:
return LBRACE_DELIM;
case $RPAREN:
return RPAREN_DELIM;
default:
return isNewline(code) ? NEWLINE_DELIM : 0;
}
}
function characterContainsDelimiter(code: number, delimiters: number): boolean {
return bitWiseAnd([getDelimFromCharacter(code), delimiters]) > 0;
}
export class CssAST {
visit(visitor: CssASTVisitor, context?: any): void {}
}
export interface CssASTVisitor {
visitCssValue(ast: CssStyleValueAST, context?: any): void;
visitInlineCssRule(ast: CssInlineRuleAST, context?: any): void;
visitCssKeyframeRule(ast: CssKeyframeRuleAST, context?: any): void;
visitCssKeyframeDefinition(ast: CssKeyframeDefinitionAST, context?: any): void;
visitCssMediaQueryRule(ast: CssMediaQueryRuleAST, context?: any): void;
visitCssSelectorRule(ast: CssSelectorRuleAST, context?: any): void;
visitCssSelector(ast: CssSelectorAST, context?: any): void;
visitCssDefinition(ast: CssDefinitionAST, context?: any): void;
visitCssBlock(ast: CssBlockAST, context?: any): void;
visitCssStyleSheet(ast: CssStyleSheetAST, context?: any): void;
visitUnkownRule(ast: CssUnknownTokenListAST, context?: any): void;
}
export class ParsedCssResult {
constructor(public errors: CssParseError[], public ast: CssStyleSheetAST) {}
}
export class CssParser {
private _errors: CssParseError[] = [];
private _file: ParseSourceFile;
constructor(private _scanner: CssScanner, private _fileName: string) {
this._file = new ParseSourceFile(this._scanner.input, _fileName);
}
/** @internal */
_resolveBlockType(token: CssToken): BlockType {
switch (token.strValue) {
case '@-o-keyframes':
case '@-moz-keyframes':
case '@-webkit-keyframes':
case '@keyframes':
return BlockType.Keyframes;
case '@charset':
return BlockType.Charset;
case '@import':
return BlockType.Import;
case '@namespace':
return BlockType.Namespace;
case '@page':
return BlockType.Page;
case '@document':
return BlockType.Document;
case '@media':
return BlockType.MediaQuery;
case '@font-face':
return BlockType.FontFace;
case '@viewport':
return BlockType.Viewport;
case '@supports':
return BlockType.Supports;
default:
return BlockType.Unsupported;
}
}
parse(): ParsedCssResult {
var delimiters: number = EOF_DELIM;
var ast = this._parseStyleSheet(delimiters);
var errors = this._errors;
this._errors = [];
return new ParsedCssResult(errors, ast);
}
/** @internal */
_parseStyleSheet(delimiters): CssStyleSheetAST {
var results = [];
this._scanner.consumeEmptyStatements();
while (this._scanner.peek != $EOF) {
this._scanner.setMode(CssLexerMode.BLOCK);
results.push(this._parseRule(delimiters));
}
return new CssStyleSheetAST(results);
}
/** @internal */
_parseRule(delimiters: number): CssRuleAST {
if (this._scanner.peek == $AT) {
return this._parseAtRule(delimiters);
}
return this._parseSelectorRule(delimiters);
}
/** @internal */
_parseAtRule(delimiters: number): CssRuleAST {
this._scanner.setMode(CssLexerMode.BLOCK);
var token = this._scan();
this._assertCondition(token.type == CssTokenType.AtKeyword,
`The CSS Rule ${token.strValue} is not a valid [@] rule.`, token);
var block, type = this._resolveBlockType(token);
switch (type) {
case BlockType.Charset:
case BlockType.Namespace:
case BlockType.Import:
var value = this._parseValue(delimiters);
this._scanner.setMode(CssLexerMode.BLOCK);
this._scanner.consumeEmptyStatements();
return new CssInlineRuleAST(type, value);
case BlockType.Viewport:
case BlockType.FontFace:
block = this._parseStyleBlock(delimiters);
return new CssBlockRuleAST(type, block);
case BlockType.Keyframes:
var tokens = this._collectUntilDelim(bitWiseOr([delimiters, RBRACE_DELIM, LBRACE_DELIM]));
// keyframes only have one identifier name
var name = tokens[0];
return new CssKeyframeRuleAST(name, this._parseKeyframeBlock(delimiters));
case BlockType.MediaQuery:
this._scanner.setMode(CssLexerMode.MEDIA_QUERY);
var tokens = this._collectUntilDelim(bitWiseOr([delimiters, RBRACE_DELIM, LBRACE_DELIM]));
return new CssMediaQueryRuleAST(tokens, this._parseBlock(delimiters));
case BlockType.Document:
case BlockType.Supports:
case BlockType.Page:
this._scanner.setMode(CssLexerMode.AT_RULE_QUERY);
var tokens = this._collectUntilDelim(bitWiseOr([delimiters, RBRACE_DELIM, LBRACE_DELIM]));
return new CssBlockDefinitionRuleAST(type, tokens, this._parseBlock(delimiters));
// if a custom @rule { ... } is used it should still tokenize the insides
default:
var listOfTokens = [];
this._scanner.setMode(CssLexerMode.ALL);
this._error(generateErrorMessage(
this._scanner.input,
`The CSS "at" rule "${token.strValue}" is not allowed to used here`,
token.strValue, token.index, token.line, token.column),
token);
this._collectUntilDelim(bitWiseOr([delimiters, LBRACE_DELIM, SEMICOLON_DELIM]))
.forEach((token) => { listOfTokens.push(token); });
if (this._scanner.peek == $LBRACE) {
this._consume(CssTokenType.Character, '{');
this._collectUntilDelim(bitWiseOr([delimiters, RBRACE_DELIM, LBRACE_DELIM]))
.forEach((token) => { listOfTokens.push(token); });
this._consume(CssTokenType.Character, '}');
}
return new CssUnknownTokenListAST(token, listOfTokens);
}
}
/** @internal */
_parseSelectorRule(delimiters: number): CssSelectorRuleAST {
var selectors = this._parseSelectors(delimiters);
var block = this._parseStyleBlock(delimiters);
this._scanner.setMode(CssLexerMode.BLOCK);
this._scanner.consumeEmptyStatements();
return new CssSelectorRuleAST(selectors, block);
}
/** @internal */
_parseSelectors(delimiters: number): CssSelectorAST[] {
delimiters = bitWiseOr([delimiters, LBRACE_DELIM]);
var selectors = [];
var isParsingSelectors = true;
while (isParsingSelectors) {
selectors.push(this._parseSelector(delimiters));
isParsingSelectors = !characterContainsDelimiter(this._scanner.peek, delimiters);
if (isParsingSelectors) {
this._consume(CssTokenType.Character, ',');
isParsingSelectors = !characterContainsDelimiter(this._scanner.peek, delimiters);
}
}
return selectors;
}
/** @internal */
_scan(): CssToken {
var output = this._scanner.scan();
var token = output.token;
var error = output.error;
if (isPresent(error)) {
this._error(error.rawMessage, token);
}
return token;
}
/** @internal */
_consume(type: CssTokenType, value: string = null): CssToken {
var output = this._scanner.consume(type, value);
var token = output.token;
var error = output.error;
if (isPresent(error)) {
this._error(error.rawMessage, token);
}
return token;
}
/** @internal */
_parseKeyframeBlock(delimiters: number): CssBlockAST {
delimiters = bitWiseOr([delimiters, RBRACE_DELIM]);
this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK);
this._consume(CssTokenType.Character, '{');
var definitions = [];
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
definitions.push(this._parseKeyframeDefinition(delimiters));
}
this._consume(CssTokenType.Character, '}');
return new CssBlockAST(definitions);
}
/** @internal */
_parseKeyframeDefinition(delimiters: number): CssKeyframeDefinitionAST {
var stepTokens = [];
delimiters = bitWiseOr([delimiters, LBRACE_DELIM]);
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
stepTokens.push(this._parseKeyframeLabel(bitWiseOr([delimiters, COMMA_DELIM])));
if (this._scanner.peek != $LBRACE) {
this._consume(CssTokenType.Character, ',');
}
}
var styles = this._parseStyleBlock(bitWiseOr([delimiters, RBRACE_DELIM]));
this._scanner.setMode(CssLexerMode.BLOCK);
return new CssKeyframeDefinitionAST(stepTokens, styles);
}
/** @internal */
_parseKeyframeLabel(delimiters: number): CssToken {
this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK);
return mergeTokens(this._collectUntilDelim(delimiters));
}
/** @internal */
_parseSelector(delimiters: number): CssSelectorAST {
delimiters = bitWiseOr([delimiters, COMMA_DELIM, LBRACE_DELIM]);
this._scanner.setMode(CssLexerMode.SELECTOR);
var selectorCssTokens = [];
var isComplex = false;
var wsCssToken;
var previousToken;
var parenCount = 0;
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
var code = this._scanner.peek;
switch (code) {
case $LPAREN:
parenCount++;
break;
case $RPAREN:
parenCount--;
break;
case $COLON:
this._scanner.setMode(CssLexerMode.PSEUDO_SELECTOR);
previousToken = this._consume(CssTokenType.Character, ':');
selectorCssTokens.push(previousToken);
continue;
case $LBRACKET:
// if we are already inside an attribute selector then we can't
// jump into the mode again. Therefore this error will get picked
// up when the scan method is called below.
if (this._scanner.getMode() != CssLexerMode.ATTRIBUTE_SELECTOR) {
selectorCssTokens.push(this._consume(CssTokenType.Character, '['));
this._scanner.setMode(CssLexerMode.ATTRIBUTE_SELECTOR);
continue;
}
break;
case $RBRACKET:
selectorCssTokens.push(this._consume(CssTokenType.Character, ']'));
this._scanner.setMode(CssLexerMode.SELECTOR);
continue;
}
var token = this._scan();
// special case for the ":not(" selector since it
// contains an inner selector that needs to be parsed
// in isolation
if (this._scanner.getMode() == CssLexerMode.PSEUDO_SELECTOR && isPresent(previousToken) &&
previousToken.numValue == $COLON && token.strValue == "not" &&
this._scanner.peek == $LPAREN) {
selectorCssTokens.push(token);
selectorCssTokens.push(this._consume(CssTokenType.Character, '('));
// the inner selector inside of :not(...) can only be one
// CSS selector (no commas allowed) therefore we parse only
// one selector by calling the method below
this._parseSelector(bitWiseOr([delimiters, RPAREN_DELIM]))
.tokens.forEach(
(innerSelectorToken) => { selectorCssTokens.push(innerSelectorToken); });
selectorCssTokens.push(this._consume(CssTokenType.Character, ')'));
continue;
}
previousToken = token;
if (token.type == CssTokenType.Whitespace) {
wsCssToken = token;
} else {
if (isPresent(wsCssToken)) {
selectorCssTokens.push(wsCssToken);
wsCssToken = null;
isComplex = true;
}
selectorCssTokens.push(token);
}
}
if (this._scanner.getMode() == CssLexerMode.ATTRIBUTE_SELECTOR) {
this._error(
`Unbalanced CSS attribute selector at column ${previousToken.line}:${previousToken.column}`,
previousToken);
} else if (parenCount > 0) {
this._error(
`Unbalanced pseudo selector function value at column ${previousToken.line}:${previousToken.column}`,
previousToken);
}
return new CssSelectorAST(selectorCssTokens, isComplex);
}
/** @internal */
_parseValue(delimiters: number): CssStyleValueAST {
delimiters = bitWiseOr([delimiters, RBRACE_DELIM, SEMICOLON_DELIM, NEWLINE_DELIM]);
this._scanner.setMode(CssLexerMode.STYLE_VALUE);
var strValue = "";
var tokens = [];
var previous: CssToken;
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
var token;
if (isPresent(previous) && previous.type == CssTokenType.Identifier &&
this._scanner.peek == $LPAREN) {
token = this._consume(CssTokenType.Character, '(');
tokens.push(token);
strValue += token.strValue;
this._scanner.setMode(CssLexerMode.STYLE_VALUE_FUNCTION);
token = this._scan();
tokens.push(token);
strValue += token.strValue;
this._scanner.setMode(CssLexerMode.STYLE_VALUE);
token = this._consume(CssTokenType.Character, ')');
tokens.push(token);
strValue += token.strValue;
} else {
token = this._scan();
if (token.type != CssTokenType.Whitespace) {
tokens.push(token);
}
strValue += token.strValue;
}
previous = token;
}
this._scanner.consumeWhitespace();
var code = this._scanner.peek;
if (code == $SEMICOLON) {
this._consume(CssTokenType.Character, ';');
} else if (code != $RBRACE) {
this._error(
generateErrorMessage(this._scanner.input,
`The CSS key/value definition did not end with a semicolon`,
previous.strValue, previous.index, previous.line, previous.column),
previous);
}
return new CssStyleValueAST(tokens, strValue);
}
/** @internal */
_collectUntilDelim(delimiters: number, assertType: CssTokenType = null): CssToken[] {
var tokens = [];
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
var val = isPresent(assertType) ? this._consume(assertType) : this._scan();
tokens.push(val);
}
return tokens;
}
/** @internal */
_parseBlock(delimiters: number): CssBlockAST {
delimiters = bitWiseOr([delimiters, RBRACE_DELIM]);
this._scanner.setMode(CssLexerMode.BLOCK);
this._consume(CssTokenType.Character, '{');
this._scanner.consumeEmptyStatements();
var results = [];
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
results.push(this._parseRule(delimiters));
}
this._consume(CssTokenType.Character, '}');
this._scanner.setMode(CssLexerMode.BLOCK);
this._scanner.consumeEmptyStatements();
return new CssBlockAST(results);
}
/** @internal */
_parseStyleBlock(delimiters: number): CssBlockAST {
delimiters = bitWiseOr([delimiters, RBRACE_DELIM, LBRACE_DELIM]);
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
this._consume(CssTokenType.Character, '{');
this._scanner.consumeEmptyStatements();
var definitions = [];
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
definitions.push(this._parseDefinition(delimiters));
this._scanner.consumeEmptyStatements();
}
this._consume(CssTokenType.Character, '}');
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
this._scanner.consumeEmptyStatements();
return new CssBlockAST(definitions);
}
/** @internal */
_parseDefinition(delimiters: number): CssDefinitionAST {
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
var prop = this._consume(CssTokenType.Identifier);
var parseValue, value = null;
// the colon value separates the prop from the style.
// there are a few cases as to what could happen if it
// is missing
switch (this._scanner.peek) {
case $COLON:
this._consume(CssTokenType.Character, ':');
parseValue = true;
break;
case $SEMICOLON:
case $RBRACE:
case $EOF:
parseValue = false;
break;
default:
var propStr = [prop.strValue];
if (this._scanner.peek != $COLON) {
// this will throw the error
var nextValue = this._consume(CssTokenType.Character, ':');
propStr.push(nextValue.strValue);
var remainingTokens = this._collectUntilDelim(
bitWiseOr([delimiters, COLON_DELIM, SEMICOLON_DELIM]), CssTokenType.Identifier);
if (remainingTokens.length > 0) {
remainingTokens.forEach((token) => { propStr.push(token.strValue); });
}
prop = new CssToken(prop.index, prop.column, prop.line, prop.type, propStr.join(" "));
}
// this means we've reached the end of the definition and/or block
if (this._scanner.peek == $COLON) {
this._consume(CssTokenType.Character, ':');
parseValue = true;
} else {
parseValue = false;
}
break;
}
if (parseValue) {
value = this._parseValue(delimiters);
} else {
this._error(generateErrorMessage(this._scanner.input,
`The CSS property was not paired with a style value`,
prop.strValue, prop.index, prop.line, prop.column),
prop);
}
return new CssDefinitionAST(prop, value);
}
/** @internal */
_assertCondition(status: boolean, errorMessage: string, problemToken: CssToken): boolean {
if (!status) {
this._error(errorMessage, problemToken);
return true;
}
return false;
}
/** @internal */
_error(message: string, problemToken: CssToken) {
var length = problemToken.strValue.length;
var error = CssParseError.create(this._file, 0, problemToken.line, problemToken.column, length,
message);
this._errors.push(error);
}
}
export class CssStyleValueAST extends CssAST {
constructor(public tokens: CssToken[], public strValue: string) { super(); }
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssValue(this); }
}
export class CssRuleAST extends CssAST {}
export class CssBlockRuleAST extends CssRuleAST {
constructor(public type: BlockType, public block: CssBlockAST, public name: CssToken = null) {
super();
}
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssBlock(this.block, context); }
}
export class CssKeyframeRuleAST extends CssBlockRuleAST {
constructor(name: CssToken, block: CssBlockAST) { super(BlockType.Keyframes, block, name); }
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssKeyframeRule(this, context); }
}
export class CssKeyframeDefinitionAST extends CssBlockRuleAST {
public steps;
constructor(_steps: CssToken[], block: CssBlockAST) {
super(BlockType.Keyframes, block, mergeTokens(_steps, ","));
this.steps = _steps;
}
visit(visitor: CssASTVisitor, context?: any) {
visitor.visitCssKeyframeDefinition(this, context);
}
}
export class CssBlockDefinitionRuleAST extends CssBlockRuleAST {
public strValue: string;
constructor(type: BlockType, public query: CssToken[], block: CssBlockAST) {
super(type, block);
this.strValue = query.map(token => token.strValue).join("");
var firstCssToken: CssToken = query[0];
this.name = new CssToken(firstCssToken.index, firstCssToken.column, firstCssToken.line,
CssTokenType.Identifier, this.strValue);
}
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssBlock(this.block, context); }
}
export class CssMediaQueryRuleAST extends CssBlockDefinitionRuleAST {
constructor(query: CssToken[], block: CssBlockAST) { super(BlockType.MediaQuery, query, block); }
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssMediaQueryRule(this, context); }
}
export class CssInlineRuleAST extends CssRuleAST {
constructor(public type: BlockType, public value: CssStyleValueAST) { super(); }
visit(visitor: CssASTVisitor, context?: any) { visitor.visitInlineCssRule(this, context); }
}
export class CssSelectorRuleAST extends CssBlockRuleAST {
public strValue: string;
constructor(public selectors: CssSelectorAST[], block: CssBlockAST) {
super(BlockType.Selector, block);
this.strValue = selectors.map(selector => selector.strValue).join(",");
}
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssSelectorRule(this, context); }
}
export class CssDefinitionAST extends CssAST {
constructor(public property: CssToken, public value: CssStyleValueAST) { super(); }
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssDefinition(this, context); }
}
export class CssSelectorAST extends CssAST {
public strValue;
constructor(public tokens: CssToken[], public isComplex: boolean = false) {
super();
this.strValue = tokens.map(token => token.strValue).join("");
}
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssSelector(this, context); }
}
export class CssBlockAST extends CssAST {
constructor(public entries: CssAST[]) { super(); }
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssBlock(this, context); }
}
export class CssStyleSheetAST extends CssAST {
constructor(public rules: CssAST[]) { super(); }
visit(visitor: CssASTVisitor, context?: any) { visitor.visitCssStyleSheet(this, context); }
}
export class CssParseError extends ParseError {
static create(file: ParseSourceFile, offset: number, line: number, col: number, length: number,
errMsg: string): CssParseError {
var start = new ParseLocation(file, offset, line, col);
var end = new ParseLocation(file, offset, line, col + length);
var span = new ParseSourceSpan(start, end);
return new CssParseError(span, "CSS Parse Error: " + errMsg);
}
constructor(span: ParseSourceSpan, message: string) { super(span, message); }
}
export class CssUnknownTokenListAST extends CssRuleAST {
constructor(public name, public tokens: CssToken[]) { super(); }
visit(visitor: CssASTVisitor, context?: any) { visitor.visitUnkownRule(this, context); }
}

View File

@ -0,0 +1,21 @@
library angular2.src.core.compiler.directive_lifecycle_reflector;
import 'package:angular2/src/core/reflection/reflection.dart';
import 'package:angular2/src/core/metadata/lifecycle_hooks.dart';
const INTERFACES = const {
LifecycleHooks.OnInit: OnInit,
LifecycleHooks.OnDestroy: OnDestroy,
LifecycleHooks.DoCheck: DoCheck,
LifecycleHooks.OnChanges: OnChanges,
LifecycleHooks.AfterContentInit: AfterContentInit,
LifecycleHooks.AfterContentChecked: AfterContentChecked,
LifecycleHooks.AfterViewInit: AfterViewInit,
LifecycleHooks.AfterViewChecked: AfterViewChecked,
};
bool hasLifecycleHook(LifecycleHooks interface, token) {
if (token is! Type) return false;
Type interfaceType = INTERFACES[interface];
return reflector.interfaces(token).contains(interfaceType);
}

View File

@ -0,0 +1,29 @@
import {Type} from 'angular2/src/facade/lang';
import {LifecycleHooks} from 'angular2/src/core/metadata/lifecycle_hooks';
export function hasLifecycleHook(lcInterface: LifecycleHooks, token): boolean {
if (!(token instanceof Type)) return false;
var proto = (<any>token).prototype;
switch (lcInterface) {
case LifecycleHooks.AfterContentInit:
return !!proto.ngAfterContentInit;
case LifecycleHooks.AfterContentChecked:
return !!proto.ngAfterContentChecked;
case LifecycleHooks.AfterViewInit:
return !!proto.ngAfterViewInit;
case LifecycleHooks.AfterViewChecked:
return !!proto.ngAfterViewChecked;
case LifecycleHooks.OnChanges:
return !!proto.ngOnChanges;
case LifecycleHooks.DoCheck:
return !!proto.ngDoCheck;
case LifecycleHooks.OnDestroy:
return !!proto.ngOnDestroy;
case LifecycleHooks.OnInit:
return !!proto.ngOnInit;
default:
return false;
}
}

View File

@ -0,0 +1,166 @@
import {
CompileTypeMetadata,
CompileDirectiveMetadata,
CompileTemplateMetadata,
CompileProviderMetadata,
CompileTokenMetadata
} from './compile_metadata';
import {isPresent, isBlank, isArray} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {PromiseWrapper} from 'angular2/src/facade/async';
import {XHR} from 'angular2/src/compiler/xhr';
import {UrlResolver} from 'angular2/src/compiler/url_resolver';
import {extractStyleUrls, isStyleUrlResolvable} from './style_url_resolver';
import {Injectable} from 'angular2/src/core/di';
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';
import {
HtmlAstVisitor,
HtmlElementAst,
HtmlTextAst,
HtmlAttrAst,
HtmlAst,
HtmlCommentAst,
HtmlExpansionAst,
HtmlExpansionCaseAst,
htmlVisitAll
} from './html_ast';
import {HtmlParser} from './html_parser';
import {preparseElement, PreparsedElement, PreparsedElementType} from './template_preparser';
@Injectable()
export class DirectiveNormalizer {
constructor(private _xhr: XHR, private _urlResolver: UrlResolver,
private _htmlParser: HtmlParser) {}
normalizeDirective(directive: CompileDirectiveMetadata): Promise<CompileDirectiveMetadata> {
if (!directive.isComponent) {
// For non components there is nothing to be normalized yet.
return PromiseWrapper.resolve(directive);
}
return this.normalizeTemplate(directive.type, directive.template)
.then((normalizedTemplate: CompileTemplateMetadata) => new CompileDirectiveMetadata({
type: directive.type,
isComponent: directive.isComponent,
selector: directive.selector,
exportAs: directive.exportAs,
changeDetection: directive.changeDetection,
inputs: directive.inputs,
outputs: directive.outputs,
hostListeners: directive.hostListeners,
hostProperties: directive.hostProperties,
hostAttributes: directive.hostAttributes,
lifecycleHooks: directive.lifecycleHooks,
providers: directive.providers,
viewProviders: directive.viewProviders,
queries: directive.queries,
viewQueries: directive.viewQueries,
template: normalizedTemplate
}));
}
normalizeTemplate(directiveType: CompileTypeMetadata,
template: CompileTemplateMetadata): Promise<CompileTemplateMetadata> {
if (isPresent(template.template)) {
return PromiseWrapper.resolve(this.normalizeLoadedTemplate(
directiveType, template, template.template, template.baseUrl));
} else if (isPresent(template.templateUrl)) {
var sourceAbsUrl = this._urlResolver.resolve(template.baseUrl, template.templateUrl);
return this._xhr.get(sourceAbsUrl)
.then(templateContent => this.normalizeLoadedTemplate(directiveType, template,
templateContent, sourceAbsUrl));
} else {
throw new BaseException(`No template specified for component ${directiveType.name}`);
}
}
normalizeLoadedTemplate(directiveType: CompileTypeMetadata, templateMeta: CompileTemplateMetadata,
template: string, templateAbsUrl: string): CompileTemplateMetadata {
var rootNodesAndErrors = this._htmlParser.parse(template, directiveType.name);
if (rootNodesAndErrors.errors.length > 0) {
var errorString = rootNodesAndErrors.errors.join('\n');
throw new BaseException(`Template parse errors:\n${errorString}`);
}
var visitor = new TemplatePreparseVisitor();
htmlVisitAll(visitor, rootNodesAndErrors.rootNodes);
var allStyles = templateMeta.styles.concat(visitor.styles);
var allStyleAbsUrls =
visitor.styleUrls.filter(isStyleUrlResolvable)
.map(url => this._urlResolver.resolve(templateAbsUrl, url))
.concat(templateMeta.styleUrls.filter(isStyleUrlResolvable)
.map(url => this._urlResolver.resolve(templateMeta.baseUrl, url)));
var allResolvedStyles = allStyles.map(style => {
var styleWithImports = extractStyleUrls(this._urlResolver, templateAbsUrl, style);
styleWithImports.styleUrls.forEach(styleUrl => allStyleAbsUrls.push(styleUrl));
return styleWithImports.style;
});
var encapsulation = templateMeta.encapsulation;
if (encapsulation === ViewEncapsulation.Emulated && allResolvedStyles.length === 0 &&
allStyleAbsUrls.length === 0) {
encapsulation = ViewEncapsulation.None;
}
return new CompileTemplateMetadata({
encapsulation: encapsulation,
template: template,
templateUrl: templateAbsUrl,
styles: allResolvedStyles,
styleUrls: allStyleAbsUrls,
ngContentSelectors: visitor.ngContentSelectors
});
}
}
class TemplatePreparseVisitor implements HtmlAstVisitor {
ngContentSelectors: string[] = [];
styles: string[] = [];
styleUrls: string[] = [];
ngNonBindableStackCount: number = 0;
visitElement(ast: HtmlElementAst, context: any): any {
var preparsedElement = preparseElement(ast);
switch (preparsedElement.type) {
case PreparsedElementType.NG_CONTENT:
if (this.ngNonBindableStackCount === 0) {
this.ngContentSelectors.push(preparsedElement.selectAttr);
}
break;
case PreparsedElementType.STYLE:
var textContent = '';
ast.children.forEach(child => {
if (child instanceof HtmlTextAst) {
textContent += (<HtmlTextAst>child).value;
}
});
this.styles.push(textContent);
break;
case PreparsedElementType.STYLESHEET:
this.styleUrls.push(preparsedElement.hrefAttr);
break;
default:
// DDC reports this as error. See:
// https://github.com/dart-lang/dev_compiler/issues/428
break;
}
if (preparsedElement.nonBindable) {
this.ngNonBindableStackCount++;
}
htmlVisitAll(this, ast.children);
if (preparsedElement.nonBindable) {
this.ngNonBindableStackCount--;
}
return null;
}
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
visitText(ast: HtmlTextAst, context: any): any { return null; }
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
}

View File

@ -0,0 +1,169 @@
import {resolveForwardRef, Injectable} from 'angular2/src/core/di';
import {Type, isPresent, isBlank, stringify} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {
DirectiveMetadata,
ComponentMetadata,
InputMetadata,
OutputMetadata,
HostBindingMetadata,
HostListenerMetadata,
ContentChildrenMetadata,
ViewChildrenMetadata,
ContentChildMetadata,
ViewChildMetadata
} from 'angular2/src/core/metadata';
import {reflector} from 'angular2/src/core/reflection/reflection';
import {ReflectorReader} from 'angular2/src/core/reflection/reflector_reader';
function _isDirectiveMetadata(type: any): boolean {
return type instanceof DirectiveMetadata;
}
/*
* Resolve a `Type` for {@link DirectiveMetadata}.
*
* This interface can be overridden by the application developer to create custom behavior.
*
* See {@link Compiler}
*/
@Injectable()
export class DirectiveResolver {
private _reflector: ReflectorReader;
constructor(_reflector?: ReflectorReader) {
if (isPresent(_reflector)) {
this._reflector = _reflector;
} else {
this._reflector = reflector;
}
}
/**
* Return {@link DirectiveMetadata} for a given `Type`.
*/
resolve(type: Type): DirectiveMetadata {
var typeMetadata = this._reflector.annotations(resolveForwardRef(type));
if (isPresent(typeMetadata)) {
var metadata = typeMetadata.find(_isDirectiveMetadata);
if (isPresent(metadata)) {
var propertyMetadata = this._reflector.propMetadata(type);
return this._mergeWithPropertyMetadata(metadata, propertyMetadata, type);
}
}
throw new BaseException(`No Directive annotation found on ${stringify(type)}`);
}
private _mergeWithPropertyMetadata(dm: DirectiveMetadata,
propertyMetadata: {[key: string]: any[]},
directiveType: Type): DirectiveMetadata {
var inputs = [];
var outputs = [];
var host: {[key: string]: string} = {};
var queries: {[key: string]: any} = {};
StringMapWrapper.forEach(propertyMetadata, (metadata: any[], propName: string) => {
metadata.forEach(a => {
if (a instanceof InputMetadata) {
if (isPresent(a.bindingPropertyName)) {
inputs.push(`${propName}: ${a.bindingPropertyName}`);
} else {
inputs.push(propName);
}
}
if (a instanceof OutputMetadata) {
if (isPresent(a.bindingPropertyName)) {
outputs.push(`${propName}: ${a.bindingPropertyName}`);
} else {
outputs.push(propName);
}
}
if (a instanceof HostBindingMetadata) {
if (isPresent(a.hostPropertyName)) {
host[`[${a.hostPropertyName}]`] = propName;
} else {
host[`[${propName}]`] = propName;
}
}
if (a instanceof HostListenerMetadata) {
var args = isPresent(a.args) ? (<any[]>a.args).join(', ') : '';
host[`(${a.eventName})`] = `${propName}(${args})`;
}
if (a instanceof ContentChildrenMetadata) {
queries[propName] = a;
}
if (a instanceof ViewChildrenMetadata) {
queries[propName] = a;
}
if (a instanceof ContentChildMetadata) {
queries[propName] = a;
}
if (a instanceof ViewChildMetadata) {
queries[propName] = a;
}
});
});
return this._merge(dm, inputs, outputs, host, queries, directiveType);
}
private _merge(dm: DirectiveMetadata, inputs: string[], outputs: string[],
host: {[key: string]: string}, queries: {[key: string]: any},
directiveType: Type): DirectiveMetadata {
var mergedInputs = isPresent(dm.inputs) ? ListWrapper.concat(dm.inputs, inputs) : inputs;
var mergedOutputs;
if (isPresent(dm.outputs)) {
dm.outputs.forEach((propName: string) => {
if (ListWrapper.contains(outputs, propName)) {
throw new BaseException(
`Output event '${propName}' defined multiple times in '${stringify(directiveType)}'`);
}
});
mergedOutputs = ListWrapper.concat(dm.outputs, outputs);
} else {
mergedOutputs = outputs;
}
var mergedHost = isPresent(dm.host) ? StringMapWrapper.merge(dm.host, host) : host;
var mergedQueries =
isPresent(dm.queries) ? StringMapWrapper.merge(dm.queries, queries) : queries;
if (dm instanceof ComponentMetadata) {
return new ComponentMetadata({
selector: dm.selector,
inputs: mergedInputs,
outputs: mergedOutputs,
host: mergedHost,
exportAs: dm.exportAs,
moduleId: dm.moduleId,
queries: mergedQueries,
changeDetection: dm.changeDetection,
providers: dm.providers,
viewProviders: dm.viewProviders
});
} else {
return new DirectiveMetadata({
selector: dm.selector,
inputs: mergedInputs,
outputs: mergedOutputs,
host: mergedHost,
exportAs: dm.exportAs,
queries: mergedQueries,
providers: dm.providers
});
}
}
}
export var CODEGEN_DIRECTIVE_RESOLVER = new DirectiveResolver(reflector);

View File

@ -0,0 +1,348 @@
import {ListWrapper} from "angular2/src/facade/collection";
export class AST {
visit(visitor: AstVisitor, context: any = null): any { return null; }
toString(): string { return "AST"; }
}
/**
* Represents a quoted expression of the form:
*
* quote = prefix `:` uninterpretedExpression
* prefix = identifier
* uninterpretedExpression = arbitrary string
*
* A quoted expression is meant to be pre-processed by an AST transformer that
* converts it into another AST that no longer contains quoted expressions.
* It is meant to allow third-party developers to extend Angular template
* expression language. The `uninterpretedExpression` part of the quote is
* therefore not interpreted by the Angular's own expression parser.
*/
export class Quote extends AST {
constructor(public prefix: string, public uninterpretedExpression: string, public location: any) {
super();
}
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitQuote(this, context); }
toString(): string { return "Quote"; }
}
export class EmptyExpr extends AST {
visit(visitor: AstVisitor, context: any = null) {
// do nothing
}
}
export class ImplicitReceiver extends AST {
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitImplicitReceiver(this, context);
}
}
/**
* Multiple expressions separated by a semicolon.
*/
export class Chain extends AST {
constructor(public expressions: any[]) { super(); }
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitChain(this, context); }
}
export class Conditional extends AST {
constructor(public condition: AST, public trueExp: AST, public falseExp: AST) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitConditional(this, context);
}
}
export class PropertyRead extends AST {
constructor(public receiver: AST, public name: string) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitPropertyRead(this, context);
}
}
export class PropertyWrite extends AST {
constructor(public receiver: AST, public name: string, public value: AST) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitPropertyWrite(this, context);
}
}
export class SafePropertyRead extends AST {
constructor(public receiver: AST, public name: string) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitSafePropertyRead(this, context);
}
}
export class KeyedRead extends AST {
constructor(public obj: AST, public key: AST) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitKeyedRead(this, context);
}
}
export class KeyedWrite extends AST {
constructor(public obj: AST, public key: AST, public value: AST) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitKeyedWrite(this, context);
}
}
export class BindingPipe extends AST {
constructor(public exp: AST, public name: string, public args: any[]) { super(); }
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitPipe(this, context); }
}
export class LiteralPrimitive extends AST {
constructor(public value) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitLiteralPrimitive(this, context);
}
}
export class LiteralArray extends AST {
constructor(public expressions: any[]) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitLiteralArray(this, context);
}
}
export class LiteralMap extends AST {
constructor(public keys: any[], public values: any[]) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitLiteralMap(this, context);
}
}
export class Interpolation extends AST {
constructor(public strings: any[], public expressions: any[]) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitInterpolation(this, context);
}
}
export class Binary extends AST {
constructor(public operation: string, public left: AST, public right: AST) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitBinary(this, context);
}
}
export class PrefixNot extends AST {
constructor(public expression: AST) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitPrefixNot(this, context);
}
}
export class MethodCall extends AST {
constructor(public receiver: AST, public name: string, public args: any[]) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitMethodCall(this, context);
}
}
export class SafeMethodCall extends AST {
constructor(public receiver: AST, public name: string, public args: any[]) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitSafeMethodCall(this, context);
}
}
export class FunctionCall extends AST {
constructor(public target: AST, public args: any[]) { super(); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitFunctionCall(this, context);
}
}
export class ASTWithSource extends AST {
constructor(public ast: AST, public source: string, public location: string) { super(); }
visit(visitor: AstVisitor, context: any = null): any { return this.ast.visit(visitor, context); }
toString(): string { return `${this.source} in ${this.location}`; }
}
export class TemplateBinding {
constructor(public key: string, public keyIsVar: boolean, public name: string,
public expression: ASTWithSource) {}
}
export interface AstVisitor {
visitBinary(ast: Binary, context: any): any;
visitChain(ast: Chain, context: any): any;
visitConditional(ast: Conditional, context: any): any;
visitFunctionCall(ast: FunctionCall, context: any): any;
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any;
visitInterpolation(ast: Interpolation, context: any): any;
visitKeyedRead(ast: KeyedRead, context: any): any;
visitKeyedWrite(ast: KeyedWrite, context: any): any;
visitLiteralArray(ast: LiteralArray, context: any): any;
visitLiteralMap(ast: LiteralMap, context: any): any;
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any;
visitMethodCall(ast: MethodCall, context: any): any;
visitPipe(ast: BindingPipe, context: any): any;
visitPrefixNot(ast: PrefixNot, context: any): any;
visitPropertyRead(ast: PropertyRead, context: any): any;
visitPropertyWrite(ast: PropertyWrite, context: any): any;
visitQuote(ast: Quote, context: any): any;
visitSafeMethodCall(ast: SafeMethodCall, context: any): any;
visitSafePropertyRead(ast: SafePropertyRead, context: any): any;
}
export class RecursiveAstVisitor implements AstVisitor {
visitBinary(ast: Binary, context: any): any {
ast.left.visit(this);
ast.right.visit(this);
return null;
}
visitChain(ast: Chain, context: any): any { return this.visitAll(ast.expressions, context); }
visitConditional(ast: Conditional, context: any): any {
ast.condition.visit(this);
ast.trueExp.visit(this);
ast.falseExp.visit(this);
return null;
}
visitPipe(ast: BindingPipe, context: any): any {
ast.exp.visit(this);
this.visitAll(ast.args, context);
return null;
}
visitFunctionCall(ast: FunctionCall, context: any): any {
ast.target.visit(this);
this.visitAll(ast.args, context);
return null;
}
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any { return null; }
visitInterpolation(ast: Interpolation, context: any): any {
return this.visitAll(ast.expressions, context);
}
visitKeyedRead(ast: KeyedRead, context: any): any {
ast.obj.visit(this);
ast.key.visit(this);
return null;
}
visitKeyedWrite(ast: KeyedWrite, context: any): any {
ast.obj.visit(this);
ast.key.visit(this);
ast.value.visit(this);
return null;
}
visitLiteralArray(ast: LiteralArray, context: any): any {
return this.visitAll(ast.expressions, context);
}
visitLiteralMap(ast: LiteralMap, context: any): any { return this.visitAll(ast.values, context); }
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any { return null; }
visitMethodCall(ast: MethodCall, context: any): any {
ast.receiver.visit(this);
return this.visitAll(ast.args, context);
}
visitPrefixNot(ast: PrefixNot, context: any): any {
ast.expression.visit(this);
return null;
}
visitPropertyRead(ast: PropertyRead, context: any): any {
ast.receiver.visit(this);
return null;
}
visitPropertyWrite(ast: PropertyWrite, context: any): any {
ast.receiver.visit(this);
ast.value.visit(this);
return null;
}
visitSafePropertyRead(ast: SafePropertyRead, context: any): any {
ast.receiver.visit(this);
return null;
}
visitSafeMethodCall(ast: SafeMethodCall, context: any): any {
ast.receiver.visit(this);
return this.visitAll(ast.args, context);
}
visitAll(asts: AST[], context: any): any {
asts.forEach(ast => ast.visit(this, context));
return null;
}
visitQuote(ast: Quote, context: any): any { return null; }
}
export class AstTransformer implements AstVisitor {
visitImplicitReceiver(ast: ImplicitReceiver, context: any): AST { return ast; }
visitInterpolation(ast: Interpolation, context: any): AST {
return new Interpolation(ast.strings, this.visitAll(ast.expressions));
}
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): AST {
return new LiteralPrimitive(ast.value);
}
visitPropertyRead(ast: PropertyRead, context: any): AST {
return new PropertyRead(ast.receiver.visit(this), ast.name);
}
visitPropertyWrite(ast: PropertyWrite, context: any): AST {
return new PropertyWrite(ast.receiver.visit(this), ast.name, ast.value);
}
visitSafePropertyRead(ast: SafePropertyRead, context: any): AST {
return new SafePropertyRead(ast.receiver.visit(this), ast.name);
}
visitMethodCall(ast: MethodCall, context: any): AST {
return new MethodCall(ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
}
visitSafeMethodCall(ast: SafeMethodCall, context: any): AST {
return new SafeMethodCall(ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
}
visitFunctionCall(ast: FunctionCall, context: any): AST {
return new FunctionCall(ast.target.visit(this), this.visitAll(ast.args));
}
visitLiteralArray(ast: LiteralArray, context: any): AST {
return new LiteralArray(this.visitAll(ast.expressions));
}
visitLiteralMap(ast: LiteralMap, context: any): AST {
return new LiteralMap(ast.keys, this.visitAll(ast.values));
}
visitBinary(ast: Binary, context: any): AST {
return new Binary(ast.operation, ast.left.visit(this), ast.right.visit(this));
}
visitPrefixNot(ast: PrefixNot, context: any): AST {
return new PrefixNot(ast.expression.visit(this));
}
visitConditional(ast: Conditional, context: any): AST {
return new Conditional(ast.condition.visit(this), ast.trueExp.visit(this),
ast.falseExp.visit(this));
}
visitPipe(ast: BindingPipe, context: any): AST {
return new BindingPipe(ast.exp.visit(this), ast.name, this.visitAll(ast.args));
}
visitKeyedRead(ast: KeyedRead, context: any): AST {
return new KeyedRead(ast.obj.visit(this), ast.key.visit(this));
}
visitKeyedWrite(ast: KeyedWrite, context: any): AST {
return new KeyedWrite(ast.obj.visit(this), ast.key.visit(this), ast.value.visit(this));
}
visitAll(asts: any[]): any[] {
var res = ListWrapper.createFixedSize(asts.length);
for (var i = 0; i < asts.length; ++i) {
res[i] = asts[i].visit(this);
}
return res;
}
visitChain(ast: Chain, context: any): AST { return new Chain(this.visitAll(ast.expressions)); }
visitQuote(ast: Quote, context: any): AST {
return new Quote(ast.prefix, ast.uninterpretedExpression, ast.location);
}
}

View File

@ -0,0 +1,473 @@
import {Injectable} from 'angular2/src/core/di/decorators';
import {ListWrapper, SetWrapper} from "angular2/src/facade/collection";
import {NumberWrapper, StringJoiner, StringWrapper, isPresent} from "angular2/src/facade/lang";
import {BaseException} from 'angular2/src/facade/exceptions';
export enum TokenType {
Character,
Identifier,
Keyword,
String,
Operator,
Number
}
@Injectable()
export class Lexer {
tokenize(text: string): any[] {
var scanner = new _Scanner(text);
var tokens = [];
var token = scanner.scanToken();
while (token != null) {
tokens.push(token);
token = scanner.scanToken();
}
return tokens;
}
}
export class Token {
constructor(public index: number, public type: TokenType, public numValue: number,
public strValue: string) {}
isCharacter(code: number): boolean {
return (this.type == TokenType.Character && this.numValue == code);
}
isNumber(): boolean { return (this.type == TokenType.Number); }
isString(): boolean { return (this.type == TokenType.String); }
isOperator(operater: string): boolean {
return (this.type == TokenType.Operator && this.strValue == operater);
}
isIdentifier(): boolean { return (this.type == TokenType.Identifier); }
isKeyword(): boolean { return (this.type == TokenType.Keyword); }
isKeywordDeprecatedVar(): boolean {
return (this.type == TokenType.Keyword && this.strValue == "var");
}
isKeywordLet(): boolean { return (this.type == TokenType.Keyword && this.strValue == "let"); }
isKeywordNull(): boolean { return (this.type == TokenType.Keyword && this.strValue == "null"); }
isKeywordUndefined(): boolean {
return (this.type == TokenType.Keyword && this.strValue == "undefined");
}
isKeywordTrue(): boolean { return (this.type == TokenType.Keyword && this.strValue == "true"); }
isKeywordFalse(): boolean { return (this.type == TokenType.Keyword && this.strValue == "false"); }
toNumber(): number {
// -1 instead of NULL ok?
return (this.type == TokenType.Number) ? this.numValue : -1;
}
toString(): string {
switch (this.type) {
case TokenType.Character:
case TokenType.Identifier:
case TokenType.Keyword:
case TokenType.Operator:
case TokenType.String:
return this.strValue;
case TokenType.Number:
return this.numValue.toString();
default:
return null;
}
}
}
function newCharacterToken(index: number, code: number): Token {
return new Token(index, TokenType.Character, code, StringWrapper.fromCharCode(code));
}
function newIdentifierToken(index: number, text: string): Token {
return new Token(index, TokenType.Identifier, 0, text);
}
function newKeywordToken(index: number, text: string): Token {
return new Token(index, TokenType.Keyword, 0, text);
}
function newOperatorToken(index: number, text: string): Token {
return new Token(index, TokenType.Operator, 0, text);
}
function newStringToken(index: number, text: string): Token {
return new Token(index, TokenType.String, 0, text);
}
function newNumberToken(index: number, n: number): Token {
return new Token(index, TokenType.Number, n, "");
}
export var EOF: Token = new Token(-1, TokenType.Character, 0, "");
export const $EOF = /*@ts2dart_const*/ 0;
export const $TAB = /*@ts2dart_const*/ 9;
export const $LF = /*@ts2dart_const*/ 10;
export const $VTAB = /*@ts2dart_const*/ 11;
export const $FF = /*@ts2dart_const*/ 12;
export const $CR = /*@ts2dart_const*/ 13;
export const $SPACE = /*@ts2dart_const*/ 32;
export const $BANG = /*@ts2dart_const*/ 33;
export const $DQ = /*@ts2dart_const*/ 34;
export const $HASH = /*@ts2dart_const*/ 35;
export const $$ = /*@ts2dart_const*/ 36;
export const $PERCENT = /*@ts2dart_const*/ 37;
export const $AMPERSAND = /*@ts2dart_const*/ 38;
export const $SQ = /*@ts2dart_const*/ 39;
export const $LPAREN = /*@ts2dart_const*/ 40;
export const $RPAREN = /*@ts2dart_const*/ 41;
export const $STAR = /*@ts2dart_const*/ 42;
export const $PLUS = /*@ts2dart_const*/ 43;
export const $COMMA = /*@ts2dart_const*/ 44;
export const $MINUS = /*@ts2dart_const*/ 45;
export const $PERIOD = /*@ts2dart_const*/ 46;
export const $SLASH = /*@ts2dart_const*/ 47;
export const $COLON = /*@ts2dart_const*/ 58;
export const $SEMICOLON = /*@ts2dart_const*/ 59;
export const $LT = /*@ts2dart_const*/ 60;
export const $EQ = /*@ts2dart_const*/ 61;
export const $GT = /*@ts2dart_const*/ 62;
export const $QUESTION = /*@ts2dart_const*/ 63;
const $0 = /*@ts2dart_const*/ 48;
const $9 = /*@ts2dart_const*/ 57;
const $A = /*@ts2dart_const*/ 65, $E = /*@ts2dart_const*/ 69, $Z = /*@ts2dart_const*/ 90;
export const $LBRACKET = /*@ts2dart_const*/ 91;
export const $BACKSLASH = /*@ts2dart_const*/ 92;
export const $RBRACKET = /*@ts2dart_const*/ 93;
const $CARET = /*@ts2dart_const*/ 94;
const $_ = /*@ts2dart_const*/ 95;
export const $BT = /*@ts2dart_const*/ 96;
const $a = /*@ts2dart_const*/ 97, $e = /*@ts2dart_const*/ 101, $f = /*@ts2dart_const*/ 102;
const $n = /*@ts2dart_const*/ 110, $r = /*@ts2dart_const*/ 114, $t = /*@ts2dart_const*/ 116,
$u = /*@ts2dart_const*/ 117, $v = /*@ts2dart_const*/ 118, $z = /*@ts2dart_const*/ 122;
export const $LBRACE = /*@ts2dart_const*/ 123;
export const $BAR = /*@ts2dart_const*/ 124;
export const $RBRACE = /*@ts2dart_const*/ 125;
const $NBSP = /*@ts2dart_const*/ 160;
export class ScannerError extends BaseException {
constructor(public message) { super(); }
toString(): string { return this.message; }
}
class _Scanner {
length: number;
peek: number = 0;
index: number = -1;
constructor(public input: string) {
this.length = input.length;
this.advance();
}
advance() {
this.peek =
++this.index >= this.length ? $EOF : StringWrapper.charCodeAt(this.input, this.index);
}
scanToken(): Token {
var input = this.input, length = this.length, peek = this.peek, index = this.index;
// Skip whitespace.
while (peek <= $SPACE) {
if (++index >= length) {
peek = $EOF;
break;
} else {
peek = StringWrapper.charCodeAt(input, index);
}
}
this.peek = peek;
this.index = index;
if (index >= length) {
return null;
}
// Handle identifiers and numbers.
if (isIdentifierStart(peek)) return this.scanIdentifier();
if (isDigit(peek)) return this.scanNumber(index);
var start: number = index;
switch (peek) {
case $PERIOD:
this.advance();
return isDigit(this.peek) ? this.scanNumber(start) : newCharacterToken(start, $PERIOD);
case $LPAREN:
case $RPAREN:
case $LBRACE:
case $RBRACE:
case $LBRACKET:
case $RBRACKET:
case $COMMA:
case $COLON:
case $SEMICOLON:
return this.scanCharacter(start, peek);
case $SQ:
case $DQ:
return this.scanString();
case $HASH:
case $PLUS:
case $MINUS:
case $STAR:
case $SLASH:
case $PERCENT:
case $CARET:
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
case $QUESTION:
return this.scanComplexOperator(start, '?', $PERIOD, '.');
case $LT:
case $GT:
return this.scanComplexOperator(start, StringWrapper.fromCharCode(peek), $EQ, '=');
case $BANG:
case $EQ:
return this.scanComplexOperator(start, StringWrapper.fromCharCode(peek), $EQ, '=', $EQ,
'=');
case $AMPERSAND:
return this.scanComplexOperator(start, '&', $AMPERSAND, '&');
case $BAR:
return this.scanComplexOperator(start, '|', $BAR, '|');
case $NBSP:
while (isWhitespace(this.peek)) this.advance();
return this.scanToken();
}
this.error(`Unexpected character [${StringWrapper.fromCharCode(peek)}]`, 0);
return null;
}
scanCharacter(start: number, code: number): Token {
this.advance();
return newCharacterToken(start, code);
}
scanOperator(start: number, str: string): Token {
this.advance();
return newOperatorToken(start, str);
}
/**
* Tokenize a 2/3 char long operator
*
* @param start start index in the expression
* @param one first symbol (always part of the operator)
* @param twoCode code point for the second symbol
* @param two second symbol (part of the operator when the second code point matches)
* @param threeCode code point for the third symbol
* @param three third symbol (part of the operator when provided and matches source expression)
* @returns {Token}
*/
scanComplexOperator(start: number, one: string, twoCode: number, two: string, threeCode?: number,
three?: string): Token {
this.advance();
var str: string = one;
if (this.peek == twoCode) {
this.advance();
str += two;
}
if (isPresent(threeCode) && this.peek == threeCode) {
this.advance();
str += three;
}
return newOperatorToken(start, str);
}
scanIdentifier(): Token {
var start: number = this.index;
this.advance();
while (isIdentifierPart(this.peek)) this.advance();
var str: string = this.input.substring(start, this.index);
if (SetWrapper.has(KEYWORDS, str)) {
return newKeywordToken(start, str);
} else {
return newIdentifierToken(start, str);
}
}
scanNumber(start: number): Token {
var simple: boolean = (this.index === start);
this.advance(); // Skip initial digit.
while (true) {
if (isDigit(this.peek)) {
// Do nothing.
} else if (this.peek == $PERIOD) {
simple = false;
} else if (isExponentStart(this.peek)) {
this.advance();
if (isExponentSign(this.peek)) this.advance();
if (!isDigit(this.peek)) this.error('Invalid exponent', -1);
simple = false;
} else {
break;
}
this.advance();
}
var str: string = this.input.substring(start, this.index);
// TODO
var value: number =
simple ? NumberWrapper.parseIntAutoRadix(str) : NumberWrapper.parseFloat(str);
return newNumberToken(start, value);
}
scanString(): Token {
var start: number = this.index;
var quote: number = this.peek;
this.advance(); // Skip initial quote.
var buffer: StringJoiner;
var marker: number = this.index;
var input: string = this.input;
while (this.peek != quote) {
if (this.peek == $BACKSLASH) {
if (buffer == null) buffer = new StringJoiner();
buffer.add(input.substring(marker, this.index));
this.advance();
var unescapedCode: number;
if (this.peek == $u) {
// 4 character hex code for unicode character.
var hex: string = input.substring(this.index + 1, this.index + 5);
try {
unescapedCode = NumberWrapper.parseInt(hex, 16);
} catch (e) {
this.error(`Invalid unicode escape [\\u${hex}]`, 0);
}
for (var i: number = 0; i < 5; i++) {
this.advance();
}
} else {
unescapedCode = unescape(this.peek);
this.advance();
}
buffer.add(StringWrapper.fromCharCode(unescapedCode));
marker = this.index;
} else if (this.peek == $EOF) {
this.error('Unterminated quote', 0);
} else {
this.advance();
}
}
var last: string = input.substring(marker, this.index);
this.advance(); // Skip terminating quote.
// Compute the unescaped string value.
var unescaped: string = last;
if (buffer != null) {
buffer.add(last);
unescaped = buffer.toString();
}
return newStringToken(start, unescaped);
}
error(message: string, offset: number) {
var position: number = this.index + offset;
throw new ScannerError(
`Lexer Error: ${message} at column ${position} in expression [${this.input}]`);
}
}
function isWhitespace(code: number): boolean {
return (code >= $TAB && code <= $SPACE) || (code == $NBSP);
}
function isIdentifierStart(code: number): boolean {
return ($a <= code && code <= $z) || ($A <= code && code <= $Z) || (code == $_) || (code == $$);
}
export function isIdentifier(input: string): boolean {
if (input.length == 0) return false;
var scanner = new _Scanner(input);
if (!isIdentifierStart(scanner.peek)) return false;
scanner.advance();
while (scanner.peek !== $EOF) {
if (!isIdentifierPart(scanner.peek)) return false;
scanner.advance();
}
return true;
}
function isIdentifierPart(code: number): boolean {
return ($a <= code && code <= $z) || ($A <= code && code <= $Z) || ($0 <= code && code <= $9) ||
(code == $_) || (code == $$);
}
function isDigit(code: number): boolean {
return $0 <= code && code <= $9;
}
function isExponentStart(code: number): boolean {
return code == $e || code == $E;
}
function isExponentSign(code: number): boolean {
return code == $MINUS || code == $PLUS;
}
export function isQuote(code: number): boolean {
return code === $SQ || code === $DQ || code === $BT;
}
function unescape(code: number): number {
switch (code) {
case $n:
return $LF;
case $f:
return $FF;
case $r:
return $CR;
case $t:
return $TAB;
case $v:
return $VTAB;
default:
return code;
}
}
var OPERATORS = SetWrapper.createFromList([
'+',
'-',
'*',
'/',
'%',
'^',
'=',
'==',
'!=',
'===',
'!==',
'<',
'>',
'<=',
'>=',
'&&',
'||',
'&',
'|',
'!',
'?',
'#',
'?.'
]);
var KEYWORDS =
SetWrapper.createFromList(['var', 'let', 'null', 'undefined', 'true', 'false', 'if', 'else']);

View File

@ -0,0 +1,732 @@
import {Injectable} from 'angular2/src/core/di/decorators';
import {isBlank, isPresent, StringWrapper} from 'angular2/src/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
import {ListWrapper} from 'angular2/src/facade/collection';
import {
Lexer,
EOF,
isIdentifier,
isQuote,
Token,
$PERIOD,
$COLON,
$SEMICOLON,
$LBRACKET,
$RBRACKET,
$COMMA,
$LBRACE,
$RBRACE,
$LPAREN,
$RPAREN,
$SLASH
} from './lexer';
import {
AST,
EmptyExpr,
ImplicitReceiver,
PropertyRead,
PropertyWrite,
SafePropertyRead,
LiteralPrimitive,
Binary,
PrefixNot,
Conditional,
BindingPipe,
Chain,
KeyedRead,
KeyedWrite,
LiteralArray,
LiteralMap,
Interpolation,
MethodCall,
SafeMethodCall,
FunctionCall,
TemplateBinding,
ASTWithSource,
AstVisitor,
Quote
} from './ast';
var _implicitReceiver = new ImplicitReceiver();
// TODO(tbosch): Cannot make this const/final right now because of the transpiler...
var INTERPOLATION_REGEXP = /\{\{([\s\S]*?)\}\}/g;
class ParseException extends BaseException {
constructor(message: string, input: string, errLocation: string, ctxLocation?: any) {
super(`Parser Error: ${message} ${errLocation} [${input}] in ${ctxLocation}`);
}
}
export class SplitInterpolation {
constructor(public strings: string[], public expressions: string[]) {}
}
export class TemplateBindingParseResult {
constructor(public templateBindings: TemplateBinding[], public warnings: string[]) {}
}
@Injectable()
export class Parser {
constructor(/** @internal */
public _lexer: Lexer) {}
parseAction(input: string, location: any): ASTWithSource {
this._checkNoInterpolation(input, location);
var tokens = this._lexer.tokenize(this._stripComments(input));
var ast = new _ParseAST(input, location, tokens, true).parseChain();
return new ASTWithSource(ast, input, location);
}
parseBinding(input: string, location: any): ASTWithSource {
var ast = this._parseBindingAst(input, location);
return new ASTWithSource(ast, input, location);
}
parseSimpleBinding(input: string, location: string): ASTWithSource {
var ast = this._parseBindingAst(input, location);
if (!SimpleExpressionChecker.check(ast)) {
throw new ParseException(
'Host binding expression can only contain field access and constants', input, location);
}
return new ASTWithSource(ast, input, location);
}
private _parseBindingAst(input: string, location: string): AST {
// Quotes expressions use 3rd-party expression language. We don't want to use
// our lexer or parser for that, so we check for that ahead of time.
var quote = this._parseQuote(input, location);
if (isPresent(quote)) {
return quote;
}
this._checkNoInterpolation(input, location);
var tokens = this._lexer.tokenize(this._stripComments(input));
return new _ParseAST(input, location, tokens, false).parseChain();
}
private _parseQuote(input: string, location: any): AST {
if (isBlank(input)) return null;
var prefixSeparatorIndex = input.indexOf(':');
if (prefixSeparatorIndex == -1) return null;
var prefix = input.substring(0, prefixSeparatorIndex).trim();
if (!isIdentifier(prefix)) return null;
var uninterpretedExpression = input.substring(prefixSeparatorIndex + 1);
return new Quote(prefix, uninterpretedExpression, location);
}
parseTemplateBindings(input: string, location: any): TemplateBindingParseResult {
var tokens = this._lexer.tokenize(input);
return new _ParseAST(input, location, tokens, false).parseTemplateBindings();
}
parseInterpolation(input: string, location: any): ASTWithSource {
let split = this.splitInterpolation(input, location);
if (split == null) return null;
let expressions = [];
for (let i = 0; i < split.expressions.length; ++i) {
var tokens = this._lexer.tokenize(this._stripComments(split.expressions[i]));
var ast = new _ParseAST(input, location, tokens, false).parseChain();
expressions.push(ast);
}
return new ASTWithSource(new Interpolation(split.strings, expressions), input, location);
}
splitInterpolation(input: string, location: string): SplitInterpolation {
var parts = StringWrapper.split(input, INTERPOLATION_REGEXP);
if (parts.length <= 1) {
return null;
}
var strings = [];
var expressions = [];
for (var i = 0; i < parts.length; i++) {
var part: string = parts[i];
if (i % 2 === 0) {
// fixed string
strings.push(part);
} else if (part.trim().length > 0) {
expressions.push(part);
} else {
throw new ParseException('Blank expressions are not allowed in interpolated strings', input,
`at column ${this._findInterpolationErrorColumn(parts, i)} in`,
location);
}
}
return new SplitInterpolation(strings, expressions);
}
wrapLiteralPrimitive(input: string, location: any): ASTWithSource {
return new ASTWithSource(new LiteralPrimitive(input), input, location);
}
private _stripComments(input: string): string {
let i = this._commentStart(input);
return isPresent(i) ? input.substring(0, i).trim() : input;
}
private _commentStart(input: string): number {
var outerQuote = null;
for (var i = 0; i < input.length - 1; i++) {
let char = StringWrapper.charCodeAt(input, i);
let nextChar = StringWrapper.charCodeAt(input, i + 1);
if (char === $SLASH && nextChar == $SLASH && isBlank(outerQuote)) return i;
if (outerQuote === char) {
outerQuote = null;
} else if (isBlank(outerQuote) && isQuote(char)) {
outerQuote = char;
}
}
return null;
}
private _checkNoInterpolation(input: string, location: any): void {
var parts = StringWrapper.split(input, INTERPOLATION_REGEXP);
if (parts.length > 1) {
throw new ParseException('Got interpolation ({{}}) where expression was expected', input,
`at column ${this._findInterpolationErrorColumn(parts, 1)} in`,
location);
}
}
private _findInterpolationErrorColumn(parts: string[], partInErrIdx: number): number {
var errLocation = '';
for (var j = 0; j < partInErrIdx; j++) {
errLocation += j % 2 === 0 ? parts[j] : `{{${parts[j]}}}`;
}
return errLocation.length;
}
}
export class _ParseAST {
index: number = 0;
constructor(public input: string, public location: any, public tokens: any[],
public parseAction: boolean) {}
peek(offset: number): Token {
var i = this.index + offset;
return i < this.tokens.length ? this.tokens[i] : EOF;
}
get next(): Token { return this.peek(0); }
get inputIndex(): number {
return (this.index < this.tokens.length) ? this.next.index : this.input.length;
}
advance() { this.index++; }
optionalCharacter(code: number): boolean {
if (this.next.isCharacter(code)) {
this.advance();
return true;
} else {
return false;
}
}
peekKeywordLet(): boolean { return this.next.isKeywordLet(); }
peekDeprecatedKeywordVar(): boolean { return this.next.isKeywordDeprecatedVar(); }
peekDeprecatedOperatorHash(): boolean { return this.next.isOperator('#'); }
expectCharacter(code: number) {
if (this.optionalCharacter(code)) return;
this.error(`Missing expected ${StringWrapper.fromCharCode(code)}`);
}
optionalOperator(op: string): boolean {
if (this.next.isOperator(op)) {
this.advance();
return true;
} else {
return false;
}
}
expectOperator(operator: string) {
if (this.optionalOperator(operator)) return;
this.error(`Missing expected operator ${operator}`);
}
expectIdentifierOrKeyword(): string {
var n = this.next;
if (!n.isIdentifier() && !n.isKeyword()) {
this.error(`Unexpected token ${n}, expected identifier or keyword`);
}
this.advance();
return n.toString();
}
expectIdentifierOrKeywordOrString(): string {
var n = this.next;
if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) {
this.error(`Unexpected token ${n}, expected identifier, keyword, or string`);
}
this.advance();
return n.toString();
}
parseChain(): AST {
var exprs = [];
while (this.index < this.tokens.length) {
var expr = this.parsePipe();
exprs.push(expr);
if (this.optionalCharacter($SEMICOLON)) {
if (!this.parseAction) {
this.error("Binding expression cannot contain chained expression");
}
while (this.optionalCharacter($SEMICOLON)) {
} // read all semicolons
} else if (this.index < this.tokens.length) {
this.error(`Unexpected token '${this.next}'`);
}
}
if (exprs.length == 0) return new EmptyExpr();
if (exprs.length == 1) return exprs[0];
return new Chain(exprs);
}
parsePipe(): AST {
var result = this.parseExpression();
if (this.optionalOperator("|")) {
if (this.parseAction) {
this.error("Cannot have a pipe in an action expression");
}
do {
var name = this.expectIdentifierOrKeyword();
var args = [];
while (this.optionalCharacter($COLON)) {
args.push(this.parseExpression());
}
result = new BindingPipe(result, name, args);
} while (this.optionalOperator("|"));
}
return result;
}
parseExpression(): AST { return this.parseConditional(); }
parseConditional(): AST {
var start = this.inputIndex;
var result = this.parseLogicalOr();
if (this.optionalOperator('?')) {
var yes = this.parsePipe();
if (!this.optionalCharacter($COLON)) {
var end = this.inputIndex;
var expression = this.input.substring(start, end);
this.error(`Conditional expression ${expression} requires all 3 expressions`);
}
var no = this.parsePipe();
return new Conditional(result, yes, no);
} else {
return result;
}
}
parseLogicalOr(): AST {
// '||'
var result = this.parseLogicalAnd();
while (this.optionalOperator('||')) {
result = new Binary('||', result, this.parseLogicalAnd());
}
return result;
}
parseLogicalAnd(): AST {
// '&&'
var result = this.parseEquality();
while (this.optionalOperator('&&')) {
result = new Binary('&&', result, this.parseEquality());
}
return result;
}
parseEquality(): AST {
// '==','!=','===','!=='
var result = this.parseRelational();
while (true) {
if (this.optionalOperator('==')) {
result = new Binary('==', result, this.parseRelational());
} else if (this.optionalOperator('===')) {
result = new Binary('===', result, this.parseRelational());
} else if (this.optionalOperator('!=')) {
result = new Binary('!=', result, this.parseRelational());
} else if (this.optionalOperator('!==')) {
result = new Binary('!==', result, this.parseRelational());
} else {
return result;
}
}
}
parseRelational(): AST {
// '<', '>', '<=', '>='
var result = this.parseAdditive();
while (true) {
if (this.optionalOperator('<')) {
result = new Binary('<', result, this.parseAdditive());
} else if (this.optionalOperator('>')) {
result = new Binary('>', result, this.parseAdditive());
} else if (this.optionalOperator('<=')) {
result = new Binary('<=', result, this.parseAdditive());
} else if (this.optionalOperator('>=')) {
result = new Binary('>=', result, this.parseAdditive());
} else {
return result;
}
}
}
parseAdditive(): AST {
// '+', '-'
var result = this.parseMultiplicative();
while (true) {
if (this.optionalOperator('+')) {
result = new Binary('+', result, this.parseMultiplicative());
} else if (this.optionalOperator('-')) {
result = new Binary('-', result, this.parseMultiplicative());
} else {
return result;
}
}
}
parseMultiplicative(): AST {
// '*', '%', '/'
var result = this.parsePrefix();
while (true) {
if (this.optionalOperator('*')) {
result = new Binary('*', result, this.parsePrefix());
} else if (this.optionalOperator('%')) {
result = new Binary('%', result, this.parsePrefix());
} else if (this.optionalOperator('/')) {
result = new Binary('/', result, this.parsePrefix());
} else {
return result;
}
}
}
parsePrefix(): AST {
if (this.optionalOperator('+')) {
return this.parsePrefix();
} else if (this.optionalOperator('-')) {
return new Binary('-', new LiteralPrimitive(0), this.parsePrefix());
} else if (this.optionalOperator('!')) {
return new PrefixNot(this.parsePrefix());
} else {
return this.parseCallChain();
}
}
parseCallChain(): AST {
var result = this.parsePrimary();
while (true) {
if (this.optionalCharacter($PERIOD)) {
result = this.parseAccessMemberOrMethodCall(result, false);
} else if (this.optionalOperator('?.')) {
result = this.parseAccessMemberOrMethodCall(result, true);
} else if (this.optionalCharacter($LBRACKET)) {
var key = this.parsePipe();
this.expectCharacter($RBRACKET);
if (this.optionalOperator("=")) {
var value = this.parseConditional();
result = new KeyedWrite(result, key, value);
} else {
result = new KeyedRead(result, key);
}
} else if (this.optionalCharacter($LPAREN)) {
var args = this.parseCallArguments();
this.expectCharacter($RPAREN);
result = new FunctionCall(result, args);
} else {
return result;
}
}
}
parsePrimary(): AST {
if (this.optionalCharacter($LPAREN)) {
let result = this.parsePipe();
this.expectCharacter($RPAREN);
return result;
} else if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) {
this.advance();
return new LiteralPrimitive(null);
} else if (this.next.isKeywordTrue()) {
this.advance();
return new LiteralPrimitive(true);
} else if (this.next.isKeywordFalse()) {
this.advance();
return new LiteralPrimitive(false);
} else if (this.optionalCharacter($LBRACKET)) {
var elements = this.parseExpressionList($RBRACKET);
this.expectCharacter($RBRACKET);
return new LiteralArray(elements);
} else if (this.next.isCharacter($LBRACE)) {
return this.parseLiteralMap();
} else if (this.next.isIdentifier()) {
return this.parseAccessMemberOrMethodCall(_implicitReceiver, false);
} else if (this.next.isNumber()) {
var value = this.next.toNumber();
this.advance();
return new LiteralPrimitive(value);
} else if (this.next.isString()) {
var literalValue = this.next.toString();
this.advance();
return new LiteralPrimitive(literalValue);
} else if (this.index >= this.tokens.length) {
this.error(`Unexpected end of expression: ${this.input}`);
} else {
this.error(`Unexpected token ${this.next}`);
}
// error() throws, so we don't reach here.
throw new BaseException("Fell through all cases in parsePrimary");
}
parseExpressionList(terminator: number): any[] {
var result = [];
if (!this.next.isCharacter(terminator)) {
do {
result.push(this.parsePipe());
} while (this.optionalCharacter($COMMA));
}
return result;
}
parseLiteralMap(): LiteralMap {
var keys = [];
var values = [];
this.expectCharacter($LBRACE);
if (!this.optionalCharacter($RBRACE)) {
do {
var key = this.expectIdentifierOrKeywordOrString();
keys.push(key);
this.expectCharacter($COLON);
values.push(this.parsePipe());
} while (this.optionalCharacter($COMMA));
this.expectCharacter($RBRACE);
}
return new LiteralMap(keys, values);
}
parseAccessMemberOrMethodCall(receiver: AST, isSafe: boolean = false): AST {
let id = this.expectIdentifierOrKeyword();
if (this.optionalCharacter($LPAREN)) {
let args = this.parseCallArguments();
this.expectCharacter($RPAREN);
return isSafe ? new SafeMethodCall(receiver, id, args) : new MethodCall(receiver, id, args);
} else {
if (isSafe) {
if (this.optionalOperator("=")) {
this.error("The '?.' operator cannot be used in the assignment");
} else {
return new SafePropertyRead(receiver, id);
}
} else {
if (this.optionalOperator("=")) {
if (!this.parseAction) {
this.error("Bindings cannot contain assignments");
}
let value = this.parseConditional();
return new PropertyWrite(receiver, id, value);
} else {
return new PropertyRead(receiver, id);
}
}
}
return null;
}
parseCallArguments(): BindingPipe[] {
if (this.next.isCharacter($RPAREN)) return [];
var positionals = [];
do {
positionals.push(this.parsePipe());
} while (this.optionalCharacter($COMMA));
return positionals;
}
parseBlockContent(): AST {
if (!this.parseAction) {
this.error("Binding expression cannot contain chained expression");
}
var exprs = [];
while (this.index < this.tokens.length && !this.next.isCharacter($RBRACE)) {
var expr = this.parseExpression();
exprs.push(expr);
if (this.optionalCharacter($SEMICOLON)) {
while (this.optionalCharacter($SEMICOLON)) {
} // read all semicolons
}
}
if (exprs.length == 0) return new EmptyExpr();
if (exprs.length == 1) return exprs[0];
return new Chain(exprs);
}
/**
* An identifier, a keyword, a string with an optional `-` inbetween.
*/
expectTemplateBindingKey(): string {
var result = '';
var operatorFound = false;
do {
result += this.expectIdentifierOrKeywordOrString();
operatorFound = this.optionalOperator('-');
if (operatorFound) {
result += '-';
}
} while (operatorFound);
return result.toString();
}
parseTemplateBindings(): TemplateBindingParseResult {
var bindings: TemplateBinding[] = [];
var prefix = null;
var warnings: string[] = [];
while (this.index < this.tokens.length) {
var keyIsVar: boolean = this.peekKeywordLet();
if (!keyIsVar && this.peekDeprecatedKeywordVar()) {
keyIsVar = true;
warnings.push(`"var" inside of expressions is deprecated. Use "let" instead!`);
}
if (!keyIsVar && this.peekDeprecatedOperatorHash()) {
keyIsVar = true;
warnings.push(`"#" inside of expressions is deprecated. Use "let" instead!`);
}
if (keyIsVar) {
this.advance();
}
var key = this.expectTemplateBindingKey();
if (!keyIsVar) {
if (prefix == null) {
prefix = key;
} else {
key = prefix + key[0].toUpperCase() + key.substring(1);
}
}
this.optionalCharacter($COLON);
var name = null;
var expression = null;
if (keyIsVar) {
if (this.optionalOperator("=")) {
name = this.expectTemplateBindingKey();
} else {
name = '\$implicit';
}
} else if (this.next !== EOF && !this.peekKeywordLet() && !this.peekDeprecatedKeywordVar() &&
!this.peekDeprecatedOperatorHash()) {
var start = this.inputIndex;
var ast = this.parsePipe();
var source = this.input.substring(start, this.inputIndex);
expression = new ASTWithSource(ast, source, this.location);
}
bindings.push(new TemplateBinding(key, keyIsVar, name, expression));
if (!this.optionalCharacter($SEMICOLON)) {
this.optionalCharacter($COMMA);
}
}
return new TemplateBindingParseResult(bindings, warnings);
}
error(message: string, index: number = null) {
if (isBlank(index)) index = this.index;
var location = (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` :
`at the end of the expression`;
throw new ParseException(message, this.input, location, this.location);
}
}
class SimpleExpressionChecker implements AstVisitor {
static check(ast: AST): boolean {
var s = new SimpleExpressionChecker();
ast.visit(s);
return s.simple;
}
simple = true;
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
visitInterpolation(ast: Interpolation, context: any) { this.simple = false; }
visitLiteralPrimitive(ast: LiteralPrimitive, context: any) {}
visitPropertyRead(ast: PropertyRead, context: any) {}
visitPropertyWrite(ast: PropertyWrite, context: any) { this.simple = false; }
visitSafePropertyRead(ast: SafePropertyRead, context: any) { this.simple = false; }
visitMethodCall(ast: MethodCall, context: any) { this.simple = false; }
visitSafeMethodCall(ast: SafeMethodCall, context: any) { this.simple = false; }
visitFunctionCall(ast: FunctionCall, context: any) { this.simple = false; }
visitLiteralArray(ast: LiteralArray, context: any) { this.visitAll(ast.expressions); }
visitLiteralMap(ast: LiteralMap, context: any) { this.visitAll(ast.values); }
visitBinary(ast: Binary, context: any) { this.simple = false; }
visitPrefixNot(ast: PrefixNot, context: any) { this.simple = false; }
visitConditional(ast: Conditional, context: any) { this.simple = false; }
visitPipe(ast: BindingPipe, context: any) { this.simple = false; }
visitKeyedRead(ast: KeyedRead, context: any) { this.simple = false; }
visitKeyedWrite(ast: KeyedWrite, context: any) { this.simple = false; }
visitAll(asts: any[]): any[] {
var res = ListWrapper.createFixedSize(asts.length);
for (var i = 0; i < asts.length; ++i) {
res[i] = asts[i].visit(this);
}
return res;
}
visitChain(ast: Chain, context: any) { this.simple = false; }
visitQuote(ast: Quote, context: any) { this.simple = false; }
}

View File

@ -0,0 +1,68 @@
import {isPresent} from 'angular2/src/facade/lang';
import {ParseSourceSpan} from './parse_util';
export interface HtmlAst {
sourceSpan: ParseSourceSpan;
visit(visitor: HtmlAstVisitor, context: any): any;
}
export class HtmlTextAst implements HtmlAst {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitText(this, context); }
}
export class HtmlExpansionAst implements HtmlAst {
constructor(public switchValue: string, public type: string, public cases: HtmlExpansionCaseAst[],
public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any {
return visitor.visitExpansion(this, context);
}
}
export class HtmlExpansionCaseAst implements HtmlAst {
constructor(public value: string, public expression: HtmlAst[],
public sourceSpan: ParseSourceSpan, public valueSourceSpan: ParseSourceSpan,
public expSourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any {
return visitor.visitExpansionCase(this, context);
}
}
export class HtmlAttrAst implements HtmlAst {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
}
export class HtmlElementAst implements HtmlAst {
constructor(public name: string, public attrs: HtmlAttrAst[], public children: HtmlAst[],
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitElement(this, context); }
}
export class HtmlCommentAst implements HtmlAst {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitComment(this, context); }
}
export interface HtmlAstVisitor {
visitElement(ast: HtmlElementAst, context: any): any;
visitAttr(ast: HtmlAttrAst, context: any): any;
visitText(ast: HtmlTextAst, context: any): any;
visitComment(ast: HtmlCommentAst, context: any): any;
visitExpansion(ast: HtmlExpansionAst, context: any): any;
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any;
}
export function htmlVisitAll(visitor: HtmlAstVisitor, asts: HtmlAst[], context: any = null): any[] {
var result = [];
asts.forEach(ast => {
var astResult = ast.visit(visitor, context);
if (isPresent(astResult)) {
result.push(astResult);
}
});
return result;
}

View File

@ -0,0 +1,728 @@
import {
StringWrapper,
NumberWrapper,
isPresent,
isBlank,
serializeEnum
} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
import {ParseLocation, ParseError, ParseSourceFile, ParseSourceSpan} from './parse_util';
import {getHtmlTagDefinition, HtmlTagContentType, NAMED_ENTITIES} from './html_tags';
export enum HtmlTokenType {
TAG_OPEN_START,
TAG_OPEN_END,
TAG_OPEN_END_VOID,
TAG_CLOSE,
TEXT,
ESCAPABLE_RAW_TEXT,
RAW_TEXT,
COMMENT_START,
COMMENT_END,
CDATA_START,
CDATA_END,
ATTR_NAME,
ATTR_VALUE,
DOC_TYPE,
EXPANSION_FORM_START,
EXPANSION_CASE_VALUE,
EXPANSION_CASE_EXP_START,
EXPANSION_CASE_EXP_END,
EXPANSION_FORM_END,
EOF
}
export class HtmlToken {
constructor(public type: HtmlTokenType, public parts: string[],
public sourceSpan: ParseSourceSpan) {}
}
export class HtmlTokenError extends ParseError {
constructor(errorMsg: string, public tokenType: HtmlTokenType, span: ParseSourceSpan) {
super(span, errorMsg);
}
}
export class HtmlTokenizeResult {
constructor(public tokens: HtmlToken[], public errors: HtmlTokenError[]) {}
}
export function tokenizeHtml(sourceContent: string, sourceUrl: string,
tokenizeExpansionForms: boolean = false): HtmlTokenizeResult {
return new _HtmlTokenizer(new ParseSourceFile(sourceContent, sourceUrl), tokenizeExpansionForms)
.tokenize();
}
const $EOF = 0;
const $TAB = 9;
const $LF = 10;
const $FF = 12;
const $CR = 13;
const $SPACE = 32;
const $BANG = 33;
const $DQ = 34;
const $HASH = 35;
const $$ = 36;
const $AMPERSAND = 38;
const $SQ = 39;
const $MINUS = 45;
const $SLASH = 47;
const $0 = 48;
const $SEMICOLON = 59;
const $9 = 57;
const $COLON = 58;
const $LT = 60;
const $EQ = 61;
const $GT = 62;
const $QUESTION = 63;
const $LBRACKET = 91;
const $RBRACKET = 93;
const $LBRACE = 123;
const $RBRACE = 125;
const $COMMA = 44;
const $A = 65;
const $F = 70;
const $X = 88;
const $Z = 90;
const $a = 97;
const $f = 102;
const $z = 122;
const $x = 120;
const $NBSP = 160;
var CR_OR_CRLF_REGEXP = /\r\n?/g;
function unexpectedCharacterErrorMsg(charCode: number): string {
var char = charCode === $EOF ? 'EOF' : StringWrapper.fromCharCode(charCode);
return `Unexpected character "${char}"`;
}
function unknownEntityErrorMsg(entitySrc: string): string {
return `Unknown entity "${entitySrc}" - use the "&#<decimal>;" or "&#x<hex>;" syntax`;
}
class ControlFlowError {
constructor(public error: HtmlTokenError) {}
}
// See http://www.w3.org/TR/html51/syntax.html#writing
class _HtmlTokenizer {
private input: string;
private length: number;
// Note: this is always lowercase!
private peek: number = -1;
private nextPeek: number = -1;
private index: number = -1;
private line: number = 0;
private column: number = -1;
private currentTokenStart: ParseLocation;
private currentTokenType: HtmlTokenType;
private expansionCaseStack = [];
tokens: HtmlToken[] = [];
errors: HtmlTokenError[] = [];
constructor(private file: ParseSourceFile, private tokenizeExpansionForms: boolean) {
this.input = file.content;
this.length = file.content.length;
this._advance();
}
private _processCarriageReturns(content: string): string {
// http://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream
// In order to keep the original position in the source, we can not
// pre-process it.
// Instead CRs are processed right before instantiating the tokens.
return StringWrapper.replaceAll(content, CR_OR_CRLF_REGEXP, '\n');
}
tokenize(): HtmlTokenizeResult {
while (this.peek !== $EOF) {
var start = this._getLocation();
try {
if (this._attemptCharCode($LT)) {
if (this._attemptCharCode($BANG)) {
if (this._attemptCharCode($LBRACKET)) {
this._consumeCdata(start);
} else if (this._attemptCharCode($MINUS)) {
this._consumeComment(start);
} else {
this._consumeDocType(start);
}
} else if (this._attemptCharCode($SLASH)) {
this._consumeTagClose(start);
} else {
this._consumeTagOpen(start);
}
} else if (isSpecialFormStart(this.peek, this.nextPeek) && this.tokenizeExpansionForms) {
this._consumeExpansionFormStart();
} else if (this.peek === $EQ && this.tokenizeExpansionForms) {
this._consumeExpansionCaseStart();
} else if (this.peek === $RBRACE && this.isInExpansionCase() &&
this.tokenizeExpansionForms) {
this._consumeExpansionCaseEnd();
} else if (this.peek === $RBRACE && this.isInExpansionForm() &&
this.tokenizeExpansionForms) {
this._consumeExpansionFormEnd();
} else {
this._consumeText();
}
} catch (e) {
if (e instanceof ControlFlowError) {
this.errors.push(e.error);
} else {
throw e;
}
}
}
this._beginToken(HtmlTokenType.EOF);
this._endToken([]);
return new HtmlTokenizeResult(mergeTextTokens(this.tokens), this.errors);
}
private _getLocation(): ParseLocation {
return new ParseLocation(this.file, this.index, this.line, this.column);
}
private _getSpan(start?: ParseLocation, end?: ParseLocation): ParseSourceSpan {
if (isBlank(start)) {
start = this._getLocation();
}
if (isBlank(end)) {
end = this._getLocation();
}
return new ParseSourceSpan(start, end);
}
private _beginToken(type: HtmlTokenType, start: ParseLocation = null) {
if (isBlank(start)) {
start = this._getLocation();
}
this.currentTokenStart = start;
this.currentTokenType = type;
}
private _endToken(parts: string[], end: ParseLocation = null): HtmlToken {
if (isBlank(end)) {
end = this._getLocation();
}
var token = new HtmlToken(this.currentTokenType, parts,
new ParseSourceSpan(this.currentTokenStart, end));
this.tokens.push(token);
this.currentTokenStart = null;
this.currentTokenType = null;
return token;
}
private _createError(msg: string, span: ParseSourceSpan): ControlFlowError {
var error = new HtmlTokenError(msg, this.currentTokenType, span);
this.currentTokenStart = null;
this.currentTokenType = null;
return new ControlFlowError(error);
}
private _advance() {
if (this.index >= this.length) {
throw this._createError(unexpectedCharacterErrorMsg($EOF), this._getSpan());
}
if (this.peek === $LF) {
this.line++;
this.column = 0;
} else if (this.peek !== $LF && this.peek !== $CR) {
this.column++;
}
this.index++;
this.peek = this.index >= this.length ? $EOF : StringWrapper.charCodeAt(this.input, this.index);
this.nextPeek =
this.index + 1 >= this.length ? $EOF : StringWrapper.charCodeAt(this.input, this.index + 1);
}
private _attemptCharCode(charCode: number): boolean {
if (this.peek === charCode) {
this._advance();
return true;
}
return false;
}
private _attemptCharCodeCaseInsensitive(charCode: number): boolean {
if (compareCharCodeCaseInsensitive(this.peek, charCode)) {
this._advance();
return true;
}
return false;
}
private _requireCharCode(charCode: number) {
var location = this._getLocation();
if (!this._attemptCharCode(charCode)) {
throw this._createError(unexpectedCharacterErrorMsg(this.peek),
this._getSpan(location, location));
}
}
private _attemptStr(chars: string): boolean {
for (var i = 0; i < chars.length; i++) {
if (!this._attemptCharCode(StringWrapper.charCodeAt(chars, i))) {
return false;
}
}
return true;
}
private _attemptStrCaseInsensitive(chars: string): boolean {
for (var i = 0; i < chars.length; i++) {
if (!this._attemptCharCodeCaseInsensitive(StringWrapper.charCodeAt(chars, i))) {
return false;
}
}
return true;
}
private _requireStr(chars: string) {
var location = this._getLocation();
if (!this._attemptStr(chars)) {
throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan(location));
}
}
private _attemptCharCodeUntilFn(predicate: Function) {
while (!predicate(this.peek)) {
this._advance();
}
}
private _requireCharCodeUntilFn(predicate: Function, len: number) {
var start = this._getLocation();
this._attemptCharCodeUntilFn(predicate);
if (this.index - start.offset < len) {
throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan(start, start));
}
}
private _attemptUntilChar(char: number) {
while (this.peek !== char) {
this._advance();
}
}
private _readChar(decodeEntities: boolean): string {
if (decodeEntities && this.peek === $AMPERSAND) {
return this._decodeEntity();
} else {
var index = this.index;
this._advance();
return this.input[index];
}
}
private _decodeEntity(): string {
var start = this._getLocation();
this._advance();
if (this._attemptCharCode($HASH)) {
let isHex = this._attemptCharCode($x) || this._attemptCharCode($X);
let numberStart = this._getLocation().offset;
this._attemptCharCodeUntilFn(isDigitEntityEnd);
if (this.peek != $SEMICOLON) {
throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan());
}
this._advance();
let strNum = this.input.substring(numberStart, this.index - 1);
try {
let charCode = NumberWrapper.parseInt(strNum, isHex ? 16 : 10);
return StringWrapper.fromCharCode(charCode);
} catch (e) {
let entity = this.input.substring(start.offset + 1, this.index - 1);
throw this._createError(unknownEntityErrorMsg(entity), this._getSpan(start));
}
} else {
let startPosition = this._savePosition();
this._attemptCharCodeUntilFn(isNamedEntityEnd);
if (this.peek != $SEMICOLON) {
this._restorePosition(startPosition);
return '&';
}
this._advance();
let name = this.input.substring(start.offset + 1, this.index - 1);
let char = NAMED_ENTITIES[name];
if (isBlank(char)) {
throw this._createError(unknownEntityErrorMsg(name), this._getSpan(start));
}
return char;
}
}
private _consumeRawText(decodeEntities: boolean, firstCharOfEnd: number,
attemptEndRest: Function): HtmlToken {
var tagCloseStart;
var textStart = this._getLocation();
this._beginToken(decodeEntities ? HtmlTokenType.ESCAPABLE_RAW_TEXT : HtmlTokenType.RAW_TEXT,
textStart);
var parts = [];
while (true) {
tagCloseStart = this._getLocation();
if (this._attemptCharCode(firstCharOfEnd) && attemptEndRest()) {
break;
}
if (this.index > tagCloseStart.offset) {
parts.push(this.input.substring(tagCloseStart.offset, this.index));
}
while (this.peek !== firstCharOfEnd) {
parts.push(this._readChar(decodeEntities));
}
}
return this._endToken([this._processCarriageReturns(parts.join(''))], tagCloseStart);
}
private _consumeComment(start: ParseLocation) {
this._beginToken(HtmlTokenType.COMMENT_START, start);
this._requireCharCode($MINUS);
this._endToken([]);
var textToken = this._consumeRawText(false, $MINUS, () => this._attemptStr('->'));
this._beginToken(HtmlTokenType.COMMENT_END, textToken.sourceSpan.end);
this._endToken([]);
}
private _consumeCdata(start: ParseLocation) {
this._beginToken(HtmlTokenType.CDATA_START, start);
this._requireStr('CDATA[');
this._endToken([]);
var textToken = this._consumeRawText(false, $RBRACKET, () => this._attemptStr(']>'));
this._beginToken(HtmlTokenType.CDATA_END, textToken.sourceSpan.end);
this._endToken([]);
}
private _consumeDocType(start: ParseLocation) {
this._beginToken(HtmlTokenType.DOC_TYPE, start);
this._attemptUntilChar($GT);
this._advance();
this._endToken([this.input.substring(start.offset + 2, this.index - 1)]);
}
private _consumePrefixAndName(): string[] {
var nameOrPrefixStart = this.index;
var prefix = null;
while (this.peek !== $COLON && !isPrefixEnd(this.peek)) {
this._advance();
}
var nameStart;
if (this.peek === $COLON) {
this._advance();
prefix = this.input.substring(nameOrPrefixStart, this.index - 1);
nameStart = this.index;
} else {
nameStart = nameOrPrefixStart;
}
this._requireCharCodeUntilFn(isNameEnd, this.index === nameStart ? 1 : 0);
var name = this.input.substring(nameStart, this.index);
return [prefix, name];
}
private _consumeTagOpen(start: ParseLocation) {
let savedPos = this._savePosition();
let lowercaseTagName;
try {
if (!isAsciiLetter(this.peek)) {
throw this._createError(unexpectedCharacterErrorMsg(this.peek), this._getSpan());
}
var nameStart = this.index;
this._consumeTagOpenStart(start);
lowercaseTagName = this.input.substring(nameStart, this.index).toLowerCase();
this._attemptCharCodeUntilFn(isNotWhitespace);
while (this.peek !== $SLASH && this.peek !== $GT) {
this._consumeAttributeName();
this._attemptCharCodeUntilFn(isNotWhitespace);
if (this._attemptCharCode($EQ)) {
this._attemptCharCodeUntilFn(isNotWhitespace);
this._consumeAttributeValue();
}
this._attemptCharCodeUntilFn(isNotWhitespace);
}
this._consumeTagOpenEnd();
} catch (e) {
if (e instanceof ControlFlowError) {
// When the start tag is invalid, assume we want a "<"
this._restorePosition(savedPos);
// Back to back text tokens are merged at the end
this._beginToken(HtmlTokenType.TEXT, start);
this._endToken(['<']);
return;
}
throw e;
}
var contentTokenType = getHtmlTagDefinition(lowercaseTagName).contentType;
if (contentTokenType === HtmlTagContentType.RAW_TEXT) {
this._consumeRawTextWithTagClose(lowercaseTagName, false);
} else if (contentTokenType === HtmlTagContentType.ESCAPABLE_RAW_TEXT) {
this._consumeRawTextWithTagClose(lowercaseTagName, true);
}
}
private _consumeRawTextWithTagClose(lowercaseTagName: string, decodeEntities: boolean) {
var textToken = this._consumeRawText(decodeEntities, $LT, () => {
if (!this._attemptCharCode($SLASH)) return false;
this._attemptCharCodeUntilFn(isNotWhitespace);
if (!this._attemptStrCaseInsensitive(lowercaseTagName)) return false;
this._attemptCharCodeUntilFn(isNotWhitespace);
if (!this._attemptCharCode($GT)) return false;
return true;
});
this._beginToken(HtmlTokenType.TAG_CLOSE, textToken.sourceSpan.end);
this._endToken([null, lowercaseTagName]);
}
private _consumeTagOpenStart(start: ParseLocation) {
this._beginToken(HtmlTokenType.TAG_OPEN_START, start);
var parts = this._consumePrefixAndName();
this._endToken(parts);
}
private _consumeAttributeName() {
this._beginToken(HtmlTokenType.ATTR_NAME);
var prefixAndName = this._consumePrefixAndName();
this._endToken(prefixAndName);
}
private _consumeAttributeValue() {
this._beginToken(HtmlTokenType.ATTR_VALUE);
var value;
if (this.peek === $SQ || this.peek === $DQ) {
var quoteChar = this.peek;
this._advance();
var parts = [];
while (this.peek !== quoteChar) {
parts.push(this._readChar(true));
}
value = parts.join('');
this._advance();
} else {
var valueStart = this.index;
this._requireCharCodeUntilFn(isNameEnd, 1);
value = this.input.substring(valueStart, this.index);
}
this._endToken([this._processCarriageReturns(value)]);
}
private _consumeTagOpenEnd() {
var tokenType = this._attemptCharCode($SLASH) ? HtmlTokenType.TAG_OPEN_END_VOID :
HtmlTokenType.TAG_OPEN_END;
this._beginToken(tokenType);
this._requireCharCode($GT);
this._endToken([]);
}
private _consumeTagClose(start: ParseLocation) {
this._beginToken(HtmlTokenType.TAG_CLOSE, start);
this._attemptCharCodeUntilFn(isNotWhitespace);
var prefixAndName;
prefixAndName = this._consumePrefixAndName();
this._attemptCharCodeUntilFn(isNotWhitespace);
this._requireCharCode($GT);
this._endToken(prefixAndName);
}
private _consumeExpansionFormStart() {
this._beginToken(HtmlTokenType.EXPANSION_FORM_START, this._getLocation());
this._requireCharCode($LBRACE);
this._endToken([]);
this._beginToken(HtmlTokenType.RAW_TEXT, this._getLocation());
let condition = this._readUntil($COMMA);
this._endToken([condition], this._getLocation());
this._requireCharCode($COMMA);
this._attemptCharCodeUntilFn(isNotWhitespace);
this._beginToken(HtmlTokenType.RAW_TEXT, this._getLocation());
let type = this._readUntil($COMMA);
this._endToken([type], this._getLocation());
this._requireCharCode($COMMA);
this._attemptCharCodeUntilFn(isNotWhitespace);
this.expansionCaseStack.push(HtmlTokenType.EXPANSION_FORM_START);
}
private _consumeExpansionCaseStart() {
this._requireCharCode($EQ);
this._beginToken(HtmlTokenType.EXPANSION_CASE_VALUE, this._getLocation());
let value = this._readUntil($LBRACE).trim();
this._endToken([value], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this._beginToken(HtmlTokenType.EXPANSION_CASE_EXP_START, this._getLocation());
this._requireCharCode($LBRACE);
this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this.expansionCaseStack.push(HtmlTokenType.EXPANSION_CASE_EXP_START);
}
private _consumeExpansionCaseEnd() {
this._beginToken(HtmlTokenType.EXPANSION_CASE_EXP_END, this._getLocation());
this._requireCharCode($RBRACE);
this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this.expansionCaseStack.pop();
}
private _consumeExpansionFormEnd() {
this._beginToken(HtmlTokenType.EXPANSION_FORM_END, this._getLocation());
this._requireCharCode($RBRACE);
this._endToken([]);
this.expansionCaseStack.pop();
}
private _consumeText() {
var start = this._getLocation();
this._beginToken(HtmlTokenType.TEXT, start);
var parts = [];
let interpolation = false;
if (this.peek === $LBRACE && this.nextPeek === $LBRACE) {
parts.push(this._readChar(true));
parts.push(this._readChar(true));
interpolation = true;
} else {
parts.push(this._readChar(true));
}
while (!this.isTextEnd(interpolation)) {
if (this.peek === $LBRACE && this.nextPeek === $LBRACE) {
parts.push(this._readChar(true));
parts.push(this._readChar(true));
interpolation = true;
} else if (this.peek === $RBRACE && this.nextPeek === $RBRACE && interpolation) {
parts.push(this._readChar(true));
parts.push(this._readChar(true));
interpolation = false;
} else {
parts.push(this._readChar(true));
}
}
this._endToken([this._processCarriageReturns(parts.join(''))]);
}
private isTextEnd(interpolation: boolean): boolean {
if (this.peek === $LT || this.peek === $EOF) return true;
if (this.tokenizeExpansionForms) {
if (isSpecialFormStart(this.peek, this.nextPeek)) return true;
if (this.peek === $RBRACE && !interpolation &&
(this.isInExpansionCase() || this.isInExpansionForm()))
return true;
}
return false;
}
private _savePosition(): number[] {
return [this.peek, this.index, this.column, this.line, this.tokens.length];
}
private _readUntil(char: number): string {
let start = this.index;
this._attemptUntilChar(char);
return this.input.substring(start, this.index);
}
private _restorePosition(position: number[]): void {
this.peek = position[0];
this.index = position[1];
this.column = position[2];
this.line = position[3];
let nbTokens = position[4];
if (nbTokens < this.tokens.length) {
// remove any extra tokens
this.tokens = ListWrapper.slice(this.tokens, 0, nbTokens);
}
}
private isInExpansionCase(): boolean {
return this.expansionCaseStack.length > 0 &&
this.expansionCaseStack[this.expansionCaseStack.length - 1] ===
HtmlTokenType.EXPANSION_CASE_EXP_START;
}
private isInExpansionForm(): boolean {
return this.expansionCaseStack.length > 0 &&
this.expansionCaseStack[this.expansionCaseStack.length - 1] ===
HtmlTokenType.EXPANSION_FORM_START;
}
}
function isNotWhitespace(code: number): boolean {
return !isWhitespace(code) || code === $EOF;
}
function isWhitespace(code: number): boolean {
return (code >= $TAB && code <= $SPACE) || (code === $NBSP);
}
function isNameEnd(code: number): boolean {
return isWhitespace(code) || code === $GT || code === $SLASH || code === $SQ || code === $DQ ||
code === $EQ;
}
function isPrefixEnd(code: number): boolean {
return (code < $a || $z < code) && (code < $A || $Z < code) && (code < $0 || code > $9);
}
function isDigitEntityEnd(code: number): boolean {
return code == $SEMICOLON || code == $EOF || !isAsciiHexDigit(code);
}
function isNamedEntityEnd(code: number): boolean {
return code == $SEMICOLON || code == $EOF || !isAsciiLetter(code);
}
function isSpecialFormStart(peek: number, nextPeek: number): boolean {
return peek === $LBRACE && nextPeek != $LBRACE;
}
function isAsciiLetter(code: number): boolean {
return code >= $a && code <= $z || code >= $A && code <= $Z;
}
function isAsciiHexDigit(code: number): boolean {
return code >= $a && code <= $f || code >= $A && code <= $F || code >= $0 && code <= $9;
}
function compareCharCodeCaseInsensitive(code1: number, code2: number): boolean {
return toUpperCaseCharCode(code1) == toUpperCaseCharCode(code2);
}
function toUpperCaseCharCode(code: number): number {
return code >= $a && code <= $z ? code - $a + $A : code;
}
function mergeTextTokens(srcTokens: HtmlToken[]): HtmlToken[] {
let dstTokens = [];
let lastDstToken: HtmlToken;
for (let i = 0; i < srcTokens.length; i++) {
let token = srcTokens[i];
if (isPresent(lastDstToken) && lastDstToken.type == HtmlTokenType.TEXT &&
token.type == HtmlTokenType.TEXT) {
lastDstToken.parts[0] += token.parts[0];
lastDstToken.sourceSpan.end = token.sourceSpan.end;
} else {
lastDstToken = token;
dstTokens.push(lastDstToken);
}
}
return dstTokens;
}

View File

@ -0,0 +1,369 @@
import {
isPresent,
isBlank,
StringWrapper,
stringify,
assertionsEnabled,
StringJoiner,
serializeEnum,
} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
import {
HtmlAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
HtmlElementAst,
HtmlExpansionAst,
HtmlExpansionCaseAst
} from './html_ast';
import {Injectable} from 'angular2/src/core/di';
import {HtmlToken, HtmlTokenType, tokenizeHtml} from './html_lexer';
import {ParseError, ParseLocation, ParseSourceSpan} from './parse_util';
import {HtmlTagDefinition, getHtmlTagDefinition, getNsPrefix, mergeNsAndName} from './html_tags';
export class HtmlTreeError extends ParseError {
static create(elementName: string, span: ParseSourceSpan, msg: string): HtmlTreeError {
return new HtmlTreeError(elementName, span, msg);
}
constructor(public elementName: string, span: ParseSourceSpan, msg: string) { super(span, msg); }
}
export class HtmlParseTreeResult {
constructor(public rootNodes: HtmlAst[], public errors: ParseError[]) {}
}
@Injectable()
export class HtmlParser {
parse(sourceContent: string, sourceUrl: string,
parseExpansionForms: boolean = false): HtmlParseTreeResult {
var tokensAndErrors = tokenizeHtml(sourceContent, sourceUrl, parseExpansionForms);
var treeAndErrors = new TreeBuilder(tokensAndErrors.tokens).build();
return new HtmlParseTreeResult(treeAndErrors.rootNodes, (<ParseError[]>tokensAndErrors.errors)
.concat(treeAndErrors.errors));
}
}
class TreeBuilder {
private index: number = -1;
private peek: HtmlToken;
private rootNodes: HtmlAst[] = [];
private errors: HtmlTreeError[] = [];
private elementStack: HtmlElementAst[] = [];
constructor(private tokens: HtmlToken[]) { this._advance(); }
build(): HtmlParseTreeResult {
while (this.peek.type !== HtmlTokenType.EOF) {
if (this.peek.type === HtmlTokenType.TAG_OPEN_START) {
this._consumeStartTag(this._advance());
} else if (this.peek.type === HtmlTokenType.TAG_CLOSE) {
this._consumeEndTag(this._advance());
} else if (this.peek.type === HtmlTokenType.CDATA_START) {
this._closeVoidElement();
this._consumeCdata(this._advance());
} else if (this.peek.type === HtmlTokenType.COMMENT_START) {
this._closeVoidElement();
this._consumeComment(this._advance());
} else if (this.peek.type === HtmlTokenType.TEXT ||
this.peek.type === HtmlTokenType.RAW_TEXT ||
this.peek.type === HtmlTokenType.ESCAPABLE_RAW_TEXT) {
this._closeVoidElement();
this._consumeText(this._advance());
} else if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START) {
this._consumeExpansion(this._advance());
} else {
// Skip all other tokens...
this._advance();
}
}
return new HtmlParseTreeResult(this.rootNodes, this.errors);
}
private _advance(): HtmlToken {
var prev = this.peek;
if (this.index < this.tokens.length - 1) {
// Note: there is always an EOF token at the end
this.index++;
}
this.peek = this.tokens[this.index];
return prev;
}
private _advanceIf(type: HtmlTokenType): HtmlToken {
if (this.peek.type === type) {
return this._advance();
}
return null;
}
private _consumeCdata(startToken: HtmlToken) {
this._consumeText(this._advance());
this._advanceIf(HtmlTokenType.CDATA_END);
}
private _consumeComment(token: HtmlToken) {
var text = this._advanceIf(HtmlTokenType.RAW_TEXT);
this._advanceIf(HtmlTokenType.COMMENT_END);
var value = isPresent(text) ? text.parts[0].trim() : null;
this._addToParent(new HtmlCommentAst(value, token.sourceSpan));
}
private _consumeExpansion(token: HtmlToken) {
let switchValue = this._advance();
let type = this._advance();
let cases = [];
// read =
while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) {
let expCase = this._parseExpansionCase();
if (isBlank(expCase)) return; // error
cases.push(expCase);
}
// read the final }
if (this.peek.type !== HtmlTokenType.EXPANSION_FORM_END) {
this.errors.push(
HtmlTreeError.create(null, this.peek.sourceSpan, `Invalid expansion form. Missing '}'.`));
return;
}
this._advance();
let mainSourceSpan = new ParseSourceSpan(token.sourceSpan.start, this.peek.sourceSpan.end);
this._addToParent(new HtmlExpansionAst(switchValue.parts[0], type.parts[0], cases,
mainSourceSpan, switchValue.sourceSpan));
}
private _parseExpansionCase(): HtmlExpansionCaseAst {
let value = this._advance();
// read {
if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) {
this.errors.push(HtmlTreeError.create(null, this.peek.sourceSpan,
`Invalid expansion form. Missing '{'.,`));
return null;
}
// read until }
let start = this._advance();
let exp = this._collectExpansionExpTokens(start);
if (isBlank(exp)) return null;
let end = this._advance();
exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan));
// parse everything in between { and }
let parsedExp = new TreeBuilder(exp).build();
if (parsedExp.errors.length > 0) {
this.errors = this.errors.concat(<HtmlTreeError[]>parsedExp.errors);
return null;
}
let sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
let expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
return new HtmlExpansionCaseAst(value.parts[0], parsedExp.rootNodes, sourceSpan,
value.sourceSpan, expSourceSpan);
}
private _collectExpansionExpTokens(start: HtmlToken): HtmlToken[] {
let exp = [];
let expansionFormStack = [HtmlTokenType.EXPANSION_CASE_EXP_START];
while (true) {
if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START ||
this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_START) {
expansionFormStack.push(this.peek.type);
}
if (this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_END) {
if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_CASE_EXP_START)) {
expansionFormStack.pop();
if (expansionFormStack.length == 0) return exp;
} else {
this.errors.push(
HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`));
return null;
}
}
if (this.peek.type === HtmlTokenType.EXPANSION_FORM_END) {
if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_FORM_START)) {
expansionFormStack.pop();
} else {
this.errors.push(
HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`));
return null;
}
}
if (this.peek.type === HtmlTokenType.EOF) {
this.errors.push(
HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`));
return null;
}
exp.push(this._advance());
}
}
private _consumeText(token: HtmlToken) {
let text = token.parts[0];
if (text.length > 0 && text[0] == '\n') {
let parent = this._getParentElement();
if (isPresent(parent) && parent.children.length == 0 &&
getHtmlTagDefinition(parent.name).ignoreFirstLf) {
text = text.substring(1);
}
}
if (text.length > 0) {
this._addToParent(new HtmlTextAst(text, token.sourceSpan));
}
}
private _closeVoidElement(): void {
if (this.elementStack.length > 0) {
let el = ListWrapper.last(this.elementStack);
if (getHtmlTagDefinition(el.name).isVoid) {
this.elementStack.pop();
}
}
}
private _consumeStartTag(startTagToken: HtmlToken) {
var prefix = startTagToken.parts[0];
var name = startTagToken.parts[1];
var attrs = [];
while (this.peek.type === HtmlTokenType.ATTR_NAME) {
attrs.push(this._consumeAttr(this._advance()));
}
var fullName = getElementFullName(prefix, name, this._getParentElement());
var selfClosing = false;
// Note: There could have been a tokenizer error
// so that we don't get a token for the end tag...
if (this.peek.type === HtmlTokenType.TAG_OPEN_END_VOID) {
this._advance();
selfClosing = true;
if (getNsPrefix(fullName) == null && !getHtmlTagDefinition(fullName).isVoid) {
this.errors.push(HtmlTreeError.create(
fullName, startTagToken.sourceSpan,
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`));
}
} else if (this.peek.type === HtmlTokenType.TAG_OPEN_END) {
this._advance();
selfClosing = false;
}
var end = this.peek.sourceSpan.start;
let span = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
var el = new HtmlElementAst(fullName, attrs, [], span, span, null);
this._pushElement(el);
if (selfClosing) {
this._popElement(fullName);
el.endSourceSpan = span;
}
}
private _pushElement(el: HtmlElementAst) {
if (this.elementStack.length > 0) {
var parentEl = ListWrapper.last(this.elementStack);
if (getHtmlTagDefinition(parentEl.name).isClosedByChild(el.name)) {
this.elementStack.pop();
}
}
var tagDef = getHtmlTagDefinition(el.name);
var parentEl = this._getParentElement();
if (tagDef.requireExtraParent(isPresent(parentEl) ? parentEl.name : null)) {
var newParent = new HtmlElementAst(tagDef.parentToAdd, [], [el], el.sourceSpan,
el.startSourceSpan, el.endSourceSpan);
this._addToParent(newParent);
this.elementStack.push(newParent);
this.elementStack.push(el);
} else {
this._addToParent(el);
this.elementStack.push(el);
}
}
private _consumeEndTag(endTagToken: HtmlToken) {
var fullName =
getElementFullName(endTagToken.parts[0], endTagToken.parts[1], this._getParentElement());
this._getParentElement().endSourceSpan = endTagToken.sourceSpan;
if (getHtmlTagDefinition(fullName).isVoid) {
this.errors.push(
HtmlTreeError.create(fullName, endTagToken.sourceSpan,
`Void elements do not have end tags "${endTagToken.parts[1]}"`));
} else if (!this._popElement(fullName)) {
this.errors.push(HtmlTreeError.create(fullName, endTagToken.sourceSpan,
`Unexpected closing tag "${endTagToken.parts[1]}"`));
}
}
private _popElement(fullName: string): boolean {
for (let stackIndex = this.elementStack.length - 1; stackIndex >= 0; stackIndex--) {
let el = this.elementStack[stackIndex];
if (el.name == fullName) {
ListWrapper.splice(this.elementStack, stackIndex, this.elementStack.length - stackIndex);
return true;
}
if (!getHtmlTagDefinition(el.name).closedByParent) {
return false;
}
}
return false;
}
private _consumeAttr(attrName: HtmlToken): HtmlAttrAst {
var fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]);
var end = attrName.sourceSpan.end;
var value = '';
if (this.peek.type === HtmlTokenType.ATTR_VALUE) {
var valueToken = this._advance();
value = valueToken.parts[0];
end = valueToken.sourceSpan.end;
}
return new HtmlAttrAst(fullName, value, new ParseSourceSpan(attrName.sourceSpan.start, end));
}
private _getParentElement(): HtmlElementAst {
return this.elementStack.length > 0 ? ListWrapper.last(this.elementStack) : null;
}
private _addToParent(node: HtmlAst) {
var parent = this._getParentElement();
if (isPresent(parent)) {
parent.children.push(node);
} else {
this.rootNodes.push(node);
}
}
}
function getElementFullName(prefix: string, localName: string,
parentElement: HtmlElementAst): string {
if (isBlank(prefix)) {
prefix = getHtmlTagDefinition(localName).implicitNamespacePrefix;
if (isBlank(prefix) && isPresent(parentElement)) {
prefix = getNsPrefix(parentElement.name);
}
}
return mergeNsAndName(prefix, localName);
}
function lastOnStack(stack: any[], element: any): boolean {
return stack.length > 0 && stack[stack.length - 1] === element;
}

View File

@ -0,0 +1,427 @@
import {
isPresent,
isBlank,
normalizeBool,
RegExpWrapper,
} from 'angular2/src/facade/lang';
// see http://www.w3.org/TR/html51/syntax.html#named-character-references
// see https://html.spec.whatwg.org/multipage/entities.json
// This list is not exhaustive to keep the compiler footprint low.
// The `&#123;` / `&#x1ab;` syntax should be used when the named character reference does not exist.
export const NAMED_ENTITIES = /*@ts2dart_const*/ {
'Aacute': '\u00C1',
'aacute': '\u00E1',
'Acirc': '\u00C2',
'acirc': '\u00E2',
'acute': '\u00B4',
'AElig': '\u00C6',
'aelig': '\u00E6',
'Agrave': '\u00C0',
'agrave': '\u00E0',
'alefsym': '\u2135',
'Alpha': '\u0391',
'alpha': '\u03B1',
'amp': '&',
'and': '\u2227',
'ang': '\u2220',
'apos': '\u0027',
'Aring': '\u00C5',
'aring': '\u00E5',
'asymp': '\u2248',
'Atilde': '\u00C3',
'atilde': '\u00E3',
'Auml': '\u00C4',
'auml': '\u00E4',
'bdquo': '\u201E',
'Beta': '\u0392',
'beta': '\u03B2',
'brvbar': '\u00A6',
'bull': '\u2022',
'cap': '\u2229',
'Ccedil': '\u00C7',
'ccedil': '\u00E7',
'cedil': '\u00B8',
'cent': '\u00A2',
'Chi': '\u03A7',
'chi': '\u03C7',
'circ': '\u02C6',
'clubs': '\u2663',
'cong': '\u2245',
'copy': '\u00A9',
'crarr': '\u21B5',
'cup': '\u222A',
'curren': '\u00A4',
'dagger': '\u2020',
'Dagger': '\u2021',
'darr': '\u2193',
'dArr': '\u21D3',
'deg': '\u00B0',
'Delta': '\u0394',
'delta': '\u03B4',
'diams': '\u2666',
'divide': '\u00F7',
'Eacute': '\u00C9',
'eacute': '\u00E9',
'Ecirc': '\u00CA',
'ecirc': '\u00EA',
'Egrave': '\u00C8',
'egrave': '\u00E8',
'empty': '\u2205',
'emsp': '\u2003',
'ensp': '\u2002',
'Epsilon': '\u0395',
'epsilon': '\u03B5',
'equiv': '\u2261',
'Eta': '\u0397',
'eta': '\u03B7',
'ETH': '\u00D0',
'eth': '\u00F0',
'Euml': '\u00CB',
'euml': '\u00EB',
'euro': '\u20AC',
'exist': '\u2203',
'fnof': '\u0192',
'forall': '\u2200',
'frac12': '\u00BD',
'frac14': '\u00BC',
'frac34': '\u00BE',
'frasl': '\u2044',
'Gamma': '\u0393',
'gamma': '\u03B3',
'ge': '\u2265',
'gt': '>',
'harr': '\u2194',
'hArr': '\u21D4',
'hearts': '\u2665',
'hellip': '\u2026',
'Iacute': '\u00CD',
'iacute': '\u00ED',
'Icirc': '\u00CE',
'icirc': '\u00EE',
'iexcl': '\u00A1',
'Igrave': '\u00CC',
'igrave': '\u00EC',
'image': '\u2111',
'infin': '\u221E',
'int': '\u222B',
'Iota': '\u0399',
'iota': '\u03B9',
'iquest': '\u00BF',
'isin': '\u2208',
'Iuml': '\u00CF',
'iuml': '\u00EF',
'Kappa': '\u039A',
'kappa': '\u03BA',
'Lambda': '\u039B',
'lambda': '\u03BB',
'lang': '\u27E8',
'laquo': '\u00AB',
'larr': '\u2190',
'lArr': '\u21D0',
'lceil': '\u2308',
'ldquo': '\u201C',
'le': '\u2264',
'lfloor': '\u230A',
'lowast': '\u2217',
'loz': '\u25CA',
'lrm': '\u200E',
'lsaquo': '\u2039',
'lsquo': '\u2018',
'lt': '<',
'macr': '\u00AF',
'mdash': '\u2014',
'micro': '\u00B5',
'middot': '\u00B7',
'minus': '\u2212',
'Mu': '\u039C',
'mu': '\u03BC',
'nabla': '\u2207',
'nbsp': '\u00A0',
'ndash': '\u2013',
'ne': '\u2260',
'ni': '\u220B',
'not': '\u00AC',
'notin': '\u2209',
'nsub': '\u2284',
'Ntilde': '\u00D1',
'ntilde': '\u00F1',
'Nu': '\u039D',
'nu': '\u03BD',
'Oacute': '\u00D3',
'oacute': '\u00F3',
'Ocirc': '\u00D4',
'ocirc': '\u00F4',
'OElig': '\u0152',
'oelig': '\u0153',
'Ograve': '\u00D2',
'ograve': '\u00F2',
'oline': '\u203E',
'Omega': '\u03A9',
'omega': '\u03C9',
'Omicron': '\u039F',
'omicron': '\u03BF',
'oplus': '\u2295',
'or': '\u2228',
'ordf': '\u00AA',
'ordm': '\u00BA',
'Oslash': '\u00D8',
'oslash': '\u00F8',
'Otilde': '\u00D5',
'otilde': '\u00F5',
'otimes': '\u2297',
'Ouml': '\u00D6',
'ouml': '\u00F6',
'para': '\u00B6',
'permil': '\u2030',
'perp': '\u22A5',
'Phi': '\u03A6',
'phi': '\u03C6',
'Pi': '\u03A0',
'pi': '\u03C0',
'piv': '\u03D6',
'plusmn': '\u00B1',
'pound': '\u00A3',
'prime': '\u2032',
'Prime': '\u2033',
'prod': '\u220F',
'prop': '\u221D',
'Psi': '\u03A8',
'psi': '\u03C8',
'quot': '\u0022',
'radic': '\u221A',
'rang': '\u27E9',
'raquo': '\u00BB',
'rarr': '\u2192',
'rArr': '\u21D2',
'rceil': '\u2309',
'rdquo': '\u201D',
'real': '\u211C',
'reg': '\u00AE',
'rfloor': '\u230B',
'Rho': '\u03A1',
'rho': '\u03C1',
'rlm': '\u200F',
'rsaquo': '\u203A',
'rsquo': '\u2019',
'sbquo': '\u201A',
'Scaron': '\u0160',
'scaron': '\u0161',
'sdot': '\u22C5',
'sect': '\u00A7',
'shy': '\u00AD',
'Sigma': '\u03A3',
'sigma': '\u03C3',
'sigmaf': '\u03C2',
'sim': '\u223C',
'spades': '\u2660',
'sub': '\u2282',
'sube': '\u2286',
'sum': '\u2211',
'sup': '\u2283',
'sup1': '\u00B9',
'sup2': '\u00B2',
'sup3': '\u00B3',
'supe': '\u2287',
'szlig': '\u00DF',
'Tau': '\u03A4',
'tau': '\u03C4',
'there4': '\u2234',
'Theta': '\u0398',
'theta': '\u03B8',
'thetasym': '\u03D1',
'thinsp': '\u2009',
'THORN': '\u00DE',
'thorn': '\u00FE',
'tilde': '\u02DC',
'times': '\u00D7',
'trade': '\u2122',
'Uacute': '\u00DA',
'uacute': '\u00FA',
'uarr': '\u2191',
'uArr': '\u21D1',
'Ucirc': '\u00DB',
'ucirc': '\u00FB',
'Ugrave': '\u00D9',
'ugrave': '\u00F9',
'uml': '\u00A8',
'upsih': '\u03D2',
'Upsilon': '\u03A5',
'upsilon': '\u03C5',
'Uuml': '\u00DC',
'uuml': '\u00FC',
'weierp': '\u2118',
'Xi': '\u039E',
'xi': '\u03BE',
'Yacute': '\u00DD',
'yacute': '\u00FD',
'yen': '\u00A5',
'yuml': '\u00FF',
'Yuml': '\u0178',
'Zeta': '\u0396',
'zeta': '\u03B6',
'zwj': '\u200D',
'zwnj': '\u200C',
};
export enum HtmlTagContentType {
RAW_TEXT,
ESCAPABLE_RAW_TEXT,
PARSABLE_DATA
}
export class HtmlTagDefinition {
private closedByChildren: {[key: string]: boolean} = {};
public closedByParent: boolean = false;
public requiredParents: {[key: string]: boolean};
public parentToAdd: string;
public implicitNamespacePrefix: string;
public contentType: HtmlTagContentType;
public isVoid: boolean;
public ignoreFirstLf: boolean;
constructor({closedByChildren, requiredParents, implicitNamespacePrefix, contentType,
closedByParent, isVoid, ignoreFirstLf}: {
closedByChildren?: string[],
closedByParent?: boolean,
requiredParents?: string[],
implicitNamespacePrefix?: string,
contentType?: HtmlTagContentType,
isVoid?: boolean,
ignoreFirstLf?: boolean
} = {}) {
if (isPresent(closedByChildren) && closedByChildren.length > 0) {
closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true);
}
this.isVoid = normalizeBool(isVoid);
this.closedByParent = normalizeBool(closedByParent) || this.isVoid;
if (isPresent(requiredParents) && requiredParents.length > 0) {
this.requiredParents = {};
this.parentToAdd = requiredParents[0];
requiredParents.forEach(tagName => this.requiredParents[tagName] = true);
}
this.implicitNamespacePrefix = implicitNamespacePrefix;
this.contentType = isPresent(contentType) ? contentType : HtmlTagContentType.PARSABLE_DATA;
this.ignoreFirstLf = normalizeBool(ignoreFirstLf);
}
requireExtraParent(currentParent: string): boolean {
if (isBlank(this.requiredParents)) {
return false;
}
if (isBlank(currentParent)) {
return true;
}
let lcParent = currentParent.toLowerCase();
return this.requiredParents[lcParent] != true && lcParent != 'template';
}
isClosedByChild(name: string): boolean {
return this.isVoid || normalizeBool(this.closedByChildren[name.toLowerCase()]);
}
}
// see http://www.w3.org/TR/html51/syntax.html#optional-tags
// This implementation does not fully conform to the HTML5 spec.
var TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
'base': new HtmlTagDefinition({isVoid: true}),
'meta': new HtmlTagDefinition({isVoid: true}),
'area': new HtmlTagDefinition({isVoid: true}),
'embed': new HtmlTagDefinition({isVoid: true}),
'link': new HtmlTagDefinition({isVoid: true}),
'img': new HtmlTagDefinition({isVoid: true}),
'input': new HtmlTagDefinition({isVoid: true}),
'param': new HtmlTagDefinition({isVoid: true}),
'hr': new HtmlTagDefinition({isVoid: true}),
'br': new HtmlTagDefinition({isVoid: true}),
'source': new HtmlTagDefinition({isVoid: true}),
'track': new HtmlTagDefinition({isVoid: true}),
'wbr': new HtmlTagDefinition({isVoid: true}),
'p': new HtmlTagDefinition({
closedByChildren: [
'address',
'article',
'aside',
'blockquote',
'div',
'dl',
'fieldset',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'header',
'hgroup',
'hr',
'main',
'nav',
'ol',
'p',
'pre',
'section',
'table',
'ul'
],
closedByParent: true
}),
'thead': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot']}),
'tbody': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot'], closedByParent: true}),
'tfoot': new HtmlTagDefinition({closedByChildren: ['tbody'], closedByParent: true}),
'tr': new HtmlTagDefinition({
closedByChildren: ['tr'],
requiredParents: ['tbody', 'tfoot', 'thead'],
closedByParent: true
}),
'td': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
'th': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
'col': new HtmlTagDefinition({requiredParents: ['colgroup'], isVoid: true}),
'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}),
'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}),
'li': new HtmlTagDefinition({closedByChildren: ['li'], closedByParent: true}),
'dt': new HtmlTagDefinition({closedByChildren: ['dt', 'dd']}),
'dd': new HtmlTagDefinition({closedByChildren: ['dt', 'dd'], closedByParent: true}),
'rb': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'rt': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'rtc': new HtmlTagDefinition({closedByChildren: ['rb', 'rtc', 'rp'], closedByParent: true}),
'rp': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'optgroup': new HtmlTagDefinition({closedByChildren: ['optgroup'], closedByParent: true}),
'option': new HtmlTagDefinition({closedByChildren: ['option', 'optgroup'], closedByParent: true}),
'pre': new HtmlTagDefinition({ignoreFirstLf: true}),
'listing': new HtmlTagDefinition({ignoreFirstLf: true}),
'style': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}),
'script': new HtmlTagDefinition({contentType: HtmlTagContentType.RAW_TEXT}),
'title': new HtmlTagDefinition({contentType: HtmlTagContentType.ESCAPABLE_RAW_TEXT}),
'textarea': new HtmlTagDefinition(
{contentType: HtmlTagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true}),
};
var DEFAULT_TAG_DEFINITION = new HtmlTagDefinition();
export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition {
var result = TAG_DEFINITIONS[tagName.toLowerCase()];
return isPresent(result) ? result : DEFAULT_TAG_DEFINITION;
}
var NS_PREFIX_RE = /^@([^:]+):(.+)/g;
export function splitNsName(elementName: string): string[] {
if (elementName[0] != '@') {
return [null, elementName];
}
let match = RegExpWrapper.firstMatch(NS_PREFIX_RE, elementName);
return [match[1], match[2]];
}
export function getNsPrefix(elementName: string): string {
return splitNsName(elementName)[0];
}
export function mergeNsAndName(prefix: string, localName: string): string {
return isPresent(prefix) ? `@${prefix}:${localName}` : localName;
}

View File

@ -0,0 +1,116 @@
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
HtmlExpansionAst,
HtmlExpansionCaseAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {BaseException} from 'angular2/src/facade/exceptions';
/**
* Expands special forms into elements.
*
* For example,
*
* ```
* { messages.length, plural,
* =0 {zero}
* =1 {one}
* =other {more than one}
* }
* ```
*
* will be expanded into
*
* ```
* <ul [ngPlural]="messages.length">
* <template [ngPluralCase]="0"><li i18n="plural_0">zero</li></template>
* <template [ngPluralCase]="1"><li i18n="plural_1">one</li></template>
* <template [ngPluralCase]="other"><li i18n="plural_other">more than one</li></template>
* </ul>
* ```
*/
export function expandNodes(nodes: HtmlAst[]): ExpansionResult {
let e = new _Expander();
let n = htmlVisitAll(e, nodes);
return new ExpansionResult(n, e.expanded);
}
export class ExpansionResult {
constructor(public nodes: HtmlAst[], public expanded: boolean) {}
}
class _Expander implements HtmlAstVisitor {
expanded: boolean = false;
constructor() {}
visitElement(ast: HtmlElementAst, context: any): any {
return new HtmlElementAst(ast.name, ast.attrs, htmlVisitAll(this, ast.children), ast.sourceSpan,
ast.startSourceSpan, ast.endSourceSpan);
}
visitAttr(ast: HtmlAttrAst, context: any): any { return ast; }
visitText(ast: HtmlTextAst, context: any): any { return ast; }
visitComment(ast: HtmlCommentAst, context: any): any { return ast; }
visitExpansion(ast: HtmlExpansionAst, context: any): any {
this.expanded = true;
return ast.type == "plural" ? _expandPluralForm(ast) : _expandDefaultForm(ast);
}
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
throw new BaseException("Should not be reached");
}
}
function _expandPluralForm(ast: HtmlExpansionAst): HtmlElementAst {
let children = ast.cases.map(c => {
let expansionResult = expandNodes(c.expression);
let i18nAttrs = expansionResult.expanded ?
[] :
[new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)];
return new HtmlElementAst(`template`,
[
new HtmlAttrAst("ngPluralCase", c.value, c.valueSourceSpan),
],
[
new HtmlElementAst(`li`, i18nAttrs, expansionResult.nodes,
c.sourceSpan, c.sourceSpan, c.sourceSpan)
],
c.sourceSpan, c.sourceSpan, c.sourceSpan);
});
let switchAttr = new HtmlAttrAst("[ngPlural]", ast.switchValue, ast.switchValueSourceSpan);
return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan,
ast.sourceSpan);
}
function _expandDefaultForm(ast: HtmlExpansionAst): HtmlElementAst {
let children = ast.cases.map(c => {
let expansionResult = expandNodes(c.expression);
let i18nAttrs = expansionResult.expanded ?
[] :
[new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)];
return new HtmlElementAst(`template`,
[
new HtmlAttrAst("ngSwitchWhen", c.value, c.valueSourceSpan),
],
[
new HtmlElementAst(`li`, i18nAttrs, expansionResult.nodes,
c.sourceSpan, c.sourceSpan, c.sourceSpan)
],
c.sourceSpan, c.sourceSpan, c.sourceSpan);
});
let switchAttr = new HtmlAttrAst("[ngSwitch]", ast.switchValue, ast.switchValueSourceSpan);
return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan,
ast.sourceSpan);
}

View File

@ -0,0 +1,377 @@
import {HtmlParser, HtmlParseTreeResult} from 'angular2/src/compiler/html_parser';
import {ParseSourceSpan, ParseError} from 'angular2/src/compiler/parse_util';
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
HtmlExpansionAst,
HtmlExpansionCaseAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {RegExpWrapper, NumberWrapper, isPresent} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {Message, id} from './message';
import {expandNodes} from './expander';
import {
messageFromAttribute,
I18nError,
I18N_ATTR_PREFIX,
I18N_ATTR,
partition,
Part,
stringifyNodes,
meaning,
getPhNameFromBinding,
dedupePhName
} from './shared';
const _I18N_ATTR = "i18n";
const _PLACEHOLDER_ELEMENT = "ph";
const _NAME_ATTR = "name";
const _I18N_ATTR_PREFIX = "i18n-";
let _PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\w)+")\\>\\<\\/ph\\>`);
/**
* Creates an i18n-ed version of the parsed template.
*
* Algorithm:
*
* To understand the algorithm, you need to know how partitioning works.
* Partitioning is required as we can use two i18n comments to group node siblings together.
* That is why we cannot just use nodes.
*
* Partitioning transforms an array of HtmlAst into an array of Part.
* A part can optionally contain a root element or a root text node. And it can also contain
* children.
* A part can contain i18n property, in which case it needs to be transalted.
*
* Example:
*
* The following array of nodes will be split into four parts:
*
* ```
* <a>A</a>
* <b i18n>B</b>
* <!-- i18n -->
* <c>C</c>
* D
* <!-- /i18n -->
* E
* ```
*
* Part 1 containing the a tag. It should not be translated.
* Part 2 containing the b tag. It should be translated.
* Part 3 containing the c tag and the D text node. It should be translated.
* Part 4 containing the E text node. It should not be translated.
*
*
* It is also important to understand how we stringify nodes to create a message.
*
* We walk the tree and replace every element node with a placeholder. We also replace
* all expressions in interpolation with placeholders. We also insert a placeholder element
* to wrap a text node containing interpolation.
*
* Example:
*
* The following tree:
*
* ```
* <a>A{{I}}</a><b>B</b>
* ```
*
* will be stringified into:
* ```
* <ph name="e0"><ph name="t1">A<ph name="0"/></ph></ph><ph name="e2">B</ph>
* ```
*
* This is what the algorithm does:
*
* 1. Use the provided html parser to get the html AST of the template.
* 2. Partition the root nodes, and process each part separately.
* 3. If a part does not have the i18n attribute, recurse to process children and attributes.
* 4. If a part has the i18n attribute, merge the translated i18n part with the original tree.
*
* This is how the merging works:
*
* 1. Use the stringify function to get the message id. Look up the message in the map.
* 2. Get the translated message. At this point we have two trees: the original tree
* and the translated tree, where all the elements are replaced with placeholders.
* 3. Use the original tree to create a mapping Index:number -> HtmlAst.
* 4. Walk the translated tree.
* 5. If we encounter a placeholder element, get is name property.
* 6. Get the type and the index of the node using the name property.
* 7. If the type is 'e', which means element, then:
* - translate the attributes of the original element
* - recurse to merge the children
* - create a new element using the original element name, original position,
* and translated children and attributes
* 8. If the type if 't', which means text, then:
* - get the list of expressions from the original node.
* - get the string version of the interpolation subtree
* - find all the placeholders in the translated message, and replace them with the
* corresponding original expressions
*/
export class I18nHtmlParser implements HtmlParser {
errors: ParseError[];
constructor(private _htmlParser: HtmlParser, private _parser: Parser,
private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]}) {}
parse(sourceContent: string, sourceUrl: string,
parseExpansionForms: boolean = false): HtmlParseTreeResult {
this.errors = [];
let res = this._htmlParser.parse(sourceContent, sourceUrl, true);
if (res.errors.length > 0) {
return res;
} else {
let nodes = this._recurse(expandNodes(res.rootNodes).nodes);
return this.errors.length > 0 ? new HtmlParseTreeResult([], this.errors) :
new HtmlParseTreeResult(nodes, []);
}
}
private _processI18nPart(p: Part): HtmlAst[] {
try {
return p.hasI18n ? this._mergeI18Part(p) : this._recurseIntoI18nPart(p);
} catch (e) {
if (e instanceof I18nError) {
this.errors.push(e);
return [];
} else {
throw e;
}
}
}
private _mergeI18Part(p: Part): HtmlAst[] {
let message = p.createMessage(this._parser);
let messageId = id(message);
if (!StringMapWrapper.contains(this._messages, messageId)) {
throw new I18nError(
p.sourceSpan, `Cannot find message for id '${messageId}', content '${message.content}'.`);
}
let parsedMessage = this._messages[messageId];
return this._mergeTrees(p, parsedMessage, p.children);
}
private _recurseIntoI18nPart(p: Part): HtmlAst[] {
// we found an element without an i18n attribute
// we need to recurse in cause its children may have i18n set
// we also need to translate its attributes
if (isPresent(p.rootElement)) {
let root = p.rootElement;
let children = this._recurse(p.children);
let attrs = this._i18nAttributes(root);
return [
new HtmlElementAst(root.name, attrs, children, root.sourceSpan, root.startSourceSpan,
root.endSourceSpan)
];
// a text node without i18n or interpolation, nothing to do
} else if (isPresent(p.rootTextNode)) {
return [p.rootTextNode];
} else {
return this._recurse(p.children);
}
}
private _recurse(nodes: HtmlAst[]): HtmlAst[] {
let ps = partition(nodes, this.errors);
return ListWrapper.flatten(ps.map(p => this._processI18nPart(p)));
}
private _mergeTrees(p: Part, translated: HtmlAst[], original: HtmlAst[]): HtmlAst[] {
let l = new _CreateNodeMapping();
htmlVisitAll(l, original);
// merge the translated tree with the original tree.
// we do it by preserving the source code position of the original tree
let merged = this._mergeTreesHelper(translated, l.mapping);
// if the root element is present, we need to create a new root element with its attributes
// translated
if (isPresent(p.rootElement)) {
let root = p.rootElement;
let attrs = this._i18nAttributes(root);
return [
new HtmlElementAst(root.name, attrs, merged, root.sourceSpan, root.startSourceSpan,
root.endSourceSpan)
];
// this should never happen with a part. Parts that have root text node should not be merged.
} else if (isPresent(p.rootTextNode)) {
throw new BaseException("should not be reached");
} else {
return merged;
}
}
private _mergeTreesHelper(translated: HtmlAst[], mapping: HtmlAst[]): HtmlAst[] {
return translated.map(t => {
if (t instanceof HtmlElementAst) {
return this._mergeElementOrInterpolation(t, translated, mapping);
} else if (t instanceof HtmlTextAst) {
return t;
} else {
throw new BaseException("should not be reached");
}
});
}
private _mergeElementOrInterpolation(t: HtmlElementAst, translated: HtmlAst[],
mapping: HtmlAst[]): HtmlAst {
let name = this._getName(t);
let type = name[0];
let index = NumberWrapper.parseInt(name.substring(1), 10);
let originalNode = mapping[index];
if (type == "t") {
return this._mergeTextInterpolation(t, <HtmlTextAst>originalNode);
} else if (type == "e") {
return this._mergeElement(t, <HtmlElementAst>originalNode, mapping);
} else {
throw new BaseException("should not be reached");
}
}
private _getName(t: HtmlElementAst): string {
if (t.name != _PLACEHOLDER_ELEMENT) {
throw new I18nError(
t.sourceSpan,
`Unexpected tag "${t.name}". Only "${_PLACEHOLDER_ELEMENT}" tags are allowed.`);
}
let names = t.attrs.filter(a => a.name == _NAME_ATTR);
if (names.length == 0) {
throw new I18nError(t.sourceSpan, `Missing "${_NAME_ATTR}" attribute.`);
}
return names[0].value;
}
private _mergeTextInterpolation(t: HtmlElementAst, originalNode: HtmlTextAst): HtmlTextAst {
let split =
this._parser.splitInterpolation(originalNode.value, originalNode.sourceSpan.toString());
let exps = isPresent(split) ? split.expressions : [];
let messageSubstring =
this._messagesContent.substring(t.startSourceSpan.end.offset, t.endSourceSpan.start.offset);
let translated =
this._replacePlaceholdersWithExpressions(messageSubstring, exps, originalNode.sourceSpan);
return new HtmlTextAst(translated, originalNode.sourceSpan);
}
private _mergeElement(t: HtmlElementAst, originalNode: HtmlElementAst,
mapping: HtmlAst[]): HtmlElementAst {
let children = this._mergeTreesHelper(t.children, mapping);
return new HtmlElementAst(originalNode.name, this._i18nAttributes(originalNode), children,
originalNode.sourceSpan, originalNode.startSourceSpan,
originalNode.endSourceSpan);
}
private _i18nAttributes(el: HtmlElementAst): HtmlAttrAst[] {
let res = [];
el.attrs.forEach(attr => {
if (attr.name.startsWith(I18N_ATTR_PREFIX) || attr.name == I18N_ATTR) return;
let i18ns = el.attrs.filter(a => a.name == `i18n-${attr.name}`);
if (i18ns.length == 0) {
res.push(attr);
return;
}
let i18n = i18ns[0];
let message = messageFromAttribute(this._parser, el, i18n);
let messageId = id(message);
if (StringMapWrapper.contains(this._messages, messageId)) {
let updatedMessage = this._replaceInterpolationInAttr(attr, this._messages[messageId]);
res.push(new HtmlAttrAst(attr.name, updatedMessage, attr.sourceSpan));
} else {
throw new I18nError(
attr.sourceSpan,
`Cannot find message for id '${messageId}', content '${message.content}'.`);
}
});
return res;
}
private _replaceInterpolationInAttr(attr: HtmlAttrAst, msg: HtmlAst[]): string {
let split = this._parser.splitInterpolation(attr.value, attr.sourceSpan.toString());
let exps = isPresent(split) ? split.expressions : [];
let first = msg[0];
let last = msg[msg.length - 1];
let start = first.sourceSpan.start.offset;
let end =
last instanceof HtmlElementAst ? last.endSourceSpan.end.offset : last.sourceSpan.end.offset;
let messageSubstring = this._messagesContent.substring(start, end);
return this._replacePlaceholdersWithExpressions(messageSubstring, exps, attr.sourceSpan);
};
private _replacePlaceholdersWithExpressions(message: string, exps: string[],
sourceSpan: ParseSourceSpan): string {
let expMap = this._buildExprMap(exps);
return RegExpWrapper.replaceAll(_PLACEHOLDER_EXPANDED_REGEXP, message, (match) => {
let nameWithQuotes = match[2];
let name = nameWithQuotes.substring(1, nameWithQuotes.length - 1);
return this._convertIntoExpression(name, expMap, sourceSpan);
});
}
private _buildExprMap(exps: string[]): Map<string, string> {
let expMap = new Map<string, string>();
let usedNames = new Map<string, number>();
for (var i = 0; i < exps.length; i++) {
let phName = getPhNameFromBinding(exps[i], i);
expMap.set(dedupePhName(usedNames, phName), exps[i]);
}
return expMap;
}
private _convertIntoExpression(name: string, expMap: Map<string, string>,
sourceSpan: ParseSourceSpan) {
if (expMap.has(name)) {
return `{{${expMap.get(name)}}}`;
} else {
throw new I18nError(sourceSpan, `Invalid interpolation name '${name}'`);
}
}
}
class _CreateNodeMapping implements HtmlAstVisitor {
mapping: HtmlAst[] = [];
visitElement(ast: HtmlElementAst, context: any): any {
this.mapping.push(ast);
htmlVisitAll(this, ast.children);
return null;
}
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
visitText(ast: HtmlTextAst, context: any): any {
this.mapping.push(ast);
return null;
}
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
}

View File

@ -0,0 +1,21 @@
import {isPresent, escape} from 'angular2/src/facade/lang';
/**
* A message extracted from a template.
*
* The identity of a message is comprised of `content` and `meaning`.
*
* `description` is additional information provided to the translator.
*/
export class Message {
constructor(public content: string, public meaning: string, public description: string = null) {}
}
/**
* Computes the id of a message
*/
export function id(m: Message): string {
let meaning = isPresent(m.meaning) ? m.meaning : "";
let content = isPresent(m.content) ? m.content : "";
return escape(`$ng|${meaning}|${content}`);
}

View File

@ -0,0 +1,178 @@
import {HtmlParser} from 'angular2/src/compiler/html_parser';
import {ParseSourceSpan, ParseError} from 'angular2/src/compiler/parse_util';
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {Message, id} from './message';
import {expandNodes} from './expander';
import {
I18nError,
Part,
I18N_ATTR_PREFIX,
partition,
meaning,
description,
stringifyNodes,
messageFromAttribute
} from './shared';
/**
* All messages extracted from a template.
*/
export class ExtractionResult {
constructor(public messages: Message[], public errors: ParseError[]) {}
}
/**
* Removes duplicate messages.
*
* E.g.
*
* ```
* var m = [new Message("message", "meaning", "desc1"), new Message("message", "meaning",
* "desc2")];
* expect(removeDuplicates(m)).toEqual([new Message("message", "meaning", "desc1")]);
* ```
*/
export function removeDuplicates(messages: Message[]): Message[] {
let uniq: {[key: string]: Message} = {};
messages.forEach(m => {
if (!StringMapWrapper.contains(uniq, id(m))) {
uniq[id(m)] = m;
}
});
return StringMapWrapper.values(uniq);
}
/**
* Extracts all messages from a template.
*
* Algorithm:
*
* To understand the algorithm, you need to know how partitioning works.
* Partitioning is required as we can use two i18n comments to group node siblings together.
* That is why we cannot just use nodes.
*
* Partitioning transforms an array of HtmlAst into an array of Part.
* A part can optionally contain a root element or a root text node. And it can also contain
* children.
* A part can contain i18n property, in which case it needs to be extracted.
*
* Example:
*
* The following array of nodes will be split into four parts:
*
* ```
* <a>A</a>
* <b i18n>B</b>
* <!-- i18n -->
* <c>C</c>
* D
* <!-- /i18n -->
* E
* ```
*
* Part 1 containing the a tag. It should not be translated.
* Part 2 containing the b tag. It should be translated.
* Part 3 containing the c tag and the D text node. It should be translated.
* Part 4 containing the E text node. It should not be translated..
*
* It is also important to understand how we stringify nodes to create a message.
*
* We walk the tree and replace every element node with a placeholder. We also replace
* all expressions in interpolation with placeholders. We also insert a placeholder element
* to wrap a text node containing interpolation.
*
* Example:
*
* The following tree:
*
* ```
* <a>A{{I}}</a><b>B</b>
* ```
*
* will be stringified into:
* ```
* <ph name="e0"><ph name="t1">A<ph name="0"/></ph></ph><ph name="e2">B</ph>
* ```
*
* This is what the algorithm does:
*
* 1. Use the provided html parser to get the html AST of the template.
* 2. Partition the root nodes, and process each part separately.
* 3. If a part does not have the i18n attribute, recurse to process children and attributes.
* 4. If a part has the i18n attribute, stringify the nodes to create a Message.
*/
export class MessageExtractor {
messages: Message[];
errors: ParseError[];
constructor(private _htmlParser: HtmlParser, private _parser: Parser) {}
extract(template: string, sourceUrl: string): ExtractionResult {
this.messages = [];
this.errors = [];
let res = this._htmlParser.parse(template, sourceUrl, true);
if (res.errors.length > 0) {
return new ExtractionResult([], res.errors);
} else {
this._recurse(expandNodes(res.rootNodes).nodes);
return new ExtractionResult(this.messages, this.errors);
}
}
private _extractMessagesFromPart(p: Part): void {
if (p.hasI18n) {
this.messages.push(p.createMessage(this._parser));
this._recurseToExtractMessagesFromAttributes(p.children);
} else {
this._recurse(p.children);
}
if (isPresent(p.rootElement)) {
this._extractMessagesFromAttributes(p.rootElement);
}
}
private _recurse(nodes: HtmlAst[]): void {
if (isPresent(nodes)) {
let ps = partition(nodes, this.errors);
ps.forEach(p => this._extractMessagesFromPart(p));
}
}
private _recurseToExtractMessagesFromAttributes(nodes: HtmlAst[]): void {
nodes.forEach(n => {
if (n instanceof HtmlElementAst) {
this._extractMessagesFromAttributes(n);
this._recurseToExtractMessagesFromAttributes(n.children);
}
});
}
private _extractMessagesFromAttributes(p: HtmlElementAst): void {
p.attrs.forEach(attr => {
if (attr.name.startsWith(I18N_ATTR_PREFIX)) {
try {
this.messages.push(messageFromAttribute(this._parser, p, attr));
} catch (e) {
if (e instanceof I18nError) {
this.errors.push(e);
} else {
throw e;
}
}
}
});
}
}

View File

@ -0,0 +1,191 @@
import {ParseSourceSpan, ParseError} from 'angular2/src/compiler/parse_util';
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
HtmlExpansionAst,
HtmlExpansionCaseAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang';
import {Message} from './message';
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
export const I18N_ATTR = "i18n";
export const I18N_ATTR_PREFIX = "i18n-";
var CUSTOM_PH_EXP = /\/\/[\s\S]*i18n[\s\S]*\([\s\S]*ph[\s\S]*=[\s\S]*"([\s\S]*?)"[\s\S]*\)/g;
/**
* An i18n error.
*/
export class I18nError extends ParseError {
constructor(span: ParseSourceSpan, msg: string) { super(span, msg); }
}
// Man, this is so ugly!
export function partition(nodes: HtmlAst[], errors: ParseError[]): Part[] {
let res = [];
for (let i = 0; i < nodes.length; ++i) {
let n = nodes[i];
let temp = [];
if (_isOpeningComment(n)) {
let i18n = (<HtmlCommentAst>n).value.substring(5).trim();
i++;
while (!_isClosingComment(nodes[i])) {
temp.push(nodes[i++]);
if (i === nodes.length) {
errors.push(new I18nError(n.sourceSpan, "Missing closing 'i18n' comment."));
break;
}
}
res.push(new Part(null, null, temp, i18n, true));
} else if (n instanceof HtmlElementAst) {
let i18n = _findI18nAttr(n);
res.push(new Part(n, null, n.children, isPresent(i18n) ? i18n.value : null, isPresent(i18n)));
} else if (n instanceof HtmlTextAst) {
res.push(new Part(null, n, null, null, false));
}
}
return res;
}
export class Part {
constructor(public rootElement: HtmlElementAst, public rootTextNode: HtmlTextAst,
public children: HtmlAst[], public i18n: string, public hasI18n: boolean) {}
get sourceSpan(): ParseSourceSpan {
if (isPresent(this.rootElement))
return this.rootElement.sourceSpan;
else if (isPresent(this.rootTextNode))
return this.rootTextNode.sourceSpan;
else
return this.children[0].sourceSpan;
}
createMessage(parser: Parser): Message {
return new Message(stringifyNodes(this.children, parser), meaning(this.i18n),
description(this.i18n));
}
}
function _isOpeningComment(n: HtmlAst): boolean {
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value.startsWith("i18n:");
}
function _isClosingComment(n: HtmlAst): boolean {
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value == "/i18n";
}
function _findI18nAttr(p: HtmlElementAst): HtmlAttrAst {
let i18n = p.attrs.filter(a => a.name == I18N_ATTR);
return i18n.length == 0 ? null : i18n[0];
}
export function meaning(i18n: string): string {
if (isBlank(i18n) || i18n == "") return null;
return i18n.split("|")[0];
}
export function description(i18n: string): string {
if (isBlank(i18n) || i18n == "") return null;
let parts = i18n.split("|");
return parts.length > 1 ? parts[1] : null;
}
export function messageFromAttribute(parser: Parser, p: HtmlElementAst,
attr: HtmlAttrAst): Message {
let expectedName = attr.name.substring(5);
let matching = p.attrs.filter(a => a.name == expectedName);
if (matching.length > 0) {
let value = removeInterpolation(matching[0].value, matching[0].sourceSpan, parser);
return new Message(value, meaning(attr.value), description(attr.value));
} else {
throw new I18nError(p.sourceSpan, `Missing attribute '${expectedName}'.`);
}
}
export function removeInterpolation(value: string, source: ParseSourceSpan,
parser: Parser): string {
try {
let parsed = parser.splitInterpolation(value, source.toString());
let usedNames = new Map<string, number>();
if (isPresent(parsed)) {
let res = "";
for (let i = 0; i < parsed.strings.length; ++i) {
res += parsed.strings[i];
if (i != parsed.strings.length - 1) {
let customPhName = getPhNameFromBinding(parsed.expressions[i], i);
customPhName = dedupePhName(usedNames, customPhName);
res += `<ph name="${customPhName}"/>`;
}
}
return res;
} else {
return value;
}
} catch (e) {
return value;
}
}
export function getPhNameFromBinding(input: string, index: number): string {
let customPhMatch = StringWrapper.split(input, CUSTOM_PH_EXP);
return customPhMatch.length > 1 ? customPhMatch[1] : `${index}`;
}
export function dedupePhName(usedNames: Map<string, number>, name: string): string {
let duplicateNameCount = usedNames.get(name);
if (isPresent(duplicateNameCount)) {
usedNames.set(name, duplicateNameCount + 1);
return `${name}_${duplicateNameCount}`;
} else {
usedNames.set(name, 1);
return name;
}
}
export function stringifyNodes(nodes: HtmlAst[], parser: Parser): string {
let visitor = new _StringifyVisitor(parser);
return htmlVisitAll(visitor, nodes).join("");
}
class _StringifyVisitor implements HtmlAstVisitor {
private _index: number = 0;
constructor(private _parser: Parser) {}
visitElement(ast: HtmlElementAst, context: any): any {
let name = this._index++;
let children = this._join(htmlVisitAll(this, ast.children), "");
return `<ph name="e${name}">${children}</ph>`;
}
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
visitText(ast: HtmlTextAst, context: any): any {
let index = this._index++;
let noInterpolation = removeInterpolation(ast.value, ast.sourceSpan, this._parser);
if (noInterpolation != ast.value) {
return `<ph name="t${index}">${noInterpolation}</ph>`;
} else {
return ast.value;
}
}
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
private _join(strs: string[], str: string): string {
return strs.filter(s => s.length > 0).join(str);
}
}

View File

@ -0,0 +1,95 @@
import {isPresent, isBlank, RegExpWrapper} from 'angular2/src/facade/lang';
import {HtmlAst, HtmlElementAst} from 'angular2/src/compiler/html_ast';
import {Message, id} from './message';
import {HtmlParser} from 'angular2/src/compiler/html_parser';
import {ParseSourceSpan, ParseError} from 'angular2/src/compiler/parse_util';
let _PLACEHOLDER_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\w)+")\\/\\>`);
const _ID_ATTR = "id";
const _MSG_ELEMENT = "msg";
const _BUNDLE_ELEMENT = "message-bundle";
export function serializeXmb(messages: Message[]): string {
let ms = messages.map((m) => _serializeMessage(m)).join("");
return `<message-bundle>${ms}</message-bundle>`;
}
export class XmbDeserializationResult {
constructor(public content: string, public messages: {[key: string]: HtmlAst[]},
public errors: ParseError[]) {}
}
export class XmbDeserializationError extends ParseError {
constructor(span: ParseSourceSpan, msg: string) { super(span, msg); }
}
export function deserializeXmb(content: string, url: string): XmbDeserializationResult {
let parser = new HtmlParser();
let normalizedContent = _expandPlaceholder(content.trim());
let parsed = parser.parse(normalizedContent, url);
if (parsed.errors.length > 0) {
return new XmbDeserializationResult(null, {}, parsed.errors);
}
if (_checkRootElement(parsed.rootNodes)) {
return new XmbDeserializationResult(
null, {}, [new XmbDeserializationError(null, `Missing element "${_BUNDLE_ELEMENT}"`)]);
}
let bundleEl = <HtmlElementAst>parsed.rootNodes[0]; // test this
let errors = [];
let messages: {[key: string]: HtmlAst[]} = {};
_createMessages(bundleEl.children, messages, errors);
return (errors.length == 0) ?
new XmbDeserializationResult(normalizedContent, messages, []) :
new XmbDeserializationResult(null, <{[key: string]: HtmlAst[]}>{}, errors);
}
function _checkRootElement(nodes: HtmlAst[]): boolean {
return nodes.length < 1 || !(nodes[0] instanceof HtmlElementAst) ||
(<HtmlElementAst>nodes[0]).name != _BUNDLE_ELEMENT;
}
function _createMessages(nodes: HtmlAst[], messages: {[key: string]: HtmlAst[]},
errors: ParseError[]): void {
nodes.forEach((item) => {
if (item instanceof HtmlElementAst) {
let msg = <HtmlElementAst>item;
if (msg.name != _MSG_ELEMENT) {
errors.push(
new XmbDeserializationError(item.sourceSpan, `Unexpected element "${msg.name}"`));
return;
}
let id = _id(msg);
if (isBlank(id)) {
errors.push(
new XmbDeserializationError(item.sourceSpan, `"${_ID_ATTR}" attribute is missing`));
return;
}
messages[id] = msg.children;
}
});
}
function _id(el: HtmlElementAst): string {
let ids = el.attrs.filter(a => a.name == _ID_ATTR);
return ids.length > 0 ? ids[0].value : null;
}
function _serializeMessage(m: Message): string {
let desc = isPresent(m.description) ? ` desc='${m.description}'` : "";
return `<msg id='${id(m)}'${desc}>${m.content}</msg>`;
}
function _expandPlaceholder(input: string): string {
return RegExpWrapper.replaceAll(_PLACEHOLDER_REGEXP, input, (match) => {
let nameWithQuotes = match[2];
return `<ph name=${nameWithQuotes}></ph>`;
});
}

View File

@ -0,0 +1,218 @@
import {CompileIdentifierMetadata, CompileTokenMetadata} from './compile_metadata';
import {AppView, DebugAppView} from 'angular2/src/core/linker/view';
import {StaticNodeDebugInfo, DebugContext} from 'angular2/src/core/linker/debug_context';
import {
ViewUtils,
flattenNestedViewRenderNodes,
interpolate,
checkBinding,
castByValue,
EMPTY_ARRAY,
EMPTY_MAP,
pureProxy1,
pureProxy2,
pureProxy3,
pureProxy4,
pureProxy5,
pureProxy6,
pureProxy7,
pureProxy8,
pureProxy9,
pureProxy10
} from 'angular2/src/core/linker/view_utils';
import {
uninitialized,
devModeEqual,
SimpleChange,
ValueUnwrapper,
ChangeDetectorRef,
ChangeDetectorState,
ChangeDetectionStrategy
} from 'angular2/src/core/change_detection/change_detection';
import {AppElement} from 'angular2/src/core/linker/element';
import {ElementRef} from 'angular2/src/core/linker/element_ref';
import {ViewContainerRef} from 'angular2/src/core/linker/view_container_ref';
import {Renderer, RenderComponentType, RenderDebugInfo} from 'angular2/src/core/render/api';
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';
import {ViewType} from 'angular2/src/core/linker/view_type';
import {QueryList} from 'angular2/src/core/linker';
import {Injector} from 'angular2/src/core/di/injector';
import {TemplateRef, TemplateRef_} from 'angular2/src/core/linker/template_ref';
import {MODULE_SUFFIX} from './util';
var APP_VIEW_MODULE_URL = 'asset:angular2/lib/src/core/linker/view' + MODULE_SUFFIX;
var VIEW_UTILS_MODULE_URL = 'asset:angular2/lib/src/core/linker/view_utils' + MODULE_SUFFIX;
var CD_MODULE_URL = 'asset:angular2/lib/src/core/change_detection/change_detection' + MODULE_SUFFIX;
// Reassign the imports to different variables so we can
// define static variables with the name of the import.
// (only needed for Dart).
var impViewUtils = ViewUtils;
var impAppView = AppView;
var impDebugAppView = DebugAppView;
var impDebugContext = DebugContext;
var impAppElement = AppElement;
var impElementRef = ElementRef;
var impViewContainerRef = ViewContainerRef;
var impChangeDetectorRef = ChangeDetectorRef;
var impRenderComponentType = RenderComponentType;
var impQueryList = QueryList;
var impTemplateRef = TemplateRef;
var impTemplateRef_ = TemplateRef_;
var impValueUnwrapper = ValueUnwrapper;
var impInjector = Injector;
var impViewEncapsulation = ViewEncapsulation;
var impViewType = ViewType;
var impChangeDetectionStrategy = ChangeDetectionStrategy;
var impStaticNodeDebugInfo = StaticNodeDebugInfo;
var impRenderer = Renderer;
var impSimpleChange = SimpleChange;
var impUninitialized = uninitialized;
var impChangeDetectorState = ChangeDetectorState;
var impFlattenNestedViewRenderNodes = flattenNestedViewRenderNodes;
var impDevModeEqual = devModeEqual;
var impInterpolate = interpolate;
var impCheckBinding = checkBinding;
var impCastByValue = castByValue;
var impEMPTY_ARRAY = EMPTY_ARRAY;
var impEMPTY_MAP = EMPTY_MAP;
export class Identifiers {
static ViewUtils = new CompileIdentifierMetadata({
name: 'ViewUtils',
moduleUrl: 'asset:angular2/lib/src/core/linker/view_utils' + MODULE_SUFFIX,
runtime: impViewUtils
});
static AppView = new CompileIdentifierMetadata(
{name: 'AppView', moduleUrl: APP_VIEW_MODULE_URL, runtime: impAppView});
static DebugAppView = new CompileIdentifierMetadata(
{name: 'DebugAppView', moduleUrl: APP_VIEW_MODULE_URL, runtime: impDebugAppView});
static AppElement = new CompileIdentifierMetadata({
name: 'AppElement',
moduleUrl: 'asset:angular2/lib/src/core/linker/element' + MODULE_SUFFIX,
runtime: impAppElement
});
static ElementRef = new CompileIdentifierMetadata({
name: 'ElementRef',
moduleUrl: 'asset:angular2/lib/src/core/linker/element_ref' + MODULE_SUFFIX,
runtime: impElementRef
});
static ViewContainerRef = new CompileIdentifierMetadata({
name: 'ViewContainerRef',
moduleUrl: 'asset:angular2/lib/src/core/linker/view_container_ref' + MODULE_SUFFIX,
runtime: impViewContainerRef
});
static ChangeDetectorRef = new CompileIdentifierMetadata({
name: 'ChangeDetectorRef',
moduleUrl: 'asset:angular2/lib/src/core/change_detection/change_detector_ref' + MODULE_SUFFIX,
runtime: impChangeDetectorRef
});
static RenderComponentType = new CompileIdentifierMetadata({
name: 'RenderComponentType',
moduleUrl: 'asset:angular2/lib/src/core/render/api' + MODULE_SUFFIX,
runtime: impRenderComponentType
});
static QueryList = new CompileIdentifierMetadata({
name: 'QueryList',
moduleUrl: 'asset:angular2/lib/src/core/linker/query_list' + MODULE_SUFFIX,
runtime: impQueryList
});
static TemplateRef = new CompileIdentifierMetadata({
name: 'TemplateRef',
moduleUrl: 'asset:angular2/lib/src/core/linker/template_ref' + MODULE_SUFFIX,
runtime: impTemplateRef
});
static TemplateRef_ = new CompileIdentifierMetadata({
name: 'TemplateRef_',
moduleUrl: 'asset:angular2/lib/src/core/linker/template_ref' + MODULE_SUFFIX,
runtime: impTemplateRef_
});
static ValueUnwrapper = new CompileIdentifierMetadata(
{name: 'ValueUnwrapper', moduleUrl: CD_MODULE_URL, runtime: impValueUnwrapper});
static Injector = new CompileIdentifierMetadata({
name: 'Injector',
moduleUrl: `asset:angular2/lib/src/core/di/injector${MODULE_SUFFIX}`,
runtime: impInjector
});
static ViewEncapsulation = new CompileIdentifierMetadata({
name: 'ViewEncapsulation',
moduleUrl: 'asset:angular2/lib/src/core/metadata/view' + MODULE_SUFFIX,
runtime: impViewEncapsulation
});
static ViewType = new CompileIdentifierMetadata({
name: 'ViewType',
moduleUrl: `asset:angular2/lib/src/core/linker/view_type${MODULE_SUFFIX}`,
runtime: impViewType
});
static ChangeDetectionStrategy = new CompileIdentifierMetadata({
name: 'ChangeDetectionStrategy',
moduleUrl: CD_MODULE_URL,
runtime: impChangeDetectionStrategy
});
static StaticNodeDebugInfo = new CompileIdentifierMetadata({
name: 'StaticNodeDebugInfo',
moduleUrl: `asset:angular2/lib/src/core/linker/debug_context${MODULE_SUFFIX}`,
runtime: impStaticNodeDebugInfo
});
static DebugContext = new CompileIdentifierMetadata({
name: 'DebugContext',
moduleUrl: `asset:angular2/lib/src/core/linker/debug_context${MODULE_SUFFIX}`,
runtime: impDebugContext
});
static Renderer = new CompileIdentifierMetadata({
name: 'Renderer',
moduleUrl: 'asset:angular2/lib/src/core/render/api' + MODULE_SUFFIX,
runtime: impRenderer
});
static SimpleChange = new CompileIdentifierMetadata(
{name: 'SimpleChange', moduleUrl: CD_MODULE_URL, runtime: impSimpleChange});
static uninitialized = new CompileIdentifierMetadata(
{name: 'uninitialized', moduleUrl: CD_MODULE_URL, runtime: impUninitialized});
static ChangeDetectorState = new CompileIdentifierMetadata(
{name: 'ChangeDetectorState', moduleUrl: CD_MODULE_URL, runtime: impChangeDetectorState});
static checkBinding = new CompileIdentifierMetadata(
{name: 'checkBinding', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: impCheckBinding});
static flattenNestedViewRenderNodes = new CompileIdentifierMetadata({
name: 'flattenNestedViewRenderNodes',
moduleUrl: VIEW_UTILS_MODULE_URL,
runtime: impFlattenNestedViewRenderNodes
});
static devModeEqual = new CompileIdentifierMetadata(
{name: 'devModeEqual', moduleUrl: CD_MODULE_URL, runtime: impDevModeEqual});
static interpolate = new CompileIdentifierMetadata(
{name: 'interpolate', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: impInterpolate});
static castByValue = new CompileIdentifierMetadata(
{name: 'castByValue', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: impCastByValue});
static EMPTY_ARRAY = new CompileIdentifierMetadata(
{name: 'EMPTY_ARRAY', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: impEMPTY_ARRAY});
static EMPTY_MAP = new CompileIdentifierMetadata(
{name: 'EMPTY_MAP', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: impEMPTY_MAP});
static pureProxies = [
null,
new CompileIdentifierMetadata(
{name: 'pureProxy1', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy1}),
new CompileIdentifierMetadata(
{name: 'pureProxy2', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy2}),
new CompileIdentifierMetadata(
{name: 'pureProxy3', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy3}),
new CompileIdentifierMetadata(
{name: 'pureProxy4', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy4}),
new CompileIdentifierMetadata(
{name: 'pureProxy5', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy5}),
new CompileIdentifierMetadata(
{name: 'pureProxy6', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy6}),
new CompileIdentifierMetadata(
{name: 'pureProxy7', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy7}),
new CompileIdentifierMetadata(
{name: 'pureProxy8', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy8}),
new CompileIdentifierMetadata(
{name: 'pureProxy9', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy9}),
new CompileIdentifierMetadata(
{name: 'pureProxy10', moduleUrl: VIEW_UTILS_MODULE_URL, runtime: pureProxy10}),
];
}
export function identifierToken(identifier: CompileIdentifierMetadata): CompileTokenMetadata {
return new CompileTokenMetadata({identifier: identifier});
}

View File

@ -0,0 +1,229 @@
import {Injectable, Provider, provide} from 'angular2/src/core/di';
import {StringWrapper, RegExpWrapper, isBlank, isPresent} from 'angular2/src/facade/lang';
import {
HtmlAstVisitor,
HtmlAttrAst,
HtmlElementAst,
HtmlTextAst,
HtmlCommentAst,
HtmlExpansionAst,
HtmlExpansionCaseAst,
HtmlAst
} from './html_ast';
import {HtmlParser, HtmlParseTreeResult} from './html_parser';
import {dashCaseToCamelCase, camelCaseToDashCase} from './util';
var LONG_SYNTAX_REGEXP = /^(?:on-(.*)|bindon-(.*)|bind-(.*)|var-(.*))$/ig;
var SHORT_SYNTAX_REGEXP = /^(?:\((.*)\)|\[\((.*)\)\]|\[(.*)\]|#(.*))$/ig;
var VARIABLE_TPL_BINDING_REGEXP = /(\bvar\s+|#)(\S+)/ig;
var TEMPLATE_SELECTOR_REGEXP = /^(\S+)/g;
var SPECIAL_PREFIXES_REGEXP = /^(class|style|attr)\./ig;
var INTERPOLATION_REGEXP = /\{\{.*?\}\}/g;
const SPECIAL_CASES = /*@ts2dart_const*/[
'ng-non-bindable',
'ng-default-control',
'ng-no-form',
];
/**
* Convert templates to the case sensitive syntax
*
* @internal
*/
export class LegacyHtmlAstTransformer implements HtmlAstVisitor {
rewrittenAst: HtmlAst[] = [];
visitingTemplateEl: boolean = false;
constructor(private dashCaseSelectors?: string[]) {}
visitComment(ast: HtmlCommentAst, context: any): any { return ast; }
visitElement(ast: HtmlElementAst, context: any): HtmlElementAst {
this.visitingTemplateEl = ast.name.toLowerCase() == 'template';
let attrs = ast.attrs.map(attr => attr.visit(this, null));
let children = ast.children.map(child => child.visit(this, null));
return new HtmlElementAst(ast.name, attrs, children, ast.sourceSpan, ast.startSourceSpan,
ast.endSourceSpan);
}
visitAttr(originalAst: HtmlAttrAst, context: any): HtmlAttrAst {
let ast = originalAst;
if (this.visitingTemplateEl) {
if (isPresent(RegExpWrapper.firstMatch(LONG_SYNTAX_REGEXP, ast.name))) {
// preserve the "-" in the prefix for the long syntax
ast = this._rewriteLongSyntax(ast);
} else {
// rewrite any other attribute
let name = dashCaseToCamelCase(ast.name);
ast = name == ast.name ? ast : new HtmlAttrAst(name, ast.value, ast.sourceSpan);
}
} else {
ast = this._rewriteTemplateAttribute(ast);
ast = this._rewriteLongSyntax(ast);
ast = this._rewriteShortSyntax(ast);
ast = this._rewriteStar(ast);
ast = this._rewriteInterpolation(ast);
ast = this._rewriteSpecialCases(ast);
}
if (ast !== originalAst) {
this.rewrittenAst.push(ast);
}
return ast;
}
visitText(ast: HtmlTextAst, context: any): HtmlTextAst { return ast; }
visitExpansion(ast: HtmlExpansionAst, context: any): any {
let cases = ast.cases.map(c => c.visit(this, null));
return new HtmlExpansionAst(ast.switchValue, ast.type, cases, ast.sourceSpan,
ast.switchValueSourceSpan);
}
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
private _rewriteLongSyntax(ast: HtmlAttrAst): HtmlAttrAst {
let m = RegExpWrapper.firstMatch(LONG_SYNTAX_REGEXP, ast.name);
let attrName = ast.name;
let attrValue = ast.value;
if (isPresent(m)) {
if (isPresent(m[1])) {
attrName = `on-${dashCaseToCamelCase(m[1])}`;
} else if (isPresent(m[2])) {
attrName = `bindon-${dashCaseToCamelCase(m[2])}`;
} else if (isPresent(m[3])) {
attrName = `bind-${dashCaseToCamelCase(m[3])}`;
} else if (isPresent(m[4])) {
attrName = `var-${dashCaseToCamelCase(m[4])}`;
attrValue = dashCaseToCamelCase(attrValue);
}
}
return attrName == ast.name && attrValue == ast.value ?
ast :
new HtmlAttrAst(attrName, attrValue, ast.sourceSpan);
}
private _rewriteTemplateAttribute(ast: HtmlAttrAst): HtmlAttrAst {
let name = ast.name;
let value = ast.value;
if (name.toLowerCase() == 'template') {
name = 'template';
// rewrite the directive selector
value = StringWrapper.replaceAllMapped(value, TEMPLATE_SELECTOR_REGEXP,
(m) => { return dashCaseToCamelCase(m[1]); });
// rewrite the var declarations
value = StringWrapper.replaceAllMapped(value, VARIABLE_TPL_BINDING_REGEXP, m => {
return `${m[1].toLowerCase()}${dashCaseToCamelCase(m[2])}`;
});
}
if (name == ast.name && value == ast.value) {
return ast;
}
return new HtmlAttrAst(name, value, ast.sourceSpan);
}
private _rewriteShortSyntax(ast: HtmlAttrAst): HtmlAttrAst {
let m = RegExpWrapper.firstMatch(SHORT_SYNTAX_REGEXP, ast.name);
let attrName = ast.name;
let attrValue = ast.value;
if (isPresent(m)) {
if (isPresent(m[1])) {
attrName = `(${dashCaseToCamelCase(m[1])})`;
} else if (isPresent(m[2])) {
attrName = `[(${dashCaseToCamelCase(m[2])})]`;
} else if (isPresent(m[3])) {
let prop = StringWrapper.replaceAllMapped(m[3], SPECIAL_PREFIXES_REGEXP,
(m) => { return m[1].toLowerCase() + '.'; });
if (prop.startsWith('class.') || prop.startsWith('attr.') || prop.startsWith('style.')) {
attrName = `[${prop}]`;
} else {
attrName = `[${dashCaseToCamelCase(prop)}]`;
}
} else if (isPresent(m[4])) {
attrName = `#${dashCaseToCamelCase(m[4])}`;
attrValue = dashCaseToCamelCase(attrValue);
}
}
return attrName == ast.name && attrValue == ast.value ?
ast :
new HtmlAttrAst(attrName, attrValue, ast.sourceSpan);
}
private _rewriteStar(ast: HtmlAttrAst): HtmlAttrAst {
let attrName = ast.name;
let attrValue = ast.value;
if (attrName[0] == '*') {
attrName = dashCaseToCamelCase(attrName);
// rewrite the var declarations
attrValue = StringWrapper.replaceAllMapped(attrValue, VARIABLE_TPL_BINDING_REGEXP, m => {
return `${m[1].toLowerCase()}${dashCaseToCamelCase(m[2])}`;
});
}
return attrName == ast.name && attrValue == ast.value ?
ast :
new HtmlAttrAst(attrName, attrValue, ast.sourceSpan);
}
private _rewriteInterpolation(ast: HtmlAttrAst): HtmlAttrAst {
let hasInterpolation = RegExpWrapper.test(INTERPOLATION_REGEXP, ast.value);
if (!hasInterpolation) {
return ast;
}
let name = ast.name;
if (!(name.startsWith('attr.') || name.startsWith('class.') || name.startsWith('style.'))) {
name = dashCaseToCamelCase(ast.name);
}
return name == ast.name ? ast : new HtmlAttrAst(name, ast.value, ast.sourceSpan);
}
private _rewriteSpecialCases(ast: HtmlAttrAst): HtmlAttrAst {
let attrName = ast.name;
if (SPECIAL_CASES.indexOf(attrName) > -1) {
return new HtmlAttrAst(dashCaseToCamelCase(attrName), ast.value, ast.sourceSpan);
}
if (isPresent(this.dashCaseSelectors) && this.dashCaseSelectors.indexOf(attrName) > -1) {
return new HtmlAttrAst(dashCaseToCamelCase(attrName), ast.value, ast.sourceSpan);
}
return ast;
}
}
@Injectable()
export class LegacyHtmlParser extends HtmlParser {
parse(sourceContent: string, sourceUrl: string,
parseExpansionForms: boolean = false): HtmlParseTreeResult {
let transformer = new LegacyHtmlAstTransformer();
let htmlParseTreeResult = super.parse(sourceContent, sourceUrl, parseExpansionForms);
let rootNodes = htmlParseTreeResult.rootNodes.map(node => node.visit(transformer, null));
return transformer.rewrittenAst.length > 0 ?
new HtmlParseTreeResult(rootNodes, htmlParseTreeResult.errors) :
htmlParseTreeResult;
}
}

View File

@ -0,0 +1,433 @@
import {resolveForwardRef} from 'angular2/src/core/di';
import {
Type,
isBlank,
isPresent,
isArray,
stringify,
isString,
isStringMap,
RegExpWrapper,
StringWrapper
} from 'angular2/src/facade/lang';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {BaseException} from 'angular2/src/facade/exceptions';
import * as cpl from './compile_metadata';
import * as md from 'angular2/src/core/metadata/directives';
import * as dimd from 'angular2/src/core/metadata/di';
import {DirectiveResolver} from './directive_resolver';
import {PipeResolver} from './pipe_resolver';
import {ViewResolver} from './view_resolver';
import {ViewMetadata} from 'angular2/src/core/metadata/view';
import {hasLifecycleHook} from './directive_lifecycle_reflector';
import {LifecycleHooks, LIFECYCLE_HOOKS_VALUES} from 'angular2/src/core/metadata/lifecycle_hooks';
import {reflector} from 'angular2/src/core/reflection/reflection';
import {Injectable, Inject, Optional} from 'angular2/src/core/di';
import {PLATFORM_DIRECTIVES, PLATFORM_PIPES} from 'angular2/src/core/platform_directives_and_pipes';
import {MODULE_SUFFIX, sanitizeIdentifier, ValueTransformer, visitValue} from './util';
import {assertArrayOfStrings} from './assertions';
import {getUrlScheme} from 'angular2/src/compiler/url_resolver';
import {Provider} from 'angular2/src/core/di/provider';
import {
OptionalMetadata,
SelfMetadata,
HostMetadata,
SkipSelfMetadata,
InjectMetadata
} from 'angular2/src/core/di/metadata';
import {AttributeMetadata, QueryMetadata} from 'angular2/src/core/metadata/di';
import {ReflectorReader} from 'angular2/src/core/reflection/reflector_reader';
import {isProviderLiteral, createProvider} from '../core/di/provider_util';
@Injectable()
export class CompileMetadataResolver {
private _directiveCache = new Map<Type, cpl.CompileDirectiveMetadata>();
private _pipeCache = new Map<Type, cpl.CompilePipeMetadata>();
private _anonymousTypes = new Map<Object, number>();
private _anonymousTypeIndex = 0;
private _reflector: ReflectorReader;
constructor(private _directiveResolver: DirectiveResolver, private _pipeResolver: PipeResolver,
private _viewResolver: ViewResolver,
@Optional() @Inject(PLATFORM_DIRECTIVES) private _platformDirectives: Type[],
@Optional() @Inject(PLATFORM_PIPES) private _platformPipes: Type[],
_reflector?: ReflectorReader) {
if (isPresent(_reflector)) {
this._reflector = _reflector;
} else {
this._reflector = reflector;
}
}
private sanitizeTokenName(token: any): string {
let identifier = stringify(token);
if (identifier.indexOf('(') >= 0) {
// case: anonymous functions!
let found = this._anonymousTypes.get(token);
if (isBlank(found)) {
this._anonymousTypes.set(token, this._anonymousTypeIndex++);
found = this._anonymousTypes.get(token);
}
identifier = `anonymous_token_${found}_`;
}
return sanitizeIdentifier(identifier);
}
getDirectiveMetadata(directiveType: Type): cpl.CompileDirectiveMetadata {
var meta = this._directiveCache.get(directiveType);
if (isBlank(meta)) {
var dirMeta = this._directiveResolver.resolve(directiveType);
var templateMeta = null;
var changeDetectionStrategy = null;
var viewProviders = [];
if (dirMeta instanceof md.ComponentMetadata) {
assertArrayOfStrings('styles', dirMeta.styles);
var cmpMeta = <md.ComponentMetadata>dirMeta;
var viewMeta = this._viewResolver.resolve(directiveType);
assertArrayOfStrings('styles', viewMeta.styles);
templateMeta = new cpl.CompileTemplateMetadata({
encapsulation: viewMeta.encapsulation,
template: viewMeta.template,
templateUrl: viewMeta.templateUrl,
styles: viewMeta.styles,
styleUrls: viewMeta.styleUrls,
baseUrl: calcTemplateBaseUrl(this._reflector, directiveType, cmpMeta)
});
changeDetectionStrategy = cmpMeta.changeDetection;
if (isPresent(dirMeta.viewProviders)) {
viewProviders = this.getProvidersMetadata(dirMeta.viewProviders);
}
}
var providers = [];
if (isPresent(dirMeta.providers)) {
providers = this.getProvidersMetadata(dirMeta.providers);
}
var queries = [];
var viewQueries = [];
if (isPresent(dirMeta.queries)) {
queries = this.getQueriesMetadata(dirMeta.queries, false);
viewQueries = this.getQueriesMetadata(dirMeta.queries, true);
}
meta = cpl.CompileDirectiveMetadata.create({
selector: dirMeta.selector,
exportAs: dirMeta.exportAs,
isComponent: isPresent(templateMeta),
type: this.getTypeMetadata(directiveType, staticTypeModuleUrl(directiveType)),
template: templateMeta,
changeDetection: changeDetectionStrategy,
inputs: dirMeta.inputs,
outputs: dirMeta.outputs,
host: dirMeta.host,
lifecycleHooks:
LIFECYCLE_HOOKS_VALUES.filter(hook => hasLifecycleHook(hook, directiveType)),
providers: providers,
viewProviders: viewProviders,
queries: queries,
viewQueries: viewQueries
});
this._directiveCache.set(directiveType, meta);
}
return meta;
}
/**
* @param someType a symbol which may or may not be a directive type
* @returns {cpl.CompileDirectiveMetadata} if possible, otherwise null.
*/
maybeGetDirectiveMetadata(someType: Type): cpl.CompileDirectiveMetadata {
try {
return this.getDirectiveMetadata(someType);
} catch (e) {
if (e.message.indexOf('No Directive annotation') !== -1) {
return null;
}
throw e;
}
}
getTypeMetadata(type: Type, moduleUrl: string): cpl.CompileTypeMetadata {
return new cpl.CompileTypeMetadata({
name: this.sanitizeTokenName(type),
moduleUrl: moduleUrl,
runtime: type,
diDeps: this.getDependenciesMetadata(type, null)
});
}
getFactoryMetadata(factory: Function, moduleUrl: string): cpl.CompileFactoryMetadata {
return new cpl.CompileFactoryMetadata({
name: this.sanitizeTokenName(factory),
moduleUrl: moduleUrl,
runtime: factory,
diDeps: this.getDependenciesMetadata(factory, null)
});
}
getPipeMetadata(pipeType: Type): cpl.CompilePipeMetadata {
var meta = this._pipeCache.get(pipeType);
if (isBlank(meta)) {
var pipeMeta = this._pipeResolver.resolve(pipeType);
meta = new cpl.CompilePipeMetadata({
type: this.getTypeMetadata(pipeType, staticTypeModuleUrl(pipeType)),
name: pipeMeta.name,
pure: pipeMeta.pure,
lifecycleHooks: LIFECYCLE_HOOKS_VALUES.filter(hook => hasLifecycleHook(hook, pipeType)),
});
this._pipeCache.set(pipeType, meta);
}
return meta;
}
getViewDirectivesMetadata(component: Type): cpl.CompileDirectiveMetadata[] {
var view = this._viewResolver.resolve(component);
var directives = flattenDirectives(view, this._platformDirectives);
for (var i = 0; i < directives.length; i++) {
if (!isValidType(directives[i])) {
throw new BaseException(
`Unexpected directive value '${stringify(directives[i])}' on the View of component '${stringify(component)}'`);
}
}
return directives.map(type => this.getDirectiveMetadata(type));
}
getViewPipesMetadata(component: Type): cpl.CompilePipeMetadata[] {
var view = this._viewResolver.resolve(component);
var pipes = flattenPipes(view, this._platformPipes);
for (var i = 0; i < pipes.length; i++) {
if (!isValidType(pipes[i])) {
throw new BaseException(
`Unexpected piped value '${stringify(pipes[i])}' on the View of component '${stringify(component)}'`);
}
}
return pipes.map(type => this.getPipeMetadata(type));
}
getDependenciesMetadata(typeOrFunc: Type | Function,
dependencies: any[]): cpl.CompileDiDependencyMetadata[] {
let params = isPresent(dependencies) ? dependencies : this._reflector.parameters(typeOrFunc);
if (isBlank(params)) {
params = [];
}
return params.map((param) => {
if (isBlank(param)) {
return null;
}
let isAttribute = false;
let isHost = false;
let isSelf = false;
let isSkipSelf = false;
let isOptional = false;
let query: dimd.QueryMetadata = null;
let viewQuery: dimd.ViewQueryMetadata = null;
var token = null;
if (isArray(param)) {
(<any[]>param)
.forEach((paramEntry) => {
if (paramEntry instanceof HostMetadata) {
isHost = true;
} else if (paramEntry instanceof SelfMetadata) {
isSelf = true;
} else if (paramEntry instanceof SkipSelfMetadata) {
isSkipSelf = true;
} else if (paramEntry instanceof OptionalMetadata) {
isOptional = true;
} else if (paramEntry instanceof AttributeMetadata) {
isAttribute = true;
token = paramEntry.attributeName;
} else if (paramEntry instanceof QueryMetadata) {
if (paramEntry.isViewQuery) {
viewQuery = paramEntry;
} else {
query = paramEntry;
}
} else if (paramEntry instanceof InjectMetadata) {
token = paramEntry.token;
} else if (isValidType(paramEntry) && isBlank(token)) {
token = paramEntry;
}
});
} else {
token = param;
}
if (isBlank(token)) {
return null;
}
return new cpl.CompileDiDependencyMetadata({
isAttribute: isAttribute,
isHost: isHost,
isSelf: isSelf,
isSkipSelf: isSkipSelf,
isOptional: isOptional,
query: isPresent(query) ? this.getQueryMetadata(query, null) : null,
viewQuery: isPresent(viewQuery) ? this.getQueryMetadata(viewQuery, null) : null,
token: this.getTokenMetadata(token)
});
});
}
getTokenMetadata(token: any): cpl.CompileTokenMetadata {
token = resolveForwardRef(token);
var compileToken;
if (isString(token)) {
compileToken = new cpl.CompileTokenMetadata({value: token});
} else {
compileToken = new cpl.CompileTokenMetadata({
identifier: new cpl.CompileIdentifierMetadata({
runtime: token,
name: this.sanitizeTokenName(token),
moduleUrl: staticTypeModuleUrl(token)
})
});
}
return compileToken;
}
getProvidersMetadata(providers: any[]):
Array<cpl.CompileProviderMetadata | cpl.CompileTypeMetadata | any[]> {
return providers.map((provider) => {
provider = resolveForwardRef(provider);
if (isArray(provider)) {
return this.getProvidersMetadata(provider);
} else if (provider instanceof Provider) {
return this.getProviderMetadata(provider);
} else if (isProviderLiteral(provider)) {
return this.getProviderMetadata(createProvider(provider));
} else {
return this.getTypeMetadata(provider, staticTypeModuleUrl(provider));
}
});
}
getProviderMetadata(provider: Provider): cpl.CompileProviderMetadata {
var compileDeps;
if (isPresent(provider.useClass)) {
compileDeps = this.getDependenciesMetadata(provider.useClass, provider.dependencies);
} else if (isPresent(provider.useFactory)) {
compileDeps = this.getDependenciesMetadata(provider.useFactory, provider.dependencies);
}
return new cpl.CompileProviderMetadata({
token: this.getTokenMetadata(provider.token),
useClass:
isPresent(provider.useClass) ?
this.getTypeMetadata(provider.useClass, staticTypeModuleUrl(provider.useClass)) :
null,
useValue: convertToCompileValue(provider.useValue),
useFactory: isPresent(provider.useFactory) ?
this.getFactoryMetadata(provider.useFactory,
staticTypeModuleUrl(provider.useFactory)) :
null,
useExisting: isPresent(provider.useExisting) ? this.getTokenMetadata(provider.useExisting) :
null,
deps: compileDeps,
multi: provider.multi
});
}
getQueriesMetadata(queries: {[key: string]: dimd.QueryMetadata},
isViewQuery: boolean): cpl.CompileQueryMetadata[] {
var compileQueries = [];
StringMapWrapper.forEach(queries, (query, propertyName) => {
if (query.isViewQuery === isViewQuery) {
compileQueries.push(this.getQueryMetadata(query, propertyName));
}
});
return compileQueries;
}
getQueryMetadata(q: dimd.QueryMetadata, propertyName: string): cpl.CompileQueryMetadata {
var selectors;
if (q.isVarBindingQuery) {
selectors = q.varBindings.map(varName => this.getTokenMetadata(varName));
} else {
selectors = [this.getTokenMetadata(q.selector)];
}
return new cpl.CompileQueryMetadata({
selectors: selectors,
first: q.first,
descendants: q.descendants,
propertyName: propertyName,
read: isPresent(q.read) ? this.getTokenMetadata(q.read) : null
});
}
}
function flattenDirectives(view: ViewMetadata, platformDirectives: any[]): Type[] {
let directives = [];
if (isPresent(platformDirectives)) {
flattenArray(platformDirectives, directives);
}
if (isPresent(view.directives)) {
flattenArray(view.directives, directives);
}
return directives;
}
function flattenPipes(view: ViewMetadata, platformPipes: any[]): Type[] {
let pipes = [];
if (isPresent(platformPipes)) {
flattenArray(platformPipes, pipes);
}
if (isPresent(view.pipes)) {
flattenArray(view.pipes, pipes);
}
return pipes;
}
function flattenArray(tree: any[], out: Array<Type | any[]>): void {
for (var i = 0; i < tree.length; i++) {
var item = resolveForwardRef(tree[i]);
if (isArray(item)) {
flattenArray(item, out);
} else {
out.push(item);
}
}
}
function isStaticType(value: any): boolean {
return isStringMap(value) && isPresent(value['name']) && isPresent(value['moduleId']);
}
function isValidType(value: any): boolean {
return isStaticType(value) || (value instanceof Type);
}
function staticTypeModuleUrl(value: any): string {
return isStaticType(value) ? value['moduleId'] : null;
}
function calcTemplateBaseUrl(reflector: ReflectorReader, type: any,
cmpMetadata: md.ComponentMetadata): string {
if (isStaticType(type)) {
return type['filePath'];
}
if (isPresent(cmpMetadata.moduleId)) {
var moduleId = cmpMetadata.moduleId;
var scheme = getUrlScheme(moduleId);
return isPresent(scheme) && scheme.length > 0 ? moduleId :
`package:${moduleId}${MODULE_SUFFIX}`;
}
return reflector.importUri(type);
}
// Only fill CompileIdentifierMetadata.runtime if needed...
function convertToCompileValue(value: any): any {
return visitValue(value, new _CompileValueConverter(), null);
}
class _CompileValueConverter extends ValueTransformer {
visitOther(value: any, context: any): any {
if (isStaticType(value)) {
return new cpl.CompileIdentifierMetadata(
{name: value['name'], moduleUrl: staticTypeModuleUrl(value)});
} else {
return new cpl.CompileIdentifierMetadata({runtime: value});
}
}
}

View File

@ -0,0 +1,141 @@
import {
CompileDirectiveMetadata,
CompileIdentifierMetadata,
CompilePipeMetadata,
createHostComponentMeta
} from './compile_metadata';
import {BaseException, unimplemented} from 'angular2/src/facade/exceptions';
import {ListWrapper} from 'angular2/src/facade/collection';
import {StyleCompiler, StylesCompileDependency, StylesCompileResult} from './style_compiler';
import {ViewCompiler, ViewCompileResult} from './view_compiler/view_compiler';
import {TemplateParser} from './template_parser';
import {DirectiveNormalizer} from './directive_normalizer';
import {OutputEmitter} from './output/abstract_emitter';
import * as o from './output/output_ast';
import {ComponentFactory} from 'angular2/src/core/linker/component_factory';
import {
MODULE_SUFFIX,
} from './util';
var _COMPONENT_FACTORY_IDENTIFIER = new CompileIdentifierMetadata({
name: 'ComponentFactory',
runtime: ComponentFactory,
moduleUrl: `asset:angular2/lib/src/core/linker/component_factory${MODULE_SUFFIX}`
});
export class SourceModule {
constructor(public moduleUrl: string, public source: string) {}
}
export class NormalizedComponentWithViewDirectives {
constructor(public component: CompileDirectiveMetadata,
public directives: CompileDirectiveMetadata[], public pipes: CompilePipeMetadata[]) {}
}
export class OfflineCompiler {
constructor(private _directiveNormalizer: DirectiveNormalizer,
private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler,
private _viewCompiler: ViewCompiler, private _outputEmitter: OutputEmitter) {}
normalizeDirectiveMetadata(directive: CompileDirectiveMetadata):
Promise<CompileDirectiveMetadata> {
return this._directiveNormalizer.normalizeDirective(directive);
}
compileTemplates(components: NormalizedComponentWithViewDirectives[]): SourceModule {
if (components.length === 0) {
throw new BaseException('No components given');
}
var statements = [];
var exportedVars = [];
var moduleUrl = _templateModuleUrl(components[0].component);
components.forEach(componentWithDirs => {
var compMeta = <CompileDirectiveMetadata>componentWithDirs.component;
_assertComponent(compMeta);
var compViewFactoryVar = this._compileComponent(compMeta, componentWithDirs.directives,
componentWithDirs.pipes, statements);
exportedVars.push(compViewFactoryVar);
var hostMeta = createHostComponentMeta(compMeta.type, compMeta.selector);
var hostViewFactoryVar = this._compileComponent(hostMeta, [compMeta], [], statements);
var compFactoryVar = `${compMeta.type.name}NgFactory`;
statements.push(
o.variable(compFactoryVar)
.set(o.importExpr(_COMPONENT_FACTORY_IDENTIFIER, [o.importType(compMeta.type)])
.instantiate(
[
o.literal(compMeta.selector),
o.variable(hostViewFactoryVar),
o.importExpr(compMeta.type)
],
o.importType(_COMPONENT_FACTORY_IDENTIFIER,
[o.importType(compMeta.type)], [o.TypeModifier.Const])))
.toDeclStmt(null, [o.StmtModifier.Final]));
exportedVars.push(compFactoryVar);
});
return this._codegenSourceModule(moduleUrl, statements, exportedVars);
}
compileStylesheet(stylesheetUrl: string, cssText: string): SourceModule[] {
var plainStyles = this._styleCompiler.compileStylesheet(stylesheetUrl, cssText, false);
var shimStyles = this._styleCompiler.compileStylesheet(stylesheetUrl, cssText, true);
return [
this._codegenSourceModule(_stylesModuleUrl(stylesheetUrl, false),
_resolveStyleStatements(plainStyles), [plainStyles.stylesVar]),
this._codegenSourceModule(_stylesModuleUrl(stylesheetUrl, true),
_resolveStyleStatements(shimStyles), [shimStyles.stylesVar])
];
}
private _compileComponent(compMeta: CompileDirectiveMetadata,
directives: CompileDirectiveMetadata[], pipes: CompilePipeMetadata[],
targetStatements: o.Statement[]): string {
var styleResult = this._styleCompiler.compileComponent(compMeta);
var parsedTemplate = this._templateParser.parse(compMeta, compMeta.template.template,
directives, pipes, compMeta.type.name);
var viewResult = this._viewCompiler.compileComponent(compMeta, parsedTemplate,
o.variable(styleResult.stylesVar), pipes);
ListWrapper.addAll(targetStatements, _resolveStyleStatements(styleResult));
ListWrapper.addAll(targetStatements, _resolveViewStatements(viewResult));
return viewResult.viewFactoryVar;
}
private _codegenSourceModule(moduleUrl: string, statements: o.Statement[],
exportedVars: string[]): SourceModule {
return new SourceModule(
moduleUrl, this._outputEmitter.emitStatements(moduleUrl, statements, exportedVars));
}
}
function _resolveViewStatements(compileResult: ViewCompileResult): o.Statement[] {
compileResult.dependencies.forEach(
(dep) => { dep.factoryPlaceholder.moduleUrl = _templateModuleUrl(dep.comp); });
return compileResult.statements;
}
function _resolveStyleStatements(compileResult: StylesCompileResult): o.Statement[] {
compileResult.dependencies.forEach((dep) => {
dep.valuePlaceholder.moduleUrl = _stylesModuleUrl(dep.sourceUrl, dep.isShimmed);
});
return compileResult.statements;
}
function _templateModuleUrl(comp: CompileDirectiveMetadata): string {
var moduleUrl = comp.type.moduleUrl;
var urlWithoutSuffix = moduleUrl.substring(0, moduleUrl.length - MODULE_SUFFIX.length);
return `${urlWithoutSuffix}.ngfactory${MODULE_SUFFIX}`;
}
function _stylesModuleUrl(stylesheetUrl: string, shim: boolean): string {
return shim ? `${stylesheetUrl}.shim${MODULE_SUFFIX}` : `${stylesheetUrl}${MODULE_SUFFIX}`;
}
function _assertComponent(meta: CompileDirectiveMetadata) {
if (!meta.isComponent) {
throw new BaseException(`Could not compile '${meta.type.name}' because it is not a component.`);
}
}

View File

@ -0,0 +1,426 @@
import {
isPresent,
isBlank,
isString,
evalExpression,
RegExpWrapper,
StringWrapper
} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
import {BaseException, unimplemented} from 'angular2/src/facade/exceptions';
import * as o from './output_ast';
var _SINGLE_QUOTE_ESCAPE_STRING_RE = /'|\\|\n|\r|\$/g;
export var CATCH_ERROR_VAR = o.variable('error');
export var CATCH_STACK_VAR = o.variable('stack');
export abstract class OutputEmitter {
abstract emitStatements(moduleUrl: string, stmts: o.Statement[], exportedVars: string[]): string;
}
class _EmittedLine {
parts: string[] = [];
constructor(public indent: number) {}
}
export class EmitterVisitorContext {
static createRoot(exportedVars: string[]): EmitterVisitorContext {
return new EmitterVisitorContext(exportedVars, 0);
}
private _lines: _EmittedLine[];
private _classes: o.ClassStmt[] = [];
constructor(private _exportedVars: string[], private _indent: number) {
this._lines = [new _EmittedLine(_indent)];
}
private get _currentLine(): _EmittedLine { return this._lines[this._lines.length - 1]; }
isExportedVar(varName: string): boolean { return this._exportedVars.indexOf(varName) !== -1; }
println(lastPart: string = ''): void { this.print(lastPart, true); }
lineIsEmpty(): boolean { return this._currentLine.parts.length === 0; }
print(part: string, newLine: boolean = false) {
if (part.length > 0) {
this._currentLine.parts.push(part);
}
if (newLine) {
this._lines.push(new _EmittedLine(this._indent));
}
}
removeEmptyLastLine() {
if (this.lineIsEmpty()) {
this._lines.pop();
}
}
incIndent() {
this._indent++;
this._currentLine.indent = this._indent;
}
decIndent() {
this._indent--;
this._currentLine.indent = this._indent;
}
pushClass(clazz: o.ClassStmt) { this._classes.push(clazz); }
popClass(): o.ClassStmt { return this._classes.pop(); }
get currentClass(): o.ClassStmt {
return this._classes.length > 0 ? this._classes[this._classes.length - 1] : null;
}
toSource(): any {
var lines = this._lines;
if (lines[lines.length - 1].parts.length === 0) {
lines = lines.slice(0, lines.length - 1);
}
return lines.map((line) => {
if (line.parts.length > 0) {
return _createIndent(line.indent) + line.parts.join('');
} else {
return '';
}
})
.join('\n');
}
}
export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.ExpressionVisitor {
constructor(private _escapeDollarInStrings: boolean) {}
visitExpressionStmt(stmt: o.ExpressionStatement, ctx: EmitterVisitorContext): any {
stmt.expr.visitExpression(this, ctx);
ctx.println(';');
return null;
}
visitReturnStmt(stmt: o.ReturnStatement, ctx: EmitterVisitorContext): any {
ctx.print(`return `);
stmt.value.visitExpression(this, ctx);
ctx.println(';');
return null;
}
abstract visitCastExpr(ast: o.CastExpr, context: any): any;
abstract visitDeclareClassStmt(stmt: o.ClassStmt, ctx: EmitterVisitorContext): any;
visitIfStmt(stmt: o.IfStmt, ctx: EmitterVisitorContext): any {
ctx.print(`if (`);
stmt.condition.visitExpression(this, ctx);
ctx.print(`) {`);
var hasElseCase = isPresent(stmt.falseCase) && stmt.falseCase.length > 0;
if (stmt.trueCase.length <= 1 && !hasElseCase) {
ctx.print(` `);
this.visitAllStatements(stmt.trueCase, ctx);
ctx.removeEmptyLastLine();
ctx.print(` `);
} else {
ctx.println();
ctx.incIndent();
this.visitAllStatements(stmt.trueCase, ctx);
ctx.decIndent();
if (hasElseCase) {
ctx.println(`} else {`);
ctx.incIndent();
this.visitAllStatements(stmt.falseCase, ctx);
ctx.decIndent();
}
}
ctx.println(`}`);
return null;
}
abstract visitTryCatchStmt(stmt: o.TryCatchStmt, ctx: EmitterVisitorContext): any;
visitThrowStmt(stmt: o.ThrowStmt, ctx: EmitterVisitorContext): any {
ctx.print(`throw `);
stmt.error.visitExpression(this, ctx);
ctx.println(`;`);
return null;
}
visitCommentStmt(stmt: o.CommentStmt, ctx: EmitterVisitorContext): any {
var lines = stmt.comment.split('\n');
lines.forEach((line) => { ctx.println(`// ${line}`); });
return null;
}
abstract visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any;
visitWriteVarExpr(expr: o.WriteVarExpr, ctx: EmitterVisitorContext): any {
var lineWasEmpty = ctx.lineIsEmpty();
if (!lineWasEmpty) {
ctx.print('(');
}
ctx.print(`${expr.name} = `);
expr.value.visitExpression(this, ctx);
if (!lineWasEmpty) {
ctx.print(')');
}
return null;
}
visitWriteKeyExpr(expr: o.WriteKeyExpr, ctx: EmitterVisitorContext): any {
var lineWasEmpty = ctx.lineIsEmpty();
if (!lineWasEmpty) {
ctx.print('(');
}
expr.receiver.visitExpression(this, ctx);
ctx.print(`[`);
expr.index.visitExpression(this, ctx);
ctx.print(`] = `);
expr.value.visitExpression(this, ctx);
if (!lineWasEmpty) {
ctx.print(')');
}
return null;
}
visitWritePropExpr(expr: o.WritePropExpr, ctx: EmitterVisitorContext): any {
var lineWasEmpty = ctx.lineIsEmpty();
if (!lineWasEmpty) {
ctx.print('(');
}
expr.receiver.visitExpression(this, ctx);
ctx.print(`.${expr.name} = `);
expr.value.visitExpression(this, ctx);
if (!lineWasEmpty) {
ctx.print(')');
}
return null;
}
visitInvokeMethodExpr(expr: o.InvokeMethodExpr, ctx: EmitterVisitorContext): any {
expr.receiver.visitExpression(this, ctx);
var name = expr.name;
if (isPresent(expr.builtin)) {
name = this.getBuiltinMethodName(expr.builtin);
if (isBlank(name)) {
// some builtins just mean to skip the call.
// e.g. `bind` in Dart.
return null;
}
}
ctx.print(`.${name}(`);
this.visitAllExpressions(expr.args, ctx, `,`);
ctx.print(`)`);
return null;
}
abstract getBuiltinMethodName(method: o.BuiltinMethod): string;
visitInvokeFunctionExpr(expr: o.InvokeFunctionExpr, ctx: EmitterVisitorContext): any {
expr.fn.visitExpression(this, ctx);
ctx.print(`(`);
this.visitAllExpressions(expr.args, ctx, ',');
ctx.print(`)`);
return null;
}
visitReadVarExpr(ast: o.ReadVarExpr, ctx: EmitterVisitorContext): any {
var varName = ast.name;
if (isPresent(ast.builtin)) {
switch (ast.builtin) {
case o.BuiltinVar.Super:
varName = 'super';
break;
case o.BuiltinVar.This:
varName = 'this';
break;
case o.BuiltinVar.CatchError:
varName = CATCH_ERROR_VAR.name;
break;
case o.BuiltinVar.CatchStack:
varName = CATCH_STACK_VAR.name;
break;
default:
throw new BaseException(`Unknown builtin variable ${ast.builtin}`);
}
}
ctx.print(varName);
return null;
}
visitInstantiateExpr(ast: o.InstantiateExpr, ctx: EmitterVisitorContext): any {
ctx.print(`new `);
ast.classExpr.visitExpression(this, ctx);
ctx.print(`(`);
this.visitAllExpressions(ast.args, ctx, ',');
ctx.print(`)`);
return null;
}
visitLiteralExpr(ast: o.LiteralExpr, ctx: EmitterVisitorContext): any {
var value = ast.value;
if (isString(value)) {
ctx.print(escapeSingleQuoteString(value, this._escapeDollarInStrings));
} else if (isBlank(value)) {
ctx.print('null');
} else {
ctx.print(`${value}`);
}
return null;
}
abstract visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any;
visitConditionalExpr(ast: o.ConditionalExpr, ctx: EmitterVisitorContext): any {
ctx.print(`(`);
ast.condition.visitExpression(this, ctx);
ctx.print('? ');
ast.trueCase.visitExpression(this, ctx);
ctx.print(': ');
ast.falseCase.visitExpression(this, ctx);
ctx.print(`)`);
return null;
}
visitNotExpr(ast: o.NotExpr, ctx: EmitterVisitorContext): any {
ctx.print('!');
ast.condition.visitExpression(this, ctx);
return null;
}
abstract visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any;
abstract visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, context: any): any;
visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, ctx: EmitterVisitorContext): any {
var opStr;
switch (ast.operator) {
case o.BinaryOperator.Equals:
opStr = '==';
break;
case o.BinaryOperator.Identical:
opStr = '===';
break;
case o.BinaryOperator.NotEquals:
opStr = '!=';
break;
case o.BinaryOperator.NotIdentical:
opStr = '!==';
break;
case o.BinaryOperator.And:
opStr = '&&';
break;
case o.BinaryOperator.Or:
opStr = '||';
break;
case o.BinaryOperator.Plus:
opStr = '+';
break;
case o.BinaryOperator.Minus:
opStr = '-';
break;
case o.BinaryOperator.Divide:
opStr = '/';
break;
case o.BinaryOperator.Multiply:
opStr = '*';
break;
case o.BinaryOperator.Modulo:
opStr = '%';
break;
case o.BinaryOperator.Lower:
opStr = '<';
break;
case o.BinaryOperator.LowerEquals:
opStr = '<=';
break;
case o.BinaryOperator.Bigger:
opStr = '>';
break;
case o.BinaryOperator.BiggerEquals:
opStr = '>=';
break;
default:
throw new BaseException(`Unknown operator ${ast.operator}`);
}
ctx.print(`(`);
ast.lhs.visitExpression(this, ctx);
ctx.print(` ${opStr} `);
ast.rhs.visitExpression(this, ctx);
ctx.print(`)`);
return null;
}
visitReadPropExpr(ast: o.ReadPropExpr, ctx: EmitterVisitorContext): any {
ast.receiver.visitExpression(this, ctx);
ctx.print(`.`);
ctx.print(ast.name);
return null;
}
visitReadKeyExpr(ast: o.ReadKeyExpr, ctx: EmitterVisitorContext): any {
ast.receiver.visitExpression(this, ctx);
ctx.print(`[`);
ast.index.visitExpression(this, ctx);
ctx.print(`]`);
return null;
}
visitLiteralArrayExpr(ast: o.LiteralArrayExpr, ctx: EmitterVisitorContext): any {
var useNewLine = ast.entries.length > 1;
ctx.print(`[`, useNewLine);
ctx.incIndent();
this.visitAllExpressions(ast.entries, ctx, ',', useNewLine);
ctx.decIndent();
ctx.print(`]`, useNewLine);
return null;
}
visitLiteralMapExpr(ast: o.LiteralMapExpr, ctx: EmitterVisitorContext): any {
var useNewLine = ast.entries.length > 1;
ctx.print(`{`, useNewLine);
ctx.incIndent();
this.visitAllObjects((entry) => {
ctx.print(`${escapeSingleQuoteString(entry[0], this._escapeDollarInStrings)}: `);
entry[1].visitExpression(this, ctx);
}, ast.entries, ctx, ',', useNewLine);
ctx.decIndent();
ctx.print(`}`, useNewLine);
return null;
}
visitAllExpressions(expressions: o.Expression[], ctx: EmitterVisitorContext, separator: string,
newLine: boolean = false): void {
this.visitAllObjects((expr) => expr.visitExpression(this, ctx), expressions, ctx, separator,
newLine);
}
visitAllObjects(handler: Function, expressions: any, ctx: EmitterVisitorContext,
separator: string, newLine: boolean = false): void {
for (var i = 0; i < expressions.length; i++) {
if (i > 0) {
ctx.print(separator, newLine);
}
handler(expressions[i]);
}
if (newLine) {
ctx.println();
}
}
visitAllStatements(statements: o.Statement[], ctx: EmitterVisitorContext): void {
statements.forEach((stmt) => { return stmt.visitStatement(this, ctx); });
}
}
export function escapeSingleQuoteString(input: string, escapeDollar: boolean): any {
if (isBlank(input)) {
return null;
}
var body = StringWrapper.replaceAllMapped(input, _SINGLE_QUOTE_ESCAPE_STRING_RE, (match) => {
if (match[0] == '$') {
return escapeDollar ? '\\$' : '$';
} else if (match[0] == '\n') {
return '\\n';
} else if (match[0] == '\r') {
return '\\r';
} else {
return `\\${match[0]}`;
}
});
return `'${body}'`;
}
function _createIndent(count: number): string {
var res = '';
for (var i = 0; i < count; i++) {
res += ' ';
}
return res;
}

View File

@ -0,0 +1,164 @@
import {isPresent} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import * as o from './output_ast';
import {
EmitterVisitorContext,
AbstractEmitterVisitor,
CATCH_ERROR_VAR,
CATCH_STACK_VAR
} from './abstract_emitter';
export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
constructor() { super(false); }
visitDeclareClassStmt(stmt: o.ClassStmt, ctx: EmitterVisitorContext): any {
ctx.pushClass(stmt);
this._visitClassConstructor(stmt, ctx);
if (isPresent(stmt.parent)) {
ctx.print(`${stmt.name}.prototype = Object.create(`);
stmt.parent.visitExpression(this, ctx);
ctx.println(`.prototype);`);
}
stmt.getters.forEach((getter) => this._visitClassGetter(stmt, getter, ctx));
stmt.methods.forEach((method) => this._visitClassMethod(stmt, method, ctx));
ctx.popClass();
return null;
}
private _visitClassConstructor(stmt: o.ClassStmt, ctx: EmitterVisitorContext) {
ctx.print(`function ${stmt.name}(`);
if (isPresent(stmt.constructorMethod)) {
this._visitParams(stmt.constructorMethod.params, ctx);
}
ctx.println(`) {`);
ctx.incIndent();
if (isPresent(stmt.constructorMethod)) {
if (stmt.constructorMethod.body.length > 0) {
ctx.println(`var self = this;`);
this.visitAllStatements(stmt.constructorMethod.body, ctx);
}
}
ctx.decIndent();
ctx.println(`}`);
}
private _visitClassGetter(stmt: o.ClassStmt, getter: o.ClassGetter, ctx: EmitterVisitorContext) {
ctx.println(
`Object.defineProperty(${stmt.name}.prototype, '${getter.name}', { get: function() {`);
ctx.incIndent();
if (getter.body.length > 0) {
ctx.println(`var self = this;`);
this.visitAllStatements(getter.body, ctx);
}
ctx.decIndent();
ctx.println(`}});`);
}
private _visitClassMethod(stmt: o.ClassStmt, method: o.ClassMethod, ctx: EmitterVisitorContext) {
ctx.print(`${stmt.name}.prototype.${method.name} = function(`);
this._visitParams(method.params, ctx);
ctx.println(`) {`);
ctx.incIndent();
if (method.body.length > 0) {
ctx.println(`var self = this;`);
this.visitAllStatements(method.body, ctx);
}
ctx.decIndent();
ctx.println(`};`);
}
visitReadVarExpr(ast: o.ReadVarExpr, ctx: EmitterVisitorContext): string {
if (ast.builtin === o.BuiltinVar.This) {
ctx.print('self');
} else if (ast.builtin === o.BuiltinVar.Super) {
throw new BaseException(
`'super' needs to be handled at a parent ast node, not at the variable level!`);
} else {
super.visitReadVarExpr(ast, ctx);
}
return null;
}
visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any {
ctx.print(`var ${stmt.name} = `);
stmt.value.visitExpression(this, ctx);
ctx.println(`;`);
return null;
}
visitCastExpr(ast: o.CastExpr, ctx: EmitterVisitorContext): any {
ast.value.visitExpression(this, ctx);
return null;
}
visitInvokeFunctionExpr(expr: o.InvokeFunctionExpr, ctx: EmitterVisitorContext): string {
var fnExpr = expr.fn;
if (fnExpr instanceof o.ReadVarExpr && fnExpr.builtin === o.BuiltinVar.Super) {
ctx.currentClass.parent.visitExpression(this, ctx);
ctx.print(`.call(this`);
if (expr.args.length > 0) {
ctx.print(`, `);
this.visitAllExpressions(expr.args, ctx, ',');
}
ctx.print(`)`);
} else {
super.visitInvokeFunctionExpr(expr, ctx);
}
return null;
}
visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any {
ctx.print(`function(`);
this._visitParams(ast.params, ctx);
ctx.println(`) {`);
ctx.incIndent();
this.visitAllStatements(ast.statements, ctx);
ctx.decIndent();
ctx.print(`}`);
return null;
}
visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, ctx: EmitterVisitorContext): any {
ctx.print(`function ${stmt.name}(`);
this._visitParams(stmt.params, ctx);
ctx.println(`) {`);
ctx.incIndent();
this.visitAllStatements(stmt.statements, ctx);
ctx.decIndent();
ctx.println(`}`);
return null;
}
visitTryCatchStmt(stmt: o.TryCatchStmt, ctx: EmitterVisitorContext): any {
ctx.println(`try {`);
ctx.incIndent();
this.visitAllStatements(stmt.bodyStmts, ctx);
ctx.decIndent();
ctx.println(`} catch (${CATCH_ERROR_VAR.name}) {`);
ctx.incIndent();
var catchStmts = [
<o.Statement>CATCH_STACK_VAR.set(CATCH_ERROR_VAR.prop('stack'))
.toDeclStmt(null, [o.StmtModifier.Final])
].concat(stmt.catchStmts);
this.visitAllStatements(catchStmts, ctx);
ctx.decIndent();
ctx.println(`}`);
return null;
}
private _visitParams(params: o.FnParam[], ctx: EmitterVisitorContext): void {
this.visitAllObjects((param) => ctx.print(param.name), params, ctx, ',');
}
getBuiltinMethodName(method: o.BuiltinMethod): string {
var name;
switch (method) {
case o.BuiltinMethod.ConcatArray:
name = 'concat';
break;
case o.BuiltinMethod.SubscribeObservable:
name = 'subscribe';
break;
case o.BuiltinMethod.bind:
name = 'bind';
break;
default:
throw new BaseException(`Unknown builtin method: ${method}`);
}
return name;
}
}

View File

@ -0,0 +1,384 @@
import {
StringWrapper,
RegExpWrapper,
isPresent,
isBlank,
Math,
isString,
isArray
} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
import {BaseException} from 'angular2/src/facade/exceptions';
import {CompileIdentifierMetadata} from '../compile_metadata';
import * as o from './output_ast';
import {
OutputEmitter,
EmitterVisitorContext,
AbstractEmitterVisitor,
CATCH_ERROR_VAR,
CATCH_STACK_VAR,
escapeSingleQuoteString
} from './abstract_emitter';
import {getImportModulePath, ImportEnv} from './path_util';
var _debugModuleUrl = 'asset://debug/lib';
export function debugOutputAstAsDart(ast: o.Statement | o.Expression | o.Type | any[]): string {
var converter = new _DartEmitterVisitor(_debugModuleUrl);
var ctx = EmitterVisitorContext.createRoot([]);
var asts: any[];
if (isArray(ast)) {
asts = <any[]>ast;
} else {
asts = [ast];
}
asts.forEach((ast) => {
if (ast instanceof o.Statement) {
ast.visitStatement(converter, ctx);
} else if (ast instanceof o.Expression) {
ast.visitExpression(converter, ctx);
} else if (ast instanceof o.Type) {
ast.visitType(converter, ctx);
} else {
throw new BaseException(`Don't know how to print debug info for ${ast}`);
}
});
return ctx.toSource();
}
export class DartEmitter implements OutputEmitter {
constructor() {}
emitStatements(moduleUrl: string, stmts: o.Statement[], exportedVars: string[]): string {
var srcParts = [];
// Note: We are not creating a library here as Dart does not need it.
// Dart analzyer might complain about it though.
var converter = new _DartEmitterVisitor(moduleUrl);
var ctx = EmitterVisitorContext.createRoot(exportedVars);
converter.visitAllStatements(stmts, ctx);
converter.importsWithPrefixes.forEach((prefix, importedModuleUrl) => {
srcParts.push(
`import '${getImportModulePath(moduleUrl, importedModuleUrl, ImportEnv.Dart)}' as ${prefix};`);
});
srcParts.push(ctx.toSource());
return srcParts.join('\n');
}
}
class _DartEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor {
importsWithPrefixes = new Map<string, string>();
constructor(private _moduleUrl: string) { super(true); }
visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any {
this._visitIdentifier(ast.value, ast.typeParams, ctx);
return null;
}
visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any {
if (stmt.hasModifier(o.StmtModifier.Final)) {
if (isConstType(stmt.type)) {
ctx.print(`const `);
} else {
ctx.print(`final `);
}
} else if (isBlank(stmt.type)) {
ctx.print(`var `);
}
if (isPresent(stmt.type)) {
stmt.type.visitType(this, ctx);
ctx.print(` `);
}
ctx.print(`${stmt.name} = `);
stmt.value.visitExpression(this, ctx);
ctx.println(`;`);
return null;
}
visitCastExpr(ast: o.CastExpr, ctx: EmitterVisitorContext): any {
ctx.print(`(`);
ast.value.visitExpression(this, ctx);
ctx.print(` as `);
ast.type.visitType(this, ctx);
ctx.print(`)`);
return null;
}
visitDeclareClassStmt(stmt: o.ClassStmt, ctx: EmitterVisitorContext): any {
ctx.pushClass(stmt);
ctx.print(`class ${stmt.name}`);
if (isPresent(stmt.parent)) {
ctx.print(` extends `);
stmt.parent.visitExpression(this, ctx);
}
ctx.println(` {`);
ctx.incIndent();
stmt.fields.forEach((field) => this._visitClassField(field, ctx));
if (isPresent(stmt.constructorMethod)) {
this._visitClassConstructor(stmt, ctx);
}
stmt.getters.forEach((getter) => this._visitClassGetter(getter, ctx));
stmt.methods.forEach((method) => this._visitClassMethod(method, ctx));
ctx.decIndent();
ctx.println(`}`);
ctx.popClass();
return null;
}
private _visitClassField(field: o.ClassField, ctx: EmitterVisitorContext) {
if (field.hasModifier(o.StmtModifier.Final)) {
ctx.print(`final `);
} else if (isBlank(field.type)) {
ctx.print(`var `);
}
if (isPresent(field.type)) {
field.type.visitType(this, ctx);
ctx.print(` `);
}
ctx.println(`${field.name};`);
}
private _visitClassGetter(getter: o.ClassGetter, ctx: EmitterVisitorContext) {
if (isPresent(getter.type)) {
getter.type.visitType(this, ctx);
ctx.print(` `);
}
ctx.println(`get ${getter.name} {`);
ctx.incIndent();
this.visitAllStatements(getter.body, ctx);
ctx.decIndent();
ctx.println(`}`);
}
private _visitClassConstructor(stmt: o.ClassStmt, ctx: EmitterVisitorContext) {
ctx.print(`${stmt.name}(`);
this._visitParams(stmt.constructorMethod.params, ctx);
ctx.print(`)`);
var ctorStmts = stmt.constructorMethod.body;
var superCtorExpr = ctorStmts.length > 0 ? getSuperConstructorCallExpr(ctorStmts[0]) : null;
if (isPresent(superCtorExpr)) {
ctx.print(`: `);
superCtorExpr.visitExpression(this, ctx);
ctorStmts = ctorStmts.slice(1);
}
ctx.println(` {`);
ctx.incIndent();
this.visitAllStatements(ctorStmts, ctx);
ctx.decIndent();
ctx.println(`}`);
}
private _visitClassMethod(method: o.ClassMethod, ctx: EmitterVisitorContext) {
if (isPresent(method.type)) {
method.type.visitType(this, ctx);
} else {
ctx.print(`void`);
}
ctx.print(` ${method.name}(`);
this._visitParams(method.params, ctx);
ctx.println(`) {`);
ctx.incIndent();
this.visitAllStatements(method.body, ctx);
ctx.decIndent();
ctx.println(`}`);
}
visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any {
ctx.print(`(`);
this._visitParams(ast.params, ctx);
ctx.println(`) {`);
ctx.incIndent();
this.visitAllStatements(ast.statements, ctx);
ctx.decIndent();
ctx.print(`}`);
return null;
}
visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, ctx: EmitterVisitorContext): any {
if (isPresent(stmt.type)) {
stmt.type.visitType(this, ctx);
} else {
ctx.print(`void`);
}
ctx.print(` ${stmt.name}(`);
this._visitParams(stmt.params, ctx);
ctx.println(`) {`);
ctx.incIndent();
this.visitAllStatements(stmt.statements, ctx);
ctx.decIndent();
ctx.println(`}`);
return null;
}
getBuiltinMethodName(method: o.BuiltinMethod): string {
var name;
switch (method) {
case o.BuiltinMethod.ConcatArray:
name = '.addAll';
break;
case o.BuiltinMethod.SubscribeObservable:
name = 'listen';
break;
case o.BuiltinMethod.bind:
name = null;
break;
default:
throw new BaseException(`Unknown builtin method: ${method}`);
}
return name;
}
visitTryCatchStmt(stmt: o.TryCatchStmt, ctx: EmitterVisitorContext): any {
ctx.println(`try {`);
ctx.incIndent();
this.visitAllStatements(stmt.bodyStmts, ctx);
ctx.decIndent();
ctx.println(`} catch (${CATCH_ERROR_VAR.name}, ${CATCH_STACK_VAR.name}) {`);
ctx.incIndent();
this.visitAllStatements(stmt.catchStmts, ctx);
ctx.decIndent();
ctx.println(`}`);
return null;
}
visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, ctx: EmitterVisitorContext): any {
switch (ast.operator) {
case o.BinaryOperator.Identical:
ctx.print(`identical(`);
ast.lhs.visitExpression(this, ctx);
ctx.print(`, `);
ast.rhs.visitExpression(this, ctx);
ctx.print(`)`);
break;
case o.BinaryOperator.NotIdentical:
ctx.print(`!identical(`);
ast.lhs.visitExpression(this, ctx);
ctx.print(`, `);
ast.rhs.visitExpression(this, ctx);
ctx.print(`)`);
break;
default:
super.visitBinaryOperatorExpr(ast, ctx);
}
return null;
}
visitLiteralArrayExpr(ast: o.LiteralArrayExpr, ctx: EmitterVisitorContext): any {
if (isConstType(ast.type)) {
ctx.print(`const `);
}
return super.visitLiteralArrayExpr(ast, ctx);
}
visitLiteralMapExpr(ast: o.LiteralMapExpr, ctx: EmitterVisitorContext): any {
if (isConstType(ast.type)) {
ctx.print(`const `);
}
if (isPresent(ast.valueType)) {
ctx.print(`<String, `);
ast.valueType.visitType(this, ctx);
ctx.print(`>`);
}
return super.visitLiteralMapExpr(ast, ctx);
}
visitInstantiateExpr(ast: o.InstantiateExpr, ctx: EmitterVisitorContext): any {
ctx.print(isConstType(ast.type) ? `const` : `new`);
ctx.print(' ');
ast.classExpr.visitExpression(this, ctx);
ctx.print(`(`);
this.visitAllExpressions(ast.args, ctx, `,`);
ctx.print(`)`);
return null;
}
visitBuiltintType(type: o.BuiltinType, ctx: EmitterVisitorContext): any {
var typeStr;
switch (type.name) {
case o.BuiltinTypeName.Bool:
typeStr = 'bool';
break;
case o.BuiltinTypeName.Dynamic:
typeStr = 'dynamic';
break;
case o.BuiltinTypeName.Function:
typeStr = 'Function';
break;
case o.BuiltinTypeName.Number:
typeStr = 'num';
break;
case o.BuiltinTypeName.Int:
typeStr = 'int';
break;
case o.BuiltinTypeName.String:
typeStr = 'String';
break;
default:
throw new BaseException(`Unsupported builtin type ${type.name}`);
}
ctx.print(typeStr);
return null;
}
visitExternalType(ast: o.ExternalType, ctx: EmitterVisitorContext): any {
this._visitIdentifier(ast.value, ast.typeParams, ctx);
return null;
}
visitArrayType(type: o.ArrayType, ctx: EmitterVisitorContext): any {
ctx.print(`List<`);
if (isPresent(type.of)) {
type.of.visitType(this, ctx);
} else {
ctx.print(`dynamic`);
}
ctx.print(`>`);
return null;
}
visitMapType(type: o.MapType, ctx: EmitterVisitorContext): any {
ctx.print(`Map<String, `);
if (isPresent(type.valueType)) {
type.valueType.visitType(this, ctx);
} else {
ctx.print(`dynamic`);
}
ctx.print(`>`);
return null;
}
private _visitParams(params: o.FnParam[], ctx: EmitterVisitorContext): void {
this.visitAllObjects((param) => {
if (isPresent(param.type)) {
param.type.visitType(this, ctx);
ctx.print(' ');
}
ctx.print(param.name);
}, params, ctx, ',');
}
private _visitIdentifier(value: CompileIdentifierMetadata, typeParams: o.Type[],
ctx: EmitterVisitorContext): void {
if (isBlank(value.name)) {
throw new BaseException(`Internal error: unknown identifier ${value}`);
}
if (isPresent(value.moduleUrl) && value.moduleUrl != this._moduleUrl) {
var prefix = this.importsWithPrefixes.get(value.moduleUrl);
if (isBlank(prefix)) {
prefix = `import${this.importsWithPrefixes.size}`;
this.importsWithPrefixes.set(value.moduleUrl, prefix);
}
ctx.print(`${prefix}.`);
}
ctx.print(value.name);
if (isPresent(typeParams) && typeParams.length > 0) {
ctx.print(`<`);
this.visitAllObjects((type) => type.visitType(this, ctx), typeParams, ctx, ',');
ctx.print(`>`);
}
}
}
function getSuperConstructorCallExpr(stmt: o.Statement): o.Expression {
if (stmt instanceof o.ExpressionStatement) {
var expr = stmt.expr;
if (expr instanceof o.InvokeFunctionExpr) {
var fn = expr.fn;
if (fn instanceof o.ReadVarExpr) {
if (fn.builtin === o.BuiltinVar.Super) {
return expr;
}
}
}
}
return null;
}
function isConstType(type: o.Type): boolean {
return isPresent(type) && type.hasModifier(o.TypeModifier.Const);
}

View File

@ -0,0 +1,68 @@
import {isPresent} from 'angular2/src/facade/lang';
import {AppView, DebugAppView} from 'angular2/src/core/linker/view';
import {AppElement} from 'angular2/src/core/linker/element';
import {BaseException} from 'angular2/src/facade/exceptions';
import {InstanceFactory, DynamicInstance} from './output_interpreter';
export class InterpretiveAppViewInstanceFactory implements InstanceFactory {
createInstance(superClass: any, clazz: any, args: any[], props: Map<string, any>,
getters: Map<string, Function>, methods: Map<string, Function>): any {
if (superClass === AppView) {
// We are always using DebugAppView as parent.
// However, in prod mode we generate a constructor call that does
// not have the argument for the debugNodeInfos.
args = args.concat([null]);
return new _InterpretiveAppView(args, props, getters, methods);
} else if (superClass === DebugAppView) {
return new _InterpretiveAppView(args, props, getters, methods);
}
throw new BaseException(`Can't instantiate class ${superClass} in interpretative mode`);
}
}
class _InterpretiveAppView extends DebugAppView<any> implements DynamicInstance {
constructor(args: any[], public props: Map<string, any>, public getters: Map<string, Function>,
public methods: Map<string, Function>) {
super(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
}
createInternal(rootSelector: string | any): AppElement {
var m = this.methods.get('createInternal');
if (isPresent(m)) {
return m(rootSelector);
} else {
return super.createInternal(rootSelector);
}
}
injectorGetInternal(token: any, nodeIndex: number, notFoundResult: any): any {
var m = this.methods.get('injectorGetInternal');
if (isPresent(m)) {
return m(token, nodeIndex, notFoundResult);
} else {
return super.injectorGet(token, nodeIndex, notFoundResult);
}
}
destroyInternal(): void {
var m = this.methods.get('destroyInternal');
if (isPresent(m)) {
return m();
} else {
return super.destroyInternal();
}
}
dirtyParentQueriesInternal(): void {
var m = this.methods.get('dirtyParentQueriesInternal');
if (isPresent(m)) {
return m();
} else {
return super.dirtyParentQueriesInternal();
}
}
detectChangesInternal(throwOnChange: boolean): void {
var m = this.methods.get('detectChangesInternal');
if (isPresent(m)) {
return m(throwOnChange);
} else {
return super.detectChangesInternal(throwOnChange);
}
}
}

View File

@ -0,0 +1,77 @@
import * as o from './output_ast';
import {
isPresent,
isBlank,
isString,
evalExpression,
RegExpWrapper,
StringWrapper
} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {OutputEmitter, EmitterVisitorContext} from './abstract_emitter';
import {AbstractJsEmitterVisitor} from './abstract_js_emitter';
import {getImportModulePath, ImportEnv} from './path_util';
export class JavaScriptEmitter implements OutputEmitter {
constructor() {}
emitStatements(moduleUrl: string, stmts: o.Statement[], exportedVars: string[]): string {
var converter = new JsEmitterVisitor(moduleUrl);
var ctx = EmitterVisitorContext.createRoot(exportedVars);
converter.visitAllStatements(stmts, ctx);
var srcParts = [];
converter.importsWithPrefixes.forEach((prefix, importedModuleUrl) => {
// Note: can't write the real word for import as it screws up system.js auto detection...
srcParts.push(`var ${prefix} = req` +
`uire('${getImportModulePath(moduleUrl, importedModuleUrl, ImportEnv.JS)}');`);
});
srcParts.push(ctx.toSource());
return srcParts.join('\n');
}
}
class JsEmitterVisitor extends AbstractJsEmitterVisitor {
importsWithPrefixes = new Map<string, string>();
constructor(private _moduleUrl: string) { super(); }
visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any {
if (isBlank(ast.value.name)) {
throw new BaseException(`Internal error: unknown identifier ${ast.value}`);
}
if (isPresent(ast.value.moduleUrl) && ast.value.moduleUrl != this._moduleUrl) {
var prefix = this.importsWithPrefixes.get(ast.value.moduleUrl);
if (isBlank(prefix)) {
prefix = `import${this.importsWithPrefixes.size}`;
this.importsWithPrefixes.set(ast.value.moduleUrl, prefix);
}
ctx.print(`${prefix}.`);
}
ctx.print(ast.value.name);
return null;
}
visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any {
super.visitDeclareVarStmt(stmt, ctx);
if (ctx.isExportedVar(stmt.name)) {
ctx.println(exportVar(stmt.name));
}
return null;
}
visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, ctx: EmitterVisitorContext): any {
super.visitDeclareFunctionStmt(stmt, ctx);
if (ctx.isExportedVar(stmt.name)) {
ctx.println(exportVar(stmt.name));
}
return null;
}
visitDeclareClassStmt(stmt: o.ClassStmt, ctx: EmitterVisitorContext): any {
super.visitDeclareClassStmt(stmt, ctx);
if (ctx.isExportedVar(stmt.name)) {
ctx.println(exportVar(stmt.name));
}
return null;
}
}
function exportVar(varName: string): string {
return `Object.defineProperty(exports, '${varName}', { get: function() { return ${varName}; }});`;
}

View File

@ -0,0 +1,870 @@
import {isString, isPresent, isBlank} from 'angular2/src/facade/lang';
import {CompileIdentifierMetadata} from '../compile_metadata';
//// Types
export enum TypeModifier {
Const
}
export abstract class Type {
constructor(public modifiers: TypeModifier[] = null) {
if (isBlank(modifiers)) {
this.modifiers = [];
}
}
abstract visitType(visitor: TypeVisitor, context: any): any;
hasModifier(modifier: TypeModifier): boolean { return this.modifiers.indexOf(modifier) !== -1; }
}
export enum BuiltinTypeName {
Dynamic,
Bool,
String,
Int,
Number,
Function
}
export class BuiltinType extends Type {
constructor(public name: BuiltinTypeName, modifiers: TypeModifier[] = null) { super(modifiers); }
visitType(visitor: TypeVisitor, context: any): any {
return visitor.visitBuiltintType(this, context);
}
}
export class ExternalType extends Type {
constructor(public value: CompileIdentifierMetadata, public typeParams: Type[] = null,
modifiers: TypeModifier[] = null) {
super(modifiers);
}
visitType(visitor: TypeVisitor, context: any): any {
return visitor.visitExternalType(this, context);
}
}
export class ArrayType extends Type {
constructor(public of: Type, modifiers: TypeModifier[] = null) { super(modifiers); }
visitType(visitor: TypeVisitor, context: any): any {
return visitor.visitArrayType(this, context);
}
}
export class MapType extends Type {
constructor(public valueType: Type, modifiers: TypeModifier[] = null) { super(modifiers); }
visitType(visitor: TypeVisitor, context: any): any { return visitor.visitMapType(this, context); }
}
export var DYNAMIC_TYPE = new BuiltinType(BuiltinTypeName.Dynamic);
export var BOOL_TYPE = new BuiltinType(BuiltinTypeName.Bool);
export var INT_TYPE = new BuiltinType(BuiltinTypeName.Int);
export var NUMBER_TYPE = new BuiltinType(BuiltinTypeName.Number);
export var STRING_TYPE = new BuiltinType(BuiltinTypeName.String);
export var FUNCTION_TYPE = new BuiltinType(BuiltinTypeName.Function);
export interface TypeVisitor {
visitBuiltintType(type: BuiltinType, context: any): any;
visitExternalType(type: ExternalType, context: any): any;
visitArrayType(type: ArrayType, context: any): any;
visitMapType(type: MapType, context: any): any;
}
///// Expressions
export enum BinaryOperator {
Equals,
NotEquals,
Identical,
NotIdentical,
Minus,
Plus,
Divide,
Multiply,
Modulo,
And,
Or,
Lower,
LowerEquals,
Bigger,
BiggerEquals
}
export abstract class Expression {
constructor(public type: Type) {}
abstract visitExpression(visitor: ExpressionVisitor, context: any): any;
prop(name: string): ReadPropExpr { return new ReadPropExpr(this, name); }
key(index: Expression, type: Type = null): ReadKeyExpr {
return new ReadKeyExpr(this, index, type);
}
callMethod(name: string | BuiltinMethod, params: Expression[]): InvokeMethodExpr {
return new InvokeMethodExpr(this, name, params);
}
callFn(params: Expression[]): InvokeFunctionExpr { return new InvokeFunctionExpr(this, params); }
instantiate(params: Expression[], type: Type = null): InstantiateExpr {
return new InstantiateExpr(this, params, type);
}
conditional(trueCase: Expression, falseCase: Expression = null): ConditionalExpr {
return new ConditionalExpr(this, trueCase, falseCase);
}
equals(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.Equals, this, rhs);
}
notEquals(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.NotEquals, this, rhs);
}
identical(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.Identical, this, rhs);
}
notIdentical(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.NotIdentical, this, rhs);
}
minus(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.Minus, this, rhs);
}
plus(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.Plus, this, rhs);
}
divide(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.Divide, this, rhs);
}
multiply(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.Multiply, this, rhs);
}
modulo(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.Modulo, this, rhs);
}
and(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.And, this, rhs);
}
or(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.Or, this, rhs);
}
lower(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.Lower, this, rhs);
}
lowerEquals(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.LowerEquals, this, rhs);
}
bigger(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.Bigger, this, rhs);
}
biggerEquals(rhs: Expression): BinaryOperatorExpr {
return new BinaryOperatorExpr(BinaryOperator.BiggerEquals, this, rhs);
}
isBlank(): Expression {
// Note: We use equals by purpose here to compare to null and undefined in JS.
return this.equals(NULL_EXPR);
}
cast(type: Type): Expression { return new CastExpr(this, type); }
toStmt(): Statement { return new ExpressionStatement(this); }
}
export enum BuiltinVar {
This,
Super,
CatchError,
CatchStack
}
export class ReadVarExpr extends Expression {
public name;
public builtin: BuiltinVar;
constructor(name: string | BuiltinVar, type: Type = null) {
super(type);
if (isString(name)) {
this.name = <string>name;
this.builtin = null;
} else {
this.name = null;
this.builtin = <BuiltinVar>name;
}
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitReadVarExpr(this, context);
}
set(value: Expression): WriteVarExpr { return new WriteVarExpr(this.name, value); }
}
export class WriteVarExpr extends Expression {
public value: Expression;
constructor(public name: string, value: Expression, type: Type = null) {
super(isPresent(type) ? type : value.type);
this.value = value;
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitWriteVarExpr(this, context);
}
toDeclStmt(type: Type = null, modifiers: StmtModifier[] = null): DeclareVarStmt {
return new DeclareVarStmt(this.name, this.value, type, modifiers);
}
}
export class WriteKeyExpr extends Expression {
public value: Expression;
constructor(public receiver: Expression, public index: Expression, value: Expression,
type: Type = null) {
super(isPresent(type) ? type : value.type);
this.value = value;
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitWriteKeyExpr(this, context);
}
}
export class WritePropExpr extends Expression {
public value: Expression;
constructor(public receiver: Expression, public name: string, value: Expression,
type: Type = null) {
super(isPresent(type) ? type : value.type);
this.value = value;
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitWritePropExpr(this, context);
}
}
export enum BuiltinMethod {
ConcatArray,
SubscribeObservable,
bind
}
export class InvokeMethodExpr extends Expression {
public name: string;
public builtin: BuiltinMethod;
constructor(public receiver: Expression, method: string | BuiltinMethod,
public args: Expression[], type: Type = null) {
super(type);
if (isString(method)) {
this.name = <string>method;
this.builtin = null;
} else {
this.name = null;
this.builtin = <BuiltinMethod>method;
}
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitInvokeMethodExpr(this, context);
}
}
export class InvokeFunctionExpr extends Expression {
constructor(public fn: Expression, public args: Expression[], type: Type = null) { super(type); }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitInvokeFunctionExpr(this, context);
}
}
export class InstantiateExpr extends Expression {
constructor(public classExpr: Expression, public args: Expression[], type?: Type) { super(type); }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitInstantiateExpr(this, context);
}
}
export class LiteralExpr extends Expression {
constructor(public value: any, type: Type = null) { super(type); }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitLiteralExpr(this, context);
}
}
export class ExternalExpr extends Expression {
constructor(public value: CompileIdentifierMetadata, type: Type = null,
public typeParams: Type[] = null) {
super(type);
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitExternalExpr(this, context);
}
}
export class ConditionalExpr extends Expression {
public trueCase: Expression;
constructor(public condition: Expression, trueCase: Expression,
public falseCase: Expression = null, type: Type = null) {
super(isPresent(type) ? type : trueCase.type);
this.trueCase = trueCase;
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitConditionalExpr(this, context);
}
}
export class NotExpr extends Expression {
constructor(public condition: Expression) { super(BOOL_TYPE); }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitNotExpr(this, context);
}
}
export class CastExpr extends Expression {
constructor(public value: Expression, type: Type) { super(type); }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitCastExpr(this, context);
}
}
export class FnParam {
constructor(public name: string, public type: Type = null) {}
}
export class FunctionExpr extends Expression {
constructor(public params: FnParam[], public statements: Statement[], type: Type = null) {
super(type);
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitFunctionExpr(this, context);
}
toDeclStmt(name: string, modifiers: StmtModifier[] = null): DeclareFunctionStmt {
return new DeclareFunctionStmt(name, this.params, this.statements, this.type, modifiers);
}
}
export class BinaryOperatorExpr extends Expression {
public lhs: Expression;
constructor(public operator: BinaryOperator, lhs: Expression, public rhs: Expression,
type: Type = null) {
super(isPresent(type) ? type : lhs.type);
this.lhs = lhs;
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitBinaryOperatorExpr(this, context);
}
}
export class ReadPropExpr extends Expression {
constructor(public receiver: Expression, public name: string, type: Type = null) { super(type); }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitReadPropExpr(this, context);
}
set(value: Expression): WritePropExpr {
return new WritePropExpr(this.receiver, this.name, value);
}
}
export class ReadKeyExpr extends Expression {
constructor(public receiver: Expression, public index: Expression, type: Type = null) {
super(type);
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitReadKeyExpr(this, context);
}
set(value: Expression): WriteKeyExpr {
return new WriteKeyExpr(this.receiver, this.index, value);
}
}
export class LiteralArrayExpr extends Expression {
public entries: Expression[];
constructor(entries: Expression[], type: Type = null) {
super(type);
this.entries = entries;
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitLiteralArrayExpr(this, context);
}
}
export class LiteralMapExpr extends Expression {
public valueType: Type = null;
;
constructor(public entries: Array<Array<string | Expression>>, type: MapType = null) {
super(type);
if (isPresent(type)) {
this.valueType = type.valueType;
}
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitLiteralMapExpr(this, context);
}
}
export interface ExpressionVisitor {
visitReadVarExpr(ast: ReadVarExpr, context: any): any;
visitWriteVarExpr(expr: WriteVarExpr, context: any): any;
visitWriteKeyExpr(expr: WriteKeyExpr, context: any): any;
visitWritePropExpr(expr: WritePropExpr, context: any): any;
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): any;
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any;
visitInstantiateExpr(ast: InstantiateExpr, context: any): any;
visitLiteralExpr(ast: LiteralExpr, context: any): any;
visitExternalExpr(ast: ExternalExpr, context: any): any;
visitConditionalExpr(ast: ConditionalExpr, context: any): any;
visitNotExpr(ast: NotExpr, context: any): any;
visitCastExpr(ast: CastExpr, context: any): any;
visitFunctionExpr(ast: FunctionExpr, context: any): any;
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any;
visitReadPropExpr(ast: ReadPropExpr, context: any): any;
visitReadKeyExpr(ast: ReadKeyExpr, context: any): any;
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): any;
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any;
}
export var THIS_EXPR = new ReadVarExpr(BuiltinVar.This);
export var SUPER_EXPR = new ReadVarExpr(BuiltinVar.Super);
export var CATCH_ERROR_VAR = new ReadVarExpr(BuiltinVar.CatchError);
export var CATCH_STACK_VAR = new ReadVarExpr(BuiltinVar.CatchStack);
export var NULL_EXPR = new LiteralExpr(null, null);
//// Statements
export enum StmtModifier {
Final,
Private
}
export abstract class Statement {
constructor(public modifiers: StmtModifier[] = null) {
if (isBlank(modifiers)) {
this.modifiers = [];
}
}
abstract visitStatement(visitor: StatementVisitor, context: any): any;
hasModifier(modifier: StmtModifier): boolean { return this.modifiers.indexOf(modifier) !== -1; }
}
export class DeclareVarStmt extends Statement {
public type: Type;
constructor(public name: string, public value: Expression, type: Type = null,
modifiers: StmtModifier[] = null) {
super(modifiers);
this.type = isPresent(type) ? type : value.type;
}
visitStatement(visitor: StatementVisitor, context: any): any {
return visitor.visitDeclareVarStmt(this, context);
}
}
export class DeclareFunctionStmt extends Statement {
constructor(public name: string, public params: FnParam[], public statements: Statement[],
public type: Type = null, modifiers: StmtModifier[] = null) {
super(modifiers);
}
visitStatement(visitor: StatementVisitor, context: any): any {
return visitor.visitDeclareFunctionStmt(this, context);
}
}
export class ExpressionStatement extends Statement {
constructor(public expr: Expression) { super(); }
visitStatement(visitor: StatementVisitor, context: any): any {
return visitor.visitExpressionStmt(this, context);
}
}
export class ReturnStatement extends Statement {
constructor(public value: Expression) { super(); }
visitStatement(visitor: StatementVisitor, context: any): any {
return visitor.visitReturnStmt(this, context);
}
}
export class AbstractClassPart {
constructor(public type: Type = null, public modifiers: StmtModifier[]) {
if (isBlank(modifiers)) {
this.modifiers = [];
}
}
hasModifier(modifier: StmtModifier): boolean { return this.modifiers.indexOf(modifier) !== -1; }
}
export class ClassField extends AbstractClassPart {
constructor(public name: string, type: Type = null, modifiers: StmtModifier[] = null) {
super(type, modifiers);
}
}
export class ClassMethod extends AbstractClassPart {
constructor(public name: string, public params: FnParam[], public body: Statement[],
type: Type = null, modifiers: StmtModifier[] = null) {
super(type, modifiers);
}
}
export class ClassGetter extends AbstractClassPart {
constructor(public name: string, public body: Statement[], type: Type = null,
modifiers: StmtModifier[] = null) {
super(type, modifiers);
}
}
export class ClassStmt extends Statement {
constructor(public name: string, public parent: Expression, public fields: ClassField[],
public getters: ClassGetter[], public constructorMethod: ClassMethod,
public methods: ClassMethod[], modifiers: StmtModifier[] = null) {
super(modifiers);
}
visitStatement(visitor: StatementVisitor, context: any): any {
return visitor.visitDeclareClassStmt(this, context);
}
}
export class IfStmt extends Statement {
constructor(public condition: Expression, public trueCase: Statement[],
public falseCase: Statement[] = /*@ts2dart_const*/[]) {
super();
}
visitStatement(visitor: StatementVisitor, context: any): any {
return visitor.visitIfStmt(this, context);
}
}
export class CommentStmt extends Statement {
constructor(public comment: string) { super(); }
visitStatement(visitor: StatementVisitor, context: any): any {
return visitor.visitCommentStmt(this, context);
}
}
export class TryCatchStmt extends Statement {
constructor(public bodyStmts: Statement[], public catchStmts: Statement[]) { super(); }
visitStatement(visitor: StatementVisitor, context: any): any {
return visitor.visitTryCatchStmt(this, context);
}
}
export class ThrowStmt extends Statement {
constructor(public error: Expression) { super(); }
visitStatement(visitor: StatementVisitor, context: any): any {
return visitor.visitThrowStmt(this, context);
}
}
export interface StatementVisitor {
visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): any;
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): any;
visitExpressionStmt(stmt: ExpressionStatement, context: any): any;
visitReturnStmt(stmt: ReturnStatement, context: any): any;
visitDeclareClassStmt(stmt: ClassStmt, context: any): any;
visitIfStmt(stmt: IfStmt, context: any): any;
visitTryCatchStmt(stmt: TryCatchStmt, context: any): any;
visitThrowStmt(stmt: ThrowStmt, context: any): any;
visitCommentStmt(stmt: CommentStmt, context: any): any;
}
export class ExpressionTransformer implements StatementVisitor, ExpressionVisitor {
visitReadVarExpr(ast: ReadVarExpr, context: any): any { return ast; }
visitWriteVarExpr(expr: WriteVarExpr, context: any): any {
return new WriteVarExpr(expr.name, expr.value.visitExpression(this, context));
}
visitWriteKeyExpr(expr: WriteKeyExpr, context: any): any {
return new WriteKeyExpr(expr.receiver.visitExpression(this, context),
expr.index.visitExpression(this, context),
expr.value.visitExpression(this, context));
}
visitWritePropExpr(expr: WritePropExpr, context: any): any {
return new WritePropExpr(expr.receiver.visitExpression(this, context), expr.name,
expr.value.visitExpression(this, context));
}
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): any {
var method = isPresent(ast.builtin) ? ast.builtin : ast.name;
return new InvokeMethodExpr(ast.receiver.visitExpression(this, context), method,
this.visitAllExpressions(ast.args, context), ast.type);
}
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any {
return new InvokeFunctionExpr(ast.fn.visitExpression(this, context),
this.visitAllExpressions(ast.args, context), ast.type);
}
visitInstantiateExpr(ast: InstantiateExpr, context: any): any {
return new InstantiateExpr(ast.classExpr.visitExpression(this, context),
this.visitAllExpressions(ast.args, context), ast.type);
}
visitLiteralExpr(ast: LiteralExpr, context: any): any { return ast; }
visitExternalExpr(ast: ExternalExpr, context: any): any { return ast; }
visitConditionalExpr(ast: ConditionalExpr, context: any): any {
return new ConditionalExpr(ast.condition.visitExpression(this, context),
ast.trueCase.visitExpression(this, context),
ast.falseCase.visitExpression(this, context));
}
visitNotExpr(ast: NotExpr, context: any): any {
return new NotExpr(ast.condition.visitExpression(this, context));
}
visitCastExpr(ast: CastExpr, context: any): any {
return new CastExpr(ast.value.visitExpression(this, context), context);
}
visitFunctionExpr(ast: FunctionExpr, context: any): any {
// Don't descend into nested functions
return ast;
}
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any {
return new BinaryOperatorExpr(ast.operator, ast.lhs.visitExpression(this, context),
ast.rhs.visitExpression(this, context), ast.type);
}
visitReadPropExpr(ast: ReadPropExpr, context: any): any {
return new ReadPropExpr(ast.receiver.visitExpression(this, context), ast.name, ast.type);
}
visitReadKeyExpr(ast: ReadKeyExpr, context: any): any {
return new ReadKeyExpr(ast.receiver.visitExpression(this, context),
ast.index.visitExpression(this, context), ast.type);
}
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): any {
return new LiteralArrayExpr(this.visitAllExpressions(ast.entries, context));
}
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any {
return new LiteralMapExpr(ast.entries.map(
(entry) => [entry[0], (<Expression>entry[1]).visitExpression(this, context)]));
}
visitAllExpressions(exprs: Expression[], context: any): Expression[] {
return exprs.map(expr => expr.visitExpression(this, context));
}
visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): any {
return new DeclareVarStmt(stmt.name, stmt.value.visitExpression(this, context), stmt.type,
stmt.modifiers);
}
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): any {
// Don't descend into nested functions
return stmt;
}
visitExpressionStmt(stmt: ExpressionStatement, context: any): any {
return new ExpressionStatement(stmt.expr.visitExpression(this, context));
}
visitReturnStmt(stmt: ReturnStatement, context: any): any {
return new ReturnStatement(stmt.value.visitExpression(this, context));
}
visitDeclareClassStmt(stmt: ClassStmt, context: any): any {
// Don't descend into nested functions
return stmt;
}
visitIfStmt(stmt: IfStmt, context: any): any {
return new IfStmt(stmt.condition.visitExpression(this, context),
this.visitAllStatements(stmt.trueCase, context),
this.visitAllStatements(stmt.falseCase, context));
}
visitTryCatchStmt(stmt: TryCatchStmt, context: any): any {
return new TryCatchStmt(this.visitAllStatements(stmt.bodyStmts, context),
this.visitAllStatements(stmt.catchStmts, context));
}
visitThrowStmt(stmt: ThrowStmt, context: any): any {
return new ThrowStmt(stmt.error.visitExpression(this, context));
}
visitCommentStmt(stmt: CommentStmt, context: any): any { return stmt; }
visitAllStatements(stmts: Statement[], context: any): Statement[] {
return stmts.map(stmt => stmt.visitStatement(this, context));
}
}
export class RecursiveExpressionVisitor implements StatementVisitor, ExpressionVisitor {
visitReadVarExpr(ast: ReadVarExpr, context: any): any { return ast; }
visitWriteVarExpr(expr: WriteVarExpr, context: any): any {
expr.value.visitExpression(this, context);
return expr;
}
visitWriteKeyExpr(expr: WriteKeyExpr, context: any): any {
expr.receiver.visitExpression(this, context);
expr.index.visitExpression(this, context);
expr.value.visitExpression(this, context);
return expr;
}
visitWritePropExpr(expr: WritePropExpr, context: any): any {
expr.receiver.visitExpression(this, context);
expr.value.visitExpression(this, context);
return expr;
}
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): any {
ast.receiver.visitExpression(this, context);
this.visitAllExpressions(ast.args, context);
return ast;
}
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any {
ast.fn.visitExpression(this, context);
this.visitAllExpressions(ast.args, context);
return ast;
}
visitInstantiateExpr(ast: InstantiateExpr, context: any): any {
ast.classExpr.visitExpression(this, context);
this.visitAllExpressions(ast.args, context);
return ast;
}
visitLiteralExpr(ast: LiteralExpr, context: any): any { return ast; }
visitExternalExpr(ast: ExternalExpr, context: any): any { return ast; }
visitConditionalExpr(ast: ConditionalExpr, context: any): any {
ast.condition.visitExpression(this, context);
ast.trueCase.visitExpression(this, context);
ast.falseCase.visitExpression(this, context);
return ast;
}
visitNotExpr(ast: NotExpr, context: any): any {
ast.condition.visitExpression(this, context);
return ast;
}
visitCastExpr(ast: CastExpr, context: any): any {
ast.value.visitExpression(this, context);
return ast;
}
visitFunctionExpr(ast: FunctionExpr, context: any): any { return ast; }
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any {
ast.lhs.visitExpression(this, context);
ast.rhs.visitExpression(this, context);
return ast;
}
visitReadPropExpr(ast: ReadPropExpr, context: any): any {
ast.receiver.visitExpression(this, context);
return ast;
}
visitReadKeyExpr(ast: ReadKeyExpr, context: any): any {
ast.receiver.visitExpression(this, context);
ast.index.visitExpression(this, context);
return ast;
}
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): any {
this.visitAllExpressions(ast.entries, context);
return ast;
}
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any {
ast.entries.forEach((entry) => (<Expression>entry[1]).visitExpression(this, context));
return ast;
}
visitAllExpressions(exprs: Expression[], context: any): void {
exprs.forEach(expr => expr.visitExpression(this, context));
}
visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): any {
stmt.value.visitExpression(this, context);
return stmt;
}
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): any {
// Don't descend into nested functions
return stmt;
}
visitExpressionStmt(stmt: ExpressionStatement, context: any): any {
stmt.expr.visitExpression(this, context);
return stmt;
}
visitReturnStmt(stmt: ReturnStatement, context: any): any {
stmt.value.visitExpression(this, context);
return stmt;
}
visitDeclareClassStmt(stmt: ClassStmt, context: any): any {
// Don't descend into nested functions
return stmt;
}
visitIfStmt(stmt: IfStmt, context: any): any {
stmt.condition.visitExpression(this, context);
this.visitAllStatements(stmt.trueCase, context);
this.visitAllStatements(stmt.falseCase, context);
return stmt;
}
visitTryCatchStmt(stmt: TryCatchStmt, context: any): any {
this.visitAllStatements(stmt.bodyStmts, context);
this.visitAllStatements(stmt.catchStmts, context);
return stmt;
}
visitThrowStmt(stmt: ThrowStmt, context: any): any {
stmt.error.visitExpression(this, context);
return stmt;
}
visitCommentStmt(stmt: CommentStmt, context: any): any { return stmt; }
visitAllStatements(stmts: Statement[], context: any): void {
stmts.forEach(stmt => stmt.visitStatement(this, context));
}
}
export function replaceVarInExpression(varName: string, newValue: Expression,
expression: Expression): Expression {
var transformer = new _ReplaceVariableTransformer(varName, newValue);
return expression.visitExpression(transformer, null);
}
class _ReplaceVariableTransformer extends ExpressionTransformer {
constructor(private _varName: string, private _newValue: Expression) { super(); }
visitReadVarExpr(ast: ReadVarExpr, context: any): any {
return ast.name == this._varName ? this._newValue : ast;
}
}
export function findReadVarNames(stmts: Statement[]): Set<string> {
var finder = new _VariableFinder();
finder.visitAllStatements(stmts, null);
return finder.varNames;
}
class _VariableFinder extends RecursiveExpressionVisitor {
varNames = new Set<string>();
visitReadVarExpr(ast: ReadVarExpr, context: any): any {
this.varNames.add(ast.name);
return null;
}
}
export function variable(name: string, type: Type = null): ReadVarExpr {
return new ReadVarExpr(name, type);
}
export function importExpr(id: CompileIdentifierMetadata, typeParams: Type[] = null): ExternalExpr {
return new ExternalExpr(id, null, typeParams);
}
export function importType(id: CompileIdentifierMetadata, typeParams: Type[] = null,
typeModifiers: TypeModifier[] = null): ExternalType {
return isPresent(id) ? new ExternalType(id, typeParams, typeModifiers) : null;
}
export function literal(value: any, type: Type = null): LiteralExpr {
return new LiteralExpr(value, type);
}
export function literalArr(values: Expression[], type: Type = null): LiteralArrayExpr {
return new LiteralArrayExpr(values, type);
}
export function literalMap(values: Array<Array<string | Expression>>,
type: MapType = null): LiteralMapExpr {
return new LiteralMapExpr(values, type);
}
export function not(expr: Expression): NotExpr {
return new NotExpr(expr);
}
export function fn(params: FnParam[], body: Statement[], type: Type = null): FunctionExpr {
return new FunctionExpr(params, body, type);
}

View File

@ -0,0 +1,422 @@
import {
isPresent,
isBlank,
isString,
evalExpression,
IS_DART,
FunctionWrapper
} from 'angular2/src/facade/lang';
import {ObservableWrapper} from 'angular2/src/facade/async';
import * as o from './output_ast';
import {reflector} from 'angular2/src/core/reflection/reflection';
import {BaseException, unimplemented} from 'angular2/src/facade/exceptions';
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {debugOutputAstAsDart} from './dart_emitter';
import {debugOutputAstAsTypeScript} from './ts_emitter';
export function interpretStatements(statements: o.Statement[], resultVar: string,
instanceFactory: InstanceFactory): any {
var stmtsWithReturn = statements.concat([new o.ReturnStatement(o.variable(resultVar))]);
var ctx = new _ExecutionContext(null, null, null, null, new Map<string, any>(),
new Map<string, any>(), new Map<string, Function>(),
new Map<string, Function>(), instanceFactory);
var visitor = new StatementInterpreter();
var result = visitor.visitAllStatements(stmtsWithReturn, ctx);
return isPresent(result) ? result.value : null;
}
export interface InstanceFactory {
createInstance(superClass: any, clazz: any, constructorArgs: any[], props: Map<string, any>,
getters: Map<string, Function>, methods: Map<string, Function>): DynamicInstance;
}
export abstract class DynamicInstance {
get props(): Map<string, any> { return unimplemented(); }
get getters(): Map<string, Function> { return unimplemented(); }
get methods(): Map<string, any> { return unimplemented(); }
get clazz(): any { return unimplemented(); }
}
function isDynamicInstance(instance: any): any {
if (IS_DART) {
return instance instanceof DynamicInstance;
} else {
return isPresent(instance) && isPresent(instance.props) && isPresent(instance.getters) &&
isPresent(instance.methods);
}
}
function _executeFunctionStatements(varNames: string[], varValues: any[], statements: o.Statement[],
ctx: _ExecutionContext, visitor: StatementInterpreter): any {
var childCtx = ctx.createChildWihtLocalVars();
for (var i = 0; i < varNames.length; i++) {
childCtx.vars.set(varNames[i], varValues[i]);
}
var result = visitor.visitAllStatements(statements, childCtx);
return isPresent(result) ? result.value : null;
}
class _ExecutionContext {
constructor(public parent: _ExecutionContext, public superClass: any, public superInstance: any,
public className: string, public vars: Map<string, any>,
public props: Map<string, any>, public getters: Map<string, Function>,
public methods: Map<string, Function>, public instanceFactory: InstanceFactory) {}
createChildWihtLocalVars(): _ExecutionContext {
return new _ExecutionContext(this, this.superClass, this.superInstance, this.className,
new Map<string, any>(), this.props, this.getters, this.methods,
this.instanceFactory);
}
}
class ReturnValue {
constructor(public value: any) {}
}
class _DynamicClass {
constructor(private _classStmt: o.ClassStmt, private _ctx: _ExecutionContext,
private _visitor: StatementInterpreter) {}
instantiate(args: any[]): DynamicInstance {
var props = new Map<string, any>();
var getters = new Map<string, Function>();
var methods = new Map<string, Function>();
var superClass = this._classStmt.parent.visitExpression(this._visitor, this._ctx);
var instanceCtx =
new _ExecutionContext(this._ctx, superClass, null, this._classStmt.name, this._ctx.vars,
props, getters, methods, this._ctx.instanceFactory);
this._classStmt.fields.forEach((field: o.ClassField) => { props.set(field.name, null); });
this._classStmt.getters.forEach((getter: o.ClassGetter) => {
getters.set(getter.name, () => _executeFunctionStatements([], [], getter.body, instanceCtx,
this._visitor));
});
this._classStmt.methods.forEach((method: o.ClassMethod) => {
var paramNames = method.params.map(param => param.name);
methods.set(method.name, _declareFn(paramNames, method.body, instanceCtx, this._visitor));
});
var ctorParamNames = this._classStmt.constructorMethod.params.map(param => param.name);
_executeFunctionStatements(ctorParamNames, args, this._classStmt.constructorMethod.body,
instanceCtx, this._visitor);
return instanceCtx.superInstance;
}
debugAst(): string { return this._visitor.debugAst(this._classStmt); }
}
class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor {
debugAst(ast: o.Expression | o.Statement | o.Type): string {
return IS_DART ? debugOutputAstAsDart(ast) : debugOutputAstAsTypeScript(ast);
}
visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: _ExecutionContext): any {
ctx.vars.set(stmt.name, stmt.value.visitExpression(this, ctx));
return null;
}
visitWriteVarExpr(expr: o.WriteVarExpr, ctx: _ExecutionContext): any {
var value = expr.value.visitExpression(this, ctx);
var currCtx = ctx;
while (currCtx != null) {
if (currCtx.vars.has(expr.name)) {
currCtx.vars.set(expr.name, value);
return value;
}
currCtx = currCtx.parent;
}
throw new BaseException(`Not declared variable ${expr.name}`);
}
visitReadVarExpr(ast: o.ReadVarExpr, ctx: _ExecutionContext): any {
var varName = ast.name;
if (isPresent(ast.builtin)) {
switch (ast.builtin) {
case o.BuiltinVar.Super:
case o.BuiltinVar.This:
return ctx.superInstance;
case o.BuiltinVar.CatchError:
varName = CATCH_ERROR_VAR;
break;
case o.BuiltinVar.CatchStack:
varName = CATCH_STACK_VAR;
break;
default:
throw new BaseException(`Unknown builtin variable ${ast.builtin}`);
}
}
var currCtx = ctx;
while (currCtx != null) {
if (currCtx.vars.has(varName)) {
return currCtx.vars.get(varName);
}
currCtx = currCtx.parent;
}
throw new BaseException(`Not declared variable ${varName}`);
}
visitWriteKeyExpr(expr: o.WriteKeyExpr, ctx: _ExecutionContext): any {
var receiver = expr.receiver.visitExpression(this, ctx);
var index = expr.index.visitExpression(this, ctx);
var value = expr.value.visitExpression(this, ctx);
receiver[index] = value;
return value;
}
visitWritePropExpr(expr: o.WritePropExpr, ctx: _ExecutionContext): any {
var receiver = expr.receiver.visitExpression(this, ctx);
var value = expr.value.visitExpression(this, ctx);
if (isDynamicInstance(receiver)) {
var di = <DynamicInstance>receiver;
if (di.props.has(expr.name)) {
di.props.set(expr.name, value);
} else {
reflector.setter(expr.name)(receiver, value);
}
} else {
reflector.setter(expr.name)(receiver, value);
}
return value;
}
visitInvokeMethodExpr(expr: o.InvokeMethodExpr, ctx: _ExecutionContext): any {
var receiver = expr.receiver.visitExpression(this, ctx);
var args = this.visitAllExpressions(expr.args, ctx);
var result;
if (isPresent(expr.builtin)) {
switch (expr.builtin) {
case o.BuiltinMethod.ConcatArray:
result = ListWrapper.concat(receiver, args[0]);
break;
case o.BuiltinMethod.SubscribeObservable:
result = ObservableWrapper.subscribe(receiver, args[0]);
break;
case o.BuiltinMethod.bind:
if (IS_DART) {
result = receiver;
} else {
result = receiver.bind(args[0]);
}
break;
default:
throw new BaseException(`Unknown builtin method ${expr.builtin}`);
}
} else if (isDynamicInstance(receiver)) {
var di = <DynamicInstance>receiver;
if (di.methods.has(expr.name)) {
result = FunctionWrapper.apply(di.methods.get(expr.name), args);
} else {
result = reflector.method(expr.name)(receiver, args);
}
} else {
result = reflector.method(expr.name)(receiver, args);
}
return result;
}
visitInvokeFunctionExpr(stmt: o.InvokeFunctionExpr, ctx: _ExecutionContext): any {
var args = this.visitAllExpressions(stmt.args, ctx);
var fnExpr = stmt.fn;
if (fnExpr instanceof o.ReadVarExpr && fnExpr.builtin === o.BuiltinVar.Super) {
ctx.superInstance = ctx.instanceFactory.createInstance(ctx.superClass, ctx.className, args,
ctx.props, ctx.getters, ctx.methods);
ctx.parent.superInstance = ctx.superInstance;
return null;
} else {
var fn = stmt.fn.visitExpression(this, ctx);
return FunctionWrapper.apply(fn, args);
}
}
visitReturnStmt(stmt: o.ReturnStatement, ctx: _ExecutionContext): any {
return new ReturnValue(stmt.value.visitExpression(this, ctx));
}
visitDeclareClassStmt(stmt: o.ClassStmt, ctx: _ExecutionContext): any {
var clazz = new _DynamicClass(stmt, ctx, this);
ctx.vars.set(stmt.name, clazz);
return null;
}
visitExpressionStmt(stmt: o.ExpressionStatement, ctx: _ExecutionContext): any {
return stmt.expr.visitExpression(this, ctx);
}
visitIfStmt(stmt: o.IfStmt, ctx: _ExecutionContext): any {
var condition = stmt.condition.visitExpression(this, ctx);
if (condition) {
return this.visitAllStatements(stmt.trueCase, ctx);
} else if (isPresent(stmt.falseCase)) {
return this.visitAllStatements(stmt.falseCase, ctx);
}
return null;
}
visitTryCatchStmt(stmt: o.TryCatchStmt, ctx: _ExecutionContext): any {
try {
return this.visitAllStatements(stmt.bodyStmts, ctx);
} catch (e) {
var childCtx = ctx.createChildWihtLocalVars();
childCtx.vars.set(CATCH_ERROR_VAR, e);
childCtx.vars.set(CATCH_STACK_VAR, e.stack);
return this.visitAllStatements(stmt.catchStmts, childCtx);
}
}
visitThrowStmt(stmt: o.ThrowStmt, ctx: _ExecutionContext): any {
throw stmt.error.visitExpression(this, ctx);
}
visitCommentStmt(stmt: o.CommentStmt, context?: any): any { return null; }
visitInstantiateExpr(ast: o.InstantiateExpr, ctx: _ExecutionContext): any {
var args = this.visitAllExpressions(ast.args, ctx);
var clazz = ast.classExpr.visitExpression(this, ctx);
if (clazz instanceof _DynamicClass) {
return clazz.instantiate(args);
} else {
return FunctionWrapper.apply(reflector.factory(clazz), args);
}
}
visitLiteralExpr(ast: o.LiteralExpr, ctx: _ExecutionContext): any { return ast.value; }
visitExternalExpr(ast: o.ExternalExpr, ctx: _ExecutionContext): any { return ast.value.runtime; }
visitConditionalExpr(ast: o.ConditionalExpr, ctx: _ExecutionContext): any {
if (ast.condition.visitExpression(this, ctx)) {
return ast.trueCase.visitExpression(this, ctx);
} else if (isPresent(ast.falseCase)) {
return ast.falseCase.visitExpression(this, ctx);
}
return null;
}
visitNotExpr(ast: o.NotExpr, ctx: _ExecutionContext): any {
return !ast.condition.visitExpression(this, ctx);
}
visitCastExpr(ast: o.CastExpr, ctx: _ExecutionContext): any {
return ast.value.visitExpression(this, ctx);
}
visitFunctionExpr(ast: o.FunctionExpr, ctx: _ExecutionContext): any {
var paramNames = ast.params.map((param) => param.name);
return _declareFn(paramNames, ast.statements, ctx, this);
}
visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, ctx: _ExecutionContext): any {
var paramNames = stmt.params.map((param) => param.name);
ctx.vars.set(stmt.name, _declareFn(paramNames, stmt.statements, ctx, this));
return null;
}
visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, ctx: _ExecutionContext): any {
var lhs = () => ast.lhs.visitExpression(this, ctx);
var rhs = () => ast.rhs.visitExpression(this, ctx);
switch (ast.operator) {
case o.BinaryOperator.Equals:
return lhs() == rhs();
case o.BinaryOperator.Identical:
return lhs() === rhs();
case o.BinaryOperator.NotEquals:
return lhs() != rhs();
case o.BinaryOperator.NotIdentical:
return lhs() !== rhs();
case o.BinaryOperator.And:
return lhs() && rhs();
case o.BinaryOperator.Or:
return lhs() || rhs();
case o.BinaryOperator.Plus:
return lhs() + rhs();
case o.BinaryOperator.Minus:
return lhs() - rhs();
case o.BinaryOperator.Divide:
return lhs() / rhs();
case o.BinaryOperator.Multiply:
return lhs() * rhs();
case o.BinaryOperator.Modulo:
return lhs() % rhs();
case o.BinaryOperator.Lower:
return lhs() < rhs();
case o.BinaryOperator.LowerEquals:
return lhs() <= rhs();
case o.BinaryOperator.Bigger:
return lhs() > rhs();
case o.BinaryOperator.BiggerEquals:
return lhs() >= rhs();
default:
throw new BaseException(`Unknown operator ${ast.operator}`);
}
}
visitReadPropExpr(ast: o.ReadPropExpr, ctx: _ExecutionContext): any {
var result;
var receiver = ast.receiver.visitExpression(this, ctx);
if (isDynamicInstance(receiver)) {
var di = <DynamicInstance>receiver;
if (di.props.has(ast.name)) {
result = di.props.get(ast.name);
} else if (di.getters.has(ast.name)) {
result = di.getters.get(ast.name)();
} else if (di.methods.has(ast.name)) {
result = di.methods.get(ast.name);
} else {
result = reflector.getter(ast.name)(receiver);
}
} else {
result = reflector.getter(ast.name)(receiver);
}
return result;
}
visitReadKeyExpr(ast: o.ReadKeyExpr, ctx: _ExecutionContext): any {
var receiver = ast.receiver.visitExpression(this, ctx);
var prop = ast.index.visitExpression(this, ctx);
return receiver[prop];
}
visitLiteralArrayExpr(ast: o.LiteralArrayExpr, ctx: _ExecutionContext): any {
return this.visitAllExpressions(ast.entries, ctx);
}
visitLiteralMapExpr(ast: o.LiteralMapExpr, ctx: _ExecutionContext): any {
var result = {};
ast.entries.forEach((entry) => result[<string>entry[0]] =
(<o.Expression>entry[1]).visitExpression(this, ctx));
return result;
}
visitAllExpressions(expressions: o.Expression[], ctx: _ExecutionContext): any {
return expressions.map((expr) => expr.visitExpression(this, ctx));
}
visitAllStatements(statements: o.Statement[], ctx: _ExecutionContext): ReturnValue {
for (var i = 0; i < statements.length; i++) {
var stmt = statements[i];
var val = stmt.visitStatement(this, ctx);
if (val instanceof ReturnValue) {
return val;
}
}
return null;
}
}
function _declareFn(varNames: string[], statements: o.Statement[], ctx: _ExecutionContext,
visitor: StatementInterpreter): Function {
switch (varNames.length) {
case 0:
return () => _executeFunctionStatements(varNames, [], statements, ctx, visitor);
case 1:
return (d0) => _executeFunctionStatements(varNames, [d0], statements, ctx, visitor);
case 2:
return (d0, d1) => _executeFunctionStatements(varNames, [d0, d1], statements, ctx, visitor);
case 3:
return (d0, d1, d2) =>
_executeFunctionStatements(varNames, [d0, d1, d2], statements, ctx, visitor);
case 4:
return (d0, d1, d2, d3) =>
_executeFunctionStatements(varNames, [d0, d1, d2, d3], statements, ctx, visitor);
case 5:
return (d0, d1, d2, d3, d4) => _executeFunctionStatements(varNames, [d0, d1, d2, d3, d4],
statements, ctx, visitor);
case 6:
return (d0, d1, d2, d3, d4, d5) => _executeFunctionStatements(
varNames, [d0, d1, d2, d3, d4, d5], statements, ctx, visitor);
case 7:
return (d0, d1, d2, d3, d4, d5, d6) => _executeFunctionStatements(
varNames, [d0, d1, d2, d3, d4, d5, d6], statements, ctx, visitor);
case 8:
return (d0, d1, d2, d3, d4, d5, d6, d7) => _executeFunctionStatements(
varNames, [d0, d1, d2, d3, d4, d5, d6, d7], statements, ctx, visitor);
case 9:
return (d0, d1, d2, d3, d4, d5, d6, d7, d8) => _executeFunctionStatements(
varNames, [d0, d1, d2, d3, d4, d5, d6, d7, d8], statements, ctx, visitor);
case 10:
return (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9) => _executeFunctionStatements(
varNames, [d0, d1, d2, d3, d4, d5, d6, d7, d8, d9], statements, ctx, visitor);
default:
throw new BaseException(
'Declaring functions with more than 10 arguments is not supported right now');
}
}
var CATCH_ERROR_VAR = 'error';
var CATCH_STACK_VAR = 'stack';

View File

@ -0,0 +1,46 @@
import {
isPresent,
isBlank,
isString,
evalExpression,
RegExpWrapper,
StringWrapper
} from 'angular2/src/facade/lang';
import * as o from './output_ast';
import {EmitterVisitorContext} from './abstract_emitter';
import {AbstractJsEmitterVisitor} from './abstract_js_emitter';
import {sanitizeIdentifier} from '../util';
export function jitStatements(sourceUrl: string, statements: o.Statement[],
resultVar: string): any {
var converter = new JitEmitterVisitor();
var ctx = EmitterVisitorContext.createRoot([resultVar]);
converter.visitAllStatements(statements, ctx);
return evalExpression(sourceUrl, resultVar, ctx.toSource(), converter.getArgs());
}
class JitEmitterVisitor extends AbstractJsEmitterVisitor {
private _evalArgNames: string[] = [];
private _evalArgValues: any[] = [];
getArgs(): {[key: string]: any} {
var result = {};
for (var i = 0; i < this._evalArgNames.length; i++) {
result[this._evalArgNames[i]] = this._evalArgValues[i];
}
return result;
}
visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any {
var value = ast.value.runtime;
var id = this._evalArgValues.indexOf(value);
if (id === -1) {
id = this._evalArgValues.length;
this._evalArgValues.push(value);
var name = isPresent(ast.value.name) ? sanitizeIdentifier(ast.value.name) : 'val';
this._evalArgNames.push(sanitizeIdentifier(`jit_${name}${id}`));
}
ctx.print(this._evalArgNames[id]);
return null;
}
}

View File

@ -0,0 +1,80 @@
import {BaseException} from 'angular2/src/facade/exceptions';
import {isPresent, isBlank, RegExpWrapper, Math} from 'angular2/src/facade/lang';
// asset:<package-name>/<realm>/<path-to-module>
var _ASSET_URL_RE = /asset:([^\/]+)\/([^\/]+)\/(.+)/g;
var _PATH_SEP = '/';
var _PATH_SEP_RE = /\//g;
export enum ImportEnv {
Dart,
JS
}
/**
* Returns the module path to use for an import.
*/
export function getImportModulePath(moduleUrlStr: string, importedUrlStr: string,
importEnv: ImportEnv): string {
var absolutePathPrefix: string = importEnv === ImportEnv.Dart ? `package:` : '';
var moduleUrl = _AssetUrl.parse(moduleUrlStr, false);
var importedUrl = _AssetUrl.parse(importedUrlStr, true);
if (isBlank(importedUrl)) {
return importedUrlStr;
}
// Try to create a relative path first
if (moduleUrl.firstLevelDir == importedUrl.firstLevelDir &&
moduleUrl.packageName == importedUrl.packageName) {
return getRelativePath(moduleUrl.modulePath, importedUrl.modulePath, importEnv);
} else if (importedUrl.firstLevelDir == 'lib') {
return `${absolutePathPrefix}${importedUrl.packageName}/${importedUrl.modulePath}`;
}
throw new BaseException(`Can't import url ${importedUrlStr} from ${moduleUrlStr}`);
}
class _AssetUrl {
static parse(url: string, allowNonMatching: boolean): _AssetUrl {
var match = RegExpWrapper.firstMatch(_ASSET_URL_RE, url);
if (isPresent(match)) {
return new _AssetUrl(match[1], match[2], match[3]);
}
if (allowNonMatching) {
return null;
}
throw new BaseException(`Url ${url} is not a valid asset: url`);
}
constructor(public packageName: string, public firstLevelDir: string, public modulePath: string) {
}
}
export function getRelativePath(modulePath: string, importedPath: string,
importEnv: ImportEnv): string {
var moduleParts = modulePath.split(_PATH_SEP_RE);
var importedParts = importedPath.split(_PATH_SEP_RE);
var longestPrefix = getLongestPathSegmentPrefix(moduleParts, importedParts);
var resultParts = [];
var goParentCount = moduleParts.length - 1 - longestPrefix;
for (var i = 0; i < goParentCount; i++) {
resultParts.push('..');
}
if (goParentCount <= 0 && importEnv === ImportEnv.JS) {
resultParts.push('.');
}
for (var i = longestPrefix; i < importedParts.length; i++) {
resultParts.push(importedParts[i]);
}
return resultParts.join(_PATH_SEP);
}
export function getLongestPathSegmentPrefix(arr1: string[], arr2: string[]): number {
var prefixSize = 0;
var minLen = Math.min(arr1.length, arr2.length);
while (prefixSize < minLen && arr1[prefixSize] == arr2[prefixSize]) {
prefixSize++;
}
return prefixSize;
}

View File

@ -0,0 +1,332 @@
import * as o from './output_ast';
import {
isPresent,
isBlank,
isString,
evalExpression,
RegExpWrapper,
StringWrapper,
isArray
} from 'angular2/src/facade/lang';
import {CompileIdentifierMetadata} from '../compile_metadata';
import {BaseException} from 'angular2/src/facade/exceptions';
import {
OutputEmitter,
EmitterVisitorContext,
AbstractEmitterVisitor,
CATCH_ERROR_VAR,
CATCH_STACK_VAR
} from './abstract_emitter';
import {getImportModulePath, ImportEnv} from './path_util';
var _debugModuleUrl = 'asset://debug/lib';
export function debugOutputAstAsTypeScript(ast: o.Statement | o.Expression | o.Type |
any[]): string {
var converter = new _TsEmitterVisitor(_debugModuleUrl);
var ctx = EmitterVisitorContext.createRoot([]);
var asts: any[];
if (isArray(ast)) {
asts = <any[]>ast;
} else {
asts = [ast];
}
asts.forEach((ast) => {
if (ast instanceof o.Statement) {
ast.visitStatement(converter, ctx);
} else if (ast instanceof o.Expression) {
ast.visitExpression(converter, ctx);
} else if (ast instanceof o.Type) {
ast.visitType(converter, ctx);
} else {
throw new BaseException(`Don't know how to print debug info for ${ast}`);
}
});
return ctx.toSource();
}
export class TypeScriptEmitter implements OutputEmitter {
constructor() {}
emitStatements(moduleUrl: string, stmts: o.Statement[], exportedVars: string[]): string {
var converter = new _TsEmitterVisitor(moduleUrl);
var ctx = EmitterVisitorContext.createRoot(exportedVars);
converter.visitAllStatements(stmts, ctx);
var srcParts = [];
converter.importsWithPrefixes.forEach((prefix, importedModuleUrl) => {
// Note: can't write the real word for import as it screws up system.js auto detection...
srcParts.push(
`imp` +
`ort * as ${prefix} from '${getImportModulePath(moduleUrl, importedModuleUrl, ImportEnv.JS)}';`);
});
srcParts.push(ctx.toSource());
return srcParts.join('\n');
}
}
class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor {
constructor(private _moduleUrl: string) { super(false); }
importsWithPrefixes = new Map<string, string>();
visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any {
this._visitIdentifier(ast.value, ast.typeParams, ctx);
return null;
}
visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any {
if (ctx.isExportedVar(stmt.name)) {
ctx.print(`export `);
}
if (stmt.hasModifier(o.StmtModifier.Final)) {
ctx.print(`const`);
} else {
ctx.print(`var`);
}
ctx.print(` ${stmt.name}`);
if (isPresent(stmt.type)) {
ctx.print(`:`);
stmt.type.visitType(this, ctx);
}
ctx.print(` = `);
stmt.value.visitExpression(this, ctx);
ctx.println(`;`);
return null;
}
visitCastExpr(ast: o.CastExpr, ctx: EmitterVisitorContext): any {
ctx.print(`(<`);
ast.type.visitType(this, ctx);
ctx.print(`>`);
ast.value.visitExpression(this, ctx);
ctx.print(`)`);
return null;
}
visitDeclareClassStmt(stmt: o.ClassStmt, ctx: EmitterVisitorContext): any {
ctx.pushClass(stmt);
if (ctx.isExportedVar(stmt.name)) {
ctx.print(`export `);
}
ctx.print(`class ${stmt.name}`);
if (isPresent(stmt.parent)) {
ctx.print(` extends `);
stmt.parent.visitExpression(this, ctx);
}
ctx.println(` {`);
ctx.incIndent();
stmt.fields.forEach((field) => this._visitClassField(field, ctx));
if (isPresent(stmt.constructorMethod)) {
this._visitClassConstructor(stmt, ctx);
}
stmt.getters.forEach((getter) => this._visitClassGetter(getter, ctx));
stmt.methods.forEach((method) => this._visitClassMethod(method, ctx));
ctx.decIndent();
ctx.println(`}`);
ctx.popClass();
return null;
}
private _visitClassField(field: o.ClassField, ctx: EmitterVisitorContext) {
if (field.hasModifier(o.StmtModifier.Private)) {
ctx.print(`private `);
}
ctx.print(field.name);
if (isPresent(field.type)) {
ctx.print(`:`);
field.type.visitType(this, ctx);
} else {
ctx.print(`: any`);
}
ctx.println(`;`);
}
private _visitClassGetter(getter: o.ClassGetter, ctx: EmitterVisitorContext) {
if (getter.hasModifier(o.StmtModifier.Private)) {
ctx.print(`private `);
}
ctx.print(`get ${getter.name}()`);
if (isPresent(getter.type)) {
ctx.print(`:`);
getter.type.visitType(this, ctx);
}
ctx.println(` {`);
ctx.incIndent();
this.visitAllStatements(getter.body, ctx);
ctx.decIndent();
ctx.println(`}`);
}
private _visitClassConstructor(stmt: o.ClassStmt, ctx: EmitterVisitorContext) {
ctx.print(`constructor(`);
this._visitParams(stmt.constructorMethod.params, ctx);
ctx.println(`) {`);
ctx.incIndent();
this.visitAllStatements(stmt.constructorMethod.body, ctx);
ctx.decIndent();
ctx.println(`}`);
}
private _visitClassMethod(method: o.ClassMethod, ctx: EmitterVisitorContext) {
if (method.hasModifier(o.StmtModifier.Private)) {
ctx.print(`private `);
}
ctx.print(`${method.name}(`);
this._visitParams(method.params, ctx);
ctx.print(`):`);
if (isPresent(method.type)) {
method.type.visitType(this, ctx);
} else {
ctx.print(`void`);
}
ctx.println(` {`);
ctx.incIndent();
this.visitAllStatements(method.body, ctx);
ctx.decIndent();
ctx.println(`}`);
}
visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any {
ctx.print(`(`);
this._visitParams(ast.params, ctx);
ctx.print(`):`);
if (isPresent(ast.type)) {
ast.type.visitType(this, ctx);
} else {
ctx.print(`void`);
}
ctx.println(` => {`);
ctx.incIndent();
this.visitAllStatements(ast.statements, ctx);
ctx.decIndent();
ctx.print(`}`);
return null;
}
visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, ctx: EmitterVisitorContext): any {
if (ctx.isExportedVar(stmt.name)) {
ctx.print(`export `);
}
ctx.print(`function ${stmt.name}(`);
this._visitParams(stmt.params, ctx);
ctx.print(`):`);
if (isPresent(stmt.type)) {
stmt.type.visitType(this, ctx);
} else {
ctx.print(`void`);
}
ctx.println(` {`);
ctx.incIndent();
this.visitAllStatements(stmt.statements, ctx);
ctx.decIndent();
ctx.println(`}`);
return null;
}
visitTryCatchStmt(stmt: o.TryCatchStmt, ctx: EmitterVisitorContext): any {
ctx.println(`try {`);
ctx.incIndent();
this.visitAllStatements(stmt.bodyStmts, ctx);
ctx.decIndent();
ctx.println(`} catch (${CATCH_ERROR_VAR.name}) {`);
ctx.incIndent();
var catchStmts = [
<o.Statement>CATCH_STACK_VAR.set(CATCH_ERROR_VAR.prop('stack'))
.toDeclStmt(null, [o.StmtModifier.Final])
].concat(stmt.catchStmts);
this.visitAllStatements(catchStmts, ctx);
ctx.decIndent();
ctx.println(`}`);
return null;
}
visitBuiltintType(type: o.BuiltinType, ctx: EmitterVisitorContext): any {
var typeStr;
switch (type.name) {
case o.BuiltinTypeName.Bool:
typeStr = 'boolean';
break;
case o.BuiltinTypeName.Dynamic:
typeStr = 'any';
break;
case o.BuiltinTypeName.Function:
typeStr = 'Function';
break;
case o.BuiltinTypeName.Number:
typeStr = 'number';
break;
case o.BuiltinTypeName.Int:
typeStr = 'number';
break;
case o.BuiltinTypeName.String:
typeStr = 'string';
break;
default:
throw new BaseException(`Unsupported builtin type ${type.name}`);
}
ctx.print(typeStr);
return null;
}
visitExternalType(ast: o.ExternalType, ctx: EmitterVisitorContext): any {
this._visitIdentifier(ast.value, ast.typeParams, ctx);
return null;
}
visitArrayType(type: o.ArrayType, ctx: EmitterVisitorContext): any {
if (isPresent(type.of)) {
type.of.visitType(this, ctx);
} else {
ctx.print(`any`);
}
ctx.print(`[]`);
return null;
}
visitMapType(type: o.MapType, ctx: EmitterVisitorContext): any {
ctx.print(`{[key: string]:`);
if (isPresent(type.valueType)) {
type.valueType.visitType(this, ctx);
} else {
ctx.print(`any`);
}
ctx.print(`}`);
return null;
}
getBuiltinMethodName(method: o.BuiltinMethod): string {
var name;
switch (method) {
case o.BuiltinMethod.ConcatArray:
name = 'concat';
break;
case o.BuiltinMethod.SubscribeObservable:
name = 'subscribe';
break;
case o.BuiltinMethod.bind:
name = 'bind';
break;
default:
throw new BaseException(`Unknown builtin method: ${method}`);
}
return name;
}
private _visitParams(params: o.FnParam[], ctx: EmitterVisitorContext): void {
this.visitAllObjects((param) => {
ctx.print(param.name);
if (isPresent(param.type)) {
ctx.print(`:`);
param.type.visitType(this, ctx);
}
}, params, ctx, ',');
}
private _visitIdentifier(value: CompileIdentifierMetadata, typeParams: o.Type[],
ctx: EmitterVisitorContext): void {
if (isBlank(value.name)) {
throw new BaseException(`Internal error: unknown identifier ${value}`);
}
if (isPresent(value.moduleUrl) && value.moduleUrl != this._moduleUrl) {
var prefix = this.importsWithPrefixes.get(value.moduleUrl);
if (isBlank(prefix)) {
prefix = `import${this.importsWithPrefixes.size}`;
this.importsWithPrefixes.set(value.moduleUrl, prefix);
}
ctx.print(`${prefix}.`);
}
ctx.print(value.name);
if (isPresent(typeParams) && typeParams.length > 0) {
ctx.print(`<`);
this.visitAllObjects((type) => type.visitType(this, ctx), typeParams, ctx, ',');
ctx.print(`>`);
}
}
}

View File

@ -0,0 +1,66 @@
export class ParseLocation {
constructor(public file: ParseSourceFile, public offset: number, public line: number,
public col: number) {}
toString(): string { return `${this.file.url}@${this.line}:${this.col}`; }
}
export class ParseSourceFile {
constructor(public content: string, public url: string) {}
}
export class ParseSourceSpan {
constructor(public start: ParseLocation, public end: ParseLocation) {}
toString(): string {
return this.start.file.content.substring(this.start.offset, this.end.offset);
}
}
export enum ParseErrorLevel {
WARNING,
FATAL
}
export abstract class ParseError {
constructor(public span: ParseSourceSpan, public msg: string,
public level: ParseErrorLevel = ParseErrorLevel.FATAL) {}
toString(): string {
var source = this.span.start.file.content;
var ctxStart = this.span.start.offset;
if (ctxStart > source.length - 1) {
ctxStart = source.length - 1;
}
var ctxEnd = ctxStart;
var ctxLen = 0;
var ctxLines = 0;
while (ctxLen < 100 && ctxStart > 0) {
ctxStart--;
ctxLen++;
if (source[ctxStart] == "\n") {
if (++ctxLines == 3) {
break;
}
}
}
ctxLen = 0;
ctxLines = 0;
while (ctxLen < 100 && ctxEnd < source.length - 1) {
ctxEnd++;
ctxLen++;
if (source[ctxEnd] == "\n") {
if (++ctxLines == 3) {
break;
}
}
}
let context = source.substring(ctxStart, this.span.start.offset) + '[ERROR ->]' +
source.substring(this.span.start.offset, ctxEnd + 1);
return `${this.msg} ("${context}"): ${this.span.start}`;
}
}

View File

@ -0,0 +1,45 @@
import {resolveForwardRef, Injectable} from 'angular2/src/core/di';
import {Type, isPresent, stringify} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {PipeMetadata} from 'angular2/src/core/metadata';
import {ReflectorReader} from 'angular2/src/core/reflection/reflector_reader';
import {reflector} from 'angular2/src/core/reflection/reflection';
function _isPipeMetadata(type: any): boolean {
return type instanceof PipeMetadata;
}
/**
* Resolve a `Type` for {@link PipeMetadata}.
*
* This interface can be overridden by the application developer to create custom behavior.
*
* See {@link Compiler}
*/
@Injectable()
export class PipeResolver {
private _reflector: ReflectorReader;
constructor(_reflector?: ReflectorReader) {
if (isPresent(_reflector)) {
this._reflector = _reflector;
} else {
this._reflector = reflector;
}
}
/**
* Return {@link PipeMetadata} for a given `Type`.
*/
resolve(type: Type): PipeMetadata {
var metas = this._reflector.annotations(resolveForwardRef(type));
if (isPresent(metas)) {
var annotation = metas.find(_isPipeMetadata);
if (isPresent(annotation)) {
return annotation;
}
}
throw new BaseException(`No Pipe decorator found on ${stringify(type)}`);
}
}
export var CODEGEN_PIPE_RESOLVER = new PipeResolver(reflector);

View File

@ -0,0 +1,431 @@
import {isPresent, isBlank, isArray, normalizeBlank} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
import {
TemplateAst,
TemplateAstVisitor,
NgContentAst,
EmbeddedTemplateAst,
ElementAst,
ReferenceAst,
BoundEventAst,
BoundElementPropertyAst,
AttrAst,
BoundTextAst,
TextAst,
DirectiveAst,
BoundDirectivePropertyAst,
templateVisitAll,
PropertyBindingType,
ProviderAst,
ProviderAstType
} from './template_ast';
import {
CompileTypeMetadata,
CompileTokenMap,
CompileQueryMetadata,
CompileTokenMetadata,
CompileProviderMetadata,
CompileDirectiveMetadata,
CompileDiDependencyMetadata
} from './compile_metadata';
import {Identifiers, identifierToken} from './identifiers';
import {ParseSourceSpan, ParseError, ParseLocation} from './parse_util';
export class ProviderError extends ParseError {
constructor(message: string, span: ParseSourceSpan) { super(span, message); }
}
export class ProviderViewContext {
/**
* @internal
*/
viewQueries: CompileTokenMap<CompileQueryMetadata[]>;
/**
* @internal
*/
viewProviders: CompileTokenMap<boolean>;
errors: ProviderError[] = [];
constructor(public component: CompileDirectiveMetadata, public sourceSpan: ParseSourceSpan) {
this.viewQueries = _getViewQueries(component);
this.viewProviders = new CompileTokenMap<boolean>();
_normalizeProviders(component.viewProviders, sourceSpan, this.errors)
.forEach((provider) => {
if (isBlank(this.viewProviders.get(provider.token))) {
this.viewProviders.add(provider.token, true);
}
});
}
}
export class ProviderElementContext {
private _contentQueries: CompileTokenMap<CompileQueryMetadata[]>;
private _transformedProviders = new CompileTokenMap<ProviderAst>();
private _seenProviders = new CompileTokenMap<boolean>();
private _allProviders: CompileTokenMap<ProviderAst>;
private _attrs: {[key: string]: string};
private _hasViewContainer: boolean = false;
constructor(private _viewContext: ProviderViewContext, private _parent: ProviderElementContext,
private _isViewRoot: boolean, private _directiveAsts: DirectiveAst[],
attrs: AttrAst[], refs: ReferenceAst[], private _sourceSpan: ParseSourceSpan) {
this._attrs = {};
attrs.forEach((attrAst) => this._attrs[attrAst.name] = attrAst.value);
var directivesMeta = _directiveAsts.map(directiveAst => directiveAst.directive);
this._allProviders =
_resolveProvidersFromDirectives(directivesMeta, _sourceSpan, _viewContext.errors);
this._contentQueries = _getContentQueries(directivesMeta);
var queriedTokens = new CompileTokenMap<boolean>();
this._allProviders.values().forEach(
(provider) => { this._addQueryReadsTo(provider.token, queriedTokens); });
refs.forEach((refAst) => {
this._addQueryReadsTo(new CompileTokenMetadata({value: refAst.name}), queriedTokens);
});
if (isPresent(queriedTokens.get(identifierToken(Identifiers.ViewContainerRef)))) {
this._hasViewContainer = true;
}
// create the providers that we know are eager first
this._allProviders.values().forEach((provider) => {
var eager = provider.eager || isPresent(queriedTokens.get(provider.token));
if (eager) {
this._getOrCreateLocalProvider(provider.providerType, provider.token, true);
}
});
}
afterElement() {
// collect lazy providers
this._allProviders.values().forEach((provider) => {
this._getOrCreateLocalProvider(provider.providerType, provider.token, false);
});
}
get transformProviders(): ProviderAst[] { return this._transformedProviders.values(); }
get transformedDirectiveAsts(): DirectiveAst[] {
var sortedProviderTypes =
this._transformedProviders.values().map(provider => provider.token.identifier);
var sortedDirectives = ListWrapper.clone(this._directiveAsts);
ListWrapper.sort(sortedDirectives,
(dir1, dir2) => sortedProviderTypes.indexOf(dir1.directive.type) -
sortedProviderTypes.indexOf(dir2.directive.type));
return sortedDirectives;
}
get transformedHasViewContainer(): boolean { return this._hasViewContainer; }
private _addQueryReadsTo(token: CompileTokenMetadata, queryReadTokens: CompileTokenMap<boolean>) {
this._getQueriesFor(token).forEach((query) => {
var queryReadToken = isPresent(query.read) ? query.read : token;
if (isBlank(queryReadTokens.get(queryReadToken))) {
queryReadTokens.add(queryReadToken, true);
}
});
}
private _getQueriesFor(token: CompileTokenMetadata): CompileQueryMetadata[] {
var result: CompileQueryMetadata[] = [];
var currentEl: ProviderElementContext = this;
var distance = 0;
var queries: CompileQueryMetadata[];
while (currentEl !== null) {
queries = currentEl._contentQueries.get(token);
if (isPresent(queries)) {
ListWrapper.addAll(result, queries.filter((query) => query.descendants || distance <= 1));
}
if (currentEl._directiveAsts.length > 0) {
distance++;
}
currentEl = currentEl._parent;
}
queries = this._viewContext.viewQueries.get(token);
if (isPresent(queries)) {
ListWrapper.addAll(result, queries);
}
return result;
}
private _getOrCreateLocalProvider(requestingProviderType: ProviderAstType,
token: CompileTokenMetadata, eager: boolean): ProviderAst {
var resolvedProvider = this._allProviders.get(token);
if (isBlank(resolvedProvider) ||
((requestingProviderType === ProviderAstType.Directive ||
requestingProviderType === ProviderAstType.PublicService) &&
resolvedProvider.providerType === ProviderAstType.PrivateService) ||
((requestingProviderType === ProviderAstType.PrivateService ||
requestingProviderType === ProviderAstType.PublicService) &&
resolvedProvider.providerType === ProviderAstType.Builtin)) {
return null;
}
var transformedProviderAst = this._transformedProviders.get(token);
if (isPresent(transformedProviderAst)) {
return transformedProviderAst;
}
if (isPresent(this._seenProviders.get(token))) {
this._viewContext.errors.push(new ProviderError(
`Cannot instantiate cyclic dependency! ${token.name}`, this._sourceSpan));
return null;
}
this._seenProviders.add(token, true);
var transformedProviders = resolvedProvider.providers.map((provider) => {
var transformedUseValue = provider.useValue;
var transformedUseExisting = provider.useExisting;
var transformedDeps;
if (isPresent(provider.useExisting)) {
var existingDiDep = this._getDependency(
resolvedProvider.providerType,
new CompileDiDependencyMetadata({token: provider.useExisting}), eager);
if (isPresent(existingDiDep.token)) {
transformedUseExisting = existingDiDep.token;
} else {
transformedUseExisting = null;
transformedUseValue = existingDiDep.value;
}
} else if (isPresent(provider.useFactory)) {
var deps = isPresent(provider.deps) ? provider.deps : provider.useFactory.diDeps;
transformedDeps =
deps.map((dep) => this._getDependency(resolvedProvider.providerType, dep, eager));
} else if (isPresent(provider.useClass)) {
var deps = isPresent(provider.deps) ? provider.deps : provider.useClass.diDeps;
transformedDeps =
deps.map((dep) => this._getDependency(resolvedProvider.providerType, dep, eager));
}
return _transformProvider(provider, {
useExisting: transformedUseExisting,
useValue: transformedUseValue,
deps: transformedDeps
});
});
transformedProviderAst =
_transformProviderAst(resolvedProvider, {eager: eager, providers: transformedProviders});
this._transformedProviders.add(token, transformedProviderAst);
return transformedProviderAst;
}
private _getLocalDependency(requestingProviderType: ProviderAstType,
dep: CompileDiDependencyMetadata,
eager: boolean = null): CompileDiDependencyMetadata {
if (dep.isAttribute) {
var attrValue = this._attrs[dep.token.value];
return new CompileDiDependencyMetadata({isValue: true, value: normalizeBlank(attrValue)});
}
if (isPresent(dep.query) || isPresent(dep.viewQuery)) {
return dep;
}
if (isPresent(dep.token)) {
// access builtints
if ((requestingProviderType === ProviderAstType.Directive ||
requestingProviderType === ProviderAstType.Component)) {
if (dep.token.equalsTo(identifierToken(Identifiers.Renderer)) ||
dep.token.equalsTo(identifierToken(Identifiers.ElementRef)) ||
dep.token.equalsTo(identifierToken(Identifiers.ChangeDetectorRef)) ||
dep.token.equalsTo(identifierToken(Identifiers.TemplateRef))) {
return dep;
}
if (dep.token.equalsTo(identifierToken(Identifiers.ViewContainerRef))) {
this._hasViewContainer = true;
}
}
// access the injector
if (dep.token.equalsTo(identifierToken(Identifiers.Injector))) {
return dep;
}
// access providers
if (isPresent(this._getOrCreateLocalProvider(requestingProviderType, dep.token, eager))) {
return dep;
}
}
return null;
}
private _getDependency(requestingProviderType: ProviderAstType, dep: CompileDiDependencyMetadata,
eager: boolean = null): CompileDiDependencyMetadata {
var currElement: ProviderElementContext = this;
var currEager: boolean = eager;
var result: CompileDiDependencyMetadata = null;
if (!dep.isSkipSelf) {
result = this._getLocalDependency(requestingProviderType, dep, eager);
}
if (dep.isSelf) {
if (isBlank(result) && dep.isOptional) {
result = new CompileDiDependencyMetadata({isValue: true, value: null});
}
} else {
// check parent elements
while (isBlank(result) && isPresent(currElement._parent)) {
var prevElement = currElement;
currElement = currElement._parent;
if (prevElement._isViewRoot) {
currEager = false;
}
result = currElement._getLocalDependency(ProviderAstType.PublicService, dep, currEager);
}
// check @Host restriction
if (isBlank(result)) {
if (!dep.isHost || this._viewContext.component.type.isHost ||
identifierToken(this._viewContext.component.type).equalsTo(dep.token) ||
isPresent(this._viewContext.viewProviders.get(dep.token))) {
result = dep;
} else {
result = dep.isOptional ?
result = new CompileDiDependencyMetadata({isValue: true, value: null}) :
null;
}
}
}
if (isBlank(result)) {
this._viewContext.errors.push(
new ProviderError(`No provider for ${dep.token.name}`, this._sourceSpan));
}
return result;
}
}
function _transformProvider(
provider: CompileProviderMetadata,
{useExisting, useValue, deps}:
{useExisting: CompileTokenMetadata, useValue: any, deps: CompileDiDependencyMetadata[]}) {
return new CompileProviderMetadata({
token: provider.token,
useClass: provider.useClass,
useExisting: useExisting,
useFactory: provider.useFactory,
useValue: useValue,
deps: deps,
multi: provider.multi
});
}
function _transformProviderAst(
provider: ProviderAst,
{eager, providers}: {eager: boolean, providers: CompileProviderMetadata[]}): ProviderAst {
return new ProviderAst(provider.token, provider.multiProvider, provider.eager || eager, providers,
provider.providerType, provider.sourceSpan);
}
function _normalizeProviders(
providers: Array<CompileProviderMetadata | CompileTypeMetadata | any[]>,
sourceSpan: ParseSourceSpan, targetErrors: ParseError[],
targetProviders: CompileProviderMetadata[] = null): CompileProviderMetadata[] {
if (isBlank(targetProviders)) {
targetProviders = [];
}
if (isPresent(providers)) {
providers.forEach((provider) => {
if (isArray(provider)) {
_normalizeProviders(<any[]>provider, sourceSpan, targetErrors, targetProviders);
} else {
var normalizeProvider: CompileProviderMetadata;
if (provider instanceof CompileProviderMetadata) {
normalizeProvider = provider;
} else if (provider instanceof CompileTypeMetadata) {
normalizeProvider = new CompileProviderMetadata(
{token: new CompileTokenMetadata({identifier: provider}), useClass: provider});
} else {
targetErrors.push(new ProviderError(`Unknown provider type ${provider}`, sourceSpan));
}
if (isPresent(normalizeProvider)) {
targetProviders.push(normalizeProvider);
}
}
});
}
return targetProviders;
}
function _resolveProvidersFromDirectives(directives: CompileDirectiveMetadata[],
sourceSpan: ParseSourceSpan,
targetErrors: ParseError[]): CompileTokenMap<ProviderAst> {
var providersByToken = new CompileTokenMap<ProviderAst>();
directives.forEach((directive) => {
var dirProvider = new CompileProviderMetadata(
{token: new CompileTokenMetadata({identifier: directive.type}), useClass: directive.type});
_resolveProviders([dirProvider],
directive.isComponent ? ProviderAstType.Component : ProviderAstType.Directive,
true, sourceSpan, targetErrors, providersByToken);
});
// Note: directives need to be able to overwrite providers of a component!
var directivesWithComponentFirst =
directives.filter(dir => dir.isComponent).concat(directives.filter(dir => !dir.isComponent));
directivesWithComponentFirst.forEach((directive) => {
_resolveProviders(_normalizeProviders(directive.providers, sourceSpan, targetErrors),
ProviderAstType.PublicService, false, sourceSpan, targetErrors,
providersByToken);
_resolveProviders(_normalizeProviders(directive.viewProviders, sourceSpan, targetErrors),
ProviderAstType.PrivateService, false, sourceSpan, targetErrors,
providersByToken);
});
return providersByToken;
}
function _resolveProviders(providers: CompileProviderMetadata[], providerType: ProviderAstType,
eager: boolean, sourceSpan: ParseSourceSpan, targetErrors: ParseError[],
targetProvidersByToken: CompileTokenMap<ProviderAst>) {
providers.forEach((provider) => {
var resolvedProvider = targetProvidersByToken.get(provider.token);
if (isPresent(resolvedProvider) && resolvedProvider.multiProvider !== provider.multi) {
targetErrors.push(new ProviderError(
`Mixing multi and non multi provider is not possible for token ${resolvedProvider.token.name}`,
sourceSpan));
}
if (isBlank(resolvedProvider)) {
resolvedProvider = new ProviderAst(provider.token, provider.multi, eager, [provider],
providerType, sourceSpan);
targetProvidersByToken.add(provider.token, resolvedProvider);
} else {
if (!provider.multi) {
ListWrapper.clear(resolvedProvider.providers);
}
resolvedProvider.providers.push(provider);
}
});
}
function _getViewQueries(
component: CompileDirectiveMetadata): CompileTokenMap<CompileQueryMetadata[]> {
var viewQueries = new CompileTokenMap<CompileQueryMetadata[]>();
if (isPresent(component.viewQueries)) {
component.viewQueries.forEach((query) => _addQueryToTokenMap(viewQueries, query));
}
component.type.diDeps.forEach((dep) => {
if (isPresent(dep.viewQuery)) {
_addQueryToTokenMap(viewQueries, dep.viewQuery);
}
});
return viewQueries;
}
function _getContentQueries(
directives: CompileDirectiveMetadata[]): CompileTokenMap<CompileQueryMetadata[]> {
var contentQueries = new CompileTokenMap<CompileQueryMetadata[]>();
directives.forEach(directive => {
if (isPresent(directive.queries)) {
directive.queries.forEach((query) => _addQueryToTokenMap(contentQueries, query));
}
directive.type.diDeps.forEach((dep) => {
if (isPresent(dep.query)) {
_addQueryToTokenMap(contentQueries, dep.query);
}
});
});
return contentQueries;
}
function _addQueryToTokenMap(map: CompileTokenMap<CompileQueryMetadata[]>,
query: CompileQueryMetadata) {
query.selectors.forEach((token: CompileTokenMetadata) => {
var entry = map.get(token);
if (isBlank(entry)) {
entry = [];
map.add(token, entry);
}
entry.push(query);
});
}

View File

@ -0,0 +1,238 @@
import {
IS_DART,
Type,
Json,
isBlank,
isPresent,
stringify,
evalExpression
} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {
ListWrapper,
SetWrapper,
MapWrapper,
StringMapWrapper
} from 'angular2/src/facade/collection';
import {PromiseWrapper} from 'angular2/src/facade/async';
import {
createHostComponentMeta,
CompileDirectiveMetadata,
CompileTypeMetadata,
CompileTemplateMetadata,
CompilePipeMetadata,
CompileMetadataWithType,
CompileIdentifierMetadata
} from './compile_metadata';
import {
TemplateAst,
TemplateAstVisitor,
NgContentAst,
EmbeddedTemplateAst,
ElementAst,
BoundEventAst,
BoundElementPropertyAst,
AttrAst,
BoundTextAst,
TextAst,
DirectiveAst,
BoundDirectivePropertyAst,
templateVisitAll
} from './template_ast';
import {Injectable} from 'angular2/src/core/di';
import {StyleCompiler, StylesCompileDependency, StylesCompileResult} from './style_compiler';
import {ViewCompiler} from './view_compiler/view_compiler';
import {TemplateParser} from './template_parser';
import {DirectiveNormalizer} from './directive_normalizer';
import {CompileMetadataResolver} from './metadata_resolver';
import {ComponentFactory} from 'angular2/src/core/linker/component_factory';
import {
ComponentResolver,
ReflectorComponentResolver
} from 'angular2/src/core/linker/component_resolver';
import {CompilerConfig} from './config';
import * as ir from './output/output_ast';
import {jitStatements} from './output/output_jit';
import {interpretStatements} from './output/output_interpreter';
import {InterpretiveAppViewInstanceFactory} from './output/interpretive_view';
import {XHR} from 'angular2/src/compiler/xhr';
/**
* An internal module of the Angular compiler that begins with component types,
* extracts templates, and eventually produces a compiled version of the component
* ready for linking into an application.
*/
@Injectable()
export class RuntimeCompiler implements ComponentResolver {
private _styleCache: Map<string, Promise<string>> = new Map<string, Promise<string>>();
private _hostCacheKeys = new Map<Type, any>();
private _compiledTemplateCache = new Map<any, CompiledTemplate>();
private _compiledTemplateDone = new Map<any, Promise<CompiledTemplate>>();
constructor(private _metadataResolver: CompileMetadataResolver,
private _templateNormalizer: DirectiveNormalizer,
private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler,
private _viewCompiler: ViewCompiler, private _xhr: XHR,
private _genConfig: CompilerConfig) {}
resolveComponent(componentType: Type): Promise<ComponentFactory<any>> {
var compMeta: CompileDirectiveMetadata =
this._metadataResolver.getDirectiveMetadata(componentType);
var hostCacheKey = this._hostCacheKeys.get(componentType);
if (isBlank(hostCacheKey)) {
hostCacheKey = new Object();
this._hostCacheKeys.set(componentType, hostCacheKey);
assertComponent(compMeta);
var hostMeta: CompileDirectiveMetadata =
createHostComponentMeta(compMeta.type, compMeta.selector);
this._loadAndCompileComponent(hostCacheKey, hostMeta, [compMeta], [], []);
}
return this._compiledTemplateDone.get(hostCacheKey)
.then((compiledTemplate: CompiledTemplate) => new ComponentFactory(
compMeta.selector, compiledTemplate.viewFactory, componentType));
}
clearCache() {
this._styleCache.clear();
this._compiledTemplateCache.clear();
this._compiledTemplateDone.clear();
this._hostCacheKeys.clear();
}
private _loadAndCompileComponent(cacheKey: any, compMeta: CompileDirectiveMetadata,
viewDirectives: CompileDirectiveMetadata[],
pipes: CompilePipeMetadata[],
compilingComponentsPath: any[]): CompiledTemplate {
var compiledTemplate = this._compiledTemplateCache.get(cacheKey);
var done = this._compiledTemplateDone.get(cacheKey);
if (isBlank(compiledTemplate)) {
compiledTemplate = new CompiledTemplate();
this._compiledTemplateCache.set(cacheKey, compiledTemplate);
done =
PromiseWrapper.all(
[<any>this._compileComponentStyles(compMeta)].concat(viewDirectives.map(
dirMeta => this._templateNormalizer.normalizeDirective(dirMeta))))
.then((stylesAndNormalizedViewDirMetas: any[]) => {
var normalizedViewDirMetas = stylesAndNormalizedViewDirMetas.slice(1);
var styles = stylesAndNormalizedViewDirMetas[0];
var parsedTemplate =
this._templateParser.parse(compMeta, compMeta.template.template,
normalizedViewDirMetas, pipes, compMeta.type.name);
var childPromises = [];
compiledTemplate.init(this._compileComponent(compMeta, parsedTemplate, styles,
pipes, compilingComponentsPath,
childPromises));
return PromiseWrapper.all(childPromises).then((_) => { return compiledTemplate; });
});
this._compiledTemplateDone.set(cacheKey, done);
}
return compiledTemplate;
}
private _compileComponent(compMeta: CompileDirectiveMetadata, parsedTemplate: TemplateAst[],
styles: string[], pipes: CompilePipeMetadata[],
compilingComponentsPath: any[],
childPromises: Promise<any>[]): Function {
var compileResult = this._viewCompiler.compileComponent(
compMeta, parsedTemplate,
new ir.ExternalExpr(new CompileIdentifierMetadata({runtime: styles})), pipes);
compileResult.dependencies.forEach((dep) => {
var childCompilingComponentsPath = ListWrapper.clone(compilingComponentsPath);
var childCacheKey = dep.comp.type.runtime;
var childViewDirectives: CompileDirectiveMetadata[] =
this._metadataResolver.getViewDirectivesMetadata(dep.comp.type.runtime);
var childViewPipes: CompilePipeMetadata[] =
this._metadataResolver.getViewPipesMetadata(dep.comp.type.runtime);
var childIsRecursive = ListWrapper.contains(childCompilingComponentsPath, childCacheKey);
childCompilingComponentsPath.push(childCacheKey);
var childComp =
this._loadAndCompileComponent(dep.comp.type.runtime, dep.comp, childViewDirectives,
childViewPipes, childCompilingComponentsPath);
dep.factoryPlaceholder.runtime = childComp.proxyViewFactory;
dep.factoryPlaceholder.name = `viewFactory_${dep.comp.type.name}`;
if (!childIsRecursive) {
// Only wait for a child if it is not a cycle
childPromises.push(this._compiledTemplateDone.get(childCacheKey));
}
});
var factory;
if (IS_DART || !this._genConfig.useJit) {
factory = interpretStatements(compileResult.statements, compileResult.viewFactoryVar,
new InterpretiveAppViewInstanceFactory());
} else {
factory = jitStatements(`${compMeta.type.name}.template.js`, compileResult.statements,
compileResult.viewFactoryVar);
}
return factory;
}
private _compileComponentStyles(compMeta: CompileDirectiveMetadata): Promise<string[]> {
var compileResult = this._styleCompiler.compileComponent(compMeta);
return this._resolveStylesCompileResult(compMeta.type.name, compileResult);
}
private _resolveStylesCompileResult(sourceUrl: string,
result: StylesCompileResult): Promise<string[]> {
var promises = result.dependencies.map((dep) => this._loadStylesheetDep(dep));
return PromiseWrapper.all(promises)
.then((cssTexts) => {
var nestedCompileResultPromises = [];
for (var i = 0; i < result.dependencies.length; i++) {
var dep = result.dependencies[i];
var cssText = cssTexts[i];
var nestedCompileResult =
this._styleCompiler.compileStylesheet(dep.sourceUrl, cssText, dep.isShimmed);
nestedCompileResultPromises.push(
this._resolveStylesCompileResult(dep.sourceUrl, nestedCompileResult));
}
return PromiseWrapper.all(nestedCompileResultPromises);
})
.then((nestedStylesArr) => {
for (var i = 0; i < result.dependencies.length; i++) {
var dep = result.dependencies[i];
dep.valuePlaceholder.runtime = nestedStylesArr[i];
dep.valuePlaceholder.name = `importedStyles${i}`;
}
if (IS_DART || !this._genConfig.useJit) {
return interpretStatements(result.statements, result.stylesVar,
new InterpretiveAppViewInstanceFactory());
} else {
return jitStatements(`${sourceUrl}.css.js`, result.statements, result.stylesVar);
}
});
}
private _loadStylesheetDep(dep: StylesCompileDependency): Promise<string> {
var cacheKey = `${dep.sourceUrl}${dep.isShimmed ? '.shim' : ''}`;
var cssTextPromise = this._styleCache.get(cacheKey);
if (isBlank(cssTextPromise)) {
cssTextPromise = this._xhr.get(dep.sourceUrl);
this._styleCache.set(cacheKey, cssTextPromise);
}
return cssTextPromise;
}
}
class CompiledTemplate {
viewFactory: Function = null;
proxyViewFactory: Function;
constructor() {
this.proxyViewFactory = (viewUtils, childInjector, contextEl) =>
this.viewFactory(viewUtils, childInjector, contextEl);
}
init(viewFactory: Function) { this.viewFactory = viewFactory; }
}
function assertComponent(meta: CompileDirectiveMetadata) {
if (!meta.isComponent) {
throw new BaseException(`Could not compile '${meta.type.name}' because it is not a component.`);
}
}

View File

@ -0,0 +1,44 @@
import {Injectable} from 'angular2/src/core/di';
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {splitNsName} from 'angular2/src/compiler/html_tags';
import {ElementSchemaRegistry} from './element_schema_registry';
const NAMESPACE_URIS =
/*@ts2dart_const*/
{'xlink': 'http://www.w3.org/1999/xlink', 'svg': 'http://www.w3.org/2000/svg'};
@Injectable()
export class DomElementSchemaRegistry extends ElementSchemaRegistry {
private _protoElements = new Map<string, Element>();
private _getProtoElement(tagName: string): Element {
var element = this._protoElements.get(tagName);
if (isBlank(element)) {
var nsAndName = splitNsName(tagName);
element = isPresent(nsAndName[0]) ?
DOM.createElementNS(NAMESPACE_URIS[nsAndName[0]], nsAndName[1]) :
DOM.createElement(nsAndName[1]);
this._protoElements.set(tagName, element);
}
return element;
}
hasProperty(tagName: string, propName: string): boolean {
if (tagName.indexOf('-') !== -1) {
// can't tell now as we don't know which properties a custom element will get
// once it is instantiated
return true;
} else {
var elm = this._getProtoElement(tagName);
return DOM.hasProperty(elm, propName);
}
}
getMappedPropName(propName: string): string {
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, propName);
return isPresent(mappedPropName) ? mappedPropName : propName;
}
}

View File

@ -0,0 +1,4 @@
export class ElementSchemaRegistry {
hasProperty(tagName: string, propName: string): boolean { return true; }
getMappedPropName(propName: string): string { return propName; }
}

View File

@ -0,0 +1,383 @@
import {Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {
isPresent,
isBlank,
RegExpWrapper,
RegExpMatcherWrapper,
StringWrapper
} from 'angular2/src/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
const _EMPTY_ATTR_VALUE = /*@ts2dart_const*/ '';
// TODO: Can't use `const` here as
// in Dart this is not transpiled into `final` yet...
var _SELECTOR_REGEXP = RegExpWrapper.create(
'(\\:not\\()|' + //":not("
'([-\\w]+)|' + // "tag"
'(?:\\.([-\\w]+))|' + // ".class"
'(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])|' + // "[name]", "[name=value]" or "[name*=value]"
'(\\))|' + // ")"
'(\\s*,\\s*)'); // ","
/**
* A css selector contains an element name,
* css classes and attribute/value pairs with the purpose
* of selecting subsets out of them.
*/
export class CssSelector {
element: string = null;
classNames: string[] = [];
attrs: string[] = [];
notSelectors: CssSelector[] = [];
static parse(selector: string): CssSelector[] {
var results: CssSelector[] = [];
var _addResult = (res: CssSelector[], cssSel) => {
if (cssSel.notSelectors.length > 0 && isBlank(cssSel.element) &&
ListWrapper.isEmpty(cssSel.classNames) && ListWrapper.isEmpty(cssSel.attrs)) {
cssSel.element = "*";
}
res.push(cssSel);
};
var cssSelector = new CssSelector();
var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector);
var match;
var current = cssSelector;
var inNot = false;
while (isPresent(match = RegExpMatcherWrapper.next(matcher))) {
if (isPresent(match[1])) {
if (inNot) {
throw new BaseException('Nesting :not is not allowed in a selector');
}
inNot = true;
current = new CssSelector();
cssSelector.notSelectors.push(current);
}
if (isPresent(match[2])) {
current.setElement(match[2]);
}
if (isPresent(match[3])) {
current.addClassName(match[3]);
}
if (isPresent(match[4])) {
current.addAttribute(match[4], match[5]);
}
if (isPresent(match[6])) {
inNot = false;
current = cssSelector;
}
if (isPresent(match[7])) {
if (inNot) {
throw new BaseException('Multiple selectors in :not are not supported');
}
_addResult(results, cssSelector);
cssSelector = current = new CssSelector();
}
}
_addResult(results, cssSelector);
return results;
}
isElementSelector(): boolean {
return isPresent(this.element) && ListWrapper.isEmpty(this.classNames) &&
ListWrapper.isEmpty(this.attrs) && this.notSelectors.length === 0;
}
setElement(element: string = null) { this.element = element; }
/** Gets a template string for an element that matches the selector. */
getMatchingElementTemplate(): string {
let tagName = isPresent(this.element) ? this.element : 'div';
let classAttr = this.classNames.length > 0 ? ` class="${this.classNames.join(' ')}"` : '';
let attrs = '';
for (let i = 0; i < this.attrs.length; i += 2) {
let attrName = this.attrs[i];
let attrValue = this.attrs[i + 1] !== '' ? `="${this.attrs[i + 1]}"` : '';
attrs += ` ${attrName}${attrValue}`;
}
return `<${tagName}${classAttr}${attrs}></${tagName}>`;
}
addAttribute(name: string, value: string = _EMPTY_ATTR_VALUE) {
this.attrs.push(name);
if (isPresent(value)) {
value = value.toLowerCase();
} else {
value = _EMPTY_ATTR_VALUE;
}
this.attrs.push(value);
}
addClassName(name: string) { this.classNames.push(name.toLowerCase()); }
toString(): string {
var res = '';
if (isPresent(this.element)) {
res += this.element;
}
if (isPresent(this.classNames)) {
for (var i = 0; i < this.classNames.length; i++) {
res += '.' + this.classNames[i];
}
}
if (isPresent(this.attrs)) {
for (var i = 0; i < this.attrs.length;) {
var attrName = this.attrs[i++];
var attrValue = this.attrs[i++];
res += '[' + attrName;
if (attrValue.length > 0) {
res += '=' + attrValue;
}
res += ']';
}
}
this.notSelectors.forEach(notSelector => res += `:not(${notSelector})`);
return res;
}
}
/**
* Reads a list of CssSelectors and allows to calculate which ones
* are contained in a given CssSelector.
*/
export class SelectorMatcher {
static createNotMatcher(notSelectors: CssSelector[]): SelectorMatcher {
var notMatcher = new SelectorMatcher();
notMatcher.addSelectables(notSelectors, null);
return notMatcher;
}
private _elementMap = new Map<string, SelectorContext[]>();
private _elementPartialMap = new Map<string, SelectorMatcher>();
private _classMap = new Map<string, SelectorContext[]>();
private _classPartialMap = new Map<string, SelectorMatcher>();
private _attrValueMap = new Map<string, Map<string, SelectorContext[]>>();
private _attrValuePartialMap = new Map<string, Map<string, SelectorMatcher>>();
private _listContexts: SelectorListContext[] = [];
addSelectables(cssSelectors: CssSelector[], callbackCtxt?: any) {
var listContext = null;
if (cssSelectors.length > 1) {
listContext = new SelectorListContext(cssSelectors);
this._listContexts.push(listContext);
}
for (var i = 0; i < cssSelectors.length; i++) {
this._addSelectable(cssSelectors[i], callbackCtxt, listContext);
}
}
/**
* Add an object that can be found later on by calling `match`.
* @param cssSelector A css selector
* @param callbackCtxt An opaque object that will be given to the callback of the `match` function
*/
private _addSelectable(cssSelector: CssSelector, callbackCtxt: any,
listContext: SelectorListContext) {
var matcher: SelectorMatcher = this;
var element = cssSelector.element;
var classNames = cssSelector.classNames;
var attrs = cssSelector.attrs;
var selectable = new SelectorContext(cssSelector, callbackCtxt, listContext);
if (isPresent(element)) {
var isTerminal = attrs.length === 0 && classNames.length === 0;
if (isTerminal) {
this._addTerminal(matcher._elementMap, element, selectable);
} else {
matcher = this._addPartial(matcher._elementPartialMap, element);
}
}
if (isPresent(classNames)) {
for (var index = 0; index < classNames.length; index++) {
var isTerminal = attrs.length === 0 && index === classNames.length - 1;
var className = classNames[index];
if (isTerminal) {
this._addTerminal(matcher._classMap, className, selectable);
} else {
matcher = this._addPartial(matcher._classPartialMap, className);
}
}
}
if (isPresent(attrs)) {
for (var index = 0; index < attrs.length;) {
var isTerminal = index === attrs.length - 2;
var attrName = attrs[index++];
var attrValue = attrs[index++];
if (isTerminal) {
var terminalMap = matcher._attrValueMap;
var terminalValuesMap = terminalMap.get(attrName);
if (isBlank(terminalValuesMap)) {
terminalValuesMap = new Map<string, SelectorContext[]>();
terminalMap.set(attrName, terminalValuesMap);
}
this._addTerminal(terminalValuesMap, attrValue, selectable);
} else {
var parttialMap = matcher._attrValuePartialMap;
var partialValuesMap = parttialMap.get(attrName);
if (isBlank(partialValuesMap)) {
partialValuesMap = new Map<string, SelectorMatcher>();
parttialMap.set(attrName, partialValuesMap);
}
matcher = this._addPartial(partialValuesMap, attrValue);
}
}
}
}
private _addTerminal(map: Map<string, SelectorContext[]>, name: string,
selectable: SelectorContext) {
var terminalList = map.get(name);
if (isBlank(terminalList)) {
terminalList = [];
map.set(name, terminalList);
}
terminalList.push(selectable);
}
private _addPartial(map: Map<string, SelectorMatcher>, name: string): SelectorMatcher {
var matcher = map.get(name);
if (isBlank(matcher)) {
matcher = new SelectorMatcher();
map.set(name, matcher);
}
return matcher;
}
/**
* Find the objects that have been added via `addSelectable`
* whose css selector is contained in the given css selector.
* @param cssSelector A css selector
* @param matchedCallback This callback will be called with the object handed into `addSelectable`
* @return boolean true if a match was found
*/
match(cssSelector: CssSelector, matchedCallback: (c: CssSelector, a: any) => void): boolean {
var result = false;
var element = cssSelector.element;
var classNames = cssSelector.classNames;
var attrs = cssSelector.attrs;
for (var i = 0; i < this._listContexts.length; i++) {
this._listContexts[i].alreadyMatched = false;
}
result = this._matchTerminal(this._elementMap, element, cssSelector, matchedCallback) || result;
result = this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) ||
result;
if (isPresent(classNames)) {
for (var index = 0; index < classNames.length; index++) {
var className = classNames[index];
result =
this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result;
result =
this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback) ||
result;
}
}
if (isPresent(attrs)) {
for (var index = 0; index < attrs.length;) {
var attrName = attrs[index++];
var attrValue = attrs[index++];
var terminalValuesMap = this._attrValueMap.get(attrName);
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) {
result = this._matchTerminal(terminalValuesMap, _EMPTY_ATTR_VALUE, cssSelector,
matchedCallback) ||
result;
}
result = this._matchTerminal(terminalValuesMap, attrValue, cssSelector, matchedCallback) ||
result;
var partialValuesMap = this._attrValuePartialMap.get(attrName);
if (!StringWrapper.equals(attrValue, _EMPTY_ATTR_VALUE)) {
result = this._matchPartial(partialValuesMap, _EMPTY_ATTR_VALUE, cssSelector,
matchedCallback) ||
result;
}
result =
this._matchPartial(partialValuesMap, attrValue, cssSelector, matchedCallback) || result;
}
}
return result;
}
/** @internal */
_matchTerminal(map: Map<string, SelectorContext[]>, name, cssSelector: CssSelector,
matchedCallback: (c: CssSelector, a: any) => void): boolean {
if (isBlank(map) || isBlank(name)) {
return false;
}
var selectables = map.get(name);
var starSelectables = map.get("*");
if (isPresent(starSelectables)) {
selectables = selectables.concat(starSelectables);
}
if (isBlank(selectables)) {
return false;
}
var selectable;
var result = false;
for (var index = 0; index < selectables.length; index++) {
selectable = selectables[index];
result = selectable.finalize(cssSelector, matchedCallback) || result;
}
return result;
}
/** @internal */
_matchPartial(map: Map<string, SelectorMatcher>, name, cssSelector: CssSelector,
matchedCallback /*: (c: CssSelector, a: any) => void*/): boolean {
if (isBlank(map) || isBlank(name)) {
return false;
}
var nestedSelector = map.get(name);
if (isBlank(nestedSelector)) {
return false;
}
// TODO(perf): get rid of recursion and measure again
// TODO(perf): don't pass the whole selector into the recursion,
// but only the not processed parts
return nestedSelector.match(cssSelector, matchedCallback);
}
}
export class SelectorListContext {
alreadyMatched: boolean = false;
constructor(public selectors: CssSelector[]) {}
}
// Store context to pass back selector and context when a selector is matched
export class SelectorContext {
notSelectors: CssSelector[];
constructor(public selector: CssSelector, public cbContext: any,
public listContext: SelectorListContext) {
this.notSelectors = selector.notSelectors;
}
finalize(cssSelector: CssSelector, callback: (c: CssSelector, a: any) => void): boolean {
var result = true;
if (this.notSelectors.length > 0 &&
(isBlank(this.listContext) || !this.listContext.alreadyMatched)) {
var notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors);
result = !notMatcher.match(cssSelector, null);
}
if (result && isPresent(callback) &&
(isBlank(this.listContext) || !this.listContext.alreadyMatched)) {
if (isPresent(this.listContext)) {
this.listContext.alreadyMatched = true;
}
callback(this.selector, this.cbContext);
}
return result;
}
}

View File

@ -0,0 +1,522 @@
import {ListWrapper} from 'angular2/src/facade/collection';
import {
StringWrapper,
RegExp,
RegExpWrapper,
RegExpMatcherWrapper,
isPresent,
isBlank
} from 'angular2/src/facade/lang';
/**
* This file is a port of shadowCSS from webcomponents.js to TypeScript.
*
* Please make sure to keep to edits in sync with the source file.
*
* Source:
* https://github.com/webcomponents/webcomponentsjs/blob/4efecd7e0e/src/ShadowCSS/ShadowCSS.js
*
* The original file level comment is reproduced below
*/
/*
This is a limited shim for ShadowDOM css styling.
https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#styles
The intention here is to support only the styling features which can be
relatively simply implemented. The goal is to allow users to avoid the
most obvious pitfalls and do so without compromising performance significantly.
For ShadowDOM styling that's not covered here, a set of best practices
can be provided that should allow users to accomplish more complex styling.
The following is a list of specific ShadowDOM styling features and a brief
discussion of the approach used to shim.
Shimmed features:
* :host, :host-context: ShadowDOM allows styling of the shadowRoot's host
element using the :host rule. To shim this feature, the :host styles are
reformatted and prefixed with a given scope name and promoted to a
document level stylesheet.
For example, given a scope name of .foo, a rule like this:
:host {
background: red;
}
}
becomes:
.foo {
background: red;
}
* encapsultion: Styles defined within ShadowDOM, apply only to
dom inside the ShadowDOM. Polymer uses one of two techniques to implement
this feature.
By default, rules are prefixed with the host element tag name
as a descendant selector. This ensures styling does not leak out of the 'top'
of the element's ShadowDOM. For example,
div {
font-weight: bold;
}
becomes:
x-foo div {
font-weight: bold;
}
becomes:
Alternatively, if WebComponents.ShadowCSS.strictStyling is set to true then
selectors are scoped by adding an attribute selector suffix to each
simple selector that contains the host element tag name. Each element
in the element's ShadowDOM template is also given the scope attribute.
Thus, these rules match only elements that have the scope attribute.
For example, given a scope name of x-foo, a rule like this:
div {
font-weight: bold;
}
becomes:
div[x-foo] {
font-weight: bold;
}
Note that elements that are dynamically added to a scope must have the scope
selector added to them manually.
* upper/lower bound encapsulation: Styles which are defined outside a
shadowRoot should not cross the ShadowDOM boundary and should not apply
inside a shadowRoot.
This styling behavior is not emulated. Some possible ways to do this that
were rejected due to complexity and/or performance concerns include: (1) reset
every possible property for every possible selector for a given scope name;
(2) re-implement css in javascript.
As an alternative, users should make sure to use selectors
specific to the scope in which they are working.
* ::distributed: This behavior is not emulated. It's often not necessary
to style the contents of a specific insertion point and instead, descendants
of the host element can be styled selectively. Users can also create an
extra node around an insertion point and style that node's contents
via descendent selectors. For example, with a shadowRoot like this:
<style>
::content(div) {
background: red;
}
</style>
<content></content>
could become:
<style>
/ *@polyfill .content-container div * /
::content(div) {
background: red;
}
</style>
<div class="content-container">
<content></content>
</div>
Note the use of @polyfill in the comment above a ShadowDOM specific style
declaration. This is a directive to the styling shim to use the selector
in comments in lieu of the next selector when running under polyfill.
*/
export class ShadowCss {
strictStyling: boolean = true;
constructor() {}
/*
* Shim some cssText with the given selector. Returns cssText that can
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
*
* When strictStyling is true:
* - selector is the attribute added to all elements inside the host,
* - hostSelector is the attribute added to the host itself.
*/
shimCssText(cssText: string, selector: string, hostSelector: string = ''): string {
cssText = stripComments(cssText);
cssText = this._insertDirectives(cssText);
return this._scopeCssText(cssText, selector, hostSelector);
}
private _insertDirectives(cssText: string): string {
cssText = this._insertPolyfillDirectivesInCssText(cssText);
return this._insertPolyfillRulesInCssText(cssText);
}
/*
* Process styles to convert native ShadowDOM rules that will trip
* up the css parser; we rely on decorating the stylesheet with inert rules.
*
* For example, we convert this rule:
*
* polyfill-next-selector { content: ':host menu-item'; }
* ::content menu-item {
*
* to this:
*
* scopeName menu-item {
*
**/
private _insertPolyfillDirectivesInCssText(cssText: string): string {
// Difference with webcomponents.js: does not handle comments
return StringWrapper.replaceAllMapped(cssText, _cssContentNextSelectorRe,
function(m) { return m[1] + '{'; });
}
/*
* Process styles to add rules which will only apply under the polyfill
*
* For example, we convert this rule:
*
* polyfill-rule {
* content: ':host menu-item';
* ...
* }
*
* to this:
*
* scopeName menu-item {...}
*
**/
private _insertPolyfillRulesInCssText(cssText: string): string {
// Difference with webcomponents.js: does not handle comments
return StringWrapper.replaceAllMapped(cssText, _cssContentRuleRe, function(m) {
var rule = m[0];
rule = StringWrapper.replace(rule, m[1], '');
rule = StringWrapper.replace(rule, m[2], '');
return m[3] + rule;
});
}
/* Ensure styles are scoped. Pseudo-scoping takes a rule like:
*
* .foo {... }
*
* and converts this to
*
* scopeName .foo { ... }
*/
private _scopeCssText(cssText: string, scopeSelector: string, hostSelector: string): string {
var unscoped = this._extractUnscopedRulesFromCssText(cssText);
cssText = this._insertPolyfillHostInCssText(cssText);
cssText = this._convertColonHost(cssText);
cssText = this._convertColonHostContext(cssText);
cssText = this._convertShadowDOMSelectors(cssText);
if (isPresent(scopeSelector)) {
cssText = this._scopeSelectors(cssText, scopeSelector, hostSelector);
}
cssText = cssText + '\n' + unscoped;
return cssText.trim();
}
/*
* Process styles to add rules which will only apply under the polyfill
* and do not process via CSSOM. (CSSOM is destructive to rules on rare
* occasions, e.g. -webkit-calc on Safari.)
* For example, we convert this rule:
*
* @polyfill-unscoped-rule {
* content: 'menu-item';
* ... }
*
* to this:
*
* menu-item {...}
*
**/
private _extractUnscopedRulesFromCssText(cssText: string): string {
// Difference with webcomponents.js: does not handle comments
var r = '', m;
var matcher = RegExpWrapper.matcher(_cssContentUnscopedRuleRe, cssText);
while (isPresent(m = RegExpMatcherWrapper.next(matcher))) {
var rule = m[0];
rule = StringWrapper.replace(rule, m[2], '');
rule = StringWrapper.replace(rule, m[1], m[3]);
r += rule + '\n\n';
}
return r;
}
/*
* convert a rule like :host(.foo) > .bar { }
*
* to
*
* scopeName.foo > .bar
*/
private _convertColonHost(cssText: string): string {
return this._convertColonRule(cssText, _cssColonHostRe, this._colonHostPartReplacer);
}
/*
* convert a rule like :host-context(.foo) > .bar { }
*
* to
*
* scopeName.foo > .bar, .foo scopeName > .bar { }
*
* and
*
* :host-context(.foo:host) .bar { ... }
*
* to
*
* scopeName.foo .bar { ... }
*/
private _convertColonHostContext(cssText: string): string {
return this._convertColonRule(cssText, _cssColonHostContextRe,
this._colonHostContextPartReplacer);
}
private _convertColonRule(cssText: string, regExp: RegExp, partReplacer: Function): string {
// p1 = :host, p2 = contents of (), p3 rest of rule
return StringWrapper.replaceAllMapped(cssText, regExp, function(m) {
if (isPresent(m[2])) {
var parts = m[2].split(','), r = [];
for (var i = 0; i < parts.length; i++) {
var p = parts[i];
if (isBlank(p)) break;
p = p.trim();
r.push(partReplacer(_polyfillHostNoCombinator, p, m[3]));
}
return r.join(',');
} else {
return _polyfillHostNoCombinator + m[3];
}
});
}
private _colonHostContextPartReplacer(host: string, part: string, suffix: string): string {
if (StringWrapper.contains(part, _polyfillHost)) {
return this._colonHostPartReplacer(host, part, suffix);
} else {
return host + part + suffix + ', ' + part + ' ' + host + suffix;
}
}
private _colonHostPartReplacer(host: string, part: string, suffix: string): string {
return host + StringWrapper.replace(part, _polyfillHost, '') + suffix;
}
/*
* Convert combinators like ::shadow and pseudo-elements like ::content
* by replacing with space.
*/
private _convertShadowDOMSelectors(cssText: string): string {
for (var i = 0; i < _shadowDOMSelectorsRe.length; i++) {
cssText = StringWrapper.replaceAll(cssText, _shadowDOMSelectorsRe[i], ' ');
}
return cssText;
}
// change a selector like 'div' to 'name div'
private _scopeSelectors(cssText: string, scopeSelector: string, hostSelector: string): string {
return processRules(cssText, (rule: CssRule) => {
var selector = rule.selector;
var content = rule.content;
if (rule.selector[0] != '@' || rule.selector.startsWith('@page')) {
selector =
this._scopeSelector(rule.selector, scopeSelector, hostSelector, this.strictStyling);
} else if (rule.selector.startsWith('@media')) {
content = this._scopeSelectors(rule.content, scopeSelector, hostSelector);
}
return new CssRule(selector, content);
});
}
private _scopeSelector(selector: string, scopeSelector: string, hostSelector: string,
strict: boolean): string {
var r = [], parts = selector.split(',');
for (var i = 0; i < parts.length; i++) {
var p = parts[i].trim();
var deepParts = StringWrapper.split(p, _shadowDeepSelectors);
var shallowPart = deepParts[0];
if (this._selectorNeedsScoping(shallowPart, scopeSelector)) {
deepParts[0] = strict && !StringWrapper.contains(shallowPart, _polyfillHostNoCombinator) ?
this._applyStrictSelectorScope(shallowPart, scopeSelector) :
this._applySelectorScope(shallowPart, scopeSelector, hostSelector);
}
// replace /deep/ with a space for child selectors
r.push(deepParts.join(' '));
}
return r.join(', ');
}
private _selectorNeedsScoping(selector: string, scopeSelector: string): boolean {
var re = this._makeScopeMatcher(scopeSelector);
return !isPresent(RegExpWrapper.firstMatch(re, selector));
}
private _makeScopeMatcher(scopeSelector: string): RegExp {
var lre = /\[/g;
var rre = /\]/g;
scopeSelector = StringWrapper.replaceAll(scopeSelector, lre, '\\[');
scopeSelector = StringWrapper.replaceAll(scopeSelector, rre, '\\]');
return RegExpWrapper.create('^(' + scopeSelector + ')' + _selectorReSuffix, 'm');
}
private _applySelectorScope(selector: string, scopeSelector: string,
hostSelector: string): string {
// Difference from webcomponentsjs: scopeSelector could not be an array
return this._applySimpleSelectorScope(selector, scopeSelector, hostSelector);
}
// scope via name and [is=name]
private _applySimpleSelectorScope(selector: string, scopeSelector: string,
hostSelector: string): string {
if (isPresent(RegExpWrapper.firstMatch(_polyfillHostRe, selector))) {
var replaceBy = this.strictStyling ? `[${hostSelector}]` : scopeSelector;
selector = StringWrapper.replace(selector, _polyfillHostNoCombinator, replaceBy);
return StringWrapper.replaceAll(selector, _polyfillHostRe, replaceBy + ' ');
} else {
return scopeSelector + ' ' + selector;
}
}
// return a selector with [name] suffix on each simple selector
// e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name] /** @internal */
private _applyStrictSelectorScope(selector: string, scopeSelector: string): string {
var isRe = /\[is=([^\]]*)\]/g;
scopeSelector = StringWrapper.replaceAllMapped(scopeSelector, isRe, (m) => m[1]);
var splits = [' ', '>', '+', '~'], scoped = selector, attrName = '[' + scopeSelector + ']';
for (var i = 0; i < splits.length; i++) {
var sep = splits[i];
var parts = scoped.split(sep);
scoped = parts.map(p => {
// remove :host since it should be unnecessary
var t = StringWrapper.replaceAll(p.trim(), _polyfillHostRe, '');
if (t.length > 0 && !ListWrapper.contains(splits, t) &&
!StringWrapper.contains(t, attrName)) {
var re = /([^:]*)(:*)(.*)/g;
var m = RegExpWrapper.firstMatch(re, t);
if (isPresent(m)) {
p = m[1] + attrName + m[2] + m[3];
}
}
return p;
})
.join(sep);
}
return scoped;
}
private _insertPolyfillHostInCssText(selector: string): string {
selector = StringWrapper.replaceAll(selector, _colonHostContextRe, _polyfillHostContext);
selector = StringWrapper.replaceAll(selector, _colonHostRe, _polyfillHost);
return selector;
}
}
var _cssContentNextSelectorRe =
/polyfill-next-selector[^}]*content:[\s]*?['"](.*?)['"][;\s]*}([^{]*?){/gim;
var _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim;
var _cssContentUnscopedRuleRe =
/(polyfill-unscoped-rule)[^}]*(content:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim;
var _polyfillHost = '-shadowcsshost';
// note: :host-context pre-processed to -shadowcsshostcontext.
var _polyfillHostContext = '-shadowcsscontext';
var _parenSuffix = ')(?:\\((' +
'(?:\\([^)(]*\\)|[^)(]*)+?' +
')\\))?([^,{]*)';
var _cssColonHostRe = RegExpWrapper.create('(' + _polyfillHost + _parenSuffix, 'im');
var _cssColonHostContextRe = RegExpWrapper.create('(' + _polyfillHostContext + _parenSuffix, 'im');
var _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
var _shadowDOMSelectorsRe = [
/::shadow/g,
/::content/g,
// Deprecated selectors
// TODO(vicb): see https://github.com/angular/clang-format/issues/16
// clang-format off
/\/shadow-deep\//g, // former /deep/
/\/shadow\//g, // former ::shadow
// clanf-format on
];
var _shadowDeepSelectors = /(?:>>>)|(?:\/deep\/)/g;
var _selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$';
var _polyfillHostRe = RegExpWrapper.create(_polyfillHost, 'im');
var _colonHostRe = /:host/gim;
var _colonHostContextRe = /:host-context/gim;
var _commentRe = /\/\*[\s\S]*?\*\//g;
function stripComments(input:string):string {
return StringWrapper.replaceAllMapped(input, _commentRe, (_) => '');
}
var _ruleRe = /(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g;
var _curlyRe = /([{}])/g;
const OPEN_CURLY = '{';
const CLOSE_CURLY = '}';
const BLOCK_PLACEHOLDER = '%BLOCK%';
export class CssRule {
constructor(public selector:string, public content:string) {}
}
export function processRules(input:string, ruleCallback:Function):string {
var inputWithEscapedBlocks = escapeBlocks(input);
var nextBlockIndex = 0;
return StringWrapper.replaceAllMapped(inputWithEscapedBlocks.escapedString, _ruleRe, function(m) {
var selector = m[2];
var content = '';
var suffix = m[4];
var contentPrefix = '';
if (isPresent(m[4]) && m[4].startsWith('{'+BLOCK_PLACEHOLDER)) {
content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
suffix = m[4].substring(BLOCK_PLACEHOLDER.length+1);
contentPrefix = '{';
}
var rule = ruleCallback(new CssRule(selector, content));
return `${m[1]}${rule.selector}${m[3]}${contentPrefix}${rule.content}${suffix}`;
});
}
class StringWithEscapedBlocks {
constructor(public escapedString:string, public blocks:string[]) {}
}
function escapeBlocks(input:string):StringWithEscapedBlocks {
var inputParts = StringWrapper.split(input, _curlyRe);
var resultParts = [];
var escapedBlocks = [];
var bracketCount = 0;
var currentBlockParts = [];
for (var partIndex = 0; partIndex<inputParts.length; partIndex++) {
var part = inputParts[partIndex];
if (part == CLOSE_CURLY) {
bracketCount--;
}
if (bracketCount > 0) {
currentBlockParts.push(part);
} else {
if (currentBlockParts.length > 0) {
escapedBlocks.push(currentBlockParts.join(''));
resultParts.push(BLOCK_PLACEHOLDER);
currentBlockParts = [];
}
resultParts.push(part);
}
if (part == OPEN_CURLY) {
bracketCount++;
}
}
if (currentBlockParts.length > 0) {
escapedBlocks.push(currentBlockParts.join(''));
resultParts.push(BLOCK_PLACEHOLDER);
}
return new StringWithEscapedBlocks(resultParts.join(''), escapedBlocks);
}

View File

@ -0,0 +1,77 @@
import {
CompileTemplateMetadata,
CompileIdentifierMetadata,
CompileDirectiveMetadata
} from './compile_metadata';
import * as o from './output/output_ast';
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';
import {ShadowCss} from 'angular2/src/compiler/shadow_css';
import {UrlResolver} from 'angular2/src/compiler/url_resolver';
import {extractStyleUrls} from './style_url_resolver';
import {Injectable} from 'angular2/src/core/di';
import {isPresent} from 'angular2/src/facade/lang';
const COMPONENT_VARIABLE = '%COMP%';
const HOST_ATTR = /*@ts2dart_const*/ `_nghost-${COMPONENT_VARIABLE}`;
const CONTENT_ATTR = /*@ts2dart_const*/ `_ngcontent-${COMPONENT_VARIABLE}`;
export class StylesCompileDependency {
constructor(public sourceUrl: string, public isShimmed: boolean,
public valuePlaceholder: CompileIdentifierMetadata) {}
}
export class StylesCompileResult {
constructor(public statements: o.Statement[], public stylesVar: string,
public dependencies: StylesCompileDependency[]) {}
}
@Injectable()
export class StyleCompiler {
private _shadowCss: ShadowCss = new ShadowCss();
constructor(private _urlResolver: UrlResolver) {}
compileComponent(comp: CompileDirectiveMetadata): StylesCompileResult {
var shim = comp.template.encapsulation === ViewEncapsulation.Emulated;
return this._compileStyles(getStylesVarName(comp), comp.template.styles,
comp.template.styleUrls, shim);
}
compileStylesheet(stylesheetUrl: string, cssText: string,
isShimmed: boolean): StylesCompileResult {
var styleWithImports = extractStyleUrls(this._urlResolver, stylesheetUrl, cssText);
return this._compileStyles(getStylesVarName(null), [styleWithImports.style],
styleWithImports.styleUrls, isShimmed);
}
private _compileStyles(stylesVar: string, plainStyles: string[], absUrls: string[],
shim: boolean): StylesCompileResult {
var styleExpressions =
plainStyles.map(plainStyle => o.literal(this._shimIfNeeded(plainStyle, shim)));
var dependencies = [];
for (var i = 0; i < absUrls.length; i++) {
var identifier = new CompileIdentifierMetadata({name: getStylesVarName(null)});
dependencies.push(new StylesCompileDependency(absUrls[i], shim, identifier));
styleExpressions.push(new o.ExternalExpr(identifier));
}
// styles variable contains plain strings and arrays of other styles arrays (recursive),
// so we set its type to dynamic.
var stmt = o.variable(stylesVar)
.set(o.literalArr(styleExpressions,
new o.ArrayType(o.DYNAMIC_TYPE, [o.TypeModifier.Const])))
.toDeclStmt(null, [o.StmtModifier.Final]);
return new StylesCompileResult([stmt], stylesVar, dependencies);
}
private _shimIfNeeded(style: string, shim: boolean): string {
return shim ? this._shadowCss.shimCssText(style, CONTENT_ATTR, HOST_ATTR) : style;
}
}
function getStylesVarName(component: CompileDirectiveMetadata): string {
var result = `styles`;
if (isPresent(component)) {
result += `_${component.type.name}`;
}
return result;
}

View File

@ -0,0 +1,39 @@
// Some of the code comes from WebComponents.JS
// https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js
import {RegExp, RegExpWrapper, StringWrapper, isPresent, isBlank} from 'angular2/src/facade/lang';
import {UrlResolver} from 'angular2/src/compiler/url_resolver';
export class StyleWithImports {
constructor(public style: string, public styleUrls: string[]) {}
}
export function isStyleUrlResolvable(url: string): boolean {
if (isBlank(url) || url.length === 0 || url[0] == '/') return false;
var schemeMatch = RegExpWrapper.firstMatch(_urlWithSchemaRe, url);
return isBlank(schemeMatch) || schemeMatch[1] == 'package' || schemeMatch[1] == 'asset';
}
/**
* Rewrites stylesheets by resolving and removing the @import urls that
* are either relative or don't have a `package:` scheme
*/
export function extractStyleUrls(resolver: UrlResolver, baseUrl: string,
cssText: string): StyleWithImports {
var foundUrls = [];
var modifiedCssText = StringWrapper.replaceAllMapped(cssText, _cssImportRe, (m) => {
var url = isPresent(m[1]) ? m[1] : m[2];
if (!isStyleUrlResolvable(url)) {
// Do not attempt to resolve non-package absolute URLs with URI scheme
return m[0];
}
foundUrls.push(resolver.resolve(baseUrl, url));
return '';
});
return new StyleWithImports(modifiedCssText, foundUrls);
}
var _cssImportRe = /@import\s+(?:url\()?\s*(?:(?:['"]([^'"]*))|([^;\)\s]*))[^;]*;?/g;
// TODO: can't use /^[^:/?#.]+:/g due to clang-format bug:
// https://github.com/angular/angular/issues/4596
var _urlWithSchemaRe = /^([a-zA-Z\-\+\.]+):/g;

View File

@ -0,0 +1,250 @@
import {AST} from './expression_parser/ast';
import {isPresent} from 'angular2/src/facade/lang';
import {
CompileDirectiveMetadata,
CompileTokenMetadata,
CompileProviderMetadata,
CompileTokenMap,
CompileQueryMetadata
} from './compile_metadata';
import {ParseSourceSpan} from './parse_util';
/**
* An Abstract Syntax Tree node representing part of a parsed Angular template.
*/
export interface TemplateAst {
/**
* The source span from which this node was parsed.
*/
sourceSpan: ParseSourceSpan;
/**
* Visit this node and possibly transform it.
*/
visit(visitor: TemplateAstVisitor, context: any): any;
}
/**
* A segment of text within the template.
*/
export class TextAst implements TemplateAst {
constructor(public value: string, public ngContentIndex: number,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitText(this, context); }
}
/**
* A bound expression within the text of a template.
*/
export class BoundTextAst implements TemplateAst {
constructor(public value: AST, public ngContentIndex: number,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitBoundText(this, context);
}
}
/**
* A plain attribute on an element.
*/
export class AttrAst implements TemplateAst {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
}
/**
* A binding for an element property (e.g. `[property]="expression"`).
*/
export class BoundElementPropertyAst implements TemplateAst {
constructor(public name: string, public type: PropertyBindingType, public value: AST,
public unit: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElementProperty(this, context);
}
}
/**
* A binding for an element event (e.g. `(event)="handler()"`).
*/
export class BoundEventAst implements TemplateAst {
constructor(public name: string, public target: string, public handler: AST,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitEvent(this, context);
}
get fullName() {
if (isPresent(this.target)) {
return `${this.target}:${this.name}`;
} else {
return this.name;
}
}
}
/**
* A reference declaration on an element (e.g. `let someName="expression"`).
*/
export class ReferenceAst implements TemplateAst {
constructor(public name: string, public value: CompileTokenMetadata,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitReference(this, context);
}
}
/**
* A variable declaration on a <template> (e.g. `var-someName="someLocalName"`).
*/
export class VariableAst implements TemplateAst {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitVariable(this, context);
}
}
/**
* An element declaration in a template.
*/
export class ElementAst implements TemplateAst {
constructor(public name: string, public attrs: AttrAst[],
public inputs: BoundElementPropertyAst[], public outputs: BoundEventAst[],
public references: ReferenceAst[], public directives: DirectiveAst[],
public providers: ProviderAst[], public hasViewContainer: boolean,
public children: TemplateAst[], public ngContentIndex: number,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElement(this, context);
}
}
/**
* A `<template>` element included in an Angular template.
*/
export class EmbeddedTemplateAst implements TemplateAst {
constructor(public attrs: AttrAst[], public outputs: BoundEventAst[],
public references: ReferenceAst[], public variables: VariableAst[],
public directives: DirectiveAst[], public providers: ProviderAst[],
public hasViewContainer: boolean, public children: TemplateAst[],
public ngContentIndex: number, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitEmbeddedTemplate(this, context);
}
}
/**
* A directive property with a bound value (e.g. `*ngIf="condition").
*/
export class BoundDirectivePropertyAst implements TemplateAst {
constructor(public directiveName: string, public templateName: string, public value: AST,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitDirectiveProperty(this, context);
}
}
/**
* A directive declared on an element.
*/
export class DirectiveAst implements TemplateAst {
constructor(public directive: CompileDirectiveMetadata,
public inputs: BoundDirectivePropertyAst[],
public hostProperties: BoundElementPropertyAst[], public hostEvents: BoundEventAst[],
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitDirective(this, context);
}
}
/**
* A provider declared on an element
*/
export class ProviderAst implements TemplateAst {
constructor(public token: CompileTokenMetadata, public multiProvider: boolean,
public eager: boolean, public providers: CompileProviderMetadata[],
public providerType: ProviderAstType, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
// No visit method in the visitor for now...
return null;
}
}
export enum ProviderAstType {
PublicService,
PrivateService,
Component,
Directive,
Builtin
}
/**
* Position where content is to be projected (instance of `<ng-content>` in a template).
*/
export class NgContentAst implements TemplateAst {
constructor(public index: number, public ngContentIndex: number,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitNgContent(this, context);
}
}
/**
* Enumeration of types of property bindings.
*/
export enum PropertyBindingType {
/**
* A normal binding to a property (e.g. `[property]="expression"`).
*/
Property,
/**
* A binding to an element attribute (e.g. `[attr.name]="expression"`).
*/
Attribute,
/**
* A binding to a CSS class (e.g. `[class.name]="condition"`).
*/
Class,
/**
* A binding to a style rule (e.g. `[style.rule]="expression"`).
*/
Style
}
/**
* A visitor for {@link TemplateAst} trees that will process each node.
*/
export interface TemplateAstVisitor {
visitNgContent(ast: NgContentAst, context: any): any;
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any;
visitElement(ast: ElementAst, 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;
visitDirective(ast: DirectiveAst, context: any): any;
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any;
}
/**
* Visit every node in a list of {@link TemplateAst}s with the given {@link TemplateAstVisitor}.
*/
export function templateVisitAll(visitor: TemplateAstVisitor, asts: TemplateAst[],
context: any = null): any[] {
var result = [];
asts.forEach(ast => {
var astResult = ast.visit(visitor, context);
if (isPresent(astResult)) {
result.push(astResult);
}
});
return result;
}

View File

@ -0,0 +1,926 @@
import {
ListWrapper,
StringMapWrapper,
SetWrapper,
MapWrapper
} from 'angular2/src/facade/collection';
import {RegExpWrapper, isPresent, StringWrapper, isBlank, isArray} from 'angular2/src/facade/lang';
import {Injectable, Inject, OpaqueToken, Optional} from 'angular2/core';
import {Console} from 'angular2/src/core/console';
import {BaseException} from 'angular2/src/facade/exceptions';
import {
AST,
Interpolation,
ASTWithSource,
TemplateBinding,
RecursiveAstVisitor,
BindingPipe
} from './expression_parser/ast';
import {Parser} from './expression_parser/parser';
import {
CompileTokenMap,
CompileDirectiveMetadata,
CompilePipeMetadata,
CompileMetadataWithType,
CompileProviderMetadata,
CompileTokenMetadata,
CompileTypeMetadata
} from './compile_metadata';
import {HtmlParser} from './html_parser';
import {splitNsName, mergeNsAndName} from './html_tags';
import {ParseSourceSpan, ParseError, ParseLocation, ParseErrorLevel} from './parse_util';
import {MAX_INTERPOLATION_VALUES} from 'angular2/src/core/linker/view_utils';
import {
ElementAst,
BoundElementPropertyAst,
BoundEventAst,
ReferenceAst,
TemplateAst,
TemplateAstVisitor,
templateVisitAll,
TextAst,
BoundTextAst,
EmbeddedTemplateAst,
AttrAst,
NgContentAst,
PropertyBindingType,
DirectiveAst,
BoundDirectivePropertyAst,
ProviderAst,
ProviderAstType,
VariableAst
} from './template_ast';
import {CssSelector, SelectorMatcher} from 'angular2/src/compiler/selector';
import {ElementSchemaRegistry} from 'angular2/src/compiler/schema/element_schema_registry';
import {preparseElement, PreparsedElement, PreparsedElementType} from './template_preparser';
import {isStyleUrlResolvable} from './style_url_resolver';
import {
HtmlAstVisitor,
HtmlAst,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
HtmlExpansionAst,
HtmlExpansionCaseAst,
htmlVisitAll
} from './html_ast';
import {splitAtColon} from './util';
import {identifierToken, Identifiers} from './identifiers';
import {ProviderElementContext, ProviderViewContext} from './provider_parser';
// Group 1 = "bind-"
// Group 2 = "var-"
// Group 3 = "let-"
// Group 4 = "ref-/#"
// Group 5 = "on-"
// Group 6 = "bindon-"
// Group 7 = the identifier after "bind-", "var-/#", or "on-"
// Group 8 = identifier inside [()]
// Group 9 = identifier inside []
// Group 10 = identifier inside ()
var BIND_NAME_REGEXP =
/^(?:(?:(?:(bind-)|(var-)|(let-)|(ref-|#)|(on-)|(bindon-))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g;
const TEMPLATE_ELEMENT = 'template';
const TEMPLATE_ATTR = 'template';
const TEMPLATE_ATTR_PREFIX = '*';
const CLASS_ATTR = 'class';
var PROPERTY_PARTS_SEPARATOR = '.';
const ATTRIBUTE_PREFIX = 'attr';
const CLASS_PREFIX = 'class';
const STYLE_PREFIX = 'style';
var TEXT_CSS_SELECTOR = CssSelector.parse('*')[0];
/**
* Provides an array of {@link TemplateAstVisitor}s which will be used to transform
* parsed templates before compilation is invoked, allowing custom expression syntax
* and other advanced transformations.
*
* This is currently an internal-only feature and not meant for general use.
*/
export const TEMPLATE_TRANSFORMS: any = /*@ts2dart_const*/ new OpaqueToken('TemplateTransforms');
export class TemplateParseError extends ParseError {
constructor(message: string, span: ParseSourceSpan, level: ParseErrorLevel) {
super(span, message, level);
}
}
export class TemplateParseResult {
constructor(public templateAst?: TemplateAst[], public errors?: ParseError[]) {}
}
@Injectable()
export class TemplateParser {
constructor(private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry,
private _htmlParser: HtmlParser, private _console: Console,
@Optional() @Inject(TEMPLATE_TRANSFORMS) public transforms: TemplateAstVisitor[]) {}
parse(component: CompileDirectiveMetadata, template: string,
directives: CompileDirectiveMetadata[], pipes: CompilePipeMetadata[],
templateUrl: string): TemplateAst[] {
var result = this.tryParse(component, template, directives, pipes, templateUrl);
var warnings = result.errors.filter(error => error.level === ParseErrorLevel.WARNING);
var errors = result.errors.filter(error => error.level === ParseErrorLevel.FATAL);
if (warnings.length > 0) {
this._console.warn(`Template parse warnings:\n${warnings.join('\n')}`);
}
if (errors.length > 0) {
var errorString = errors.join('\n');
throw new BaseException(`Template parse errors:\n${errorString}`);
}
return result.templateAst;
}
tryParse(component: CompileDirectiveMetadata, template: string,
directives: CompileDirectiveMetadata[], pipes: CompilePipeMetadata[],
templateUrl: string): TemplateParseResult {
var htmlAstWithErrors = this._htmlParser.parse(template, templateUrl);
var errors: ParseError[] = htmlAstWithErrors.errors;
var result;
if (htmlAstWithErrors.rootNodes.length > 0) {
var uniqDirectives = <CompileDirectiveMetadata[]>removeDuplicates(directives);
var uniqPipes = <CompilePipeMetadata[]>removeDuplicates(pipes);
var providerViewContext =
new ProviderViewContext(component, htmlAstWithErrors.rootNodes[0].sourceSpan);
var parseVisitor = new TemplateParseVisitor(providerViewContext, uniqDirectives, uniqPipes,
this._exprParser, this._schemaRegistry);
result = htmlVisitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT);
errors = errors.concat(parseVisitor.errors).concat(providerViewContext.errors);
} else {
result = [];
}
if (errors.length > 0) {
return new TemplateParseResult(result, errors);
}
if (isPresent(this.transforms)) {
this.transforms.forEach(
(transform: TemplateAstVisitor) => { result = templateVisitAll(transform, result); });
}
return new TemplateParseResult(result, errors);
}
}
class TemplateParseVisitor implements HtmlAstVisitor {
selectorMatcher: SelectorMatcher;
errors: TemplateParseError[] = [];
directivesIndex = new Map<CompileDirectiveMetadata, number>();
ngContentCount: number = 0;
pipesByName: Map<string, CompilePipeMetadata>;
constructor(public providerViewContext: ProviderViewContext,
directives: CompileDirectiveMetadata[], pipes: CompilePipeMetadata[],
private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry) {
this.selectorMatcher = new SelectorMatcher();
ListWrapper.forEachWithIndex(directives,
(directive: CompileDirectiveMetadata, index: number) => {
var selector = CssSelector.parse(directive.selector);
this.selectorMatcher.addSelectables(selector, directive);
this.directivesIndex.set(directive, index);
});
this.pipesByName = new Map<string, CompilePipeMetadata>();
pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe));
}
private _reportError(message: string, sourceSpan: ParseSourceSpan,
level: ParseErrorLevel = ParseErrorLevel.FATAL) {
this.errors.push(new TemplateParseError(message, sourceSpan, level));
}
private _parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
var sourceInfo = sourceSpan.start.toString();
try {
var ast = this._exprParser.parseInterpolation(value, sourceInfo);
this._checkPipes(ast, sourceSpan);
if (isPresent(ast) &&
(<Interpolation>ast.ast).expressions.length > MAX_INTERPOLATION_VALUES) {
throw new BaseException(
`Only support at most ${MAX_INTERPOLATION_VALUES} interpolation values!`);
}
return ast;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
}
}
private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
var sourceInfo = sourceSpan.start.toString();
try {
var ast = this._exprParser.parseAction(value, sourceInfo);
this._checkPipes(ast, sourceSpan);
return ast;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
}
}
private _parseBinding(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
var sourceInfo = sourceSpan.start.toString();
try {
var ast = this._exprParser.parseBinding(value, sourceInfo);
this._checkPipes(ast, sourceSpan);
return ast;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
}
}
private _parseTemplateBindings(value: string, sourceSpan: ParseSourceSpan): TemplateBinding[] {
var sourceInfo = sourceSpan.start.toString();
try {
var bindingsResult = this._exprParser.parseTemplateBindings(value, sourceInfo);
bindingsResult.templateBindings.forEach((binding) => {
if (isPresent(binding.expression)) {
this._checkPipes(binding.expression, sourceSpan);
}
});
bindingsResult.warnings.forEach(
(warning) => { this._reportError(warning, sourceSpan, ParseErrorLevel.WARNING); });
return bindingsResult.templateBindings;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return [];
}
}
private _checkPipes(ast: ASTWithSource, sourceSpan: ParseSourceSpan) {
if (isPresent(ast)) {
var collector = new PipeCollector();
ast.visit(collector);
collector.pipes.forEach((pipeName) => {
if (!this.pipesByName.has(pipeName)) {
this._reportError(`The pipe '${pipeName}' could not be found`, sourceSpan);
}
});
}
}
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
visitText(ast: HtmlTextAst, parent: ElementContext): any {
var ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
var expr = this._parseInterpolation(ast.value, ast.sourceSpan);
if (isPresent(expr)) {
return new BoundTextAst(expr, ngContentIndex, ast.sourceSpan);
} else {
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
}
}
visitAttr(ast: HtmlAttrAst, contex: any): any {
return new AttrAst(ast.name, ast.value, ast.sourceSpan);
}
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
visitElement(element: HtmlElementAst, parent: ElementContext): any {
var nodeName = element.name;
var preparsedElement = preparseElement(element);
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
preparsedElement.type === PreparsedElementType.STYLE) {
// Skipping <script> for security reasons
// Skipping <style> as we already processed them
// in the StyleCompiler
return null;
}
if (preparsedElement.type === PreparsedElementType.STYLESHEET &&
isStyleUrlResolvable(preparsedElement.hrefAttr)) {
// Skipping stylesheets with either relative urls or package scheme as we already processed
// them in the StyleCompiler
return null;
}
var matchableAttrs: string[][] = [];
var elementOrDirectiveProps: BoundElementOrDirectiveProperty[] = [];
var elementOrDirectiveRefs: ElementOrDirectiveRef[] = [];
var elementVars: VariableAst[] = [];
var events: BoundEventAst[] = [];
var templateElementOrDirectiveProps: BoundElementOrDirectiveProperty[] = [];
var templateMatchableAttrs: string[][] = [];
var templateElementVars: VariableAst[] = [];
var hasInlineTemplates = false;
var attrs = [];
var lcElName = splitNsName(nodeName.toLowerCase())[1];
var isTemplateElement = lcElName == TEMPLATE_ELEMENT;
element.attrs.forEach(attr => {
var hasBinding =
this._parseAttr(isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, events,
elementOrDirectiveRefs, elementVars);
var hasTemplateBinding = this._parseInlineTemplateBinding(
attr, templateMatchableAttrs, templateElementOrDirectiveProps, templateElementVars);
if (!hasBinding && !hasTemplateBinding) {
// don't include the bindings as attributes as well in the AST
attrs.push(this.visitAttr(attr, null));
matchableAttrs.push([attr.name, attr.value]);
}
if (hasTemplateBinding) {
hasInlineTemplates = true;
}
});
var elementCssSelector = createElementCssSelector(nodeName, matchableAttrs);
var directiveMetas = this._parseDirectives(this.selectorMatcher, elementCssSelector);
var references: ReferenceAst[] = [];
var directiveAsts = this._createDirectiveAsts(isTemplateElement, element.name, directiveMetas,
elementOrDirectiveProps, elementOrDirectiveRefs,
element.sourceSpan, references);
var elementProps: BoundElementPropertyAst[] =
this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directiveAsts);
var isViewRoot = parent.isTemplateElement || hasInlineTemplates;
var providerContext =
new ProviderElementContext(this.providerViewContext, parent.providerContext, isViewRoot,
directiveAsts, attrs, references, element.sourceSpan);
var children = htmlVisitAll(
preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this, element.children,
ElementContext.create(isTemplateElement, directiveAsts,
isTemplateElement ? parent.providerContext : providerContext));
providerContext.afterElement();
// Override the actual selector when the `ngProjectAs` attribute is provided
var projectionSelector = isPresent(preparsedElement.projectAs) ?
CssSelector.parse(preparsedElement.projectAs)[0] :
elementCssSelector;
var ngContentIndex = parent.findNgContentIndex(projectionSelector);
var parsedElement;
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
if (isPresent(element.children) && element.children.length > 0) {
this._reportError(
`<ng-content> element cannot have content. <ng-content> must be immediately followed by </ng-content>`,
element.sourceSpan);
}
parsedElement = new NgContentAst(
this.ngContentCount++, hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
} else if (isTemplateElement) {
this._assertAllEventsPublishedByDirectives(directiveAsts, events);
this._assertNoComponentsNorElementBindingsOnTemplate(directiveAsts, elementProps,
element.sourceSpan);
parsedElement = new EmbeddedTemplateAst(
attrs, events, references, elementVars, providerContext.transformedDirectiveAsts,
providerContext.transformProviders, providerContext.transformedHasViewContainer, children,
hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
} else {
this._assertOnlyOneComponent(directiveAsts, element.sourceSpan);
let ngContentIndex =
hasInlineTemplates ? null : parent.findNgContentIndex(projectionSelector);
parsedElement = new ElementAst(
nodeName, attrs, elementProps, events, references,
providerContext.transformedDirectiveAsts, providerContext.transformProviders,
providerContext.transformedHasViewContainer, children,
hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
}
if (hasInlineTemplates) {
var templateCssSelector = createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs);
var templateDirectiveMetas = this._parseDirectives(this.selectorMatcher, templateCssSelector);
var templateDirectiveAsts =
this._createDirectiveAsts(true, element.name, templateDirectiveMetas,
templateElementOrDirectiveProps, [], element.sourceSpan, []);
var templateElementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts(
element.name, templateElementOrDirectiveProps, templateDirectiveAsts);
this._assertNoComponentsNorElementBindingsOnTemplate(
templateDirectiveAsts, templateElementProps, element.sourceSpan);
var templateProviderContext = new ProviderElementContext(
this.providerViewContext, parent.providerContext, parent.isTemplateElement,
templateDirectiveAsts, [], [], element.sourceSpan);
templateProviderContext.afterElement();
parsedElement = new EmbeddedTemplateAst([], [], [], templateElementVars,
templateProviderContext.transformedDirectiveAsts,
templateProviderContext.transformProviders,
templateProviderContext.transformedHasViewContainer,
[parsedElement], ngContentIndex, element.sourceSpan);
}
return parsedElement;
}
private _parseInlineTemplateBinding(attr: HtmlAttrAst, targetMatchableAttrs: string[][],
targetProps: BoundElementOrDirectiveProperty[],
targetVars: VariableAst[]): boolean {
var templateBindingsSource = null;
if (attr.name == TEMPLATE_ATTR) {
templateBindingsSource = attr.value;
} else if (attr.name.startsWith(TEMPLATE_ATTR_PREFIX)) {
var key = attr.name.substring(TEMPLATE_ATTR_PREFIX.length); // remove the star
templateBindingsSource = (attr.value.length == 0) ? key : key + ' ' + attr.value;
}
if (isPresent(templateBindingsSource)) {
var bindings = this._parseTemplateBindings(templateBindingsSource, attr.sourceSpan);
for (var i = 0; i < bindings.length; i++) {
var binding = bindings[i];
if (binding.keyIsVar) {
targetVars.push(new VariableAst(binding.key, binding.name, attr.sourceSpan));
} else if (isPresent(binding.expression)) {
this._parsePropertyAst(binding.key, binding.expression, attr.sourceSpan,
targetMatchableAttrs, targetProps);
} else {
targetMatchableAttrs.push([binding.key, '']);
this._parseLiteralAttr(binding.key, null, attr.sourceSpan, targetProps);
}
}
return true;
}
return false;
}
private _parseAttr(isTemplateElement: boolean, attr: HtmlAttrAst,
targetMatchableAttrs: string[][],
targetProps: BoundElementOrDirectiveProperty[], targetEvents: BoundEventAst[],
targetRefs: ElementOrDirectiveRef[], targetVars: VariableAst[]): boolean {
var attrName = this._normalizeAttributeName(attr.name);
var attrValue = attr.value;
var bindParts = RegExpWrapper.firstMatch(BIND_NAME_REGEXP, attrName);
var hasBinding = false;
if (isPresent(bindParts)) {
hasBinding = true;
if (isPresent(bindParts[1])) { // match: bind-prop
this._parseProperty(bindParts[7], attrValue, attr.sourceSpan, targetMatchableAttrs,
targetProps);
} else if (isPresent(bindParts[2])) { // match: var-name / var-name="iden"
var identifier = bindParts[7];
if (isTemplateElement) {
this._reportError(`"var-" on <template> elements is deprecated. Use "let-" instead!`,
attr.sourceSpan, ParseErrorLevel.WARNING);
this._parseVariable(identifier, attrValue, attr.sourceSpan, targetVars);
} else {
this._reportError(`"var-" on non <template> elements is deprecated. Use "ref-" instead!`,
attr.sourceSpan, ParseErrorLevel.WARNING);
this._parseReference(identifier, attrValue, attr.sourceSpan, targetRefs);
}
} else if (isPresent(bindParts[3])) { // match: let-name
if (isTemplateElement) {
var identifier = bindParts[7];
this._parseVariable(identifier, attrValue, attr.sourceSpan, targetVars);
} else {
this._reportError(`"let-" is only supported on template elements.`, attr.sourceSpan);
}
} else if (isPresent(bindParts[4])) { // match: ref- / #iden
var identifier = bindParts[7];
this._parseReference(identifier, attrValue, attr.sourceSpan, targetRefs);
} else if (isPresent(bindParts[5])) { // match: on-event
this._parseEvent(bindParts[7], attrValue, attr.sourceSpan, targetMatchableAttrs,
targetEvents);
} else if (isPresent(bindParts[6])) { // match: bindon-prop
this._parseProperty(bindParts[7], attrValue, attr.sourceSpan, targetMatchableAttrs,
targetProps);
this._parseAssignmentEvent(bindParts[7], attrValue, attr.sourceSpan, targetMatchableAttrs,
targetEvents);
} else if (isPresent(bindParts[8])) { // match: [(expr)]
this._parseProperty(bindParts[8], attrValue, attr.sourceSpan, targetMatchableAttrs,
targetProps);
this._parseAssignmentEvent(bindParts[8], attrValue, attr.sourceSpan, targetMatchableAttrs,
targetEvents);
} else if (isPresent(bindParts[9])) { // match: [expr]
this._parseProperty(bindParts[9], attrValue, attr.sourceSpan, targetMatchableAttrs,
targetProps);
} else if (isPresent(bindParts[10])) { // match: (event)
this._parseEvent(bindParts[10], attrValue, attr.sourceSpan, targetMatchableAttrs,
targetEvents);
}
} else {
hasBinding = this._parsePropertyInterpolation(attrName, attrValue, attr.sourceSpan,
targetMatchableAttrs, targetProps);
}
if (!hasBinding) {
this._parseLiteralAttr(attrName, attrValue, attr.sourceSpan, targetProps);
}
return hasBinding;
}
private _normalizeAttributeName(attrName: string): string {
return attrName.toLowerCase().startsWith('data-') ? attrName.substring(5) : attrName;
}
private _parseVariable(identifier: string, value: string, sourceSpan: ParseSourceSpan,
targetVars: VariableAst[]) {
if (identifier.indexOf('-') > -1) {
this._reportError(`"-" is not allowed in variable names`, sourceSpan);
}
targetVars.push(new VariableAst(identifier, value, sourceSpan));
}
private _parseReference(identifier: string, value: string, sourceSpan: ParseSourceSpan,
targetRefs: ElementOrDirectiveRef[]) {
if (identifier.indexOf('-') > -1) {
this._reportError(`"-" is not allowed in reference names`, sourceSpan);
}
targetRefs.push(new ElementOrDirectiveRef(identifier, value, sourceSpan));
}
private _parseProperty(name: string, expression: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][],
targetProps: BoundElementOrDirectiveProperty[]) {
this._parsePropertyAst(name, this._parseBinding(expression, sourceSpan), sourceSpan,
targetMatchableAttrs, targetProps);
}
private _parsePropertyInterpolation(name: string, value: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][],
targetProps: BoundElementOrDirectiveProperty[]): boolean {
var expr = this._parseInterpolation(value, sourceSpan);
if (isPresent(expr)) {
this._parsePropertyAst(name, expr, sourceSpan, targetMatchableAttrs, targetProps);
return true;
}
return false;
}
private _parsePropertyAst(name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][],
targetProps: BoundElementOrDirectiveProperty[]) {
targetMatchableAttrs.push([name, ast.source]);
targetProps.push(new BoundElementOrDirectiveProperty(name, ast, false, sourceSpan));
}
private _parseAssignmentEvent(name: string, expression: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
this._parseEvent(`${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs,
targetEvents);
}
private _parseEvent(name: string, expression: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
// long format: 'target: eventName'
var parts = splitAtColon(name, [null, name]);
var target = parts[0];
var eventName = parts[1];
var ast = this._parseAction(expression, sourceSpan);
targetMatchableAttrs.push([name, ast.source]);
targetEvents.push(new BoundEventAst(eventName, target, ast, sourceSpan));
// Don't detect directives for event names for now,
// so don't add the event name to the matchableAttrs
}
private _parseLiteralAttr(name: string, value: string, sourceSpan: ParseSourceSpan,
targetProps: BoundElementOrDirectiveProperty[]) {
targetProps.push(new BoundElementOrDirectiveProperty(
name, this._exprParser.wrapLiteralPrimitive(value, ''), true, sourceSpan));
}
private _parseDirectives(selectorMatcher: SelectorMatcher,
elementCssSelector: CssSelector): CompileDirectiveMetadata[] {
// Need to sort the directives so that we get consistent results throughout,
// as selectorMatcher uses Maps inside.
// Also dedupe directives as they might match more than one time!
var directives = ListWrapper.createFixedSize(this.directivesIndex.size);
selectorMatcher.match(elementCssSelector, (selector, directive) => {
directives[this.directivesIndex.get(directive)] = directive;
});
return directives.filter(dir => isPresent(dir));
}
private _createDirectiveAsts(isTemplateElement: boolean, elementName: string,
directives: CompileDirectiveMetadata[],
props: BoundElementOrDirectiveProperty[],
elementOrDirectiveRefs: ElementOrDirectiveRef[],
sourceSpan: ParseSourceSpan,
targetReferences: ReferenceAst[]): DirectiveAst[] {
var matchedReferences = new Set<string>();
var component: CompileDirectiveMetadata = null;
var directiveAsts = directives.map((directive: CompileDirectiveMetadata) => {
if (directive.isComponent) {
component = directive;
}
var hostProperties: BoundElementPropertyAst[] = [];
var hostEvents: BoundEventAst[] = [];
var directiveProperties: BoundDirectivePropertyAst[] = [];
this._createDirectiveHostPropertyAsts(elementName, directive.hostProperties, sourceSpan,
hostProperties);
this._createDirectiveHostEventAsts(directive.hostListeners, sourceSpan, hostEvents);
this._createDirectivePropertyAsts(directive.inputs, props, directiveProperties);
elementOrDirectiveRefs.forEach((elOrDirRef) => {
if ((elOrDirRef.value.length === 0 && directive.isComponent) ||
(directive.exportAs == elOrDirRef.value)) {
targetReferences.push(new ReferenceAst(elOrDirRef.name, identifierToken(directive.type),
elOrDirRef.sourceSpan));
matchedReferences.add(elOrDirRef.name);
}
});
return new DirectiveAst(directive, directiveProperties, hostProperties, hostEvents,
sourceSpan);
});
elementOrDirectiveRefs.forEach((elOrDirRef) => {
if (elOrDirRef.value.length > 0) {
if (!SetWrapper.has(matchedReferences, elOrDirRef.name)) {
this._reportError(`There is no directive with "exportAs" set to "${elOrDirRef.value}"`,
elOrDirRef.sourceSpan);
};
} else if (isBlank(component)) {
var refToken = null;
if (isTemplateElement) {
refToken = identifierToken(Identifiers.TemplateRef);
}
targetReferences.push(new ReferenceAst(elOrDirRef.name, refToken, elOrDirRef.sourceSpan));
}
});
return directiveAsts;
}
private _createDirectiveHostPropertyAsts(elementName: string, hostProps: {[key: string]: string},
sourceSpan: ParseSourceSpan,
targetPropertyAsts: BoundElementPropertyAst[]) {
if (isPresent(hostProps)) {
StringMapWrapper.forEach(hostProps, (expression: string, propName: string) => {
var exprAst = this._parseBinding(expression, sourceSpan);
targetPropertyAsts.push(
this._createElementPropertyAst(elementName, propName, exprAst, sourceSpan));
});
}
}
private _createDirectiveHostEventAsts(hostListeners: {[key: string]: string},
sourceSpan: ParseSourceSpan,
targetEventAsts: BoundEventAst[]) {
if (isPresent(hostListeners)) {
StringMapWrapper.forEach(hostListeners, (expression: string, propName: string) => {
this._parseEvent(propName, expression, sourceSpan, [], targetEventAsts);
});
}
}
private _createDirectivePropertyAsts(directiveProperties: {[key: string]: string},
boundProps: BoundElementOrDirectiveProperty[],
targetBoundDirectiveProps: BoundDirectivePropertyAst[]) {
if (isPresent(directiveProperties)) {
var boundPropsByName = new Map<string, BoundElementOrDirectiveProperty>();
boundProps.forEach(boundProp => {
var prevValue = boundPropsByName.get(boundProp.name);
if (isBlank(prevValue) || prevValue.isLiteral) {
// give [a]="b" a higher precedence than a="b" on the same element
boundPropsByName.set(boundProp.name, boundProp);
}
});
StringMapWrapper.forEach(directiveProperties, (elProp: string, dirProp: string) => {
var boundProp = boundPropsByName.get(elProp);
// Bindings are optional, so this binding only needs to be set up if an expression is given.
if (isPresent(boundProp)) {
targetBoundDirectiveProps.push(new BoundDirectivePropertyAst(
dirProp, boundProp.name, boundProp.expression, boundProp.sourceSpan));
}
});
}
}
private _createElementPropertyAsts(elementName: string, props: BoundElementOrDirectiveProperty[],
directives: DirectiveAst[]): BoundElementPropertyAst[] {
var boundElementProps: BoundElementPropertyAst[] = [];
var boundDirectivePropsIndex = new Map<string, BoundDirectivePropertyAst>();
directives.forEach((directive: DirectiveAst) => {
directive.inputs.forEach((prop: BoundDirectivePropertyAst) => {
boundDirectivePropsIndex.set(prop.templateName, prop);
});
});
props.forEach((prop: BoundElementOrDirectiveProperty) => {
if (!prop.isLiteral && isBlank(boundDirectivePropsIndex.get(prop.name))) {
boundElementProps.push(this._createElementPropertyAst(elementName, prop.name,
prop.expression, prop.sourceSpan));
}
});
return boundElementProps;
}
private _createElementPropertyAst(elementName: string, name: string, ast: AST,
sourceSpan: ParseSourceSpan): BoundElementPropertyAst {
var unit = null;
var bindingType;
var boundPropertyName;
var parts = name.split(PROPERTY_PARTS_SEPARATOR);
if (parts.length === 1) {
boundPropertyName = this._schemaRegistry.getMappedPropName(parts[0]);
bindingType = PropertyBindingType.Property;
if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName)) {
this._reportError(
`Can't bind to '${boundPropertyName}' since it isn't a known native property`,
sourceSpan);
}
} else {
if (parts[0] == ATTRIBUTE_PREFIX) {
boundPropertyName = parts[1];
let nsSeparatorIdx = boundPropertyName.indexOf(':');
if (nsSeparatorIdx > -1) {
let ns = boundPropertyName.substring(0, nsSeparatorIdx);
let name = boundPropertyName.substring(nsSeparatorIdx + 1);
boundPropertyName = mergeNsAndName(ns, name);
}
bindingType = PropertyBindingType.Attribute;
} else if (parts[0] == CLASS_PREFIX) {
boundPropertyName = parts[1];
bindingType = PropertyBindingType.Class;
} else if (parts[0] == STYLE_PREFIX) {
unit = parts.length > 2 ? parts[2] : null;
boundPropertyName = parts[1];
bindingType = PropertyBindingType.Style;
} else {
this._reportError(`Invalid property name '${name}'`, sourceSpan);
bindingType = null;
}
}
return new BoundElementPropertyAst(boundPropertyName, bindingType, ast, unit, sourceSpan);
}
private _findComponentDirectiveNames(directives: DirectiveAst[]): string[] {
var componentTypeNames: string[] = [];
directives.forEach(directive => {
var typeName = directive.directive.type.name;
if (directive.directive.isComponent) {
componentTypeNames.push(typeName);
}
});
return componentTypeNames;
}
private _assertOnlyOneComponent(directives: DirectiveAst[], sourceSpan: ParseSourceSpan) {
var componentTypeNames = this._findComponentDirectiveNames(directives);
if (componentTypeNames.length > 1) {
this._reportError(`More than one component: ${componentTypeNames.join(',')}`, sourceSpan);
}
}
private _assertNoComponentsNorElementBindingsOnTemplate(directives: DirectiveAst[],
elementProps: BoundElementPropertyAst[],
sourceSpan: ParseSourceSpan) {
var componentTypeNames: string[] = this._findComponentDirectiveNames(directives);
if (componentTypeNames.length > 0) {
this._reportError(`Components on an embedded template: ${componentTypeNames.join(',')}`,
sourceSpan);
}
elementProps.forEach(prop => {
this._reportError(
`Property binding ${prop.name} not used by any directive on an embedded template`,
sourceSpan);
});
}
private _assertAllEventsPublishedByDirectives(directives: DirectiveAst[],
events: BoundEventAst[]) {
var allDirectiveEvents = new Set<string>();
directives.forEach(directive => {
StringMapWrapper.forEach(directive.directive.outputs,
(eventName: string, _) => { allDirectiveEvents.add(eventName); });
});
events.forEach(event => {
if (isPresent(event.target) || !SetWrapper.has(allDirectiveEvents, event.name)) {
this._reportError(
`Event binding ${event.fullName} not emitted by any directive on an embedded template`,
event.sourceSpan);
}
});
}
}
class NonBindableVisitor implements HtmlAstVisitor {
visitElement(ast: HtmlElementAst, parent: ElementContext): ElementAst {
var preparsedElement = preparseElement(ast);
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
preparsedElement.type === PreparsedElementType.STYLE ||
preparsedElement.type === PreparsedElementType.STYLESHEET) {
// Skipping <script> for security reasons
// Skipping <style> and stylesheets as we already processed them
// in the StyleCompiler
return null;
}
var attrNameAndValues = ast.attrs.map(attrAst => [attrAst.name, attrAst.value]);
var selector = createElementCssSelector(ast.name, attrNameAndValues);
var ngContentIndex = parent.findNgContentIndex(selector);
var children = htmlVisitAll(this, ast.children, EMPTY_ELEMENT_CONTEXT);
return new ElementAst(ast.name, htmlVisitAll(this, ast.attrs), [], [], [], [], [], false,
children, ngContentIndex, ast.sourceSpan);
}
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
visitAttr(ast: HtmlAttrAst, context: any): AttrAst {
return new AttrAst(ast.name, ast.value, ast.sourceSpan);
}
visitText(ast: HtmlTextAst, parent: ElementContext): TextAst {
var ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
}
visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; }
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
}
class BoundElementOrDirectiveProperty {
constructor(public name: string, public expression: AST, public isLiteral: boolean,
public sourceSpan: ParseSourceSpan) {}
}
class ElementOrDirectiveRef {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
}
export function splitClasses(classAttrValue: string): string[] {
return StringWrapper.split(classAttrValue.trim(), /\s+/g);
}
class ElementContext {
static create(isTemplateElement: boolean, directives: DirectiveAst[],
providerContext: ProviderElementContext): ElementContext {
var matcher = new SelectorMatcher();
var wildcardNgContentIndex = null;
var component = directives.find(directive => directive.directive.isComponent);
if (isPresent(component)) {
var ngContentSelectors = component.directive.template.ngContentSelectors;
for (var i = 0; i < ngContentSelectors.length; i++) {
var selector = ngContentSelectors[i];
if (StringWrapper.equals(selector, '*')) {
wildcardNgContentIndex = i;
} else {
matcher.addSelectables(CssSelector.parse(ngContentSelectors[i]), i);
}
}
}
return new ElementContext(isTemplateElement, matcher, wildcardNgContentIndex, providerContext);
}
constructor(public isTemplateElement: boolean, private _ngContentIndexMatcher: SelectorMatcher,
private _wildcardNgContentIndex: number,
public providerContext: ProviderElementContext) {}
findNgContentIndex(selector: CssSelector): number {
var ngContentIndices = [];
this._ngContentIndexMatcher.match(
selector, (selector, ngContentIndex) => { ngContentIndices.push(ngContentIndex); });
ListWrapper.sort(ngContentIndices);
if (isPresent(this._wildcardNgContentIndex)) {
ngContentIndices.push(this._wildcardNgContentIndex);
}
return ngContentIndices.length > 0 ? ngContentIndices[0] : null;
}
}
function createElementCssSelector(elementName: string, matchableAttrs: string[][]): CssSelector {
var cssSelector = new CssSelector();
let elNameNoNs = splitNsName(elementName)[1];
cssSelector.setElement(elNameNoNs);
for (var i = 0; i < matchableAttrs.length; i++) {
let attrName = matchableAttrs[i][0];
let attrNameNoNs = splitNsName(attrName)[1];
let attrValue = matchableAttrs[i][1];
cssSelector.addAttribute(attrNameNoNs, attrValue);
if (attrName.toLowerCase() == CLASS_ATTR) {
var classes = splitClasses(attrValue);
classes.forEach(className => cssSelector.addClassName(className));
}
}
return cssSelector;
}
var EMPTY_ELEMENT_CONTEXT = new ElementContext(true, new SelectorMatcher(), null, null);
var NON_BINDABLE_VISITOR = new NonBindableVisitor();
export class PipeCollector extends RecursiveAstVisitor {
pipes: Set<string> = new Set<string>();
visitPipe(ast: BindingPipe, context: any): any {
this.pipes.add(ast.name);
ast.exp.visit(this);
this.visitAll(ast.args, context);
return null;
}
}
function removeDuplicates(items: CompileMetadataWithType[]): CompileMetadataWithType[] {
let res = [];
items.forEach(item => {
let hasMatch =
res.filter(r => r.type.name == item.type.name && r.type.moduleUrl == item.type.moduleUrl &&
r.type.runtime == item.type.runtime)
.length > 0;
if (!hasMatch) {
res.push(item);
}
});
return res;
}

View File

@ -0,0 +1,72 @@
import {HtmlElementAst} from './html_ast';
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {splitNsName} from './html_tags';
const NG_CONTENT_SELECT_ATTR = 'select';
const NG_CONTENT_ELEMENT = 'ng-content';
const LINK_ELEMENT = 'link';
const LINK_STYLE_REL_ATTR = 'rel';
const LINK_STYLE_HREF_ATTR = 'href';
const LINK_STYLE_REL_VALUE = 'stylesheet';
const STYLE_ELEMENT = 'style';
const SCRIPT_ELEMENT = 'script';
const NG_NON_BINDABLE_ATTR = 'ngNonBindable';
const NG_PROJECT_AS = 'ngProjectAs';
export function preparseElement(ast: HtmlElementAst): PreparsedElement {
var selectAttr = null;
var hrefAttr = null;
var relAttr = null;
var nonBindable = false;
var projectAs: string = null;
ast.attrs.forEach(attr => {
let lcAttrName = attr.name.toLowerCase();
if (lcAttrName == NG_CONTENT_SELECT_ATTR) {
selectAttr = attr.value;
} else if (lcAttrName == LINK_STYLE_HREF_ATTR) {
hrefAttr = attr.value;
} else if (lcAttrName == LINK_STYLE_REL_ATTR) {
relAttr = attr.value;
} else if (attr.name == NG_NON_BINDABLE_ATTR) {
nonBindable = true;
} else if (attr.name == NG_PROJECT_AS) {
if (attr.value.length > 0) {
projectAs = attr.value;
}
}
});
selectAttr = normalizeNgContentSelect(selectAttr);
var nodeName = ast.name.toLowerCase();
var type = PreparsedElementType.OTHER;
if (splitNsName(nodeName)[1] == NG_CONTENT_ELEMENT) {
type = PreparsedElementType.NG_CONTENT;
} else if (nodeName == STYLE_ELEMENT) {
type = PreparsedElementType.STYLE;
} else if (nodeName == SCRIPT_ELEMENT) {
type = PreparsedElementType.SCRIPT;
} else if (nodeName == LINK_ELEMENT && relAttr == LINK_STYLE_REL_VALUE) {
type = PreparsedElementType.STYLESHEET;
}
return new PreparsedElement(type, selectAttr, hrefAttr, nonBindable, projectAs);
}
export enum PreparsedElementType {
NG_CONTENT,
STYLE,
STYLESHEET,
SCRIPT,
OTHER
}
export class PreparsedElement {
constructor(public type: PreparsedElementType, public selectAttr: string, public hrefAttr: string,
public nonBindable: boolean, public projectAs: string) {}
}
function normalizeNgContentSelect(selectAttr: string): string {
if (isBlank(selectAttr) || selectAttr.length === 0) {
return '*';
}
return selectAttr;
}

View File

@ -0,0 +1,69 @@
library angular2.src.services.url_resolver;
import 'package:angular2/src/core/di.dart' show Injectable, Inject, Provider;
import 'package:angular2/src/facade/lang.dart' show isPresent, StringWrapper;
import 'package:angular2/src/core/application_tokens.dart' show PACKAGE_ROOT_URL;
const _ASSET_SCHEME = 'asset:';
UrlResolver createUrlResolverWithoutPackagePrefix() {
return new UrlResolver.withUrlPrefix(null);
}
UrlResolver createOfflineCompileUrlResolver() {
return new UrlResolver.withUrlPrefix(_ASSET_SCHEME);
}
const DEFAULT_PACKAGE_URL_PROVIDER = const Provider(PACKAGE_ROOT_URL, useValue: "/packages");
@Injectable()
class UrlResolver {
/// This will be the location where 'package:' Urls will resolve. Default is
/// '/packages'
final String _packagePrefix;
UrlResolver([@Inject(PACKAGE_ROOT_URL) this._packagePrefix]);
/// Creates a UrlResolver that will resolve 'package:' Urls to a different
/// prefixed location.
const UrlResolver.withUrlPrefix(this._packagePrefix);
/**
* Resolves the `url` given the `baseUrl`:
* - when the `url` is null, the `baseUrl` is returned,
* - if `url` is relative ('path/to/here', './path/to/here'), the resolved url is a combination of
* `baseUrl` and `url`,
* - if `url` is absolute (it has a scheme: 'http://', 'https://' or start with '/'), the `url` is
* returned as is (ignoring the `baseUrl`)
*
* @param {string} baseUrl
* @param {string} url
* @returns {string} the resolved URL
*/
String resolve(String baseUrl, String url) {
Uri uri = Uri.parse(url);
if (isPresent(baseUrl) && baseUrl.length > 0) {
Uri baseUri = Uri.parse(baseUrl);
uri = baseUri.resolveUri(uri);
}
var prefix = this._packagePrefix;
if (prefix != null && uri.scheme == 'package') {
if (prefix == _ASSET_SCHEME) {
var pathSegments = uri.pathSegments.toList()..insert(1, 'lib');
return new Uri(scheme: 'asset', pathSegments: pathSegments).toString();
} else {
prefix = StringWrapper.stripRight(prefix, '/');
var path = StringWrapper.stripLeft(uri.path, '/');
return '$prefix/$path';
}
} else {
return uri.toString();
}
}
}
String getUrlScheme(String url) {
return Uri.parse(url).scheme;
}

View File

@ -0,0 +1,357 @@
import {Provider, Injectable, Inject} from 'angular2/src/core/di';
import {
StringWrapper,
isPresent,
isBlank,
RegExpWrapper,
normalizeBlank
} from 'angular2/src/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
import {ListWrapper} from 'angular2/src/facade/collection';
import {PACKAGE_ROOT_URL} from 'angular2/src/core/application_tokens';
const _ASSET_SCHEME = 'asset:';
/**
* Create a {@link UrlResolver} with no package prefix.
*/
export function createUrlResolverWithoutPackagePrefix(): UrlResolver {
return new UrlResolver();
}
export function createOfflineCompileUrlResolver(): UrlResolver {
return new UrlResolver(_ASSET_SCHEME);
}
/**
* A default provider for {@link PACKAGE_ROOT_URL} that maps to '/'.
*/
export var DEFAULT_PACKAGE_URL_PROVIDER = {
provide: PACKAGE_ROOT_URL,
useValue: "/"
};
/**
* Used by the {@link Compiler} when resolving HTML and CSS template URLs.
*
* This class can be overridden by the application developer to create custom behavior.
*
* See {@link Compiler}
*
* ## Example
*
* {@example compiler/ts/url_resolver/url_resolver.ts region='url_resolver'}
*/
@Injectable()
export class UrlResolver {
constructor(@Inject(PACKAGE_ROOT_URL) private _packagePrefix: string = null) {}
/**
* Resolves the `url` given the `baseUrl`:
* - when the `url` is null, the `baseUrl` is returned,
* - if `url` is relative ('path/to/here', './path/to/here'), the resolved url is a combination of
* `baseUrl` and `url`,
* - if `url` is absolute (it has a scheme: 'http://', 'https://' or start with '/'), the `url` is
* returned as is (ignoring the `baseUrl`)
*
* @param {string} baseUrl
* @param {string} url
* @returns {string} the resolved URL
*/
resolve(baseUrl: string, url: string): string {
var resolvedUrl = url;
if (isPresent(baseUrl) && baseUrl.length > 0) {
resolvedUrl = _resolveUrl(baseUrl, resolvedUrl);
}
var resolvedParts = _split(resolvedUrl);
var prefix = this._packagePrefix;
if (isPresent(prefix) && isPresent(resolvedParts) &&
resolvedParts[_ComponentIndex.Scheme] == "package") {
var path = resolvedParts[_ComponentIndex.Path];
if (this._packagePrefix === _ASSET_SCHEME) {
var pathSegements = path.split(/\//);
resolvedUrl = `asset:${pathSegements[0]}/lib/${pathSegements.slice(1).join('/')}`;
} else {
prefix = StringWrapper.stripRight(prefix, '/');
path = StringWrapper.stripLeft(path, '/');
return `${prefix}/${path}`;
}
}
return resolvedUrl;
}
}
/**
* Extract the scheme of a URL.
*/
export function getUrlScheme(url: string): string {
var match = _split(url);
return (match && match[_ComponentIndex.Scheme]) || "";
}
// The code below is adapted from Traceur:
// https://github.com/google/traceur-compiler/blob/9511c1dafa972bf0de1202a8a863bad02f0f95a8/src/runtime/url.js
/**
* Builds a URI string from already-encoded parts.
*
* No encoding is performed. Any component may be omitted as either null or
* undefined.
*
* @param {?string=} opt_scheme The scheme such as 'http'.
* @param {?string=} opt_userInfo The user name before the '@'.
* @param {?string=} opt_domain The domain such as 'www.google.com', already
* URI-encoded.
* @param {(string|null)=} opt_port The port number.
* @param {?string=} opt_path The path, already URI-encoded. If it is not
* empty, it must begin with a slash.
* @param {?string=} opt_queryData The URI-encoded query data.
* @param {?string=} opt_fragment The URI-encoded fragment identifier.
* @return {string} The fully combined URI.
*/
function _buildFromEncodedParts(opt_scheme?: string, opt_userInfo?: string, opt_domain?: string,
opt_port?: string, opt_path?: string, opt_queryData?: string,
opt_fragment?: string): string {
var out = [];
if (isPresent(opt_scheme)) {
out.push(opt_scheme + ':');
}
if (isPresent(opt_domain)) {
out.push('//');
if (isPresent(opt_userInfo)) {
out.push(opt_userInfo + '@');
}
out.push(opt_domain);
if (isPresent(opt_port)) {
out.push(':' + opt_port);
}
}
if (isPresent(opt_path)) {
out.push(opt_path);
}
if (isPresent(opt_queryData)) {
out.push('?' + opt_queryData);
}
if (isPresent(opt_fragment)) {
out.push('#' + opt_fragment);
}
return out.join('');
}
/**
* A regular expression for breaking a URI into its component parts.
*
* {@link http://www.gbiv.com/protocols/uri/rfc/rfc3986.html#RFC2234} says
* As the "first-match-wins" algorithm is identical to the "greedy"
* disambiguation method used by POSIX regular expressions, it is natural and
* commonplace to use a regular expression for parsing the potential five
* components of a URI reference.
*
* The following line is the regular expression for breaking-down a
* well-formed URI reference into its components.
*
* <pre>
* ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
* 12 3 4 5 6 7 8 9
* </pre>
*
* The numbers in the second line above are only to assist readability; they
* indicate the reference points for each subexpression (i.e., each paired
* parenthesis). We refer to the value matched for subexpression <n> as $<n>.
* For example, matching the above expression to
* <pre>
* http://www.ics.uci.edu/pub/ietf/uri/#Related
* </pre>
* results in the following subexpression matches:
* <pre>
* $1 = http:
* $2 = http
* $3 = //www.ics.uci.edu
* $4 = www.ics.uci.edu
* $5 = /pub/ietf/uri/
* $6 = <undefined>
* $7 = <undefined>
* $8 = #Related
* $9 = Related
* </pre>
* where <undefined> indicates that the component is not present, as is the
* case for the query component in the above example. Therefore, we can
* determine the value of the five components as
* <pre>
* scheme = $2
* authority = $4
* path = $5
* query = $7
* fragment = $9
* </pre>
*
* The regular expression has been modified slightly to expose the
* userInfo, domain, and port separately from the authority.
* The modified version yields
* <pre>
* $1 = http scheme
* $2 = <undefined> userInfo -\
* $3 = www.ics.uci.edu domain | authority
* $4 = <undefined> port -/
* $5 = /pub/ietf/uri/ path
* $6 = <undefined> query without ?
* $7 = Related fragment without #
* </pre>
* @type {!RegExp}
* @internal
*/
var _splitRe =
RegExpWrapper.create('^' +
'(?:' +
'([^:/?#.]+)' + // scheme - ignore special characters
// used by other URL parts such as :,
// ?, /, #, and .
':)?' +
'(?://' +
'(?:([^/?#]*)@)?' + // userInfo
'([\\w\\d\\-\\u0100-\\uffff.%]*)' + // domain - restrict to letters,
// digits, dashes, dots, percent
// escapes, and unicode characters.
'(?::([0-9]+))?' + // port
')?' +
'([^?#]+)?' + // path
'(?:\\?([^#]*))?' + // query
'(?:#(.*))?' + // fragment
'$');
/**
* The index of each URI component in the return value of goog.uri.utils.split.
* @enum {number}
*/
enum _ComponentIndex {
Scheme = 1,
UserInfo,
Domain,
Port,
Path,
QueryData,
Fragment
}
/**
* Splits a URI into its component parts.
*
* Each component can be accessed via the component indices; for example:
* <pre>
* goog.uri.utils.split(someStr)[goog.uri.utils.CompontentIndex.QUERY_DATA];
* </pre>
*
* @param {string} uri The URI string to examine.
* @return {!Array.<string|undefined>} Each component still URI-encoded.
* Each component that is present will contain the encoded value, whereas
* components that are not present will be undefined or empty, depending
* on the browser's regular expression implementation. Never null, since
* arbitrary strings may still look like path names.
*/
function _split(uri: string): Array<string | any> {
return RegExpWrapper.firstMatch(_splitRe, uri);
}
/**
* Removes dot segments in given path component, as described in
* RFC 3986, section 5.2.4.
*
* @param {string} path A non-empty path component.
* @return {string} Path component with removed dot segments.
*/
function _removeDotSegments(path: string): string {
if (path == '/') return '/';
var leadingSlash = path[0] == '/' ? '/' : '';
var trailingSlash = path[path.length - 1] === '/' ? '/' : '';
var segments = path.split('/');
var out: string[] = [];
var up = 0;
for (var pos = 0; pos < segments.length; pos++) {
var segment = segments[pos];
switch (segment) {
case '':
case '.':
break;
case '..':
if (out.length > 0) {
out.pop();
} else {
up++;
}
break;
default:
out.push(segment);
}
}
if (leadingSlash == '') {
while (up-- > 0) {
out.unshift('..');
}
if (out.length === 0) out.push('.');
}
return leadingSlash + out.join('/') + trailingSlash;
}
/**
* Takes an array of the parts from split and canonicalizes the path part
* and then joins all the parts.
* @param {Array.<string?>} parts
* @return {string}
*/
function _joinAndCanonicalizePath(parts: any[]): string {
var path = parts[_ComponentIndex.Path];
path = isBlank(path) ? '' : _removeDotSegments(path);
parts[_ComponentIndex.Path] = path;
return _buildFromEncodedParts(parts[_ComponentIndex.Scheme], parts[_ComponentIndex.UserInfo],
parts[_ComponentIndex.Domain], parts[_ComponentIndex.Port], path,
parts[_ComponentIndex.QueryData], parts[_ComponentIndex.Fragment]);
}
/**
* Resolves a URL.
* @param {string} base The URL acting as the base URL.
* @param {string} to The URL to resolve.
* @return {string}
*/
function _resolveUrl(base: string, url: string): string {
var parts = _split(encodeURI(url));
var baseParts = _split(base);
if (isPresent(parts[_ComponentIndex.Scheme])) {
return _joinAndCanonicalizePath(parts);
} else {
parts[_ComponentIndex.Scheme] = baseParts[_ComponentIndex.Scheme];
}
for (var i = _ComponentIndex.Scheme; i <= _ComponentIndex.Port; i++) {
if (isBlank(parts[i])) {
parts[i] = baseParts[i];
}
}
if (parts[_ComponentIndex.Path][0] == '/') {
return _joinAndCanonicalizePath(parts);
}
var path = baseParts[_ComponentIndex.Path];
if (isBlank(path)) path = '/';
var index = path.lastIndexOf('/');
path = path.substring(0, index + 1) + parts[_ComponentIndex.Path];
parts[_ComponentIndex.Path] = path;
return _joinAndCanonicalizePath(parts);
}

View File

@ -0,0 +1,71 @@
import {
IS_DART,
StringWrapper,
Math,
isBlank,
isArray,
isStrictStringMap,
isPrimitive
} from 'angular2/src/facade/lang';
import {StringMapWrapper} from 'angular2/src/facade/collection';
export var MODULE_SUFFIX = IS_DART ? '.dart' : '';
var CAMEL_CASE_REGEXP = /([A-Z])/g;
var DASH_CASE_REGEXP = /-([a-z])/g;
export function camelCaseToDashCase(input: string): string {
return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP,
(m) => { return '-' + m[1].toLowerCase(); });
}
export function dashCaseToCamelCase(input: string): string {
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP,
(m) => { return m[1].toUpperCase(); });
}
export function splitAtColon(input: string, defaultValues: string[]): string[] {
var parts = StringWrapper.split(input.trim(), /\s*:\s*/g);
if (parts.length > 1) {
return parts;
} else {
return defaultValues;
}
}
export function sanitizeIdentifier(name: string): string {
return StringWrapper.replaceAll(name, /\W/g, '_');
}
export function visitValue(value: any, visitor: ValueVisitor, context: any): any {
if (isArray(value)) {
return visitor.visitArray(<any[]>value, context);
} else if (isStrictStringMap(value)) {
return visitor.visitStringMap(<{[key: string]: any}>value, context);
} else if (isBlank(value) || isPrimitive(value)) {
return visitor.visitPrimitive(value, context);
} else {
return visitor.visitOther(value, context);
}
}
export interface ValueVisitor {
visitArray(arr: any[], context: any): any;
visitStringMap(map: {[key: string]: any}, context: any): any;
visitPrimitive(value: any, context: any): any;
visitOther(value: any, context: any): any;
}
export class ValueTransformer implements ValueVisitor {
visitArray(arr: any[], context: any): any {
return arr.map(value => visitValue(value, this, context));
}
visitStringMap(map: {[key: string]: any}, context: any): any {
var result = {};
StringMapWrapper.forEach(map,
(value, key) => { result[key] = visitValue(value, this, context); });
return result;
}
visitPrimitive(value: any, context: any): any { return value; }
visitOther(value: any, context: any): any { return value; }
}

View File

@ -0,0 +1,6 @@
import {CompileNode} from './compile_element';
import {TemplateAst} from '../template_ast';
export class CompileBinding {
constructor(public node: CompileNode, public sourceAst: TemplateAst) {}
}

View File

@ -0,0 +1,427 @@
import {BaseException} from 'angular2/src/facade/exceptions';
import * as o from '../output/output_ast';
import {Identifiers, identifierToken} from '../identifiers';
import {InjectMethodVars} from './constants';
import {CompileView} from './compile_view';
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {TemplateAst, ProviderAst, ProviderAstType, ReferenceAst} from '../template_ast';
import {
CompileTokenMap,
CompileDirectiveMetadata,
CompileTokenMetadata,
CompileQueryMetadata,
CompileProviderMetadata,
CompileDiDependencyMetadata,
CompileIdentifierMetadata,
CompileTypeMetadata,
} from '../compile_metadata';
import {getPropertyInView, createDiTokenExpression, injectFromViewParentInjector} from './util';
import {CompileQuery, createQueryList, addQueryToTokenMap} from './compile_query';
import {CompileMethod} from './compile_method';
import {ValueTransformer, visitValue} from '../util';
export class CompileNode {
constructor(public parent: CompileElement, public view: CompileView, public nodeIndex: number,
public renderNode: o.Expression, public sourceAst: TemplateAst) {}
isNull(): boolean { return isBlank(this.renderNode); }
isRootElement(): boolean { return this.view != this.parent.view; }
}
export class CompileElement extends CompileNode {
static createNull(): CompileElement {
return new CompileElement(null, null, null, null, null, null, [], [], false, false, []);
}
private _compViewExpr: o.Expression = null;
public appElement: o.ReadPropExpr;
public elementRef: o.Expression;
public injector: o.Expression;
private _instances = new CompileTokenMap<o.Expression>();
private _resolvedProviders: CompileTokenMap<ProviderAst>;
private _queryCount = 0;
private _queries = new CompileTokenMap<CompileQuery[]>();
private _componentConstructorViewQueryLists: o.Expression[] = [];
public contentNodesByNgContentIndex: Array<o.Expression>[] = null;
public embeddedView: CompileView;
public directiveInstances: o.Expression[];
public referenceTokens: {[key: string]: CompileTokenMetadata};
constructor(parent: CompileElement, view: CompileView, nodeIndex: number,
renderNode: o.Expression, sourceAst: TemplateAst,
public component: CompileDirectiveMetadata,
private _directives: CompileDirectiveMetadata[],
private _resolvedProvidersArray: ProviderAst[], public hasViewContainer: boolean,
public hasEmbeddedView: boolean, references: ReferenceAst[]) {
super(parent, view, nodeIndex, renderNode, sourceAst);
this.referenceTokens = {};
references.forEach(ref => this.referenceTokens[ref.name] = ref.value);
this.elementRef = o.importExpr(Identifiers.ElementRef).instantiate([this.renderNode]);
this._instances.add(identifierToken(Identifiers.ElementRef), this.elementRef);
this.injector = o.THIS_EXPR.callMethod('injector', [o.literal(this.nodeIndex)]);
this._instances.add(identifierToken(Identifiers.Injector), this.injector);
this._instances.add(identifierToken(Identifiers.Renderer), o.THIS_EXPR.prop('renderer'));
if (this.hasViewContainer || this.hasEmbeddedView || isPresent(this.component)) {
this._createAppElement();
}
}
private _createAppElement() {
var fieldName = `_appEl_${this.nodeIndex}`;
var parentNodeIndex = this.isRootElement() ? null : this.parent.nodeIndex;
// private is fine here as no child view will reference an AppElement
this.view.fields.push(new o.ClassField(fieldName, o.importType(Identifiers.AppElement),
[o.StmtModifier.Private]));
var statement = o.THIS_EXPR.prop(fieldName)
.set(o.importExpr(Identifiers.AppElement)
.instantiate([
o.literal(this.nodeIndex),
o.literal(parentNodeIndex),
o.THIS_EXPR,
this.renderNode
]))
.toStmt();
this.view.createMethod.addStmt(statement);
this.appElement = o.THIS_EXPR.prop(fieldName);
this._instances.add(identifierToken(Identifiers.AppElement), this.appElement);
}
setComponentView(compViewExpr: o.Expression) {
this._compViewExpr = compViewExpr;
this.contentNodesByNgContentIndex =
ListWrapper.createFixedSize(this.component.template.ngContentSelectors.length);
for (var i = 0; i < this.contentNodesByNgContentIndex.length; i++) {
this.contentNodesByNgContentIndex[i] = [];
}
}
setEmbeddedView(embeddedView: CompileView) {
this.embeddedView = embeddedView;
if (isPresent(embeddedView)) {
var createTemplateRefExpr =
o.importExpr(Identifiers.TemplateRef_)
.instantiate([this.appElement, this.embeddedView.viewFactory]);
var provider = new CompileProviderMetadata(
{token: identifierToken(Identifiers.TemplateRef), useValue: createTemplateRefExpr});
// Add TemplateRef as first provider as it does not have deps on other providers
this._resolvedProvidersArray.unshift(new ProviderAst(provider.token, false, true, [provider],
ProviderAstType.Builtin,
this.sourceAst.sourceSpan));
}
}
beforeChildren(): void {
if (this.hasViewContainer) {
this._instances.add(identifierToken(Identifiers.ViewContainerRef),
this.appElement.prop('vcRef'));
}
this._resolvedProviders = new CompileTokenMap<ProviderAst>();
this._resolvedProvidersArray.forEach(provider =>
this._resolvedProviders.add(provider.token, provider));
// create all the provider instances, some in the view constructor,
// some as getters. We rely on the fact that they are already sorted topologically.
this._resolvedProviders.values().forEach((resolvedProvider) => {
var providerValueExpressions = resolvedProvider.providers.map((provider) => {
if (isPresent(provider.useExisting)) {
return this._getDependency(
resolvedProvider.providerType,
new CompileDiDependencyMetadata({token: provider.useExisting}));
} else if (isPresent(provider.useFactory)) {
var deps = isPresent(provider.deps) ? provider.deps : provider.useFactory.diDeps;
var depsExpr = deps.map((dep) => this._getDependency(resolvedProvider.providerType, dep));
return o.importExpr(provider.useFactory).callFn(depsExpr);
} else if (isPresent(provider.useClass)) {
var deps = isPresent(provider.deps) ? provider.deps : provider.useClass.diDeps;
var depsExpr = deps.map((dep) => this._getDependency(resolvedProvider.providerType, dep));
return o.importExpr(provider.useClass)
.instantiate(depsExpr, o.importType(provider.useClass));
} else {
return _convertValueToOutputAst(provider.useValue);
}
});
var propName = `_${resolvedProvider.token.name}_${this.nodeIndex}_${this._instances.size}`;
var instance =
createProviderProperty(propName, resolvedProvider, providerValueExpressions,
resolvedProvider.multiProvider, resolvedProvider.eager, this);
this._instances.add(resolvedProvider.token, instance);
});
this.directiveInstances =
this._directives.map((directive) => this._instances.get(identifierToken(directive.type)));
for (var i = 0; i < this.directiveInstances.length; i++) {
var directiveInstance = this.directiveInstances[i];
var directive = this._directives[i];
directive.queries.forEach((queryMeta) => { this._addQuery(queryMeta, directiveInstance); });
}
var queriesWithReads: _QueryWithRead[] = [];
this._resolvedProviders.values().forEach((resolvedProvider) => {
var queriesForProvider = this._getQueriesFor(resolvedProvider.token);
ListWrapper.addAll(
queriesWithReads,
queriesForProvider.map(query => new _QueryWithRead(query, resolvedProvider.token)));
});
StringMapWrapper.forEach(this.referenceTokens, (_, varName) => {
var token = this.referenceTokens[varName];
var varValue;
if (isPresent(token)) {
varValue = this._instances.get(token);
} else {
varValue = this.renderNode;
}
this.view.locals.set(varName, varValue);
var varToken = new CompileTokenMetadata({value: varName});
ListWrapper.addAll(queriesWithReads, this._getQueriesFor(varToken)
.map(query => new _QueryWithRead(query, varToken)));
});
queriesWithReads.forEach((queryWithRead) => {
var value: o.Expression;
if (isPresent(queryWithRead.read.identifier)) {
// query for an identifier
value = this._instances.get(queryWithRead.read);
} else {
// query for a reference
var token = this.referenceTokens[queryWithRead.read.value];
if (isPresent(token)) {
value = this._instances.get(token);
} else {
value = this.elementRef;
}
}
if (isPresent(value)) {
queryWithRead.query.addValue(value, this.view);
}
});
if (isPresent(this.component)) {
var componentConstructorViewQueryList =
isPresent(this.component) ? o.literalArr(this._componentConstructorViewQueryLists) :
o.NULL_EXPR;
var compExpr = isPresent(this.getComponent()) ? this.getComponent() : o.NULL_EXPR;
this.view.createMethod.addStmt(
this.appElement.callMethod(
'initComponent',
[compExpr, componentConstructorViewQueryList, this._compViewExpr])
.toStmt());
}
}
afterChildren(childNodeCount: number) {
this._resolvedProviders.values().forEach((resolvedProvider) => {
// Note: afterChildren is called after recursing into children.
// This is good so that an injector match in an element that is closer to a requesting element
// matches first.
var providerExpr = this._instances.get(resolvedProvider.token);
// Note: view providers are only visible on the injector of that element.
// This is not fully correct as the rules during codegen don't allow a directive
// to get hold of a view provdier on the same element. We still do this semantic
// as it simplifies our model to having only one runtime injector per element.
var providerChildNodeCount =
resolvedProvider.providerType === ProviderAstType.PrivateService ? 0 : childNodeCount;
this.view.injectorGetMethod.addStmt(createInjectInternalCondition(
this.nodeIndex, providerChildNodeCount, resolvedProvider, providerExpr));
});
this._queries.values().forEach(
(queries) =>
queries.forEach((query) => query.afterChildren(this.view.updateContentQueriesMethod)));
}
addContentNode(ngContentIndex: number, nodeExpr: o.Expression) {
this.contentNodesByNgContentIndex[ngContentIndex].push(nodeExpr);
}
getComponent(): o.Expression {
return isPresent(this.component) ? this._instances.get(identifierToken(this.component.type)) :
null;
}
getProviderTokens(): o.Expression[] {
return this._resolvedProviders.values().map(
(resolvedProvider) => createDiTokenExpression(resolvedProvider.token));
}
private _getQueriesFor(token: CompileTokenMetadata): CompileQuery[] {
var result: CompileQuery[] = [];
var currentEl: CompileElement = this;
var distance = 0;
var queries: CompileQuery[];
while (!currentEl.isNull()) {
queries = currentEl._queries.get(token);
if (isPresent(queries)) {
ListWrapper.addAll(result,
queries.filter((query) => query.meta.descendants || distance <= 1));
}
if (currentEl._directives.length > 0) {
distance++;
}
currentEl = currentEl.parent;
}
queries = this.view.componentView.viewQueries.get(token);
if (isPresent(queries)) {
ListWrapper.addAll(result, queries);
}
return result;
}
private _addQuery(queryMeta: CompileQueryMetadata,
directiveInstance: o.Expression): CompileQuery {
var propName = `_query_${queryMeta.selectors[0].name}_${this.nodeIndex}_${this._queryCount++}`;
var queryList = createQueryList(queryMeta, directiveInstance, propName, this.view);
var query = new CompileQuery(queryMeta, queryList, directiveInstance, this.view);
addQueryToTokenMap(this._queries, query);
return query;
}
private _getLocalDependency(requestingProviderType: ProviderAstType,
dep: CompileDiDependencyMetadata): o.Expression {
var result = null;
// constructor content query
if (isBlank(result) && isPresent(dep.query)) {
result = this._addQuery(dep.query, null).queryList;
}
// constructor view query
if (isBlank(result) && isPresent(dep.viewQuery)) {
result = createQueryList(
dep.viewQuery, null,
`_viewQuery_${dep.viewQuery.selectors[0].name}_${this.nodeIndex}_${this._componentConstructorViewQueryLists.length}`,
this.view);
this._componentConstructorViewQueryLists.push(result);
}
if (isPresent(dep.token)) {
// access builtins with special visibility
if (isBlank(result)) {
if (dep.token.equalsTo(identifierToken(Identifiers.ChangeDetectorRef))) {
if (requestingProviderType === ProviderAstType.Component) {
return this._compViewExpr.prop('ref');
} else {
return getPropertyInView(o.THIS_EXPR.prop('ref'), this.view, this.view.componentView);
}
}
}
// access regular providers on the element
if (isBlank(result)) {
result = this._instances.get(dep.token);
}
}
return result;
}
private _getDependency(requestingProviderType: ProviderAstType,
dep: CompileDiDependencyMetadata): o.Expression {
var currElement: CompileElement = this;
var result = null;
if (dep.isValue) {
result = o.literal(dep.value);
}
if (isBlank(result) && !dep.isSkipSelf) {
result = this._getLocalDependency(requestingProviderType, dep);
}
// check parent elements
while (isBlank(result) && !currElement.parent.isNull()) {
currElement = currElement.parent;
result = currElement._getLocalDependency(ProviderAstType.PublicService,
new CompileDiDependencyMetadata({token: dep.token}));
}
if (isBlank(result)) {
result = injectFromViewParentInjector(dep.token, dep.isOptional);
}
if (isBlank(result)) {
result = o.NULL_EXPR;
}
return getPropertyInView(result, this.view, currElement.view);
}
}
function createInjectInternalCondition(nodeIndex: number, childNodeCount: number,
provider: ProviderAst,
providerExpr: o.Expression): o.Statement {
var indexCondition;
if (childNodeCount > 0) {
indexCondition = o.literal(nodeIndex)
.lowerEquals(InjectMethodVars.requestNodeIndex)
.and(InjectMethodVars.requestNodeIndex.lowerEquals(
o.literal(nodeIndex + childNodeCount)));
} else {
indexCondition = o.literal(nodeIndex).identical(InjectMethodVars.requestNodeIndex);
}
return new o.IfStmt(
InjectMethodVars.token.identical(createDiTokenExpression(provider.token)).and(indexCondition),
[new o.ReturnStatement(providerExpr)]);
}
function createProviderProperty(propName: string, provider: ProviderAst,
providerValueExpressions: o.Expression[], isMulti: boolean,
isEager: boolean, compileElement: CompileElement): o.Expression {
var view = compileElement.view;
var resolvedProviderValueExpr;
var type;
if (isMulti) {
resolvedProviderValueExpr = o.literalArr(providerValueExpressions);
type = new o.ArrayType(o.DYNAMIC_TYPE);
} else {
resolvedProviderValueExpr = providerValueExpressions[0];
type = providerValueExpressions[0].type;
}
if (isBlank(type)) {
type = o.DYNAMIC_TYPE;
}
if (isEager) {
view.fields.push(new o.ClassField(propName, type));
view.createMethod.addStmt(o.THIS_EXPR.prop(propName).set(resolvedProviderValueExpr).toStmt());
} else {
var internalField = `_${propName}`;
view.fields.push(new o.ClassField(internalField, type));
var getter = new CompileMethod(view);
getter.resetDebugInfo(compileElement.nodeIndex, compileElement.sourceAst);
// Note: Equals is important for JS so that it also checks the undefined case!
getter.addStmt(
new o.IfStmt(o.THIS_EXPR.prop(internalField).isBlank(),
[o.THIS_EXPR.prop(internalField).set(resolvedProviderValueExpr).toStmt()]));
getter.addStmt(new o.ReturnStatement(o.THIS_EXPR.prop(internalField)));
view.getters.push(new o.ClassGetter(propName, getter.finish(), type));
}
return o.THIS_EXPR.prop(propName);
}
class _QueryWithRead {
public read: CompileTokenMetadata;
constructor(public query: CompileQuery, match: CompileTokenMetadata) {
this.read = isPresent(query.meta.read) ? query.meta.read : match;
}
}
function _convertValueToOutputAst(value: any): o.Expression {
return visitValue(value, new _ValueOutputAstTransformer(), null);
}
class _ValueOutputAstTransformer extends ValueTransformer {
visitArray(arr: any[], context: any): o.Expression {
return o.literalArr(arr.map(value => visitValue(value, this, context)));
}
visitStringMap(map: {[key: string]: any}, context: any): o.Expression {
var entries = [];
StringMapWrapper.forEach(
map, (value, key) => { entries.push([key, visitValue(value, this, context)]); });
return o.literalMap(entries);
}
visitPrimitive(value: any, context: any): o.Expression { return o.literal(value); }
visitOther(value: any, context: any): o.Expression {
if (value instanceof CompileIdentifierMetadata) {
return o.importExpr(value);
} else if (value instanceof o.Expression) {
return value;
} else {
throw new BaseException(`Illegal state: Don't now how to compile value ${value}`);
}
}
}

View File

@ -0,0 +1,75 @@
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import * as o from '../output/output_ast';
import {TemplateAst} from '../template_ast';
import {CompileView} from './compile_view';
class _DebugState {
constructor(public nodeIndex: number, public sourceAst: TemplateAst) {}
}
var NULL_DEBUG_STATE = new _DebugState(null, null);
export class CompileMethod {
private _newState: _DebugState = NULL_DEBUG_STATE;
private _currState: _DebugState = NULL_DEBUG_STATE;
private _debugEnabled: boolean;
private _bodyStatements: o.Statement[] = [];
constructor(private _view: CompileView) {
this._debugEnabled = this._view.genConfig.genDebugInfo;
}
private _updateDebugContextIfNeeded() {
if (this._newState.nodeIndex !== this._currState.nodeIndex ||
this._newState.sourceAst !== this._currState.sourceAst) {
var expr = this._updateDebugContext(this._newState);
if (isPresent(expr)) {
this._bodyStatements.push(expr.toStmt());
}
}
}
private _updateDebugContext(newState: _DebugState): o.Expression {
this._currState = this._newState = newState;
if (this._debugEnabled) {
var sourceLocation =
isPresent(newState.sourceAst) ? newState.sourceAst.sourceSpan.start : null;
return o.THIS_EXPR.callMethod('debug', [
o.literal(newState.nodeIndex),
isPresent(sourceLocation) ? o.literal(sourceLocation.line) : o.NULL_EXPR,
isPresent(sourceLocation) ? o.literal(sourceLocation.col) : o.NULL_EXPR
]);
} else {
return null;
}
}
resetDebugInfoExpr(nodeIndex: number, templateAst: TemplateAst): o.Expression {
var res = this._updateDebugContext(new _DebugState(nodeIndex, templateAst));
return isPresent(res) ? res : o.NULL_EXPR;
}
resetDebugInfo(nodeIndex: number, templateAst: TemplateAst) {
this._newState = new _DebugState(nodeIndex, templateAst);
}
addStmt(stmt: o.Statement) {
this._updateDebugContextIfNeeded();
this._bodyStatements.push(stmt);
}
addStmts(stmts: o.Statement[]) {
this._updateDebugContextIfNeeded();
ListWrapper.addAll(this._bodyStatements, stmts);
}
finish(): o.Statement[] { return this._bodyStatements; }
isEmpty(): boolean { return this._bodyStatements.length === 0; }
}

View File

@ -0,0 +1,75 @@
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import * as o from '../output/output_ast';
import {CompileView} from './compile_view';
import {CompilePipeMetadata} from '../compile_metadata';
import {Identifiers, identifierToken} from '../identifiers';
import {injectFromViewParentInjector, createPureProxy, getPropertyInView} from './util';
class _PurePipeProxy {
constructor(public instance: o.ReadPropExpr, public argCount: number) {}
}
export class CompilePipe {
meta: CompilePipeMetadata;
instance: o.ReadPropExpr;
private _purePipeProxies: _PurePipeProxy[] = [];
constructor(public view: CompileView, name: string) {
this.meta = _findPipeMeta(view, name);
this.instance = o.THIS_EXPR.prop(`_pipe_${name}_${view.pipeCount++}`);
}
get pure(): boolean { return this.meta.pure; }
create(): void {
var deps = this.meta.type.diDeps.map((diDep) => {
if (diDep.token.equalsTo(identifierToken(Identifiers.ChangeDetectorRef))) {
return getPropertyInView(o.THIS_EXPR.prop('ref'), this.view, this.view.componentView);
}
return injectFromViewParentInjector(diDep.token, false);
});
this.view.fields.push(new o.ClassField(this.instance.name, o.importType(this.meta.type)));
this.view.createMethod.resetDebugInfo(null, null);
this.view.createMethod.addStmt(o.THIS_EXPR.prop(this.instance.name)
.set(o.importExpr(this.meta.type).instantiate(deps))
.toStmt());
this._purePipeProxies.forEach((purePipeProxy) => {
createPureProxy(
this.instance.prop('transform').callMethod(o.BuiltinMethod.bind, [this.instance]),
purePipeProxy.argCount, purePipeProxy.instance, this.view);
});
}
call(callingView: CompileView, args: o.Expression[]): o.Expression {
if (this.meta.pure) {
var purePipeProxy = new _PurePipeProxy(
o.THIS_EXPR.prop(`${this.instance.name}_${this._purePipeProxies.length}`), args.length);
this._purePipeProxies.push(purePipeProxy);
return getPropertyInView(
o.importExpr(Identifiers.castByValue)
.callFn([purePipeProxy.instance, this.instance.prop('transform')]),
callingView, this.view)
.callFn(args);
} else {
return getPropertyInView(this.instance, callingView, this.view).callMethod('transform', args);
}
}
}
function _findPipeMeta(view: CompileView, name: string): CompilePipeMetadata {
var pipeMeta: CompilePipeMetadata = null;
for (var i = view.pipeMetas.length - 1; i >= 0; i--) {
var localPipeMeta = view.pipeMetas[i];
if (localPipeMeta.name == name) {
pipeMeta = localPipeMeta;
break;
}
}
if (isBlank(pipeMeta)) {
throw new BaseException(
`Illegal state: Could not find pipe ${name} although the parser should have detected this error!`);
}
return pipeMeta;
}

View File

@ -0,0 +1,117 @@
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
import * as o from '../output/output_ast';
import {Identifiers} from '../identifiers';
import {
CompileQueryMetadata,
CompileIdentifierMetadata,
CompileTokenMap
} from '../compile_metadata';
import {CompileView} from './compile_view';
import {CompileElement} from './compile_element';
import {CompileMethod} from './compile_method';
import {getPropertyInView} from './util';
class ViewQueryValues {
constructor(public view: CompileView, public values: Array<o.Expression | ViewQueryValues>) {}
}
export class CompileQuery {
private _values: ViewQueryValues;
constructor(public meta: CompileQueryMetadata, public queryList: o.Expression,
public ownerDirectiveExpression: o.Expression, public view: CompileView) {
this._values = new ViewQueryValues(view, []);
}
addValue(value: o.Expression, view: CompileView) {
var currentView = view;
var elPath: CompileElement[] = [];
while (isPresent(currentView) && currentView !== this.view) {
var parentEl = currentView.declarationElement;
elPath.unshift(parentEl);
currentView = parentEl.view;
}
var queryListForDirtyExpr = getPropertyInView(this.queryList, view, this.view);
var viewValues = this._values;
elPath.forEach((el) => {
var last =
viewValues.values.length > 0 ? viewValues.values[viewValues.values.length - 1] : null;
if (last instanceof ViewQueryValues && last.view === el.embeddedView) {
viewValues = last;
} else {
var newViewValues = new ViewQueryValues(el.embeddedView, []);
viewValues.values.push(newViewValues);
viewValues = newViewValues;
}
});
viewValues.values.push(value);
if (elPath.length > 0) {
view.dirtyParentQueriesMethod.addStmt(
queryListForDirtyExpr.callMethod('setDirty', []).toStmt());
}
}
afterChildren(targetMethod: CompileMethod) {
var values = createQueryValues(this._values);
var updateStmts = [this.queryList.callMethod('reset', [o.literalArr(values)]).toStmt()];
if (isPresent(this.ownerDirectiveExpression)) {
var valueExpr = this.meta.first ? this.queryList.prop('first') : this.queryList;
updateStmts.push(
this.ownerDirectiveExpression.prop(this.meta.propertyName).set(valueExpr).toStmt());
}
if (!this.meta.first) {
updateStmts.push(this.queryList.callMethod('notifyOnChanges', []).toStmt());
}
targetMethod.addStmt(new o.IfStmt(this.queryList.prop('dirty'), updateStmts));
}
}
function createQueryValues(viewValues: ViewQueryValues): o.Expression[] {
return ListWrapper.flatten(viewValues.values.map((entry) => {
if (entry instanceof ViewQueryValues) {
return mapNestedViews(entry.view.declarationElement.appElement, entry.view,
createQueryValues(entry));
} else {
return <o.Expression>entry;
}
}));
}
function mapNestedViews(declarationAppElement: o.Expression, view: CompileView,
expressions: o.Expression[]): o.Expression {
var adjustedExpressions: o.Expression[] = expressions.map((expr) => {
return o.replaceVarInExpression(o.THIS_EXPR.name, o.variable('nestedView'), expr);
});
return declarationAppElement.callMethod('mapNestedViews', [
o.variable(view.className),
o.fn([new o.FnParam('nestedView', view.classType)],
[new o.ReturnStatement(o.literalArr(adjustedExpressions))])
]);
}
export function createQueryList(query: CompileQueryMetadata, directiveInstance: o.Expression,
propertyName: string, compileView: CompileView): o.Expression {
compileView.fields.push(new o.ClassField(propertyName, o.importType(Identifiers.QueryList)));
var expr = o.THIS_EXPR.prop(propertyName);
compileView.createMethod.addStmt(o.THIS_EXPR.prop(propertyName)
.set(o.importExpr(Identifiers.QueryList).instantiate([]))
.toStmt());
return expr;
}
export function addQueryToTokenMap(map: CompileTokenMap<CompileQuery[]>, query: CompileQuery) {
query.meta.selectors.forEach((selector) => {
var entry = map.get(selector);
if (isBlank(entry)) {
entry = [];
map.add(selector, entry);
}
entry.push(query);
});
}

View File

@ -0,0 +1,209 @@
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {ListWrapper, StringMapWrapper, MapWrapper} from 'angular2/src/facade/collection';
import * as o from '../output/output_ast';
import {EventHandlerVars} from './constants';
import {CompileQuery, createQueryList, addQueryToTokenMap} from './compile_query';
import {NameResolver} from './expression_converter';
import {CompileElement, CompileNode} from './compile_element';
import {CompileMethod} from './compile_method';
import {CompilePipe} from './compile_pipe';
import {ViewType} from 'angular2/src/core/linker/view_type';
import {
CompileDirectiveMetadata,
CompilePipeMetadata,
CompileIdentifierMetadata,
CompileTokenMap
} from '../compile_metadata';
import {
getViewFactoryName,
injectFromViewParentInjector,
createDiTokenExpression,
getPropertyInView,
createPureProxy
} from './util';
import {CompilerConfig} from '../config';
import {CompileBinding} from './compile_binding';
import {Identifiers} from '../identifiers';
export class CompileView implements NameResolver {
public viewType: ViewType;
public viewQueries: CompileTokenMap<CompileQuery[]>;
public nodes: CompileNode[] = [];
// root nodes or AppElements for ViewContainers
public rootNodesOrAppElements: o.Expression[] = [];
public bindings: CompileBinding[] = [];
public classStatements: o.Statement[] = [];
public createMethod: CompileMethod;
public injectorGetMethod: CompileMethod;
public updateContentQueriesMethod: CompileMethod;
public dirtyParentQueriesMethod: CompileMethod;
public updateViewQueriesMethod: CompileMethod;
public detectChangesInInputsMethod: CompileMethod;
public detectChangesRenderPropertiesMethod: CompileMethod;
public afterContentLifecycleCallbacksMethod: CompileMethod;
public afterViewLifecycleCallbacksMethod: CompileMethod;
public destroyMethod: CompileMethod;
public eventHandlerMethods: o.ClassMethod[] = [];
public fields: o.ClassField[] = [];
public getters: o.ClassGetter[] = [];
public disposables: o.Expression[] = [];
public subscriptions: o.Expression[] = [];
public componentView: CompileView;
public purePipes = new Map<string, CompilePipe>();
public pipes: CompilePipe[] = [];
public locals = new Map<string, o.Expression>();
public className: string;
public classType: o.Type;
public viewFactory: o.ReadVarExpr;
public literalArrayCount = 0;
public literalMapCount = 0;
public pipeCount = 0;
public componentContext: o.Expression;
constructor(public component: CompileDirectiveMetadata, public genConfig: CompilerConfig,
public pipeMetas: CompilePipeMetadata[], public styles: o.Expression,
public viewIndex: number, public declarationElement: CompileElement,
public templateVariableBindings: string[][]) {
this.createMethod = new CompileMethod(this);
this.injectorGetMethod = new CompileMethod(this);
this.updateContentQueriesMethod = new CompileMethod(this);
this.dirtyParentQueriesMethod = new CompileMethod(this);
this.updateViewQueriesMethod = new CompileMethod(this);
this.detectChangesInInputsMethod = new CompileMethod(this);
this.detectChangesRenderPropertiesMethod = new CompileMethod(this);
this.afterContentLifecycleCallbacksMethod = new CompileMethod(this);
this.afterViewLifecycleCallbacksMethod = new CompileMethod(this);
this.destroyMethod = new CompileMethod(this);
this.viewType = getViewType(component, viewIndex);
this.className = `_View_${component.type.name}${viewIndex}`;
this.classType = o.importType(new CompileIdentifierMetadata({name: this.className}));
this.viewFactory = o.variable(getViewFactoryName(component, viewIndex));
if (this.viewType === ViewType.COMPONENT || this.viewType === ViewType.HOST) {
this.componentView = this;
} else {
this.componentView = this.declarationElement.view.componentView;
}
this.componentContext =
getPropertyInView(o.THIS_EXPR.prop('context'), this, this.componentView);
var viewQueries = new CompileTokenMap<CompileQuery[]>();
if (this.viewType === ViewType.COMPONENT) {
var directiveInstance = o.THIS_EXPR.prop('context');
ListWrapper.forEachWithIndex(this.component.viewQueries, (queryMeta, queryIndex) => {
var propName = `_viewQuery_${queryMeta.selectors[0].name}_${queryIndex}`;
var queryList = createQueryList(queryMeta, directiveInstance, propName, this);
var query = new CompileQuery(queryMeta, queryList, directiveInstance, this);
addQueryToTokenMap(viewQueries, query);
});
var constructorViewQueryCount = 0;
this.component.type.diDeps.forEach((dep) => {
if (isPresent(dep.viewQuery)) {
var queryList = o.THIS_EXPR.prop('declarationAppElement')
.prop('componentConstructorViewQueries')
.key(o.literal(constructorViewQueryCount++));
var query = new CompileQuery(dep.viewQuery, queryList, null, this);
addQueryToTokenMap(viewQueries, query);
}
});
}
this.viewQueries = viewQueries;
templateVariableBindings.forEach(
(entry) => { this.locals.set(entry[1], o.THIS_EXPR.prop('context').prop(entry[0])); });
if (!this.declarationElement.isNull()) {
this.declarationElement.setEmbeddedView(this);
}
}
callPipe(name: string, input: o.Expression, args: o.Expression[]): o.Expression {
var compView = this.componentView;
var pipe = compView.purePipes.get(name);
if (isBlank(pipe)) {
pipe = new CompilePipe(compView, name);
if (pipe.pure) {
compView.purePipes.set(name, pipe);
}
compView.pipes.push(pipe);
}
return pipe.call(this, [input].concat(args));
}
getLocal(name: string): o.Expression {
if (name == EventHandlerVars.event.name) {
return EventHandlerVars.event;
}
var currView: CompileView = this;
var result = currView.locals.get(name);
while (isBlank(result) && isPresent(currView.declarationElement.view)) {
currView = currView.declarationElement.view;
result = currView.locals.get(name);
}
if (isPresent(result)) {
return getPropertyInView(result, this, currView);
} else {
return null;
}
}
createLiteralArray(values: o.Expression[]): o.Expression {
if (values.length === 0) {
return o.importExpr(Identifiers.EMPTY_ARRAY);
}
var proxyExpr = o.THIS_EXPR.prop(`_arr_${this.literalArrayCount++}`);
var proxyParams: o.FnParam[] = [];
var proxyReturnEntries: o.Expression[] = [];
for (var i = 0; i < values.length; i++) {
var paramName = `p${i}`;
proxyParams.push(new o.FnParam(paramName));
proxyReturnEntries.push(o.variable(paramName));
}
createPureProxy(o.fn(proxyParams, [new o.ReturnStatement(o.literalArr(proxyReturnEntries))]),
values.length, proxyExpr, this);
return proxyExpr.callFn(values);
}
createLiteralMap(entries: Array<Array<string | o.Expression>>): o.Expression {
if (entries.length === 0) {
return o.importExpr(Identifiers.EMPTY_MAP);
}
var proxyExpr = o.THIS_EXPR.prop(`_map_${this.literalMapCount++}`);
var proxyParams: o.FnParam[] = [];
var proxyReturnEntries: Array<Array<string | o.Expression>> = [];
var values: o.Expression[] = [];
for (var i = 0; i < entries.length; i++) {
var paramName = `p${i}`;
proxyParams.push(new o.FnParam(paramName));
proxyReturnEntries.push([entries[i][0], o.variable(paramName)]);
values.push(<o.Expression>entries[i][1]);
}
createPureProxy(o.fn(proxyParams, [new o.ReturnStatement(o.literalMap(proxyReturnEntries))]),
entries.length, proxyExpr, this);
return proxyExpr.callFn(values);
}
afterNodes() {
this.pipes.forEach((pipe) => pipe.create());
this.viewQueries.values().forEach(
(queries) => queries.forEach((query) => query.afterChildren(this.updateViewQueriesMethod)));
}
}
function getViewType(component: CompileDirectiveMetadata, embeddedTemplateIndex: number): ViewType {
if (embeddedTemplateIndex > 0) {
return ViewType.EMBEDDED;
} else if (component.type.isHost) {
return ViewType.HOST;
} else {
return ViewType.COMPONENT;
}
}

View File

@ -0,0 +1,86 @@
import {serializeEnum, isBlank, resolveEnumToken} from 'angular2/src/facade/lang';
import {CompileIdentifierMetadata, CompileTokenMetadata} from '../compile_metadata';
import {
ChangeDetectorState,
ChangeDetectionStrategy
} from 'angular2/src/core/change_detection/change_detection';
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';
import {ViewType} from 'angular2/src/core/linker/view_type';
import * as o from '../output/output_ast';
import {Identifiers} from '../identifiers';
function _enumExpression(classIdentifier: CompileIdentifierMetadata, value: any): o.Expression {
if (isBlank(value)) return o.NULL_EXPR;
var name = resolveEnumToken(classIdentifier.runtime, value);
return o.importExpr(new CompileIdentifierMetadata({
name: `${classIdentifier.name}.${name}`,
moduleUrl: classIdentifier.moduleUrl,
runtime: value
}));
}
export class ViewTypeEnum {
static fromValue(value: ViewType): o.Expression {
return _enumExpression(Identifiers.ViewType, value);
}
static HOST = ViewTypeEnum.fromValue(ViewType.HOST);
static COMPONENT = ViewTypeEnum.fromValue(ViewType.COMPONENT);
static EMBEDDED = ViewTypeEnum.fromValue(ViewType.EMBEDDED);
}
export class ViewEncapsulationEnum {
static fromValue(value: ViewEncapsulation): o.Expression {
return _enumExpression(Identifiers.ViewEncapsulation, value);
}
static Emulated = ViewEncapsulationEnum.fromValue(ViewEncapsulation.Emulated);
static Native = ViewEncapsulationEnum.fromValue(ViewEncapsulation.Native);
static None = ViewEncapsulationEnum.fromValue(ViewEncapsulation.None);
}
export class ChangeDetectorStateEnum {
static fromValue(value: ChangeDetectorState): o.Expression {
return _enumExpression(Identifiers.ChangeDetectorState, value);
}
static NeverChecked = ChangeDetectorStateEnum.fromValue(ChangeDetectorState.NeverChecked);
static CheckedBefore = ChangeDetectorStateEnum.fromValue(ChangeDetectorState.CheckedBefore);
static Errored = ChangeDetectorStateEnum.fromValue(ChangeDetectorState.Errored);
}
export class ChangeDetectionStrategyEnum {
static fromValue(value: ChangeDetectionStrategy): o.Expression {
return _enumExpression(Identifiers.ChangeDetectionStrategy, value);
}
static CheckOnce = ChangeDetectionStrategyEnum.fromValue(ChangeDetectionStrategy.CheckOnce);
static Checked = ChangeDetectionStrategyEnum.fromValue(ChangeDetectionStrategy.Checked);
static CheckAlways = ChangeDetectionStrategyEnum.fromValue(ChangeDetectionStrategy.CheckAlways);
static Detached = ChangeDetectionStrategyEnum.fromValue(ChangeDetectionStrategy.Detached);
static OnPush = ChangeDetectionStrategyEnum.fromValue(ChangeDetectionStrategy.OnPush);
static Default = ChangeDetectionStrategyEnum.fromValue(ChangeDetectionStrategy.Default);
}
export class ViewConstructorVars {
static viewUtils = o.variable('viewUtils');
static parentInjector = o.variable('parentInjector');
static declarationEl = o.variable('declarationEl');
}
export class ViewProperties {
static renderer = o.THIS_EXPR.prop('renderer');
static projectableNodes = o.THIS_EXPR.prop('projectableNodes');
static viewUtils = o.THIS_EXPR.prop('viewUtils');
}
export class EventHandlerVars { static event = o.variable('$event'); }
export class InjectMethodVars {
static token = o.variable('token');
static requestNodeIndex = o.variable('requestNodeIndex');
static notFoundResult = o.variable('notFoundResult');
}
export class DetectChangesVars {
static throwOnChange = o.variable(`throwOnChange`);
static changes = o.variable(`changes`);
static changed = o.variable(`changed`);
static valUnwrapper = o.variable(`valUnwrapper`);
}

View File

@ -0,0 +1,163 @@
import {isBlank, isPresent, StringWrapper} from 'angular2/src/facade/lang';
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {EventHandlerVars, ViewProperties} from './constants';
import * as o from '../output/output_ast';
import {CompileElement} from './compile_element';
import {CompileMethod} from './compile_method';
import {BoundEventAst, DirectiveAst} from '../template_ast';
import {CompileDirectiveMetadata} from '../compile_metadata';
import {convertCdStatementToIr} from './expression_converter';
import {CompileBinding} from './compile_binding';
export class CompileEventListener {
private _method: CompileMethod;
private _hasComponentHostListener: boolean = false;
private _methodName: string;
private _eventParam: o.FnParam;
private _actionResultExprs: o.Expression[] = [];
static getOrCreate(compileElement: CompileElement, eventTarget: string, eventName: string,
targetEventListeners: CompileEventListener[]): CompileEventListener {
var listener = targetEventListeners.find(listener => listener.eventTarget == eventTarget &&
listener.eventName == eventName);
if (isBlank(listener)) {
listener = new CompileEventListener(compileElement, eventTarget, eventName,
targetEventListeners.length);
targetEventListeners.push(listener);
}
return listener;
}
constructor(public compileElement: CompileElement, public eventTarget: string,
public eventName: string, listenerIndex: number) {
this._method = new CompileMethod(compileElement.view);
this._methodName =
`_handle_${santitizeEventName(eventName)}_${compileElement.nodeIndex}_${listenerIndex}`;
this._eventParam =
new o.FnParam(EventHandlerVars.event.name,
o.importType(this.compileElement.view.genConfig.renderTypes.renderEvent));
}
addAction(hostEvent: BoundEventAst, directive: CompileDirectiveMetadata,
directiveInstance: o.Expression) {
if (isPresent(directive) && directive.isComponent) {
this._hasComponentHostListener = true;
}
this._method.resetDebugInfo(this.compileElement.nodeIndex, hostEvent);
var context = isPresent(directiveInstance) ? directiveInstance :
this.compileElement.view.componentContext;
var actionStmts = convertCdStatementToIr(this.compileElement.view, context, hostEvent.handler);
var lastIndex = actionStmts.length - 1;
if (lastIndex >= 0) {
var lastStatement = actionStmts[lastIndex];
var returnExpr = convertStmtIntoExpression(lastStatement);
var preventDefaultVar = o.variable(`pd_${this._actionResultExprs.length}`);
this._actionResultExprs.push(preventDefaultVar);
if (isPresent(returnExpr)) {
// Note: We need to cast the result of the method call to dynamic,
// as it might be a void method!
actionStmts[lastIndex] =
preventDefaultVar.set(returnExpr.cast(o.DYNAMIC_TYPE).notIdentical(o.literal(false)))
.toDeclStmt(null, [o.StmtModifier.Final]);
}
}
this._method.addStmts(actionStmts);
}
finishMethod() {
var markPathToRootStart = this._hasComponentHostListener ?
this.compileElement.appElement.prop('componentView') :
o.THIS_EXPR;
var resultExpr: o.Expression = o.literal(true);
this._actionResultExprs.forEach((expr) => { resultExpr = resultExpr.and(expr); });
var stmts =
(<o.Statement[]>[markPathToRootStart.callMethod('markPathToRootAsCheckOnce', []).toStmt()])
.concat(this._method.finish())
.concat([new o.ReturnStatement(resultExpr)]);
// private is fine here as no child view will reference the event handler...
this.compileElement.view.eventHandlerMethods.push(new o.ClassMethod(
this._methodName, [this._eventParam], stmts, o.BOOL_TYPE, [o.StmtModifier.Private]));
}
listenToRenderer() {
var listenExpr;
var eventListener = o.THIS_EXPR.callMethod(
'eventHandler',
[o.THIS_EXPR.prop(this._methodName).callMethod(o.BuiltinMethod.bind, [o.THIS_EXPR])]);
if (isPresent(this.eventTarget)) {
listenExpr = ViewProperties.renderer.callMethod(
'listenGlobal', [o.literal(this.eventTarget), o.literal(this.eventName), eventListener]);
} else {
listenExpr = ViewProperties.renderer.callMethod(
'listen', [this.compileElement.renderNode, o.literal(this.eventName), eventListener]);
}
var disposable = o.variable(`disposable_${this.compileElement.view.disposables.length}`);
this.compileElement.view.disposables.push(disposable);
// private is fine here as no child view will reference the event handler...
this.compileElement.view.createMethod.addStmt(
disposable.set(listenExpr).toDeclStmt(o.FUNCTION_TYPE, [o.StmtModifier.Private]));
}
listenToDirective(directiveInstance: o.Expression, observablePropName: string) {
var subscription = o.variable(`subscription_${this.compileElement.view.subscriptions.length}`);
this.compileElement.view.subscriptions.push(subscription);
var eventListener = o.THIS_EXPR.callMethod(
'eventHandler',
[o.THIS_EXPR.prop(this._methodName).callMethod(o.BuiltinMethod.bind, [o.THIS_EXPR])]);
this.compileElement.view.createMethod.addStmt(
subscription.set(directiveInstance.prop(observablePropName)
.callMethod(o.BuiltinMethod.SubscribeObservable, [eventListener]))
.toDeclStmt(null, [o.StmtModifier.Final]));
}
}
export function collectEventListeners(hostEvents: BoundEventAst[], dirs: DirectiveAst[],
compileElement: CompileElement): CompileEventListener[] {
var eventListeners: CompileEventListener[] = [];
hostEvents.forEach((hostEvent) => {
compileElement.view.bindings.push(new CompileBinding(compileElement, hostEvent));
var listener = CompileEventListener.getOrCreate(compileElement, hostEvent.target,
hostEvent.name, eventListeners);
listener.addAction(hostEvent, null, null);
});
ListWrapper.forEachWithIndex(dirs, (directiveAst, i) => {
var directiveInstance = compileElement.directiveInstances[i];
directiveAst.hostEvents.forEach((hostEvent) => {
compileElement.view.bindings.push(new CompileBinding(compileElement, hostEvent));
var listener = CompileEventListener.getOrCreate(compileElement, hostEvent.target,
hostEvent.name, eventListeners);
listener.addAction(hostEvent, directiveAst.directive, directiveInstance);
});
});
eventListeners.forEach((listener) => listener.finishMethod());
return eventListeners;
}
export function bindDirectiveOutputs(directiveAst: DirectiveAst, directiveInstance: o.Expression,
eventListeners: CompileEventListener[]) {
StringMapWrapper.forEach(directiveAst.directive.outputs, (eventName, observablePropName) => {
eventListeners.filter(listener => listener.eventName == eventName)
.forEach(
(listener) => { listener.listenToDirective(directiveInstance, observablePropName); });
});
}
export function bindRenderOutputs(eventListeners: CompileEventListener[]) {
eventListeners.forEach(listener => listener.listenToRenderer());
}
function convertStmtIntoExpression(stmt: o.Statement): o.Expression {
if (stmt instanceof o.ExpressionStatement) {
return stmt.expr;
} else if (stmt instanceof o.ReturnStatement) {
return stmt.value;
}
return null;
}
function santitizeEventName(name: string): string {
return StringWrapper.replaceAll(name, /[^a-zA-Z_]/g, '_');
}

View File

@ -0,0 +1,252 @@
import * as cdAst from '../expression_parser/ast';
import * as o from '../output/output_ast';
import {Identifiers} from '../identifiers';
import {BaseException} from 'angular2/src/facade/exceptions';
import {isBlank, isPresent, isArray} from 'angular2/src/facade/lang';
var IMPLICIT_RECEIVER = o.variable('#implicit');
export interface NameResolver {
callPipe(name: string, input: o.Expression, args: o.Expression[]): o.Expression;
getLocal(name: string): o.Expression;
createLiteralArray(values: o.Expression[]): o.Expression;
createLiteralMap(values: Array<Array<string | o.Expression>>): o.Expression;
}
export class ExpressionWithWrappedValueInfo {
constructor(public expression: o.Expression, public needsValueUnwrapper: boolean) {}
}
export function convertCdExpressionToIr(
nameResolver: NameResolver, implicitReceiver: o.Expression, expression: cdAst.AST,
valueUnwrapper: o.ReadVarExpr): ExpressionWithWrappedValueInfo {
var visitor = new _AstToIrVisitor(nameResolver, implicitReceiver, valueUnwrapper);
var irAst: o.Expression = expression.visit(visitor, _Mode.Expression);
return new ExpressionWithWrappedValueInfo(irAst, visitor.needsValueUnwrapper);
}
export function convertCdStatementToIr(nameResolver: NameResolver, implicitReceiver: o.Expression,
stmt: cdAst.AST): o.Statement[] {
var visitor = new _AstToIrVisitor(nameResolver, implicitReceiver, null);
var statements = [];
flattenStatements(stmt.visit(visitor, _Mode.Statement), statements);
return statements;
}
enum _Mode {
Statement,
Expression
}
function ensureStatementMode(mode: _Mode, ast: cdAst.AST) {
if (mode !== _Mode.Statement) {
throw new BaseException(`Expected a statement, but saw ${ast}`);
}
}
function ensureExpressionMode(mode: _Mode, ast: cdAst.AST) {
if (mode !== _Mode.Expression) {
throw new BaseException(`Expected an expression, but saw ${ast}`);
}
}
function convertToStatementIfNeeded(mode: _Mode, expr: o.Expression): o.Expression | o.Statement {
if (mode === _Mode.Statement) {
return expr.toStmt();
} else {
return expr;
}
}
class _AstToIrVisitor implements cdAst.AstVisitor {
public needsValueUnwrapper: boolean = false;
constructor(private _nameResolver: NameResolver, private _implicitReceiver: o.Expression,
private _valueUnwrapper: o.ReadVarExpr) {}
visitBinary(ast: cdAst.Binary, mode: _Mode): any {
var op;
switch (ast.operation) {
case '+':
op = o.BinaryOperator.Plus;
break;
case '-':
op = o.BinaryOperator.Minus;
break;
case '*':
op = o.BinaryOperator.Multiply;
break;
case '/':
op = o.BinaryOperator.Divide;
break;
case '%':
op = o.BinaryOperator.Modulo;
break;
case '&&':
op = o.BinaryOperator.And;
break;
case '||':
op = o.BinaryOperator.Or;
break;
case '==':
op = o.BinaryOperator.Equals;
break;
case '!=':
op = o.BinaryOperator.NotEquals;
break;
case '===':
op = o.BinaryOperator.Identical;
break;
case '!==':
op = o.BinaryOperator.NotIdentical;
break;
case '<':
op = o.BinaryOperator.Lower;
break;
case '>':
op = o.BinaryOperator.Bigger;
break;
case '<=':
op = o.BinaryOperator.LowerEquals;
break;
case '>=':
op = o.BinaryOperator.BiggerEquals;
break;
default:
throw new BaseException(`Unsupported operation ${ast.operation}`);
}
return convertToStatementIfNeeded(
mode, new o.BinaryOperatorExpr(op, ast.left.visit(this, _Mode.Expression),
ast.right.visit(this, _Mode.Expression)));
}
visitChain(ast: cdAst.Chain, mode: _Mode): any {
ensureStatementMode(mode, ast);
return this.visitAll(ast.expressions, mode);
}
visitConditional(ast: cdAst.Conditional, mode: _Mode): any {
var value: o.Expression = ast.condition.visit(this, _Mode.Expression);
return convertToStatementIfNeeded(
mode, value.conditional(ast.trueExp.visit(this, _Mode.Expression),
ast.falseExp.visit(this, _Mode.Expression)));
}
visitPipe(ast: cdAst.BindingPipe, mode: _Mode): any {
var input = ast.exp.visit(this, _Mode.Expression);
var args = this.visitAll(ast.args, _Mode.Expression);
var value = this._nameResolver.callPipe(ast.name, input, args);
this.needsValueUnwrapper = true;
return convertToStatementIfNeeded(mode, this._valueUnwrapper.callMethod('unwrap', [value]));
}
visitFunctionCall(ast: cdAst.FunctionCall, mode: _Mode): any {
return convertToStatementIfNeeded(mode, ast.target.visit(this, _Mode.Expression)
.callFn(this.visitAll(ast.args, _Mode.Expression)));
}
visitImplicitReceiver(ast: cdAst.ImplicitReceiver, mode: _Mode): any {
ensureExpressionMode(mode, ast);
return IMPLICIT_RECEIVER;
}
visitInterpolation(ast: cdAst.Interpolation, mode: _Mode): any {
ensureExpressionMode(mode, ast);
var args = [o.literal(ast.expressions.length)];
for (var i = 0; i < ast.strings.length - 1; i++) {
args.push(o.literal(ast.strings[i]));
args.push(ast.expressions[i].visit(this, _Mode.Expression));
}
args.push(o.literal(ast.strings[ast.strings.length - 1]));
return o.importExpr(Identifiers.interpolate).callFn(args);
}
visitKeyedRead(ast: cdAst.KeyedRead, mode: _Mode): any {
return convertToStatementIfNeeded(
mode, ast.obj.visit(this, _Mode.Expression).key(ast.key.visit(this, _Mode.Expression)));
}
visitKeyedWrite(ast: cdAst.KeyedWrite, mode: _Mode): any {
var obj: o.Expression = ast.obj.visit(this, _Mode.Expression);
var key: o.Expression = ast.key.visit(this, _Mode.Expression);
var value: o.Expression = ast.value.visit(this, _Mode.Expression);
return convertToStatementIfNeeded(mode, obj.key(key).set(value));
}
visitLiteralArray(ast: cdAst.LiteralArray, mode: _Mode): any {
return convertToStatementIfNeeded(
mode, this._nameResolver.createLiteralArray(this.visitAll(ast.expressions, mode)));
}
visitLiteralMap(ast: cdAst.LiteralMap, mode: _Mode): any {
var parts = [];
for (var i = 0; i < ast.keys.length; i++) {
parts.push([ast.keys[i], ast.values[i].visit(this, _Mode.Expression)]);
}
return convertToStatementIfNeeded(mode, this._nameResolver.createLiteralMap(parts));
}
visitLiteralPrimitive(ast: cdAst.LiteralPrimitive, mode: _Mode): any {
return convertToStatementIfNeeded(mode, o.literal(ast.value));
}
visitMethodCall(ast: cdAst.MethodCall, mode: _Mode): any {
var args = this.visitAll(ast.args, _Mode.Expression);
var result = null;
var receiver = ast.receiver.visit(this, _Mode.Expression);
if (receiver === IMPLICIT_RECEIVER) {
var varExpr = this._nameResolver.getLocal(ast.name);
if (isPresent(varExpr)) {
result = varExpr.callFn(args);
} else {
receiver = this._implicitReceiver;
}
}
if (isBlank(result)) {
result = receiver.callMethod(ast.name, args);
}
return convertToStatementIfNeeded(mode, result);
}
visitPrefixNot(ast: cdAst.PrefixNot, mode: _Mode): any {
return convertToStatementIfNeeded(mode, o.not(ast.expression.visit(this, _Mode.Expression)));
}
visitPropertyRead(ast: cdAst.PropertyRead, mode: _Mode): any {
var result = null;
var receiver = ast.receiver.visit(this, _Mode.Expression);
if (receiver === IMPLICIT_RECEIVER) {
result = this._nameResolver.getLocal(ast.name);
if (isBlank(result)) {
receiver = this._implicitReceiver;
}
}
if (isBlank(result)) {
result = receiver.prop(ast.name);
}
return convertToStatementIfNeeded(mode, result);
}
visitPropertyWrite(ast: cdAst.PropertyWrite, mode: _Mode): any {
var receiver: o.Expression = ast.receiver.visit(this, _Mode.Expression);
if (receiver === IMPLICIT_RECEIVER) {
var varExpr = this._nameResolver.getLocal(ast.name);
if (isPresent(varExpr)) {
throw new BaseException('Cannot assign to a reference or variable!');
}
receiver = this._implicitReceiver;
}
return convertToStatementIfNeeded(
mode, receiver.prop(ast.name).set(ast.value.visit(this, _Mode.Expression)));
}
visitSafePropertyRead(ast: cdAst.SafePropertyRead, mode: _Mode): any {
var receiver = ast.receiver.visit(this, _Mode.Expression);
return convertToStatementIfNeeded(
mode, receiver.isBlank().conditional(o.NULL_EXPR, receiver.prop(ast.name)));
}
visitSafeMethodCall(ast: cdAst.SafeMethodCall, mode: _Mode): any {
var receiver = ast.receiver.visit(this, _Mode.Expression);
var args = this.visitAll(ast.args, _Mode.Expression);
return convertToStatementIfNeeded(
mode, receiver.isBlank().conditional(o.NULL_EXPR, receiver.callMethod(ast.name, args)));
}
visitAll(asts: cdAst.AST[], mode: _Mode): any { return asts.map(ast => ast.visit(this, mode)); }
visitQuote(ast: cdAst.Quote, mode: _Mode): any {
throw new BaseException('Quotes are not supported for evaluation!');
}
}
function flattenStatements(arg: any, output: o.Statement[]) {
if (isArray(arg)) {
(<any[]>arg).forEach((entry) => flattenStatements(entry, output));
} else {
output.push(arg);
}
}

View File

@ -0,0 +1,87 @@
import * as o from '../output/output_ast';
import {DetectChangesVars, ChangeDetectorStateEnum} from './constants';
import {LifecycleHooks} from 'angular2/src/core/metadata/lifecycle_hooks';
import {CompileDirectiveMetadata, CompilePipeMetadata} from '../compile_metadata';
import {DirectiveAst} from '../template_ast';
import {CompileElement} from './compile_element';
import {CompileView} from './compile_view';
var STATE_IS_NEVER_CHECKED =
o.THIS_EXPR.prop('cdState').identical(ChangeDetectorStateEnum.NeverChecked);
var NOT_THROW_ON_CHANGES = o.not(DetectChangesVars.throwOnChange);
export function bindDirectiveDetectChangesLifecycleCallbacks(
directiveAst: DirectiveAst, directiveInstance: o.Expression, compileElement: CompileElement) {
var view = compileElement.view;
var detectChangesInInputsMethod = view.detectChangesInInputsMethod;
var lifecycleHooks = directiveAst.directive.lifecycleHooks;
if (lifecycleHooks.indexOf(LifecycleHooks.OnChanges) !== -1 && directiveAst.inputs.length > 0) {
detectChangesInInputsMethod.addStmt(new o.IfStmt(
DetectChangesVars.changes.notIdentical(o.NULL_EXPR),
[directiveInstance.callMethod('ngOnChanges', [DetectChangesVars.changes]).toStmt()]));
}
if (lifecycleHooks.indexOf(LifecycleHooks.OnInit) !== -1) {
detectChangesInInputsMethod.addStmt(
new o.IfStmt(STATE_IS_NEVER_CHECKED.and(NOT_THROW_ON_CHANGES),
[directiveInstance.callMethod('ngOnInit', []).toStmt()]));
}
if (lifecycleHooks.indexOf(LifecycleHooks.DoCheck) !== -1) {
detectChangesInInputsMethod.addStmt(new o.IfStmt(
NOT_THROW_ON_CHANGES, [directiveInstance.callMethod('ngDoCheck', []).toStmt()]));
}
}
export function bindDirectiveAfterContentLifecycleCallbacks(directiveMeta: CompileDirectiveMetadata,
directiveInstance: o.Expression,
compileElement: CompileElement) {
var view = compileElement.view;
var lifecycleHooks = directiveMeta.lifecycleHooks;
var afterContentLifecycleCallbacksMethod = view.afterContentLifecycleCallbacksMethod;
afterContentLifecycleCallbacksMethod.resetDebugInfo(compileElement.nodeIndex,
compileElement.sourceAst);
if (lifecycleHooks.indexOf(LifecycleHooks.AfterContentInit) !== -1) {
afterContentLifecycleCallbacksMethod.addStmt(new o.IfStmt(
STATE_IS_NEVER_CHECKED, [directiveInstance.callMethod('ngAfterContentInit', []).toStmt()]));
}
if (lifecycleHooks.indexOf(LifecycleHooks.AfterContentChecked) !== -1) {
afterContentLifecycleCallbacksMethod.addStmt(
directiveInstance.callMethod('ngAfterContentChecked', []).toStmt());
}
}
export function bindDirectiveAfterViewLifecycleCallbacks(directiveMeta: CompileDirectiveMetadata,
directiveInstance: o.Expression,
compileElement: CompileElement) {
var view = compileElement.view;
var lifecycleHooks = directiveMeta.lifecycleHooks;
var afterViewLifecycleCallbacksMethod = view.afterViewLifecycleCallbacksMethod;
afterViewLifecycleCallbacksMethod.resetDebugInfo(compileElement.nodeIndex,
compileElement.sourceAst);
if (lifecycleHooks.indexOf(LifecycleHooks.AfterViewInit) !== -1) {
afterViewLifecycleCallbacksMethod.addStmt(new o.IfStmt(
STATE_IS_NEVER_CHECKED, [directiveInstance.callMethod('ngAfterViewInit', []).toStmt()]));
}
if (lifecycleHooks.indexOf(LifecycleHooks.AfterViewChecked) !== -1) {
afterViewLifecycleCallbacksMethod.addStmt(
directiveInstance.callMethod('ngAfterViewChecked', []).toStmt());
}
}
export function bindDirectiveDestroyLifecycleCallbacks(directiveMeta: CompileDirectiveMetadata,
directiveInstance: o.Expression,
compileElement: CompileElement) {
var onDestroyMethod = compileElement.view.destroyMethod;
onDestroyMethod.resetDebugInfo(compileElement.nodeIndex, compileElement.sourceAst);
if (directiveMeta.lifecycleHooks.indexOf(LifecycleHooks.OnDestroy) !== -1) {
onDestroyMethod.addStmt(directiveInstance.callMethod('ngOnDestroy', []).toStmt());
}
}
export function bindPipeDestroyLifecycleCallbacks(pipeMeta: CompilePipeMetadata,
pipeInstance: o.Expression, view: CompileView) {
var onDestroyMethod = view.destroyMethod;
if (pipeMeta.lifecycleHooks.indexOf(LifecycleHooks.OnDestroy) !== -1) {
onDestroyMethod.addStmt(pipeInstance.callMethod('ngOnDestroy', []).toStmt());
}
}

View File

@ -0,0 +1,210 @@
import * as cdAst from '../expression_parser/ast';
import * as o from '../output/output_ast';
import {Identifiers} from '../identifiers';
import {DetectChangesVars} from './constants';
import {
BoundTextAst,
BoundElementPropertyAst,
DirectiveAst,
PropertyBindingType,
TemplateAst
} from '../template_ast';
import {isBlank, isPresent, isArray} from 'angular2/src/facade/lang';
import {CompileView} from './compile_view';
import {CompileElement, CompileNode} from './compile_element';
import {CompileMethod} from './compile_method';
import {LifecycleHooks} from 'angular2/src/core/metadata/lifecycle_hooks';
import {isDefaultChangeDetectionStrategy} from 'angular2/src/core/change_detection/constants';
import {camelCaseToDashCase} from '../util';
import {convertCdExpressionToIr} from './expression_converter';
import {CompileBinding} from './compile_binding';
function createBindFieldExpr(exprIndex: number): o.ReadPropExpr {
return o.THIS_EXPR.prop(`_expr_${exprIndex}`);
}
function createCurrValueExpr(exprIndex: number): o.ReadVarExpr {
return o.variable(`currVal_${exprIndex}`);
}
function bind(view: CompileView, currValExpr: o.ReadVarExpr, fieldExpr: o.ReadPropExpr,
parsedExpression: cdAst.AST, context: o.Expression, actions: o.Statement[],
method: CompileMethod) {
var checkExpression =
convertCdExpressionToIr(view, context, parsedExpression, DetectChangesVars.valUnwrapper);
if (isBlank(checkExpression.expression)) {
// e.g. an empty expression was given
return;
}
// private is fine here as no child view will reference the cached value...
view.fields.push(new o.ClassField(fieldExpr.name, null, [o.StmtModifier.Private]));
view.createMethod.addStmt(
o.THIS_EXPR.prop(fieldExpr.name).set(o.importExpr(Identifiers.uninitialized)).toStmt());
if (checkExpression.needsValueUnwrapper) {
var initValueUnwrapperStmt = DetectChangesVars.valUnwrapper.callMethod('reset', []).toStmt();
method.addStmt(initValueUnwrapperStmt);
}
method.addStmt(
currValExpr.set(checkExpression.expression).toDeclStmt(null, [o.StmtModifier.Final]));
var condition: o.Expression =
o.importExpr(Identifiers.checkBinding)
.callFn([DetectChangesVars.throwOnChange, fieldExpr, currValExpr]);
if (checkExpression.needsValueUnwrapper) {
condition = DetectChangesVars.valUnwrapper.prop('hasWrappedValue').or(condition);
}
method.addStmt(new o.IfStmt(
condition,
actions.concat([<o.Statement>o.THIS_EXPR.prop(fieldExpr.name).set(currValExpr).toStmt()])));
}
export function bindRenderText(boundText: BoundTextAst, compileNode: CompileNode,
view: CompileView) {
var bindingIndex = view.bindings.length;
view.bindings.push(new CompileBinding(compileNode, boundText));
var currValExpr = createCurrValueExpr(bindingIndex);
var valueField = createBindFieldExpr(bindingIndex);
view.detectChangesRenderPropertiesMethod.resetDebugInfo(compileNode.nodeIndex, boundText);
bind(view, currValExpr, valueField, boundText.value, view.componentContext,
[
o.THIS_EXPR.prop('renderer')
.callMethod('setText', [compileNode.renderNode, currValExpr])
.toStmt()
],
view.detectChangesRenderPropertiesMethod);
}
function bindAndWriteToRenderer(boundProps: BoundElementPropertyAst[], context: o.Expression,
compileElement: CompileElement) {
var view = compileElement.view;
var renderNode = compileElement.renderNode;
boundProps.forEach((boundProp) => {
var bindingIndex = view.bindings.length;
view.bindings.push(new CompileBinding(compileElement, boundProp));
view.detectChangesRenderPropertiesMethod.resetDebugInfo(compileElement.nodeIndex, boundProp);
var fieldExpr = createBindFieldExpr(bindingIndex);
var currValExpr = createCurrValueExpr(bindingIndex);
var renderMethod: string;
var renderValue: o.Expression = currValExpr;
var updateStmts = [];
switch (boundProp.type) {
case PropertyBindingType.Property:
renderMethod = 'setElementProperty';
if (view.genConfig.logBindingUpdate) {
updateStmts.push(logBindingUpdateStmt(renderNode, boundProp.name, currValExpr));
}
break;
case PropertyBindingType.Attribute:
renderMethod = 'setElementAttribute';
renderValue =
renderValue.isBlank().conditional(o.NULL_EXPR, renderValue.callMethod('toString', []));
break;
case PropertyBindingType.Class:
renderMethod = 'setElementClass';
break;
case PropertyBindingType.Style:
renderMethod = 'setElementStyle';
var strValue: o.Expression = renderValue.callMethod('toString', []);
if (isPresent(boundProp.unit)) {
strValue = strValue.plus(o.literal(boundProp.unit));
}
renderValue = renderValue.isBlank().conditional(o.NULL_EXPR, strValue);
break;
}
updateStmts.push(
o.THIS_EXPR.prop('renderer')
.callMethod(renderMethod, [renderNode, o.literal(boundProp.name), renderValue])
.toStmt());
bind(view, currValExpr, fieldExpr, boundProp.value, context, updateStmts,
view.detectChangesRenderPropertiesMethod);
});
}
export function bindRenderInputs(boundProps: BoundElementPropertyAst[],
compileElement: CompileElement): void {
bindAndWriteToRenderer(boundProps, compileElement.view.componentContext, compileElement);
}
export function bindDirectiveHostProps(directiveAst: DirectiveAst, directiveInstance: o.Expression,
compileElement: CompileElement): void {
bindAndWriteToRenderer(directiveAst.hostProperties, directiveInstance, compileElement);
}
export function bindDirectiveInputs(directiveAst: DirectiveAst, directiveInstance: o.Expression,
compileElement: CompileElement) {
if (directiveAst.inputs.length === 0) {
return;
}
var view = compileElement.view;
var detectChangesInInputsMethod = view.detectChangesInInputsMethod;
detectChangesInInputsMethod.resetDebugInfo(compileElement.nodeIndex, compileElement.sourceAst);
var lifecycleHooks = directiveAst.directive.lifecycleHooks;
var calcChangesMap = lifecycleHooks.indexOf(LifecycleHooks.OnChanges) !== -1;
var isOnPushComp = directiveAst.directive.isComponent &&
!isDefaultChangeDetectionStrategy(directiveAst.directive.changeDetection);
if (calcChangesMap) {
detectChangesInInputsMethod.addStmt(DetectChangesVars.changes.set(o.NULL_EXPR).toStmt());
}
if (isOnPushComp) {
detectChangesInInputsMethod.addStmt(DetectChangesVars.changed.set(o.literal(false)).toStmt());
}
directiveAst.inputs.forEach((input) => {
var bindingIndex = view.bindings.length;
view.bindings.push(new CompileBinding(compileElement, input));
detectChangesInInputsMethod.resetDebugInfo(compileElement.nodeIndex, input);
var fieldExpr = createBindFieldExpr(bindingIndex);
var currValExpr = createCurrValueExpr(bindingIndex);
var statements: o.Statement[] =
[directiveInstance.prop(input.directiveName).set(currValExpr).toStmt()];
if (calcChangesMap) {
statements.push(new o.IfStmt(DetectChangesVars.changes.identical(o.NULL_EXPR), [
DetectChangesVars.changes.set(o.literalMap([], new o.MapType(
o.importType(Identifiers.SimpleChange))))
.toStmt()
]));
statements.push(
DetectChangesVars.changes.key(o.literal(input.directiveName))
.set(o.importExpr(Identifiers.SimpleChange).instantiate([fieldExpr, currValExpr]))
.toStmt());
}
if (isOnPushComp) {
statements.push(DetectChangesVars.changed.set(o.literal(true)).toStmt());
}
if (view.genConfig.logBindingUpdate) {
statements.push(
logBindingUpdateStmt(compileElement.renderNode, input.directiveName, currValExpr));
}
bind(view, currValExpr, fieldExpr, input.value, view.componentContext, statements,
detectChangesInInputsMethod);
});
if (isOnPushComp) {
detectChangesInInputsMethod.addStmt(new o.IfStmt(DetectChangesVars.changed, [
compileElement.appElement.prop('componentView')
.callMethod('markAsCheckOnce', [])
.toStmt()
]));
}
}
function logBindingUpdateStmt(renderNode: o.Expression, propName: string,
value: o.Expression): o.Statement {
return o.THIS_EXPR.prop('renderer')
.callMethod('setBindingDebugInfo',
[
renderNode,
o.literal(`ng-reflect-${camelCaseToDashCase(propName)}`),
value.isBlank().conditional(o.NULL_EXPR, value.callMethod('toString', []))
])
.toStmt();
}

View File

@ -0,0 +1,99 @@
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import * as o from '../output/output_ast';
import {
CompileTokenMetadata,
CompileDirectiveMetadata,
CompileIdentifierMetadata
} from '../compile_metadata';
import {CompileView} from './compile_view';
import {Identifiers} from '../identifiers';
export function getPropertyInView(property: o.Expression, callingView: CompileView,
definedView: CompileView): o.Expression {
if (callingView === definedView) {
return property;
} else {
var viewProp: o.Expression = o.THIS_EXPR;
var currView: CompileView = callingView;
while (currView !== definedView && isPresent(currView.declarationElement.view)) {
currView = currView.declarationElement.view;
viewProp = viewProp.prop('parent');
}
if (currView !== definedView) {
throw new BaseException(
`Internal error: Could not calculate a property in a parent view: ${property}`);
}
if (property instanceof o.ReadPropExpr) {
let readPropExpr: o.ReadPropExpr = property;
// Note: Don't cast for members of the AppView base class...
if (definedView.fields.some((field) => field.name == readPropExpr.name) ||
definedView.getters.some((field) => field.name == readPropExpr.name)) {
viewProp = viewProp.cast(definedView.classType);
}
}
return o.replaceVarInExpression(o.THIS_EXPR.name, viewProp, property);
}
}
export function injectFromViewParentInjector(token: CompileTokenMetadata,
optional: boolean): o.Expression {
var args = [createDiTokenExpression(token)];
if (optional) {
args.push(o.NULL_EXPR);
}
return o.THIS_EXPR.prop('parentInjector').callMethod('get', args);
}
export function getViewFactoryName(component: CompileDirectiveMetadata,
embeddedTemplateIndex: number): string {
return `viewFactory_${component.type.name}${embeddedTemplateIndex}`;
}
export function createDiTokenExpression(token: CompileTokenMetadata): o.Expression {
if (isPresent(token.value)) {
return o.literal(token.value);
} else if (token.identifierIsInstance) {
return o.importExpr(token.identifier)
.instantiate([], o.importType(token.identifier, [], [o.TypeModifier.Const]));
} else {
return o.importExpr(token.identifier);
}
}
export function createFlatArray(expressions: o.Expression[]): o.Expression {
var lastNonArrayExpressions = [];
var result: o.Expression = o.literalArr([]);
for (var i = 0; i < expressions.length; i++) {
var expr = expressions[i];
if (expr.type instanceof o.ArrayType) {
if (lastNonArrayExpressions.length > 0) {
result =
result.callMethod(o.BuiltinMethod.ConcatArray, [o.literalArr(lastNonArrayExpressions)]);
lastNonArrayExpressions = [];
}
result = result.callMethod(o.BuiltinMethod.ConcatArray, [expr]);
} else {
lastNonArrayExpressions.push(expr);
}
}
if (lastNonArrayExpressions.length > 0) {
result =
result.callMethod(o.BuiltinMethod.ConcatArray, [o.literalArr(lastNonArrayExpressions)]);
}
return result;
}
export function createPureProxy(fn: o.Expression, argCount: number, pureProxyProp: o.ReadPropExpr,
view: CompileView) {
view.fields.push(new o.ClassField(pureProxyProp.name, null));
var pureProxyId =
argCount < Identifiers.pureProxies.length ? Identifiers.pureProxies[argCount] : null;
if (isBlank(pureProxyId)) {
throw new BaseException(`Unsupported number of argument for pure functions: ${argCount}`);
}
view.createMethod.addStmt(
o.THIS_EXPR.prop(pureProxyProp.name).set(o.importExpr(pureProxyId).callFn([fn])).toStmt());
}

View File

@ -0,0 +1,121 @@
import {
ListWrapper,
} from 'angular2/src/facade/collection';
import {
TemplateAst,
TemplateAstVisitor,
NgContentAst,
EmbeddedTemplateAst,
ElementAst,
ReferenceAst,
VariableAst,
BoundEventAst,
BoundElementPropertyAst,
AttrAst,
BoundTextAst,
TextAst,
DirectiveAst,
BoundDirectivePropertyAst,
templateVisitAll,
PropertyBindingType,
ProviderAst
} from '../template_ast';
import {
bindRenderText,
bindRenderInputs,
bindDirectiveInputs,
bindDirectiveHostProps
} from './property_binder';
import {bindRenderOutputs, collectEventListeners, bindDirectiveOutputs} from './event_binder';
import {
bindDirectiveAfterContentLifecycleCallbacks,
bindDirectiveAfterViewLifecycleCallbacks,
bindDirectiveDestroyLifecycleCallbacks,
bindPipeDestroyLifecycleCallbacks,
bindDirectiveDetectChangesLifecycleCallbacks
} from './lifecycle_binder';
import {CompileView} from './compile_view';
import {CompileElement, CompileNode} from './compile_element';
export function bindView(view: CompileView, parsedTemplate: TemplateAst[]): void {
var visitor = new ViewBinderVisitor(view);
templateVisitAll(visitor, parsedTemplate);
view.pipes.forEach(
(pipe) => { bindPipeDestroyLifecycleCallbacks(pipe.meta, pipe.instance, pipe.view); });
}
class ViewBinderVisitor implements TemplateAstVisitor {
private _nodeIndex: number = 0;
constructor(public view: CompileView) {}
visitBoundText(ast: BoundTextAst, parent: CompileElement): any {
var node = this.view.nodes[this._nodeIndex++];
bindRenderText(ast, node, this.view);
return null;
}
visitText(ast: TextAst, parent: CompileElement): any {
this._nodeIndex++;
return null;
}
visitNgContent(ast: NgContentAst, parent: CompileElement): any { return null; }
visitElement(ast: ElementAst, parent: CompileElement): any {
var compileElement = <CompileElement>this.view.nodes[this._nodeIndex++];
var eventListeners = collectEventListeners(ast.outputs, ast.directives, compileElement);
bindRenderInputs(ast.inputs, compileElement);
bindRenderOutputs(eventListeners);
ListWrapper.forEachWithIndex(ast.directives, (directiveAst, index) => {
var directiveInstance = compileElement.directiveInstances[index];
bindDirectiveInputs(directiveAst, directiveInstance, compileElement);
bindDirectiveDetectChangesLifecycleCallbacks(directiveAst, directiveInstance, compileElement);
bindDirectiveHostProps(directiveAst, directiveInstance, compileElement);
bindDirectiveOutputs(directiveAst, directiveInstance, eventListeners);
});
templateVisitAll(this, ast.children, compileElement);
// afterContent and afterView lifecycles need to be called bottom up
// so that children are notified before parents
ListWrapper.forEachWithIndex(ast.directives, (directiveAst, index) => {
var directiveInstance = compileElement.directiveInstances[index];
bindDirectiveAfterContentLifecycleCallbacks(directiveAst.directive, directiveInstance,
compileElement);
bindDirectiveAfterViewLifecycleCallbacks(directiveAst.directive, directiveInstance,
compileElement);
bindDirectiveDestroyLifecycleCallbacks(directiveAst.directive, directiveInstance,
compileElement);
});
return null;
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, parent: CompileElement): any {
var compileElement = <CompileElement>this.view.nodes[this._nodeIndex++];
var eventListeners = collectEventListeners(ast.outputs, ast.directives, compileElement);
ListWrapper.forEachWithIndex(ast.directives, (directiveAst, index) => {
var directiveInstance = compileElement.directiveInstances[index];
bindDirectiveInputs(directiveAst, directiveInstance, compileElement);
bindDirectiveDetectChangesLifecycleCallbacks(directiveAst, directiveInstance, compileElement);
bindDirectiveOutputs(directiveAst, directiveInstance, eventListeners);
bindDirectiveAfterContentLifecycleCallbacks(directiveAst.directive, directiveInstance,
compileElement);
bindDirectiveAfterViewLifecycleCallbacks(directiveAst.directive, directiveInstance,
compileElement);
bindDirectiveDestroyLifecycleCallbacks(directiveAst.directive, directiveInstance,
compileElement);
});
bindView(compileElement.embeddedView, ast.children);
return null;
}
visitAttr(ast: AttrAst, ctx: any): any { return null; }
visitDirective(ast: DirectiveAst, ctx: any): any { return null; }
visitEvent(ast: BoundEventAst, eventTargetAndNames: Map<string, BoundEventAst>): any {
return null;
}
visitReference(ast: ReferenceAst, ctx: any): any { return null; }
visitVariable(ast: VariableAst, ctx: any): any { return null; }
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any { return null; }
visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; }
}

View File

@ -0,0 +1,580 @@
import {isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang';
import {ListWrapper, StringMapWrapper, SetWrapper} from 'angular2/src/facade/collection';
import * as o from '../output/output_ast';
import {Identifiers, identifierToken} from '../identifiers';
import {
ViewConstructorVars,
InjectMethodVars,
DetectChangesVars,
ViewTypeEnum,
ViewEncapsulationEnum,
ChangeDetectionStrategyEnum,
ViewProperties
} from './constants';
import {
ChangeDetectionStrategy,
isDefaultChangeDetectionStrategy
} from 'angular2/src/core/change_detection/change_detection';
import {CompileView} from './compile_view';
import {CompileElement, CompileNode} from './compile_element';
import {
TemplateAst,
TemplateAstVisitor,
NgContentAst,
EmbeddedTemplateAst,
ElementAst,
ReferenceAst,
VariableAst,
BoundEventAst,
BoundElementPropertyAst,
AttrAst,
BoundTextAst,
TextAst,
DirectiveAst,
BoundDirectivePropertyAst,
templateVisitAll,
PropertyBindingType,
ProviderAst
} from '../template_ast';
import {getViewFactoryName, createFlatArray, createDiTokenExpression} from './util';
import {ViewType} from 'angular2/src/core/linker/view_type';
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';
import {
CompileIdentifierMetadata,
CompileDirectiveMetadata,
CompileTokenMetadata
} from '../compile_metadata';
const IMPLICIT_TEMPLATE_VAR = '\$implicit';
const CLASS_ATTR = 'class';
const STYLE_ATTR = 'style';
var parentRenderNodeVar = o.variable('parentRenderNode');
var rootSelectorVar = o.variable('rootSelector');
export class ViewCompileDependency {
constructor(public comp: CompileDirectiveMetadata,
public factoryPlaceholder: CompileIdentifierMetadata) {}
}
export function buildView(view: CompileView, template: TemplateAst[],
targetDependencies: ViewCompileDependency[]): number {
var builderVisitor = new ViewBuilderVisitor(view, targetDependencies);
templateVisitAll(builderVisitor, template, view.declarationElement.isNull() ?
view.declarationElement :
view.declarationElement.parent);
return builderVisitor.nestedViewCount;
}
export function finishView(view: CompileView, targetStatements: o.Statement[]) {
view.afterNodes();
createViewTopLevelStmts(view, targetStatements);
view.nodes.forEach((node) => {
if (node instanceof CompileElement && node.hasEmbeddedView) {
finishView(node.embeddedView, targetStatements);
}
});
}
class ViewBuilderVisitor implements TemplateAstVisitor {
nestedViewCount: number = 0;
constructor(public view: CompileView, public targetDependencies: ViewCompileDependency[]) {}
private _isRootNode(parent: CompileElement): boolean { return parent.view !== this.view; }
private _addRootNodeAndProject(node: CompileNode, ngContentIndex: number,
parent: CompileElement) {
var vcAppEl =
(node instanceof CompileElement && node.hasViewContainer) ? node.appElement : null;
if (this._isRootNode(parent)) {
// store appElement as root node only for ViewContainers
if (this.view.viewType !== ViewType.COMPONENT) {
this.view.rootNodesOrAppElements.push(isPresent(vcAppEl) ? vcAppEl : node.renderNode);
}
} else if (isPresent(parent.component) && isPresent(ngContentIndex)) {
parent.addContentNode(ngContentIndex, isPresent(vcAppEl) ? vcAppEl : node.renderNode);
}
}
private _getParentRenderNode(parent: CompileElement): o.Expression {
if (this._isRootNode(parent)) {
if (this.view.viewType === ViewType.COMPONENT) {
return parentRenderNodeVar;
} else {
// root node of an embedded/host view
return o.NULL_EXPR;
}
} else {
return isPresent(parent.component) &&
parent.component.template.encapsulation !== ViewEncapsulation.Native ?
o.NULL_EXPR :
parent.renderNode;
}
}
visitBoundText(ast: BoundTextAst, parent: CompileElement): any {
return this._visitText(ast, '', ast.ngContentIndex, parent);
}
visitText(ast: TextAst, parent: CompileElement): any {
return this._visitText(ast, ast.value, ast.ngContentIndex, parent);
}
private _visitText(ast: TemplateAst, value: string, ngContentIndex: number,
parent: CompileElement): o.Expression {
var fieldName = `_text_${this.view.nodes.length}`;
this.view.fields.push(
new o.ClassField(fieldName, o.importType(this.view.genConfig.renderTypes.renderText)));
var renderNode = o.THIS_EXPR.prop(fieldName);
var compileNode = new CompileNode(parent, this.view, this.view.nodes.length, renderNode, ast);
var createRenderNode =
o.THIS_EXPR.prop(fieldName)
.set(ViewProperties.renderer.callMethod(
'createText',
[
this._getParentRenderNode(parent),
o.literal(value),
this.view.createMethod.resetDebugInfoExpr(this.view.nodes.length, ast)
]))
.toStmt();
this.view.nodes.push(compileNode);
this.view.createMethod.addStmt(createRenderNode);
this._addRootNodeAndProject(compileNode, ngContentIndex, parent);
return renderNode;
}
visitNgContent(ast: NgContentAst, parent: CompileElement): any {
// the projected nodes originate from a different view, so we don't
// have debug information for them...
this.view.createMethod.resetDebugInfo(null, ast);
var parentRenderNode = this._getParentRenderNode(parent);
var nodesExpression = ViewProperties.projectableNodes.key(
o.literal(ast.index),
new o.ArrayType(o.importType(this.view.genConfig.renderTypes.renderNode)));
if (parentRenderNode !== o.NULL_EXPR) {
this.view.createMethod.addStmt(
ViewProperties.renderer.callMethod(
'projectNodes',
[
parentRenderNode,
o.importExpr(Identifiers.flattenNestedViewRenderNodes)
.callFn([nodesExpression])
])
.toStmt());
} else if (this._isRootNode(parent)) {
if (this.view.viewType !== ViewType.COMPONENT) {
// store root nodes only for embedded/host views
this.view.rootNodesOrAppElements.push(nodesExpression);
}
} else {
if (isPresent(parent.component) && isPresent(ast.ngContentIndex)) {
parent.addContentNode(ast.ngContentIndex, nodesExpression);
}
}
return null;
}
visitElement(ast: ElementAst, parent: CompileElement): any {
var nodeIndex = this.view.nodes.length;
var createRenderNodeExpr;
var debugContextExpr = this.view.createMethod.resetDebugInfoExpr(nodeIndex, ast);
if (nodeIndex === 0 && this.view.viewType === ViewType.HOST) {
createRenderNodeExpr = o.THIS_EXPR.callMethod(
'selectOrCreateHostElement', [o.literal(ast.name), rootSelectorVar, debugContextExpr]);
} else {
createRenderNodeExpr = ViewProperties.renderer.callMethod(
'createElement',
[this._getParentRenderNode(parent), o.literal(ast.name), debugContextExpr]);
}
var fieldName = `_el_${nodeIndex}`;
this.view.fields.push(
new o.ClassField(fieldName, o.importType(this.view.genConfig.renderTypes.renderElement)));
this.view.createMethod.addStmt(o.THIS_EXPR.prop(fieldName).set(createRenderNodeExpr).toStmt());
var renderNode = o.THIS_EXPR.prop(fieldName);
var directives = ast.directives.map(directiveAst => directiveAst.directive);
var component = directives.find(directive => directive.isComponent);
var htmlAttrs = _readHtmlAttrs(ast.attrs);
var attrNameAndValues = _mergeHtmlAndDirectiveAttrs(htmlAttrs, directives);
for (var i = 0; i < attrNameAndValues.length; i++) {
var attrName = attrNameAndValues[i][0];
var attrValue = attrNameAndValues[i][1];
this.view.createMethod.addStmt(
ViewProperties.renderer.callMethod(
'setElementAttribute',
[renderNode, o.literal(attrName), o.literal(attrValue)])
.toStmt());
}
var compileElement =
new CompileElement(parent, this.view, nodeIndex, renderNode, ast, component, directives,
ast.providers, ast.hasViewContainer, false, ast.references);
this.view.nodes.push(compileElement);
var compViewExpr: o.ReadVarExpr = null;
if (isPresent(component)) {
var nestedComponentIdentifier =
new CompileIdentifierMetadata({name: getViewFactoryName(component, 0)});
this.targetDependencies.push(new ViewCompileDependency(component, nestedComponentIdentifier));
compViewExpr = o.variable(`compView_${nodeIndex}`);
compileElement.setComponentView(compViewExpr);
this.view.createMethod.addStmt(compViewExpr.set(o.importExpr(nestedComponentIdentifier)
.callFn([
ViewProperties.viewUtils,
compileElement.injector,
compileElement.appElement
]))
.toDeclStmt());
}
compileElement.beforeChildren();
this._addRootNodeAndProject(compileElement, ast.ngContentIndex, parent);
templateVisitAll(this, ast.children, compileElement);
compileElement.afterChildren(this.view.nodes.length - nodeIndex - 1);
if (isPresent(compViewExpr)) {
var codeGenContentNodes;
if (this.view.component.type.isHost) {
codeGenContentNodes = ViewProperties.projectableNodes;
} else {
codeGenContentNodes = o.literalArr(
compileElement.contentNodesByNgContentIndex.map(nodes => createFlatArray(nodes)));
}
this.view.createMethod.addStmt(
compViewExpr.callMethod('create',
[compileElement.getComponent(), codeGenContentNodes, o.NULL_EXPR])
.toStmt());
}
return null;
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, parent: CompileElement): any {
var nodeIndex = this.view.nodes.length;
var fieldName = `_anchor_${nodeIndex}`;
this.view.fields.push(
new o.ClassField(fieldName, o.importType(this.view.genConfig.renderTypes.renderComment)));
this.view.createMethod.addStmt(
o.THIS_EXPR.prop(fieldName)
.set(ViewProperties.renderer.callMethod(
'createTemplateAnchor',
[
this._getParentRenderNode(parent),
this.view.createMethod.resetDebugInfoExpr(nodeIndex, ast)
]))
.toStmt());
var renderNode = o.THIS_EXPR.prop(fieldName);
var templateVariableBindings = ast.variables.map(
varAst => [varAst.value.length > 0 ? varAst.value : IMPLICIT_TEMPLATE_VAR, varAst.name]);
var directives = ast.directives.map(directiveAst => directiveAst.directive);
var compileElement =
new CompileElement(parent, this.view, nodeIndex, renderNode, ast, null, directives,
ast.providers, ast.hasViewContainer, true, ast.references);
this.view.nodes.push(compileElement);
this.nestedViewCount++;
var embeddedView = new CompileView(
this.view.component, this.view.genConfig, this.view.pipeMetas, o.NULL_EXPR,
this.view.viewIndex + this.nestedViewCount, compileElement, templateVariableBindings);
this.nestedViewCount += buildView(embeddedView, ast.children, this.targetDependencies);
compileElement.beforeChildren();
this._addRootNodeAndProject(compileElement, ast.ngContentIndex, parent);
compileElement.afterChildren(0);
return null;
}
visitAttr(ast: AttrAst, ctx: any): any { return null; }
visitDirective(ast: DirectiveAst, ctx: any): any { return null; }
visitEvent(ast: BoundEventAst, eventTargetAndNames: Map<string, BoundEventAst>): any {
return null;
}
visitReference(ast: ReferenceAst, ctx: any): any { return null; }
visitVariable(ast: VariableAst, ctx: any): any { return null; }
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any { return null; }
visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; }
}
function _mergeHtmlAndDirectiveAttrs(declaredHtmlAttrs: {[key: string]: string},
directives: CompileDirectiveMetadata[]): string[][] {
var result: {[key: string]: string} = {};
StringMapWrapper.forEach(declaredHtmlAttrs, (value, key) => { result[key] = value; });
directives.forEach(directiveMeta => {
StringMapWrapper.forEach(directiveMeta.hostAttributes, (value, name) => {
var prevValue = result[name];
result[name] = isPresent(prevValue) ? mergeAttributeValue(name, prevValue, value) : value;
});
});
return mapToKeyValueArray(result);
}
function _readHtmlAttrs(attrs: AttrAst[]): {[key: string]: string} {
var htmlAttrs: {[key: string]: string} = {};
attrs.forEach((ast) => { htmlAttrs[ast.name] = ast.value; });
return htmlAttrs;
}
function mergeAttributeValue(attrName: string, attrValue1: string, attrValue2: string): string {
if (attrName == CLASS_ATTR || attrName == STYLE_ATTR) {
return `${attrValue1} ${attrValue2}`;
} else {
return attrValue2;
}
}
function mapToKeyValueArray(data: {[key: string]: string}): string[][] {
var entryArray = [];
StringMapWrapper.forEach(data, (value, name) => { entryArray.push([name, value]); });
// We need to sort to get a defined output order
// for tests and for caching generated artifacts...
ListWrapper.sort(entryArray, (entry1, entry2) => StringWrapper.compare(entry1[0], entry2[0]));
var keyValueArray = [];
entryArray.forEach((entry) => { keyValueArray.push([entry[0], entry[1]]); });
return keyValueArray;
}
function createViewTopLevelStmts(view: CompileView, targetStatements: o.Statement[]) {
var nodeDebugInfosVar: o.Expression = o.NULL_EXPR;
if (view.genConfig.genDebugInfo) {
nodeDebugInfosVar = o.variable(`nodeDebugInfos_${view.component.type.name}${view.viewIndex}`);
targetStatements.push(
(<o.ReadVarExpr>nodeDebugInfosVar)
.set(o.literalArr(view.nodes.map(createStaticNodeDebugInfo),
new o.ArrayType(new o.ExternalType(Identifiers.StaticNodeDebugInfo),
[o.TypeModifier.Const])))
.toDeclStmt(null, [o.StmtModifier.Final]));
}
var renderCompTypeVar: o.ReadVarExpr = o.variable(`renderType_${view.component.type.name}`);
if (view.viewIndex === 0) {
targetStatements.push(renderCompTypeVar.set(o.NULL_EXPR)
.toDeclStmt(o.importType(Identifiers.RenderComponentType)));
}
var viewClass = createViewClass(view, renderCompTypeVar, nodeDebugInfosVar);
targetStatements.push(viewClass);
targetStatements.push(createViewFactory(view, viewClass, renderCompTypeVar));
}
function createStaticNodeDebugInfo(node: CompileNode): o.Expression {
var compileElement = node instanceof CompileElement ? node : null;
var providerTokens: o.Expression[] = [];
var componentToken: o.Expression = o.NULL_EXPR;
var varTokenEntries = [];
if (isPresent(compileElement)) {
providerTokens = compileElement.getProviderTokens();
if (isPresent(compileElement.component)) {
componentToken = createDiTokenExpression(identifierToken(compileElement.component.type));
}
StringMapWrapper.forEach(compileElement.referenceTokens, (token, varName) => {
varTokenEntries.push(
[varName, isPresent(token) ? createDiTokenExpression(token) : o.NULL_EXPR]);
});
}
return o.importExpr(Identifiers.StaticNodeDebugInfo)
.instantiate(
[
o.literalArr(providerTokens, new o.ArrayType(o.DYNAMIC_TYPE, [o.TypeModifier.Const])),
componentToken,
o.literalMap(varTokenEntries, new o.MapType(o.DYNAMIC_TYPE, [o.TypeModifier.Const]))
],
o.importType(Identifiers.StaticNodeDebugInfo, null, [o.TypeModifier.Const]));
}
function createViewClass(view: CompileView, renderCompTypeVar: o.ReadVarExpr,
nodeDebugInfosVar: o.Expression): o.ClassStmt {
var viewConstructorArgs = [
new o.FnParam(ViewConstructorVars.viewUtils.name, o.importType(Identifiers.ViewUtils)),
new o.FnParam(ViewConstructorVars.parentInjector.name, o.importType(Identifiers.Injector)),
new o.FnParam(ViewConstructorVars.declarationEl.name, o.importType(Identifiers.AppElement))
];
var superConstructorArgs = [
o.variable(view.className),
renderCompTypeVar,
ViewTypeEnum.fromValue(view.viewType),
ViewConstructorVars.viewUtils,
ViewConstructorVars.parentInjector,
ViewConstructorVars.declarationEl,
ChangeDetectionStrategyEnum.fromValue(getChangeDetectionMode(view))
];
if (view.genConfig.genDebugInfo) {
superConstructorArgs.push(nodeDebugInfosVar);
}
var viewConstructor = new o.ClassMethod(null, viewConstructorArgs,
[o.SUPER_EXPR.callFn(superConstructorArgs).toStmt()]);
var viewMethods = [
new o.ClassMethod('createInternal', [new o.FnParam(rootSelectorVar.name, o.STRING_TYPE)],
generateCreateMethod(view), o.importType(Identifiers.AppElement)),
new o.ClassMethod(
'injectorGetInternal',
[
new o.FnParam(InjectMethodVars.token.name, o.DYNAMIC_TYPE),
// Note: Can't use o.INT_TYPE here as the method in AppView uses number
new o.FnParam(InjectMethodVars.requestNodeIndex.name, o.NUMBER_TYPE),
new o.FnParam(InjectMethodVars.notFoundResult.name, o.DYNAMIC_TYPE)
],
addReturnValuefNotEmpty(view.injectorGetMethod.finish(), InjectMethodVars.notFoundResult),
o.DYNAMIC_TYPE),
new o.ClassMethod('detectChangesInternal',
[new o.FnParam(DetectChangesVars.throwOnChange.name, o.BOOL_TYPE)],
generateDetectChangesMethod(view)),
new o.ClassMethod('dirtyParentQueriesInternal', [], view.dirtyParentQueriesMethod.finish()),
new o.ClassMethod('destroyInternal', [], view.destroyMethod.finish())
].concat(view.eventHandlerMethods);
var superClass = view.genConfig.genDebugInfo ? Identifiers.DebugAppView : Identifiers.AppView;
var viewClass = new o.ClassStmt(view.className, o.importExpr(superClass, [getContextType(view)]),
view.fields, view.getters, viewConstructor,
viewMethods.filter((method) => method.body.length > 0));
return viewClass;
}
function createViewFactory(view: CompileView, viewClass: o.ClassStmt,
renderCompTypeVar: o.ReadVarExpr): o.Statement {
var viewFactoryArgs = [
new o.FnParam(ViewConstructorVars.viewUtils.name, o.importType(Identifiers.ViewUtils)),
new o.FnParam(ViewConstructorVars.parentInjector.name, o.importType(Identifiers.Injector)),
new o.FnParam(ViewConstructorVars.declarationEl.name, o.importType(Identifiers.AppElement))
];
var initRenderCompTypeStmts = [];
var templateUrlInfo;
if (view.component.template.templateUrl == view.component.type.moduleUrl) {
templateUrlInfo =
`${view.component.type.moduleUrl} class ${view.component.type.name} - inline template`;
} else {
templateUrlInfo = view.component.template.templateUrl;
}
if (view.viewIndex === 0) {
initRenderCompTypeStmts = [
new o.IfStmt(renderCompTypeVar.identical(o.NULL_EXPR),
[
renderCompTypeVar.set(ViewConstructorVars
.viewUtils.callMethod('createRenderComponentType',
[
o.literal(templateUrlInfo),
o.literal(view.component
.template.ngContentSelectors.length),
ViewEncapsulationEnum
.fromValue(view.component.template.encapsulation),
view.styles
]))
.toStmt()
])
];
}
return o.fn(viewFactoryArgs, initRenderCompTypeStmts.concat([
new o.ReturnStatement(o.variable(viewClass.name)
.instantiate(viewClass.constructorMethod.params.map(
(param) => o.variable(param.name))))
]),
o.importType(Identifiers.AppView, [getContextType(view)]))
.toDeclStmt(view.viewFactory.name, [o.StmtModifier.Final]);
}
function generateCreateMethod(view: CompileView): o.Statement[] {
var parentRenderNodeExpr: o.Expression = o.NULL_EXPR;
var parentRenderNodeStmts = [];
if (view.viewType === ViewType.COMPONENT) {
parentRenderNodeExpr = ViewProperties.renderer.callMethod(
'createViewRoot', [o.THIS_EXPR.prop('declarationAppElement').prop('nativeElement')]);
parentRenderNodeStmts = [
parentRenderNodeVar.set(parentRenderNodeExpr)
.toDeclStmt(o.importType(view.genConfig.renderTypes.renderNode), [o.StmtModifier.Final])
];
}
var resultExpr: o.Expression;
if (view.viewType === ViewType.HOST) {
resultExpr = (<CompileElement>view.nodes[0]).appElement;
} else {
resultExpr = o.NULL_EXPR;
}
return parentRenderNodeStmts.concat(view.createMethod.finish())
.concat([
o.THIS_EXPR.callMethod('init',
[
createFlatArray(view.rootNodesOrAppElements),
o.literalArr(view.nodes.map(node => node.renderNode)),
o.literalArr(view.disposables),
o.literalArr(view.subscriptions)
])
.toStmt(),
new o.ReturnStatement(resultExpr)
]);
}
function generateDetectChangesMethod(view: CompileView): o.Statement[] {
var stmts = [];
if (view.detectChangesInInputsMethod.isEmpty() && view.updateContentQueriesMethod.isEmpty() &&
view.afterContentLifecycleCallbacksMethod.isEmpty() &&
view.detectChangesRenderPropertiesMethod.isEmpty() &&
view.updateViewQueriesMethod.isEmpty() && view.afterViewLifecycleCallbacksMethod.isEmpty()) {
return stmts;
}
ListWrapper.addAll(stmts, view.detectChangesInInputsMethod.finish());
stmts.push(
o.THIS_EXPR.callMethod('detectContentChildrenChanges', [DetectChangesVars.throwOnChange])
.toStmt());
var afterContentStmts = view.updateContentQueriesMethod.finish().concat(
view.afterContentLifecycleCallbacksMethod.finish());
if (afterContentStmts.length > 0) {
stmts.push(new o.IfStmt(o.not(DetectChangesVars.throwOnChange), afterContentStmts));
}
ListWrapper.addAll(stmts, view.detectChangesRenderPropertiesMethod.finish());
stmts.push(o.THIS_EXPR.callMethod('detectViewChildrenChanges', [DetectChangesVars.throwOnChange])
.toStmt());
var afterViewStmts =
view.updateViewQueriesMethod.finish().concat(view.afterViewLifecycleCallbacksMethod.finish());
if (afterViewStmts.length > 0) {
stmts.push(new o.IfStmt(o.not(DetectChangesVars.throwOnChange), afterViewStmts));
}
var varStmts = [];
var readVars = o.findReadVarNames(stmts);
if (SetWrapper.has(readVars, DetectChangesVars.changed.name)) {
varStmts.push(DetectChangesVars.changed.set(o.literal(true)).toDeclStmt(o.BOOL_TYPE));
}
if (SetWrapper.has(readVars, DetectChangesVars.changes.name)) {
varStmts.push(DetectChangesVars.changes.set(o.NULL_EXPR)
.toDeclStmt(new o.MapType(o.importType(Identifiers.SimpleChange))));
}
if (SetWrapper.has(readVars, DetectChangesVars.valUnwrapper.name)) {
varStmts.push(
DetectChangesVars.valUnwrapper.set(o.importExpr(Identifiers.ValueUnwrapper).instantiate([]))
.toDeclStmt(null, [o.StmtModifier.Final]));
}
return varStmts.concat(stmts);
}
function addReturnValuefNotEmpty(statements: o.Statement[], value: o.Expression): o.Statement[] {
if (statements.length > 0) {
return statements.concat([new o.ReturnStatement(value)]);
} else {
return statements;
}
}
function getContextType(view: CompileView): o.Type {
if (view.viewType === ViewType.COMPONENT) {
return o.importType(view.component.type);
}
return o.DYNAMIC_TYPE;
}
function getChangeDetectionMode(view: CompileView): ChangeDetectionStrategy {
var mode: ChangeDetectionStrategy;
if (view.viewType === ViewType.COMPONENT) {
mode = isDefaultChangeDetectionStrategy(view.component.changeDetection) ?
ChangeDetectionStrategy.CheckAlways :
ChangeDetectionStrategy.CheckOnce;
} else {
mode = ChangeDetectionStrategy.CheckAlways;
}
return mode;
}

View File

@ -0,0 +1,37 @@
import {Injectable} from 'angular2/src/core/di';
import * as o from '../output/output_ast';
import {CompileElement} from './compile_element';
import {CompileView} from './compile_view';
import {buildView, finishView, ViewCompileDependency} from './view_builder';
import {bindView} from './view_binder';
import {CompileDirectiveMetadata, CompilePipeMetadata} from '../compile_metadata';
import {TemplateAst} from '../template_ast';
import {CompilerConfig} from '../config';
export class ViewCompileResult {
constructor(public statements: o.Statement[], public viewFactoryVar: string,
public dependencies: ViewCompileDependency[]) {}
}
@Injectable()
export class ViewCompiler {
constructor(private _genConfig: CompilerConfig) {}
compileComponent(component: CompileDirectiveMetadata, template: TemplateAst[],
styles: o.Expression, pipes: CompilePipeMetadata[]): ViewCompileResult {
var statements = [];
var dependencies = [];
var view = new CompileView(component, this._genConfig, pipes, styles, 0,
CompileElement.createNull(), []);
buildView(view, template, dependencies);
// Need to separate binding from creation to be able to refer to
// variables that have been declared after usage.
bindView(view, template);
finishView(view, statements);
return new ViewCompileResult(statements, view.viewFactory.name, dependencies);
}
}

View File

@ -0,0 +1,111 @@
import {Injectable} from 'angular2/src/core/di';
import {ViewMetadata} from 'angular2/src/core/metadata/view';
import {ComponentMetadata} from 'angular2/src/core/metadata/directives';
import {Type, stringify, isBlank, isPresent} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {Map} from 'angular2/src/facade/collection';
import {ReflectorReader} from 'angular2/src/core/reflection/reflector_reader';
import {reflector} from 'angular2/src/core/reflection/reflection';
/**
* Resolves types to {@link ViewMetadata}.
*/
@Injectable()
export class ViewResolver {
private _reflector: ReflectorReader;
/** @internal */
_cache = new Map<Type, ViewMetadata>();
constructor(_reflector?: ReflectorReader) {
if (isPresent(_reflector)) {
this._reflector = _reflector;
} else {
this._reflector = reflector;
}
}
resolve(component: Type): ViewMetadata {
var view = this._cache.get(component);
if (isBlank(view)) {
view = this._resolve(component);
this._cache.set(component, view);
}
return view;
}
/** @internal */
_resolve(component: Type): ViewMetadata {
var compMeta: ComponentMetadata;
var viewMeta: ViewMetadata;
this._reflector.annotations(component).forEach(m => {
if (m instanceof ViewMetadata) {
viewMeta = m;
}
if (m instanceof ComponentMetadata) {
compMeta = m;
}
});
if (isPresent(compMeta)) {
if (isBlank(compMeta.template) && isBlank(compMeta.templateUrl) && isBlank(viewMeta)) {
throw new BaseException(
`Component '${stringify(component)}' must have either 'template' or 'templateUrl' set.`);
} else if (isPresent(compMeta.template) && isPresent(viewMeta)) {
this._throwMixingViewAndComponent("template", component);
} else if (isPresent(compMeta.templateUrl) && isPresent(viewMeta)) {
this._throwMixingViewAndComponent("templateUrl", component);
} else if (isPresent(compMeta.directives) && isPresent(viewMeta)) {
this._throwMixingViewAndComponent("directives", component);
} else if (isPresent(compMeta.pipes) && isPresent(viewMeta)) {
this._throwMixingViewAndComponent("pipes", component);
} else if (isPresent(compMeta.encapsulation) && isPresent(viewMeta)) {
this._throwMixingViewAndComponent("encapsulation", component);
} else if (isPresent(compMeta.styles) && isPresent(viewMeta)) {
this._throwMixingViewAndComponent("styles", component);
} else if (isPresent(compMeta.styleUrls) && isPresent(viewMeta)) {
this._throwMixingViewAndComponent("styleUrls", component);
} else if (isPresent(viewMeta)) {
return viewMeta;
} else {
return new ViewMetadata({
templateUrl: compMeta.templateUrl,
template: compMeta.template,
directives: compMeta.directives,
pipes: compMeta.pipes,
encapsulation: compMeta.encapsulation,
styles: compMeta.styles,
styleUrls: compMeta.styleUrls
});
}
} else {
if (isBlank(viewMeta)) {
throw new BaseException(
`Could not compile '${stringify(component)}' because it is not a component.`);
} else {
return viewMeta;
}
}
return null;
}
/** @internal */
_throwMixingViewAndComponent(propertyName: string, component: Type): void {
throw new BaseException(
`Component '${stringify(component)}' cannot have both '${propertyName}' and '@View' set at the same time"`);
}
}

View File

@ -0,0 +1,8 @@
// TODO: vsavkin rename it into TemplateLoader
/**
* An interface for retrieving documents by URL that the compiler uses
* to load templates.
*/
export class XHR {
get(url: string): Promise<string> { return null; }
}

View File

@ -0,0 +1,177 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
el,
expect,
iit,
inject,
it,
xit,
TestComponentBuilder
} from 'angular2/testing_internal';
import {
CompileDirectiveMetadata,
CompileTypeMetadata,
CompileTemplateMetadata,
CompileProviderMetadata,
CompileDiDependencyMetadata,
CompileQueryMetadata,
CompileIdentifierMetadata,
CompileFactoryMetadata,
CompileTokenMetadata
} from 'angular2/src/compiler/compile_metadata';
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';
import {ChangeDetectionStrategy} from 'angular2/src/core/change_detection';
import {LifecycleHooks} from 'angular2/src/core/metadata/lifecycle_hooks';
export function main() {
describe('CompileMetadata', () => {
var fullTypeMeta: CompileTypeMetadata;
var fullTemplateMeta: CompileTemplateMetadata;
var fullDirectiveMeta: CompileDirectiveMetadata;
beforeEach(() => {
var diDep = new CompileDiDependencyMetadata({
isAttribute: true,
isSelf: true,
isHost: true,
isSkipSelf: true,
isOptional: true,
token: new CompileTokenMetadata({value: 'someToken'}),
query: new CompileQueryMetadata({
selectors: [new CompileTokenMetadata({value: 'one'})],
descendants: true,
first: true,
propertyName: 'one'
}),
viewQuery: new CompileQueryMetadata({
selectors: [new CompileTokenMetadata({value: 'one'})],
descendants: true,
first: true,
propertyName: 'one'
})
});
fullTypeMeta = new CompileTypeMetadata(
{name: 'SomeType', moduleUrl: 'someUrl', isHost: true, diDeps: [diDep]});
fullTemplateMeta = new CompileTemplateMetadata({
encapsulation: ViewEncapsulation.Emulated,
template: '<a></a>',
templateUrl: 'someTemplateUrl',
styles: ['someStyle'],
styleUrls: ['someStyleUrl'],
ngContentSelectors: ['*'],
baseUrl: 'someBaseUrl'
});
fullDirectiveMeta = CompileDirectiveMetadata.create({
selector: 'someSelector',
isComponent: true,
type: fullTypeMeta,
template: fullTemplateMeta,
changeDetection: ChangeDetectionStrategy.Default,
inputs: ['someProp'],
outputs: ['someEvent'],
host: {'(event1)': 'handler1', '[prop1]': 'expr1', 'attr1': 'attrValue2'},
lifecycleHooks: [LifecycleHooks.OnChanges],
providers: [
new CompileProviderMetadata({
token: new CompileTokenMetadata({value: 'token'}),
multi: true,
useClass: fullTypeMeta,
useExisting: new CompileTokenMetadata({
identifier: new CompileIdentifierMetadata({name: 'someName'}),
identifierIsInstance: true
}),
useFactory: new CompileFactoryMetadata({name: 'someName', diDeps: [diDep]}),
useValue: 'someValue',
})
],
viewProviders: [
new CompileProviderMetadata({
token: new CompileTokenMetadata({value: 'token'}),
useClass: fullTypeMeta,
useExisting: new CompileTokenMetadata(
{identifier: new CompileIdentifierMetadata({name: 'someName'})}),
useFactory: new CompileFactoryMetadata({name: 'someName', diDeps: [diDep]}),
useValue: 'someValue'
})
],
queries: [
new CompileQueryMetadata({
selectors: [new CompileTokenMetadata({value: 'selector'})],
descendants: true,
first: false,
propertyName: 'prop',
read: new CompileTokenMetadata({value: 'readToken'})
})
],
viewQueries: [
new CompileQueryMetadata({
selectors: [new CompileTokenMetadata({value: 'selector'})],
descendants: true,
first: false,
propertyName: 'prop',
read: new CompileTokenMetadata({value: 'readToken'})
})
]
});
});
describe('CompileIdentifierMetadata', () => {
it('should serialize with full data', () => {
let full = new CompileIdentifierMetadata(
{name: 'name', moduleUrl: 'module', value: ['one', ['two']]});
expect(CompileIdentifierMetadata.fromJson(full.toJson())).toEqual(full);
});
it('should serialize with no data', () => {
let empty = new CompileIdentifierMetadata();
expect(CompileIdentifierMetadata.fromJson(empty.toJson())).toEqual(empty);
});
});
describe('DirectiveMetadata', () => {
it('should serialize with full data', () => {
expect(CompileDirectiveMetadata.fromJson(fullDirectiveMeta.toJson()))
.toEqual(fullDirectiveMeta);
});
it('should serialize with no data', () => {
var empty = CompileDirectiveMetadata.create();
expect(CompileDirectiveMetadata.fromJson(empty.toJson())).toEqual(empty);
});
});
describe('TypeMetadata', () => {
it('should serialize with full data', () => {
expect(CompileTypeMetadata.fromJson(fullTypeMeta.toJson())).toEqual(fullTypeMeta);
});
it('should serialize with no data', () => {
var empty = new CompileTypeMetadata();
expect(CompileTypeMetadata.fromJson(empty.toJson())).toEqual(empty);
});
});
describe('TemplateMetadata', () => {
it('should use ViewEncapsulation.Emulated by default', () => {
expect(new CompileTemplateMetadata({encapsulation: null}).encapsulation)
.toBe(ViewEncapsulation.Emulated);
});
it('should serialize with full data', () => {
expect(CompileTemplateMetadata.fromJson(fullTemplateMeta.toJson()))
.toEqual(fullTemplateMeta);
});
it('should serialize with no data', () => {
var empty = new CompileTemplateMetadata();
expect(CompileTemplateMetadata.fromJson(empty.toJson())).toEqual(empty);
});
});
});
}

View File

@ -0,0 +1,393 @@
import {
ddescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach
} from 'angular2/testing_internal';
import {isPresent} from "angular2/src/facade/lang";
import {
CssToken,
CssScannerError,
CssLexer,
CssLexerMode,
CssTokenType
} from 'angular2/src/compiler/css/lexer';
export function main() {
function tokenize(code, trackComments: boolean = false,
mode: CssLexerMode = CssLexerMode.ALL): CssToken[] {
var scanner = new CssLexer().scan(code, trackComments);
scanner.setMode(mode);
var tokens = [];
var output = scanner.scan();
while (output != null) {
var error = output.error;
if (isPresent(error)) {
throw new CssScannerError(error.token, error.rawMessage);
}
tokens.push(output.token);
output = scanner.scan();
}
return tokens;
}
describe('CssLexer', () => {
it('should lex newline characters as whitespace when whitespace mode is on', () => {
var newlines = ["\n", "\r\n", "\r", "\f"];
newlines.forEach((line) => {
var token = tokenize(line, false, CssLexerMode.ALL_TRACK_WS)[0];
expect(token.type).toEqual(CssTokenType.Whitespace);
});
});
it('should combined newline characters as one newline token when whitespace mode is on', () => {
var newlines = ["\n", "\r\n", "\r", "\f"].join("");
var tokens = tokenize(newlines, false, CssLexerMode.ALL_TRACK_WS);
expect(tokens.length).toEqual(1);
expect(tokens[0].type).toEqual(CssTokenType.Whitespace);
});
it('should not consider whitespace or newline values at all when whitespace mode is off',
() => {
var newlines = ["\n", "\r\n", "\r", "\f"].join("");
var tokens = tokenize(newlines);
expect(tokens.length).toEqual(0);
});
it('should lex simple selectors and their inner properties', () => {
var cssCode = "\n" + " .selector { my-prop: my-value; }\n";
var tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Character);
expect(tokens[0].strValue).toEqual('.');
expect(tokens[1].type).toEqual(CssTokenType.Identifier);
expect(tokens[1].strValue).toEqual('selector');
expect(tokens[2].type).toEqual(CssTokenType.Character);
expect(tokens[2].strValue).toEqual('{');
expect(tokens[3].type).toEqual(CssTokenType.Identifier);
expect(tokens[3].strValue).toEqual('my-prop');
expect(tokens[4].type).toEqual(CssTokenType.Character);
expect(tokens[4].strValue).toEqual(':');
expect(tokens[5].type).toEqual(CssTokenType.Identifier);
expect(tokens[5].strValue).toEqual('my-value');
expect(tokens[6].type).toEqual(CssTokenType.Character);
expect(tokens[6].strValue).toEqual(';');
expect(tokens[7].type).toEqual(CssTokenType.Character);
expect(tokens[7].strValue).toEqual('}');
});
it('should capture the column and line values for each token', () => {
var cssCode = "#id {\n" + " prop:value;\n" + "}";
var tokens = tokenize(cssCode);
// #
expect(tokens[0].type).toEqual(CssTokenType.Character);
expect(tokens[0].column).toEqual(0);
expect(tokens[0].line).toEqual(0);
// id
expect(tokens[1].type).toEqual(CssTokenType.Identifier);
expect(tokens[1].column).toEqual(1);
expect(tokens[1].line).toEqual(0);
// {
expect(tokens[2].type).toEqual(CssTokenType.Character);
expect(tokens[2].column).toEqual(4);
expect(tokens[2].line).toEqual(0);
// prop
expect(tokens[3].type).toEqual(CssTokenType.Identifier);
expect(tokens[3].column).toEqual(2);
expect(tokens[3].line).toEqual(1);
// :
expect(tokens[4].type).toEqual(CssTokenType.Character);
expect(tokens[4].column).toEqual(6);
expect(tokens[4].line).toEqual(1);
// value
expect(tokens[5].type).toEqual(CssTokenType.Identifier);
expect(tokens[5].column).toEqual(7);
expect(tokens[5].line).toEqual(1);
// ;
expect(tokens[6].type).toEqual(CssTokenType.Character);
expect(tokens[6].column).toEqual(12);
expect(tokens[6].line).toEqual(1);
// }
expect(tokens[7].type).toEqual(CssTokenType.Character);
expect(tokens[7].column).toEqual(0);
expect(tokens[7].line).toEqual(2);
});
it('should lex quoted strings and escape accordingly', () => {
var cssCode = "prop: 'some { value } \\' that is quoted'";
var tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Identifier);
expect(tokens[1].type).toEqual(CssTokenType.Character);
expect(tokens[2].type).toEqual(CssTokenType.String);
expect(tokens[2].strValue).toEqual("'some { value } \\' that is quoted'");
});
it('should treat attribute operators as regular characters', () => {
tokenize('^|~+*').forEach((token) => { expect(token.type).toEqual(CssTokenType.Character); });
});
it('should lex numbers properly and set them as numbers', () => {
var cssCode = "0 1 -2 3.0 -4.001";
var tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Number);
expect(tokens[0].strValue).toEqual("0");
expect(tokens[1].type).toEqual(CssTokenType.Number);
expect(tokens[1].strValue).toEqual("1");
expect(tokens[2].type).toEqual(CssTokenType.Number);
expect(tokens[2].strValue).toEqual("-2");
expect(tokens[3].type).toEqual(CssTokenType.Number);
expect(tokens[3].strValue).toEqual("3.0");
expect(tokens[4].type).toEqual(CssTokenType.Number);
expect(tokens[4].strValue).toEqual("-4.001");
});
it('should lex @keywords', () => {
var cssCode = "@import()@something";
var tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.AtKeyword);
expect(tokens[0].strValue).toEqual('@import');
expect(tokens[1].type).toEqual(CssTokenType.Character);
expect(tokens[1].strValue).toEqual('(');
expect(tokens[2].type).toEqual(CssTokenType.Character);
expect(tokens[2].strValue).toEqual(')');
expect(tokens[3].type).toEqual(CssTokenType.AtKeyword);
expect(tokens[3].strValue).toEqual('@something');
});
it('should still lex a number even if it has a dimension suffix', () => {
var cssCode = "40% is 40 percent";
var tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Number);
expect(tokens[0].strValue).toEqual('40');
expect(tokens[1].type).toEqual(CssTokenType.Character);
expect(tokens[1].strValue).toEqual('%');
expect(tokens[2].type).toEqual(CssTokenType.Identifier);
expect(tokens[2].strValue).toEqual('is');
expect(tokens[3].type).toEqual(CssTokenType.Number);
expect(tokens[3].strValue).toEqual('40');
});
it('should allow escaped character and unicode character-strings in CSS selectors', () => {
var cssCode = "\\123456 .some\\thing \{\}";
var tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Identifier);
expect(tokens[0].strValue).toEqual('\\123456');
expect(tokens[1].type).toEqual(CssTokenType.Character);
expect(tokens[2].type).toEqual(CssTokenType.Identifier);
expect(tokens[2].strValue).toEqual('some\\thing');
});
it('should distinguish identifiers and numbers from special characters', () => {
var cssCode = "one*two=-4+three-4-equals_value$";
var tokens = tokenize(cssCode);
expect(tokens[0].type).toEqual(CssTokenType.Identifier);
expect(tokens[0].strValue).toEqual("one");
expect(tokens[1].type).toEqual(CssTokenType.Character);
expect(tokens[1].strValue).toEqual("*");
expect(tokens[2].type).toEqual(CssTokenType.Identifier);
expect(tokens[2].strValue).toEqual("two");
expect(tokens[3].type).toEqual(CssTokenType.Character);
expect(tokens[3].strValue).toEqual("=");
expect(tokens[4].type).toEqual(CssTokenType.Number);
expect(tokens[4].strValue).toEqual("-4");
expect(tokens[5].type).toEqual(CssTokenType.Character);
expect(tokens[5].strValue).toEqual("+");
expect(tokens[6].type).toEqual(CssTokenType.Identifier);
expect(tokens[6].strValue).toEqual("three-4-equals_value");
expect(tokens[7].type).toEqual(CssTokenType.Character);
expect(tokens[7].strValue).toEqual("$");
});
it('should filter out comments and whitespace by default', () => {
var cssCode = ".selector /* comment */ { /* value */ }";
var tokens = tokenize(cssCode);
expect(tokens[0].strValue).toEqual(".");
expect(tokens[1].strValue).toEqual("selector");
expect(tokens[2].strValue).toEqual("{");
expect(tokens[3].strValue).toEqual("}");
});
it('should track comments when the flag is set to true', () => {
var cssCode = ".selector /* comment */ { /* value */ }";
var trackComments = true;
var tokens = tokenize(cssCode, trackComments, CssLexerMode.ALL_TRACK_WS);
expect(tokens[0].strValue).toEqual(".");
expect(tokens[1].strValue).toEqual("selector");
expect(tokens[2].strValue).toEqual(" ");
expect(tokens[3].type).toEqual(CssTokenType.Comment);
expect(tokens[3].strValue).toEqual("/* comment */");
expect(tokens[4].strValue).toEqual(" ");
expect(tokens[5].strValue).toEqual("{");
expect(tokens[6].strValue).toEqual(" ");
expect(tokens[7].type).toEqual(CssTokenType.Comment);
expect(tokens[7].strValue).toEqual("/* value */");
});
describe('Selector Mode', () => {
it('should throw an error if a selector is being parsed while in the wrong mode', () => {
var cssCode = ".class > tag";
var capturedMessage;
try {
tokenize(cssCode, false, CssLexerMode.STYLE_BLOCK);
} catch (e) {
capturedMessage = e.rawMessage;
}
expect(capturedMessage)
.toMatchPattern(/Unexpected character \[\>\] at column 0:7 in expression/g);
capturedMessage = null;
try {
tokenize(cssCode, false, CssLexerMode.SELECTOR);
} catch (e) {
capturedMessage = e.rawMessage;
}
expect(capturedMessage).toEqual(null);
});
});
describe('Attribute Mode', () => {
it('should consider attribute selectors as valid input and throw when an invalid modifier is used',
() => {
function tokenizeAttr(modifier) {
var cssCode = "value" + modifier + "='something'";
return tokenize(cssCode, false, CssLexerMode.ATTRIBUTE_SELECTOR);
}
expect(tokenizeAttr("*").length).toEqual(4);
expect(tokenizeAttr("|").length).toEqual(4);
expect(tokenizeAttr("^").length).toEqual(4);
expect(tokenizeAttr("$").length).toEqual(4);
expect(tokenizeAttr("~").length).toEqual(4);
expect(tokenizeAttr("").length).toEqual(3);
expect(() => { tokenizeAttr("+"); }).toThrow();
});
});
describe('Media Query Mode', () => {
it('should validate media queries with a reduced subset of valid characters', () => {
function tokenizeQuery(code) { return tokenize(code, false, CssLexerMode.MEDIA_QUERY); }
// the reason why the numbers are so high is because MediaQueries keep
// track of the whitespace values
expect(tokenizeQuery("(prop: value)").length).toEqual(5);
expect(tokenizeQuery("(prop: value) and (prop2: value2)").length).toEqual(11);
expect(tokenizeQuery("tv and (prop: value)").length).toEqual(7);
expect(tokenizeQuery("print and ((prop: value) or (prop2: value2))").length).toEqual(15);
expect(tokenizeQuery("(content: 'something $ crazy inside &')").length).toEqual(5);
expect(() => { tokenizeQuery("(max-height: 10 + 20)"); }).toThrow();
expect(() => { tokenizeQuery("(max-height: fifty < 100)"); }).toThrow();
});
});
describe('Pseudo Selector Mode', () => {
it('should validate pseudo selector identifiers with a reduced subset of valid characters',
() => {
function tokenizePseudo(code) {
return tokenize(code, false, CssLexerMode.PSEUDO_SELECTOR);
}
expect(tokenizePseudo("lang(en-us)").length).toEqual(4);
expect(tokenizePseudo("hover").length).toEqual(1);
expect(tokenizePseudo("focus").length).toEqual(1);
expect(() => { tokenizePseudo("lang(something:broken)"); }).toThrow();
expect(() => { tokenizePseudo("not(.selector)"); }).toThrow();
});
});
describe('Pseudo Selector Mode', () => {
it('should validate pseudo selector identifiers with a reduced subset of valid characters',
() => {
function tokenizePseudo(code) {
return tokenize(code, false, CssLexerMode.PSEUDO_SELECTOR);
}
expect(tokenizePseudo("lang(en-us)").length).toEqual(4);
expect(tokenizePseudo("hover").length).toEqual(1);
expect(tokenizePseudo("focus").length).toEqual(1);
expect(() => { tokenizePseudo("lang(something:broken)"); }).toThrow();
expect(() => { tokenizePseudo("not(.selector)"); }).toThrow();
});
});
describe('Style Block Mode', () => {
it('should style blocks with a reduced subset of valid characters', () => {
function tokenizeStyles(code) { return tokenize(code, false, CssLexerMode.STYLE_BLOCK); }
expect(tokenizeStyles(`
key: value;
prop: 100;
style: value3!important;
`).length)
.toEqual(14);
expect(() => tokenizeStyles(` key$: value; `)).toThrow();
expect(() => tokenizeStyles(` key: value$; `)).toThrow();
expect(() => tokenizeStyles(` key: value + 10; `)).toThrow();
expect(() => tokenizeStyles(` key: &value; `)).toThrow();
});
});
});
}

View File

@ -0,0 +1,640 @@
import {
ddescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach
} from 'angular2/testing_internal';
import {BaseException} from 'angular2/src/facade/exceptions';
import {
ParsedCssResult,
CssParser,
BlockType,
CssSelectorRuleAST,
CssKeyframeRuleAST,
CssKeyframeDefinitionAST,
CssBlockDefinitionRuleAST,
CssMediaQueryRuleAST,
CssBlockRuleAST,
CssInlineRuleAST,
CssStyleValueAST,
CssSelectorAST,
CssDefinitionAST,
CssStyleSheetAST,
CssRuleAST,
CssBlockAST,
CssParseError
} from 'angular2/src/compiler/css/parser';
import {CssLexer} from 'angular2/src/compiler/css/lexer';
export function assertTokens(tokens, valuesArr) {
for (var i = 0; i < tokens.length; i++) {
expect(tokens[i].strValue == valuesArr[i]);
}
}
export function main() {
describe('CssParser', () => {
function parse(css): ParsedCssResult {
var lexer = new CssLexer();
var scanner = lexer.scan(css);
var parser = new CssParser(scanner, 'some-fake-file-name.css');
return parser.parse();
}
function makeAST(css): CssStyleSheetAST {
var output = parse(css);
var errors = output.errors;
if (errors.length > 0) {
throw new BaseException(errors.map((error: CssParseError) => error.msg).join(', '));
}
return output.ast;
}
it('should parse CSS into a stylesheet AST', () => {
var styles = `
.selector {
prop: value123;
}
`;
var ast = makeAST(styles);
expect(ast.rules.length).toEqual(1);
var rule = <CssSelectorRuleAST>ast.rules[0];
var selector = rule.selectors[0];
expect(selector.strValue).toEqual('.selector');
var block: CssBlockAST = rule.block;
expect(block.entries.length).toEqual(1);
var definition = <CssDefinitionAST>block.entries[0];
expect(definition.property.strValue).toEqual('prop');
var value = <CssStyleValueAST>definition.value;
expect(value.tokens[0].strValue).toEqual('value123');
});
it('should parse mutliple CSS selectors sharing the same set of styles', () => {
var styles = `
.class, #id, tag, [attr], key + value, * value, :-moz-any-link {
prop: value123;
}
`;
var ast = makeAST(styles);
expect(ast.rules.length).toEqual(1);
var rule = <CssSelectorRuleAST>ast.rules[0];
expect(rule.selectors.length).toBe(7);
assertTokens(rule.selectors[0].tokens, [".", "class"]);
assertTokens(rule.selectors[1].tokens, ["#", "id"]);
assertTokens(rule.selectors[2].tokens, ["tag"]);
assertTokens(rule.selectors[3].tokens, ["[", "attr", "]"]);
assertTokens(rule.selectors[4].tokens, ["key", " ", "+", " ", "value"]);
assertTokens(rule.selectors[5].tokens, ["*", " ", "value"]);
assertTokens(rule.selectors[6].tokens, [":", "-moz-any-link"]);
var style1 = <CssDefinitionAST>rule.block.entries[0];
expect(style1.property.strValue).toEqual("prop");
assertTokens(style1.value.tokens, ["value123"]);
});
it('should parse keyframe rules', () => {
var styles = `
@keyframes rotateMe {
from {
transform: rotate(-360deg);
}
50% {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
`;
var ast = makeAST(styles);
expect(ast.rules.length).toEqual(1);
var rule = <CssKeyframeRuleAST>ast.rules[0];
expect(rule.name.strValue).toEqual('rotateMe');
var block = <CssBlockAST>rule.block;
var fromRule = <CssKeyframeDefinitionAST>block.entries[0];
expect(fromRule.name.strValue).toEqual('from');
var fromStyle = <CssDefinitionAST>(<CssBlockAST>fromRule.block).entries[0];
expect(fromStyle.property.strValue).toEqual('transform');
assertTokens(fromStyle.value.tokens, ['rotate', '(', '-360', 'deg', ')']);
var midRule = <CssKeyframeDefinitionAST>block.entries[1];
expect(midRule.name.strValue).toEqual('50%');
var midStyle = <CssDefinitionAST>(<CssBlockAST>midRule.block).entries[0];
expect(midStyle.property.strValue).toEqual('transform');
assertTokens(midStyle.value.tokens, ['rotate', '(', '0', 'deg', ')']);
var toRule = <CssKeyframeDefinitionAST>block.entries[2];
expect(toRule.name.strValue).toEqual('to');
var toStyle = <CssDefinitionAST>(<CssBlockAST>toRule.block).entries[0];
expect(toStyle.property.strValue).toEqual('transform');
assertTokens(toStyle.value.tokens, ['rotate', '(', '360', 'deg', ')']);
});
it('should parse media queries into a stylesheet AST', () => {
var styles = `
@media all and (max-width:100px) {
.selector {
prop: value123;
}
}
`;
var ast = makeAST(styles);
expect(ast.rules.length).toEqual(1);
var rule = <CssMediaQueryRuleAST>ast.rules[0];
assertTokens(rule.query, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']);
var block = <CssBlockAST>rule.block;
expect(block.entries.length).toEqual(1);
var rule2 = <CssSelectorRuleAST>block.entries[0];
expect(rule2.selectors[0].strValue).toEqual('.selector');
var block2 = <CssBlockAST>rule2.block;
expect(block2.entries.length).toEqual(1);
});
it('should parse inline CSS values', () => {
var styles = `
@import url('remote.css');
@charset "UTF-8";
@namespace ng url(http://angular.io/namespace/ng);
`;
var ast = makeAST(styles);
var importRule = <CssInlineRuleAST>ast.rules[0];
expect(importRule.type).toEqual(BlockType.Import);
assertTokens(importRule.value.tokens, ["url", "(", "remote", ".", "css", ")"]);
var charsetRule = <CssInlineRuleAST>ast.rules[1];
expect(charsetRule.type).toEqual(BlockType.Charset);
assertTokens(charsetRule.value.tokens, ["UTF-8"]);
var namespaceRule = <CssInlineRuleAST>ast.rules[2];
expect(namespaceRule.type).toEqual(BlockType.Namespace);
assertTokens(namespaceRule.value.tokens,
["ng", "url", "(", "http://angular.io/namespace/ng", ")"]);
});
it('should parse CSS values that contain functions and leave the inner function data untokenized',
() => {
var styles = `
.class {
background: url(matias.css);
animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
height: calc(100% - 50px);
}
`;
var ast = makeAST(styles);
expect(ast.rules.length).toEqual(1);
var defs = (<CssSelectorRuleAST>ast.rules[0]).block.entries;
expect(defs.length).toEqual(3);
assertTokens((<CssDefinitionAST>defs[0]).value.tokens, ['url', '(', 'matias.css', ')']);
assertTokens((<CssDefinitionAST>defs[1]).value.tokens,
['cubic-bezier', '(', '0.755, 0.050, 0.855, 0.060', ')']);
assertTokens((<CssDefinitionAST>defs[2]).value.tokens, ['calc', '(', '100% - 50px', ')']);
});
it('should parse un-named block-level CSS values', () => {
var styles = `
@font-face {
font-family: "Matias";
font-weight: bold;
src: url(font-face.ttf);
}
@viewport {
max-width: 100px;
min-height: 1000px;
}
`;
var ast = makeAST(styles);
var fontFaceRule = <CssBlockRuleAST>ast.rules[0];
expect(fontFaceRule.type).toEqual(BlockType.FontFace);
expect(fontFaceRule.block.entries.length).toEqual(3);
var viewportRule = <CssBlockRuleAST>ast.rules[1];
expect(viewportRule.type).toEqual(BlockType.Viewport);
expect(viewportRule.block.entries.length).toEqual(2);
});
it('should parse multiple levels of semicolons', () => {
var styles = `
;;;
@import url('something something')
;;;;;;;;
;;;;;;;;
;@font-face {
;src : url(font-face.ttf);;;;;;;;
;;;-webkit-animation:my-animation
};;;
@media all and (max-width:100px)
{;
.selector {prop: value123;};
;.selector2{prop:1}}
`;
var ast = makeAST(styles);
var importRule = <CssInlineRuleAST>ast.rules[0];
expect(importRule.type).toEqual(BlockType.Import);
assertTokens(importRule.value.tokens, ["url", "(", "something something", ")"]);
var fontFaceRule = <CssBlockRuleAST>ast.rules[1];
expect(fontFaceRule.type).toEqual(BlockType.FontFace);
expect(fontFaceRule.block.entries.length).toEqual(2);
var mediaQueryRule = <CssMediaQueryRuleAST>ast.rules[2];
assertTokens(mediaQueryRule.query, ['all', 'and', '(', 'max-width', ':', '100', 'px', ')']);
expect(mediaQueryRule.block.entries.length).toEqual(2);
});
it('should throw an error if an unknown @value block rule is parsed', () => {
var styles = `
@matias { hello: there; }
`;
expect(() => {
makeAST(styles);
}).toThrowError(/^CSS Parse Error: The CSS "at" rule "@matias" is not allowed to used here/g);
});
it('should parse empty rules', () => {
var styles = `
.empty-rule { }
.somewhat-empty-rule { /* property: value; */ }
.non-empty-rule { property: value; }
`;
var ast = makeAST(styles);
var rules = ast.rules;
expect((<CssSelectorRuleAST>rules[0]).block.entries.length).toEqual(0);
expect((<CssSelectorRuleAST>rules[1]).block.entries.length).toEqual(0);
expect((<CssSelectorRuleAST>rules[2]).block.entries.length).toEqual(1);
});
it('should parse the @document rule', () => {
var styles = `
@document url(http://www.w3.org/),
url-prefix(http://www.w3.org/Style/),
domain(mozilla.org),
regexp("https:.*")
{
/* CSS rules here apply to:
- The page "http://www.w3.org/".
- Any page whose URL begins with "http://www.w3.org/Style/"
- Any page whose URL's host is "mozilla.org" or ends with
".mozilla.org"
- Any page whose URL starts with "https:" */
/* make the above-mentioned pages really ugly */
body {
color: purple;
background: yellow;
}
}
`;
var ast = makeAST(styles);
var rules = ast.rules;
var documentRule = <CssBlockDefinitionRuleAST>rules[0];
expect(documentRule.type).toEqual(BlockType.Document);
var rule = <CssSelectorRuleAST>documentRule.block.entries[0];
expect(rule.strValue).toEqual("body");
});
it('should parse the @page rule', () => {
var styles = `
@page one {
.selector { prop: value; }
}
@page two {
.selector2 { prop: value2; }
}
`;
var ast = makeAST(styles);
var rules = ast.rules;
var pageRule1 = <CssBlockDefinitionRuleAST>rules[0];
expect(pageRule1.strValue).toEqual("one");
expect(pageRule1.type).toEqual(BlockType.Page);
var pageRule2 = <CssBlockDefinitionRuleAST>rules[1];
expect(pageRule2.strValue).toEqual("two");
expect(pageRule2.type).toEqual(BlockType.Page);
var selectorOne = <CssSelectorRuleAST>pageRule1.block.entries[0];
expect(selectorOne.strValue).toEqual('.selector');
var selectorTwo = <CssSelectorRuleAST>pageRule2.block.entries[0];
expect(selectorTwo.strValue).toEqual('.selector2');
});
it('should parse the @supports rule', () => {
var styles = `
@supports (animation-name: "rotate") {
a:hover { animation: rotate 1s; }
}
`;
var ast = makeAST(styles);
var rules = ast.rules;
var supportsRule = <CssBlockDefinitionRuleAST>rules[0];
assertTokens(supportsRule.query, ['(', 'animation-name', ':', 'rotate', ')']);
expect(supportsRule.type).toEqual(BlockType.Supports);
var selectorOne = <CssSelectorRuleAST>supportsRule.block.entries[0];
expect(selectorOne.strValue).toEqual('a:hover');
});
it('should collect multiple errors during parsing', () => {
var styles = `
.class$value { something: something }
@custom { something: something }
#id { cool^: value }
`;
var output = parse(styles);
expect(output.errors.length).toEqual(3);
});
it('should recover from selector errors and continue parsing', () => {
var styles = `
tag& { key: value; }
.%tag { key: value; }
#tag$ { key: value; }
`;
var output = parse(styles);
var errors = output.errors;
var ast = output.ast;
expect(errors.length).toEqual(3);
expect(ast.rules.length).toEqual(3);
var rule1 = <CssSelectorRuleAST>ast.rules[0];
expect(rule1.selectors[0].strValue).toEqual("tag&");
expect(rule1.block.entries.length).toEqual(1);
var rule2 = <CssSelectorRuleAST>ast.rules[1];
expect(rule2.selectors[0].strValue).toEqual(".%tag");
expect(rule2.block.entries.length).toEqual(1);
var rule3 = <CssSelectorRuleAST>ast.rules[2];
expect(rule3.selectors[0].strValue).toEqual("#tag$");
expect(rule3.block.entries.length).toEqual(1);
});
it('should throw an error when parsing invalid CSS Selectors', () => {
var styles = '.class[[prop%=value}] { style: val; }';
var output = parse(styles);
var errors = output.errors;
expect(errors.length).toEqual(3);
expect(errors[0].msg).toMatchPattern(/Unexpected character \[\[\] at column 0:7/g);
expect(errors[1].msg).toMatchPattern(/Unexpected character \[%\] at column 0:12/g);
expect(errors[2].msg).toMatchPattern(/Unexpected character \[}\] at column 0:19/g);
});
it('should throw an error if an attribute selector is not closed properly', () => {
var styles = '.class[prop=value { style: val; }';
var output = parse(styles);
var errors = output.errors;
expect(errors[0].msg).toMatchPattern(/Unbalanced CSS attribute selector at column 0:12/g);
});
it('should throw an error if a pseudo function selector is not closed properly', () => {
var styles = 'body:lang(en { key:value; }';
var output = parse(styles);
var errors = output.errors;
expect(errors[0].msg)
.toMatchPattern(/Unbalanced pseudo selector function value at column 0:10/g);
});
it('should raise an error when a semi colon is missing from a CSS style/pair that isn\'t the last entry',
() => {
var styles = `.class {
color: red
background: blue
}`;
var output = parse(styles);
var errors = output.errors;
expect(errors.length).toEqual(1);
expect(errors[0].msg)
.toMatchPattern(
/The CSS key\/value definition did not end with a semicolon at column 1:15/g);
});
it('should parse the inner value of a :not() pseudo-selector as a CSS selector', () => {
var styles = `div:not(.ignore-this-div) {
prop: value;
}`;
var output = parse(styles);
var errors = output.errors;
var ast = output.ast;
expect(errors.length).toEqual(0);
var rule1 = <CssSelectorRuleAST>ast.rules[0];
expect(rule1.selectors.length).toEqual(1);
var selector = rule1.selectors[0];
assertTokens(selector.tokens, ['div', ':', 'not', '(', '.', 'ignore-this-div', ')']);
});
it('should raise parse errors when CSS key/value pairs are invalid', () => {
var styles = `.class {
background color: value;
color: value
font-size;
font-weight
}`;
var output = parse(styles);
var errors = output.errors;
expect(errors.length).toEqual(4);
expect(errors[0].msg)
.toMatchPattern(
/Identifier does not match expected Character value \("color" should match ":"\) at column 1:19/g);
expect(errors[1].msg)
.toMatchPattern(
/The CSS key\/value definition did not end with a semicolon at column 2:15/g);
expect(errors[2].msg)
.toMatchPattern(/The CSS property was not paired with a style value at column 3:8/g);
expect(errors[3].msg)
.toMatchPattern(/The CSS property was not paired with a style value at column 4:8/g);
});
it('should recover from CSS key/value parse errors', () => {
var styles = `
.problem-class { background color: red; color: white; }
.good-boy-class { background-color: red; color: white; }
`;
var output = parse(styles);
var ast = output.ast;
expect(ast.rules.length).toEqual(2);
var rule1 = <CssSelectorRuleAST>ast.rules[0];
expect(rule1.block.entries.length).toEqual(2);
var style1 = <CssDefinitionAST>rule1.block.entries[0];
expect(style1.property.strValue).toEqual('background color');
assertTokens(style1.value.tokens, ['red']);
var style2 = <CssDefinitionAST>rule1.block.entries[1];
expect(style2.property.strValue).toEqual('color');
assertTokens(style2.value.tokens, ['white']);
});
it('should parse minified CSS content properly', () => {
// this code was taken from the angular.io webpage's CSS code
var styles = `
.is-hidden{display:none!important}
.is-visible{display:block!important}
.is-visually-hidden{height:1px;width:1px;overflow:hidden;opacity:0.01;position:absolute;bottom:0;right:0;z-index:1}
.grid-fluid,.grid-fixed{margin:0 auto}
.grid-fluid .c1,.grid-fixed .c1,.grid-fluid .c2,.grid-fixed .c2,.grid-fluid .c3,.grid-fixed .c3,.grid-fluid .c4,.grid-fixed .c4,.grid-fluid .c5,.grid-fixed .c5,.grid-fluid .c6,.grid-fixed .c6,.grid-fluid .c7,.grid-fixed .c7,.grid-fluid .c8,.grid-fixed .c8,.grid-fluid .c9,.grid-fixed .c9,.grid-fluid .c10,.grid-fixed .c10,.grid-fluid .c11,.grid-fixed .c11,.grid-fluid .c12,.grid-fixed .c12{display:inline;float:left}
.grid-fluid .c1.grid-right,.grid-fixed .c1.grid-right,.grid-fluid .c2.grid-right,.grid-fixed .c2.grid-right,.grid-fluid .c3.grid-right,.grid-fixed .c3.grid-right,.grid-fluid .c4.grid-right,.grid-fixed .c4.grid-right,.grid-fluid .c5.grid-right,.grid-fixed .c5.grid-right,.grid-fluid .c6.grid-right,.grid-fixed .c6.grid-right,.grid-fluid .c7.grid-right,.grid-fixed .c7.grid-right,.grid-fluid .c8.grid-right,.grid-fixed .c8.grid-right,.grid-fluid .c9.grid-right,.grid-fixed .c9.grid-right,.grid-fluid .c10.grid-right,.grid-fixed .c10.grid-right,.grid-fluid .c11.grid-right,.grid-fixed .c11.grid-right,.grid-fluid .c12.grid-right,.grid-fixed .c12.grid-right{float:right}
.grid-fluid .c1.nb,.grid-fixed .c1.nb,.grid-fluid .c2.nb,.grid-fixed .c2.nb,.grid-fluid .c3.nb,.grid-fixed .c3.nb,.grid-fluid .c4.nb,.grid-fixed .c4.nb,.grid-fluid .c5.nb,.grid-fixed .c5.nb,.grid-fluid .c6.nb,.grid-fixed .c6.nb,.grid-fluid .c7.nb,.grid-fixed .c7.nb,.grid-fluid .c8.nb,.grid-fixed .c8.nb,.grid-fluid .c9.nb,.grid-fixed .c9.nb,.grid-fluid .c10.nb,.grid-fixed .c10.nb,.grid-fluid .c11.nb,.grid-fixed .c11.nb,.grid-fluid .c12.nb,.grid-fixed .c12.nb{margin-left:0}
.grid-fluid .c1.na,.grid-fixed .c1.na,.grid-fluid .c2.na,.grid-fixed .c2.na,.grid-fluid .c3.na,.grid-fixed .c3.na,.grid-fluid .c4.na,.grid-fixed .c4.na,.grid-fluid .c5.na,.grid-fixed .c5.na,.grid-fluid .c6.na,.grid-fixed .c6.na,.grid-fluid .c7.na,.grid-fixed .c7.na,.grid-fluid .c8.na,.grid-fixed .c8.na,.grid-fluid .c9.na,.grid-fixed .c9.na,.grid-fluid .c10.na,.grid-fixed .c10.na,.grid-fluid .c11.na,.grid-fixed .c11.na,.grid-fluid .c12.na,.grid-fixed .c12.na{margin-right:0}
`;
var output = parse(styles);
var errors = output.errors;
expect(errors.length).toEqual(0);
var ast = output.ast;
expect(ast.rules.length).toEqual(8);
});
it('should parse a snippet of keyframe code from animate.css properly', () => {
// this code was taken from the angular.io webpage's CSS code
var styles = `
@charset "UTF-8";
/*!
* animate.css -http://daneden.me/animate
* Version - 3.5.1
* Licensed under the MIT license - http://opensource.org/licenses/MIT
*
* Copyright (c) 2016 Daniel Eden
*/
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.animated.hinge {
-webkit-animation-duration: 2s;
animation-duration: 2s;
}
.animated.flipOutX,
.animated.flipOutY,
.animated.bounceIn,
.animated.bounceOut {
-webkit-animation-duration: .75s;
animation-duration: .75s;
}
@-webkit-keyframes bounce {
from, 20%, 53%, 80%, to {
-webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
40%, 43% {
-webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -30px, 0);
transform: translate3d(0, -30px, 0);
}
70% {
-webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -15px, 0);
transform: translate3d(0, -15px, 0);
}
90% {
-webkit-transform: translate3d(0,-4px,0);
transform: translate3d(0,-4px,0);
}
}
`;
var output = parse(styles);
var errors = output.errors;
expect(errors.length).toEqual(0);
var ast = output.ast;
expect(ast.rules.length).toEqual(6);
var finalRule = <CssBlockRuleAST>ast.rules[ast.rules.length - 1];
expect(finalRule.type).toEqual(BlockType.Keyframes);
expect(finalRule.block.entries.length).toEqual(4);
});
});
}

View File

@ -0,0 +1,252 @@
import {
ddescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach
} from 'angular2/testing_internal';
import {NumberWrapper, StringWrapper, isPresent} from "angular2/src/facade/lang";
import {BaseException} from 'angular2/src/facade/exceptions';
import {
CssToken,
CssParser,
CssParseError,
BlockType,
CssAST,
CssSelectorRuleAST,
CssKeyframeRuleAST,
CssKeyframeDefinitionAST,
CssBlockDefinitionRuleAST,
CssMediaQueryRuleAST,
CssBlockRuleAST,
CssInlineRuleAST,
CssStyleValueAST,
CssSelectorAST,
CssDefinitionAST,
CssStyleSheetAST,
CssRuleAST,
CssBlockAST,
CssASTVisitor,
CssUnknownTokenListAST
} from 'angular2/src/compiler/css/parser';
import {CssLexer} from 'angular2/src/compiler/css/lexer';
function _assertTokens(tokens, valuesArr) {
for (var i = 0; i < tokens.length; i++) {
expect(tokens[i].strValue == valuesArr[i]);
}
}
class MyVisitor implements CssASTVisitor {
captures: {[key: string]: any[]} = {};
_capture(method, ast, context) {
this.captures[method] = isPresent(this.captures[method]) ? this.captures[method] : [];
this.captures[method].push([ast, context]);
}
constructor(ast: CssStyleSheetAST, context?: any) { ast.visit(this, context); }
visitCssValue(ast, context?: any): void { this._capture("visitCssValue", ast, context); }
visitInlineCssRule(ast, context?: any): void {
this._capture("visitInlineCssRule", ast, context);
}
visitCssKeyframeRule(ast: CssKeyframeRuleAST, context?: any): void {
this._capture("visitCssKeyframeRule", ast, context);
ast.block.visit(this, context);
}
visitCssKeyframeDefinition(ast: CssKeyframeDefinitionAST, context?: any): void {
this._capture("visitCssKeyframeDefinition", ast, context);
ast.block.visit(this, context);
}
visitCssMediaQueryRule(ast: CssMediaQueryRuleAST, context?: any): void {
this._capture("visitCssMediaQueryRule", ast, context);
ast.block.visit(this, context);
}
visitCssSelectorRule(ast: CssSelectorRuleAST, context?: any): void {
this._capture("visitCssSelectorRule", ast, context);
ast.selectors.forEach((selAST: CssSelectorAST) => { selAST.visit(this, context); });
ast.block.visit(this, context);
}
visitCssSelector(ast: CssSelectorAST, context?: any): void {
this._capture("visitCssSelector", ast, context);
}
visitCssDefinition(ast: CssDefinitionAST, context?: any): void {
this._capture("visitCssDefinition", ast, context);
ast.value.visit(this, context);
}
visitCssBlock(ast: CssBlockAST, context?: any): void {
this._capture("visitCssBlock", ast, context);
ast.entries.forEach((entryAST: CssAST) => { entryAST.visit(this, context); });
}
visitCssStyleSheet(ast: CssStyleSheetAST, context?: any): void {
this._capture("visitCssStyleSheet", ast, context);
ast.rules.forEach((ruleAST: CssRuleAST) => { ruleAST.visit(this, context); });
}
visitUnkownRule(ast: CssUnknownTokenListAST, context?: any): void {
// nothing
}
}
export function main() {
function parse(cssCode: string) {
var lexer = new CssLexer();
var scanner = lexer.scan(cssCode);
var parser = new CssParser(scanner, 'some-fake-file-name.css');
var output = parser.parse();
var errors = output.errors;
if (errors.length > 0) {
throw new BaseException(errors.map((error: CssParseError) => error.msg).join(', '));
}
return output.ast;
}
describe('CSS parsing and visiting', () => {
var ast;
var context = {};
beforeEach(() => {
var cssCode = `
.rule1 { prop1: value1 }
.rule2 { prop2: value2 }
@media all (max-width: 100px) {
#id { prop3 :value3; }
}
@import url(file.css);
@keyframes rotate {
from {
prop4: value4;
}
50%, 100% {
prop5: value5;
}
}
`;
ast = parse(cssCode);
});
it('should parse and visit a stylesheet', () => {
var visitor = new MyVisitor(ast, context);
var captures = visitor.captures['visitCssStyleSheet'];
expect(captures.length).toEqual(1);
var capture = captures[0];
expect(capture[0]).toEqual(ast);
expect(capture[1]).toEqual(context);
});
it('should parse and visit each of the stylesheet selectors', () => {
var visitor = new MyVisitor(ast, context);
var captures = visitor.captures['visitCssSelectorRule'];
expect(captures.length).toEqual(3);
var rule1 = <CssSelectorRuleAST>captures[0][0];
expect(rule1).toEqual(ast.rules[0]);
_assertTokens(rule1.selectors[0].tokens, ['.', 'rule1']);
var rule2 = <CssSelectorRuleAST>captures[1][0];
expect(rule2).toEqual(ast.rules[1]);
_assertTokens(rule2.selectors[0].tokens, ['.', 'rule2']);
var rule3 = captures[2][0];
expect(rule3).toEqual((<CssMediaQueryRuleAST>ast.rules[2]).block.entries[0]);
_assertTokens(rule3.selectors[0].tokens, ['#', 'rule3']);
});
it('should parse and visit each of the stylesheet style key/value definitions', () => {
var visitor = new MyVisitor(ast, context);
var captures = visitor.captures['visitCssDefinition'];
expect(captures.length).toEqual(5);
var def1 = <CssDefinitionAST>captures[0][0];
expect(def1.property.strValue).toEqual('prop1');
expect(def1.value.tokens[0].strValue).toEqual('value1');
var def2 = <CssDefinitionAST>captures[1][0];
expect(def2.property.strValue).toEqual('prop2');
expect(def2.value.tokens[0].strValue).toEqual('value2');
var def3 = <CssDefinitionAST>captures[2][0];
expect(def3.property.strValue).toEqual('prop3');
expect(def3.value.tokens[0].strValue).toEqual('value3');
var def4 = <CssDefinitionAST>captures[3][0];
expect(def4.property.strValue).toEqual('prop4');
expect(def4.value.tokens[0].strValue).toEqual('value4');
var def5 = <CssDefinitionAST>captures[4][0];
expect(def5.property.strValue).toEqual('prop5');
expect(def5.value.tokens[0].strValue).toEqual('value5');
});
it('should parse and visit the associated media query values', () => {
var visitor = new MyVisitor(ast, context);
var captures = visitor.captures['visitCssMediaQueryRule'];
expect(captures.length).toEqual(1);
var query1 = <CssMediaQueryRuleAST>captures[0][0];
_assertTokens(query1.query, ["all", "and", "(", "max-width", "100", "px", ")"]);
expect(query1.block.entries.length).toEqual(1);
});
it('should parse and visit the associated "@inline" rule values', () => {
var visitor = new MyVisitor(ast, context);
var captures = visitor.captures['visitInlineCssRule'];
expect(captures.length).toEqual(1);
var query1 = <CssInlineRuleAST>captures[0][0];
expect(query1.type).toEqual(BlockType.Import);
_assertTokens(query1.value.tokens, ['url', '(', 'file.css', ')']);
});
it('should parse and visit the keyframe blocks', () => {
var visitor = new MyVisitor(ast, context);
var captures = visitor.captures['visitCssKeyframeRule'];
expect(captures.length).toEqual(1);
var keyframe1 = <CssKeyframeRuleAST>captures[0][0];
expect(keyframe1.name.strValue).toEqual('rotate');
expect(keyframe1.block.entries.length).toEqual(2);
});
it('should parse and visit the associated keyframe rules', () => {
var visitor = new MyVisitor(ast, context);
var captures = visitor.captures['visitCssKeyframeDefinition'];
expect(captures.length).toEqual(2);
var def1 = <CssKeyframeDefinitionAST>captures[0][0];
_assertTokens(def1.steps, ['from']);
expect(def1.block.entries.length).toEqual(1);
var def2 = <CssKeyframeDefinitionAST>captures[1][0];
_assertTokens(def2.steps, ['50%', '100%']);
expect(def2.block.entries.length).toEqual(1);
});
});
}

View File

@ -0,0 +1,148 @@
library angular2.test.core.compiler.directive_lifecycle_spec;
import 'package:angular2/testing_internal.dart';
import 'package:angular2/src/compiler/directive_lifecycle_reflector.dart';
import 'package:angular2/src/core/metadata/lifecycle_hooks.dart';
main() {
describe('Create DirectiveMetadata', () {
describe('lifecycle', () {
describe("ngOnChanges", () {
it("should be true when the directive has the ngOnChanges method", () {
expect(hasLifecycleHook(
LifecycleHooks.OnChanges, DirectiveImplementingOnChanges))
.toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(LifecycleHooks.OnChanges, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngOnDestroy", () {
it("should be true when the directive has the ngOnDestroy method", () {
expect(hasLifecycleHook(
LifecycleHooks.OnDestroy, DirectiveImplementingOnDestroy))
.toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(LifecycleHooks.OnDestroy, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngOnInit", () {
it("should be true when the directive has the ngOnInit method", () {
expect(hasLifecycleHook(
LifecycleHooks.OnInit, DirectiveImplementingOnInit)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(LifecycleHooks.OnInit, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngDoCheck", () {
it("should be true when the directive has the ngDoCheck method", () {
expect(hasLifecycleHook(
LifecycleHooks.DoCheck, DirectiveImplementingOnCheck)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(LifecycleHooks.DoCheck, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngAfterContentInit", () {
it("should be true when the directive has the ngAfterContentInit method",
() {
expect(hasLifecycleHook(LifecycleHooks.AfterContentInit,
DirectiveImplementingAfterContentInit)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(
LifecycleHooks.AfterContentInit, DirectiveNoHooks)).toBe(false);
});
});
describe("ngAfterContentChecked", () {
it("should be true when the directive has the ngAfterContentChecked method",
() {
expect(hasLifecycleHook(LifecycleHooks.AfterContentChecked,
DirectiveImplementingAfterContentChecked)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(
LifecycleHooks.AfterContentChecked, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngAfterViewInit", () {
it("should be true when the directive has the ngAfterViewInit method",
() {
expect(hasLifecycleHook(LifecycleHooks.AfterViewInit,
DirectiveImplementingAfterViewInit)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(
LifecycleHooks.AfterViewInit, DirectiveNoHooks)).toBe(false);
});
});
describe("ngAfterViewChecked", () {
it("should be true when the directive has the ngAfterViewChecked method",
() {
expect(hasLifecycleHook(LifecycleHooks.AfterViewChecked,
DirectiveImplementingAfterViewChecked)).toBe(true);
});
it("should be false otherwise", () {
expect(hasLifecycleHook(
LifecycleHooks.AfterViewChecked, DirectiveNoHooks)).toBe(false);
});
});
});
});
}
class DirectiveNoHooks {}
class DirectiveImplementingOnChanges implements OnChanges {
ngOnChanges(_) {}
}
class DirectiveImplementingOnCheck implements DoCheck {
ngDoCheck() {}
}
class DirectiveImplementingOnInit implements OnInit {
ngOnInit() {}
}
class DirectiveImplementingOnDestroy implements OnDestroy {
ngOnDestroy() {}
}
class DirectiveImplementingAfterContentInit implements AfterContentInit {
ngAfterContentInit() {}
}
class DirectiveImplementingAfterContentChecked implements AfterContentChecked {
ngAfterContentChecked() {}
}
class DirectiveImplementingAfterViewInit implements AfterViewInit {
ngAfterViewInit() {}
}
class DirectiveImplementingAfterViewChecked implements AfterViewChecked {
ngAfterViewChecked() {}
}

View File

@ -0,0 +1,149 @@
import {
AsyncTestCompleter,
beforeEach,
xdescribe,
ddescribe,
describe,
el,
expect,
iit,
inject,
it,
SpyObject,
proxy
} from 'angular2/testing_internal';
import {hasLifecycleHook} from 'angular2/src/compiler/directive_lifecycle_reflector';
import {LifecycleHooks} from 'angular2/src/core/metadata/lifecycle_hooks';
export function main() {
describe('Create DirectiveMetadata', () => {
describe('lifecycle', () => {
describe("ngOnChanges", () => {
it("should be true when the directive has the ngOnChanges method", () => {
expect(hasLifecycleHook(LifecycleHooks.OnChanges, DirectiveWithOnChangesMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.OnChanges, DirectiveNoHooks)).toBe(false);
});
});
describe("ngOnDestroy", () => {
it("should be true when the directive has the ngOnDestroy method", () => {
expect(hasLifecycleHook(LifecycleHooks.OnDestroy, DirectiveWithOnDestroyMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.OnDestroy, DirectiveNoHooks)).toBe(false);
});
});
describe("ngOnInit", () => {
it("should be true when the directive has the ngOnInit method", () => {
expect(hasLifecycleHook(LifecycleHooks.OnInit, DirectiveWithOnInitMethod)).toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.OnInit, DirectiveNoHooks)).toBe(false);
});
});
describe("ngDoCheck", () => {
it("should be true when the directive has the ngDoCheck method", () => {
expect(hasLifecycleHook(LifecycleHooks.DoCheck, DirectiveWithOnCheckMethod)).toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.DoCheck, DirectiveNoHooks)).toBe(false);
});
});
describe("ngAfterContentInit", () => {
it("should be true when the directive has the ngAfterContentInit method", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterContentInit,
DirectiveWithAfterContentInitMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterContentInit, DirectiveNoHooks)).toBe(false);
});
});
describe("ngAfterContentChecked", () => {
it("should be true when the directive has the ngAfterContentChecked method", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterContentChecked,
DirectiveWithAfterContentCheckedMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterContentChecked, DirectiveNoHooks))
.toBe(false);
});
});
describe("ngAfterViewInit", () => {
it("should be true when the directive has the ngAfterViewInit method", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterViewInit, DirectiveWithAfterViewInitMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterViewInit, DirectiveNoHooks)).toBe(false);
});
});
describe("ngAfterViewChecked", () => {
it("should be true when the directive has the ngAfterViewChecked method", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterViewChecked,
DirectiveWithAfterViewCheckedMethod))
.toBe(true);
});
it("should be false otherwise", () => {
expect(hasLifecycleHook(LifecycleHooks.AfterViewChecked, DirectiveNoHooks)).toBe(false);
});
});
});
});
}
class DirectiveNoHooks {}
class DirectiveWithOnChangesMethod {
ngOnChanges(_) {}
}
class DirectiveWithOnInitMethod {
ngOnInit() {}
}
class DirectiveWithOnCheckMethod {
ngDoCheck() {}
}
class DirectiveWithOnDestroyMethod {
ngOnDestroy() {}
}
class DirectiveWithAfterContentInitMethod {
ngAfterContentInit() {}
}
class DirectiveWithAfterContentCheckedMethod {
ngAfterContentChecked() {}
}
class DirectiveWithAfterViewInitMethod {
ngAfterViewInit() {}
}
class DirectiveWithAfterViewCheckedMethod {
ngAfterViewChecked() {}
}

View File

@ -0,0 +1,386 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
el,
expect,
iit,
inject,
it,
xit,
TestComponentBuilder,
beforeEachProviders
} from 'angular2/testing_internal';
import {CompileTypeMetadata, CompileTemplateMetadata} from 'angular2/src/compiler/compile_metadata';
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';
import {DirectiveNormalizer} from 'angular2/src/compiler/directive_normalizer';
import {XHR} from 'angular2/src/compiler/xhr';
import {MockXHR} from 'angular2/src/compiler/xhr_mock';
import {TEST_PROVIDERS} from './test_bindings';
export function main() {
describe('DirectiveNormalizer', () => {
var dirType: CompileTypeMetadata;
beforeEachProviders(() => TEST_PROVIDERS);
beforeEach(() => { dirType = new CompileTypeMetadata({name: 'SomeComp'}); });
describe('loadTemplate', () => {
describe('inline template', () => {
it('should store the template',
inject([AsyncTestCompleter, DirectiveNormalizer],
(async, normalizer: DirectiveNormalizer) => {
normalizer.normalizeTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
template: 'a',
templateUrl: null,
styles: [],
styleUrls: ['test.css'],
baseUrl: 'package:some/module/a.js'
}))
.then((template: CompileTemplateMetadata) => {
expect(template.template).toEqual('a');
expect(template.templateUrl).toEqual('package:some/module/a.js');
async.done();
});
}));
it('should resolve styles on the annotation against the baseUrl',
inject([AsyncTestCompleter, DirectiveNormalizer],
(async, normalizer: DirectiveNormalizer) => {
normalizer.normalizeTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
template: '',
templateUrl: null,
styles: [],
styleUrls: ['test.css'],
baseUrl: 'package:some/module/a.js'
}))
.then((template: CompileTemplateMetadata) => {
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
async.done();
});
}));
it('should resolve styles in the template against the baseUrl',
inject([AsyncTestCompleter, DirectiveNormalizer],
(async, normalizer: DirectiveNormalizer) => {
normalizer.normalizeTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
template: '<style>@import test.css</style>',
templateUrl: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}))
.then((template: CompileTemplateMetadata) => {
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
async.done();
});
}));
});
describe('templateUrl', () => {
it('should load a template from a url that is resolved against baseUrl',
inject([AsyncTestCompleter, DirectiveNormalizer, XHR],
(async, normalizer: DirectiveNormalizer, xhr: MockXHR) => {
xhr.expect('package:some/module/sometplurl.html', 'a');
normalizer.normalizeTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
template: null,
templateUrl: 'sometplurl.html',
styles: [],
styleUrls: ['test.css'],
baseUrl: 'package:some/module/a.js'
}))
.then((template: CompileTemplateMetadata) => {
expect(template.template).toEqual('a');
expect(template.templateUrl)
.toEqual('package:some/module/sometplurl.html');
async.done();
});
xhr.flush();
}));
it('should resolve styles on the annotation against the baseUrl',
inject([AsyncTestCompleter, DirectiveNormalizer, XHR],
(async, normalizer: DirectiveNormalizer, xhr: MockXHR) => {
xhr.expect('package:some/module/tpl/sometplurl.html', '');
normalizer.normalizeTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
template: null,
templateUrl: 'tpl/sometplurl.html',
styles: [],
styleUrls: ['test.css'],
baseUrl: 'package:some/module/a.js'
}))
.then((template: CompileTemplateMetadata) => {
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
async.done();
});
xhr.flush();
}));
it('should resolve styles in the template against the templateUrl',
inject([AsyncTestCompleter, DirectiveNormalizer, XHR],
(async, normalizer: DirectiveNormalizer, xhr: MockXHR) => {
xhr.expect('package:some/module/tpl/sometplurl.html',
'<style>@import test.css</style>');
normalizer.normalizeTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
template: null,
templateUrl: 'tpl/sometplurl.html',
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}))
.then((template: CompileTemplateMetadata) => {
expect(template.styleUrls).toEqual(['package:some/module/tpl/test.css']);
async.done();
});
xhr.flush();
}));
});
it('should throw if no template was specified',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
expect(() => normalizer.normalizeTemplate(
dirType, new CompileTemplateMetadata(
{encapsulation: null, styles: [], styleUrls: []})))
.toThrowError('No template specified for component SomeComp');
}));
});
describe('normalizeLoadedTemplate', () => {
it('should store the viewEncapsulationin the result',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var viewEncapsulation = ViewEncapsulation.Native;
var template = normalizer.normalizeLoadedTemplate(dirType, new CompileTemplateMetadata({
encapsulation: viewEncapsulation,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'', 'package:some/module/');
expect(template.encapsulation).toBe(viewEncapsulation);
}));
it('should keep the template as html',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'a', 'package:some/module/');
expect(template.template).toEqual('a')
}));
it('should collect ngContent',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'<ng-content select="a"></ng-content>',
'package:some/module/');
expect(template.ngContentSelectors).toEqual(['a']);
}));
it('should normalize ngContent wildcard selector',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(
dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'<ng-content></ng-content><ng-content select></ng-content><ng-content select="*"></ng-content>',
'package:some/module/');
expect(template.ngContentSelectors).toEqual(['*', '*', '*']);
}));
it('should collect top level styles in the template',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template =
normalizer.normalizeLoadedTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'<style>a</style>', 'package:some/module/');
expect(template.styles).toEqual(['a']);
}));
it('should collect styles inside in elements',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'<div><style>a</style></div>',
'package:some/module/');
expect(template.styles).toEqual(['a']);
}));
it('should collect styleUrls in the template',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'<link rel="stylesheet" href="aUrl">',
'package:some/module/');
expect(template.styleUrls).toEqual(['package:some/module/aUrl']);
}));
it('should collect styleUrls in elements',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(
dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'<div><link rel="stylesheet" href="aUrl"></div>', 'package:some/module/');
expect(template.styleUrls).toEqual(['package:some/module/aUrl']);
}));
it('should ignore link elements with non stylesheet rel attribute',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'<link href="b" rel="a">',
'package:some/module/');
expect(template.styleUrls).toEqual([]);
}));
it('should ignore link elements with absolute urls but non package: scheme',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(
dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'<link href="http://some/external.css" rel="stylesheet">', 'package:some/module/');
expect(template.styleUrls).toEqual([]);
}));
it('should extract @import style urls into styleAbsUrl',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: ['@import "test.css";'],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'', 'package:some/module/id');
expect(template.styles).toEqual(['']);
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
}));
it('should not resolve relative urls in inline styles',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(
dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: ['.foo{background-image: url(\'double.jpg\');'],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'', 'package:some/module/id');
expect(template.styles).toEqual(['.foo{background-image: url(\'double.jpg\');']);
}));
it('should resolve relative style urls in styleUrls',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: ['test.css'],
baseUrl: 'package:some/module/a.js'
}),
'', 'package:some/module/id');
expect(template.styles).toEqual([]);
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
}));
it('should resolve relative style urls in styleUrls with http directive url',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: ['test.css'],
baseUrl: 'http://some/module/a.js'
}),
'', 'http://some/module/id');
expect(template.styles).toEqual([]);
expect(template.styleUrls).toEqual(['http://some/module/test.css']);
}));
it('should normalize ViewEncapsulation.Emulated to ViewEncapsulation.None if there are no styles nor stylesheets',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template =
normalizer.normalizeLoadedTemplate(dirType, new CompileTemplateMetadata({
encapsulation: ViewEncapsulation.Emulated,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'', 'package:some/module/id');
expect(template.encapsulation).toEqual(ViewEncapsulation.None);
}));
it('should ignore ng-content in elements with ngNonBindable',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(
dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'<div ngNonBindable><ng-content select="a"></ng-content></div>',
'package:some/module/');
expect(template.ngContentSelectors).toEqual([]);
}));
it('should still collect <style> in elements with ngNonBindable',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
var template = normalizer.normalizeLoadedTemplate(
dirType, new CompileTemplateMetadata({
encapsulation: null,
styles: [],
styleUrls: [],
baseUrl: 'package:some/module/a.js'
}),
'<div ngNonBindable><style>div {color:red}</style></div>', 'package:some/module/');
expect(template.styles).toEqual(['div {color:red}']);
}));
});
});
}

View File

@ -0,0 +1,209 @@
import {ddescribe, describe, it, iit, expect, beforeEach} from 'angular2/testing_internal';
import {DirectiveResolver} from 'angular2/src/compiler/directive_resolver';
import {
DirectiveMetadata,
Directive,
Input,
Output,
HostBinding,
HostListener,
ContentChildren,
ContentChildrenMetadata,
ViewChildren,
ViewChildrenMetadata,
ContentChild,
ContentChildMetadata,
ViewChild,
ViewChildMetadata
} from 'angular2/src/core/metadata';
@Directive({selector: 'someDirective'})
class SomeDirective {
}
@Directive({selector: 'someChildDirective'})
class SomeChildDirective extends SomeDirective {
}
@Directive({selector: 'someDirective', inputs: ['c']})
class SomeDirectiveWithInputs {
@Input() a;
@Input("renamed") b;
c;
}
@Directive({selector: 'someDirective', outputs: ['c']})
class SomeDirectiveWithOutputs {
@Output() a;
@Output("renamed") b;
c;
}
@Directive({selector: 'someDirective', outputs: ['a']})
class SomeDirectiveWithDuplicateOutputs {
@Output() a;
}
@Directive({selector: 'someDirective', properties: ['a']})
class SomeDirectiveWithProperties {
}
@Directive({selector: 'someDirective', events: ['a']})
class SomeDirectiveWithEvents {
}
@Directive({selector: 'someDirective'})
class SomeDirectiveWithSetterProps {
@Input("renamed")
set a(value) {
}
}
@Directive({selector: 'someDirective'})
class SomeDirectiveWithGetterOutputs {
@Output("renamed")
get a() {
return null;
}
}
@Directive({selector: 'someDirective', host: {'[c]': 'c'}})
class SomeDirectiveWithHostBindings {
@HostBinding() a;
@HostBinding("renamed") b;
c;
}
@Directive({selector: 'someDirective', host: {'(c)': 'onC()'}})
class SomeDirectiveWithHostListeners {
@HostListener('a')
onA() {
}
@HostListener('b', ['$event.value'])
onB(value) {
}
}
@Directive({selector: 'someDirective', queries: {"cs": new ContentChildren("c")}})
class SomeDirectiveWithContentChildren {
@ContentChildren("a") as: any;
c;
}
@Directive({selector: 'someDirective', queries: {"cs": new ViewChildren("c")}})
class SomeDirectiveWithViewChildren {
@ViewChildren("a") as: any;
c;
}
@Directive({selector: 'someDirective', queries: {"c": new ContentChild("c")}})
class SomeDirectiveWithContentChild {
@ContentChild("a") a: any;
c;
}
@Directive({selector: 'someDirective', queries: {"c": new ViewChild("c")}})
class SomeDirectiveWithViewChild {
@ViewChild("a") a: any;
c;
}
class SomeDirectiveWithoutMetadata {}
export function main() {
describe("DirectiveResolver", () => {
var resolver: DirectiveResolver;
beforeEach(() => { resolver = new DirectiveResolver(); });
it('should read out the Directive metadata', () => {
var directiveMetadata = resolver.resolve(SomeDirective);
expect(directiveMetadata)
.toEqual(new DirectiveMetadata(
{selector: 'someDirective', inputs: [], outputs: [], host: {}, queries: {}}));
});
it('should throw if not matching metadata is found', () => {
expect(() => { resolver.resolve(SomeDirectiveWithoutMetadata); })
.toThrowError('No Directive annotation found on SomeDirectiveWithoutMetadata');
});
it('should not read parent class Directive metadata', function() {
var directiveMetadata = resolver.resolve(SomeChildDirective);
expect(directiveMetadata)
.toEqual(new DirectiveMetadata(
{selector: 'someChildDirective', inputs: [], outputs: [], host: {}, queries: {}}));
});
describe('inputs', () => {
it('should append directive inputs', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithInputs);
expect(directiveMetadata.inputs).toEqual(['c', 'a', 'b: renamed']);
});
it('should work with getters and setters', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithSetterProps);
expect(directiveMetadata.inputs).toEqual(['a: renamed']);
});
});
describe('outputs', () => {
it('should append directive outputs', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithOutputs);
expect(directiveMetadata.outputs).toEqual(['c', 'a', 'b: renamed']);
});
it('should work with getters and setters', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithGetterOutputs);
expect(directiveMetadata.outputs).toEqual(['a: renamed']);
});
it('should throw if duplicate outputs', () => {
expect(() => { resolver.resolve(SomeDirectiveWithDuplicateOutputs); })
.toThrowError(
`Output event 'a' defined multiple times in 'SomeDirectiveWithDuplicateOutputs'`);
});
});
describe('host', () => {
it('should append host bindings', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithHostBindings);
expect(directiveMetadata.host).toEqual({'[c]': 'c', '[a]': 'a', '[renamed]': 'b'});
});
it('should append host listeners', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithHostListeners);
expect(directiveMetadata.host)
.toEqual({'(c)': 'onC()', '(a)': 'onA()', '(b)': 'onB($event.value)'});
});
});
describe('queries', () => {
it('should append ContentChildren', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithContentChildren);
expect(directiveMetadata.queries)
.toEqual({"cs": new ContentChildren("c"), "as": new ContentChildren("a")});
});
it('should append ViewChildren', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithViewChildren);
expect(directiveMetadata.queries)
.toEqual({"cs": new ViewChildren("c"), "as": new ViewChildren("a")});
});
it('should append ContentChild', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithContentChild);
expect(directiveMetadata.queries)
.toEqual({"c": new ContentChild("c"), "a": new ContentChild("a")});
});
it('should append ViewChild', () => {
var directiveMetadata = resolver.resolve(SomeDirectiveWithViewChild);
expect(directiveMetadata.queries)
.toEqual({"c": new ViewChild("c"), "a": new ViewChild("a")});
});
});
});
}

View File

@ -0,0 +1,249 @@
import {ddescribe, describe, it, expect} from 'angular2/testing_internal';
import {Lexer, Token} from 'angular2/src/compiler/expression_parser/lexer';
import {StringWrapper} from "angular2/src/facade/lang";
function lex(text: string): any[] {
return new Lexer().tokenize(text);
}
function expectToken(token, index) {
expect(token instanceof Token).toBe(true);
expect(token.index).toEqual(index);
}
function expectCharacterToken(token, index, character) {
expect(character.length).toBe(1);
expectToken(token, index);
expect(token.isCharacter(StringWrapper.charCodeAt(character, 0))).toBe(true);
}
function expectOperatorToken(token, index, operator) {
expectToken(token, index);
expect(token.isOperator(operator)).toBe(true);
}
function expectNumberToken(token, index, n) {
expectToken(token, index);
expect(token.isNumber()).toBe(true);
expect(token.toNumber()).toEqual(n);
}
function expectStringToken(token, index, str) {
expectToken(token, index);
expect(token.isString()).toBe(true);
expect(token.toString()).toEqual(str);
}
function expectIdentifierToken(token, index, identifier) {
expectToken(token, index);
expect(token.isIdentifier()).toBe(true);
expect(token.toString()).toEqual(identifier);
}
function expectKeywordToken(token, index, keyword) {
expectToken(token, index);
expect(token.isKeyword()).toBe(true);
expect(token.toString()).toEqual(keyword);
}
export function main() {
describe('lexer', function() {
describe('token', function() {
it('should tokenize a simple identifier', function() {
var tokens: number[] = lex("j");
expect(tokens.length).toEqual(1);
expectIdentifierToken(tokens[0], 0, 'j');
});
it('should tokenize a dotted identifier', function() {
var tokens: number[] = lex("j.k");
expect(tokens.length).toEqual(3);
expectIdentifierToken(tokens[0], 0, 'j');
expectCharacterToken(tokens[1], 1, '.');
expectIdentifierToken(tokens[2], 2, 'k');
});
it('should tokenize an operator', function() {
var tokens: number[] = lex("j-k");
expect(tokens.length).toEqual(3);
expectOperatorToken(tokens[1], 1, '-');
});
it('should tokenize an indexed operator', function() {
var tokens: number[] = lex("j[k]");
expect(tokens.length).toEqual(4);
expectCharacterToken(tokens[1], 1, "[");
expectCharacterToken(tokens[3], 3, "]");
});
it('should tokenize numbers', function() {
var tokens: number[] = lex("88");
expect(tokens.length).toEqual(1);
expectNumberToken(tokens[0], 0, 88);
});
it('should tokenize numbers within index ops',
function() { expectNumberToken(lex("a[22]")[2], 2, 22); });
it('should tokenize simple quoted strings',
function() { expectStringToken(lex('"a"')[0], 0, "a"); });
it('should tokenize quoted strings with escaped quotes',
function() { expectStringToken(lex('"a\\""')[0], 0, 'a"'); });
it('should tokenize a string', function() {
var tokens: Token[] = lex("j-a.bc[22]+1.3|f:'a\\\'c':\"d\\\"e\"");
expectIdentifierToken(tokens[0], 0, 'j');
expectOperatorToken(tokens[1], 1, '-');
expectIdentifierToken(tokens[2], 2, 'a');
expectCharacterToken(tokens[3], 3, '.');
expectIdentifierToken(tokens[4], 4, 'bc');
expectCharacterToken(tokens[5], 6, '[');
expectNumberToken(tokens[6], 7, 22);
expectCharacterToken(tokens[7], 9, ']');
expectOperatorToken(tokens[8], 10, '+');
expectNumberToken(tokens[9], 11, 1.3);
expectOperatorToken(tokens[10], 14, '|');
expectIdentifierToken(tokens[11], 15, 'f');
expectCharacterToken(tokens[12], 16, ':');
expectStringToken(tokens[13], 17, "a'c");
expectCharacterToken(tokens[14], 23, ':');
expectStringToken(tokens[15], 24, 'd"e');
});
it('should tokenize undefined', function() {
var tokens: Token[] = lex("undefined");
expectKeywordToken(tokens[0], 0, "undefined");
expect(tokens[0].isKeywordUndefined()).toBe(true);
});
it('should ignore whitespace', function() {
var tokens: Token[] = lex("a \t \n \r b");
expectIdentifierToken(tokens[0], 0, 'a');
expectIdentifierToken(tokens[1], 8, 'b');
});
it('should tokenize quoted string', () => {
var str = "['\\'', \"\\\"\"]";
var tokens: Token[] = lex(str);
expectStringToken(tokens[1], 1, "'");
expectStringToken(tokens[3], 7, '"');
});
it('should tokenize escaped quoted string', () => {
var str = '"\\"\\n\\f\\r\\t\\v\\u00A0"';
var tokens: Token[] = lex(str);
expect(tokens.length).toEqual(1);
expect(tokens[0].toString()).toEqual('"\n\f\r\t\v\u00A0');
});
it('should tokenize unicode', function() {
var tokens: Token[] = lex('"\\u00A0"');
expect(tokens.length).toEqual(1);
expect(tokens[0].toString()).toEqual('\u00a0');
});
it('should tokenize relation', function() {
var tokens: Token[] = lex("! == != < > <= >= === !==");
expectOperatorToken(tokens[0], 0, '!');
expectOperatorToken(tokens[1], 2, '==');
expectOperatorToken(tokens[2], 5, '!=');
expectOperatorToken(tokens[3], 8, '<');
expectOperatorToken(tokens[4], 10, '>');
expectOperatorToken(tokens[5], 12, '<=');
expectOperatorToken(tokens[6], 15, '>=');
expectOperatorToken(tokens[7], 18, '===');
expectOperatorToken(tokens[8], 22, '!==');
});
it('should tokenize statements', function() {
var tokens: Token[] = lex("a;b;");
expectIdentifierToken(tokens[0], 0, 'a');
expectCharacterToken(tokens[1], 1, ';');
expectIdentifierToken(tokens[2], 2, 'b');
expectCharacterToken(tokens[3], 3, ';');
});
it('should tokenize function invocation', function() {
var tokens: Token[] = lex("a()");
expectIdentifierToken(tokens[0], 0, 'a');
expectCharacterToken(tokens[1], 1, '(');
expectCharacterToken(tokens[2], 2, ')');
});
it('should tokenize simple method invocations', function() {
var tokens: Token[] = lex("a.method()");
expectIdentifierToken(tokens[2], 2, 'method');
});
it('should tokenize method invocation', function() {
var tokens: Token[] = lex("a.b.c (d) - e.f()");
expectIdentifierToken(tokens[0], 0, 'a');
expectCharacterToken(tokens[1], 1, '.');
expectIdentifierToken(tokens[2], 2, 'b');
expectCharacterToken(tokens[3], 3, '.');
expectIdentifierToken(tokens[4], 4, 'c');
expectCharacterToken(tokens[5], 6, '(');
expectIdentifierToken(tokens[6], 7, 'd');
expectCharacterToken(tokens[7], 8, ')');
expectOperatorToken(tokens[8], 10, '-');
expectIdentifierToken(tokens[9], 12, 'e');
expectCharacterToken(tokens[10], 13, '.');
expectIdentifierToken(tokens[11], 14, 'f');
expectCharacterToken(tokens[12], 15, '(');
expectCharacterToken(tokens[13], 16, ')');
});
it('should tokenize number', function() {
var tokens: Token[] = lex("0.5");
expectNumberToken(tokens[0], 0, 0.5);
});
// NOTE(deboer): NOT A LEXER TEST
// it('should tokenize negative number', () => {
// var tokens:Token[] = lex("-0.5");
// expectNumberToken(tokens[0], 0, -0.5);
// });
it('should tokenize number with exponent', function() {
var tokens: Token[] = lex("0.5E-10");
expect(tokens.length).toEqual(1);
expectNumberToken(tokens[0], 0, 0.5E-10);
tokens = lex("0.5E+10");
expectNumberToken(tokens[0], 0, 0.5E+10);
});
it('should throws exception for invalid exponent', function() {
expect(() => { lex("0.5E-"); })
.toThrowError('Lexer Error: Invalid exponent at column 4 in expression [0.5E-]');
expect(() => { lex("0.5E-A"); })
.toThrowError('Lexer Error: Invalid exponent at column 4 in expression [0.5E-A]');
});
it('should tokenize number starting with a dot', function() {
var tokens: Token[] = lex(".5");
expectNumberToken(tokens[0], 0, 0.5);
});
it('should throw error on invalid unicode', function() {
expect(() => { lex("'\\u1''bla'"); })
.toThrowError(
"Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla']");
});
it('should tokenize hash as operator', function() {
var tokens: Token[] = lex("#");
expectOperatorToken(tokens[0], 0, '#');
});
it('should tokenize ?. as operator', () => {
var tokens: Token[] = lex('?.');
expectOperatorToken(tokens[0], 0, '?.');
});
});
});
}

View File

@ -0,0 +1,511 @@
import {ddescribe, describe, it, xit, iit, expect, beforeEach} from 'angular2/testing_internal';
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {Unparser} from './unparser';
import {Lexer} from 'angular2/src/compiler/expression_parser/lexer';
import {BindingPipe, LiteralPrimitive, AST} from 'angular2/src/compiler/expression_parser/ast';
export function main() {
function createParser() { return new Parser(new Lexer()); }
function parseAction(text, location = null): any {
return createParser().parseAction(text, location);
}
function parseBinding(text, location = null): any {
return createParser().parseBinding(text, location);
}
function parseTemplateBindings(text, location = null): any {
return createParser().parseTemplateBindings(text, location).templateBindings;
}
function parseInterpolation(text, location = null): any {
return createParser().parseInterpolation(text, location);
}
function parseSimpleBinding(text, location = null): any {
return createParser().parseSimpleBinding(text, location);
}
function unparse(ast: AST): string { return new Unparser().unparse(ast); }
function checkInterpolation(exp: string, expected?: string) {
var ast = parseInterpolation(exp);
if (isBlank(expected)) expected = exp;
expect(unparse(ast)).toEqual(expected);
}
function checkBinding(exp: string, expected?: string) {
var ast = parseBinding(exp);
if (isBlank(expected)) expected = exp;
expect(unparse(ast)).toEqual(expected);
}
function checkAction(exp: string, expected?: string) {
var ast = parseAction(exp);
if (isBlank(expected)) expected = exp;
expect(unparse(ast)).toEqual(expected);
}
function expectActionError(text) { return expect(() => parseAction(text)); }
function expectBindingError(text) { return expect(() => parseBinding(text)); }
describe("parser", () => {
describe("parseAction", () => {
it('should parse numbers', () => { checkAction("1"); });
it('should parse strings', () => {
checkAction("'1'", '"1"');
checkAction('"1"');
});
it('should parse null', () => { checkAction("null"); });
it('should parse unary - expressions', () => {
checkAction("-1", "0 - 1");
checkAction("+1", "1");
});
it('should parse unary ! expressions', () => {
checkAction("!true");
checkAction("!!true");
checkAction("!!!true");
});
it('should parse multiplicative expressions',
() => { checkAction("3*4/2%5", "3 * 4 / 2 % 5"); });
it('should parse additive expressions', () => { checkAction("3 + 6 - 2"); });
it('should parse relational expressions', () => {
checkAction("2 < 3");
checkAction("2 > 3");
checkAction("2 <= 2");
checkAction("2 >= 2");
});
it('should parse equality expressions', () => {
checkAction("2 == 3");
checkAction("2 != 3");
});
it('should parse strict equality expressions', () => {
checkAction("2 === 3");
checkAction("2 !== 3");
});
it('should parse expressions', () => {
checkAction("true && true");
checkAction("true || false");
});
it('should parse grouped expressions', () => { checkAction("(1 + 2) * 3", "1 + 2 * 3"); });
it('should ignore comments in expressions', () => { checkAction('a //comment', 'a'); });
it('should retain // in string literals',
() => { checkAction(`"http://www.google.com"`, `"http://www.google.com"`); });
it('should parse an empty string', () => { checkAction(''); });
describe("literals", () => {
it('should parse array', () => {
checkAction("[1][0]");
checkAction("[[1]][0][0]");
checkAction("[]");
checkAction("[].length");
checkAction("[1, 2].length");
});
it('should parse map', () => {
checkAction("{}");
checkAction("{a: 1}[2]");
checkAction("{}[\"a\"]");
});
it('should only allow identifier, string, or keyword as map key', () => {
expectActionError('{(:0}')
.toThrowError(new RegExp('expected identifier, keyword, or string'));
expectActionError('{1234:0}')
.toThrowError(new RegExp('expected identifier, keyword, or string'));
});
});
describe("member access", () => {
it("should parse field access", () => {
checkAction("a");
checkAction("a.a");
});
it('should only allow identifier or keyword as member names', () => {
expectActionError('x.(').toThrowError(new RegExp('identifier or keyword'));
expectActionError('x. 1234').toThrowError(new RegExp('identifier or keyword'));
expectActionError('x."foo"').toThrowError(new RegExp('identifier or keyword'));
});
it('should parse safe field access', () => {
checkAction('a?.a');
checkAction('a.a?.a');
});
});
describe("method calls", () => {
it("should parse method calls", () => {
checkAction("fn()");
checkAction("add(1, 2)");
checkAction("a.add(1, 2)");
checkAction("fn().add(1, 2)");
});
});
describe("functional calls",
() => { it("should parse function calls", () => { checkAction("fn()(1, 2)"); }); });
describe("conditional", () => {
it('should parse ternary/conditional expressions', () => {
checkAction("7 == 3 + 4 ? 10 : 20");
checkAction("false ? 10 : 20");
});
it('should throw on incorrect ternary operator syntax', () => {
expectActionError("true?1").toThrowError(new RegExp(
'Parser Error: Conditional expression true\\?1 requires all 3 expressions'));
});
});
describe("assignment", () => {
it("should support field assignments", () => {
checkAction("a = 12");
checkAction("a.a.a = 123");
checkAction("a = 123; b = 234;");
});
it("should throw on safe field assignments", () => {
expectActionError("a?.a = 123")
.toThrowError(new RegExp('cannot be used in the assignment'));
});
it("should support array updates", () => { checkAction("a[0] = 200"); });
});
it("should error when using pipes",
() => { expectActionError('x|blah').toThrowError(new RegExp('Cannot have a pipe')); });
it('should store the source in the result',
() => { expect(parseAction('someExpr').source).toBe('someExpr'); });
it('should store the passed-in location',
() => { expect(parseAction('someExpr', 'location').location).toBe('location'); });
it("should throw when encountering interpolation", () => {
expectActionError("{{a()}}")
.toThrowErrorWith('Got interpolation ({{}}) where expression was expected');
});
});
describe("general error handling", () => {
it("should throw on an unexpected token", () => {
expectActionError("[1,2] trac").toThrowError(new RegExp('Unexpected token \'trac\''));
});
it('should throw a reasonable error for unconsumed tokens', () => {
expectActionError(")")
.toThrowError(new RegExp("Unexpected token \\) at column 1 in \\[\\)\\]"));
});
it('should throw on missing expected token', () => {
expectActionError("a(b").toThrowError(
new RegExp("Missing expected \\) at the end of the expression \\[a\\(b\\]"));
});
});
describe("parseBinding", () => {
describe("pipes", () => {
it("should parse pipes", () => {
checkBinding('a(b | c)', 'a((b | c))');
checkBinding('a.b(c.d(e) | f)', 'a.b((c.d(e) | f))');
checkBinding('[1, 2, 3] | a', '([1, 2, 3] | a)');
checkBinding('{a: 1} | b', '({a: 1} | b)');
checkBinding('a[b] | c', '(a[b] | c)');
checkBinding('a?.b | c', '(a?.b | c)');
checkBinding('true | a', '(true | a)');
checkBinding('a | b:c | d', '((a | b:c) | d)');
checkBinding('a | b:(c | d)', '(a | b:(c | d))');
});
it('should only allow identifier or keyword as formatter names', () => {
expectBindingError('"Foo"|(').toThrowError(new RegExp('identifier or keyword'));
expectBindingError('"Foo"|1234').toThrowError(new RegExp('identifier or keyword'));
expectBindingError('"Foo"|"uppercase"').toThrowError(new RegExp('identifier or keyword'));
});
it('should parse quoted expressions', () => { checkBinding('a:b', 'a:b'); });
it('should not crash when prefix part is not tokenizable',
() => { checkBinding('"a:b"', '"a:b"'); });
it('should ignore whitespace around quote prefix', () => { checkBinding(' a :b', 'a:b'); });
it('should refuse prefixes that are not single identifiers', () => {
expectBindingError('a + b:c').toThrowError();
expectBindingError('1:c').toThrowError();
});
});
it('should store the source in the result',
() => { expect(parseBinding('someExpr').source).toBe('someExpr'); });
it('should store the passed-in location',
() => { expect(parseBinding('someExpr', 'location').location).toBe('location'); });
it('should throw on chain expressions', () => {
expect(() => parseBinding("1;2")).toThrowError(new RegExp("contain chained expression"));
});
it('should throw on assignment', () => {
expect(() => parseBinding("a=2")).toThrowError(new RegExp("contain assignments"));
});
it('should throw when encountering interpolation', () => {
expectBindingError("{{a.b}}")
.toThrowErrorWith('Got interpolation ({{}}) where expression was expected');
});
it('should parse conditional expression', () => { checkBinding('a < b ? a : b'); });
it('should ignore comments in bindings', () => { checkBinding('a //comment', 'a'); });
it('should retain // in string literals',
() => { checkBinding(`"http://www.google.com"`, `"http://www.google.com"`); });
it('should retain // in : microsyntax', () => { checkBinding('one:a//b', 'one:a//b'); });
});
describe('parseTemplateBindings', () => {
function keys(templateBindings: any[]) {
return templateBindings.map(binding => binding.key);
}
function keyValues(templateBindings: any[]) {
return templateBindings.map(binding => {
if (binding.keyIsVar) {
return 'let ' + binding.key + (isBlank(binding.name) ? '=null' : '=' + binding.name);
} else {
return binding.key + (isBlank(binding.expression) ? '' : `=${binding.expression}`)
}
});
}
function exprSources(templateBindings: any[]) {
return templateBindings.map(
binding => isPresent(binding.expression) ? binding.expression.source : null);
}
it('should parse an empty string', () => { expect(parseTemplateBindings('')).toEqual([]); });
it('should parse a string without a value',
() => { expect(keys(parseTemplateBindings('a'))).toEqual(['a']); });
it('should only allow identifier, string, or keyword including dashes as keys', () => {
var bindings = parseTemplateBindings("a:'b'");
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings("'a':'b'");
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings("\"a\":'b'");
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings("a-b:'c'");
expect(keys(bindings)).toEqual(['a-b']);
expect(() => { parseTemplateBindings('(:0'); })
.toThrowError(new RegExp('expected identifier, keyword, or string'));
expect(() => { parseTemplateBindings('1234:0'); })
.toThrowError(new RegExp('expected identifier, keyword, or string'));
});
it('should detect expressions as value', () => {
var bindings = parseTemplateBindings("a:b");
expect(exprSources(bindings)).toEqual(['b']);
bindings = parseTemplateBindings("a:1+1");
expect(exprSources(bindings)).toEqual(['1+1']);
});
it('should detect names as value', () => {
var bindings = parseTemplateBindings("a:let b");
expect(keyValues(bindings)).toEqual(['a', 'let b=\$implicit']);
});
it('should allow space and colon as separators', () => {
var bindings = parseTemplateBindings("a:b");
expect(keys(bindings)).toEqual(['a']);
expect(exprSources(bindings)).toEqual(['b']);
bindings = parseTemplateBindings("a b");
expect(keys(bindings)).toEqual(['a']);
expect(exprSources(bindings)).toEqual(['b']);
});
it('should allow multiple pairs', () => {
var bindings = parseTemplateBindings("a 1 b 2");
expect(keys(bindings)).toEqual(['a', 'aB']);
expect(exprSources(bindings)).toEqual(['1 ', '2']);
});
it('should store the sources in the result', () => {
var bindings = parseTemplateBindings("a 1,b 2");
expect(bindings[0].expression.source).toEqual('1');
expect(bindings[1].expression.source).toEqual('2');
});
it('should store the passed-in location', () => {
var bindings = parseTemplateBindings("a 1,b 2", 'location');
expect(bindings[0].expression.location).toEqual('location');
});
it('should support var notation with a deprecation warning', () => {
var bindings = createParser().parseTemplateBindings("var i", null);
expect(keyValues(bindings.templateBindings)).toEqual(['let i=\$implicit']);
expect(bindings.warnings)
.toEqual(['"var" inside of expressions is deprecated. Use "let" instead!']);
});
it('should support # notation with a deprecation warning', () => {
var bindings = createParser().parseTemplateBindings("#i", null);
expect(keyValues(bindings.templateBindings)).toEqual(['let i=\$implicit']);
expect(bindings.warnings)
.toEqual(['"#" inside of expressions is deprecated. Use "let" instead!']);
});
it('should support let notation', () => {
var bindings = parseTemplateBindings("let i");
expect(keyValues(bindings)).toEqual(['let i=\$implicit']);
bindings = parseTemplateBindings("let i");
expect(keyValues(bindings)).toEqual(['let i=\$implicit']);
bindings = parseTemplateBindings("let a; let b");
expect(keyValues(bindings)).toEqual(['let a=\$implicit', 'let b=\$implicit']);
bindings = parseTemplateBindings("let a; let b;");
expect(keyValues(bindings)).toEqual(['let a=\$implicit', 'let b=\$implicit']);
bindings = parseTemplateBindings("let i-a = k-a");
expect(keyValues(bindings)).toEqual(['let i-a=k-a']);
bindings = parseTemplateBindings("keyword let item; let i = k");
expect(keyValues(bindings)).toEqual(['keyword', 'let item=\$implicit', 'let i=k']);
bindings = parseTemplateBindings("keyword: let item; let i = k");
expect(keyValues(bindings)).toEqual(['keyword', 'let item=\$implicit', 'let i=k']);
bindings = parseTemplateBindings("directive: let item in expr; let a = b", 'location');
expect(keyValues(bindings))
.toEqual(
['directive', 'let item=\$implicit', 'directiveIn=expr in location', 'let a=b']);
});
it('should parse pipes', () => {
var bindings = parseTemplateBindings('key value|pipe');
var ast = bindings[0].expression.ast;
expect(ast).toBeAnInstanceOf(BindingPipe);
});
});
describe('parseInterpolation', () => {
it('should return null if no interpolation',
() => { expect(parseInterpolation('nothing')).toBe(null); });
it('should parse no prefix/suffix interpolation', () => {
var ast = parseInterpolation('{{a}}').ast;
expect(ast.strings).toEqual(['', '']);
expect(ast.expressions.length).toEqual(1);
expect(ast.expressions[0].name).toEqual('a');
});
it('should parse prefix/suffix with multiple interpolation', () => {
var originalExp = 'before {{ a }} middle {{ b }} after';
var ast = parseInterpolation(originalExp).ast;
expect(new Unparser().unparse(ast)).toEqual(originalExp);
});
it("should throw on empty interpolation expressions", () => {
expect(() => parseInterpolation("{{}}"))
.toThrowErrorWith(
"Parser Error: Blank expressions are not allowed in interpolated strings");
expect(() => parseInterpolation("foo {{ }}"))
.toThrowErrorWith(
"Parser Error: Blank expressions are not allowed in interpolated strings");
});
it('should parse conditional expression',
() => { checkInterpolation('{{ a < b ? a : b }}'); });
it('should parse expression with newline characters', () => {
checkInterpolation(`{{ 'foo' +\n 'bar' +\r 'baz' }}`, `{{ "foo" + "bar" + "baz" }}`);
});
describe("comments", () => {
it('should ignore comments in interpolation expressions',
() => { checkInterpolation('{{a //comment}}', '{{ a }}'); });
it('should retain // in single quote strings', () => {
checkInterpolation(`{{ 'http://www.google.com' }}`, `{{ "http://www.google.com" }}`);
});
it('should retain // in double quote strings', () => {
checkInterpolation(`{{ "http://www.google.com" }}`, `{{ "http://www.google.com" }}`);
});
it('should ignore comments after string literals',
() => { checkInterpolation(`{{ "a//b" //comment }}`, `{{ "a//b" }}`); });
it('should retain // in complex strings', () => {
checkInterpolation(`{{"//a\'//b\`//c\`//d\'//e" //comment}}`, `{{ "//a\'//b\`//c\`//d\'//e" }}`);
});
it('should retain // in nested, unterminated strings', () => {
checkInterpolation(`{{ "a\'b\`" //comment}}`, `{{ "a\'b\`" }}`);
});
});
});
describe("parseSimpleBinding", () => {
it("should parse a field access", () => {
var p = parseSimpleBinding("name");
expect(unparse(p)).toEqual("name");
});
it("should parse a constant", () => {
var p = parseSimpleBinding("[1, 2]");
expect(unparse(p)).toEqual("[1, 2]");
});
it("should throw when the given expression is not just a field name", () => {
expect(() => parseSimpleBinding("name + 1"))
.toThrowErrorWith(
'Host binding expression can only contain field access and constants');
});
it('should throw when encountering interpolation', () => {
expect(() => parseSimpleBinding('{{exp}}'))
.toThrowErrorWith('Got interpolation ({{}}) where expression was expected');
});
});
describe('wrapLiteralPrimitive', () => {
it('should wrap a literal primitive', () => {
expect(unparse(createParser().wrapLiteralPrimitive("foo", null))).toEqual('"foo"');
});
});
});
}

View File

@ -0,0 +1,196 @@
import {
AST,
AstVisitor,
PropertyRead,
PropertyWrite,
Binary,
Chain,
Conditional,
EmptyExpr,
BindingPipe,
FunctionCall,
ImplicitReceiver,
Interpolation,
KeyedRead,
KeyedWrite,
LiteralArray,
LiteralMap,
LiteralPrimitive,
MethodCall,
PrefixNot,
Quote,
SafePropertyRead,
SafeMethodCall
} from 'angular2/src/compiler/expression_parser/ast';
import {StringWrapper, isPresent, isString} from 'angular2/src/facade/lang';
export class Unparser implements AstVisitor {
private static _quoteRegExp = /"/g;
private _expression: string;
unparse(ast: AST) {
this._expression = '';
this._visit(ast);
return this._expression;
}
visitPropertyRead(ast: PropertyRead, context: any) {
this._visit(ast.receiver);
this._expression += ast.receiver instanceof ImplicitReceiver ? `${ast.name}` : `.${ast.name}`;
}
visitPropertyWrite(ast: PropertyWrite, context: any) {
this._visit(ast.receiver);
this._expression +=
ast.receiver instanceof ImplicitReceiver ? `${ast.name} = ` : `.${ast.name} = `;
this._visit(ast.value);
}
visitBinary(ast: Binary, context: any) {
this._visit(ast.left);
this._expression += ` ${ast.operation} `;
this._visit(ast.right);
}
visitChain(ast: Chain, context: any) {
var len = ast.expressions.length;
for (let i = 0; i < len; i++) {
this._visit(ast.expressions[i]);
this._expression += i == len - 1 ? ';' : '; ';
}
}
visitConditional(ast: Conditional, context: any) {
this._visit(ast.condition);
this._expression += ' ? ';
this._visit(ast.trueExp);
this._expression += ' : ';
this._visit(ast.falseExp);
}
visitPipe(ast: BindingPipe, context: any) {
this._expression += '(';
this._visit(ast.exp);
this._expression += ` | ${ast.name}`;
ast.args.forEach(arg => {
this._expression += ':';
this._visit(arg);
});
this._expression += ')';
}
visitFunctionCall(ast: FunctionCall, context: any) {
this._visit(ast.target);
this._expression += '(';
var isFirst = true;
ast.args.forEach(arg => {
if (!isFirst) this._expression += ', ';
isFirst = false;
this._visit(arg);
});
this._expression += ')';
}
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
visitInterpolation(ast: Interpolation, context: any) {
for (let i = 0; i < ast.strings.length; i++) {
this._expression += ast.strings[i];
if (i < ast.expressions.length) {
this._expression += '{{ ';
this._visit(ast.expressions[i]);
this._expression += ' }}';
}
}
}
visitKeyedRead(ast: KeyedRead, context: any) {
this._visit(ast.obj);
this._expression += '[';
this._visit(ast.key);
this._expression += ']';
}
visitKeyedWrite(ast: KeyedWrite, context: any) {
this._visit(ast.obj);
this._expression += '[';
this._visit(ast.key);
this._expression += '] = ';
this._visit(ast.value);
}
visitLiteralArray(ast: LiteralArray, context: any) {
this._expression += '[';
var isFirst = true;
ast.expressions.forEach(expression => {
if (!isFirst) this._expression += ', ';
isFirst = false;
this._visit(expression);
});
this._expression += ']';
}
visitLiteralMap(ast: LiteralMap, context: any) {
this._expression += '{';
var isFirst = true;
for (let i = 0; i < ast.keys.length; i++) {
if (!isFirst) this._expression += ', ';
isFirst = false;
this._expression += `${ast.keys[i]}: `;
this._visit(ast.values[i]);
}
this._expression += '}';
}
visitLiteralPrimitive(ast: LiteralPrimitive, context: any) {
if (isString(ast.value)) {
this._expression += `"${StringWrapper.replaceAll(ast.value, Unparser._quoteRegExp, '\"')}"`;
} else {
this._expression += `${ast.value}`;
}
}
visitMethodCall(ast: MethodCall, context: any) {
this._visit(ast.receiver);
this._expression += ast.receiver instanceof ImplicitReceiver ? `${ast.name}(` : `.${ast.name}(`;
var isFirst = true;
ast.args.forEach(arg => {
if (!isFirst) this._expression += ', ';
isFirst = false;
this._visit(arg);
});
this._expression += ')';
}
visitPrefixNot(ast: PrefixNot, context: any) {
this._expression += '!';
this._visit(ast.expression);
}
visitSafePropertyRead(ast: SafePropertyRead, context: any) {
this._visit(ast.receiver);
this._expression += `?.${ast.name}`;
}
visitSafeMethodCall(ast: SafeMethodCall, context: any) {
this._visit(ast.receiver);
this._expression += `?.${ast.name}(`;
var isFirst = true;
ast.args.forEach(arg => {
if (!isFirst) this._expression += ', ';
isFirst = false;
this._visit(arg);
});
this._expression += ')';
}
visitQuote(ast: Quote, context: any) {
this._expression += `${ast.prefix}:${ast.uninterpretedExpression}`;
}
private _visit(ast: AST) { ast.visit(this); }
}

View File

@ -0,0 +1,93 @@
import {HtmlParser, HtmlParseTreeResult, HtmlTreeError} from 'angular2/src/compiler/html_parser';
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
HtmlExpansionAst,
HtmlExpansionCaseAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util';
import {BaseException} from 'angular2/src/facade/exceptions';
export function humanizeDom(parseResult: HtmlParseTreeResult): any[] {
if (parseResult.errors.length > 0) {
var errorString = parseResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
}
var humanizer = new _Humanizer(false);
htmlVisitAll(humanizer, parseResult.rootNodes);
return humanizer.result;
}
export function humanizeDomSourceSpans(parseResult: HtmlParseTreeResult): any[] {
if (parseResult.errors.length > 0) {
var errorString = parseResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
}
var humanizer = new _Humanizer(true);
htmlVisitAll(humanizer, parseResult.rootNodes);
return humanizer.result;
}
export function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
class _Humanizer implements HtmlAstVisitor {
result: any[] = [];
elDepth: number = 0;
constructor(private includeSourceSpan: boolean){};
visitElement(ast: HtmlElementAst, context: any): any {
var res = this._appendContext(ast, [HtmlElementAst, ast.name, this.elDepth++]);
this.result.push(res);
htmlVisitAll(this, ast.attrs);
htmlVisitAll(this, ast.children);
this.elDepth--;
return null;
}
visitAttr(ast: HtmlAttrAst, context: any): any {
var res = this._appendContext(ast, [HtmlAttrAst, ast.name, ast.value]);
this.result.push(res);
return null;
}
visitText(ast: HtmlTextAst, context: any): any {
var res = this._appendContext(ast, [HtmlTextAst, ast.value, this.elDepth]);
this.result.push(res);
return null;
}
visitComment(ast: HtmlCommentAst, context: any): any {
var res = this._appendContext(ast, [HtmlCommentAst, ast.value, this.elDepth]);
this.result.push(res);
return null;
}
visitExpansion(ast: HtmlExpansionAst, context: any): any {
var res = this._appendContext(ast, [HtmlExpansionAst, ast.switchValue, ast.type]);
this.result.push(res);
htmlVisitAll(this, ast.cases);
return null;
}
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
var res = this._appendContext(ast, [HtmlExpansionCaseAst, ast.value]);
this.result.push(res);
return null;
}
private _appendContext(ast: HtmlAst, input: any[]): any[] {
if (!this.includeSourceSpan) return input;
input.push(ast.sourceSpan.toString());
return input;
}
}

View File

@ -0,0 +1,739 @@
import {
ddescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach
} from 'angular2/testing_internal';
import {BaseException} from 'angular2/src/facade/exceptions';
import {
tokenizeHtml,
HtmlToken,
HtmlTokenType,
HtmlTokenError
} from 'angular2/src/compiler/html_lexer';
import {ParseSourceSpan, ParseLocation, ParseSourceFile} from 'angular2/src/compiler/parse_util';
export function main() {
describe('HtmlLexer', () => {
describe('line/column numbers', () => {
it('should work without newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t>a</t>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '0:2'],
[HtmlTokenType.TEXT, '0:3'],
[HtmlTokenType.TAG_CLOSE, '0:4'],
[HtmlTokenType.EOF, '0:8']
]);
});
it('should work with one newline', () => {
expect(tokenizeAndHumanizeLineColumn('<t>\na</t>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '0:2'],
[HtmlTokenType.TEXT, '0:3'],
[HtmlTokenType.TAG_CLOSE, '1:1'],
[HtmlTokenType.EOF, '1:5']
]);
});
it('should work with multiple newlines', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\na</t>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '1:0'],
[HtmlTokenType.TEXT, '1:1'],
[HtmlTokenType.TAG_CLOSE, '2:1'],
[HtmlTokenType.EOF, '2:5']
]);
});
it('should work with CR and LF', () => {
expect(tokenizeAndHumanizeLineColumn('<t\n>\r\na\r</t>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, '0:0'],
[HtmlTokenType.TAG_OPEN_END, '1:0'],
[HtmlTokenType.TEXT, '1:1'],
[HtmlTokenType.TAG_CLOSE, '2:1'],
[HtmlTokenType.EOF, '2:5']
]);
});
});
describe('comments', () => {
it('should parse comments', () => {
expect(tokenizeAndHumanizeParts('<!--t\ne\rs\r\nt-->'))
.toEqual([
[HtmlTokenType.COMMENT_START],
[HtmlTokenType.RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.COMMENT_END],
[HtmlTokenType.EOF]
]);
});
it('should store the locations',
() => {expect(tokenizeAndHumanizeSourceSpans('<!--t\ne\rs\r\nt-->'))
.toEqual([
[HtmlTokenType.COMMENT_START, '<!--'],
[HtmlTokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[HtmlTokenType.COMMENT_END, '-->'],
[HtmlTokenType.EOF, '']
])});
it('should report <!- without -', () => {
expect(tokenizeAndHumanizeErrors('<!-a'))
.toEqual([[HtmlTokenType.COMMENT_START, 'Unexpected character "a"', '0:3']]);
});
it('should report missing end comment', () => {
expect(tokenizeAndHumanizeErrors('<!--'))
.toEqual([[HtmlTokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:4']]);
});
});
describe('doctype', () => {
it('should parse doctypes', () => {
expect(tokenizeAndHumanizeParts('<!doctype html>'))
.toEqual([[HtmlTokenType.DOC_TYPE, 'doctype html'], [HtmlTokenType.EOF]]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<!doctype html>'))
.toEqual([[HtmlTokenType.DOC_TYPE, '<!doctype html>'], [HtmlTokenType.EOF, '']]);
});
it('should report missing end doctype', () => {
expect(tokenizeAndHumanizeErrors('<!'))
.toEqual([[HtmlTokenType.DOC_TYPE, 'Unexpected character "EOF"', '0:2']]);
});
});
describe('CDATA', () => {
it('should parse CDATA', () => {
expect(tokenizeAndHumanizeParts('<![CDATA[t\ne\rs\r\nt]]>'))
.toEqual([
[HtmlTokenType.CDATA_START],
[HtmlTokenType.RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.CDATA_END],
[HtmlTokenType.EOF]
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<![CDATA[t\ne\rs\r\nt]]>'))
.toEqual([
[HtmlTokenType.CDATA_START, '<![CDATA['],
[HtmlTokenType.RAW_TEXT, 't\ne\rs\r\nt'],
[HtmlTokenType.CDATA_END, ']]>'],
[HtmlTokenType.EOF, '']
]);
});
it('should report <![ without CDATA[', () => {
expect(tokenizeAndHumanizeErrors('<![a'))
.toEqual([[HtmlTokenType.CDATA_START, 'Unexpected character "a"', '0:3']]);
});
it('should report missing end cdata', () => {
expect(tokenizeAndHumanizeErrors('<![CDATA['))
.toEqual([[HtmlTokenType.RAW_TEXT, 'Unexpected character "EOF"', '0:9']]);
});
});
describe('open tags', () => {
it('should parse open tags without prefix', () => {
expect(tokenizeAndHumanizeParts('<test>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'test'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should parse namespace prefix', () => {
expect(tokenizeAndHumanizeParts('<ns1:test>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, 'ns1', 'test'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should parse void tags', () => {
expect(tokenizeAndHumanizeParts('<test/>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'test'],
[HtmlTokenType.TAG_OPEN_END_VOID],
[HtmlTokenType.EOF]
]);
});
it('should allow whitespace after the tag name', () => {
expect(tokenizeAndHumanizeParts('<test >'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'test'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<test>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, '<test'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.EOF, '']
]);
});
});
describe('attributes', () => {
it('should parse attributes without prefix', () => {
expect(tokenizeAndHumanizeParts('<t a>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should parse attributes with prefix', () => {
expect(tokenizeAndHumanizeParts('<t ns1:a>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, 'ns1', 'a'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should parse attributes whose prefix is not valid', () => {
expect(tokenizeAndHumanizeParts('<t (ns1:a)>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, '(ns1:a)'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should parse attributes with single quote value', () => {
expect(tokenizeAndHumanizeParts("<t a='b'>"))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should parse attributes with double quote value', () => {
expect(tokenizeAndHumanizeParts('<t a="b">'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should parse attributes with unquoted value', () => {
expect(tokenizeAndHumanizeParts('<t a=b>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('<t a = b >'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should parse attributes with entities in values', () => {
expect(tokenizeAndHumanizeParts('<t a="&#65;&#x41;">'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'AA'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should not decode entities without trailing ";"', () => {
expect(tokenizeAndHumanizeParts('<t a="&amp" b="c&&d">'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, '&amp'],
[HtmlTokenType.ATTR_NAME, null, 'b'],
[HtmlTokenType.ATTR_VALUE, 'c&&d'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should parse attributes with "&" in values', () => {
expect(tokenizeAndHumanizeParts('<t a="b && c &">'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b && c &'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should parse values with CR and LF', () => {
expect(tokenizeAndHumanizeParts("<t a='t\ne\rs\r\nt'>"))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 't'],
[HtmlTokenType.ATTR_NAME, null, 'a'],
[HtmlTokenType.ATTR_VALUE, 't\ne\ns\nt'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.EOF]
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('<t a=b>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, '<t'],
[HtmlTokenType.ATTR_NAME, 'a'],
[HtmlTokenType.ATTR_VALUE, 'b'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.EOF, '']
]);
});
});
describe('closing tags', () => {
it('should parse closing tags without prefix', () => {
expect(tokenizeAndHumanizeParts('</test>'))
.toEqual([[HtmlTokenType.TAG_CLOSE, null, 'test'], [HtmlTokenType.EOF]]);
});
it('should parse closing tags with prefix', () => {
expect(tokenizeAndHumanizeParts('</ns1:test>'))
.toEqual([[HtmlTokenType.TAG_CLOSE, 'ns1', 'test'], [HtmlTokenType.EOF]]);
});
it('should allow whitespace', () => {
expect(tokenizeAndHumanizeParts('</ test >'))
.toEqual([[HtmlTokenType.TAG_CLOSE, null, 'test'], [HtmlTokenType.EOF]]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('</test>'))
.toEqual([[HtmlTokenType.TAG_CLOSE, '</test>'], [HtmlTokenType.EOF, '']]);
});
it('should report missing name after </', () => {
expect(tokenizeAndHumanizeErrors('</'))
.toEqual([[HtmlTokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:2']]);
});
it('should report missing >', () => {
expect(tokenizeAndHumanizeErrors('</test'))
.toEqual([[HtmlTokenType.TAG_CLOSE, 'Unexpected character "EOF"', '0:6']]);
});
});
describe('entities', () => {
it('should parse named entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b'))
.toEqual([[HtmlTokenType.TEXT, 'a&b'], [HtmlTokenType.EOF]]);
});
it('should parse hexadecimal entities', () => {
expect(tokenizeAndHumanizeParts('&#x41;&#X41;'))
.toEqual([[HtmlTokenType.TEXT, 'AA'], [HtmlTokenType.EOF]]);
});
it('should parse decimal entities', () => {
expect(tokenizeAndHumanizeParts('&#65;'))
.toEqual([[HtmlTokenType.TEXT, 'A'], [HtmlTokenType.EOF]]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a&amp;b'))
.toEqual([[HtmlTokenType.TEXT, 'a&amp;b'], [HtmlTokenType.EOF, '']]);
});
it('should report malformed/unknown entities', () => {
expect(tokenizeAndHumanizeErrors('&tbo;'))
.toEqual([
[
HtmlTokenType.TEXT,
'Unknown entity "tbo" - use the "&#<decimal>;" or "&#x<hex>;" syntax',
'0:0'
]
]);
expect(tokenizeAndHumanizeErrors('&#asdf;'))
.toEqual([[HtmlTokenType.TEXT, 'Unexpected character "s"', '0:3']]);
expect(tokenizeAndHumanizeErrors('&#xasdf;'))
.toEqual([[HtmlTokenType.TEXT, 'Unexpected character "s"', '0:4']]);
expect(tokenizeAndHumanizeErrors('&#xABC'))
.toEqual([[HtmlTokenType.TEXT, 'Unexpected character "EOF"', '0:6']]);
});
});
describe('regular text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts('a'))
.toEqual([[HtmlTokenType.TEXT, 'a'], [HtmlTokenType.EOF]]);
});
it('should handle CR & LF', () => {
expect(tokenizeAndHumanizeParts('t\ne\rs\r\nt'))
.toEqual([[HtmlTokenType.TEXT, 't\ne\ns\nt'], [HtmlTokenType.EOF]]);
});
it('should parse entities', () => {
expect(tokenizeAndHumanizeParts('a&amp;b'))
.toEqual([[HtmlTokenType.TEXT, 'a&b'], [HtmlTokenType.EOF]]);
});
it('should parse text starting with "&"', () => {
expect(tokenizeAndHumanizeParts('a && b &'))
.toEqual([[HtmlTokenType.TEXT, 'a && b &'], [HtmlTokenType.EOF]]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans('a'))
.toEqual([[HtmlTokenType.TEXT, 'a'], [HtmlTokenType.EOF, '']]);
});
it('should allow "<" in text nodes', () => {
expect(tokenizeAndHumanizeParts('{{ a < b ? c : d }}'))
.toEqual([[HtmlTokenType.TEXT, '{{ a < b ? c : d }}'], [HtmlTokenType.EOF]]);
expect(tokenizeAndHumanizeSourceSpans('<p>a<b</p>'))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, '<p'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.TEXT, 'a<b'],
[HtmlTokenType.TAG_CLOSE, '</p>'],
[HtmlTokenType.EOF, ''],
]);
expect(tokenizeAndHumanizeParts('< a>'))
.toEqual([[HtmlTokenType.TEXT, '< a>'], [HtmlTokenType.EOF]]);
});
// TODO(vicb): make the lexer aware of Angular expressions
// see https://github.com/angular/angular/issues/5679
it('should parse valid start tag in interpolation', () => {
expect(tokenizeAndHumanizeParts('{{ a <b && c > d }}'))
.toEqual([
[HtmlTokenType.TEXT, '{{ a '],
[HtmlTokenType.TAG_OPEN_START, null, 'b'],
[HtmlTokenType.ATTR_NAME, null, '&&'],
[HtmlTokenType.ATTR_NAME, null, 'c'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.TEXT, ' d }}'],
[HtmlTokenType.EOF]
]);
});
});
describe('raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<script>t\ne\rs\r\nt</script>`))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF]
]);
});
it('should not detect entities', () => {
expect(tokenizeAndHumanizeParts(`<script>&amp;</SCRIPT>`))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, '&amp;'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF]
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a<div></script>`))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, 'a<div>'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF]
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<script>a</test></script>`))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'script'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.RAW_TEXT, 'a</test>'],
[HtmlTokenType.TAG_CLOSE, null, 'script'],
[HtmlTokenType.EOF]
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<script>a</script>`))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, '<script'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.RAW_TEXT, 'a'],
[HtmlTokenType.TAG_CLOSE, '</script>'],
[HtmlTokenType.EOF, '']
]);
});
});
describe('escapable raw text', () => {
it('should parse text', () => {
expect(tokenizeAndHumanizeParts(`<title>t\ne\rs\r\nt</title>`))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 't\ne\ns\nt'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF]
]);
});
it('should detect entities', () => {
expect(tokenizeAndHumanizeParts(`<title>&amp;</title>`))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, '&'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF]
]);
});
it('should ignore other opening tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a<div></title>`))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 'a<div>'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF]
]);
});
it('should ignore other closing tags', () => {
expect(tokenizeAndHumanizeParts(`<title>a</test></title>`))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, null, 'title'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 'a</test>'],
[HtmlTokenType.TAG_CLOSE, null, 'title'],
[HtmlTokenType.EOF]
]);
});
it('should store the locations', () => {
expect(tokenizeAndHumanizeSourceSpans(`<title>a</title>`))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, '<title'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.ESCAPABLE_RAW_TEXT, 'a'],
[HtmlTokenType.TAG_CLOSE, '</title>'],
[HtmlTokenType.EOF, '']
]);
});
});
describe("expansion forms", () => {
it("should parse an expansion form", () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four} =5 {five} }', true))
.toEqual([
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'one.two'],
[HtmlTokenType.RAW_TEXT, 'three'],
[HtmlTokenType.EXPANSION_CASE_VALUE, '4'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'four'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_CASE_VALUE, '5'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'five'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.EOF]
]);
});
it("should parse an expansion form with text elements surrounding it", () => {
expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true))
.toEqual([
[HtmlTokenType.TEXT, "before"],
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'one.two'],
[HtmlTokenType.RAW_TEXT, 'three'],
[HtmlTokenType.EXPANSION_CASE_VALUE, '4'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'four'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.TEXT, "after"],
[HtmlTokenType.EOF]
]);
});
it("should parse an expansion forms with elements in it", () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four <b>a</b>}}', true))
.toEqual([
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'one.two'],
[HtmlTokenType.RAW_TEXT, 'three'],
[HtmlTokenType.EXPANSION_CASE_VALUE, '4'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'four '],
[HtmlTokenType.TAG_OPEN_START, null, 'b'],
[HtmlTokenType.TAG_OPEN_END],
[HtmlTokenType.TEXT, 'a'],
[HtmlTokenType.TAG_CLOSE, null, 'b'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.EOF]
]);
});
it("should parse an expansion forms with interpolation in it", () => {
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', true))
.toEqual([
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'one.two'],
[HtmlTokenType.RAW_TEXT, 'three'],
[HtmlTokenType.EXPANSION_CASE_VALUE, '4'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'four {{a}}'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.EOF]
]);
});
it("should parse nested expansion forms", () => {
expect(tokenizeAndHumanizeParts(`{one.two, three, =4 { {xx, yy, =x {one}} }}`, true))
.toEqual([
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'one.two'],
[HtmlTokenType.RAW_TEXT, 'three'],
[HtmlTokenType.EXPANSION_CASE_VALUE, '4'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.EXPANSION_FORM_START],
[HtmlTokenType.RAW_TEXT, 'xx'],
[HtmlTokenType.RAW_TEXT, 'yy'],
[HtmlTokenType.EXPANSION_CASE_VALUE, 'x'],
[HtmlTokenType.EXPANSION_CASE_EXP_START],
[HtmlTokenType.TEXT, 'one'],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.TEXT, ' '],
[HtmlTokenType.EXPANSION_CASE_EXP_END],
[HtmlTokenType.EXPANSION_FORM_END],
[HtmlTokenType.EOF]
]);
});
});
describe('errors', () => {
it('should include 2 lines of context in message', () => {
let src = "111\n222\n333\nE\n444\n555\n666\n";
let file = new ParseSourceFile(src, 'file://');
let location = new ParseLocation(file, 12, 123, 456);
let span = new ParseSourceSpan(location, location);
let error = new HtmlTokenError('**ERROR**', null, span);
expect(error.toString())
.toEqual(`**ERROR** ("\n222\n333\n[ERROR ->]E\n444\n555\n"): file://@123:456`);
});
});
describe('unicode characters', () => {
it('should support unicode characters', () => {
expect(tokenizeAndHumanizeSourceSpans(`<p>İ</p>`))
.toEqual([
[HtmlTokenType.TAG_OPEN_START, '<p'],
[HtmlTokenType.TAG_OPEN_END, '>'],
[HtmlTokenType.TEXT, 'İ'],
[HtmlTokenType.TAG_CLOSE, '</p>'],
[HtmlTokenType.EOF, '']
]);
});
});
});
}
function tokenizeWithoutErrors(input: string,
tokenizeExpansionForms: boolean = false): HtmlToken[] {
var tokenizeResult = tokenizeHtml(input, 'someUrl', tokenizeExpansionForms);
if (tokenizeResult.errors.length > 0) {
var errorString = tokenizeResult.errors.join('\n');
throw new BaseException(`Unexpected parse errors:\n${errorString}`);
}
return tokenizeResult.tokens;
}
function tokenizeAndHumanizeParts(input: string, tokenizeExpansionForms: boolean = false): any[] {
return tokenizeWithoutErrors(input, tokenizeExpansionForms)
.map(token => [<any>token.type].concat(token.parts));
}
function tokenizeAndHumanizeSourceSpans(input: string): any[] {
return tokenizeWithoutErrors(input).map(token => [<any>token.type, token.sourceSpan.toString()]);
}
function humanizeLineColumn(location: ParseLocation): string {
return `${location.line}:${location.col}`;
}
function tokenizeAndHumanizeLineColumn(input: string): any[] {
return tokenizeWithoutErrors(input)
.map(token => [<any>token.type, humanizeLineColumn(token.sourceSpan.start)]);
}
function tokenizeAndHumanizeErrors(input: string): any[] {
return tokenizeHtml(input, 'someUrl')
.errors.map(tokenError => [
<any>tokenError.tokenType,
tokenError.msg,
humanizeLineColumn(tokenError.span.start)
]);
}

View File

@ -0,0 +1,392 @@
import {
ddescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach
} from 'angular2/testing_internal';
import {HtmlTokenType} from 'angular2/src/compiler/html_lexer';
import {HtmlParser, HtmlParseTreeResult, HtmlTreeError} from 'angular2/src/compiler/html_parser';
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
htmlVisitAll,
HtmlExpansionAst,
HtmlExpansionCaseAst
} from 'angular2/src/compiler/html_ast';
import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util';
import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './html_ast_spec_utils';
export function main() {
describe('HtmlParser', () => {
var parser: HtmlParser;
beforeEach(() => { parser = new HtmlParser(); });
describe('parse', () => {
describe('text nodes', () => {
it('should parse root level text nodes', () => {
expect(humanizeDom(parser.parse('a', 'TestComp'))).toEqual([[HtmlTextAst, 'a', 0]]);
});
it('should parse text nodes inside regular elements', () => {
expect(humanizeDom(parser.parse('<div>a</div>', 'TestComp')))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlTextAst, 'a', 1]]);
});
it('should parse text nodes inside template elements', () => {
expect(humanizeDom(parser.parse('<template>a</template>', 'TestComp')))
.toEqual([[HtmlElementAst, 'template', 0], [HtmlTextAst, 'a', 1]]);
});
it('should parse CDATA', () => {
expect(humanizeDom(parser.parse('<![CDATA[text]]>', 'TestComp')))
.toEqual([[HtmlTextAst, 'text', 0]]);
});
});
describe('elements', () => {
it('should parse root level elements', () => {
expect(humanizeDom(parser.parse('<div></div>', 'TestComp')))
.toEqual([[HtmlElementAst, 'div', 0]]);
});
it('should parse elements inside of regular elements', () => {
expect(humanizeDom(parser.parse('<div><span></span></div>', 'TestComp')))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlElementAst, 'span', 1]]);
});
it('should parse elements inside of template elements', () => {
expect(humanizeDom(parser.parse('<template><span></span></template>', 'TestComp')))
.toEqual([[HtmlElementAst, 'template', 0], [HtmlElementAst, 'span', 1]]);
});
it('should support void elements', () => {
expect(humanizeDom(parser.parse('<link rel="author license" href="/about">', 'TestComp')))
.toEqual([
[HtmlElementAst, 'link', 0],
[HtmlAttrAst, 'rel', 'author license'],
[HtmlAttrAst, 'href', '/about'],
]);
});
it('should not error on void elements from HTML5 spec',
() => { // http://www.w3.org/TR/html-markup/syntax.html#syntax-elements without:
// <base> - it can be present in head only
// <meta> - it can be present in head only
// <command> - obsolete
// <keygen> - obsolete
['<map><area></map>', '<div><br></div>', '<colgroup><col></colgroup>',
'<div><embed></div>', '<div><hr></div>', '<div><img></div>', '<div><input></div>',
'<object><param>/<object>', '<audio><source></audio>', '<audio><track></audio>',
'<p><wbr></p>',
].forEach((html) => { expect(parser.parse(html, 'TestComp').errors).toEqual([]); });
});
it('should close void elements on text nodes', () => {
expect(humanizeDom(parser.parse('<p>before<br>after</p>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'p', 0],
[HtmlTextAst, 'before', 1],
[HtmlElementAst, 'br', 1],
[HtmlTextAst, 'after', 1],
]);
});
it('should support optional end tags', () => {
expect(humanizeDom(parser.parse('<div><p>1<p>2</div>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'p', 1],
[HtmlTextAst, '1', 2],
[HtmlElementAst, 'p', 1],
[HtmlTextAst, '2', 2],
]);
});
it('should support nested elements', () => {
expect(humanizeDom(parser.parse('<ul><li><ul><li></li></ul></li></ul>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'ul', 0],
[HtmlElementAst, 'li', 1],
[HtmlElementAst, 'ul', 2],
[HtmlElementAst, 'li', 3],
]);
});
it('should add the requiredParent', () => {
expect(
humanizeDom(parser.parse(
'<table><thead><tr head></tr></thead><tr noparent></tr><tbody><tr body></tr></tbody><tfoot><tr foot></tr></tfoot></table>',
'TestComp')))
.toEqual([
[HtmlElementAst, 'table', 0],
[HtmlElementAst, 'thead', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'head', ''],
[HtmlElementAst, 'tbody', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'noparent', ''],
[HtmlElementAst, 'tbody', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'body', ''],
[HtmlElementAst, 'tfoot', 1],
[HtmlElementAst, 'tr', 2],
[HtmlAttrAst, 'foot', '']
]);
});
it('should not add the requiredParent when the parent is a template', () => {
expect(humanizeDom(parser.parse('<template><tr></tr></template>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'template', 0],
[HtmlElementAst, 'tr', 1],
]);
});
it('should support explicit mamespace', () => {
expect(humanizeDom(parser.parse('<myns:div></myns:div>', 'TestComp')))
.toEqual([[HtmlElementAst, '@myns:div', 0]]);
});
it('should support implicit mamespace', () => {
expect(humanizeDom(parser.parse('<svg></svg>', 'TestComp')))
.toEqual([[HtmlElementAst, '@svg:svg', 0]]);
});
it('should propagate the namespace', () => {
expect(humanizeDom(parser.parse('<myns:div><p></p></myns:div>', 'TestComp')))
.toEqual([[HtmlElementAst, '@myns:div', 0], [HtmlElementAst, '@myns:p', 1]]);
});
it('should match closing tags case sensitive', () => {
let errors = parser.parse('<DiV><P></p></dIv>', 'TestComp').errors;
expect(errors.length).toEqual(2);
expect(humanizeErrors(errors))
.toEqual([
['p', 'Unexpected closing tag "p"', '0:8'],
['dIv', 'Unexpected closing tag "dIv"', '0:12'],
]);
});
it('should support self closing void elements', () => {
expect(humanizeDom(parser.parse('<input />', 'TestComp')))
.toEqual([[HtmlElementAst, 'input', 0]]);
});
it('should support self closing foreign elements', () => {
expect(humanizeDom(parser.parse('<math />', 'TestComp')))
.toEqual([[HtmlElementAst, '@math:math', 0]]);
});
it('should ignore LF immediately after textarea, pre and listing', () => {
expect(humanizeDom(parser.parse(
'<p>\n</p><textarea>\n</textarea><pre>\n\n</pre><listing>\n\n</listing>',
'TestComp')))
.toEqual([
[HtmlElementAst, 'p', 0],
[HtmlTextAst, '\n', 1],
[HtmlElementAst, 'textarea', 0],
[HtmlElementAst, 'pre', 0],
[HtmlTextAst, '\n', 1],
[HtmlElementAst, 'listing', 0],
[HtmlTextAst, '\n', 1],
]);
});
});
describe('attributes', () => {
it('should parse attributes on regular elements case sensitive', () => {
expect(humanizeDom(parser.parse('<div kEy="v" key2=v2></div>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'kEy', 'v'],
[HtmlAttrAst, 'key2', 'v2'],
]);
});
it('should parse attributes without values', () => {
expect(humanizeDom(parser.parse('<div k></div>', 'TestComp')))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'k', '']]);
});
it('should parse attributes on svg elements case sensitive', () => {
expect(humanizeDom(parser.parse('<svg viewBox="0"></svg>', 'TestComp')))
.toEqual([[HtmlElementAst, '@svg:svg', 0], [HtmlAttrAst, 'viewBox', '0']]);
});
it('should parse attributes on template elements', () => {
expect(humanizeDom(parser.parse('<template k="v"></template>', 'TestComp')))
.toEqual([[HtmlElementAst, 'template', 0], [HtmlAttrAst, 'k', 'v']]);
});
it('should support namespace', () => {
expect(humanizeDom(parser.parse('<svg:use xlink:href="Port" />', 'TestComp')))
.toEqual([[HtmlElementAst, '@svg:use', 0], [HtmlAttrAst, '@xlink:href', 'Port']]);
});
});
describe('comments', () => {
it('should preserve comments', () => {
expect(humanizeDom(parser.parse('<!-- comment --><div></div>', 'TestComp')))
.toEqual([[HtmlCommentAst, 'comment', 0], [HtmlElementAst, 'div', 0]]);
});
});
describe("expansion forms", () => {
it("should parse out expansion forms", () => {
let parsed = parser.parse(`<div>before{messages.length, plural, =0 {You have <b>no</b> messages} =1 {One {{message}}}}after</div>`,
'TestComp', true);
expect(humanizeDom(parsed))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlTextAst, 'before', 1],
[HtmlExpansionAst, 'messages.length', 'plural'],
[HtmlExpansionCaseAst, '0'],
[HtmlExpansionCaseAst, '1'],
[HtmlTextAst, 'after', 1]
]);
let cases = (<any>parsed.rootNodes[0]).children[1].cases;
expect(humanizeDom(new HtmlParseTreeResult(cases[0].expression, [])))
.toEqual([
[HtmlTextAst, 'You have ', 0],
[HtmlElementAst, 'b', 0],
[HtmlTextAst, 'no', 1],
[HtmlTextAst, ' messages', 0],
]);
expect(humanizeDom(new HtmlParseTreeResult(cases[1].expression, [])))
.toEqual([[HtmlTextAst, 'One {{message}}', 0]]);
});
it("should parse out nested expansion forms", () => {
let parsed = parser.parse(`{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`,
'TestComp', true);
expect(humanizeDom(parsed))
.toEqual([
[HtmlExpansionAst, 'messages.length', 'plural'],
[HtmlExpansionCaseAst, '0'],
]);
let firstCase = (<any>parsed.rootNodes[0]).cases[0];
expect(humanizeDom(new HtmlParseTreeResult(firstCase.expression, [])))
.toEqual([
[HtmlExpansionAst, 'p.gender', 'gender'],
[HtmlExpansionCaseAst, 'm'],
[HtmlTextAst, ' ', 0],
]);
});
it("should error when expansion form is not closed", () => {
let p = parser.parse(`{messages.length, plural, =0 {one}`, 'TestComp', true);
expect(humanizeErrors(p.errors))
.toEqual([[null, "Invalid expansion form. Missing '}'.", '0:34']]);
});
it("should error when expansion case is not closed", () => {
let p = parser.parse(`{messages.length, plural, =0 {one`, 'TestComp', true);
expect(humanizeErrors(p.errors))
.toEqual([[null, "Invalid expansion form. Missing '}'.", '0:29']]);
});
it("should error when invalid html in the case", () => {
let p = parser.parse(`{messages.length, plural, =0 {<b/>}`, 'TestComp', true);
expect(humanizeErrors(p.errors))
.toEqual([['b', 'Only void and foreign elements can be self closed "b"', '0:30']]);
});
});
describe('source spans', () => {
it('should store the location', () => {
expect(humanizeDomSourceSpans(parser.parse(
'<div [prop]="v1" (e)="do()" attr="v2" noValue>\na\n</div>', 'TestComp')))
.toEqual([
[HtmlElementAst, 'div', 0, '<div [prop]="v1" (e)="do()" attr="v2" noValue>'],
[HtmlAttrAst, '[prop]', 'v1', '[prop]="v1"'],
[HtmlAttrAst, '(e)', 'do()', '(e)="do()"'],
[HtmlAttrAst, 'attr', 'v2', 'attr="v2"'],
[HtmlAttrAst, 'noValue', '', 'noValue'],
[HtmlTextAst, '\na\n', 1, '\na\n'],
]);
});
it('should set the start and end source spans', () => {
let node = <HtmlElementAst>parser.parse('<div>a</div>', 'TestComp').rootNodes[0];
expect(node.startSourceSpan.start.offset).toEqual(0);
expect(node.startSourceSpan.end.offset).toEqual(5);
expect(node.endSourceSpan.start.offset).toEqual(6);
expect(node.endSourceSpan.end.offset).toEqual(12);
});
});
describe('errors', () => {
it('should report unexpected closing tags', () => {
let errors = parser.parse('<div></p></div>', 'TestComp').errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([['p', 'Unexpected closing tag "p"', '0:5']]);
});
it('should report closing tag for void elements', () => {
let errors = parser.parse('<input></input>', 'TestComp').errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors))
.toEqual([['input', 'Void elements do not have end tags "input"', '0:7']]);
});
it('should report self closing html element', () => {
let errors = parser.parse('<p />', 'TestComp').errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors))
.toEqual([['p', 'Only void and foreign elements can be self closed "p"', '0:0']]);
});
it('should report self closing custom element', () => {
let errors = parser.parse('<my-cmp />', 'TestComp').errors;
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors))
.toEqual([
['my-cmp', 'Only void and foreign elements can be self closed "my-cmp"', '0:0']
]);
});
it('should also report lexer errors', () => {
let errors = parser.parse('<!-err--><div></p></div>', 'TestComp').errors;
expect(errors.length).toEqual(2);
expect(humanizeErrors(errors))
.toEqual([
[HtmlTokenType.COMMENT_START, 'Unexpected character "e"', '0:3'],
['p', 'Unexpected closing tag "p"', '0:14']
]);
});
});
});
});
}
export function humanizeErrors(errors: ParseError[]): any[] {
return errors.map(error => {
if (error instanceof HtmlTreeError) {
// Parser errors
return [<any>error.elementName, error.msg, humanizeLineColumn(error.span.start)];
}
// Tokenizer errors
return [(<any>error).tokenType, error.msg, humanizeLineColumn(error.span.start)];
});
}

View File

@ -0,0 +1,341 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
expect,
iit,
inject,
it,
xdescribe,
xit
} from 'angular2/testing_internal';
import {I18nHtmlParser} from 'angular2/src/i18n/i18n_html_parser';
import {Message, id} from 'angular2/src/i18n/message';
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {Lexer} from 'angular2/src/compiler/expression_parser/lexer';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {HtmlParser, HtmlParseTreeResult} from 'angular2/src/compiler/html_parser';
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {serializeXmb, deserializeXmb} from 'angular2/src/i18n/xmb_serializer';
import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util';
import {humanizeDom} from '../../test/compiler/html_ast_spec_utils';
export function main() {
describe('I18nHtmlParser', () => {
function parse(template: string, messages: {[key: string]: string}): HtmlParseTreeResult {
var parser = new Parser(new Lexer());
let htmlParser = new HtmlParser();
let msgs = '';
StringMapWrapper.forEach(messages, (v, k) => msgs += `<msg id="${k}">${v}</msg>`);
let res = deserializeXmb(`<message-bundle>${msgs}</message-bundle>`, 'someUrl');
return new I18nHtmlParser(htmlParser, parser, res.content, res.messages)
.parse(template, "someurl", true);
}
it("should delegate to the provided parser when no i18n", () => {
expect(humanizeDom(parse('<div>a</div>', {})))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlTextAst, 'a', 1]]);
});
it("should replace attributes", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message("some message", "meaning", null))] = "another message";
expect(humanizeDom(parse("<div value='some message' i18n-value='meaning|comment'></div>",
translations)))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', 'another message']]);
});
it("should replace elements with the i18n attr", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message("message", "meaning", null))] = "another message";
expect(humanizeDom(parse("<div i18n='meaning|desc'>message</div>", translations)))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlTextAst, 'another message', 1]]);
});
it("should handle interpolation", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="0"/> and <ph name="1"/>', null, null))] =
'<ph name="1"/> or <ph name="0"/>';
expect(humanizeDom(parse("<div value='{{a}} and {{b}}' i18n-value></div>", translations)))
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', '{{b}} or {{a}}']]);
});
it('should handle interpolation with custom placeholder names', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="FIRST"/> and <ph name="SECOND"/>', null, null))] =
'<ph name="SECOND"/> or <ph name="FIRST"/>';
expect(
humanizeDom(parse(
`<div value='{{a //i18n(ph="FIRST")}} and {{b //i18n(ph="SECOND")}}' i18n-value></div>`,
translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'value', '{{b //i18n(ph="SECOND")}} or {{a //i18n(ph="FIRST")}}']
]);
});
it('should handle interpolation with duplicate placeholder names', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="FIRST"/> and <ph name="FIRST_1"/>', null, null))] =
'<ph name="FIRST_1"/> or <ph name="FIRST"/>';
expect(
humanizeDom(parse(
`<div value='{{a //i18n(ph="FIRST")}} and {{b //i18n(ph="FIRST")}}' i18n-value></div>`,
translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'value', '{{b //i18n(ph="FIRST")}} or {{a //i18n(ph="FIRST")}}']
]);
});
it("should handle nested html", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">a</ph><ph name="e2">b</ph>', null, null))] =
'<ph name="e2">B</ph><ph name="e0">A</ph>';
expect(humanizeDom(parse('<div i18n><a>a</a><b>b</b></div>', translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'b', 1],
[HtmlTextAst, 'B', 2],
[HtmlElementAst, 'a', 1],
[HtmlTextAst, 'A', 2],
]);
});
it("should support interpolation", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message(
'<ph name="e0">a</ph><ph name="e2"><ph name="t3">b<ph name="0"/></ph></ph>', null,
null))] = '<ph name="e2"><ph name="t3"><ph name="0"/>B</ph></ph><ph name="e0">A</ph>';
expect(humanizeDom(parse('<div i18n><a>a</a><b>b{{i}}</b></div>', translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'b', 1],
[HtmlTextAst, '{{i}}B', 2],
[HtmlElementAst, 'a', 1],
[HtmlTextAst, 'A', 2],
]);
});
it("should i18n attributes of placeholder elements", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">a</ph>', null, null))] = '<ph name="e0">A</ph>';
translations[id(new Message('b', null, null))] = 'B';
expect(humanizeDom(parse('<div i18n><a value="b" i18n-value>a</a></div>', translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlElementAst, 'a', 1],
[HtmlAttrAst, 'value', "B"],
[HtmlTextAst, 'A', 2],
]);
});
it("should preserve non-i18n attributes", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message', null, null))] = 'another message';
expect(humanizeDom(parse('<div i18n value="b">message</div>', translations)))
.toEqual([
[HtmlElementAst, 'div', 0],
[HtmlAttrAst, 'value', "b"],
[HtmlTextAst, 'another message', 1]
]);
});
it('should extract from partitions', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message1', 'meaning1', null))] = 'another message1';
translations[id(new Message('message2', 'meaning2', null))] = 'another message2';
let res = parse(`<!-- i18n: meaning1|desc1 -->message1<!-- /i18n --><!-- i18n: meaning2|desc2 -->message2<!-- /i18n -->`, translations);
expect(humanizeDom(res))
.toEqual([
[HtmlTextAst, 'another message1', 0],
[HtmlTextAst, 'another message2', 0],
]);
});
it("should preserve original positions", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">a</ph><ph name="e2">b</ph>', null, null))] =
'<ph name="e2">B</ph><ph name="e0">A</ph>';
let res =
(<any>parse('<div i18n><a>a</a><b>b</b></div>', translations).rootNodes[0]).children;
expect(res[0].sourceSpan.start.offset).toEqual(18);
expect(res[1].sourceSpan.start.offset).toEqual(10);
});
it("should handle the plural expansion form", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('zero<ph name="e1">bold</ph>', "plural_0", null))] =
'ZERO<ph name="e1">BOLD</ph>';
let res = parse(`{messages.length, plural,=0 {zero<b>bold</b>}}`, translations);
expect(humanizeDom(res))
.toEqual([
[HtmlElementAst, 'ul', 0],
[HtmlAttrAst, '[ngPlural]', 'messages.length'],
[HtmlElementAst, 'template', 1],
[HtmlAttrAst, 'ngPluralCase', '0'],
[HtmlElementAst, 'li', 2],
[HtmlTextAst, 'ZERO', 3],
[HtmlElementAst, 'b', 3],
[HtmlTextAst, 'BOLD', 4]
]);
});
it("should handle nested expansion forms", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('m', "gender_m", null))] = 'M';
let res = parse(`{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`, translations);
expect(humanizeDom(res))
.toEqual([
[HtmlElementAst, 'ul', 0],
[HtmlAttrAst, '[ngPlural]', 'messages.length'],
[HtmlElementAst, 'template', 1],
[HtmlAttrAst, 'ngPluralCase', '0'],
[HtmlElementAst, 'li', 2],
[HtmlElementAst, 'ul', 3],
[HtmlAttrAst, '[ngSwitch]', 'p.gender'],
[HtmlElementAst, 'template', 4],
[HtmlAttrAst, 'ngSwitchWhen', 'm'],
[HtmlElementAst, 'li', 5],
[HtmlTextAst, 'M', 6],
[HtmlTextAst, ' ', 3]
]);
});
it("should correctly set source code positions", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">bold</ph>', "plural_0", null))] =
'<ph name="e0">BOLD</ph>';
let nodes = parse(`{messages.length, plural,=0 {<b>bold</b>}}`, translations).rootNodes;
let ul: HtmlElementAst = <HtmlElementAst>nodes[0];
expect(ul.sourceSpan.start.col).toEqual(0);
expect(ul.sourceSpan.end.col).toEqual(42);
expect(ul.startSourceSpan.start.col).toEqual(0);
expect(ul.startSourceSpan.end.col).toEqual(42);
expect(ul.endSourceSpan.start.col).toEqual(0);
expect(ul.endSourceSpan.end.col).toEqual(42);
let switchExp = ul.attrs[0];
expect(switchExp.sourceSpan.start.col).toEqual(1);
expect(switchExp.sourceSpan.end.col).toEqual(16);
let template: HtmlElementAst = <HtmlElementAst>ul.children[0];
expect(template.sourceSpan.start.col).toEqual(26);
expect(template.sourceSpan.end.col).toEqual(41);
let switchCheck = template.attrs[0];
expect(switchCheck.sourceSpan.start.col).toEqual(26);
expect(switchCheck.sourceSpan.end.col).toEqual(28);
let li: HtmlElementAst = <HtmlElementAst>template.children[0];
expect(li.sourceSpan.start.col).toEqual(26);
expect(li.sourceSpan.end.col).toEqual(41);
let b: HtmlElementAst = <HtmlElementAst>li.children[0];
expect(b.sourceSpan.start.col).toEqual(29);
expect(b.sourceSpan.end.col).toEqual(32);
});
it("should handle other special forms", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('m', "gender_male", null))] = 'M';
let res = parse(`{person.gender, gender,=male {m}}`, translations);
expect(humanizeDom(res))
.toEqual([
[HtmlElementAst, 'ul', 0],
[HtmlAttrAst, '[ngSwitch]', 'person.gender'],
[HtmlElementAst, 'template', 1],
[HtmlAttrAst, 'ngSwitchWhen', 'male'],
[HtmlElementAst, 'li', 2],
[HtmlTextAst, 'M', 3],
]);
});
describe("errors", () => {
it("should error when giving an invalid template", () => {
expect(humanizeErrors(parse("<a>a</b>", {}).errors))
.toEqual(['Unexpected closing tag "b"']);
});
it("should error when no matching message (attr)", () => {
let mid = id(new Message("some message", null, null));
expect(humanizeErrors(parse("<div value='some message' i18n-value></div>", {}).errors))
.toEqual([`Cannot find message for id '${mid}', content 'some message'.`]);
});
it("should error when no matching message (text)", () => {
let mid = id(new Message("some message", null, null));
expect(humanizeErrors(parse("<div i18n>some message</div>", {}).errors))
.toEqual([`Cannot find message for id '${mid}', content 'some message'.`]);
});
it("should error when a non-placeholder element appears in translation", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message("some message", null, null))] = "<a>a</a>";
expect(humanizeErrors(parse("<div i18n>some message</div>", translations).errors))
.toEqual([`Unexpected tag "a". Only "ph" tags are allowed.`]);
});
it("should error when a placeholder element does not have the name attribute", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message("some message", null, null))] = "<ph>a</ph>";
expect(humanizeErrors(parse("<div i18n>some message</div>", translations).errors))
.toEqual([`Missing "name" attribute.`]);
});
it("should error when the translation refers to an invalid expression", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('hi <ph name="0"/>', null, null))] = 'hi <ph name="99"/>';
expect(
humanizeErrors(parse("<div value='hi {{a}}' i18n-value></div>", translations).errors))
.toEqual(["Invalid interpolation name '99'"]);
});
});
});
}
function humanizeErrors(errors: ParseError[]): string[] {
return errors.map(error => error.msg);
}

View File

@ -0,0 +1,238 @@
import {
AsyncTestCompleter,
beforeEach,
describe,
ddescribe,
expect,
iit,
inject,
it,
xdescribe,
xit
} from 'angular2/testing_internal';
import {HtmlParser} from 'angular2/src/compiler/html_parser';
import {MessageExtractor, removeDuplicates} from 'angular2/src/i18n/message_extractor';
import {Message} from 'angular2/src/i18n/message';
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {Lexer} from 'angular2/src/compiler/expression_parser/lexer';
export function main() {
describe('MessageExtractor', () => {
let extractor: MessageExtractor;
beforeEach(() => {
let htmlParser = new HtmlParser();
var parser = new Parser(new Lexer());
extractor = new MessageExtractor(htmlParser, parser);
});
it('should extract from elements with the i18n attr', () => {
let res = extractor.extract("<div i18n='meaning|desc'>message</div>", "someurl");
expect(res.messages).toEqual([new Message("message", 'meaning', 'desc')]);
});
it('should extract from elements with the i18n attr without a desc', () => {
let res = extractor.extract("<div i18n='meaning'>message</div>", "someurl");
expect(res.messages).toEqual([new Message("message", 'meaning', null)]);
});
it('should extract from elements with the i18n attr without a meaning', () => {
let res = extractor.extract("<div i18n>message</div>", "someurl");
expect(res.messages).toEqual([new Message("message", null, null)]);
});
it('should extract from attributes', () => {
let res = extractor.extract(`
<div
title1='message1' i18n-title1='meaning1|desc1'
title2='message2' i18n-title2='meaning2|desc2'>
</div>
`,
"someurl");
expect(res.messages)
.toEqual([
new Message("message1", "meaning1", "desc1"),
new Message("message2", "meaning2", "desc2")
]);
});
it('should extract from partitions', () => {
let res = extractor.extract(`
<!-- i18n: meaning1|desc1 -->message1<!-- /i18n -->
<!-- i18n: meaning2|desc2 -->message2<!-- /i18n -->`,
"someUrl");
expect(res.messages)
.toEqual([
new Message("message1", "meaning1", "desc1"),
new Message("message2", "meaning2", "desc2")
]);
});
it('should ignore other comments', () => {
let res = extractor.extract(`
<!-- i18n: meaning1|desc1 --><!-- other -->message1<!-- /i18n -->`,
"someUrl");
expect(res.messages).toEqual([new Message("message1", "meaning1", "desc1")]);
});
it('should replace interpolation with placeholders (text nodes)', () => {
let res = extractor.extract("<div i18n>Hi {{one}} and {{two}}</div>", "someurl");
expect(res.messages)
.toEqual(
[new Message('<ph name="t0">Hi <ph name="0"/> and <ph name="1"/></ph>', null, null)]);
});
it('should replace interpolation with placeholders (attributes)', () => {
let res =
extractor.extract("<div title='Hi {{one}} and {{two}}' i18n-title></div>", "someurl");
expect(res.messages)
.toEqual([new Message('Hi <ph name="0"/> and <ph name="1"/>', null, null)]);
});
it('should replace interpolation with named placeholders if provided (text nodes)', () => {
let res = extractor.extract(`
<div i18n>Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="SECOND")}}</div>`,
'someurl');
expect(res.messages)
.toEqual([
new Message('<ph name="t0">Hi <ph name="FIRST"/> and <ph name="SECOND"/></ph>', null,
null)
]);
});
it('should replace interpolation with named placeholders if provided (attributes)', () => {
let res = extractor.extract(`
<div title='Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="SECOND")}}'
i18n-title></div>`,
'someurl');
expect(res.messages)
.toEqual([new Message('Hi <ph name="FIRST"/> and <ph name="SECOND"/>', null, null)]);
});
it('should match named placeholders with extra spacing', () => {
let res = extractor.extract(`
<div title='Hi {{one // i18n ( ph = "FIRST" )}} and {{two // i18n ( ph = "SECOND" )}}'
i18n-title></div>`,
'someurl');
expect(res.messages)
.toEqual([new Message('Hi <ph name="FIRST"/> and <ph name="SECOND"/>', null, null)]);
});
it('should suffix duplicate placeholder names with numbers', () => {
let res = extractor.extract(`
<div title='Hi {{one //i18n(ph="FIRST")}} and {{two //i18n(ph="FIRST")}} and {{three //i18n(ph="FIRST")}}'
i18n-title></div>`,
'someurl');
expect(res.messages)
.toEqual([
new Message('Hi <ph name="FIRST"/> and <ph name="FIRST_1"/> and <ph name="FIRST_2"/>',
null, null)
]);
});
it("should handle html content", () => {
let res = extractor.extract(
'<div i18n><div attr="value">zero<div>one</div></div><div>two</div></div>', "someurl");
expect(res.messages)
.toEqual([
new Message('<ph name="e0">zero<ph name="e2">one</ph></ph><ph name="e4">two</ph>', null,
null)
]);
});
it("should handle html content with interpolation", () => {
let res =
extractor.extract('<div i18n><div>zero{{a}}<div>{{b}}</div></div></div>', "someurl");
expect(res.messages)
.toEqual([
new Message(
'<ph name="e0"><ph name="t1">zero<ph name="0"/></ph><ph name="e2"><ph name="t3"><ph name="0"/></ph></ph></ph>',
null, null)
]);
});
it("should extract from nested elements", () => {
let res = extractor.extract(
'<div title="message1" i18n-title="meaning1|desc1"><div i18n="meaning2|desc2">message2</div></div>',
"someurl");
expect(res.messages)
.toEqual([
new Message("message2", "meaning2", "desc2"),
new Message("message1", "meaning1", "desc1")
]);
});
it("should extract messages from attributes in i18n blocks", () => {
let res = extractor.extract(
'<div i18n><div attr="value" i18n-attr="meaning|desc">message</div></div>', "someurl");
expect(res.messages)
.toEqual([
new Message('<ph name="e0">message</ph>', null, null),
new Message('value', "meaning", "desc")
]);
});
it("should extract messages from special forms", () => {
let res = extractor.extract(`
<div>
{messages.length, plural,
=0 {You have <b>no</b> messages}
=1 {You have one message}
}
</div>
`,
"someurl");
expect(res.messages)
.toEqual([
new Message('You have <ph name="e1">no</ph> messages', "plural_0", null),
new Message('You have one message', "plural_1", null)
]);
});
it("should remove duplicate messages", () => {
let res = extractor.extract(`
<!-- i18n: meaning|desc1 -->message<!-- /i18n -->
<!-- i18n: meaning|desc2 -->message<!-- /i18n -->`,
"someUrl");
expect(removeDuplicates(res.messages))
.toEqual([
new Message("message", "meaning", "desc1"),
]);
});
describe("errors", () => {
it('should error on i18n attributes without matching "real" attributes', () => {
let res = extractor.extract(`
<div
title1='message1' i18n-title1='meaning1|desc1' i18n-title2='meaning2|desc2'>
</div>
`,
"someurl");
expect(res.errors.length).toEqual(1);
expect(res.errors[0].msg).toEqual("Missing attribute 'title2'.");
});
it('should error when cannot find a matching desc', () => {
let res = extractor.extract(`
<!-- i18n: meaning1|desc1 -->message1`,
"someUrl");
expect(res.errors.length).toEqual(1);
expect(res.errors[0].msg).toEqual("Missing closing 'i18n' comment.");
});
it("should return parse errors when the template is invalid", () => {
let res = extractor.extract("<input&#Besfs", "someurl");
expect(res.errors.length).toEqual(1);
expect(res.errors[0].msg).toEqual('Unexpected character "s"');
});
});
});
}

View File

@ -0,0 +1,27 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
expect,
iit,
inject,
it,
xdescribe,
xit
} from 'angular2/testing_internal';
import {Message, id} from 'angular2/src/i18n/message';
export function main() {
describe('Message', () => {
describe("id", () => {
it("should return a different id for messages with and without the meaning", () => {
let m1 = new Message("content", "meaning", null);
let m2 = new Message("content", null, null);
expect(id(m1)).toEqual(id(m1));
expect(id(m1)).not.toEqual(id(m2));
});
});
});
}

View File

@ -0,0 +1,117 @@
import {
AsyncTestCompleter,
beforeEach,
ddescribe,
describe,
expect,
iit,
inject,
it,
xdescribe,
xit
} from 'angular2/testing_internal';
import {HtmlAst} from 'angular2/src/compiler/html_ast';
import {Message, id} from 'angular2/src/i18n/message';
import {serializeXmb, deserializeXmb} from 'angular2/src/i18n/xmb_serializer';
import {ParseSourceSpan, ParseError} from 'angular2/src/compiler/parse_util';
export function main() {
describe("Xmb", () => {
describe('Xmb Serialization', () => {
it("should return an empty message bundle for an empty list of messages",
() => { expect(serializeXmb([])).toEqual("<message-bundle></message-bundle>"); });
it("should serializeXmb messages without desc", () => {
let m = new Message("content", "meaning", null);
let expected = `<message-bundle><msg id='${id(m)}'>content</msg></message-bundle>`;
expect(serializeXmb([m])).toEqual(expected);
});
it("should serializeXmb messages with desc", () => {
let m = new Message("content", "meaning", "description");
let expected =
`<message-bundle><msg id='${id(m)}' desc='description'>content</msg></message-bundle>`;
expect(serializeXmb([m])).toEqual(expected);
});
});
describe("Xmb Deserialization", () => {
it("should parse an empty bundle", () => {
let mb = "<message-bundle></message-bundle>";
expect(deserializeXmb(mb, "url").messages).toEqual({});
});
it("should parse an non-empty bundle", () => {
let mb = `
<message-bundle>
<msg id="id1" desc="description1">content1</msg>
<msg id="id2">content2</msg>
</message-bundle>
`;
let parsed = deserializeXmb(mb, "url").messages;
expect(_serialize(parsed["id1"])).toEqual("content1");
expect(_serialize(parsed["id2"])).toEqual("content2");
});
it("should error when cannot parse the content", () => {
let mb = `
<message-bundle>
<msg id="id1" desc="description1">content
</message-bundle>
`;
let res = deserializeXmb(mb, "url");
expect(_serializeErrors(res.errors)).toEqual(['Unexpected closing tag "message-bundle"']);
});
it("should error when cannot find the id attribute", () => {
let mb = `
<message-bundle>
<msg>content</msg>
</message-bundle>
`;
let res = deserializeXmb(mb, "url");
expect(_serializeErrors(res.errors)).toEqual(['"id" attribute is missing']);
});
it("should error on empty content", () => {
let mb = ``;
let res = deserializeXmb(mb, "url");
expect(_serializeErrors(res.errors)).toEqual(['Missing element "message-bundle"']);
});
it("should error on an invalid element", () => {
let mb = `
<message-bundle>
<invalid>content</invalid>
</message-bundle>
`;
let res = deserializeXmb(mb, "url");
expect(_serializeErrors(res.errors)).toEqual(['Unexpected element "invalid"']);
});
it("should expand 'ph' elements", () => {
let mb = `
<message-bundle>
<msg id="id1">a<ph name="i0"/></msg>
</message-bundle>
`;
let res = deserializeXmb(mb, "url").messages["id1"];
expect((<any>res[1]).name).toEqual("ph");
});
});
});
}
function _serialize(nodes: HtmlAst[]): string {
return (<any>nodes[0]).value;
}
function _serializeErrors(errors: ParseError[]): string[] {
return errors.map(e => e.msg);
}

View File

@ -0,0 +1,289 @@
import {
TestComponentBuilder,
AsyncTestCompleter,
ddescribe,
describe,
it,
iit,
xit,
expect,
beforeEach,
afterEach,
beforeEachProviders,
inject
} from 'angular2/testing_internal';
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {LegacyHtmlAstTransformer} from 'angular2/src/compiler/legacy_template';
export function main() {
describe('Support for legacy template', () => {
describe('Template rewriting', () => {
let visitor;
beforeEach(() => { visitor = new LegacyHtmlAstTransformer(['yes-mapped']); });
describe('non template elements', () => {
it('should rewrite event binding', () => {
let fixtures = [
{'from': 'on-dash-case', 'to': 'on-dashCase'},
{'from': 'ON-dash-case', 'to': 'on-dashCase'},
{'from': 'bindon-dash-case', 'to': 'bindon-dashCase'},
{'from': '(dash-case)', 'to': '(dashCase)'},
{'from': '[(dash-case)]', 'to': '[(dashCase)]'},
{'from': 'on-camelCase', 'to': 'on-camelCase'},
{'from': 'bindon-camelCase', 'to': 'bindon-camelCase'},
{'from': '(camelCase)', 'to': '(camelCase)'},
{'from': '[(camelCase)]', 'to': '[(camelCase)]'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual(f['to']);
expect(attr.value).toEqual('expression');
});
});
it('should not rewrite style binding', () => {
let fixtures = [
{'from': '[style.background-color]', 'to': '[style.background-color]'},
{'from': '[style.margin-top.px]', 'to': '[style.margin-top.px]'},
{'from': '[style.camelCase]', 'to': '[style.camelCase]'},
{'from': '[STYLE.camelCase]', 'to': '[style.camelCase]'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual(f['to']);
expect(attr.value).toEqual('expression');
});
});
it('should not rewrite attribute bindings', () => {
let fixtures = [
{'from': '[attr.my-attr]', 'to': '[attr.my-attr]'},
{'from': '[ATTR.my-attr]', 'to': '[attr.my-attr]'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual(f['to']);
expect(attr.value).toEqual('expression');
});
});
it('should not rewrite class bindings', () => {
let fixtures = [
{'from': '[class.my-class]', 'to': '[class.my-class]'},
{'from': '[CLASS.my-class]', 'to': '[class.my-class]'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual(f['to']);
expect(attr.value).toEqual('expression');
});
});
it('should rewrite variables', () => {
let fixtures = [
{'from': '#dash-case', 'to': '#dashCase'},
{'from': 'var-dash-case', 'to': 'var-dashCase'},
{'from': 'VAR-dash-case', 'to': 'var-dashCase'},
{'from': 'VAR-camelCase', 'to': 'var-camelCase'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual(f['to']);
expect(attr.value).toEqual('expression');
});
});
it('should rewrite variable values', () => {
let fixtures = [
{'from': 'dash-case', 'to': 'dashCase'},
{'from': 'lower', 'to': 'lower'},
{'from': 'camelCase', 'to': 'camelCase'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst('#a', f['from'], null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual('#a');
expect(attr.value).toEqual(f['to']);
legacyAttr = new HtmlAttrAst('var-a', f['from'], null);
attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual('var-a');
expect(attr.value).toEqual(f['to']);
});
});
it('should rewrite variables in template bindings', () => {
let fixtures = [
{'from': 'dir: #a-b', 'to': 'dir: #aB'},
{'from': 'dir: var a-b', 'to': 'dir: var aB'},
{'from': 'dir: VAR a-b;', 'to': 'dir: var aB;'},
{'from': 'dir: VAR a-b; #c-d=e', 'to': 'dir: var aB; #cD=e'},
{'from': 'dir: VAR aB; #cD=e', 'to': 'dir: var aB; #cD=e'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst('template', f['from'], null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.value).toEqual(f['to']);
});
});
it('should lowercase the "template" attribute', () => {
let fixtures = ['Template', 'TEMPLATE', 'template'];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f, 'expression', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual('template');
expect(attr.value).toEqual('expression');
});
});
it('should rewrite property binding', () => {
let fixtures = [
{'from': '[my-prop]', 'to': '[myProp]'},
{'from': 'bind-my-prop', 'to': 'bind-myProp'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual(f['to']);
expect(attr.value).toEqual('expression');
});
});
it('should rewrite structural directive selectors template="..."', () => {
let legacyAttr = new HtmlAttrAst('TEMPLATE', 'ng-if condition', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual('template');
expect(attr.value).toEqual('ngIf condition');
});
it('should rewrite *-selectors', () => {
let legacyAttr = new HtmlAttrAst('*ng-for', '#my-item of myItems', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual('*ngFor');
expect(attr.value).toEqual('#myItem of myItems');
});
it('should rewrite directive special cases', () => {
let fixtures = [
{'from': 'ng-non-bindable', 'to': 'ngNonBindable'},
{'from': 'yes-mapped', 'to': 'yesMapped'},
{'from': 'no-mapped', 'to': 'no-mapped'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual(f['to']);
expect(attr.value).toEqual('expression');
});
});
it('should not rewrite random attributes', () => {
let fixtures = [
{'from': 'custom-attr', 'to': 'custom-attr'},
{'from': 'ng-if', 'to': 'ng-if'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual(f['to']);
expect(attr.value).toEqual('expression');
});
});
it('should rewrite interpolation', () => {
let fixtures = [
{'from': 'dash-case', 'to': 'dashCase'},
{'from': 'lcase', 'to': 'lcase'},
{'from': 'camelCase', 'to': 'camelCase'},
{'from': 'attr.dash-case', 'to': 'attr.dash-case'},
{'from': 'class.dash-case', 'to': 'class.dash-case'},
{'from': 'style.dash-case', 'to': 'style.dash-case'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f['from'], '{{ exp }}', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual(f['to']);
expect(attr.value).toEqual('{{ exp }}');
});
});
});
});
describe('template elements', () => {
let visitor;
beforeEach(() => {
visitor = new LegacyHtmlAstTransformer();
visitor.visitingTemplateEl = true;
});
it('should rewrite angular constructs', () => {
let fixtures = [
{'from': 'on-dash-case', 'to': 'on-dashCase'},
{'from': 'ON-dash-case', 'to': 'on-dashCase'},
{'from': 'bindon-dash-case', 'to': 'bindon-dashCase'},
{'from': '(dash-case)', 'to': '(dashCase)'},
{'from': '[(dash-case)]', 'to': '[(dashCase)]'},
{'from': 'on-camelCase', 'to': 'on-camelCase'},
{'from': 'bindon-camelCase', 'to': 'bindon-camelCase'},
{'from': '(camelCase)', 'to': '(camelCase)'},
{'from': '[(camelCase)]', 'to': '[(camelCase)]'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual(f['to']);
expect(attr.value).toEqual('expression');
});
});
it('should rewrite all attributes', () => {
let fixtures = [
{'from': 'custom-attr', 'to': 'customAttr'},
{'from': 'ng-if', 'to': 'ngIf'},
];
fixtures.forEach((f) => {
let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null);
let attr = visitor.visitAttr(legacyAttr, null);
expect(attr.name).toEqual(f['to']);
expect(attr.value).toEqual('expression');
});
});
});
});
}

View File

@ -0,0 +1,9 @@
library angular2.test.compiler.metadata_resolver_fixture;
import "package:angular2/core.dart" show Component;
// This component is not actually malformed; this fixture is here to
// make Dart not complain about a missing import for a test case that only
// matters in an JavaScript app.
@Component(template: "")
class MalformedStylesComponent {}

View File

@ -0,0 +1,5 @@
import {Component} from 'angular2/core';
@Component({styles:<any>('foo'), template: ''})
export class MalformedStylesComponent {
}

View File

@ -0,0 +1,154 @@
import {
ddescribe,
describe,
xdescribe,
it,
iit,
xit,
expect,
beforeEach,
afterEach,
AsyncTestCompleter,
inject,
beforeEachProviders
} from 'angular2/testing_internal';
import {IS_DART, stringify} from 'angular2/src/facade/lang';
import {CompileMetadataResolver} from 'angular2/src/compiler/metadata_resolver';
import {LifecycleHooks, LIFECYCLE_HOOKS_VALUES} from 'angular2/src/core/metadata/lifecycle_hooks';
import {
Component,
Directive,
ViewEncapsulation,
ChangeDetectionStrategy,
OnChanges,
OnInit,
DoCheck,
OnDestroy,
AfterContentInit,
AfterContentChecked,
AfterViewInit,
AfterViewChecked,
SimpleChange,
provide
} from 'angular2/core';
import {TEST_PROVIDERS} from './test_bindings';
import {MODULE_SUFFIX} from 'angular2/src/compiler/util';
import {PLATFORM_DIRECTIVES} from 'angular2/src/core/platform_directives_and_pipes';
import {MalformedStylesComponent} from './metadata_resolver_fixture';
export function main() {
describe('CompileMetadataResolver', () => {
beforeEachProviders(() => TEST_PROVIDERS);
describe('getMetadata', () => {
it('should read metadata',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
var meta = resolver.getDirectiveMetadata(ComponentWithEverything);
expect(meta.selector).toEqual('someSelector');
expect(meta.exportAs).toEqual('someExportAs');
expect(meta.isComponent).toBe(true);
expect(meta.type.runtime).toBe(ComponentWithEverything);
expect(meta.type.name).toEqual(stringify(ComponentWithEverything));
expect(meta.lifecycleHooks).toEqual(LIFECYCLE_HOOKS_VALUES);
expect(meta.changeDetection).toBe(ChangeDetectionStrategy.CheckAlways);
expect(meta.inputs).toEqual({'someProp': 'someProp'});
expect(meta.outputs).toEqual({'someEvent': 'someEvent'});
expect(meta.hostListeners).toEqual({'someHostListener': 'someHostListenerExpr'});
expect(meta.hostProperties).toEqual({'someHostProp': 'someHostPropExpr'});
expect(meta.hostAttributes).toEqual({'someHostAttr': 'someHostAttrValue'});
expect(meta.template.encapsulation).toBe(ViewEncapsulation.Emulated);
expect(meta.template.styles).toEqual(['someStyle']);
expect(meta.template.styleUrls).toEqual(['someStyleUrl']);
expect(meta.template.template).toEqual('someTemplate');
expect(meta.template.templateUrl).toEqual('someTemplateUrl');
expect(meta.template.baseUrl).toEqual(`package:someModuleId${MODULE_SUFFIX}`);
}));
it('should use the moduleUrl from the reflector if none is given',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
var value: string =
resolver.getDirectiveMetadata(ComponentWithoutModuleId).template.baseUrl;
var expectedEndValue =
IS_DART ? 'test/compiler/metadata_resolver_spec.dart' : './ComponentWithoutModuleId';
expect(value.endsWith(expectedEndValue)).toBe(true);
}));
it('should throw when metadata is incorrectly typed',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
if (!IS_DART) {
expect(() => resolver.getDirectiveMetadata(MalformedStylesComponent))
.toThrowError(`Expected 'styles' to be an array of strings.`);
}
}));
});
describe('getViewDirectivesMetadata', () => {
it('should return the directive metadatas',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
expect(resolver.getViewDirectivesMetadata(ComponentWithEverything))
.toContain(resolver.getDirectiveMetadata(SomeDirective));
}));
describe("platform directives", () => {
beforeEachProviders(
() => [provide(PLATFORM_DIRECTIVES, {useValue: [ADirective], multi: true})]);
it('should include platform directives when available',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
expect(resolver.getViewDirectivesMetadata(ComponentWithEverything))
.toContain(resolver.getDirectiveMetadata(ADirective));
expect(resolver.getViewDirectivesMetadata(ComponentWithEverything))
.toContain(resolver.getDirectiveMetadata(SomeDirective));
}));
});
});
});
}
@Directive({selector: 'a-directive'})
class ADirective {
}
@Directive({selector: 'someSelector'})
class SomeDirective {
}
@Component({selector: 'someComponent', template: ''})
class ComponentWithoutModuleId {
}
@Component({
selector: 'someSelector',
inputs: ['someProp'],
outputs: ['someEvent'],
host: {
'[someHostProp]': 'someHostPropExpr',
'(someHostListener)': 'someHostListenerExpr',
'someHostAttr': 'someHostAttrValue'
},
exportAs: 'someExportAs',
moduleId: 'someModuleId',
changeDetection: ChangeDetectionStrategy.CheckAlways,
template: 'someTemplate',
templateUrl: 'someTemplateUrl',
encapsulation: ViewEncapsulation.Emulated,
styles: ['someStyle'],
styleUrls: ['someStyleUrl'],
directives: [SomeDirective]
})
class ComponentWithEverything implements OnChanges,
OnInit, DoCheck, OnDestroy, AfterContentInit, AfterContentChecked, AfterViewInit,
AfterViewChecked {
ngOnChanges(changes: {[key: string]: SimpleChange}): void {}
ngOnInit(): void {}
ngDoCheck(): void {}
ngOnDestroy(): void {}
ngAfterContentInit(): void {}
ngAfterContentChecked(): void {}
ngAfterViewInit(): void {}
ngAfterViewChecked(): void {}
}

View File

@ -0,0 +1,19 @@
// ATTENTION: This file will be overwritten with generated code by main()
import {print, IS_DART} from 'angular2/src/facade/lang';
import {TypeScriptEmitter} from 'angular2/src/compiler/output/ts_emitter';
import {DartEmitter} from 'angular2/src/compiler/output/dart_emitter';
import {compileComp, compAMetadata} from './offline_compiler_util';
import {ComponentFactory} from 'angular2/src/core/linker/component_factory';
import {CompA} from './offline_compiler_util';
export const CompANgFactory: ComponentFactory<CompA> = null;
// Generator
export function main(args: string[]) {
var emitter = IS_DART ? new DartEmitter() : new TypeScriptEmitter();
compileComp(emitter, compAMetadata)
.then((source) => {
// debug: console.error(source);
print(source);
});
}

View File

@ -0,0 +1,18 @@
// ATTENTION: This file will be overwritten with generated code by main()
import {print} from 'angular2/src/facade/lang';
import {JavaScriptEmitter} from 'angular2/src/compiler/output/js_emitter';
import {compileComp, compAMetadata} from './offline_compiler_util';
import {ComponentFactory} from 'angular2/src/core/linker/component_factory';
import {CompA} from './offline_compiler_util';
export const CompANgFactory: ComponentFactory<CompA> = null;
// Generator
export function main(args: string[]) {
var emitter = new JavaScriptEmitter();
compileComp(emitter, compAMetadata)
.then((source) => {
// debug: console.error(source);
print(source);
});
}

View File

@ -0,0 +1,2 @@
export const styles = /*@ts2dart_const*/[`.greenStyle[_ngcontent-a-1] { color: green; }`];

View File

@ -0,0 +1,69 @@
import {
ddescribe,
describe,
xdescribe,
it,
iit,
xit,
expect,
beforeEach,
afterEach,
AsyncTestCompleter,
inject,
beforeEachProviders,
el
} from 'angular2/testing_internal';
import {IS_DART} from 'angular2/src/facade/lang';
import {Injector} from 'angular2/core';
import {ComponentFactory} from 'angular2/src/core/linker/component_factory';
import * as typed from './offline_compiler_codegen_typed';
import * as untyped from './offline_compiler_codegen_untyped';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {SharedStylesHost} from "angular2/src/platform/dom/shared_styles_host";
import {CompA} from './offline_compiler_util';
export function main() {
var typedComponentFactory = typed.CompANgFactory;
var untypedComponentFactory = untyped.CompANgFactory;
var fixtures: TestFixture[] = [];
if (IS_DART || !DOM.supportsDOMEvents()) {
// Our generator only works on node.js and Dart...
fixtures.push(new TestFixture(typedComponentFactory, 'typed'));
}
if (!IS_DART) {
// Our generator only works on node.js and Dart...
if (!DOM.supportsDOMEvents()) {
fixtures.push(new TestFixture(untypedComponentFactory, 'untyped'));
}
}
describe('OfflineCompiler', () => {
var injector: Injector;
var sharedStylesHost: SharedStylesHost;
beforeEach(inject([Injector, SharedStylesHost], (_injector, _sharedStylesHost) => {
injector = _injector;
sharedStylesHost = _sharedStylesHost;
}));
fixtures.forEach((fixture) => {
describe(`${fixture.name}`, () => {
it('should compile components', () => {
var hostEl = fixture.compFactory.create(injector);
expect(hostEl.instance).toBeAnInstanceOf(CompA);
var styles = sharedStylesHost.getAllStyles();
expect(styles[0]).toContain('.redStyle[_ngcontent');
expect(styles[1]).toContain('.greenStyle[_ngcontent');
});
});
});
});
}
class TestFixture {
constructor(public compFactory: ComponentFactory<CompA>, public name: string) {}
}

View File

@ -0,0 +1,69 @@
import {print, IS_DART} from 'angular2/src/facade/lang';
import {OutputEmitter} from 'angular2/src/compiler/output/abstract_emitter';
import {Console} from 'angular2/src/core/console';
import {
OfflineCompiler,
NormalizedComponentWithViewDirectives,
SourceModule
} from 'angular2/src/compiler/offline_compiler';
import {TemplateParser} from 'angular2/src/compiler/template_parser';
import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {Lexer} from 'angular2/src/compiler/expression_parser/lexer';
import {HtmlParser} from 'angular2/src/compiler/html_parser';
import {StyleCompiler} from 'angular2/src/compiler/style_compiler';
import {ViewCompiler} from 'angular2/src/compiler/view_compiler/view_compiler';
import {DirectiveNormalizer} from 'angular2/src/compiler/directive_normalizer';
import {CompilerConfig} from 'angular2/src/compiler/config';
import {createOfflineCompileUrlResolver} from 'angular2/src/compiler/url_resolver';
import {MockSchemaRegistry} from './schema_registry_mock';
import {MODULE_SUFFIX} from 'angular2/src/compiler/util';
import {MockXHR} from 'angular2/src/compiler/xhr_mock';
import {
CompileDirectiveMetadata,
CompileTypeMetadata,
CompileTemplateMetadata
} from 'angular2/src/compiler/compile_metadata';
export class CompA { user: string; }
var THIS_MODULE_PATH = `asset:angular2/test/compiler`;
var THIS_MODULE_URL = `${THIS_MODULE_PATH}/offline_compiler_util${MODULE_SUFFIX}`;
export var compAMetadata = CompileDirectiveMetadata.create({
isComponent: true,
selector: 'comp-a',
type: new CompileTypeMetadata(
{name: 'CompA', moduleUrl: THIS_MODULE_URL, runtime: CompA, diDeps: []}),
template: new CompileTemplateMetadata({
templateUrl: './offline_compiler_compa.html',
styles: ['.redStyle { color: red; }'],
styleUrls: ['./offline_compiler_compa.css'],
baseUrl: THIS_MODULE_URL,
})
});
function _createOfflineCompiler(xhr: MockXHR, emitter: OutputEmitter): OfflineCompiler {
var urlResolver = createOfflineCompileUrlResolver();
xhr.when(`${THIS_MODULE_PATH}/offline_compiler_compa.html`, 'Hello World {{user}}!');
var htmlParser = new HtmlParser();
var normalizer = new DirectiveNormalizer(xhr, urlResolver, htmlParser);
return new OfflineCompiler(
normalizer, new TemplateParser(new Parser(new Lexer()), new MockSchemaRegistry({}, {}),
htmlParser, new Console(), []),
new StyleCompiler(urlResolver), new ViewCompiler(new CompilerConfig(true, true, true)),
emitter);
}
export function compileComp(emitter: OutputEmitter,
comp: CompileDirectiveMetadata): Promise<string> {
var xhr = new MockXHR();
var compiler = _createOfflineCompiler(xhr, emitter);
var result = compiler.normalizeDirectiveMetadata(comp).then((normComp) => {
return compiler.compileTemplates([new NormalizedComponentWithViewDirectives(normComp, [], [])])
.source;
});
xhr.flush();
return result;
}

Some files were not shown because too many files have changed in this diff Show More