chore(core): move compiler out of core

BREAKING CHANGE

All imports from 'angular2/core/compiler' should be changed to 'angular2/compiler'.
This commit is contained in:
vsavkin
2015-11-05 14:07:57 -08:00
parent f39bbff7bc
commit 6edd964a83
121 changed files with 217 additions and 234 deletions
@@ -0,0 +1,18 @@
import {AppRootUrl} from "angular2/src/compiler/app_root_url";
import {DOM} from "angular2/src/core/dom/dom_adapter";
import {Injectable} from "angular2/src/core/di";
/**
* Extension of {@link AppRootUrl} that uses a DOM anchor tag to set the root url to
* the current page's url.
*/
@Injectable()
export class AnchorBasedAppRootUrl extends AppRootUrl {
constructor() {
super("");
// compute the root url to pass to AppRootUrl
var a = DOM.createElement('a');
DOM.resolveAndSetHref(a, './', null);
this.value = DOM.getHref(a);
}
}
@@ -0,0 +1,16 @@
import {Injectable} from 'angular2/src/core/di';
import {isBlank} from 'angular2/src/core/facade/lang';
/**
* Specifies app root url for the application.
*
* Used by the {@link Compiler} when resolving HTML and CSS template URLs.
*
* This interface can be overridden by the application developer to create custom behavior.
*
* See {@link Compiler}
*/
@Injectable()
export class AppRootUrl {
constructor(public value: string) {}
}
@@ -0,0 +1,207 @@
import {ListWrapper} from 'angular2/src/core/facade/collection';
import {isPresent, isBlank} from 'angular2/src/core/facade/lang';
import {reflector} from 'angular2/src/core/reflection/reflection';
import {
DirectiveIndex,
BindingRecord,
DirectiveRecord,
ChangeDetectionStrategy,
ChangeDetectorDefinition,
ChangeDetectorGenConfig,
ASTWithSource
} from 'angular2/src/core/change_detection/change_detection';
import {CompileDirectiveMetadata, CompileTypeMetadata} from './directive_metadata';
import {
TemplateAst,
ElementAst,
BoundTextAst,
PropertyBindingType,
DirectiveAst,
TemplateAstVisitor,
templateVisitAll,
NgContentAst,
EmbeddedTemplateAst,
VariableAst,
BoundElementPropertyAst,
BoundEventAst,
BoundDirectivePropertyAst,
AttrAst,
TextAst
} from './template_ast';
import {LifecycleHooks} from 'angular2/src/core/linker/interfaces';
export function createChangeDetectorDefinitions(
componentType: CompileTypeMetadata, componentStrategy: ChangeDetectionStrategy,
genConfig: ChangeDetectorGenConfig, parsedTemplate: TemplateAst[]): ChangeDetectorDefinition[] {
var pvVisitors = [];
var visitor = new ProtoViewVisitor(null, pvVisitors, componentStrategy);
templateVisitAll(visitor, parsedTemplate);
return createChangeDefinitions(pvVisitors, componentType, genConfig);
}
class ProtoViewVisitor implements TemplateAstVisitor {
viewIndex: number;
boundTextCount: number = 0;
boundElementCount: number = 0;
variableNames: string[] = [];
bindingRecords: BindingRecord[] = [];
eventRecords: BindingRecord[] = [];
directiveRecords: DirectiveRecord[] = [];
constructor(public parent: ProtoViewVisitor, public allVisitors: ProtoViewVisitor[],
public strategy: ChangeDetectionStrategy) {
this.viewIndex = allVisitors.length;
allVisitors.push(this);
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
this.boundElementCount++;
templateVisitAll(this, ast.outputs);
for (var i = 0; i < ast.directives.length; i++) {
ast.directives[i].visit(this, i);
}
var childVisitor =
new ProtoViewVisitor(this, this.allVisitors, ChangeDetectionStrategy.Default);
// Attention: variables present on an embedded template count towards
// the embedded template and not the template anchor!
templateVisitAll(childVisitor, ast.vars);
templateVisitAll(childVisitor, ast.children);
return null;
}
visitElement(ast: ElementAst, context: any): any {
if (ast.isBound()) {
this.boundElementCount++;
}
templateVisitAll(this, ast.inputs, null);
templateVisitAll(this, ast.outputs);
templateVisitAll(this, ast.exportAsVars);
for (var i = 0; i < ast.directives.length; i++) {
ast.directives[i].visit(this, i);
}
templateVisitAll(this, ast.children);
return null;
}
visitNgContent(ast: NgContentAst, context: any): any { return null; }
visitVariable(ast: VariableAst, context: any): any {
this.variableNames.push(ast.name);
return null;
}
visitEvent(ast: BoundEventAst, directiveRecord: DirectiveRecord): any {
var bindingRecord =
isPresent(directiveRecord) ?
BindingRecord.createForHostEvent(ast.handler, ast.fullName, directiveRecord) :
BindingRecord.createForEvent(ast.handler, ast.fullName, this.boundElementCount - 1);
this.eventRecords.push(bindingRecord);
return null;
}
visitElementProperty(ast: BoundElementPropertyAst, directiveRecord: DirectiveRecord): any {
var boundElementIndex = this.boundElementCount - 1;
var dirIndex = isPresent(directiveRecord) ? directiveRecord.directiveIndex : null;
var bindingRecord;
if (ast.type === PropertyBindingType.Property) {
bindingRecord =
isPresent(dirIndex) ?
BindingRecord.createForHostProperty(dirIndex, ast.value, ast.name) :
BindingRecord.createForElementProperty(ast.value, boundElementIndex, ast.name);
} else if (ast.type === PropertyBindingType.Attribute) {
bindingRecord =
isPresent(dirIndex) ?
BindingRecord.createForHostAttribute(dirIndex, ast.value, ast.name) :
BindingRecord.createForElementAttribute(ast.value, boundElementIndex, ast.name);
} else if (ast.type === PropertyBindingType.Class) {
bindingRecord =
isPresent(dirIndex) ?
BindingRecord.createForHostClass(dirIndex, ast.value, ast.name) :
BindingRecord.createForElementClass(ast.value, boundElementIndex, ast.name);
} else if (ast.type === PropertyBindingType.Style) {
bindingRecord =
isPresent(dirIndex) ?
BindingRecord.createForHostStyle(dirIndex, ast.value, ast.name, ast.unit) :
BindingRecord.createForElementStyle(ast.value, boundElementIndex, ast.name, ast.unit);
}
this.bindingRecords.push(bindingRecord);
return null;
}
visitAttr(ast: AttrAst, context: any): any { return null; }
visitBoundText(ast: BoundTextAst, context: any): any {
var boundTextIndex = this.boundTextCount++;
this.bindingRecords.push(BindingRecord.createForTextNode(ast.value, boundTextIndex));
return null;
}
visitText(ast: TextAst, context: any): any { return null; }
visitDirective(ast: DirectiveAst, directiveIndexAsNumber: number): any {
var directiveIndex = new DirectiveIndex(this.boundElementCount - 1, directiveIndexAsNumber);
var directiveMetadata = ast.directive;
var directiveRecord = new DirectiveRecord({
directiveIndex: directiveIndex,
callAfterContentInit:
directiveMetadata.lifecycleHooks.indexOf(LifecycleHooks.AfterContentInit) !== -1,
callAfterContentChecked:
directiveMetadata.lifecycleHooks.indexOf(LifecycleHooks.AfterContentChecked) !== -1,
callAfterViewInit:
directiveMetadata.lifecycleHooks.indexOf(LifecycleHooks.AfterViewInit) !== -1,
callAfterViewChecked:
directiveMetadata.lifecycleHooks.indexOf(LifecycleHooks.AfterViewChecked) !== -1,
callOnChanges: directiveMetadata.lifecycleHooks.indexOf(LifecycleHooks.OnChanges) !== -1,
callDoCheck: directiveMetadata.lifecycleHooks.indexOf(LifecycleHooks.DoCheck) !== -1,
callOnInit: directiveMetadata.lifecycleHooks.indexOf(LifecycleHooks.OnInit) !== -1,
changeDetection: directiveMetadata.changeDetection
});
this.directiveRecords.push(directiveRecord);
templateVisitAll(this, ast.inputs, directiveRecord);
var bindingRecords = this.bindingRecords;
if (directiveRecord.callOnChanges) {
bindingRecords.push(BindingRecord.createDirectiveOnChanges(directiveRecord));
}
if (directiveRecord.callOnInit) {
bindingRecords.push(BindingRecord.createDirectiveOnInit(directiveRecord));
}
if (directiveRecord.callDoCheck) {
bindingRecords.push(BindingRecord.createDirectiveDoCheck(directiveRecord));
}
templateVisitAll(this, ast.hostProperties, directiveRecord);
templateVisitAll(this, ast.hostEvents, directiveRecord);
templateVisitAll(this, ast.exportAsVars);
return null;
}
visitDirectiveProperty(ast: BoundDirectivePropertyAst, directiveRecord: DirectiveRecord): any {
// TODO: these setters should eventually be created by change detection, to make
// it monomorphic!
var setter = reflector.setter(ast.directiveName);
this.bindingRecords.push(
BindingRecord.createForDirective(ast.value, ast.directiveName, setter, directiveRecord));
return null;
}
}
function createChangeDefinitions(pvVisitors: ProtoViewVisitor[], componentType: CompileTypeMetadata,
genConfig: ChangeDetectorGenConfig): ChangeDetectorDefinition[] {
var pvVariableNames = _collectNestedProtoViewsVariableNames(pvVisitors);
return pvVisitors.map(pvVisitor => {
var id = `${componentType.name}_${pvVisitor.viewIndex}`;
return new ChangeDetectorDefinition(
id, pvVisitor.strategy, pvVariableNames[pvVisitor.viewIndex], pvVisitor.bindingRecords,
pvVisitor.eventRecords, pvVisitor.directiveRecords, genConfig);
});
}
function _collectNestedProtoViewsVariableNames(pvVisitors: ProtoViewVisitor[]): string[][] {
var nestedPvVariableNames: string[][] = ListWrapper.createFixedSize(pvVisitors.length);
pvVisitors.forEach((pv) => {
var parentVariableNames: string[] =
isPresent(pv.parent) ? nestedPvVariableNames[pv.parent.viewIndex] : [];
nestedPvVariableNames[pv.viewIndex] = parentVariableNames.concat(pv.variableNames);
});
return nestedPvVariableNames;
}
@@ -0,0 +1,92 @@
import {CompileTypeMetadata} from './directive_metadata';
import {SourceExpressions, moduleRef} from './source_module';
import {
ChangeDetectorJITGenerator
} from 'angular2/src/core/change_detection/change_detection_jit_generator';
import {createChangeDetectorDefinitions} from './change_definition_factory';
import {IS_DART, isJsObject, CONST_EXPR} from 'angular2/src/core/facade/lang';
import {
ChangeDetectorGenConfig,
ChangeDetectorDefinition,
DynamicProtoChangeDetector,
ChangeDetectionStrategy
} from 'angular2/src/core/change_detection/change_detection';
import {TemplateAst} from './template_ast';
import {Codegen} from 'angular2/src/transform/template_compiler/change_detector_codegen';
import {MODULE_SUFFIX} from './util';
import {Injectable} from 'angular2/src/core/di';
const ABSTRACT_CHANGE_DETECTOR = "AbstractChangeDetector";
const UTIL = "ChangeDetectionUtil";
const CHANGE_DETECTOR_STATE = "ChangeDetectorState";
var ABSTRACT_CHANGE_DETECTOR_MODULE = moduleRef(
`package:angular2/src/core/change_detection/abstract_change_detector${MODULE_SUFFIX}`);
var UTIL_MODULE =
moduleRef(`package:angular2/src/core/change_detection/change_detection_util${MODULE_SUFFIX}`);
var PREGEN_PROTO_CHANGE_DETECTOR_MODULE = moduleRef(
`package:angular2/src/core/change_detection/pregen_proto_change_detector${MODULE_SUFFIX}`);
var CONSTANTS_MODULE =
moduleRef(`package:angular2/src/core/change_detection/constants${MODULE_SUFFIX}`);
@Injectable()
export class ChangeDetectionCompiler {
constructor(private _genConfig: ChangeDetectorGenConfig) {}
compileComponentRuntime(componentType: CompileTypeMetadata, strategy: ChangeDetectionStrategy,
parsedTemplate: TemplateAst[]): Function[] {
var changeDetectorDefinitions =
createChangeDetectorDefinitions(componentType, strategy, this._genConfig, parsedTemplate);
return changeDetectorDefinitions.map(definition =>
this._createChangeDetectorFactory(definition));
}
private _createChangeDetectorFactory(definition: ChangeDetectorDefinition): Function {
if (IS_DART || !this._genConfig.useJit) {
var proto = new DynamicProtoChangeDetector(definition);
return (dispatcher) => proto.instantiate(dispatcher);
} else {
return new ChangeDetectorJITGenerator(definition, UTIL, ABSTRACT_CHANGE_DETECTOR,
CHANGE_DETECTOR_STATE)
.generate();
}
}
compileComponentCodeGen(componentType: CompileTypeMetadata, strategy: ChangeDetectionStrategy,
parsedTemplate: TemplateAst[]): SourceExpressions {
var changeDetectorDefinitions =
createChangeDetectorDefinitions(componentType, strategy, this._genConfig, parsedTemplate);
var factories = [];
var index = 0;
var sourceParts = changeDetectorDefinitions.map(definition => {
var codegen: any;
var sourcePart;
// TODO(tbosch): move the 2 code generators to the same place, one with .dart and one with .ts
// suffix
// and have the same API for calling them!
if (IS_DART) {
codegen = new Codegen(PREGEN_PROTO_CHANGE_DETECTOR_MODULE);
var className = `_${definition.id}`;
var typeRef = (index === 0 && componentType.isHost) ?
'dynamic' :
`${moduleRef(componentType.moduleUrl)}${componentType.name}`;
codegen.generate(typeRef, className, definition);
factories.push(`(dispatcher) => new ${className}(dispatcher)`);
sourcePart = codegen.toString();
} else {
codegen = new ChangeDetectorJITGenerator(
definition, `${UTIL_MODULE}${UTIL}`,
`${ABSTRACT_CHANGE_DETECTOR_MODULE}${ABSTRACT_CHANGE_DETECTOR}`,
`${CONSTANTS_MODULE}${CHANGE_DETECTOR_STATE}`);
factories.push(`function(dispatcher) { return new ${codegen.typeName}(dispatcher); }`);
sourcePart = codegen.generateSource();
}
index++;
return sourcePart;
});
return new SourceExpressions(sourceParts, factories);
}
}
@@ -0,0 +1,393 @@
import {isPresent, isBlank, Type, isString, StringWrapper} from 'angular2/src/core/facade/lang';
import {SetWrapper, StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
import {
TemplateCmd,
text,
ngContent,
beginElement,
endElement,
beginComponent,
endComponent,
embeddedTemplate,
CompiledTemplate
} from 'angular2/src/core/linker/template_commands';
import {
TemplateAst,
TemplateAstVisitor,
NgContentAst,
EmbeddedTemplateAst,
ElementAst,
VariableAst,
BoundEventAst,
BoundElementPropertyAst,
AttrAst,
BoundTextAst,
TextAst,
DirectiveAst,
BoundDirectivePropertyAst,
templateVisitAll
} from './template_ast';
import {CompileTypeMetadata, CompileDirectiveMetadata} from './directive_metadata';
import {SourceExpressions, SourceExpression, moduleRef} from './source_module';
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';
import {
shimHostAttribute,
shimContentAttribute,
shimContentAttributeExpr,
shimHostAttributeExpr
} from './style_compiler';
import {escapeSingleQuoteString, MODULE_SUFFIX} from './util';
import {Injectable} from 'angular2/src/core/di';
export var TEMPLATE_COMMANDS_MODULE_REF =
moduleRef(`package:angular2/src/core/linker/template_commands${MODULE_SUFFIX}`);
const IMPLICIT_TEMPLATE_VAR = '\$implicit';
const CLASS_ATTR = 'class';
const STYLE_ATTR = 'style';
@Injectable()
export class CommandCompiler {
compileComponentRuntime(component: CompileDirectiveMetadata, appId: string, templateId: number,
template: TemplateAst[], changeDetectorFactories: Function[],
componentTemplateFactory: Function): TemplateCmd[] {
var visitor = new CommandBuilderVisitor(
new RuntimeCommandFactory(component, appId, templateId, componentTemplateFactory,
changeDetectorFactories),
0);
templateVisitAll(visitor, template);
return visitor.result;
}
compileComponentCodeGen(component: CompileDirectiveMetadata, appIdExpr: string,
templateIdExpr: string, template: TemplateAst[],
changeDetectorFactoryExpressions: string[],
componentTemplateFactory: Function): SourceExpression {
var visitor = new CommandBuilderVisitor(
new CodegenCommandFactory(component, appIdExpr, templateIdExpr, componentTemplateFactory,
changeDetectorFactoryExpressions),
0);
templateVisitAll(visitor, template);
var source = `[${visitor.result.join(',')}]`;
return new SourceExpression([], source);
}
}
interface CommandFactory<R> {
createText(value: string, isBound: boolean, ngContentIndex: number): R;
createNgContent(index: number, ngContentIndex: number): R;
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
isBound: boolean, ngContentIndex: number): R;
createEndElement(): R;
createBeginComponent(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
nativeShadow: boolean, ngContentIndex: number): R;
createEndComponent(): R;
createEmbeddedTemplate(embeddedTemplateIndex: number, attrNameAndValues: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
isMerged: boolean, ngContentIndex: number, children: R[]): R;
}
class RuntimeCommandFactory implements CommandFactory<TemplateCmd> {
constructor(private component: CompileDirectiveMetadata, private appId: string,
private templateId: number, private componentTemplateFactory: Function,
private changeDetectorFactories: Function[]) {}
private _mapDirectives(directives: CompileDirectiveMetadata[]): Type[] {
return directives.map(directive => directive.type.runtime);
}
private _addStyleShimAttributes(attrNameAndValues: string[],
localComponent: CompileDirectiveMetadata,
localTemplateId: number): string[] {
var additionalStyles = [];
if (isPresent(localComponent) &&
localComponent.template.encapsulation === ViewEncapsulation.Emulated) {
additionalStyles.push(shimHostAttribute(this.appId, localTemplateId));
additionalStyles.push('');
}
if (this.component.template.encapsulation === ViewEncapsulation.Emulated) {
additionalStyles.push(shimContentAttribute(this.appId, this.templateId));
additionalStyles.push('');
}
return additionalStyles.concat(attrNameAndValues);
}
createText(value: string, isBound: boolean, ngContentIndex: number): TemplateCmd {
return text(value, isBound, ngContentIndex);
}
createNgContent(index: number, ngContentIndex: number): TemplateCmd {
return ngContent(index, ngContentIndex);
}
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
isBound: boolean, ngContentIndex: number): TemplateCmd {
return beginElement(name, this._addStyleShimAttributes(attrNameAndValues, null, null),
eventTargetAndNames, variableNameAndValues, this._mapDirectives(directives),
isBound, ngContentIndex);
}
createEndElement(): TemplateCmd { return endElement(); }
createBeginComponent(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
nativeShadow: boolean, ngContentIndex: number): TemplateCmd {
var nestedTemplate = this.componentTemplateFactory(directives[0]);
return beginComponent(
name, this._addStyleShimAttributes(attrNameAndValues, directives[0], nestedTemplate.id),
eventTargetAndNames, variableNameAndValues, this._mapDirectives(directives), nativeShadow,
ngContentIndex, nestedTemplate);
}
createEndComponent(): TemplateCmd { return endComponent(); }
createEmbeddedTemplate(embeddedTemplateIndex: number, attrNameAndValues: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
isMerged: boolean, ngContentIndex: number,
children: TemplateCmd[]): TemplateCmd {
return embeddedTemplate(attrNameAndValues, variableNameAndValues,
this._mapDirectives(directives), isMerged, ngContentIndex,
this.changeDetectorFactories[embeddedTemplateIndex], children);
}
}
class CodegenCommandFactory implements CommandFactory<string> {
constructor(private component: CompileDirectiveMetadata, private appIdExpr: string,
private templateIdExpr: string, private componentTemplateFactory: Function,
private changeDetectorFactoryExpressions: string[]) {}
private _addStyleShimAttributes(attrNameAndValues: string[],
localComponent: CompileDirectiveMetadata,
localTemplateIdExpr: string): any[] {
var additionalStlyes = [];
if (isPresent(localComponent) &&
localComponent.template.encapsulation === ViewEncapsulation.Emulated) {
additionalStlyes.push(
new Expression(shimHostAttributeExpr(this.appIdExpr, localTemplateIdExpr)));
additionalStlyes.push('');
}
if (this.component.template.encapsulation === ViewEncapsulation.Emulated) {
additionalStlyes.push(
new Expression(shimContentAttributeExpr(this.appIdExpr, this.templateIdExpr)));
additionalStlyes.push('');
}
return additionalStlyes.concat(attrNameAndValues);
}
createText(value: string, isBound: boolean, ngContentIndex: number): string {
return `${TEMPLATE_COMMANDS_MODULE_REF}text(${escapeSingleQuoteString(value)}, ${isBound}, ${ngContentIndex})`;
}
createNgContent(index: number, ngContentIndex: number): string {
return `${TEMPLATE_COMMANDS_MODULE_REF}ngContent(${index}, ${ngContentIndex})`;
}
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
isBound: boolean, ngContentIndex: number): string {
var attrsExpression = codeGenArray(this._addStyleShimAttributes(attrNameAndValues, null, null));
return `${TEMPLATE_COMMANDS_MODULE_REF}beginElement(${escapeSingleQuoteString(name)}, ${attrsExpression}, ${codeGenArray(eventTargetAndNames)}, ${codeGenArray(variableNameAndValues)}, ${codeGenDirectivesArray(directives)}, ${isBound}, ${ngContentIndex})`;
}
createEndElement(): string { return `${TEMPLATE_COMMANDS_MODULE_REF}endElement()`; }
createBeginComponent(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
nativeShadow: boolean, ngContentIndex: number): string {
var nestedCompExpr = this.componentTemplateFactory(directives[0]);
var attrsExpression = codeGenArray(
this._addStyleShimAttributes(attrNameAndValues, directives[0], `${nestedCompExpr}.id`));
return `${TEMPLATE_COMMANDS_MODULE_REF}beginComponent(${escapeSingleQuoteString(name)}, ${attrsExpression}, ${codeGenArray(eventTargetAndNames)}, ${codeGenArray(variableNameAndValues)}, ${codeGenDirectivesArray(directives)}, ${nativeShadow}, ${ngContentIndex}, ${nestedCompExpr})`;
}
createEndComponent(): string { return `${TEMPLATE_COMMANDS_MODULE_REF}endComponent()`; }
createEmbeddedTemplate(embeddedTemplateIndex: number, attrNameAndValues: string[],
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
isMerged: boolean, ngContentIndex: number, children: string[]): string {
return `${TEMPLATE_COMMANDS_MODULE_REF}embeddedTemplate(${codeGenArray(attrNameAndValues)}, ${codeGenArray(variableNameAndValues)}, ` +
`${codeGenDirectivesArray(directives)}, ${isMerged}, ${ngContentIndex}, ${this.changeDetectorFactoryExpressions[embeddedTemplateIndex]}, [${children.join(',')}])`;
}
}
function visitAndReturnContext(visitor: TemplateAstVisitor, asts: TemplateAst[],
context: any): any {
templateVisitAll(visitor, asts, context);
return context;
}
class CommandBuilderVisitor<R> implements TemplateAstVisitor {
result: R[] = [];
transitiveNgContentCount: number = 0;
constructor(public commandFactory: CommandFactory<R>, public embeddedTemplateIndex: number) {}
private _readAttrNameAndValues(directives: CompileDirectiveMetadata[],
attrAsts: TemplateAst[]): string[] {
var attrs = keyValueArrayToMap(visitAndReturnContext(this, attrAsts, []));
directives.forEach(directiveMeta => {
StringMapWrapper.forEach(directiveMeta.hostAttributes, (value, name) => {
var prevValue = attrs[name];
attrs[name] = isPresent(prevValue) ? mergeAttributeValue(name, prevValue, value) : value;
});
});
return mapToKeyValueArray(attrs);
}
visitNgContent(ast: NgContentAst, context: any): any {
this.transitiveNgContentCount++;
this.result.push(this.commandFactory.createNgContent(ast.index, ast.ngContentIndex));
return null;
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
this.embeddedTemplateIndex++;
var childVisitor = new CommandBuilderVisitor(this.commandFactory, this.embeddedTemplateIndex);
templateVisitAll(childVisitor, ast.children);
var isMerged = childVisitor.transitiveNgContentCount > 0;
var variableNameAndValues = [];
ast.vars.forEach((varAst) => {
variableNameAndValues.push(varAst.name);
variableNameAndValues.push(varAst.value.length > 0 ? varAst.value : IMPLICIT_TEMPLATE_VAR);
});
var directives = [];
ListWrapper.forEachWithIndex(ast.directives, (directiveAst: DirectiveAst, index: number) => {
directiveAst.visit(this, new DirectiveContext(index, [], [], directives));
});
this.result.push(this.commandFactory.createEmbeddedTemplate(
this.embeddedTemplateIndex, this._readAttrNameAndValues(directives, ast.attrs),
variableNameAndValues, directives, isMerged, ast.ngContentIndex, childVisitor.result));
this.transitiveNgContentCount += childVisitor.transitiveNgContentCount;
this.embeddedTemplateIndex = childVisitor.embeddedTemplateIndex;
return null;
}
visitElement(ast: ElementAst, context: any): any {
var component = ast.getComponent();
var eventTargetAndNames = visitAndReturnContext(this, ast.outputs, []);
var variableNameAndValues = [];
if (isBlank(component)) {
ast.exportAsVars.forEach((varAst) => {
variableNameAndValues.push(varAst.name);
variableNameAndValues.push(null);
});
}
var directives = [];
ListWrapper.forEachWithIndex(ast.directives, (directiveAst: DirectiveAst, index: number) => {
directiveAst.visit(this, new DirectiveContext(index, eventTargetAndNames,
variableNameAndValues, directives));
});
eventTargetAndNames = removeKeyValueArrayDuplicates(eventTargetAndNames);
var attrNameAndValues = this._readAttrNameAndValues(directives, ast.attrs);
if (isPresent(component)) {
this.result.push(this.commandFactory.createBeginComponent(
ast.name, attrNameAndValues, eventTargetAndNames, variableNameAndValues, directives,
component.template.encapsulation === ViewEncapsulation.Native, ast.ngContentIndex));
templateVisitAll(this, ast.children);
this.result.push(this.commandFactory.createEndComponent());
} else {
this.result.push(this.commandFactory.createBeginElement(
ast.name, attrNameAndValues, eventTargetAndNames, variableNameAndValues, directives,
ast.isBound(), ast.ngContentIndex));
templateVisitAll(this, ast.children);
this.result.push(this.commandFactory.createEndElement());
}
return null;
}
visitVariable(ast: VariableAst, ctx: any): any { return null; }
visitAttr(ast: AttrAst, attrNameAndValues: string[]): any {
attrNameAndValues.push(ast.name);
attrNameAndValues.push(ast.value);
return null;
}
visitBoundText(ast: BoundTextAst, context: any): any {
this.result.push(this.commandFactory.createText(null, true, ast.ngContentIndex));
return null;
}
visitText(ast: TextAst, context: any): any {
this.result.push(this.commandFactory.createText(ast.value, false, ast.ngContentIndex));
return null;
}
visitDirective(ast: DirectiveAst, ctx: DirectiveContext): any {
ctx.targetDirectives.push(ast.directive);
templateVisitAll(this, ast.hostEvents, ctx.eventTargetAndNames);
ast.exportAsVars.forEach(varAst => {
ctx.targetVariableNameAndValues.push(varAst.name);
ctx.targetVariableNameAndValues.push(ctx.index);
});
return null;
}
visitEvent(ast: BoundEventAst, eventTargetAndNames: string[]): any {
eventTargetAndNames.push(ast.target);
eventTargetAndNames.push(ast.name);
return null;
}
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any { return null; }
visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; }
}
function removeKeyValueArrayDuplicates(keyValueArray: string[]): string[] {
var knownPairs = new Set();
var resultKeyValueArray = [];
for (var i = 0; i < keyValueArray.length; i += 2) {
var key = keyValueArray[i];
var value = keyValueArray[i + 1];
var pairId = `${key}:${value}`;
if (!SetWrapper.has(knownPairs, pairId)) {
resultKeyValueArray.push(key);
resultKeyValueArray.push(value);
knownPairs.add(pairId);
}
}
return resultKeyValueArray;
}
function keyValueArrayToMap(keyValueArr: string[]): {[key: string]: string} {
var data: {[key: string]: string} = {};
for (var i = 0; i < keyValueArr.length; i += 2) {
data[keyValueArr[i]] = keyValueArr[i + 1];
}
return data;
}
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]);
keyValueArray.push(entry[1]);
});
return keyValueArray;
}
function mergeAttributeValue(attrName: string, attrValue1: string, attrValue2: string): string {
if (attrName == CLASS_ATTR || attrName == STYLE_ATTR) {
return `${attrValue1} ${attrValue2}`;
} else {
return attrValue2;
}
}
class DirectiveContext {
constructor(public index: number, public eventTargetAndNames: string[],
public targetVariableNameAndValues: any[],
public targetDirectives: CompileDirectiveMetadata[]) {}
}
class Expression {
constructor(public value: string) {}
}
function escapeValue(value: any): string {
if (value instanceof Expression) {
return value.value;
} else if (isString(value)) {
return escapeSingleQuoteString(value);
} else if (isBlank(value)) {
return 'null';
} else {
return `${value}`;
}
}
function codeGenArray(data: any[]): string {
return `[${data.map(escapeValue).join(',')}]`;
}
function codeGenDirectivesArray(directives: CompileDirectiveMetadata[]): string {
var expressions = directives.map(
directiveType => `${moduleRef(directiveType.type.moduleUrl)}${directiveType.type.name}`);
return `[${expressions.join(',')}]`;
}
+56
View File
@@ -0,0 +1,56 @@
import {RuntimeCompiler_} from "./runtime_compiler";
export {TemplateCompiler} from './template_compiler';
export {
CompileDirectiveMetadata,
CompileTypeMetadata,
CompileTemplateMetadata
} from './directive_metadata';
export {SourceModule, SourceWithImports} from './source_module';
export {AMBIENT_DIRECTIVES, AMBIENT_PIPES} from 'angular2/src/core/ambient';
import {assertionsEnabled, Type} from 'angular2/src/core/facade/lang';
import {provide, Provider} from 'angular2/src/core/di';
import {TemplateParser} from 'angular2/src/compiler/template_parser';
import {HtmlParser} from 'angular2/src/compiler/html_parser';
import {TemplateNormalizer} from 'angular2/src/compiler/template_normalizer';
import {RuntimeMetadataResolver} from 'angular2/src/compiler/runtime_metadata';
import {ChangeDetectionCompiler} from 'angular2/src/compiler/change_detector_compiler';
import {StyleCompiler} from 'angular2/src/compiler/style_compiler';
import {CommandCompiler} from 'angular2/src/compiler/command_compiler';
import {TemplateCompiler} from 'angular2/src/compiler/template_compiler';
import {ChangeDetectorGenConfig} from 'angular2/src/core/change_detection/change_detection';
import {Compiler} from 'angular2/src/core/linker/compiler';
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} from 'angular2/src/compiler/url_resolver';
import {AppRootUrl} from 'angular2/src/compiler/app_root_url';
import {AnchorBasedAppRootUrl} from 'angular2/src/compiler/anchor_based_app_root_url';
import {Parser, Lexer} from 'angular2/src/core/change_detection/change_detection';
export function compilerProviders(): Array<Type | Provider | any[]> {
return [
Lexer,
Parser,
HtmlParser,
TemplateParser,
TemplateNormalizer,
RuntimeMetadataResolver,
StyleCompiler,
CommandCompiler,
ChangeDetectionCompiler,
provide(ChangeDetectorGenConfig,
{
useValue:
new ChangeDetectorGenConfig(assertionsEnabled(), assertionsEnabled(), false, true)
}),
TemplateCompiler,
provide(RuntimeCompiler, {useClass: RuntimeCompiler_}),
provide(Compiler, {useExisting: RuntimeCompiler}),
DomElementSchemaRegistry,
provide(ElementSchemaRegistry, {useExisting: DomElementSchemaRegistry}),
AnchorBasedAppRootUrl,
provide(AppRootUrl, {useExisting: AnchorBasedAppRootUrl}),
UrlResolver
];
}
@@ -0,0 +1,274 @@
import {
isPresent,
isBlank,
normalizeBool,
serializeEnum,
Type,
RegExpWrapper,
StringWrapper
} from 'angular2/src/core/facade/lang';
import {StringMapWrapper} from 'angular2/src/core/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} from './util';
import {LifecycleHooks, LIFECYCLE_HOOKS_VALUES} from 'angular2/src/core/linker/interfaces';
// group 1: "property" from "[property]"
// group 2: "event" from "(event)"
var HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))$/g;
export class CompileTypeMetadata {
runtime: Type;
name: string;
moduleUrl: string;
isHost: boolean;
constructor({runtime, name, moduleUrl, isHost}:
{runtime?: Type, name?: string, moduleUrl?: string, isHost?: boolean} = {}) {
this.runtime = runtime;
this.name = name;
this.moduleUrl = moduleUrl;
this.isHost = normalizeBool(isHost);
}
static fromJson(data: {[key: string]: any}): CompileTypeMetadata {
return new CompileTypeMetadata(
{name: data['name'], moduleUrl: data['moduleUrl'], isHost: data['isHost']});
}
toJson(): {[key: string]: any} {
return {
// Note: Runtime type can't be serialized...
'name': this.name,
'moduleUrl': this.moduleUrl,
'isHost': this.isHost
};
}
}
export class CompileTemplateMetadata {
encapsulation: ViewEncapsulation;
template: string;
templateUrl: string;
styles: string[];
styleUrls: string[];
ngContentSelectors: string[];
constructor({encapsulation, template, templateUrl, styles, styleUrls, ngContentSelectors}: {
encapsulation?: ViewEncapsulation,
template?: string,
templateUrl?: string,
styles?: string[],
styleUrls?: string[],
ngContentSelectors?: 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 : [];
}
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']
});
}
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
};
}
}
export class CompileDirectiveMetadata {
static create({type, isComponent, dynamicLoadable, selector, exportAs, changeDetection, inputs,
outputs, host, lifecycleHooks, template}: {
type?: CompileTypeMetadata,
isComponent?: boolean,
dynamicLoadable?: boolean,
selector?: string,
exportAs?: string,
changeDetection?: ChangeDetectionStrategy,
inputs?: string[],
outputs?: string[],
host?: {[key: string]: string},
lifecycleHooks?: LifecycleHooks[],
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),
dynamicLoadable: normalizeBool(dynamicLoadable),
selector: selector,
exportAs: exportAs,
changeDetection: changeDetection,
inputs: inputsMap,
outputs: outputsMap,
hostListeners: hostListeners,
hostProperties: hostProperties,
hostAttributes: hostAttributes,
lifecycleHooks: isPresent(lifecycleHooks) ? lifecycleHooks : [],
template: template
});
}
type: CompileTypeMetadata;
isComponent: boolean;
dynamicLoadable: 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[];
template: CompileTemplateMetadata;
constructor({type, isComponent, dynamicLoadable, selector, exportAs, changeDetection, inputs,
outputs, hostListeners, hostProperties, hostAttributes, lifecycleHooks, template}: {
type?: CompileTypeMetadata,
isComponent?: boolean,
dynamicLoadable?: 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[],
template?: CompileTemplateMetadata
} = {}) {
this.type = type;
this.isComponent = isComponent;
this.dynamicLoadable = dynamicLoadable;
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 = lifecycleHooks;
this.template = template;
}
static fromJson(data: {[key: string]: any}): CompileDirectiveMetadata {
return new CompileDirectiveMetadata({
isComponent: data['isComponent'],
dynamicLoadable: data['dynamicLoadable'],
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']
});
}
toJson(): {[key: string]: any} {
return {
'isComponent': this.isComponent,
'dynamicLoadable': this.dynamicLoadable,
'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
};
}
}
export function createHostComponentMeta(componentType: CompileTypeMetadata,
componentSelector: string): CompileDirectiveMetadata {
var template = CssSelector.parse(componentSelector)[0].getMatchingElementTemplate();
return CompileDirectiveMetadata.create({
type: new CompileTypeMetadata({
runtime: Object,
name: `Host${componentType.name}`,
moduleUrl: componentType.moduleUrl,
isHost: true
}),
template: new CompileTemplateMetadata(
{template: template, templateUrl: '', styles: [], styleUrls: [], ngContentSelectors: []}),
changeDetection: ChangeDetectionStrategy.Default,
inputs: [],
outputs: [],
host: {},
lifecycleHooks: [],
isComponent: true,
dynamicLoadable: false,
selector: '*'
});
}
+39
View File
@@ -0,0 +1,39 @@
import {isPresent} from 'angular2/src/core/facade/lang';
export interface HtmlAst {
sourceInfo: string;
visit(visitor: HtmlAstVisitor, context: any): any;
}
export class HtmlTextAst implements HtmlAst {
constructor(public value: string, public sourceInfo: string) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitText(this, context); }
}
export class HtmlAttrAst implements HtmlAst {
constructor(public name: string, public value: string, public sourceInfo: string) {}
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 sourceInfo: string) {}
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitElement(this, context); }
}
export interface HtmlAstVisitor {
visitElement(ast: HtmlElementAst, context: any): any;
visitAttr(ast: HtmlAttrAst, context: any): any;
visitText(ast: HtmlTextAst, 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;
}
@@ -0,0 +1,120 @@
import {
isPresent,
StringWrapper,
stringify,
assertionsEnabled,
StringJoiner
} from 'angular2/src/core/facade/lang';
import {DOM} from 'angular2/src/core/dom/dom_adapter';
import {
HtmlAst,
HtmlAttrAst,
HtmlTextAst,
HtmlElementAst,
HtmlAstVisitor,
htmlVisitAll
} from './html_ast';
import {escapeDoubleQuoteString} from './util';
import {Injectable} from 'angular2/src/core/di';
@Injectable()
export class HtmlParser {
parse(template: string, sourceInfo: string): HtmlAst[] {
var root = DOM.createTemplate(template);
return parseChildNodes(root, sourceInfo);
}
unparse(nodes: HtmlAst[]): string {
var visitor = new UnparseVisitor();
var parts = [];
htmlVisitAll(visitor, nodes, parts);
return parts.join('');
}
}
function parseText(text: Text, indexInParent: number, parentSourceInfo: string): HtmlTextAst {
// TODO(tbosch): add source row/column source info from parse5 / package:html
var value = DOM.getText(text);
return new HtmlTextAst(value,
`${parentSourceInfo} > #text(${value}):nth-child(${indexInParent})`);
}
function parseAttr(element: Element, parentSourceInfo: string, attrName: string,
attrValue: string): HtmlAttrAst {
// TODO(tbosch): add source row/column source info from parse5 / package:html
var lowerCaseAttrName = attrName.toLowerCase();
return new HtmlAttrAst(lowerCaseAttrName, attrValue,
`${parentSourceInfo}[${lowerCaseAttrName}=${attrValue}]`);
}
function parseElement(element: Element, indexInParent: number,
parentSourceInfo: string): HtmlElementAst {
// normalize nodename always as lower case so that following build steps
// can rely on this
var nodeName = DOM.nodeName(element).toLowerCase();
// TODO(tbosch): add source row/column source info from parse5 / package:html
var sourceInfo = `${parentSourceInfo} > ${nodeName}:nth-child(${indexInParent})`;
var attrs = parseAttrs(element, sourceInfo);
var childNodes = parseChildNodes(element, sourceInfo);
return new HtmlElementAst(nodeName, attrs, childNodes, sourceInfo);
}
function parseAttrs(element: Element, elementSourceInfo: string): HtmlAttrAst[] {
// Note: sort the attributes early in the pipeline to get
// consistent results throughout the pipeline, as attribute order is not defined
// in DOM parsers!
var attrMap = DOM.attributeMap(element);
var attrList: string[][] = [];
attrMap.forEach((value, name) => attrList.push([name, value]));
attrList.sort((entry1, entry2) => StringWrapper.compare(entry1[0], entry2[0]));
return attrList.map(entry => parseAttr(element, elementSourceInfo, entry[0], entry[1]));
}
function parseChildNodes(element: Element, parentSourceInfo: string): HtmlAst[] {
var root = DOM.templateAwareRoot(element);
var childNodes = DOM.childNodesAsList(root);
var result = [];
var index = 0;
childNodes.forEach(childNode => {
var childResult = null;
if (DOM.isTextNode(childNode)) {
var text = <Text>childNode;
childResult = parseText(text, index, parentSourceInfo);
} else if (DOM.isElementNode(childNode)) {
var el = <Element>childNode;
childResult = parseElement(el, index, parentSourceInfo);
}
if (isPresent(childResult)) {
// Won't have a childResult for e.g. comment nodes
result.push(childResult);
}
index++;
});
return result;
}
class UnparseVisitor implements HtmlAstVisitor {
visitElement(ast: HtmlElementAst, parts: string[]): any {
parts.push(`<${ast.name}`);
var attrs = [];
htmlVisitAll(this, ast.attrs, attrs);
if (ast.attrs.length > 0) {
parts.push(' ');
parts.push(attrs.join(' '));
}
parts.push(`>`);
htmlVisitAll(this, ast.children, parts);
parts.push(`</${ast.name}>`);
return null;
}
visitAttr(ast: HtmlAttrAst, parts: string[]): any {
parts.push(`${ast.name}=${escapeDoubleQuoteString(ast.value)}`);
return null;
}
visitText(ast: HtmlTextAst, parts: string[]): any {
parts.push(ast.value);
return null;
}
}
@@ -0,0 +1,27 @@
import {Compiler, Compiler_, internalCreateProtoView} from 'angular2/src/core/linker/compiler';
import {ProtoViewRef} from 'angular2/src/core/linker/view_ref';
import {ProtoViewFactory} from 'angular2/src/core/linker/proto_view_factory';
import {TemplateCompiler} from './template_compiler';
import {Injectable} from 'angular2/src/core/di';
import {Type} from 'angular2/src/core/facade/lang';
import {Promise, PromiseWrapper} from 'angular2/src/core/facade/async';
export abstract class RuntimeCompiler extends Compiler {}
@Injectable()
export class RuntimeCompiler_ extends Compiler_ implements RuntimeCompiler {
constructor(_protoViewFactory: ProtoViewFactory, private _templateCompiler: TemplateCompiler) {
super(_protoViewFactory);
}
compileInHost(componentType: Type): Promise<ProtoViewRef> {
return this._templateCompiler.compileHostComponentRuntime(componentType)
.then(compiledHostTemplate => internalCreateProtoView(this, compiledHostTemplate));
}
clearCache() {
super.clearCache();
this._templateCompiler.clearCache();
}
}
@@ -0,0 +1,123 @@
import {resolveForwardRef} from 'angular2/src/core/di';
import {
Type,
isBlank,
isPresent,
isArray,
stringify,
RegExpWrapper
} from 'angular2/src/core/facade/lang';
import {BaseException} from 'angular2/src/core/facade/exceptions';
import {MapWrapper, StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
import * as cpl from './directive_metadata';
import * as md from 'angular2/src/core/metadata/directives';
import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver';
import {ViewResolver} from 'angular2/src/core/linker/view_resolver';
import {ViewMetadata} from 'angular2/src/core/metadata/view';
import {hasLifecycleHook} from 'angular2/src/core/linker/directive_lifecycle_reflector';
import {LifecycleHooks, LIFECYCLE_HOOKS_VALUES} from 'angular2/src/core/linker/interfaces';
import {reflector} from 'angular2/src/core/reflection/reflection';
import {Injectable, Inject, Optional} from 'angular2/src/core/di';
import {AMBIENT_DIRECTIVES} from 'angular2/src/core/ambient';
import {MODULE_SUFFIX} from './util';
@Injectable()
export class RuntimeMetadataResolver {
private _cache = new Map<Type, cpl.CompileDirectiveMetadata>();
constructor(private _directiveResolver: DirectiveResolver, private _viewResolver: ViewResolver,
@Optional() @Inject(AMBIENT_DIRECTIVES) private _ambientDirectives: Type[]) {}
getMetadata(directiveType: Type): cpl.CompileDirectiveMetadata {
var meta = this._cache.get(directiveType);
if (isBlank(meta)) {
var dirMeta = this._directiveResolver.resolve(directiveType);
var moduleUrl = calcModuleUrl(directiveType, dirMeta);
var templateMeta = null;
var changeDetectionStrategy = null;
if (dirMeta instanceof md.ComponentMetadata) {
var cmpMeta = <md.ComponentMetadata>dirMeta;
var viewMeta = this._viewResolver.resolve(directiveType);
templateMeta = new cpl.CompileTemplateMetadata({
encapsulation: viewMeta.encapsulation,
template: viewMeta.template,
templateUrl: viewMeta.templateUrl,
styles: viewMeta.styles,
styleUrls: viewMeta.styleUrls
});
changeDetectionStrategy = cmpMeta.changeDetection;
}
meta = cpl.CompileDirectiveMetadata.create({
selector: dirMeta.selector,
exportAs: dirMeta.exportAs,
isComponent: isPresent(templateMeta),
dynamicLoadable: true,
type: new cpl.CompileTypeMetadata(
{name: stringify(directiveType), moduleUrl: moduleUrl, runtime: directiveType}),
template: templateMeta,
changeDetection: changeDetectionStrategy,
inputs: dirMeta.inputs,
outputs: dirMeta.outputs,
host: dirMeta.host,
lifecycleHooks: ListWrapper.filter(LIFECYCLE_HOOKS_VALUES,
hook => hasLifecycleHook(hook, directiveType))
});
this._cache.set(directiveType, meta);
}
return meta;
}
getViewDirectivesMetadata(component: Type): cpl.CompileDirectiveMetadata[] {
var view = this._viewResolver.resolve(component);
var directives = flattenDirectives(view, this._ambientDirectives);
for (var i = 0; i < directives.length; i++) {
if (!isValidDirective(directives[i])) {
throw new BaseException(
`Unexpected directive value '${stringify(directives[i])}' on the View of component '${stringify(component)}'`);
}
}
return removeDuplicates(directives).map(type => this.getMetadata(type));
}
}
function removeDuplicates(items: any[]): any[] {
let m = new Map<any, any>();
items.forEach(i => m.set(i, null));
return MapWrapper.keys(m);
}
function flattenDirectives(view: ViewMetadata, ambientDirectives: any[]): Type[] {
let directives = [];
if (isPresent(ambientDirectives)) {
flattenArray(ambientDirectives, directives);
}
if (isPresent(view.directives)) {
flattenArray(view.directives, directives);
}
return directives;
}
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 isValidDirective(value: Type): boolean {
return isPresent(value) && (value instanceof Type);
}
function calcModuleUrl(type: Type, dirMeta: md.DirectiveMetadata): string {
if (isPresent(dirMeta.moduleId)) {
return `package:${dirMeta.moduleId}${MODULE_SUFFIX}`;
} else {
return reflector.importUri(type);
}
}
@@ -0,0 +1,36 @@
import {Injectable} from 'angular2/src/core/di';
import {isPresent, isBlank} from 'angular2/src/core/facade/lang';
import {StringMapWrapper} from 'angular2/src/core/facade/collection';
import {DOM} from 'angular2/src/core/dom/dom_adapter';
import {ElementSchemaRegistry} from './element_schema_registry';
@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)) {
element = DOM.createElement(tagName);
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;
}
}
@@ -0,0 +1,4 @@
export class ElementSchemaRegistry {
hasProperty(tagName: string, propName: string): boolean { return true; }
getMappedPropName(propName: string): string { return propName; }
}
+388
View File
@@ -0,0 +1,388 @@
import {Map, ListWrapper, MapWrapper} from 'angular2/src/core/facade/collection';
import {
isPresent,
isBlank,
RegExpWrapper,
RegExpMatcherWrapper,
StringWrapper
} from 'angular2/src/core/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/core/facade/exceptions';
const _EMPTY_ATTR_VALUE = '';
// 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) {
if (isPresent(element)) {
element = element.toLowerCase();
}
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.toLowerCase());
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 = 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;
}
}
+521
View File
@@ -0,0 +1,521 @@
import {ListWrapper} from 'angular2/src/core/facade/collection';
import {
StringWrapper,
RegExp,
RegExpWrapper,
RegExpMatcherWrapper,
isPresent,
isBlank
} from 'angular2/src/core/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 imlement
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];
p = p.trim();
if (this._selectorNeedsScoping(p, scopeSelector)) {
p = strict && !StringWrapper.contains(p, _polyfillHostNoCombinator) ?
this._applyStrictSelectorScope(p, scopeSelector) :
this._applySelectorScope(p, scopeSelector, hostSelector);
}
r.push(p);
}
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 = [
/>>>/g,
/::shadow/g,
/::content/g,
// Deprecated selectors
// TODO(vicb): see https://github.com/angular/clang-format/issues/16
// clang-format off
/\/deep\//g, // former >>>
/\/shadow-deep\//g, // former /deep/
/\/shadow\//g, // former ::shadow
// clanf-format on
];
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);
}
@@ -0,0 +1,44 @@
import {StringWrapper, isBlank} from 'angular2/src/core/facade/lang';
var MODULE_REGEXP = /#MODULE\[([^\]]*)\]/g;
export function moduleRef(moduleUrl): string {
return `#MODULE[${moduleUrl}]`;
}
export class SourceModule {
constructor(public moduleUrl: string, public sourceWithModuleRefs: string) {}
getSourceWithImports(): SourceWithImports {
var moduleAliases = {};
var imports: string[][] = [];
var newSource =
StringWrapper.replaceAllMapped(this.sourceWithModuleRefs, MODULE_REGEXP, (match) => {
var moduleUrl = match[1];
var alias = moduleAliases[moduleUrl];
if (isBlank(alias)) {
if (moduleUrl == this.moduleUrl) {
alias = '';
} else {
alias = `import${imports.length}`;
imports.push([moduleUrl, alias]);
}
moduleAliases[moduleUrl] = alias;
}
return alias.length > 0 ? `${alias}.` : '';
});
return new SourceWithImports(newSource, imports);
}
}
export class SourceExpression {
constructor(public declarations: string[], public expression: string) {}
}
export class SourceExpressions {
constructor(public declarations: string[], public expressions: string[]) {}
}
export class SourceWithImports {
constructor(public source: string, public imports: string[][]) {}
}
@@ -0,0 +1,150 @@
import {CompileTypeMetadata, CompileTemplateMetadata} from './directive_metadata';
import {SourceModule, SourceExpression, moduleRef} from './source_module';
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';
import {XHR} from 'angular2/src/compiler/xhr';
import {IS_DART, StringWrapper, isBlank} from 'angular2/src/core/facade/lang';
import {PromiseWrapper, Promise} from 'angular2/src/core/facade/async';
import {ShadowCss} from 'angular2/src/compiler/shadow_css';
import {UrlResolver} from 'angular2/src/compiler/url_resolver';
import {extractStyleUrls} from './style_url_resolver';
import {
escapeSingleQuoteString,
codeGenConcatArray,
codeGenMapArray,
codeGenReplaceAll,
codeGenExportVariable,
codeGenToString,
MODULE_SUFFIX
} from './util';
import {Injectable} from 'angular2/src/core/di';
const COMPONENT_VARIABLE = '%COMP%';
var COMPONENT_REGEX = /%COMP%/g;
const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`;
const HOST_ATTR_EXPR = `'_nghost-'+${COMPONENT_VARIABLE}`;
const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`;
const CONTENT_ATTR_EXPR = `'_ngcontent-'+${COMPONENT_VARIABLE}`;
@Injectable()
export class StyleCompiler {
private _styleCache: Map<string, Promise<string[]>> = new Map<string, Promise<string[]>>();
private _shadowCss: ShadowCss = new ShadowCss();
constructor(private _xhr: XHR, private _urlResolver: UrlResolver) {}
compileComponentRuntime(appId: string, templateId: number,
template: CompileTemplateMetadata): Promise<string[]> {
var styles = template.styles;
var styleAbsUrls = template.styleUrls;
return this._loadStyles(styles, styleAbsUrls,
template.encapsulation === ViewEncapsulation.Emulated)
.then(styles => styles.map(style => StringWrapper.replaceAll(
style, COMPONENT_REGEX, componentId(appId, templateId))));
}
compileComponentCodeGen(appIdExpression: string, templateIdExpression: string,
template: CompileTemplateMetadata): SourceExpression {
var shim = template.encapsulation === ViewEncapsulation.Emulated;
var suffix;
if (shim) {
suffix = codeGenMapArray(
['style'],
`style${codeGenReplaceAll(COMPONENT_VARIABLE, componentIdExpression(appIdExpression, templateIdExpression))}`);
} else {
suffix = '';
}
return this._styleCodeGen(template.styles, template.styleUrls, shim, suffix);
}
compileStylesheetCodeGen(stylesheetUrl: string, cssText: string): SourceModule[] {
var styleWithImports = extractStyleUrls(this._urlResolver, stylesheetUrl, cssText);
return [
this._styleModule(
stylesheetUrl, false,
this._styleCodeGen([styleWithImports.style], styleWithImports.styleUrls, false, '')),
this._styleModule(
stylesheetUrl, true,
this._styleCodeGen([styleWithImports.style], styleWithImports.styleUrls, true, ''))
];
}
clearCache() { this._styleCache.clear(); }
private _loadStyles(plainStyles: string[], absUrls: string[],
encapsulate: boolean): Promise<string[]> {
var promises = absUrls.map((absUrl) => {
var cacheKey = `${absUrl}${encapsulate ? '.shim' : ''}`;
var result = this._styleCache.get(cacheKey);
if (isBlank(result)) {
result = this._xhr.get(absUrl).then((style) => {
var styleWithImports = extractStyleUrls(this._urlResolver, absUrl, style);
return this._loadStyles([styleWithImports.style], styleWithImports.styleUrls,
encapsulate);
});
this._styleCache.set(cacheKey, result);
}
return result;
});
return PromiseWrapper.all(promises).then((nestedStyles: string[][]) => {
var result = plainStyles.map(plainStyle => this._shimIfNeeded(plainStyle, encapsulate));
nestedStyles.forEach(styles => styles.forEach(style => result.push(style)));
return result;
});
}
private _styleCodeGen(plainStyles: string[], absUrls: string[], shim: boolean,
suffix: string): SourceExpression {
var expressionSource = `(`;
expressionSource +=
`[${plainStyles.map( plainStyle => escapeSingleQuoteString(this._shimIfNeeded(plainStyle, shim)) ).join(',')}]`;
for (var i = 0; i < absUrls.length; i++) {
var moduleUrl = this._createModuleUrl(absUrls[i], shim);
expressionSource += codeGenConcatArray(`${moduleRef(moduleUrl)}STYLES`);
}
expressionSource += `)${suffix}`;
return new SourceExpression([], expressionSource);
}
private _styleModule(stylesheetUrl: string, shim: boolean,
expression: SourceExpression): SourceModule {
var moduleSource = `
${expression.declarations.join('\n')}
${codeGenExportVariable('STYLES')}${expression.expression};
`;
return new SourceModule(this._createModuleUrl(stylesheetUrl, shim), moduleSource);
}
private _shimIfNeeded(style: string, shim: boolean): string {
return shim ? this._shadowCss.shimCssText(style, CONTENT_ATTR, HOST_ATTR) : style;
}
private _createModuleUrl(stylesheetUrl: string, shim: boolean): string {
return shim ? `${stylesheetUrl}.shim${MODULE_SUFFIX}` : `${stylesheetUrl}${MODULE_SUFFIX}`;
}
}
export function shimContentAttribute(appId: string, templateId: number): string {
return StringWrapper.replaceAll(CONTENT_ATTR, COMPONENT_REGEX, componentId(appId, templateId));
}
export function shimContentAttributeExpr(appIdExpr: string, templateIdExpr: string): string {
return StringWrapper.replaceAll(CONTENT_ATTR_EXPR, COMPONENT_REGEX,
componentIdExpression(appIdExpr, templateIdExpr));
}
export function shimHostAttribute(appId: string, templateId: number): string {
return StringWrapper.replaceAll(HOST_ATTR, COMPONENT_REGEX, componentId(appId, templateId));
}
export function shimHostAttributeExpr(appIdExpr: string, templateIdExpr: string): string {
return StringWrapper.replaceAll(HOST_ATTR_EXPR, COMPONENT_REGEX,
componentIdExpression(appIdExpr, templateIdExpr));
}
function componentId(appId: string, templateId: number): string {
return `${appId}-${templateId}`;
}
function componentIdExpression(appIdExpression: string, templateIdExpression: string): string {
return `${appIdExpression}+'-'+${codeGenToString(templateIdExpression)}`;
}
@@ -0,0 +1,45 @@
// 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/core/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;
@@ -0,0 +1,145 @@
import {AST} from 'angular2/src/core/change_detection/change_detection';
import {isPresent} from 'angular2/src/core/facade/lang';
import {CompileDirectiveMetadata} from './directive_metadata';
export interface TemplateAst {
sourceInfo: string;
visit(visitor: TemplateAstVisitor, context: any): any;
}
export class TextAst implements TemplateAst {
constructor(public value: string, public ngContentIndex: number, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitText(this, context); }
}
export class BoundTextAst implements TemplateAst {
constructor(public value: AST, public ngContentIndex: number, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitBoundText(this, context);
}
}
export class AttrAst implements TemplateAst {
constructor(public name: string, public value: string, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
}
export class BoundElementPropertyAst implements TemplateAst {
constructor(public name: string, public type: PropertyBindingType, public value: AST,
public unit: string, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElementProperty(this, context);
}
}
export class BoundEventAst implements TemplateAst {
constructor(public name: string, public target: string, public handler: AST,
public sourceInfo: string) {}
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;
}
}
}
export class VariableAst implements TemplateAst {
constructor(public name: string, public value: string, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitVariable(this, context);
}
}
export class ElementAst implements TemplateAst {
constructor(public name: string, public attrs: AttrAst[],
public inputs: BoundElementPropertyAst[], public outputs: BoundEventAst[],
public exportAsVars: VariableAst[], public directives: DirectiveAst[],
public children: TemplateAst[], public ngContentIndex: number,
public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElement(this, context);
}
isBound(): boolean {
return (this.inputs.length > 0 || this.outputs.length > 0 || this.exportAsVars.length > 0 ||
this.directives.length > 0);
}
getComponent(): CompileDirectiveMetadata {
return this.directives.length > 0 && this.directives[0].directive.isComponent ?
this.directives[0].directive :
null;
}
}
export class EmbeddedTemplateAst implements TemplateAst {
constructor(public attrs: AttrAst[], public outputs: BoundEventAst[], public vars: VariableAst[],
public directives: DirectiveAst[], public children: TemplateAst[],
public ngContentIndex: number, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitEmbeddedTemplate(this, context);
}
}
export class BoundDirectivePropertyAst implements TemplateAst {
constructor(public directiveName: string, public templateName: string, public value: AST,
public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitDirectiveProperty(this, context);
}
}
export class DirectiveAst implements TemplateAst {
constructor(public directive: CompileDirectiveMetadata,
public inputs: BoundDirectivePropertyAst[],
public hostProperties: BoundElementPropertyAst[], public hostEvents: BoundEventAst[],
public exportAsVars: VariableAst[], public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitDirective(this, context);
}
}
export class NgContentAst implements TemplateAst {
constructor(public index: number, public ngContentIndex: number, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitNgContent(this, context);
}
}
export enum PropertyBindingType {
Property,
Attribute,
Class,
Style
}
export interface TemplateAstVisitor {
visitNgContent(ast: NgContentAst, context: any): any;
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any;
visitElement(ast: ElementAst, 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;
}
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;
}
@@ -0,0 +1,267 @@
import {IS_DART, Type, Json, isBlank, stringify} from 'angular2/src/core/facade/lang';
import {BaseException} from 'angular2/src/core/facade/exceptions';
import {ListWrapper, SetWrapper} from 'angular2/src/core/facade/collection';
import {PromiseWrapper, Promise} from 'angular2/src/core/facade/async';
import {
CompiledTemplate,
TemplateCmd,
nextTemplateId,
CompiledHostTemplate
} from 'angular2/src/core/linker/template_commands';
import {
createHostComponentMeta,
CompileDirectiveMetadata,
CompileTypeMetadata,
CompileTemplateMetadata
} from './directive_metadata';
import {TemplateAst} from './template_ast';
import {Injectable} from 'angular2/src/core/di';
import {SourceModule, moduleRef} from './source_module';
import {ChangeDetectionCompiler} from './change_detector_compiler';
import {StyleCompiler} from './style_compiler';
import {CommandCompiler} from './command_compiler';
import {TemplateParser} from './template_parser';
import {TemplateNormalizer} from './template_normalizer';
import {RuntimeMetadataResolver} from './runtime_metadata';
import {APP_ID} from 'angular2/src/core/application_tokens';
import {TEMPLATE_COMMANDS_MODULE_REF} from './command_compiler';
import {
codeGenExportVariable,
escapeSingleQuoteString,
codeGenValueFn,
MODULE_SUFFIX
} from './util';
import {Inject} from 'angular2/src/core/di';
@Injectable()
export class TemplateCompiler {
private _hostCacheKeys = new Map<Type, any>();
private _compiledTemplateCache = new Map<any, CompiledTemplate>();
private _compiledTemplateDone = new Map<any, Promise<CompiledTemplate>>();
private _appId: string;
constructor(private _runtimeMetadataResolver: RuntimeMetadataResolver,
private _templateNormalizer: TemplateNormalizer,
private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler,
private _commandCompiler: CommandCompiler,
private _cdCompiler: ChangeDetectionCompiler, @Inject(APP_ID) appId: string) {
this._appId = appId;
}
normalizeDirectiveMetadata(directive: CompileDirectiveMetadata):
Promise<CompileDirectiveMetadata> {
if (!directive.isComponent) {
// For non components there is nothing to be normalized yet.
return PromiseWrapper.resolve(directive);
}
return this._templateNormalizer.normalizeTemplate(directive.type, directive.template)
.then((normalizedTemplate: CompileTemplateMetadata) => new CompileDirectiveMetadata({
type: directive.type,
isComponent: directive.isComponent,
dynamicLoadable: directive.dynamicLoadable,
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,
template: normalizedTemplate
}));
}
compileHostComponentRuntime(type: Type): Promise<CompiledHostTemplate> {
var hostCacheKey = this._hostCacheKeys.get(type);
if (isBlank(hostCacheKey)) {
hostCacheKey = new Object();
this._hostCacheKeys.set(type, hostCacheKey);
var compMeta: CompileDirectiveMetadata = this._runtimeMetadataResolver.getMetadata(type);
assertComponent(compMeta);
var hostMeta: CompileDirectiveMetadata =
createHostComponentMeta(compMeta.type, compMeta.selector);
this._compileComponentRuntime(hostCacheKey, hostMeta, [compMeta], new Set());
}
return this._compiledTemplateDone.get(hostCacheKey)
.then(compiledTemplate => new CompiledHostTemplate(() => compiledTemplate));
}
clearCache() {
this._hostCacheKeys.clear();
this._styleCompiler.clearCache();
this._compiledTemplateCache.clear();
this._compiledTemplateDone.clear();
}
private _compileComponentRuntime(cacheKey: any, compMeta: CompileDirectiveMetadata,
viewDirectives: CompileDirectiveMetadata[],
compilingComponentCacheKeys: Set<any>): CompiledTemplate {
var compiledTemplate = this._compiledTemplateCache.get(cacheKey);
var done = this._compiledTemplateDone.get(cacheKey);
if (isBlank(compiledTemplate)) {
var styles;
var changeDetectorFactory;
var commands;
var templateId = nextTemplateId();
compiledTemplate =
new CompiledTemplate(templateId, (_a, _b) => [changeDetectorFactory, commands, styles]);
this._compiledTemplateCache.set(cacheKey, compiledTemplate);
compilingComponentCacheKeys.add(cacheKey);
done =
PromiseWrapper.all([
<any>this._styleCompiler.compileComponentRuntime(this._appId, templateId,
compMeta.template)
].concat(viewDirectives.map(dirMeta =>
this.normalizeDirectiveMetadata(dirMeta))))
.then((stylesAndNormalizedViewDirMetas: any[]) => {
var childPromises = [];
var normalizedViewDirMetas = stylesAndNormalizedViewDirMetas.slice(1);
var parsedTemplate = this._templateParser.parse(
compMeta.template.template, normalizedViewDirMetas, compMeta.type.name);
var changeDetectorFactories = this._cdCompiler.compileComponentRuntime(
compMeta.type, compMeta.changeDetection, parsedTemplate);
changeDetectorFactory = changeDetectorFactories[0];
styles = stylesAndNormalizedViewDirMetas[0];
commands = this._compileCommandsRuntime(compMeta, templateId, parsedTemplate,
changeDetectorFactories,
compilingComponentCacheKeys, childPromises);
return PromiseWrapper.all(childPromises);
})
.then((_) => {
SetWrapper.delete(compilingComponentCacheKeys, cacheKey);
return compiledTemplate;
});
this._compiledTemplateDone.set(cacheKey, done);
}
return compiledTemplate;
}
private _compileCommandsRuntime(compMeta: CompileDirectiveMetadata, templateId: number,
parsedTemplate: TemplateAst[],
changeDetectorFactories: Function[],
compilingComponentCacheKeys: Set<Type>,
childPromises: Promise<any>[]): TemplateCmd[] {
return this._commandCompiler.compileComponentRuntime(
compMeta, this._appId, templateId, parsedTemplate, changeDetectorFactories,
(childComponentDir: CompileDirectiveMetadata) => {
var childCacheKey = childComponentDir.type.runtime;
var childViewDirectives: CompileDirectiveMetadata[] =
this._runtimeMetadataResolver.getViewDirectivesMetadata(
childComponentDir.type.runtime);
var childIsRecursive = SetWrapper.has(compilingComponentCacheKeys, childCacheKey);
var childTemplate = this._compileComponentRuntime(
childCacheKey, childComponentDir, childViewDirectives, compilingComponentCacheKeys);
if (!childIsRecursive) {
// Only wait for a child if it is not a cycle
childPromises.push(this._compiledTemplateDone.get(childCacheKey));
}
return childTemplate;
});
}
compileTemplatesCodeGen(components: NormalizedComponentWithViewDirectives[]): SourceModule {
if (components.length === 0) {
throw new BaseException('No components given');
}
var declarations = [];
var templateArguments = [];
var componentMetas: CompileDirectiveMetadata[] = [];
var templateIdVariable = 'templateId';
var appIdVariable = 'appId';
components.forEach(componentWithDirs => {
var compMeta = <CompileDirectiveMetadata>componentWithDirs.component;
assertComponent(compMeta);
componentMetas.push(compMeta);
this._processTemplateCodeGen(compMeta, appIdVariable, templateIdVariable,
<CompileDirectiveMetadata[]>componentWithDirs.directives,
declarations, templateArguments);
if (compMeta.dynamicLoadable) {
var hostMeta = createHostComponentMeta(compMeta.type, compMeta.selector);
componentMetas.push(hostMeta);
this._processTemplateCodeGen(hostMeta, appIdVariable, templateIdVariable, [compMeta],
declarations, templateArguments);
}
});
ListWrapper.forEachWithIndex(componentMetas, (compMeta: CompileDirectiveMetadata,
index: number) => {
var templateDataFn = codeGenValueFn([appIdVariable, templateIdVariable],
`[${(<any[]>templateArguments[index]).join(',')}]`);
var compiledTemplateExpr =
`new ${TEMPLATE_COMMANDS_MODULE_REF}CompiledTemplate(${TEMPLATE_COMMANDS_MODULE_REF}nextTemplateId(),${templateDataFn})`;
var variableValueExpr;
if (compMeta.type.isHost) {
var factoryName = `_hostTemplateFactory${index}`;
declarations.push(`${codeGenValueFn([], compiledTemplateExpr, factoryName)};`);
var constructionKeyword = IS_DART ? 'const' : 'new';
variableValueExpr =
`${constructionKeyword} ${TEMPLATE_COMMANDS_MODULE_REF}CompiledHostTemplate(${factoryName})`;
} else {
variableValueExpr = compiledTemplateExpr;
}
declarations.push(
`${codeGenExportVariable(templateVariableName(compMeta.type), compMeta.type.isHost)}${variableValueExpr};`);
});
var moduleUrl = components[0].component.type.moduleUrl;
return new SourceModule(`${templateModuleUrl(moduleUrl)}`, declarations.join('\n'));
}
compileStylesheetCodeGen(stylesheetUrl: string, cssText: string): SourceModule[] {
return this._styleCompiler.compileStylesheetCodeGen(stylesheetUrl, cssText);
}
private _processTemplateCodeGen(compMeta: CompileDirectiveMetadata, appIdExpr: string,
templateIdExpr: string, directives: CompileDirectiveMetadata[],
targetDeclarations: string[], targetTemplateArguments: any[][]) {
var styleExpr =
this._styleCompiler.compileComponentCodeGen(appIdExpr, templateIdExpr, compMeta.template);
var parsedTemplate =
this._templateParser.parse(compMeta.template.template, directives, compMeta.type.name);
var changeDetectorsExprs = this._cdCompiler.compileComponentCodeGen(
compMeta.type, compMeta.changeDetection, parsedTemplate);
var commandsExpr = this._commandCompiler.compileComponentCodeGen(
compMeta, appIdExpr, templateIdExpr, parsedTemplate, changeDetectorsExprs.expressions,
codeGenComponentTemplateFactory);
addAll(styleExpr.declarations, targetDeclarations);
addAll(changeDetectorsExprs.declarations, targetDeclarations);
addAll(commandsExpr.declarations, targetDeclarations);
targetTemplateArguments.push(
[changeDetectorsExprs.expressions[0], commandsExpr.expression, styleExpr.expression]);
}
}
export class NormalizedComponentWithViewDirectives {
constructor(public component: CompileDirectiveMetadata,
public directives: CompileDirectiveMetadata[]) {}
}
function assertComponent(meta: CompileDirectiveMetadata) {
if (!meta.isComponent) {
throw new BaseException(`Could not compile '${meta.type.name}' because it is not a component.`);
}
}
function templateVariableName(type: CompileTypeMetadata): string {
return `${type.name}Template`;
}
function templateModuleUrl(moduleUrl: string): string {
var urlWithoutSuffix = moduleUrl.substring(0, moduleUrl.length - MODULE_SUFFIX.length);
return `${urlWithoutSuffix}.template${MODULE_SUFFIX}`;
}
function addAll(source: any[], target: any[]) {
for (var i = 0; i < source.length; i++) {
target.push(source[i]);
}
}
function codeGenComponentTemplateFactory(nestedCompType: CompileDirectiveMetadata): string {
return `${moduleRef(templateModuleUrl(nestedCompType.type.moduleUrl))}${templateVariableName(nestedCompType.type)}`;
}
@@ -0,0 +1,123 @@
import {
CompileTypeMetadata,
CompileDirectiveMetadata,
CompileTemplateMetadata
} from './directive_metadata';
import {isPresent, isBlank} from 'angular2/src/core/facade/lang';
import {ListWrapper} from 'angular2/src/core/facade/collection';
import {BaseException} from 'angular2/src/core/facade/exceptions';
import {Promise, PromiseWrapper} from 'angular2/src/core/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,
htmlVisitAll
} from './html_ast';
import {HtmlParser} from './html_parser';
import {preparseElement, PreparsedElement, PreparsedElementType} from './template_preparser';
@Injectable()
export class TemplateNormalizer {
constructor(private _xhr: XHR, private _urlResolver: UrlResolver,
private _domParser: HtmlParser) {}
normalizeTemplate(directiveType: CompileTypeMetadata,
template: CompileTemplateMetadata): Promise<CompileTemplateMetadata> {
if (isPresent(template.template)) {
return PromiseWrapper.resolve(this.normalizeLoadedTemplate(
directiveType, template, template.template, directiveType.moduleUrl));
} else if (isPresent(template.templateUrl)) {
var sourceAbsUrl = this._urlResolver.resolve(directiveType.moduleUrl, 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 domNodes = this._domParser.parse(template, directiveType.name);
var visitor = new TemplatePreparseVisitor();
htmlVisitAll(visitor, domNodes);
var allStyles = templateMeta.styles.concat(visitor.styles);
var allStyleAbsUrls =
ListWrapper.filter(visitor.styleUrls, isStyleUrlResolvable)
.map(url => this._urlResolver.resolve(templateAbsUrl, url))
.concat(ListWrapper.filter(templateMeta.styleUrls, isStyleUrlResolvable)
.map(url => this._urlResolver.resolve(directiveType.moduleUrl, 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;
}
if (preparsedElement.nonBindable) {
this.ngNonBindableStackCount++;
}
htmlVisitAll(this, ast.children);
if (preparsedElement.nonBindable) {
this.ngNonBindableStackCount--;
}
return null;
}
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
visitText(ast: HtmlTextAst, context: any): any { return null; }
}
@@ -0,0 +1,685 @@
import {
MapWrapper,
ListWrapper,
StringMapWrapper,
SetWrapper
} from 'angular2/src/core/facade/collection';
import {
RegExpWrapper,
isPresent,
StringWrapper,
StringJoiner,
stringify,
assertionsEnabled,
isBlank
} from 'angular2/src/core/facade/lang';
import {Injectable} from 'angular2/src/core/di';
import {BaseException} from 'angular2/src/core/facade/exceptions';
import {Parser, AST, ASTWithSource} from 'angular2/src/core/change_detection/change_detection';
import {TemplateBinding} from 'angular2/src/core/change_detection/parser/ast';
import {CompileDirectiveMetadata} from './directive_metadata';
import {HtmlParser} from './html_parser';
import {
ElementAst,
BoundElementPropertyAst,
BoundEventAst,
VariableAst,
TemplateAst,
TextAst,
BoundTextAst,
EmbeddedTemplateAst,
AttrAst,
NgContentAst,
PropertyBindingType,
DirectiveAst,
BoundDirectivePropertyAst
} 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,
htmlVisitAll
} from './html_ast';
import {dashCaseToCamelCase, camelCaseToDashCase, splitAtColon} from './util';
// Group 1 = "bind-"
// Group 2 = "var-" or "#"
// Group 3 = "on-"
// Group 4 = "bindon-"
// Group 5 = the identifier after "bind-", "var-/#", or "on-"
// Group 6 = idenitifer inside [()]
// Group 7 = idenitifer inside []
// Group 8 = identifier inside ()
var BIND_NAME_REGEXP =
/^(?:(?:(?:(bind-)|(var-|#)|(on-)|(bindon-))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g;
const TEMPLATE_ELEMENT = 'template';
const TEMPLATE_ATTR = 'template';
const TEMPLATE_ATTR_PREFIX = '*';
const CLASS_ATTR = 'class';
var PROPERTY_PARTS_SEPARATOR = new RegExp('\\.');
const ATTRIBUTE_PREFIX = 'attr';
const CLASS_PREFIX = 'class';
const STYLE_PREFIX = 'style';
var TEXT_CSS_SELECTOR = CssSelector.parse('*')[0];
@Injectable()
export class TemplateParser {
constructor(private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry,
private _htmlParser: HtmlParser) {}
parse(template: string, directives: CompileDirectiveMetadata[],
sourceInfo: string): TemplateAst[] {
var parseVisitor = new TemplateParseVisitor(directives, this._exprParser, this._schemaRegistry);
var result =
htmlVisitAll(parseVisitor, this._htmlParser.parse(template, sourceInfo), EMPTY_COMPONENT);
if (parseVisitor.errors.length > 0) {
var errorString = parseVisitor.errors.join('\n');
throw new BaseException(`Template parse errors:\n${errorString}`);
}
return result;
}
}
class TemplateParseVisitor implements HtmlAstVisitor {
selectorMatcher: SelectorMatcher;
errors: string[] = [];
directivesIndex = new Map<CompileDirectiveMetadata, number>();
ngContentCount: number = 0;
constructor(directives: CompileDirectiveMetadata[], 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);
});
}
private _reportError(message: string) { this.errors.push(message); }
private _parseInterpolation(value: string, sourceInfo: string): ASTWithSource {
try {
return this._exprParser.parseInterpolation(value, sourceInfo);
} catch (e) {
this._reportError(`${e}`); // sourceInfo is already contained in the AST
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
}
}
private _parseAction(value: string, sourceInfo: string): ASTWithSource {
try {
return this._exprParser.parseAction(value, sourceInfo);
} catch (e) {
this._reportError(`${e}`); // sourceInfo is already contained in the AST
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
}
}
private _parseBinding(value: string, sourceInfo: string): ASTWithSource {
try {
return this._exprParser.parseBinding(value, sourceInfo);
} catch (e) {
this._reportError(`${e}`); // sourceInfo is already contained in the AST
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
}
}
private _parseTemplateBindings(value: string, sourceInfo: string): TemplateBinding[] {
try {
return this._exprParser.parseTemplateBindings(value, sourceInfo);
} catch (e) {
this._reportError(`${e}`); // sourceInfo is already contained in the AST
return [];
}
}
visitText(ast: HtmlTextAst, component: Component): any {
var ngContentIndex = component.findNgContentIndex(TEXT_CSS_SELECTOR);
var expr = this._parseInterpolation(ast.value, ast.sourceInfo);
if (isPresent(expr)) {
return new BoundTextAst(expr, ngContentIndex, ast.sourceInfo);
} else {
return new TextAst(ast.value, ngContentIndex, ast.sourceInfo);
}
}
visitAttr(ast: HtmlAttrAst, contex: any): any {
return new AttrAst(ast.name, ast.value, ast.sourceInfo);
}
visitElement(element: HtmlElementAst, component: Component): 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 vars: VariableAst[] = [];
var events: BoundEventAst[] = [];
var templateElementOrDirectiveProps: BoundElementOrDirectiveProperty[] = [];
var templateVars: VariableAst[] = [];
var templateMatchableAttrs: string[][] = [];
var hasInlineTemplates = false;
var attrs = [];
element.attrs.forEach(attr => {
matchableAttrs.push([attr.name, attr.value]);
var hasBinding = this._parseAttr(attr, matchableAttrs, elementOrDirectiveProps, events, vars);
var hasTemplateBinding = this._parseInlineTemplateBinding(
attr, templateMatchableAttrs, templateElementOrDirectiveProps, templateVars);
if (!hasBinding && !hasTemplateBinding) {
// don't include the bindings as attributes as well in the AST
attrs.push(this.visitAttr(attr, null));
}
if (hasTemplateBinding) {
hasInlineTemplates = true;
}
});
var isTemplateElement = nodeName == TEMPLATE_ELEMENT;
var elementCssSelector = createElementCssSelector(nodeName, matchableAttrs);
var directives = this._createDirectiveAsts(
element.name, this._parseDirectives(this.selectorMatcher, elementCssSelector),
elementOrDirectiveProps, isTemplateElement ? [] : vars, element.sourceInfo);
var elementProps: BoundElementPropertyAst[] =
this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directives);
var children = htmlVisitAll(preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this,
element.children, Component.create(directives));
var elementNgContentIndex =
hasInlineTemplates ? null : component.findNgContentIndex(elementCssSelector);
var parsedElement;
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
parsedElement =
new NgContentAst(this.ngContentCount++, elementNgContentIndex, element.sourceInfo);
} else if (isTemplateElement) {
this._assertAllEventsPublishedByDirectives(directives, events, element.sourceInfo);
this._assertNoComponentsNorElementBindingsOnTemplate(directives, elementProps,
element.sourceInfo);
parsedElement = new EmbeddedTemplateAst(attrs, events, vars, directives, children,
elementNgContentIndex, element.sourceInfo);
} else {
this._assertOnlyOneComponent(directives, element.sourceInfo);
var elementExportAsVars = ListWrapper.filter(vars, varAst => varAst.value.length === 0);
parsedElement =
new ElementAst(nodeName, attrs, elementProps, events, elementExportAsVars, directives,
children, elementNgContentIndex, element.sourceInfo);
}
if (hasInlineTemplates) {
var templateCssSelector = createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs);
var templateDirectives = this._createDirectiveAsts(
element.name, this._parseDirectives(this.selectorMatcher, templateCssSelector),
templateElementOrDirectiveProps, [], element.sourceInfo);
var templateElementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts(
element.name, templateElementOrDirectiveProps, templateDirectives);
this._assertNoComponentsNorElementBindingsOnTemplate(templateDirectives, templateElementProps,
element.sourceInfo);
parsedElement = new EmbeddedTemplateAst(
[], [], templateVars, templateDirectives, [parsedElement],
component.findNgContentIndex(templateCssSelector), element.sourceInfo);
}
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 (StringWrapper.startsWith(attr.name, TEMPLATE_ATTR_PREFIX)) {
var key = StringWrapper.substring(attr.name, 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.sourceInfo);
for (var i = 0; i < bindings.length; i++) {
var binding = bindings[i];
var dashCaseKey = camelCaseToDashCase(binding.key);
if (binding.keyIsVar) {
targetVars.push(
new VariableAst(dashCaseToCamelCase(binding.key), binding.name, attr.sourceInfo));
targetMatchableAttrs.push([dashCaseKey, binding.name]);
} else if (isPresent(binding.expression)) {
this._parsePropertyAst(dashCaseKey, binding.expression, attr.sourceInfo,
targetMatchableAttrs, targetProps);
} else {
targetMatchableAttrs.push([dashCaseKey, '']);
this._parseLiteralAttr(dashCaseKey, null, attr.sourceInfo, targetProps);
}
}
return true;
}
return false;
}
private _parseAttr(attr: HtmlAttrAst, targetMatchableAttrs: string[][],
targetProps: BoundElementOrDirectiveProperty[], targetEvents: BoundEventAst[],
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[5], attrValue, attr.sourceInfo, targetMatchableAttrs,
targetProps);
} else if (isPresent(
bindParts[2])) { // match: var-name / var-name="iden" / #name / #name="iden"
var identifier = bindParts[5];
this._parseVariable(identifier, attrValue, attr.sourceInfo, targetVars);
} else if (isPresent(bindParts[3])) { // match: on-event
this._parseEvent(bindParts[5], attrValue, attr.sourceInfo, targetMatchableAttrs,
targetEvents);
} else if (isPresent(bindParts[4])) { // match: bindon-prop
this._parseProperty(bindParts[5], attrValue, attr.sourceInfo, targetMatchableAttrs,
targetProps);
this._parseAssignmentEvent(bindParts[5], attrValue, attr.sourceInfo, targetMatchableAttrs,
targetEvents);
} else if (isPresent(bindParts[6])) { // match: [(expr)]
this._parseProperty(bindParts[6], attrValue, attr.sourceInfo, targetMatchableAttrs,
targetProps);
this._parseAssignmentEvent(bindParts[6], attrValue, attr.sourceInfo, targetMatchableAttrs,
targetEvents);
} else if (isPresent(bindParts[7])) { // match: [expr]
this._parseProperty(bindParts[7], attrValue, attr.sourceInfo, targetMatchableAttrs,
targetProps);
} else if (isPresent(bindParts[8])) { // match: (event)
this._parseEvent(bindParts[8], attrValue, attr.sourceInfo, targetMatchableAttrs,
targetEvents);
}
} else {
hasBinding = this._parsePropertyInterpolation(attrName, attrValue, attr.sourceInfo,
targetMatchableAttrs, targetProps);
}
if (!hasBinding) {
this._parseLiteralAttr(attrName, attrValue, attr.sourceInfo, targetProps);
}
return hasBinding;
}
private _normalizeAttributeName(attrName: string): string {
return StringWrapper.startsWith(attrName, 'data-') ? StringWrapper.substring(attrName, 5) :
attrName;
}
private _parseVariable(identifier: string, value: string, sourceInfo: any,
targetVars: VariableAst[]) {
targetVars.push(new VariableAst(dashCaseToCamelCase(identifier), value, sourceInfo));
}
private _parseProperty(name: string, expression: string, sourceInfo: any,
targetMatchableAttrs: string[][],
targetProps: BoundElementOrDirectiveProperty[]) {
this._parsePropertyAst(name, this._parseBinding(expression, sourceInfo), sourceInfo,
targetMatchableAttrs, targetProps);
}
private _parsePropertyInterpolation(name: string, value: string, sourceInfo: any,
targetMatchableAttrs: string[][],
targetProps: BoundElementOrDirectiveProperty[]): boolean {
var expr = this._parseInterpolation(value, sourceInfo);
if (isPresent(expr)) {
this._parsePropertyAst(name, expr, sourceInfo, targetMatchableAttrs, targetProps);
return true;
}
return false;
}
private _parsePropertyAst(name: string, ast: ASTWithSource, sourceInfo: any,
targetMatchableAttrs: string[][],
targetProps: BoundElementOrDirectiveProperty[]) {
targetMatchableAttrs.push([name, ast.source]);
targetProps.push(new BoundElementOrDirectiveProperty(name, ast, false, sourceInfo));
}
private _parseAssignmentEvent(name: string, expression: string, sourceInfo: string,
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
this._parseEvent(`${name}-change`, `${expression}=$event`, sourceInfo, targetMatchableAttrs,
targetEvents);
}
private _parseEvent(name: string, expression: string, sourceInfo: string,
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
// long format: 'target: eventName'
var parts = splitAtColon(name, [null, name]);
var target = parts[0];
var eventName = parts[1];
targetEvents.push(new BoundEventAst(dashCaseToCamelCase(eventName), target,
this._parseAction(expression, sourceInfo), sourceInfo));
// 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, sourceInfo: string,
targetProps: BoundElementOrDirectiveProperty[]) {
targetProps.push(new BoundElementOrDirectiveProperty(
dashCaseToCamelCase(name), this._exprParser.wrapLiteralPrimitive(value, sourceInfo), true,
sourceInfo));
}
private _parseDirectives(selectorMatcher: SelectorMatcher,
elementCssSelector: CssSelector): CompileDirectiveMetadata[] {
var directives = [];
selectorMatcher.match(elementCssSelector,
(selector, directive) => { directives.push(directive); });
// Need to sort the directives so that we get consistent results throughout,
// as selectorMatcher uses Maps inside.
// Also need to make components the first directive in the array
ListWrapper.sort(directives,
(dir1: CompileDirectiveMetadata, dir2: CompileDirectiveMetadata) => {
var dir1Comp = dir1.isComponent;
var dir2Comp = dir2.isComponent;
if (dir1Comp && !dir2Comp) {
return -1;
} else if (!dir1Comp && dir2Comp) {
return 1;
} else {
return this.directivesIndex.get(dir1) - this.directivesIndex.get(dir2);
}
});
return directives;
}
private _createDirectiveAsts(elementName: string, directives: CompileDirectiveMetadata[],
props: BoundElementOrDirectiveProperty[],
possibleExportAsVars: VariableAst[],
sourceInfo: string): DirectiveAst[] {
var matchedVariables = new Set<string>();
var directiveAsts = directives.map((directive: CompileDirectiveMetadata) => {
var hostProperties: BoundElementPropertyAst[] = [];
var hostEvents: BoundEventAst[] = [];
var directiveProperties: BoundDirectivePropertyAst[] = [];
this._createDirectiveHostPropertyAsts(elementName, directive.hostProperties, sourceInfo,
hostProperties);
this._createDirectiveHostEventAsts(directive.hostListeners, sourceInfo, hostEvents);
this._createDirectivePropertyAsts(directive.inputs, props, directiveProperties);
var exportAsVars = [];
possibleExportAsVars.forEach((varAst) => {
if ((varAst.value.length === 0 && directive.isComponent) ||
(directive.exportAs == varAst.value)) {
exportAsVars.push(varAst);
matchedVariables.add(varAst.name);
}
});
return new DirectiveAst(directive, directiveProperties, hostProperties, hostEvents,
exportAsVars, sourceInfo);
});
possibleExportAsVars.forEach((varAst) => {
if (varAst.value.length > 0 && !SetWrapper.has(matchedVariables, varAst.name)) {
this._reportError(
`There is no directive with "exportAs" set to "${varAst.value}" at ${varAst.sourceInfo}`);
}
});
return directiveAsts;
}
private _createDirectiveHostPropertyAsts(elementName: string, hostProps: {[key: string]: string},
sourceInfo: string,
targetPropertyAsts: BoundElementPropertyAst[]) {
if (isPresent(hostProps)) {
StringMapWrapper.forEach(hostProps, (expression, propName) => {
var exprAst = this._parseBinding(expression, sourceInfo);
targetPropertyAsts.push(
this._createElementPropertyAst(elementName, propName, exprAst, sourceInfo));
});
}
}
private _createDirectiveHostEventAsts(hostListeners: {[key: string]: string}, sourceInfo: string,
targetEventAsts: BoundEventAst[]) {
if (isPresent(hostListeners)) {
StringMapWrapper.forEach(hostListeners, (expression, propName) => {
this._parseEvent(propName, expression, sourceInfo, [], targetEventAsts);
});
}
}
private _createDirectivePropertyAsts(directiveProperties: {[key: string]: string},
boundProps: BoundElementOrDirectiveProperty[],
targetBoundDirectiveProps: BoundDirectivePropertyAst[]) {
if (isPresent(directiveProperties)) {
var boundPropsByName = new Map<string, BoundElementOrDirectiveProperty>();
boundProps.forEach(boundProp => {
var key = dashCaseToCamelCase(boundProp.name);
var prevValue = boundPropsByName.get(boundProp.name);
if (isBlank(prevValue) || prevValue.isLiteral) {
// give [a]="b" a higher precedence thatn a="b" on the same element
boundPropsByName.set(key, boundProp);
}
});
StringMapWrapper.forEach(directiveProperties, (elProp: string, dirProp: string) => {
elProp = dashCaseToCamelCase(elProp);
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.sourceInfo));
}
});
}
}
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.sourceInfo));
}
});
return boundElementProps;
}
private _createElementPropertyAst(elementName: string, name: string, ast: AST,
sourceInfo: any): BoundElementPropertyAst {
var unit = null;
var bindingType;
var boundPropertyName;
var parts = StringWrapper.split(name, PROPERTY_PARTS_SEPARATOR);
if (parts.length === 1) {
boundPropertyName = this._schemaRegistry.getMappedPropName(dashCaseToCamelCase(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 in ${sourceInfo}`);
}
} else if (parts[0] == ATTRIBUTE_PREFIX) {
boundPropertyName = dashCaseToCamelCase(parts[1]);
bindingType = PropertyBindingType.Attribute;
} else if (parts[0] == CLASS_PREFIX) {
// keep original case!
boundPropertyName = parts[1];
bindingType = PropertyBindingType.Class;
} else if (parts[0] == STYLE_PREFIX) {
unit = parts.length > 2 ? parts[2] : null;
boundPropertyName = dashCaseToCamelCase(parts[1]);
bindingType = PropertyBindingType.Style;
} else {
this._reportError(`Invalid property name ${name} in ${sourceInfo}`);
bindingType = null;
}
return new BoundElementPropertyAst(boundPropertyName, bindingType, ast, unit, sourceInfo);
}
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[], sourceInfo: string) {
var componentTypeNames = this._findComponentDirectiveNames(directives);
if (componentTypeNames.length > 1) {
this._reportError(
`More than one component: ${componentTypeNames.join(',')} in ${sourceInfo}`);
}
}
private _assertNoComponentsNorElementBindingsOnTemplate(directives: DirectiveAst[],
elementProps: BoundElementPropertyAst[],
sourceInfo: string) {
var componentTypeNames: string[] = this._findComponentDirectiveNames(directives);
if (componentTypeNames.length > 0) {
this._reportError(
`Components on an embedded template: ${componentTypeNames.join(',')} in ${sourceInfo}`);
}
elementProps.forEach(prop => {
this._reportError(
`Property binding ${prop.name} not used by any directive on an embedded template in ${prop.sourceInfo}`);
});
}
private _assertAllEventsPublishedByDirectives(directives: DirectiveAst[], events: BoundEventAst[],
sourceInfo: string) {
var allDirectiveEvents = new Set<string>();
directives.forEach(directive => {
StringMapWrapper.forEach(directive.directive.outputs,
(eventName, _) => { 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 in ${sourceInfo}`);
}
});
}
}
class NonBindableVisitor implements HtmlAstVisitor {
visitElement(ast: HtmlElementAst, component: Component): 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 = component.findNgContentIndex(selector);
var children = htmlVisitAll(this, ast.children, EMPTY_COMPONENT);
return new ElementAst(ast.name, htmlVisitAll(this, ast.attrs), [], [], [], [], children,
ngContentIndex, ast.sourceInfo);
}
visitAttr(ast: HtmlAttrAst, context: any): AttrAst {
return new AttrAst(ast.name, ast.value, ast.sourceInfo);
}
visitText(ast: HtmlTextAst, component: Component): TextAst {
var ngContentIndex = component.findNgContentIndex(TEXT_CSS_SELECTOR);
return new TextAst(ast.value, ngContentIndex, ast.sourceInfo);
}
}
class BoundElementOrDirectiveProperty {
constructor(public name: string, public expression: AST, public isLiteral: boolean,
public sourceInfo: string) {}
}
export function splitClasses(classAttrValue: string): string[] {
return StringWrapper.split(classAttrValue.trim(), /\s+/g);
}
class Component {
static create(directives: DirectiveAst[]): Component {
if (directives.length === 0 || !directives[0].directive.isComponent) {
return EMPTY_COMPONENT;
}
var matcher = new SelectorMatcher();
var ngContentSelectors = directives[0].directive.template.ngContentSelectors;
var wildcardNgContentIndex = null;
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 Component(matcher, wildcardNgContentIndex);
}
constructor(public ngContentIndexMatcher: SelectorMatcher,
public wildcardNgContentIndex: number) {}
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();
cssSelector.setElement(elementName);
for (var i = 0; i < matchableAttrs.length; i++) {
var attrName = matchableAttrs[i][0].toLowerCase();
var attrValue = matchableAttrs[i][1];
cssSelector.addAttribute(attrName, attrValue);
if (attrName == CLASS_ATTR) {
var classes = splitClasses(attrValue);
classes.forEach(className => cssSelector.addClassName(className));
}
}
return cssSelector;
}
var EMPTY_COMPONENT = new Component(new SelectorMatcher(), null);
var NON_BINDABLE_VISITOR = new NonBindableVisitor();
@@ -0,0 +1,64 @@
import {HtmlElementAst} from './html_ast';
import {isBlank, isPresent} from 'angular2/src/core/facade/lang';
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 = 'ng-non-bindable';
export function preparseElement(ast: HtmlElementAst): PreparsedElement {
var selectAttr = null;
var hrefAttr = null;
var relAttr = null;
var nonBindable = false;
ast.attrs.forEach(attr => {
if (attr.name == NG_CONTENT_SELECT_ATTR) {
selectAttr = attr.value;
} else if (attr.name == LINK_STYLE_HREF_ATTR) {
hrefAttr = attr.value;
} else if (attr.name == LINK_STYLE_REL_ATTR) {
relAttr = attr.value;
} else if (attr.name == NG_NON_BINDABLE_ATTR) {
nonBindable = true;
}
});
selectAttr = normalizeNgContentSelect(selectAttr);
var nodeName = ast.name;
var type = PreparsedElementType.OTHER;
if (nodeName == 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);
}
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) {}
}
function normalizeNgContentSelect(selectAttr: string): string {
if (isBlank(selectAttr) || selectAttr.length === 0) {
return '*';
}
return selectAttr;
}
@@ -0,0 +1,46 @@
library angular2.src.services.url_resolver;
import 'package:angular2/src/core/di.dart' show Injectable, Provider;
UrlResolver createWithoutPackagePrefix() {
return new UrlResolver.withUrlPrefix(null);
}
@Injectable()
class UrlResolver {
/// This will be the location where 'package:' Urls will resolve. Default is
/// '/packages'
final String _packagePrefix;
const UrlResolver() : _packagePrefix = '/packages';
/// 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 (!uri.isAbsolute) {
Uri baseUri = Uri.parse(baseUrl);
uri = baseUri.resolveUri(uri);
}
if (_packagePrefix != null && uri.scheme == 'package') {
return '$_packagePrefix/${uri.path}';
} else {
return uri.toString();
}
}
}
@@ -0,0 +1,300 @@
import {Injectable} from 'angular2/src/core/di';
import {isPresent, isBlank, RegExpWrapper, normalizeBlank} from 'angular2/src/core/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/core/facade/exceptions';
import {ListWrapper} from 'angular2/src/core/facade/collection';
export function createWithoutPackagePrefix(): UrlResolver {
return new UrlResolver();
}
/**
* Used by the {@link Compiler} when resolving HTML and CSS template URLs.
*
* This interface can be overridden by the application developer to create custom behavior.
*
* See {@link Compiler}
*/
@Injectable()
export class UrlResolver {
/**
* 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 { return _resolveUrl(baseUrl, url); }
}
// 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);
}
+98
View File
@@ -0,0 +1,98 @@
import {IS_DART, StringWrapper, isBlank} from 'angular2/src/core/facade/lang';
var CAMEL_CASE_REGEXP = /([A-Z])/g;
var DASH_CASE_REGEXP = /-([a-z])/g;
var SINGLE_QUOTE_ESCAPE_STRING_RE = /'|\\|\n|\$/g;
var DOUBLE_QUOTE_ESCAPE_STRING_RE = /"|\\|\n|\$/g;
export var MODULE_SUFFIX = IS_DART ? '.dart' : '.js';
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 escapeSingleQuoteString(input: string): string {
if (isBlank(input)) {
return null;
}
return `'${escapeString(input, SINGLE_QUOTE_ESCAPE_STRING_RE)}'`;
}
export function escapeDoubleQuoteString(input: string): string {
if (isBlank(input)) {
return null;
}
return `"${escapeString(input, DOUBLE_QUOTE_ESCAPE_STRING_RE)}"`;
}
function escapeString(input: string, re: RegExp): string {
return StringWrapper.replaceAllMapped(input, re, (match) => {
if (match[0] == '$') {
return IS_DART ? '\\$' : '$';
} else if (match[0] == '\n') {
return '\\n';
} else {
return `\\${match[0]}`;
}
});
}
export function codeGenExportVariable(name: string, isConst: boolean = false): string {
if (IS_DART) {
return isConst ? `const ${name} = ` : `final ${name} = `;
} else {
return `var ${name} = exports['${name}'] = `;
}
}
export function codeGenConcatArray(expression: string): string {
return `${IS_DART ? '..addAll' : '.concat'}(${expression})`;
}
export function codeGenMapArray(argNames: string[], callback: string): string {
if (IS_DART) {
return `.map( (${argNames.join(',')}) => ${callback} ).toList()`;
} else {
return `.map(function(${argNames.join(',')}) { return ${callback}; })`;
}
}
export function codeGenReplaceAll(pattern: string, expression: string): string {
if (IS_DART) {
return `.replaceAll('${pattern}', ${expression})`;
} else {
return `.replace(/${pattern}/g, ${expression})`;
}
}
export function codeGenValueFn(params: string[], value: string, fnName: string = ''): string {
if (IS_DART) {
return `${fnName}(${params.join(',')}) => ${value}`;
} else {
return `function ${fnName}(${params.join(',')}) { return ${value}; }`;
}
}
export function codeGenToString(expr: string): string {
if (IS_DART) {
return `'\${${expr}}'`;
} else {
// JS automatically convets to string...
return expr;
}
}
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;
}
}
+6
View File
@@ -0,0 +1,6 @@
import {Promise} from 'angular2/src/core/facade/async';
// TODO: vsavkin rename it into TemplateLoader and do not reexport it via DomAdapter
export class XHR {
get(url: string): Promise<string> { return null; }
}
@@ -0,0 +1,14 @@
library angular2.src.services.xhr_impl;
import 'dart:async' show Future;
import 'dart:html' show HttpRequest;
import 'package:angular2/core.dart';
import './xhr.dart' show XHR;
@Injectable()
class XHRImpl extends XHR {
Future<String> get(String url) {
return HttpRequest.request(url).then((HttpRequest req) => req.responseText,
onError: (_) => new Future.error('Failed to load $url'));
}
}
+39
View File
@@ -0,0 +1,39 @@
import {Promise, PromiseWrapper, PromiseCompleter} from 'angular2/src/core/facade/promise';
import {isPresent} from 'angular2/src/core/facade/lang';
import {XHR} from './xhr';
export class XHRImpl extends XHR {
get(url: string): Promise<string> {
var completer: PromiseCompleter < string >= PromiseWrapper.completer();
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'text';
xhr.onload = function() {
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
// response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
var response = isPresent(xhr.response) ? xhr.response : xhr.responseText;
// normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
var status = xhr.status === 1223 ? 204 : xhr.status;
// fix status code when it is 0 (0 status is undocumented).
// Occurs when accessing file resources or on Android 4.1 stock browser
// while retrieving files from application cache.
if (status === 0) {
status = response ? 200 : 0;
}
if (200 <= status && status <= 300) {
completer.resolve(response);
} else {
completer.reject(`Failed to load ${url}`, null);
}
};
xhr.onerror = function() { completer.reject(`Failed to load ${url}`, null); };
xhr.send();
return completer.promise;
}
}
+98
View File
@@ -0,0 +1,98 @@
import {XHR} from 'angular2/src/compiler/xhr';
import {ListWrapper, Map, MapWrapper} from 'angular2/src/core/facade/collection';
import {isBlank, isPresent, normalizeBlank} from 'angular2/src/core/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/core/facade/exceptions';
import {PromiseCompleter, PromiseWrapper, Promise} from 'angular2/src/core/facade/async';
export class MockXHR extends XHR {
private _expectations: _Expectation[] = [];
private _definitions = new Map<string, string>();
private _requests: _PendingRequest[] = [];
get(url: string): Promise<string> {
var request = new _PendingRequest(url);
this._requests.push(request);
return request.getPromise();
}
expect(url: string, response: string) {
var expectation = new _Expectation(url, response);
this._expectations.push(expectation);
}
when(url: string, response: string) { this._definitions.set(url, response); }
flush() {
if (this._requests.length === 0) {
throw new BaseException('No pending requests to flush');
}
do {
this._processRequest(this._requests.shift());
} while (this._requests.length > 0);
this.verifyNoOustandingExpectations();
}
verifyNoOustandingExpectations() {
if (this._expectations.length === 0) return;
var urls = [];
for (var i = 0; i < this._expectations.length; i++) {
var expectation = this._expectations[i];
urls.push(expectation.url);
}
throw new BaseException(`Unsatisfied requests: ${urls.join(', ')}`);
}
private _processRequest(request: _PendingRequest) {
var url = request.url;
if (this._expectations.length > 0) {
var expectation = this._expectations[0];
if (expectation.url == url) {
ListWrapper.remove(this._expectations, expectation);
request.complete(expectation.response);
return;
}
}
if (this._definitions.has(url)) {
var response = this._definitions.get(url);
request.complete(normalizeBlank(response));
return;
}
throw new BaseException(`Unexpected request ${url}`);
}
}
class _PendingRequest {
url: string;
completer: PromiseCompleter<string>;
constructor(url) {
this.url = url;
this.completer = PromiseWrapper.completer();
}
complete(response: string) {
if (isBlank(response)) {
this.completer.reject(`Failed to load ${this.url}`, null);
} else {
this.completer.resolve(response);
}
}
getPromise(): Promise<string> { return this.completer.promise; }
}
class _Expectation {
url: string;
response: string;
constructor(url: string, response: string) {
this.url = url;
this.response = response;
}
}