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:
@@ -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(',')}]`;
|
||||
}
|
||||
@@ -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: '*'
|
||||
});
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user