refactor: move angular source to /packages rather than modules/@angular

This commit is contained in:
Jason Aden
2017-03-02 10:48:42 -08:00
parent 5ad5301a3e
commit 3e51a19983
1051 changed files with 18 additions and 18 deletions

View File

@ -0,0 +1,411 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileProviderMetadata, componentFactoryName, createHostComponentMeta, flatten, identifierName} from '../compile_metadata';
import {CompilerConfig} from '../config';
import {Identifiers, createIdentifier, createIdentifierToken} from '../identifiers';
import {CompileMetadataResolver} from '../metadata_resolver';
import {NgModuleCompiler} from '../ng_module_compiler';
import {OutputEmitter} from '../output/abstract_emitter';
import * as o from '../output/output_ast';
import {CompiledStylesheet, StyleCompiler} from '../style_compiler';
import {SummaryResolver} from '../summary_resolver';
import {TemplateParser} from '../template_parser/template_parser';
import {syntaxError} from '../util';
import {ViewCompiler} from '../view_compiler/view_compiler';
import {AotCompilerHost} from './compiler_host';
import {GeneratedFile} from './generated_file';
import {StaticSymbol} from './static_symbol';
import {StaticSymbolResolver} from './static_symbol_resolver';
import {serializeSummaries} from './summary_serializer';
import {ngfactoryFilePath, splitTypescriptSuffix, summaryFileName} from './util';
export class AotCompiler {
constructor(
private _config: CompilerConfig, private _host: AotCompilerHost,
private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser,
private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler,
private _ngModuleCompiler: NgModuleCompiler, private _outputEmitter: OutputEmitter,
private _summaryResolver: SummaryResolver<StaticSymbol>, private _localeId: string,
private _translationFormat: string, private _symbolResolver: StaticSymbolResolver) {}
clearCache() { this._metadataResolver.clearCache(); }
compileAll(rootFiles: string[]): Promise<GeneratedFile[]> {
const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host);
const {ngModuleByPipeOrDirective, files, ngModules} =
analyzeAndValidateNgModules(programSymbols, this._host, this._metadataResolver);
return Promise
.all(ngModules.map(
ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
ngModule.type.reference, false)))
.then(() => {
const sourceModules = files.map(
file => this._compileSrcFile(
file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.pipes,
file.ngModules, file.injectables));
return flatten(sourceModules);
});
}
private _compileSrcFile(
srcFileUrl: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: StaticSymbol[],
injectables: StaticSymbol[]): GeneratedFile[] {
const fileSuffix = splitTypescriptSuffix(srcFileUrl)[1];
const statements: o.Statement[] = [];
const exportedVars: string[] = [];
const generatedFiles: GeneratedFile[] = [];
generatedFiles.push(this._createSummary(
srcFileUrl, directives, pipes, ngModules, injectables, statements, exportedVars));
// compile all ng modules
exportedVars.push(
...ngModules.map((ngModuleType) => this._compileModule(ngModuleType, statements)));
// compile components
directives.forEach((dirType) => {
const compMeta = this._metadataResolver.getDirectiveMetadata(<any>dirType);
if (!compMeta.isComponent) {
return Promise.resolve(null);
}
const ngModule = ngModuleByPipeOrDirective.get(dirType);
if (!ngModule) {
throw new Error(
`Internal Error: cannot determine the module for component ${identifierName(compMeta.type)}!`);
}
_assertComponent(compMeta);
// compile styles
const stylesCompileResults = this._styleCompiler.compileComponent(compMeta);
stylesCompileResults.externalStylesheets.forEach((compiledStyleSheet) => {
generatedFiles.push(this._codgenStyles(srcFileUrl, compiledStyleSheet, fileSuffix));
});
// compile components
const compViewVars = this._compileComponent(
compMeta, ngModule, ngModule.transitiveModule.directives,
stylesCompileResults.componentStylesheet, fileSuffix, statements);
exportedVars.push(
this._compileComponentFactory(compMeta, ngModule, fileSuffix, statements),
compViewVars.viewClassVar, compViewVars.compRenderTypeVar);
});
if (statements.length > 0) {
const srcModule = this._codegenSourceModule(
srcFileUrl, ngfactoryFilePath(srcFileUrl), statements, exportedVars);
generatedFiles.unshift(srcModule);
}
return generatedFiles;
}
private _createSummary(
srcFileUrl: string, directives: StaticSymbol[], pipes: StaticSymbol[],
ngModules: StaticSymbol[], injectables: StaticSymbol[], targetStatements: o.Statement[],
targetExportedVars: string[]): GeneratedFile {
const symbolSummaries = this._symbolResolver.getSymbolsOf(srcFileUrl)
.map(symbol => this._symbolResolver.resolveSymbol(symbol));
const typeSummaries = [
...ngModules.map(ref => this._metadataResolver.getNgModuleSummary(ref)),
...directives.map(ref => this._metadataResolver.getDirectiveSummary(ref)),
...pipes.map(ref => this._metadataResolver.getPipeSummary(ref)),
...injectables.map(ref => this._metadataResolver.getInjectableSummary(ref))
];
const {json, exportAs} = serializeSummaries(
this._summaryResolver, this._symbolResolver, symbolSummaries, typeSummaries);
exportAs.forEach((entry) => {
targetStatements.push(
o.variable(entry.exportAs).set(o.importExpr({reference: entry.symbol})).toDeclStmt());
targetExportedVars.push(entry.exportAs);
});
return new GeneratedFile(srcFileUrl, summaryFileName(srcFileUrl), json);
}
private _compileModule(ngModuleType: StaticSymbol, targetStatements: o.Statement[]): string {
const ngModule = this._metadataResolver.getNgModuleMetadata(ngModuleType);
const providers: CompileProviderMetadata[] = [];
if (this._localeId) {
providers.push({
token: createIdentifierToken(Identifiers.LOCALE_ID),
useValue: this._localeId,
});
}
if (this._translationFormat) {
providers.push({
token: createIdentifierToken(Identifiers.TRANSLATIONS_FORMAT),
useValue: this._translationFormat
});
}
const appCompileResult = this._ngModuleCompiler.compile(ngModule, providers);
targetStatements.push(...appCompileResult.statements);
return appCompileResult.ngModuleFactoryVar;
}
private _compileComponentFactory(
compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata, fileSuffix: string,
targetStatements: o.Statement[]): string {
const hostType = this._metadataResolver.getHostComponentType(compMeta.type.reference);
const hostMeta = createHostComponentMeta(
hostType, compMeta, this._metadataResolver.getHostComponentViewClass(hostType));
const hostViewFactoryVar =
this._compileComponent(
hostMeta, ngModule, [compMeta.type], null, fileSuffix, targetStatements)
.viewClassVar;
const compFactoryVar = componentFactoryName(compMeta.type.reference);
targetStatements.push(
o.variable(compFactoryVar)
.set(o.importExpr(createIdentifier(Identifiers.createComponentFactory)).callFn([
o.literal(compMeta.selector),
o.importExpr(compMeta.type),
o.variable(hostViewFactoryVar),
]))
.toDeclStmt(
o.importType(
createIdentifier(Identifiers.ComponentFactory), [o.importType(compMeta.type)],
[o.TypeModifier.Const]),
[o.StmtModifier.Final]));
return compFactoryVar;
}
private _compileComponent(
compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata,
directiveIdentifiers: CompileIdentifierMetadata[], componentStyles: CompiledStylesheet,
fileSuffix: string,
targetStatements: o.Statement[]): {viewClassVar: string, compRenderTypeVar: string} {
const directives =
directiveIdentifiers.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference));
const pipes = ngModule.transitiveModule.pipes.map(
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
compMeta, compMeta.template.template, directives, pipes, ngModule.schemas,
identifierName(compMeta.type));
const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]);
const viewResult =
this._viewCompiler.compileComponent(compMeta, parsedTemplate, stylesExpr, usedPipes);
if (componentStyles) {
targetStatements.push(
..._resolveStyleStatements(this._symbolResolver, componentStyles, fileSuffix));
}
targetStatements.push(...viewResult.statements);
return {viewClassVar: viewResult.viewClassVar, compRenderTypeVar: viewResult.rendererTypeVar};
}
private _codgenStyles(
fileUrl: string, stylesCompileResult: CompiledStylesheet, fileSuffix: string): GeneratedFile {
_resolveStyleStatements(this._symbolResolver, stylesCompileResult, fileSuffix);
return this._codegenSourceModule(
fileUrl, _stylesModuleUrl(
stylesCompileResult.meta.moduleUrl, stylesCompileResult.isShimmed, fileSuffix),
stylesCompileResult.statements, [stylesCompileResult.stylesVar]);
}
private _codegenSourceModule(
srcFileUrl: string, genFileUrl: string, statements: o.Statement[],
exportedVars: string[]): GeneratedFile {
return new GeneratedFile(
srcFileUrl, genFileUrl,
this._outputEmitter.emitStatements(genFileUrl, statements, exportedVars));
}
}
function _resolveStyleStatements(
reflector: StaticSymbolResolver, compileResult: CompiledStylesheet,
fileSuffix: string): o.Statement[] {
compileResult.dependencies.forEach((dep) => {
dep.valuePlaceholder.reference = reflector.getStaticSymbol(
_stylesModuleUrl(dep.moduleUrl, dep.isShimmed, fileSuffix), dep.name);
});
return compileResult.statements;
}
function _stylesModuleUrl(stylesheetUrl: string, shim: boolean, suffix: string): string {
return `${stylesheetUrl}${shim ? '.shim' : ''}.ngstyle${suffix}`;
}
function _assertComponent(meta: CompileDirectiveMetadata) {
if (!meta.isComponent) {
throw new Error(
`Could not compile '${identifierName(meta.type)}' because it is not a component.`);
}
}
export interface NgAnalyzedModules {
ngModules: CompileNgModuleMetadata[];
ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>;
files: Array<{
srcUrl: string,
directives: StaticSymbol[],
pipes: StaticSymbol[],
ngModules: StaticSymbol[],
injectables: StaticSymbol[]
}>;
symbolsMissingModule?: StaticSymbol[];
}
export interface NgAnalyzeModulesHost { isSourceFile(filePath: string): boolean; }
// Returns all the source files and a mapping from modules to directives
export function analyzeNgModules(
programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost,
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
const {ngModules, symbolsMissingModule} =
_createNgModules(programStaticSymbols, host, metadataResolver);
return _analyzeNgModules(programStaticSymbols, ngModules, symbolsMissingModule, metadataResolver);
}
export function analyzeAndValidateNgModules(
programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost,
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
const result = analyzeNgModules(programStaticSymbols, host, metadataResolver);
if (result.symbolsMissingModule && result.symbolsMissingModule.length) {
const messages = result.symbolsMissingModule.map(
s =>
`Cannot determine the module for class ${s.name} in ${s.filePath}! Add ${s.name} to the NgModule to fix it.`);
throw syntaxError(messages.join('\n'));
}
return result;
}
function _analyzeNgModules(
programSymbols: StaticSymbol[], ngModuleMetas: CompileNgModuleMetadata[],
symbolsMissingModule: StaticSymbol[],
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
const moduleMetasByRef = new Map<any, CompileNgModuleMetadata>();
ngModuleMetas.forEach((ngModule) => moduleMetasByRef.set(ngModule.type.reference, ngModule));
const ngModuleByPipeOrDirective = new Map<StaticSymbol, CompileNgModuleMetadata>();
const ngModulesByFile = new Map<string, StaticSymbol[]>();
const ngDirectivesByFile = new Map<string, StaticSymbol[]>();
const ngPipesByFile = new Map<string, StaticSymbol[]>();
const ngInjectablesByFile = new Map<string, StaticSymbol[]>();
const filePaths = new Set<string>();
// Make sure we produce an analyzed file for each input file
programSymbols.forEach((symbol) => {
const filePath = symbol.filePath;
filePaths.add(filePath);
if (metadataResolver.isInjectable(symbol)) {
ngInjectablesByFile.set(filePath, (ngInjectablesByFile.get(filePath) || []).concat(symbol));
}
});
// Looping over all modules to construct:
// - a map from file to modules `ngModulesByFile`,
// - a map from file to directives `ngDirectivesByFile`,
// - a map from file to pipes `ngPipesByFile`,
// - a map from directive/pipe to module `ngModuleByPipeOrDirective`.
ngModuleMetas.forEach((ngModuleMeta) => {
const srcFileUrl = ngModuleMeta.type.reference.filePath;
filePaths.add(srcFileUrl);
ngModulesByFile.set(
srcFileUrl, (ngModulesByFile.get(srcFileUrl) || []).concat(ngModuleMeta.type.reference));
ngModuleMeta.declaredDirectives.forEach((dirIdentifier) => {
const fileUrl = dirIdentifier.reference.filePath;
filePaths.add(fileUrl);
ngDirectivesByFile.set(
fileUrl, (ngDirectivesByFile.get(fileUrl) || []).concat(dirIdentifier.reference));
ngModuleByPipeOrDirective.set(dirIdentifier.reference, ngModuleMeta);
});
ngModuleMeta.declaredPipes.forEach((pipeIdentifier) => {
const fileUrl = pipeIdentifier.reference.filePath;
filePaths.add(fileUrl);
ngPipesByFile.set(
fileUrl, (ngPipesByFile.get(fileUrl) || []).concat(pipeIdentifier.reference));
ngModuleByPipeOrDirective.set(pipeIdentifier.reference, ngModuleMeta);
});
});
const files: {
srcUrl: string,
directives: StaticSymbol[],
pipes: StaticSymbol[],
ngModules: StaticSymbol[],
injectables: StaticSymbol[]
}[] = [];
filePaths.forEach((srcUrl) => {
const directives = ngDirectivesByFile.get(srcUrl) || [];
const pipes = ngPipesByFile.get(srcUrl) || [];
const ngModules = ngModulesByFile.get(srcUrl) || [];
const injectables = ngInjectablesByFile.get(srcUrl) || [];
files.push({srcUrl, directives, pipes, ngModules, injectables});
});
return {
// map directive/pipe to module
ngModuleByPipeOrDirective,
// list modules and directives for every source file
files,
ngModules: ngModuleMetas, symbolsMissingModule
};
}
export function extractProgramSymbols(
staticSymbolResolver: StaticSymbolResolver, files: string[],
host: NgAnalyzeModulesHost): StaticSymbol[] {
const staticSymbols: StaticSymbol[] = [];
files.filter(fileName => host.isSourceFile(fileName)).forEach(sourceFile => {
staticSymbolResolver.getSymbolsOf(sourceFile).forEach((symbol) => {
const resolvedSymbol = staticSymbolResolver.resolveSymbol(symbol);
const symbolMeta = resolvedSymbol.metadata;
if (symbolMeta) {
if (symbolMeta.__symbolic != 'error') {
// Ignore symbols that are only included to record error information.
staticSymbols.push(resolvedSymbol.symbol);
}
}
});
});
return staticSymbols;
}
// Load the NgModules and check
// that all directives / pipes that are present in the program
// are also declared by a module.
function _createNgModules(
programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost,
metadataResolver: CompileMetadataResolver):
{ngModules: CompileNgModuleMetadata[], symbolsMissingModule: StaticSymbol[]} {
const ngModules = new Map<any, CompileNgModuleMetadata>();
const programPipesAndDirectives: StaticSymbol[] = [];
const ngModulePipesAndDirective = new Set<StaticSymbol>();
const addNgModule = (staticSymbol: any) => {
if (ngModules.has(staticSymbol) || !host.isSourceFile(staticSymbol.filePath)) {
return false;
}
const ngModule = metadataResolver.getNgModuleMetadata(staticSymbol, false);
if (ngModule) {
ngModules.set(ngModule.type.reference, ngModule);
ngModule.declaredDirectives.forEach((dir) => ngModulePipesAndDirective.add(dir.reference));
ngModule.declaredPipes.forEach((pipe) => ngModulePipesAndDirective.add(pipe.reference));
// For every input module add the list of transitively included modules
ngModule.transitiveModule.modules.forEach(modMeta => addNgModule(modMeta.reference));
}
return !!ngModule;
};
programStaticSymbols.forEach((staticSymbol) => {
if (!addNgModule(staticSymbol) &&
(metadataResolver.isDirective(staticSymbol) || metadataResolver.isPipe(staticSymbol))) {
programPipesAndDirectives.push(staticSymbol);
}
});
// Throw an error if any of the program pipe or directives is not declared by a module
const symbolsMissingModule =
programPipesAndDirectives.filter(s => !ngModulePipesAndDirective.has(s));
return {ngModules: Array.from(ngModules.values()), symbolsMissingModule};
}

View File

@ -0,0 +1,84 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {MissingTranslationStrategy, ViewEncapsulation, ɵConsole as Console} from '@angular/core';
import {CompilerConfig} from '../config';
import {DirectiveNormalizer} from '../directive_normalizer';
import {DirectiveResolver} from '../directive_resolver';
import {Lexer} from '../expression_parser/lexer';
import {Parser} from '../expression_parser/parser';
import {I18NHtmlParser} from '../i18n/i18n_html_parser';
import {CompileMetadataResolver} from '../metadata_resolver';
import {HtmlParser} from '../ml_parser/html_parser';
import {NgModuleCompiler} from '../ng_module_compiler';
import {NgModuleResolver} from '../ng_module_resolver';
import {TypeScriptEmitter} from '../output/ts_emitter';
import {PipeResolver} from '../pipe_resolver';
import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry';
import {StyleCompiler} from '../style_compiler';
import {TemplateParser} from '../template_parser/template_parser';
import {createOfflineCompileUrlResolver} from '../url_resolver';
import {ViewCompiler} from '../view_compiler/view_compiler';
import {AotCompiler} from './compiler';
import {AotCompilerHost} from './compiler_host';
import {AotCompilerOptions} from './compiler_options';
import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities';
import {StaticReflector} from './static_reflector';
import {StaticSymbol, StaticSymbolCache} from './static_symbol';
import {StaticSymbolResolver} from './static_symbol_resolver';
import {AotSummaryResolver} from './summary_resolver';
/**
* Creates a new AotCompiler based on options and a host.
*/
export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCompilerOptions):
{compiler: AotCompiler, reflector: StaticReflector} {
let translations: string = options.translations || '';
const urlResolver = createOfflineCompileUrlResolver();
const symbolCache = new StaticSymbolCache();
const summaryResolver = new AotSummaryResolver(compilerHost, symbolCache);
const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver);
const staticReflector = new StaticReflector(symbolResolver);
StaticAndDynamicReflectionCapabilities.install(staticReflector);
const console = new Console();
const htmlParser = new I18NHtmlParser(
new HtmlParser(), translations, options.i18nFormat, MissingTranslationStrategy.Warning,
console);
const config = new CompilerConfig({
defaultEncapsulation: ViewEncapsulation.Emulated,
useJit: false,
enableLegacyTemplate: options.enableLegacyTemplate !== false,
});
const normalizer = new DirectiveNormalizer(
{get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config);
const expressionParser = new Parser(new Lexer());
const elementSchemaRegistry = new DomElementSchemaRegistry();
const tmplParser =
new TemplateParser(config, expressionParser, elementSchemaRegistry, htmlParser, console, []);
const resolver = new CompileMetadataResolver(
config, new NgModuleResolver(staticReflector), new DirectiveResolver(staticReflector),
new PipeResolver(staticReflector), summaryResolver, elementSchemaRegistry, normalizer,
symbolCache, staticReflector);
// TODO(vicb): do not pass options.i18nFormat here
const importResolver = {
getImportAs: (symbol: StaticSymbol) => symbolResolver.getImportAs(symbol),
fileNameToModuleName: (fileName: string, containingFilePath: string) =>
compilerHost.fileNameToModuleName(fileName, containingFilePath),
getTypeArity: (symbol: StaticSymbol) => symbolResolver.getTypeArity(symbol)
};
const viewCompiler = new ViewCompiler(config, elementSchemaRegistry);
const compiler = new AotCompiler(
config, compilerHost, resolver, tmplParser, new StyleCompiler(urlResolver), viewCompiler,
new NgModuleCompiler(), new TypeScriptEmitter(importResolver), summaryResolver,
options.locale, options.i18nFormat, symbolResolver);
return {compiler, reflector: staticReflector};
}

View File

@ -0,0 +1,30 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StaticSymbolResolverHost} from './static_symbol_resolver';
import {AotSummaryResolverHost} from './summary_resolver';
/**
* The host of the AotCompiler disconnects the implementation from TypeScript / other language
* services and from underlying file systems.
*/
export interface AotCompilerHost extends StaticSymbolResolverHost, AotSummaryResolverHost {
/**
* Converts a file path to a module name that can be used as an `import.
* I.e. `path/to/importedFile.ts` should be imported by `path/to/containingFile.ts`.
*
* See ImportResolver.
*/
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string
/*|null*/;
/**
* Loads a resource (e.g. html / css)
*/
loadResource(path: string): Promise<string>;
}

View File

@ -0,0 +1,14 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export interface AotCompilerOptions {
locale?: string;
i18nFormat?: string;
translations?: string;
enableLegacyTemplate?: boolean;
}

View File

@ -0,0 +1,11 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export class GeneratedFile {
constructor(public srcFileUrl: string, public genFileUrl: string, public source: string) {}
}

View File

@ -0,0 +1,59 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵGetterFn, ɵMethodFn, ɵReflectionCapabilities, ɵSetterFn, ɵreflector} from '@angular/core';
import {StaticReflector} from './static_reflector';
import {StaticSymbol} from './static_symbol';
export class StaticAndDynamicReflectionCapabilities {
static install(staticDelegate: StaticReflector) {
ɵreflector.updateCapabilities(new StaticAndDynamicReflectionCapabilities(staticDelegate));
}
private dynamicDelegate = new ɵReflectionCapabilities();
constructor(private staticDelegate: StaticReflector) {}
isReflectionEnabled(): boolean { return true; }
factory(type: any): Function { return this.dynamicDelegate.factory(type); }
hasLifecycleHook(type: any, lcProperty: string): boolean {
return isStaticType(type) ? this.staticDelegate.hasLifecycleHook(type, lcProperty) :
this.dynamicDelegate.hasLifecycleHook(type, lcProperty);
}
parameters(type: any): any[][] {
return isStaticType(type) ? this.staticDelegate.parameters(type) :
this.dynamicDelegate.parameters(type);
}
annotations(type: any): any[] {
return isStaticType(type) ? this.staticDelegate.annotations(type) :
this.dynamicDelegate.annotations(type);
}
propMetadata(typeOrFunc: any): {[key: string]: any[]} {
return isStaticType(typeOrFunc) ? this.staticDelegate.propMetadata(typeOrFunc) :
this.dynamicDelegate.propMetadata(typeOrFunc);
}
getter(name: string): ɵGetterFn { return this.dynamicDelegate.getter(name); }
setter(name: string): ɵSetterFn { return this.dynamicDelegate.setter(name); }
method(name: string): ɵMethodFn { return this.dynamicDelegate.method(name); }
importUri(type: any): string { return this.staticDelegate.importUri(type); }
resolveIdentifier(name: string, moduleUrl: string, members: string[], runtime: any) {
return this.staticDelegate.resolveIdentifier(name, moduleUrl, members);
}
resolveEnum(enumIdentifier: any, name: string): any {
if (isStaticType(enumIdentifier)) {
return this.staticDelegate.resolveEnum(enumIdentifier, name);
} else {
return null;
}
}
}
function isStaticType(type: any): boolean {
return typeof type === 'object' && type.name && type.filePath;
}

View File

@ -0,0 +1,700 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Attribute, Component, ContentChild, ContentChildren, Directive, Host, HostBinding, HostListener, Inject, Injectable, Input, NgModule, Optional, Output, Pipe, Self, SkipSelf, ViewChild, ViewChildren, animate, group, keyframes, sequence, state, style, transition, trigger, ɵReflectorReader} from '@angular/core';
import {syntaxError} from '../util';
import {StaticSymbol} from './static_symbol';
import {StaticSymbolResolver} from './static_symbol_resolver';
const ANGULAR_CORE = '@angular/core';
const HIDDEN_KEY = /^\$.*\$$/;
const IGNORE = {
__symbolic: 'ignore'
};
function shouldIgnore(value: any): boolean {
return value && value.__symbolic == 'ignore';
}
/**
* A static reflector implements enough of the Reflector API that is necessary to compile
* templates statically.
*/
export class StaticReflector implements ɵReflectorReader {
private annotationCache = new Map<StaticSymbol, any[]>();
private propertyCache = new Map<StaticSymbol, {[key: string]: any[]}>();
private parameterCache = new Map<StaticSymbol, any[]>();
private methodCache = new Map<StaticSymbol, {[key: string]: boolean}>();
private conversionMap = new Map<StaticSymbol, (context: StaticSymbol, args: any[]) => any>();
private injectionToken: StaticSymbol;
private opaqueToken: StaticSymbol;
constructor(
private symbolResolver: StaticSymbolResolver,
knownMetadataClasses: {name: string, filePath: string, ctor: any}[] = [],
knownMetadataFunctions: {name: string, filePath: string, fn: any}[] = [],
private errorRecorder?: (error: any, fileName: string) => void) {
this.initializeConversionMap();
knownMetadataClasses.forEach(
(kc) => this._registerDecoratorOrConstructor(
this.getStaticSymbol(kc.filePath, kc.name), kc.ctor));
knownMetadataFunctions.forEach(
(kf) => this._registerFunction(this.getStaticSymbol(kf.filePath, kf.name), kf.fn));
}
importUri(typeOrFunc: StaticSymbol): string {
const staticSymbol = this.findSymbolDeclaration(typeOrFunc);
return staticSymbol ? staticSymbol.filePath : null;
}
resolveIdentifier(name: string, moduleUrl: string, members: string[]): StaticSymbol {
const importSymbol = this.getStaticSymbol(moduleUrl, name);
const rootSymbol = this.findDeclaration(moduleUrl, name);
if (importSymbol != rootSymbol) {
this.symbolResolver.recordImportAs(rootSymbol, importSymbol);
}
if (members && members.length) {
return this.getStaticSymbol(rootSymbol.filePath, rootSymbol.name, members);
}
return rootSymbol;
}
findDeclaration(moduleUrl: string, name: string, containingFile?: string): StaticSymbol {
return this.findSymbolDeclaration(
this.symbolResolver.getSymbolByModule(moduleUrl, name, containingFile));
}
findSymbolDeclaration(symbol: StaticSymbol): StaticSymbol {
const resolvedSymbol = this.symbolResolver.resolveSymbol(symbol);
if (resolvedSymbol && resolvedSymbol.metadata instanceof StaticSymbol) {
return this.findSymbolDeclaration(resolvedSymbol.metadata);
} else {
return symbol;
}
}
resolveEnum(enumIdentifier: any, name: string): any {
const staticSymbol: StaticSymbol = enumIdentifier;
const members = (staticSymbol.members || []).concat(name);
return this.getStaticSymbol(staticSymbol.filePath, staticSymbol.name, members);
}
public annotations(type: StaticSymbol): any[] {
let annotations = this.annotationCache.get(type);
if (!annotations) {
annotations = [];
const classMetadata = this.getTypeMetadata(type);
if (classMetadata['extends']) {
const parentType = this.trySimplify(type, classMetadata['extends']);
if (parentType && (parentType instanceof StaticSymbol)) {
const parentAnnotations = this.annotations(parentType);
annotations.push(...parentAnnotations);
}
}
if (classMetadata['decorators']) {
const ownAnnotations: any[] = this.simplify(type, classMetadata['decorators']);
annotations.push(...ownAnnotations);
}
this.annotationCache.set(type, annotations.filter(ann => !!ann));
}
return annotations;
}
public propMetadata(type: StaticSymbol): {[key: string]: any[]} {
let propMetadata = this.propertyCache.get(type);
if (!propMetadata) {
const classMetadata = this.getTypeMetadata(type);
propMetadata = {};
if (classMetadata['extends']) {
const parentType = this.simplify(type, classMetadata['extends']);
if (parentType instanceof StaticSymbol) {
const parentPropMetadata = this.propMetadata(parentType);
Object.keys(parentPropMetadata).forEach((parentProp) => {
propMetadata[parentProp] = parentPropMetadata[parentProp];
});
}
}
const members = classMetadata['members'] || {};
Object.keys(members).forEach((propName) => {
const propData = members[propName];
const prop = (<any[]>propData)
.find(a => a['__symbolic'] == 'property' || a['__symbolic'] == 'method');
const decorators: any[] = [];
if (propMetadata[propName]) {
decorators.push(...propMetadata[propName]);
}
propMetadata[propName] = decorators;
if (prop && prop['decorators']) {
decorators.push(...this.simplify(type, prop['decorators']));
}
});
this.propertyCache.set(type, propMetadata);
}
return propMetadata;
}
public parameters(type: StaticSymbol): any[] {
if (!(type instanceof StaticSymbol)) {
this.reportError(
new Error(`parameters received ${JSON.stringify(type)} which is not a StaticSymbol`),
type);
return [];
}
try {
let parameters = this.parameterCache.get(type);
if (!parameters) {
const classMetadata = this.getTypeMetadata(type);
const members = classMetadata ? classMetadata['members'] : null;
const ctorData = members ? members['__ctor__'] : null;
if (ctorData) {
const ctor = (<any[]>ctorData).find(a => a['__symbolic'] == 'constructor');
const parameterTypes = <any[]>this.simplify(type, ctor['parameters'] || []);
const parameterDecorators = <any[]>this.simplify(type, ctor['parameterDecorators'] || []);
parameters = [];
parameterTypes.forEach((paramType, index) => {
const nestedResult: any[] = [];
if (paramType) {
nestedResult.push(paramType);
}
const decorators = parameterDecorators ? parameterDecorators[index] : null;
if (decorators) {
nestedResult.push(...decorators);
}
parameters.push(nestedResult);
});
} else if (classMetadata['extends']) {
const parentType = this.simplify(type, classMetadata['extends']);
if (parentType instanceof StaticSymbol) {
parameters = this.parameters(parentType);
}
}
if (!parameters) {
parameters = [];
}
this.parameterCache.set(type, parameters);
}
return parameters;
} catch (e) {
console.error(`Failed on type ${JSON.stringify(type)} with error ${e}`);
throw e;
}
}
private _methodNames(type: any): {[key: string]: boolean} {
let methodNames = this.methodCache.get(type);
if (!methodNames) {
const classMetadata = this.getTypeMetadata(type);
methodNames = {};
if (classMetadata['extends']) {
const parentType = this.simplify(type, classMetadata['extends']);
if (parentType instanceof StaticSymbol) {
const parentMethodNames = this._methodNames(parentType);
Object.keys(parentMethodNames).forEach((parentProp) => {
methodNames[parentProp] = parentMethodNames[parentProp];
});
}
}
const members = classMetadata['members'] || {};
Object.keys(members).forEach((propName) => {
const propData = members[propName];
const isMethod = (<any[]>propData).some(a => a['__symbolic'] == 'method');
methodNames[propName] = methodNames[propName] || isMethod;
});
this.methodCache.set(type, methodNames);
}
return methodNames;
}
hasLifecycleHook(type: any, lcProperty: string): boolean {
if (!(type instanceof StaticSymbol)) {
this.reportError(
new Error(
`hasLifecycleHook received ${JSON.stringify(type)} which is not a StaticSymbol`),
type);
}
try {
return !!this._methodNames(type)[lcProperty];
} catch (e) {
console.error(`Failed on type ${JSON.stringify(type)} with error ${e}`);
throw e;
}
}
private _registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void {
this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => new ctor(...args));
}
private _registerFunction(type: StaticSymbol, fn: any): void {
this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => fn.apply(undefined, args));
}
private initializeConversionMap(): void {
this.injectionToken = this.findDeclaration(ANGULAR_CORE, 'InjectionToken');
this.opaqueToken = this.findDeclaration(ANGULAR_CORE, 'OpaqueToken');
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Host'), Host);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'Injectable'), Injectable);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Self'), Self);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'SkipSelf'), SkipSelf);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Inject'), Inject);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Optional'), Optional);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'Attribute'), Attribute);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'ContentChild'), ContentChild);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'ContentChildren'), ContentChildren);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'ViewChild'), ViewChild);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'ViewChildren'), ViewChildren);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Input'), Input);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Output'), Output);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Pipe'), Pipe);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'HostBinding'), HostBinding);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'HostListener'), HostListener);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'Directive'), Directive);
this._registerDecoratorOrConstructor(
this.findDeclaration(ANGULAR_CORE, 'Component'), Component);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'NgModule'), NgModule);
// Note: Some metadata classes can be used directly with Provider.deps.
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Host'), Host);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Self'), Self);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'SkipSelf'), SkipSelf);
this._registerDecoratorOrConstructor(this.findDeclaration(ANGULAR_CORE, 'Optional'), Optional);
this._registerFunction(this.findDeclaration(ANGULAR_CORE, 'trigger'), trigger);
this._registerFunction(this.findDeclaration(ANGULAR_CORE, 'state'), state);
this._registerFunction(this.findDeclaration(ANGULAR_CORE, 'transition'), transition);
this._registerFunction(this.findDeclaration(ANGULAR_CORE, 'style'), style);
this._registerFunction(this.findDeclaration(ANGULAR_CORE, 'animate'), animate);
this._registerFunction(this.findDeclaration(ANGULAR_CORE, 'keyframes'), keyframes);
this._registerFunction(this.findDeclaration(ANGULAR_CORE, 'sequence'), sequence);
this._registerFunction(this.findDeclaration(ANGULAR_CORE, 'group'), group);
}
/**
* getStaticSymbol produces a Type whose metadata is known but whose implementation is not loaded.
* All types passed to the StaticResolver should be pseudo-types returned by this method.
*
* @param declarationFile the absolute path of the file where the symbol is declared
* @param name the name of the type.
*/
getStaticSymbol(declarationFile: string, name: string, members?: string[]): StaticSymbol {
return this.symbolResolver.getStaticSymbol(declarationFile, name, members);
}
private reportError(error: Error, context: StaticSymbol, path?: string) {
if (this.errorRecorder) {
this.errorRecorder(error, (context && context.filePath) || path);
} else {
throw error;
}
}
/**
* Simplify but discard any errors
*/
private trySimplify(context: StaticSymbol, value: any): any {
const originalRecorder = this.errorRecorder;
this.errorRecorder = (error: any, fileName: string) => {};
const result = this.simplify(context, value);
this.errorRecorder = originalRecorder;
return result;
}
/** @internal */
public simplify(context: StaticSymbol, value: any): any {
const self = this;
let scope = BindingScope.empty;
const calling = new Map<StaticSymbol, boolean>();
function simplifyInContext(context: StaticSymbol, value: any, depth: number): any {
function resolveReferenceValue(staticSymbol: StaticSymbol): any {
const resolvedSymbol = self.symbolResolver.resolveSymbol(staticSymbol);
return resolvedSymbol ? resolvedSymbol.metadata : null;
}
function simplifyCall(functionSymbol: StaticSymbol, targetFunction: any, args: any[]) {
if (targetFunction && targetFunction['__symbolic'] == 'function') {
if (calling.get(functionSymbol)) {
throw new Error('Recursion not supported');
}
calling.set(functionSymbol, true);
try {
const value = targetFunction['value'];
if (value && (depth != 0 || value.__symbolic != 'error')) {
const parameters: string[] = targetFunction['parameters'];
const defaults: any[] = targetFunction.defaults;
args = args.map(arg => simplifyInContext(context, arg, depth + 1))
.map(arg => shouldIgnore(arg) ? undefined : arg);
if (defaults && defaults.length > args.length) {
args.push(...defaults.slice(args.length).map((value: any) => simplify(value)));
}
const functionScope = BindingScope.build();
for (let i = 0; i < parameters.length; i++) {
functionScope.define(parameters[i], args[i]);
}
const oldScope = scope;
let result: any;
try {
scope = functionScope.done();
result = simplifyInContext(functionSymbol, value, depth + 1);
} finally {
scope = oldScope;
}
return result;
}
} finally {
calling.delete(functionSymbol);
}
}
if (depth === 0) {
// If depth is 0 we are evaluating the top level expression that is describing element
// decorator. In this case, it is a decorator we don't understand, such as a custom
// non-angular decorator, and we should just ignore it.
return IGNORE;
}
return simplify(
{__symbolic: 'error', message: 'Function call not supported', context: functionSymbol});
}
function simplify(expression: any): any {
if (isPrimitive(expression)) {
return expression;
}
if (expression instanceof Array) {
const result: any[] = [];
for (const item of (<any>expression)) {
// Check for a spread expression
if (item && item.__symbolic === 'spread') {
const spreadArray = simplify(item.expression);
if (Array.isArray(spreadArray)) {
for (const spreadItem of spreadArray) {
result.push(spreadItem);
}
continue;
}
}
const value = simplify(item);
if (shouldIgnore(value)) {
continue;
}
result.push(value);
}
return result;
}
if (expression instanceof StaticSymbol) {
// Stop simplification at builtin symbols
if (expression === self.injectionToken || expression === self.opaqueToken ||
self.conversionMap.has(expression)) {
return expression;
} else {
const staticSymbol = expression;
const declarationValue = resolveReferenceValue(staticSymbol);
if (declarationValue) {
return simplifyInContext(staticSymbol, declarationValue, depth + 1);
} else {
return staticSymbol;
}
}
}
if (expression) {
if (expression['__symbolic']) {
let staticSymbol: StaticSymbol;
switch (expression['__symbolic']) {
case 'binop':
let left = simplify(expression['left']);
if (shouldIgnore(left)) return left;
let right = simplify(expression['right']);
if (shouldIgnore(right)) return right;
switch (expression['operator']) {
case '&&':
return left && right;
case '||':
return left || right;
case '|':
return left | right;
case '^':
return left ^ right;
case '&':
return left & right;
case '==':
return left == right;
case '!=':
return left != right;
case '===':
return left === right;
case '!==':
return left !== right;
case '<':
return left < right;
case '>':
return left > right;
case '<=':
return left <= right;
case '>=':
return left >= right;
case '<<':
return left << right;
case '>>':
return left >> right;
case '+':
return left + right;
case '-':
return left - right;
case '*':
return left * right;
case '/':
return left / right;
case '%':
return left % right;
}
return null;
case 'if':
let condition = simplify(expression['condition']);
return condition ? simplify(expression['thenExpression']) :
simplify(expression['elseExpression']);
case 'pre':
let operand = simplify(expression['operand']);
if (shouldIgnore(operand)) return operand;
switch (expression['operator']) {
case '+':
return operand;
case '-':
return -operand;
case '!':
return !operand;
case '~':
return ~operand;
}
return null;
case 'index':
let indexTarget = simplify(expression['expression']);
let index = simplify(expression['index']);
if (indexTarget && isPrimitive(index)) return indexTarget[index];
return null;
case 'select':
const member = expression['member'];
let selectContext = context;
let selectTarget = simplify(expression['expression']);
if (selectTarget instanceof StaticSymbol) {
const members = selectTarget.members.concat(member);
selectContext =
self.getStaticSymbol(selectTarget.filePath, selectTarget.name, members);
const declarationValue = resolveReferenceValue(selectContext);
if (declarationValue) {
return simplifyInContext(selectContext, declarationValue, depth + 1);
} else {
return selectContext;
}
}
if (selectTarget && isPrimitive(member))
return simplifyInContext(selectContext, selectTarget[member], depth + 1);
return null;
case 'reference':
// Note: This only has to deal with variable references,
// as symbol references have been converted into StaticSymbols already
// in the StaticSymbolResolver!
const name: string = expression['name'];
const localValue = scope.resolve(name);
if (localValue != BindingScope.missing) {
return localValue;
}
break;
case 'class':
return context;
case 'function':
return context;
case 'new':
case 'call':
// Determine if the function is a built-in conversion
staticSymbol = simplifyInContext(context, expression['expression'], depth + 1);
if (staticSymbol instanceof StaticSymbol) {
if (staticSymbol === self.injectionToken || staticSymbol === self.opaqueToken) {
// if somebody calls new InjectionToken, don't create an InjectionToken,
// but rather return the symbol to which the InjectionToken is assigned to.
return context;
}
const argExpressions: any[] = expression['arguments'] || [];
let converter = self.conversionMap.get(staticSymbol);
if (converter) {
const args =
argExpressions.map(arg => simplifyInContext(context, arg, depth + 1))
.map(arg => shouldIgnore(arg) ? undefined : arg);
return converter(context, args);
} else {
// Determine if the function is one we can simplify.
const targetFunction = resolveReferenceValue(staticSymbol);
return simplifyCall(staticSymbol, targetFunction, argExpressions);
}
}
break;
case 'error':
let message = produceErrorMessage(expression);
if (expression['line']) {
message =
`${message} (position ${expression['line']+1}:${expression['character']+1} in the original .ts file)`;
self.reportError(
positionalError(
message, context.filePath, expression['line'], expression['character']),
context);
} else {
self.reportError(new Error(message), context);
}
return IGNORE;
case 'ignore':
return expression;
}
return null;
}
return mapStringMap(expression, (value, name) => simplify(value));
}
return IGNORE;
}
try {
return simplify(value);
} catch (e) {
const members = context.members.length ? `.${context.members.join('.')}` : '';
const message =
`${e.message}, resolving symbol ${context.name}${members} in ${context.filePath}`;
if (e.fileName) {
throw positionalError(message, e.fileName, e.line, e.column);
}
throw syntaxError(message);
}
}
const recordedSimplifyInContext = (context: StaticSymbol, value: any, depth: number) => {
try {
return simplifyInContext(context, value, depth);
} catch (e) {
this.reportError(e, context);
}
};
const result = this.errorRecorder ? recordedSimplifyInContext(context, value, 0) :
simplifyInContext(context, value, 0);
if (shouldIgnore(result)) {
return undefined;
}
return result;
}
private getTypeMetadata(type: StaticSymbol): {[key: string]: any} {
const resolvedSymbol = this.symbolResolver.resolveSymbol(type);
return resolvedSymbol && resolvedSymbol.metadata ? resolvedSymbol.metadata :
{__symbolic: 'class'};
}
}
function expandedMessage(error: any): string {
switch (error.message) {
case 'Reference to non-exported class':
if (error.context && error.context.className) {
return `Reference to a non-exported class ${error.context.className}. Consider exporting the class`;
}
break;
case 'Variable not initialized':
return 'Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler';
case 'Destructuring not supported':
return 'Referencing an exported destructured variable or constant is not supported by the template compiler. Consider simplifying this to avoid destructuring';
case 'Could not resolve type':
if (error.context && error.context.typeName) {
return `Could not resolve type ${error.context.typeName}`;
}
break;
case 'Function call not supported':
let prefix =
error.context && error.context.name ? `Calling function '${error.context.name}', f` : 'F';
return prefix +
'unction calls are not supported. Consider replacing the function or lambda with a reference to an exported function';
case 'Reference to a local symbol':
if (error.context && error.context.name) {
return `Reference to a local (non-exported) symbol '${error.context.name}'. Consider exporting the symbol`;
}
break;
}
return error.message;
}
function produceErrorMessage(error: any): string {
return `Error encountered resolving symbol values statically. ${expandedMessage(error)}`;
}
function mapStringMap(input: {[key: string]: any}, transform: (value: any, key: string) => any):
{[key: string]: any} {
if (!input) return {};
const result: {[key: string]: any} = {};
Object.keys(input).forEach((key) => {
const value = transform(input[key], key);
if (!shouldIgnore(value)) {
if (HIDDEN_KEY.test(key)) {
Object.defineProperty(result, key, {enumerable: false, configurable: true, value: value});
} else {
result[key] = value;
}
}
});
return result;
}
function isPrimitive(o: any): boolean {
return o === null || (typeof o !== 'function' && typeof o !== 'object');
}
interface BindingScopeBuilder {
define(name: string, value: any): BindingScopeBuilder;
done(): BindingScope;
}
abstract class BindingScope {
abstract resolve(name: string): any;
public static missing = {};
public static empty: BindingScope = {resolve: name => BindingScope.missing};
public static build(): BindingScopeBuilder {
const current = new Map<string, any>();
return {
define: function(name, value) {
current.set(name, value);
return this;
},
done: function() {
return current.size > 0 ? new PopulatedScope(current) : BindingScope.empty;
}
};
}
}
class PopulatedScope extends BindingScope {
constructor(private bindings: Map<string, any>) { super(); }
resolve(name: string): any {
return this.bindings.has(name) ? this.bindings.get(name) : BindingScope.missing;
}
}
function positionalError(message: string, fileName: string, line: number, column: number): Error {
const result = new Error(message);
(result as any).fileName = fileName;
(result as any).line = line;
(result as any).column = column;
return result;
}

View File

@ -0,0 +1,43 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* A token representing the a reference to a static type.
*
* This token is unique for a filePath and name and can be used as a hash table key.
*/
export class StaticSymbol {
constructor(public filePath: string, public name: string, public members: string[]) {}
assertNoMembers() {
if (this.members.length) {
throw new Error(
`Illegal state: symbol without members expected, but got ${JSON.stringify(this)}.`);
}
}
}
/**
* A cache of static symbol used by the StaticReflector to return the same symbol for the
* same symbol values.
*/
export class StaticSymbolCache {
private cache = new Map<string, StaticSymbol>();
get(declarationFile: string, name: string, members?: string[]): StaticSymbol {
members = members || [];
const memberSuffix = members.length ? `.${ members.join('.')}` : '';
const key = `"${declarationFile}".${name}${memberSuffix}`;
let result = this.cache.get(key);
if (!result) {
result = new StaticSymbol(declarationFile, name, members);
this.cache.set(key, result);
}
return result;
}
}

View File

@ -0,0 +1,391 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SummaryResolver} from '../summary_resolver';
import {ValueTransformer, visitValue} from '../util';
import {StaticSymbol, StaticSymbolCache} from './static_symbol';
import {isNgFactoryFile} from './util';
export class ResolvedStaticSymbol {
constructor(public symbol: StaticSymbol, public metadata: any) {}
}
/**
* The host of the SymbolResolverHost disconnects the implementation from TypeScript / other
* language
* services and from underlying file systems.
*/
export interface StaticSymbolResolverHost {
/**
* Return a ModuleMetadata for the given module.
* Angular CLI will produce this metadata for a module whenever a .d.ts files is
* produced and the module has exported variables or classes with decorators. Module metadata can
* also be produced directly from TypeScript sources by using MetadataCollector in tools/metadata.
*
* @param modulePath is a string identifier for a module as an absolute path.
* @returns the metadata for the given module.
*/
getMetadataFor(modulePath: string): {[key: string]: any}[];
/**
* Converts a module name that is used in an `import` to a file path.
* I.e.
* `path/to/containingFile.ts` containing `import {...} from 'module-name'`.
*/
moduleNameToFileName(moduleName: string, containingFile: string): string /*|null*/;
}
const SUPPORTED_SCHEMA_VERSION = 3;
/**
* This class is responsible for loading metadata per symbol,
* and normalizing references between symbols.
*
* Internally, it only uses symbols without members,
* and deduces the values for symbols with members based
* on these symbols.
*/
export class StaticSymbolResolver {
private metadataCache = new Map<string, {[key: string]: any}>();
// Note: this will only contain StaticSymbols without members!
private resolvedSymbols = new Map<StaticSymbol, ResolvedStaticSymbol>();
private resolvedFilePaths = new Set<string>();
// Note: this will only contain StaticSymbols without members!
private importAs = new Map<StaticSymbol, StaticSymbol>();
constructor(
private host: StaticSymbolResolverHost, private staticSymbolCache: StaticSymbolCache,
private summaryResolver: SummaryResolver<StaticSymbol>,
private errorRecorder?: (error: any, fileName: string) => void) {}
resolveSymbol(staticSymbol: StaticSymbol): ResolvedStaticSymbol {
if (staticSymbol.members.length > 0) {
return this._resolveSymbolMembers(staticSymbol);
}
let result = this.resolvedSymbols.get(staticSymbol);
if (result) {
return result;
}
result = this._resolveSymbolFromSummary(staticSymbol);
if (result) {
return result;
}
// Note: Some users use libraries that were not compiled with ngc, i.e. they don't
// have summaries, only .d.ts files. So we always need to check both, the summary
// and metadata.
this._createSymbolsOf(staticSymbol.filePath);
result = this.resolvedSymbols.get(staticSymbol);
return result;
}
/**
* getImportAs produces a symbol that can be used to import the given symbol.
* The import might be different than the symbol if the symbol is exported from
* a library with a summary; in which case we want to import the symbol from the
* ngfactory re-export instead of directly to avoid introducing a direct dependency
* on an otherwise indirect dependency.
*
* @param staticSymbol the symbol for which to generate a import symbol
*/
getImportAs(staticSymbol: StaticSymbol): StaticSymbol {
if (staticSymbol.members.length) {
const baseSymbol = this.getStaticSymbol(staticSymbol.filePath, staticSymbol.name);
const baseImportAs = this.getImportAs(baseSymbol);
return baseImportAs ?
this.getStaticSymbol(baseImportAs.filePath, baseImportAs.name, staticSymbol.members) :
null;
}
let result = this.summaryResolver.getImportAs(staticSymbol);
if (!result) {
result = this.importAs.get(staticSymbol);
}
return result;
}
/**
* getTypeArity returns the number of generic type parameters the given symbol
* has. If the symbol is not a type the result is null.
*/
getTypeArity(staticSymbol: StaticSymbol): number /*|null*/ {
// If the file is a factory file, don't resolve the symbol as doing so would
// cause the metadata for an factory file to be loaded which doesn't exist.
// All references to generated classes must include the correct arity whenever
// generating code.
if (isNgFactoryFile(staticSymbol.filePath)) {
return null;
}
let resolvedSymbol = this.resolveSymbol(staticSymbol);
while (resolvedSymbol && resolvedSymbol.metadata instanceof StaticSymbol) {
resolvedSymbol = this.resolveSymbol(resolvedSymbol.metadata);
}
return (resolvedSymbol && resolvedSymbol.metadata && resolvedSymbol.metadata.arity) || null;
}
recordImportAs(sourceSymbol: StaticSymbol, targetSymbol: StaticSymbol) {
sourceSymbol.assertNoMembers();
targetSymbol.assertNoMembers();
this.importAs.set(sourceSymbol, targetSymbol);
}
private _resolveSymbolMembers(staticSymbol: StaticSymbol): ResolvedStaticSymbol {
const members = staticSymbol.members;
const baseResolvedSymbol =
this.resolveSymbol(this.getStaticSymbol(staticSymbol.filePath, staticSymbol.name));
if (!baseResolvedSymbol) {
return null;
}
const baseMetadata = baseResolvedSymbol.metadata;
if (baseMetadata instanceof StaticSymbol) {
return new ResolvedStaticSymbol(
staticSymbol, this.getStaticSymbol(baseMetadata.filePath, baseMetadata.name, members));
} else if (baseMetadata && baseMetadata.__symbolic === 'class') {
if (baseMetadata.statics && members.length === 1) {
return new ResolvedStaticSymbol(staticSymbol, baseMetadata.statics[members[0]]);
}
} else {
let value = baseMetadata;
for (let i = 0; i < members.length && value; i++) {
value = value[members[i]];
}
return new ResolvedStaticSymbol(staticSymbol, value);
}
return null;
}
private _resolveSymbolFromSummary(staticSymbol: StaticSymbol): ResolvedStaticSymbol {
const summary = this.summaryResolver.resolveSummary(staticSymbol);
return summary ? new ResolvedStaticSymbol(staticSymbol, summary.metadata) : null;
}
/**
* getStaticSymbol produces a Type whose metadata is known but whose implementation is not loaded.
* All types passed to the StaticResolver should be pseudo-types returned by this method.
*
* @param declarationFile the absolute path of the file where the symbol is declared
* @param name the name of the type.
* @param members a symbol for a static member of the named type
*/
getStaticSymbol(declarationFile: string, name: string, members?: string[]): StaticSymbol {
return this.staticSymbolCache.get(declarationFile, name, members);
}
getSymbolsOf(filePath: string): StaticSymbol[] {
// Note: Some users use libraries that were not compiled with ngc, i.e. they don't
// have summaries, only .d.ts files. So we always need to check both, the summary
// and metadata.
let symbols = new Set<StaticSymbol>(this.summaryResolver.getSymbolsOf(filePath));
this._createSymbolsOf(filePath);
this.resolvedSymbols.forEach((resolvedSymbol) => {
if (resolvedSymbol.symbol.filePath === filePath) {
symbols.add(resolvedSymbol.symbol);
}
});
return Array.from(symbols);
}
private _createSymbolsOf(filePath: string) {
if (this.resolvedFilePaths.has(filePath)) {
return;
}
this.resolvedFilePaths.add(filePath);
const resolvedSymbols: ResolvedStaticSymbol[] = [];
const metadata = this.getModuleMetadata(filePath);
if (metadata['metadata']) {
// handle direct declarations of the symbol
const topLevelSymbolNames =
new Set<string>(Object.keys(metadata['metadata']).map(unescapeIdentifier));
Object.keys(metadata['metadata']).forEach((metadataKey) => {
const symbolMeta = metadata['metadata'][metadataKey];
const name = unescapeIdentifier(metadataKey);
const canonicalSymbol = this.getStaticSymbol(filePath, name);
if (metadata['importAs']) {
// Index bundle indexes should use the importAs module name instead of a reference
// to the .d.ts file directly.
const importSymbol = this.getStaticSymbol(metadata['importAs'], name);
this.recordImportAs(canonicalSymbol, importSymbol);
}
resolvedSymbols.push(
this.createResolvedSymbol(canonicalSymbol, topLevelSymbolNames, symbolMeta));
});
}
// handle the symbols in one of the re-export location
if (metadata['exports']) {
for (const moduleExport of metadata['exports']) {
// handle the symbols in the list of explicitly re-exported symbols.
if (moduleExport.export) {
moduleExport.export.forEach((exportSymbol: any) => {
let symbolName: string;
if (typeof exportSymbol === 'string') {
symbolName = exportSymbol;
} else {
symbolName = exportSymbol.as;
}
symbolName = unescapeIdentifier(symbolName);
let symName = symbolName;
if (typeof exportSymbol !== 'string') {
symName = unescapeIdentifier(exportSymbol.name);
}
const resolvedModule = this.resolveModule(moduleExport.from, filePath);
if (resolvedModule) {
const targetSymbol = this.getStaticSymbol(resolvedModule, symName);
const sourceSymbol = this.getStaticSymbol(filePath, symbolName);
resolvedSymbols.push(this.createExport(sourceSymbol, targetSymbol));
}
});
} else {
// handle the symbols via export * directives.
const resolvedModule = this.resolveModule(moduleExport.from, filePath);
if (resolvedModule) {
const nestedExports = this.getSymbolsOf(resolvedModule);
nestedExports.forEach((targetSymbol) => {
const sourceSymbol = this.getStaticSymbol(filePath, targetSymbol.name);
resolvedSymbols.push(this.createExport(sourceSymbol, targetSymbol));
});
}
}
}
}
resolvedSymbols.forEach(
(resolvedSymbol) => this.resolvedSymbols.set(resolvedSymbol.symbol, resolvedSymbol));
}
private createResolvedSymbol(
sourceSymbol: StaticSymbol, topLevelSymbolNames: Set<string>,
metadata: any): ResolvedStaticSymbol {
const self = this;
class ReferenceTransformer extends ValueTransformer {
visitStringMap(map: {[key: string]: any}, functionParams: string[]): any {
const symbolic = map['__symbolic'];
if (symbolic === 'function') {
const oldLen = functionParams.length;
functionParams.push(...(map['parameters'] || []));
const result = super.visitStringMap(map, functionParams);
functionParams.length = oldLen;
return result;
} else if (symbolic === 'reference') {
const module = map['module'];
const name = map['name'] ? unescapeIdentifier(map['name']) : map['name'];
if (!name) {
return null;
}
let filePath: string;
if (module) {
filePath = self.resolveModule(module, sourceSymbol.filePath);
if (!filePath) {
return {
__symbolic: 'error',
message: `Could not resolve ${module} relative to ${sourceSymbol.filePath}.`
};
}
return self.getStaticSymbol(filePath, name);
} else if (functionParams.indexOf(name) >= 0) {
// reference to a function parameter
return {__symbolic: 'reference', name: name};
} else {
if (topLevelSymbolNames.has(name)) {
return self.getStaticSymbol(sourceSymbol.filePath, name);
}
// ambient value
null;
}
} else {
return super.visitStringMap(map, functionParams);
}
}
}
const transformedMeta = visitValue(metadata, new ReferenceTransformer(), []);
if (transformedMeta instanceof StaticSymbol) {
return this.createExport(sourceSymbol, transformedMeta);
}
return new ResolvedStaticSymbol(sourceSymbol, transformedMeta);
}
private createExport(sourceSymbol: StaticSymbol, targetSymbol: StaticSymbol):
ResolvedStaticSymbol {
sourceSymbol.assertNoMembers();
targetSymbol.assertNoMembers();
if (this.summaryResolver.isLibraryFile(sourceSymbol.filePath)) {
// This case is for an ng library importing symbols from a plain ts library
// transitively.
// Note: We rely on the fact that we discover symbols in the direction
// from source files to library files
this.importAs.set(targetSymbol, this.getImportAs(sourceSymbol) || sourceSymbol);
}
return new ResolvedStaticSymbol(sourceSymbol, targetSymbol);
}
private reportError(error: Error, context: StaticSymbol, path?: string) {
if (this.errorRecorder) {
this.errorRecorder(error, (context && context.filePath) || path);
} else {
throw error;
}
}
/**
* @param module an absolute path to a module file.
*/
private getModuleMetadata(module: string): {[key: string]: any} {
let moduleMetadata = this.metadataCache.get(module);
if (!moduleMetadata) {
const moduleMetadatas = this.host.getMetadataFor(module);
if (moduleMetadatas) {
let maxVersion = -1;
moduleMetadatas.forEach((md) => {
if (md['version'] > maxVersion) {
maxVersion = md['version'];
moduleMetadata = md;
}
});
}
if (!moduleMetadata) {
moduleMetadata =
{__symbolic: 'module', version: SUPPORTED_SCHEMA_VERSION, module: module, metadata: {}};
}
if (moduleMetadata['version'] != SUPPORTED_SCHEMA_VERSION) {
const errorMessage = moduleMetadata['version'] == 2 ?
`Unsupported metadata version ${moduleMetadata['version']} for module ${module}. This module should be compiled with a newer version of ngc` :
`Metadata version mismatch for module ${module}, found version ${moduleMetadata['version']}, expected ${SUPPORTED_SCHEMA_VERSION}`;
this.reportError(new Error(errorMessage), null);
}
this.metadataCache.set(module, moduleMetadata);
}
return moduleMetadata;
}
getSymbolByModule(module: string, symbolName: string, containingFile?: string): StaticSymbol {
const filePath = this.resolveModule(module, containingFile);
if (!filePath) {
this.reportError(
new Error(`Could not resolve module ${module}${containingFile ? ` relative to $ {
containingFile
} `: ''}`),
null);
return this.getStaticSymbol(`ERROR:${module}`, symbolName);
}
return this.getStaticSymbol(filePath, symbolName);
}
private resolveModule(module: string, containingFile: string): string {
try {
return this.host.moduleNameToFileName(module, containingFile);
} catch (e) {
console.error(`Could not resolve module '${module}' relative to file ${containingFile}`);
this.reportError(e, null, containingFile);
}
}
}
// Remove extra underscore from escaped identifier.
// See https://github.com/Microsoft/TypeScript/blob/master/src/compiler/utilities.ts
export function unescapeIdentifier(identifier: string): string {
return identifier.startsWith('___') ? identifier.substr(1) : identifier;
}

View File

@ -0,0 +1,96 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Summary, SummaryResolver} from '../summary_resolver';
import {StaticSymbol, StaticSymbolCache} from './static_symbol';
import {deserializeSummaries} from './summary_serializer';
import {ngfactoryFilePath, stripNgFactory, summaryFileName} from './util';
export interface AotSummaryResolverHost {
/**
* Loads an NgModule/Directive/Pipe summary file
*/
loadSummary(filePath: string): string /*|null*/;
/**
* Returns whether a file is a source file or not.
*/
isSourceFile(sourceFilePath: string): boolean;
/**
* Returns the output file path of a source file.
* E.g.
* `some_file.ts` -> `some_file.d.ts`
*/
getOutputFileName(sourceFilePath: string): string;
}
export class AotSummaryResolver implements SummaryResolver<StaticSymbol> {
// Note: this will only contain StaticSymbols without members!
private summaryCache = new Map<StaticSymbol, Summary<StaticSymbol>>();
private loadedFilePaths = new Set<string>();
// Note: this will only contain StaticSymbols without members!
private importAs = new Map<StaticSymbol, StaticSymbol>();
constructor(private host: AotSummaryResolverHost, private staticSymbolCache: StaticSymbolCache) {}
isLibraryFile(filePath: string): boolean {
// Note: We need to strip the .ngfactory. file path,
// so this method also works for generated files
// (for which host.isSourceFile will always return false).
return !this.host.isSourceFile(stripNgFactory(filePath));
}
getLibraryFileName(filePath: string) { return this.host.getOutputFileName(filePath); }
resolveSummary(staticSymbol: StaticSymbol): Summary<StaticSymbol> {
staticSymbol.assertNoMembers();
let summary = this.summaryCache.get(staticSymbol);
if (!summary) {
this._loadSummaryFile(staticSymbol.filePath);
summary = this.summaryCache.get(staticSymbol);
}
return summary;
}
getSymbolsOf(filePath: string): StaticSymbol[] {
this._loadSummaryFile(filePath);
return Array.from(this.summaryCache.keys()).filter((symbol) => symbol.filePath === filePath);
}
getImportAs(staticSymbol: StaticSymbol): StaticSymbol {
staticSymbol.assertNoMembers();
return this.importAs.get(staticSymbol);
}
private _loadSummaryFile(filePath: string) {
if (this.loadedFilePaths.has(filePath)) {
return;
}
this.loadedFilePaths.add(filePath);
if (this.isLibraryFile(filePath)) {
const summaryFilePath = summaryFileName(filePath);
let json: string;
try {
json = this.host.loadSummary(summaryFilePath);
} catch (e) {
console.error(`Error loading summary file ${summaryFilePath}`);
throw e;
}
if (json) {
const {summaries, importAs} = deserializeSummaries(this.staticSymbolCache, json);
summaries.forEach((summary) => this.summaryCache.set(summary.symbol, summary));
importAs.forEach((importAs) => {
this.importAs.set(
importAs.symbol,
this.staticSymbolCache.get(ngfactoryFilePath(filePath), importAs.importAs));
});
}
}
}
}

View File

@ -0,0 +1,192 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CompileNgModuleSummary, CompileSummaryKind, CompileTypeSummary} from '../compile_metadata';
import {Summary, SummaryResolver} from '../summary_resolver';
import {ValueTransformer, visitValue} from '../util';
import {StaticSymbol, StaticSymbolCache} from './static_symbol';
import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolver';
export function serializeSummaries(
summaryResolver: SummaryResolver<StaticSymbol>, symbolResolver: StaticSymbolResolver,
symbols: ResolvedStaticSymbol[], types: CompileTypeSummary[]):
{json: string, exportAs: {symbol: StaticSymbol, exportAs: string}[]} {
const serializer = new Serializer(symbolResolver, summaryResolver);
// for symbols, we use everything except for the class metadata itself
// (we keep the statics though), as the class metadata is contained in the
// CompileTypeSummary.
symbols.forEach(
(resolvedSymbol) => serializer.addOrMergeSummary(
{symbol: resolvedSymbol.symbol, metadata: resolvedSymbol.metadata}));
// Add summaries that are referenced by the given symbols (transitively)
// Note: the serializer.symbols array might be growing while
// we execute the loop!
for (let processedIndex = 0; processedIndex < serializer.symbols.length; processedIndex++) {
const symbol = serializer.symbols[processedIndex];
if (summaryResolver.isLibraryFile(symbol.filePath)) {
let summary = summaryResolver.resolveSummary(symbol);
if (!summary) {
// some symbols might originate from a plain typescript library
// that just exported .d.ts and .metadata.json files, i.e. where no summary
// files were created.
const resolvedSymbol = symbolResolver.resolveSymbol(symbol);
if (resolvedSymbol) {
summary = {symbol: resolvedSymbol.symbol, metadata: resolvedSymbol.metadata};
}
}
if (summary) {
serializer.addOrMergeSummary(summary);
}
}
}
// Add type summaries.
// Note: We don't add the summaries of all referenced symbols as for the ResolvedSymbols,
// as the type summaries already contain the transitive data that they require
// (in a minimal way).
types.forEach((typeSummary) => {
serializer.addOrMergeSummary(
{symbol: typeSummary.type.reference, metadata: {__symbolic: 'class'}, type: typeSummary});
if (typeSummary.summaryKind === CompileSummaryKind.NgModule) {
const ngModuleSummary = <CompileNgModuleSummary>typeSummary;
ngModuleSummary.exportedDirectives.concat(ngModuleSummary.exportedPipes).forEach((id) => {
const symbol: StaticSymbol = id.reference;
if (summaryResolver.isLibraryFile(symbol.filePath)) {
const summary = summaryResolver.resolveSummary(symbol);
if (summary) {
serializer.addOrMergeSummary(summary);
}
}
});
}
});
return serializer.serialize();
}
export function deserializeSummaries(symbolCache: StaticSymbolCache, json: string):
{summaries: Summary<StaticSymbol>[], importAs: {symbol: StaticSymbol, importAs: string}[]} {
const deserializer = new Deserializer(symbolCache);
return deserializer.deserialize(json);
}
class Serializer extends ValueTransformer {
// Note: This only contains symbols without members.
symbols: StaticSymbol[] = [];
private indexBySymbol = new Map<StaticSymbol, number>();
// This now contains a `__symbol: number` in the place of
// StaticSymbols, but otherwise has the same shape as the original objects.
private processedSummaryBySymbol = new Map<StaticSymbol, any>();
private processedSummaries: any[] = [];
constructor(
private symbolResolver: StaticSymbolResolver,
private summaryResolver: SummaryResolver<StaticSymbol>) {
super();
}
addOrMergeSummary(summary: Summary<StaticSymbol>) {
let symbolMeta = summary.metadata;
if (symbolMeta && symbolMeta.__symbolic === 'class') {
// For classes, we only keep their statics and arity, but not the metadata
// of the class itself as that has been captured already via other summaries
// (e.g. DirectiveSummary, ...).
symbolMeta = {__symbolic: 'class', statics: symbolMeta.statics, arity: symbolMeta.arity};
}
let processedSummary = this.processedSummaryBySymbol.get(summary.symbol);
if (!processedSummary) {
processedSummary = this.processValue({symbol: summary.symbol});
this.processedSummaries.push(processedSummary);
this.processedSummaryBySymbol.set(summary.symbol, processedSummary);
}
// Note: == on purpose to compare with undefined!
if (processedSummary.metadata == null && symbolMeta != null) {
processedSummary.metadata = this.processValue(symbolMeta);
}
// Note: == on purpose to compare with undefined!
if (processedSummary.type == null && summary.type != null) {
processedSummary.type = this.processValue(summary.type);
}
}
serialize(): {json: string, exportAs: {symbol: StaticSymbol, exportAs: string}[]} {
const exportAs: {symbol: StaticSymbol, exportAs: string}[] = [];
const json = JSON.stringify({
summaries: this.processedSummaries,
symbols: this.symbols.map((symbol, index) => {
symbol.assertNoMembers();
let importAs: string;
if (this.summaryResolver.isLibraryFile(symbol.filePath)) {
importAs = `${symbol.name}_${index}`;
exportAs.push({symbol, exportAs: importAs});
}
return {
__symbol: index,
name: symbol.name,
// We convert the source filenames tinto output filenames,
// as the generated summary file will be used when teh current
// compilation unit is used as a library
filePath: this.summaryResolver.getLibraryFileName(symbol.filePath),
importAs: importAs
};
})
});
return {json, exportAs};
}
private processValue(value: any): any { return visitValue(value, this, null); }
visitOther(value: any, context: any): any {
if (value instanceof StaticSymbol) {
const baseSymbol = this.symbolResolver.getStaticSymbol(value.filePath, value.name);
let index = this.indexBySymbol.get(baseSymbol);
// Note: == on purpose to compare with undefined!
if (index == null) {
index = this.indexBySymbol.size;
this.indexBySymbol.set(baseSymbol, index);
this.symbols.push(baseSymbol);
}
return {__symbol: index, members: value.members};
}
}
}
class Deserializer extends ValueTransformer {
private symbols: StaticSymbol[];
constructor(private symbolCache: StaticSymbolCache) { super(); }
deserialize(json: string):
{summaries: Summary<StaticSymbol>[], importAs: {symbol: StaticSymbol, importAs: string}[]} {
const data: {summaries: any[], symbols: any[]} = JSON.parse(json);
const importAs: {symbol: StaticSymbol, importAs: string}[] = [];
this.symbols = [];
data.symbols.forEach((serializedSymbol) => {
const symbol = this.symbolCache.get(serializedSymbol.filePath, serializedSymbol.name);
this.symbols.push(symbol);
if (serializedSymbol.importAs) {
importAs.push({symbol: symbol, importAs: serializedSymbol.importAs});
}
});
const summaries = visitValue(data.summaries, this, null);
return {summaries, importAs};
}
visitStringMap(map: {[key: string]: any}, context: any): any {
if ('__symbol' in map) {
const baseSymbol = this.symbols[map['__symbol']];
const members = map['members'];
return members.length ? this.symbolCache.get(baseSymbol.filePath, baseSymbol.name, members) :
baseSymbol;
} else {
return super.visitStringMap(map, context);
}
}
}

View File

@ -0,0 +1,42 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const STRIP_SRC_FILE_SUFFIXES = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const NG_FACTORY = /\.ngfactory\./;
export function ngfactoryFilePath(filePath: string): string {
const urlWithSuffix = splitTypescriptSuffix(filePath);
return `${urlWithSuffix[0]}.ngfactory${urlWithSuffix[1]}`;
}
export function stripNgFactory(filePath: string): string {
return filePath.replace(NG_FACTORY, '.');
}
export function isNgFactoryFile(filePath: string): boolean {
return NG_FACTORY.test(filePath);
}
export function splitTypescriptSuffix(path: string): string[] {
if (path.endsWith('.d.ts')) {
return [path.slice(0, -5), '.ts'];
}
const lastDot = path.lastIndexOf('.');
if (lastDot !== -1) {
return [path.substring(0, lastDot), path.substring(lastDot)];
}
return [path, ''];
}
export function summaryFileName(fileName: string): string {
const fileNameWithoutSuffix = fileName.replace(STRIP_SRC_FILE_SUFFIXES, '');
return `${fileNameWithoutSuffix}.ngsummary.json`;
}

View File

@ -0,0 +1,47 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {isDevMode} from '@angular/core';
export function assertArrayOfStrings(identifier: string, value: any) {
if (!isDevMode() || value == null) {
return;
}
if (!Array.isArray(value)) {
throw new Error(`Expected '${identifier}' to be an array of strings.`);
}
for (let i = 0; i < value.length; i += 1) {
if (typeof value[i] !== 'string') {
throw new Error(`Expected '${identifier}' to be an array of strings.`);
}
}
}
const INTERPOLATION_BLACKLIST_REGEXPS = [
/^\s*$/, // empty
/[<>]/, // html tag
/^[{}]$/, // i18n expansion
/&(#|[a-z])/i, // character reference,
/^\/\//, // comment
];
export function assertInterpolationSymbols(identifier: string, value: any): void {
if (value != null && !(Array.isArray(value) && value.length == 2)) {
throw new Error(`Expected '${identifier}' to be an array, [start, end].`);
} else if (isDevMode() && value != null) {
const start = value[0] as string;
const end = value[1] as string;
// black list checking
INTERPOLATION_BLACKLIST_REGEXPS.forEach(regexp => {
if (regexp.test(start) || regexp.test(end)) {
throw new Error(`['${start}', '${end}'] contains unusable interpolation symbol.`);
}
});
}
}

View File

@ -0,0 +1,89 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export const $EOF = 0;
export const $TAB = 9;
export const $LF = 10;
export const $VTAB = 11;
export const $FF = 12;
export const $CR = 13;
export const $SPACE = 32;
export const $BANG = 33;
export const $DQ = 34;
export const $HASH = 35;
export const $$ = 36;
export const $PERCENT = 37;
export const $AMPERSAND = 38;
export const $SQ = 39;
export const $LPAREN = 40;
export const $RPAREN = 41;
export const $STAR = 42;
export const $PLUS = 43;
export const $COMMA = 44;
export const $MINUS = 45;
export const $PERIOD = 46;
export const $SLASH = 47;
export const $COLON = 58;
export const $SEMICOLON = 59;
export const $LT = 60;
export const $EQ = 61;
export const $GT = 62;
export const $QUESTION = 63;
export const $0 = 48;
export const $9 = 57;
export const $A = 65;
export const $E = 69;
export const $F = 70;
export const $X = 88;
export const $Z = 90;
export const $LBRACKET = 91;
export const $BACKSLASH = 92;
export const $RBRACKET = 93;
export const $CARET = 94;
export const $_ = 95;
export const $a = 97;
export const $e = 101;
export const $f = 102;
export const $n = 110;
export const $r = 114;
export const $t = 116;
export const $u = 117;
export const $v = 118;
export const $x = 120;
export const $z = 122;
export const $LBRACE = 123;
export const $BAR = 124;
export const $RBRACE = 125;
export const $NBSP = 160;
export const $PIPE = 124;
export const $TILDA = 126;
export const $AT = 64;
export const $BT = 96;
export function isWhitespace(code: number): boolean {
return (code >= $TAB && code <= $SPACE) || (code == $NBSP);
}
export function isDigit(code: number): boolean {
return $0 <= code && code <= $9;
}
export function isAsciiLetter(code: number): boolean {
return code >= $a && code <= $z || code >= $A && code <= $Z;
}
export function isAsciiHexDigit(code: number): boolean {
return code >= $a && code <= $f || code >= $A && code <= $F || isDigit(code);
}

View File

@ -0,0 +1,740 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectionStrategy, ComponentFactory, RendererType2, SchemaMetadata, Type, ViewEncapsulation, ɵLifecycleHooks, ɵreflector, ɵstringify as stringify} from '@angular/core';
import {StaticSymbol} from './aot/static_symbol';
import {CssSelector} from './selector';
import {splitAtColon} from './util';
// group 0: "[prop] or (event) or @trigger"
// group 1: "prop" from "[prop]"
// group 2: "event" from "(event)"
// group 3: "@trigger" from "@trigger"
const HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))|(\@[-\w]+)$/;
export class CompileAnimationEntryMetadata {
constructor(
public name: string = null, public definitions: CompileAnimationStateMetadata[] = null) {}
}
export abstract class CompileAnimationStateMetadata {}
export class CompileAnimationStateDeclarationMetadata extends CompileAnimationStateMetadata {
constructor(public stateNameExpr: string, public styles: CompileAnimationStyleMetadata) {
super();
}
}
export class CompileAnimationStateTransitionMetadata extends CompileAnimationStateMetadata {
constructor(
public stateChangeExpr: string|StaticSymbol|((stateA: string, stateB: string) => boolean),
public steps: CompileAnimationMetadata) {
super();
}
}
export abstract class CompileAnimationMetadata {}
export class CompileAnimationKeyframesSequenceMetadata extends CompileAnimationMetadata {
constructor(public steps: CompileAnimationStyleMetadata[] = []) { super(); }
}
export class CompileAnimationStyleMetadata extends CompileAnimationMetadata {
constructor(
public offset: number, public styles: Array<string|{[key: string]: string | number}> = null) {
super();
}
}
export class CompileAnimationAnimateMetadata extends CompileAnimationMetadata {
constructor(
public timings: string|number = 0, public styles: CompileAnimationStyleMetadata|
CompileAnimationKeyframesSequenceMetadata = null) {
super();
}
}
export abstract class CompileAnimationWithStepsMetadata extends CompileAnimationMetadata {
constructor(public steps: CompileAnimationMetadata[] = null) { super(); }
}
export class CompileAnimationSequenceMetadata extends CompileAnimationWithStepsMetadata {
constructor(steps: CompileAnimationMetadata[] = null) { super(steps); }
}
export class CompileAnimationGroupMetadata extends CompileAnimationWithStepsMetadata {
constructor(steps: CompileAnimationMetadata[] = null) { super(steps); }
}
function _sanitizeIdentifier(name: string): string {
return name.replace(/\W/g, '_');
}
let _anonymousTypeIndex = 0;
export function identifierName(compileIdentifier: CompileIdentifierMetadata): string {
if (!compileIdentifier || !compileIdentifier.reference) {
return null;
}
const ref = compileIdentifier.reference;
if (ref instanceof StaticSymbol) {
return ref.name;
}
if (ref['__anonymousType']) {
return ref['__anonymousType'];
}
let identifier = stringify(ref);
if (identifier.indexOf('(') >= 0) {
// case: anonymous functions!
identifier = `anonymous_${_anonymousTypeIndex++}`;
ref['__anonymousType'] = identifier;
} else {
identifier = _sanitizeIdentifier(identifier);
}
return identifier;
}
export function identifierModuleUrl(compileIdentifier: CompileIdentifierMetadata): string {
const ref = compileIdentifier.reference;
if (ref instanceof StaticSymbol) {
return ref.filePath;
}
return ɵreflector.importUri(ref);
}
export function viewClassName(compType: any, embeddedTemplateIndex: number): string {
return `View_${identifierName({reference: compType})}_${embeddedTemplateIndex}`;
}
export function rendererTypeName(compType: any): string {
return `RenderType_${identifierName({reference: compType})}`;
}
export function hostViewClassName(compType: any): string {
return `HostView_${identifierName({reference: compType})}`;
}
export function dirWrapperClassName(dirType: any) {
return `Wrapper_${identifierName({reference: dirType})}`;
}
export function componentFactoryName(compType: any): string {
return `${identifierName({reference: compType})}NgFactory`;
}
export interface ProxyClass { setDelegate(delegate: any): void; }
export interface CompileIdentifierMetadata { reference: any; }
export enum CompileSummaryKind {
Pipe,
Directive,
NgModule,
Injectable
}
/**
* A CompileSummary is the data needed to use a directive / pipe / module
* in other modules / components. However, this data is not enough to compile
* the directive / module itself.
*/
export interface CompileTypeSummary {
summaryKind: CompileSummaryKind;
type: CompileTypeMetadata;
}
export interface CompileDiDependencyMetadata {
isAttribute?: boolean;
isSelf?: boolean;
isHost?: boolean;
isSkipSelf?: boolean;
isOptional?: boolean;
isValue?: boolean;
token?: CompileTokenMetadata;
value?: any;
}
export interface CompileProviderMetadata {
token: CompileTokenMetadata;
useClass?: CompileTypeMetadata;
useValue?: any;
useExisting?: CompileTokenMetadata;
useFactory?: CompileFactoryMetadata;
deps?: CompileDiDependencyMetadata[];
multi?: boolean;
}
export interface CompileFactoryMetadata extends CompileIdentifierMetadata {
diDeps: CompileDiDependencyMetadata[];
reference: any;
}
export function tokenName(token: CompileTokenMetadata) {
return token.value != null ? _sanitizeIdentifier(token.value) : identifierName(token.identifier);
}
export function tokenReference(token: CompileTokenMetadata) {
if (token.identifier != null) {
return token.identifier.reference;
} else {
return token.value;
}
}
export interface CompileTokenMetadata {
value?: any;
identifier?: CompileIdentifierMetadata|CompileTypeMetadata;
}
/**
* Metadata regarding compilation of a type.
*/
export interface CompileTypeMetadata extends CompileIdentifierMetadata {
diDeps: CompileDiDependencyMetadata[];
lifecycleHooks: ɵLifecycleHooks[];
reference: any;
}
export interface CompileQueryMetadata {
selectors: Array<CompileTokenMetadata>;
descendants: boolean;
first: boolean;
propertyName: string;
read: CompileTokenMetadata;
}
/**
* Metadata about a stylesheet
*/
export class CompileStylesheetMetadata {
moduleUrl: string;
styles: string[];
styleUrls: string[];
constructor(
{moduleUrl, styles,
styleUrls}: {moduleUrl?: string, styles?: string[], styleUrls?: string[]} = {}) {
this.moduleUrl = moduleUrl;
this.styles = _normalizeArray(styles);
this.styleUrls = _normalizeArray(styleUrls);
}
}
/**
* Summary Metadata regarding compilation of a template.
*/
export interface CompileTemplateSummary {
animations: string[];
ngContentSelectors: string[];
encapsulation: ViewEncapsulation;
}
/**
* Metadata regarding compilation of a template.
*/
export class CompileTemplateMetadata {
encapsulation: ViewEncapsulation;
template: string;
templateUrl: string;
styles: string[];
styleUrls: string[];
externalStylesheets: CompileStylesheetMetadata[];
animations: any[];
ngContentSelectors: string[];
interpolation: [string, string];
constructor(
{encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets, animations,
ngContentSelectors, interpolation}: {
encapsulation?: ViewEncapsulation,
template?: string,
templateUrl?: string,
styles?: string[],
styleUrls?: string[],
externalStylesheets?: CompileStylesheetMetadata[],
ngContentSelectors?: string[],
animations?: any[],
interpolation?: [string, string],
} = {}) {
this.encapsulation = encapsulation;
this.template = template;
this.templateUrl = templateUrl;
this.styles = _normalizeArray(styles);
this.styleUrls = _normalizeArray(styleUrls);
this.externalStylesheets = _normalizeArray(externalStylesheets);
this.animations = animations ? flatten(animations) : [];
this.ngContentSelectors = ngContentSelectors || [];
if (interpolation && interpolation.length != 2) {
throw new Error(`'interpolation' should have a start and an end symbol.`);
}
this.interpolation = interpolation;
}
toSummary(): CompileTemplateSummary {
return {
animations: this.animations.map(anim => anim.name),
ngContentSelectors: this.ngContentSelectors,
encapsulation: this.encapsulation,
};
}
}
export interface CompileEntryComponentMetadata {
componentType: any;
componentFactory: StaticSymbol|ComponentFactory<any>;
}
// Note: This should only use interfaces as nested data types
// as we need to be able to serialize this from/to JSON!
export interface CompileDirectiveSummary extends CompileTypeSummary {
type: CompileTypeMetadata;
isComponent: boolean;
selector: string;
exportAs: string;
inputs: {[key: string]: string};
outputs: {[key: string]: string};
hostListeners: {[key: string]: string};
hostProperties: {[key: string]: string};
hostAttributes: {[key: string]: string};
providers: CompileProviderMetadata[];
viewProviders: CompileProviderMetadata[];
queries: CompileQueryMetadata[];
viewQueries: CompileQueryMetadata[];
entryComponents: CompileEntryComponentMetadata[];
changeDetection: ChangeDetectionStrategy;
template: CompileTemplateSummary;
componentViewType: StaticSymbol|ProxyClass;
rendererType: StaticSymbol|RendererType2;
componentFactory: StaticSymbol|ComponentFactory<any>;
}
/**
* Metadata regarding compilation of a directive.
*/
export class CompileDirectiveMetadata {
static create(
{isHost, type, isComponent, selector, exportAs, changeDetection, inputs, outputs, host,
providers, viewProviders, queries, viewQueries, entryComponents, template, componentViewType,
rendererType, componentFactory}: {
isHost?: boolean,
type?: CompileTypeMetadata,
isComponent?: boolean,
selector?: string,
exportAs?: string,
changeDetection?: ChangeDetectionStrategy,
inputs?: string[],
outputs?: string[],
host?: {[key: string]: string},
providers?: CompileProviderMetadata[],
viewProviders?: CompileProviderMetadata[],
queries?: CompileQueryMetadata[],
viewQueries?: CompileQueryMetadata[],
entryComponents?: CompileEntryComponentMetadata[],
template?: CompileTemplateMetadata,
componentViewType?: StaticSymbol|ProxyClass,
rendererType?: StaticSymbol|RendererType2,
componentFactory?: StaticSymbol|ComponentFactory<any>,
} = {}): CompileDirectiveMetadata {
const hostListeners: {[key: string]: string} = {};
const hostProperties: {[key: string]: string} = {};
const hostAttributes: {[key: string]: string} = {};
if (host != null) {
Object.keys(host).forEach(key => {
const value = host[key];
const matches = key.match(HOST_REG_EXP);
if (matches === null) {
hostAttributes[key] = value;
} else if (matches[1] != null) {
hostProperties[matches[1]] = value;
} else if (matches[2] != null) {
hostListeners[matches[2]] = value;
}
});
}
const inputsMap: {[key: string]: string} = {};
if (inputs != null) {
inputs.forEach((bindConfig: string) => {
// canonical syntax: `dirProp: elProp`
// if there is no `:`, use dirProp = elProp
const parts = splitAtColon(bindConfig, [bindConfig, bindConfig]);
inputsMap[parts[0]] = parts[1];
});
}
const outputsMap: {[key: string]: string} = {};
if (outputs != null) {
outputs.forEach((bindConfig: string) => {
// canonical syntax: `dirProp: elProp`
// if there is no `:`, use dirProp = elProp
const parts = splitAtColon(bindConfig, [bindConfig, bindConfig]);
outputsMap[parts[0]] = parts[1];
});
}
return new CompileDirectiveMetadata({
isHost,
type,
isComponent: !!isComponent, selector, exportAs, changeDetection,
inputs: inputsMap,
outputs: outputsMap,
hostListeners,
hostProperties,
hostAttributes,
providers,
viewProviders,
queries,
viewQueries,
entryComponents,
template,
componentViewType,
rendererType,
componentFactory,
});
}
isHost: boolean;
type: CompileTypeMetadata;
isComponent: boolean;
selector: string;
exportAs: string;
changeDetection: ChangeDetectionStrategy;
inputs: {[key: string]: string};
outputs: {[key: string]: string};
hostListeners: {[key: string]: string};
hostProperties: {[key: string]: string};
hostAttributes: {[key: string]: string};
providers: CompileProviderMetadata[];
viewProviders: CompileProviderMetadata[];
queries: CompileQueryMetadata[];
viewQueries: CompileQueryMetadata[];
entryComponents: CompileEntryComponentMetadata[];
template: CompileTemplateMetadata;
componentViewType: StaticSymbol|ProxyClass;
rendererType: StaticSymbol|RendererType2;
componentFactory: StaticSymbol|ComponentFactory<any>;
constructor({isHost, type, isComponent, selector, exportAs,
changeDetection, inputs, outputs, hostListeners, hostProperties,
hostAttributes, providers, viewProviders, queries, viewQueries,
entryComponents, template, componentViewType, rendererType, componentFactory}: {
isHost?: boolean,
type?: CompileTypeMetadata,
isComponent?: boolean,
selector?: string,
exportAs?: string,
changeDetection?: ChangeDetectionStrategy,
inputs?: {[key: string]: string},
outputs?: {[key: string]: string},
hostListeners?: {[key: string]: string},
hostProperties?: {[key: string]: string},
hostAttributes?: {[key: string]: string},
providers?: CompileProviderMetadata[],
viewProviders?: CompileProviderMetadata[],
queries?: CompileQueryMetadata[],
viewQueries?: CompileQueryMetadata[],
entryComponents?: CompileEntryComponentMetadata[],
template?: CompileTemplateMetadata,
componentViewType?: StaticSymbol|ProxyClass,
rendererType?: StaticSymbol|RendererType2,
componentFactory?: StaticSymbol|ComponentFactory<any>,
} = {}) {
this.isHost = !!isHost;
this.type = type;
this.isComponent = isComponent;
this.selector = selector;
this.exportAs = exportAs;
this.changeDetection = changeDetection;
this.inputs = inputs;
this.outputs = outputs;
this.hostListeners = hostListeners;
this.hostProperties = hostProperties;
this.hostAttributes = hostAttributes;
this.providers = _normalizeArray(providers);
this.viewProviders = _normalizeArray(viewProviders);
this.queries = _normalizeArray(queries);
this.viewQueries = _normalizeArray(viewQueries);
this.entryComponents = _normalizeArray(entryComponents);
this.template = template;
this.componentViewType = componentViewType;
this.rendererType = rendererType;
this.componentFactory = componentFactory;
}
toSummary(): CompileDirectiveSummary {
return {
summaryKind: CompileSummaryKind.Directive,
type: this.type,
isComponent: this.isComponent,
selector: this.selector,
exportAs: this.exportAs,
inputs: this.inputs,
outputs: this.outputs,
hostListeners: this.hostListeners,
hostProperties: this.hostProperties,
hostAttributes: this.hostAttributes,
providers: this.providers,
viewProviders: this.viewProviders,
queries: this.queries,
viewQueries: this.viewQueries,
entryComponents: this.entryComponents,
changeDetection: this.changeDetection,
template: this.template && this.template.toSummary(),
componentViewType: this.componentViewType,
rendererType: this.rendererType,
componentFactory: this.componentFactory
};
}
}
/**
* Construct {@link CompileDirectiveMetadata} from {@link ComponentTypeMetadata} and a selector.
*/
export function createHostComponentMeta(
hostTypeReference: any, compMeta: CompileDirectiveMetadata,
hostViewType: StaticSymbol | ProxyClass): CompileDirectiveMetadata {
const template = CssSelector.parse(compMeta.selector)[0].getMatchingElementTemplate();
return CompileDirectiveMetadata.create({
isHost: true,
type: {reference: hostTypeReference, diDeps: [], lifecycleHooks: []},
template: new CompileTemplateMetadata({
encapsulation: ViewEncapsulation.None,
template: template,
templateUrl: '',
styles: [],
styleUrls: [],
ngContentSelectors: [],
animations: []
}),
changeDetection: ChangeDetectionStrategy.Default,
inputs: [],
outputs: [],
host: {},
isComponent: true,
selector: '*',
providers: [],
viewProviders: [],
queries: [],
viewQueries: [],
componentViewType: hostViewType,
rendererType: {id: '__Host__', encapsulation: ViewEncapsulation.None, styles: [], data: {}}
});
}
export interface CompilePipeSummary extends CompileTypeSummary {
type: CompileTypeMetadata;
name: string;
pure: boolean;
}
export class CompilePipeMetadata {
type: CompileTypeMetadata;
name: string;
pure: boolean;
constructor({type, name, pure}: {
type?: CompileTypeMetadata,
name?: string,
pure?: boolean,
} = {}) {
this.type = type;
this.name = name;
this.pure = !!pure;
}
toSummary(): CompilePipeSummary {
return {
summaryKind: CompileSummaryKind.Pipe,
type: this.type,
name: this.name,
pure: this.pure
};
}
}
// Note: This should only use interfaces as nested data types
// as we need to be able to serialize this from/to JSON!
export interface CompileNgModuleSummary extends CompileTypeSummary {
type: CompileTypeMetadata;
// Note: This is transitive over the exported modules.
exportedDirectives: CompileIdentifierMetadata[];
// Note: This is transitive over the exported modules.
exportedPipes: CompileIdentifierMetadata[];
// Note: This is transitive.
entryComponents: CompileEntryComponentMetadata[];
// Note: This is transitive.
providers: {provider: CompileProviderMetadata, module: CompileIdentifierMetadata}[];
// Note: This is transitive.
modules: CompileTypeMetadata[];
}
/**
* Metadata regarding compilation of a module.
*/
export class CompileNgModuleMetadata {
type: CompileTypeMetadata;
declaredDirectives: CompileIdentifierMetadata[];
exportedDirectives: CompileIdentifierMetadata[];
declaredPipes: CompileIdentifierMetadata[];
exportedPipes: CompileIdentifierMetadata[];
entryComponents: CompileEntryComponentMetadata[];
bootstrapComponents: CompileIdentifierMetadata[];
providers: CompileProviderMetadata[];
importedModules: CompileNgModuleSummary[];
exportedModules: CompileNgModuleSummary[];
schemas: SchemaMetadata[];
id: string;
transitiveModule: TransitiveCompileNgModuleMetadata;
constructor(
{type, providers, declaredDirectives, exportedDirectives, declaredPipes, exportedPipes,
entryComponents, bootstrapComponents, importedModules, exportedModules, schemas,
transitiveModule, id}: {
type?: CompileTypeMetadata,
providers?: CompileProviderMetadata[],
declaredDirectives?: CompileIdentifierMetadata[],
exportedDirectives?: CompileIdentifierMetadata[],
declaredPipes?: CompileIdentifierMetadata[],
exportedPipes?: CompileIdentifierMetadata[],
entryComponents?: CompileEntryComponentMetadata[],
bootstrapComponents?: CompileIdentifierMetadata[],
importedModules?: CompileNgModuleSummary[],
exportedModules?: CompileNgModuleSummary[],
transitiveModule?: TransitiveCompileNgModuleMetadata,
schemas?: SchemaMetadata[],
id?: string
} = {}) {
this.type = type;
this.declaredDirectives = _normalizeArray(declaredDirectives);
this.exportedDirectives = _normalizeArray(exportedDirectives);
this.declaredPipes = _normalizeArray(declaredPipes);
this.exportedPipes = _normalizeArray(exportedPipes);
this.providers = _normalizeArray(providers);
this.entryComponents = _normalizeArray(entryComponents);
this.bootstrapComponents = _normalizeArray(bootstrapComponents);
this.importedModules = _normalizeArray(importedModules);
this.exportedModules = _normalizeArray(exportedModules);
this.schemas = _normalizeArray(schemas);
this.id = id;
this.transitiveModule = transitiveModule;
}
toSummary(): CompileNgModuleSummary {
return {
summaryKind: CompileSummaryKind.NgModule,
type: this.type,
entryComponents: this.transitiveModule.entryComponents,
providers: this.transitiveModule.providers,
modules: this.transitiveModule.modules,
exportedDirectives: this.transitiveModule.exportedDirectives,
exportedPipes: this.transitiveModule.exportedPipes
};
}
}
export class TransitiveCompileNgModuleMetadata {
directivesSet = new Set<any>();
directives: CompileIdentifierMetadata[] = [];
exportedDirectivesSet = new Set<any>();
exportedDirectives: CompileIdentifierMetadata[] = [];
pipesSet = new Set<any>();
pipes: CompileIdentifierMetadata[] = [];
exportedPipesSet = new Set<any>();
exportedPipes: CompileIdentifierMetadata[] = [];
modulesSet = new Set<any>();
modules: CompileTypeMetadata[] = [];
entryComponentsSet = new Set<any>();
entryComponents: CompileEntryComponentMetadata[] = [];
providers: {provider: CompileProviderMetadata, module: CompileIdentifierMetadata}[] = [];
addProvider(provider: CompileProviderMetadata, module: CompileIdentifierMetadata) {
this.providers.push({provider: provider, module: module});
}
addDirective(id: CompileIdentifierMetadata) {
if (!this.directivesSet.has(id.reference)) {
this.directivesSet.add(id.reference);
this.directives.push(id);
}
}
addExportedDirective(id: CompileIdentifierMetadata) {
if (!this.exportedDirectivesSet.has(id.reference)) {
this.exportedDirectivesSet.add(id.reference);
this.exportedDirectives.push(id);
}
}
addPipe(id: CompileIdentifierMetadata) {
if (!this.pipesSet.has(id.reference)) {
this.pipesSet.add(id.reference);
this.pipes.push(id);
}
}
addExportedPipe(id: CompileIdentifierMetadata) {
if (!this.exportedPipesSet.has(id.reference)) {
this.exportedPipesSet.add(id.reference);
this.exportedPipes.push(id);
}
}
addModule(id: CompileTypeMetadata) {
if (!this.modulesSet.has(id.reference)) {
this.modulesSet.add(id.reference);
this.modules.push(id);
}
}
addEntryComponent(ec: CompileEntryComponentMetadata) {
if (!this.entryComponentsSet.has(ec.componentType)) {
this.entryComponentsSet.add(ec.componentType);
this.entryComponents.push(ec);
}
}
}
function _normalizeArray(obj: any[]): any[] {
return obj || [];
}
export class ProviderMeta {
token: any;
useClass: Type<any>;
useValue: any;
useExisting: any;
useFactory: Function;
dependencies: Object[];
multi: boolean;
constructor(token: any, {useClass, useValue, useExisting, useFactory, deps, multi}: {
useClass?: Type<any>,
useValue?: any,
useExisting?: any,
useFactory?: Function,
deps?: Object[],
multi?: boolean
}) {
this.token = token;
this.useClass = useClass;
this.useValue = useValue;
this.useExisting = useExisting;
this.useFactory = useFactory;
this.dependencies = deps;
this.multi = !!multi;
}
}
export function flatten<T>(list: Array<T|T[]>): T[] {
return list.reduce((flat: any[], item: T | T[]): T[] => {
const flatItem = Array.isArray(item) ? flatten(item) : item;
return (<T[]>flat).concat(flatItem);
}, []);
}

View File

@ -0,0 +1,69 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* @module
* @description
* Entry point for all APIs of the compiler package.
*
* <div class="callout is-critical">
* <header>Unstable APIs</header>
* <p>
* All compiler apis are currently considered experimental and private!
* </p>
* <p>
* We expect the APIs in this package to keep on changing. Do not rely on them.
* </p>
* </div>
*/
export {VERSION} from './version';
export * from './template_parser/template_ast';
export {TEMPLATE_TRANSFORMS} from './template_parser/template_parser';
export {CompilerConfig} from './config';
export * from './compile_metadata';
export * from './aot/compiler_factory';
export * from './aot/compiler';
export * from './aot/compiler_options';
export * from './aot/compiler_host';
export * from './aot/static_reflector';
export * from './aot/static_reflection_capabilities';
export * from './aot/static_symbol';
export * from './aot/static_symbol_resolver';
export * from './aot/summary_resolver';
export * from './summary_resolver';
export {JitCompiler} from './jit/compiler';
export * from './jit/compiler_factory';
export * from './url_resolver';
export * from './resource_loader';
export {DirectiveResolver} from './directive_resolver';
export {PipeResolver} from './pipe_resolver';
export {NgModuleResolver} from './ng_module_resolver';
export {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/interpolation_config';
export * from './schema/element_schema_registry';
export * from './i18n/index';
export * from './directive_normalizer';
export * from './expression_parser/ast';
export * from './expression_parser/lexer';
export * from './expression_parser/parser';
export * from './metadata_resolver';
export * from './ml_parser/ast';
export * from './ml_parser/html_parser';
export * from './ml_parser/html_tags';
export * from './ml_parser/interpolation_config';
export * from './ml_parser/tags';
export {NgModuleCompiler} from './ng_module_compiler';
export * from './output/path_util';
export * from './output/ts_emitter';
export * from './parse_util';
export * from './schema/dom_element_schema_registry';
export * from './selector';
export * from './style_compiler';
export * from './template_parser/template_parser';
export {ViewCompiler} from './view_compiler/view_compiler';
export {isSyntaxError, syntaxError} from './util';
// This file only reexports content of the `src` folder. Keep it that way.

View File

@ -0,0 +1,609 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as cdAst from '../expression_parser/ast';
import {Identifiers, createIdentifier} from '../identifiers';
import * as o from '../output/output_ast';
export class EventHandlerVars { static event = o.variable('$event'); }
export interface LocalResolver { getLocal(name: string): o.Expression; }
export class ConvertActionBindingResult {
constructor(public stmts: o.Statement[], public allowDefault: o.ReadVarExpr) {}
}
/**
* Converts the given expression AST into an executable output AST, assuming the expression is
* used in an action binding (e.g. an event handler).
*/
export function convertActionBinding(
localResolver: LocalResolver, implicitReceiver: o.Expression, action: cdAst.AST,
bindingId: string): ConvertActionBindingResult {
if (!localResolver) {
localResolver = new DefaultLocalResolver();
}
const actionWithoutBuiltins = convertPropertyBindingBuiltins(
{
createLiteralArrayConverter: (argCount: number) => {
// Note: no caching for literal arrays in actions.
return (args: o.Expression[]) => o.literalArr(args);
},
createLiteralMapConverter: (keys: string[]) => {
// Note: no caching for literal maps in actions.
return (args: o.Expression[]) =>
o.literalMap(<[string, o.Expression][]>keys.map((key, i) => [key, args[i]]));
},
createPipeConverter: (name: string) => {
throw new Error(`Illegal State: Actions are not allowed to contain pipes. Pipe: ${name}`);
}
},
action);
const visitor = new _AstToIrVisitor(localResolver, implicitReceiver, bindingId);
const actionStmts: o.Statement[] = [];
flattenStatements(actionWithoutBuiltins.visit(visitor, _Mode.Statement), actionStmts);
prependTemporaryDecls(visitor.temporaryCount, bindingId, actionStmts);
const lastIndex = actionStmts.length - 1;
let preventDefaultVar: o.ReadVarExpr = null;
if (lastIndex >= 0) {
const lastStatement = actionStmts[lastIndex];
const returnExpr = convertStmtIntoExpression(lastStatement);
if (returnExpr) {
// Note: We need to cast the result of the method call to dynamic,
// as it might be a void method!
preventDefaultVar = createPreventDefaultVar(bindingId);
actionStmts[lastIndex] =
preventDefaultVar.set(returnExpr.cast(o.DYNAMIC_TYPE).notIdentical(o.literal(false)))
.toDeclStmt(null, [o.StmtModifier.Final]);
}
}
return new ConvertActionBindingResult(actionStmts, preventDefaultVar);
}
export interface BuiltinConverter { (args: o.Expression[]): o.Expression; }
export interface BuiltinConverterFactory {
createLiteralArrayConverter(argCount: number): BuiltinConverter;
createLiteralMapConverter(keys: string[]): BuiltinConverter;
createPipeConverter(name: string, argCount: number): BuiltinConverter;
}
export function convertPropertyBindingBuiltins(
converterFactory: BuiltinConverterFactory, ast: cdAst.AST): cdAst.AST {
return convertBuiltins(converterFactory, ast);
}
export class ConvertPropertyBindingResult {
constructor(public stmts: o.Statement[], public currValExpr: o.Expression) {}
}
/**
* Converts the given expression AST into an executable output AST, assuming the expression
* is used in property binding. The expression has to be preprocessed via
* `convertPropertyBindingBuiltins`.
*/
export function convertPropertyBinding(
localResolver: LocalResolver, implicitReceiver: o.Expression,
expressionWithoutBuiltins: cdAst.AST, bindingId: string): ConvertPropertyBindingResult {
if (!localResolver) {
localResolver = new DefaultLocalResolver();
}
const currValExpr = createCurrValueExpr(bindingId);
const stmts: o.Statement[] = [];
const visitor = new _AstToIrVisitor(localResolver, implicitReceiver, bindingId);
const outputExpr: o.Expression = expressionWithoutBuiltins.visit(visitor, _Mode.Expression);
if (visitor.temporaryCount) {
for (let i = 0; i < visitor.temporaryCount; i++) {
stmts.push(temporaryDeclaration(bindingId, i));
}
}
stmts.push(currValExpr.set(outputExpr).toDeclStmt(null, [o.StmtModifier.Final]));
return new ConvertPropertyBindingResult(stmts, currValExpr);
}
function convertBuiltins(converterFactory: BuiltinConverterFactory, ast: cdAst.AST): cdAst.AST {
const visitor = new _BuiltinAstConverter(converterFactory);
return ast.visit(visitor);
}
function temporaryName(bindingId: string, temporaryNumber: number): string {
return `tmp_${bindingId}_${temporaryNumber}`;
}
export function temporaryDeclaration(bindingId: string, temporaryNumber: number): o.Statement {
return new o.DeclareVarStmt(temporaryName(bindingId, temporaryNumber), o.NULL_EXPR);
}
function prependTemporaryDecls(
temporaryCount: number, bindingId: string, statements: o.Statement[]) {
for (let i = temporaryCount - 1; i >= 0; i--) {
statements.unshift(temporaryDeclaration(bindingId, i));
}
}
enum _Mode {
Statement,
Expression
}
function ensureStatementMode(mode: _Mode, ast: cdAst.AST) {
if (mode !== _Mode.Statement) {
throw new Error(`Expected a statement, but saw ${ast}`);
}
}
function ensureExpressionMode(mode: _Mode, ast: cdAst.AST) {
if (mode !== _Mode.Expression) {
throw new Error(`Expected an expression, but saw ${ast}`);
}
}
function convertToStatementIfNeeded(mode: _Mode, expr: o.Expression): o.Expression|o.Statement {
if (mode === _Mode.Statement) {
return expr.toStmt();
} else {
return expr;
}
}
class _BuiltinAstConverter extends cdAst.AstTransformer {
constructor(private _converterFactory: BuiltinConverterFactory) { super(); }
visitPipe(ast: cdAst.BindingPipe, context: any): any {
const args = [ast.exp, ...ast.args].map(ast => ast.visit(this, context));
return new BuiltinFunctionCall(
ast.span, args, this._converterFactory.createPipeConverter(ast.name, args.length));
}
visitLiteralArray(ast: cdAst.LiteralArray, context: any): any {
const args = ast.expressions.map(ast => ast.visit(this, context));
return new BuiltinFunctionCall(
ast.span, args, this._converterFactory.createLiteralArrayConverter(ast.expressions.length));
}
visitLiteralMap(ast: cdAst.LiteralMap, context: any): any {
const args = ast.values.map(ast => ast.visit(this, context));
return new BuiltinFunctionCall(
ast.span, args, this._converterFactory.createLiteralMapConverter(ast.keys));
}
}
class _AstToIrVisitor implements cdAst.AstVisitor {
private _nodeMap = new Map<cdAst.AST, cdAst.AST>();
private _resultMap = new Map<cdAst.AST, o.Expression>();
private _currentTemporary: number = 0;
public temporaryCount: number = 0;
constructor(
private _localResolver: LocalResolver, private _implicitReceiver: o.Expression,
private bindingId: string) {}
visitBinary(ast: cdAst.Binary, mode: _Mode): any {
let op: o.BinaryOperator;
switch (ast.operation) {
case '+':
op = o.BinaryOperator.Plus;
break;
case '-':
op = o.BinaryOperator.Minus;
break;
case '*':
op = o.BinaryOperator.Multiply;
break;
case '/':
op = o.BinaryOperator.Divide;
break;
case '%':
op = o.BinaryOperator.Modulo;
break;
case '&&':
op = o.BinaryOperator.And;
break;
case '||':
op = o.BinaryOperator.Or;
break;
case '==':
op = o.BinaryOperator.Equals;
break;
case '!=':
op = o.BinaryOperator.NotEquals;
break;
case '===':
op = o.BinaryOperator.Identical;
break;
case '!==':
op = o.BinaryOperator.NotIdentical;
break;
case '<':
op = o.BinaryOperator.Lower;
break;
case '>':
op = o.BinaryOperator.Bigger;
break;
case '<=':
op = o.BinaryOperator.LowerEquals;
break;
case '>=':
op = o.BinaryOperator.BiggerEquals;
break;
default:
throw new Error(`Unsupported operation ${ast.operation}`);
}
return convertToStatementIfNeeded(
mode,
new o.BinaryOperatorExpr(
op, this.visit(ast.left, _Mode.Expression), this.visit(ast.right, _Mode.Expression)));
}
visitChain(ast: cdAst.Chain, mode: _Mode): any {
ensureStatementMode(mode, ast);
return this.visitAll(ast.expressions, mode);
}
visitConditional(ast: cdAst.Conditional, mode: _Mode): any {
const value: o.Expression = this.visit(ast.condition, _Mode.Expression);
return convertToStatementIfNeeded(
mode,
value.conditional(
this.visit(ast.trueExp, _Mode.Expression), this.visit(ast.falseExp, _Mode.Expression)));
}
visitPipe(ast: cdAst.BindingPipe, mode: _Mode): any {
throw new Error(
`Illegal state: Pipes should have been converted into functions. Pipe: ${ast.name}`);
}
visitFunctionCall(ast: cdAst.FunctionCall, mode: _Mode): any {
const convertedArgs = this.visitAll(ast.args, _Mode.Expression);
let fnResult: o.Expression;
if (ast instanceof BuiltinFunctionCall) {
fnResult = ast.converter(convertedArgs);
} else {
fnResult = this.visit(ast.target, _Mode.Expression).callFn(convertedArgs);
}
return convertToStatementIfNeeded(mode, fnResult);
}
visitImplicitReceiver(ast: cdAst.ImplicitReceiver, mode: _Mode): any {
ensureExpressionMode(mode, ast);
return this._implicitReceiver;
}
visitInterpolation(ast: cdAst.Interpolation, mode: _Mode): any {
ensureExpressionMode(mode, ast);
const args = [o.literal(ast.expressions.length)];
for (let i = 0; i < ast.strings.length - 1; i++) {
args.push(o.literal(ast.strings[i]));
args.push(this.visit(ast.expressions[i], _Mode.Expression));
}
args.push(o.literal(ast.strings[ast.strings.length - 1]));
return ast.expressions.length <= 9 ?
o.importExpr(createIdentifier(Identifiers.inlineInterpolate)).callFn(args) :
o.importExpr(createIdentifier(Identifiers.interpolate)).callFn([
args[0], o.literalArr(args.slice(1))
]);
}
visitKeyedRead(ast: cdAst.KeyedRead, mode: _Mode): any {
const leftMostSafe = this.leftMostSafeNode(ast);
if (leftMostSafe) {
return this.convertSafeAccess(ast, leftMostSafe, mode);
} else {
return convertToStatementIfNeeded(
mode, this.visit(ast.obj, _Mode.Expression).key(this.visit(ast.key, _Mode.Expression)));
}
}
visitKeyedWrite(ast: cdAst.KeyedWrite, mode: _Mode): any {
const obj: o.Expression = this.visit(ast.obj, _Mode.Expression);
const key: o.Expression = this.visit(ast.key, _Mode.Expression);
const value: o.Expression = this.visit(ast.value, _Mode.Expression);
return convertToStatementIfNeeded(mode, obj.key(key).set(value));
}
visitLiteralArray(ast: cdAst.LiteralArray, mode: _Mode): any {
throw new Error(`Illegal State: literal arrays should have been converted into functions`);
}
visitLiteralMap(ast: cdAst.LiteralMap, mode: _Mode): any {
throw new Error(`Illegal State: literal maps should have been converted into functions`);
}
visitLiteralPrimitive(ast: cdAst.LiteralPrimitive, mode: _Mode): any {
return convertToStatementIfNeeded(mode, o.literal(ast.value));
}
private _getLocal(name: string): o.Expression { return this._localResolver.getLocal(name); }
visitMethodCall(ast: cdAst.MethodCall, mode: _Mode): any {
const leftMostSafe = this.leftMostSafeNode(ast);
if (leftMostSafe) {
return this.convertSafeAccess(ast, leftMostSafe, mode);
} else {
const args = this.visitAll(ast.args, _Mode.Expression);
let result: any = null;
const receiver = this.visit(ast.receiver, _Mode.Expression);
if (receiver === this._implicitReceiver) {
const varExpr = this._getLocal(ast.name);
if (varExpr) {
result = varExpr.callFn(args);
}
}
if (result == null) {
result = receiver.callMethod(ast.name, args);
}
return convertToStatementIfNeeded(mode, result);
}
}
visitPrefixNot(ast: cdAst.PrefixNot, mode: _Mode): any {
return convertToStatementIfNeeded(mode, o.not(this.visit(ast.expression, _Mode.Expression)));
}
visitPropertyRead(ast: cdAst.PropertyRead, mode: _Mode): any {
const leftMostSafe = this.leftMostSafeNode(ast);
if (leftMostSafe) {
return this.convertSafeAccess(ast, leftMostSafe, mode);
} else {
let result: any = null;
const receiver = this.visit(ast.receiver, _Mode.Expression);
if (receiver === this._implicitReceiver) {
result = this._getLocal(ast.name);
}
if (result == null) {
result = receiver.prop(ast.name);
}
return convertToStatementIfNeeded(mode, result);
}
}
visitPropertyWrite(ast: cdAst.PropertyWrite, mode: _Mode): any {
const receiver: o.Expression = this.visit(ast.receiver, _Mode.Expression);
if (receiver === this._implicitReceiver) {
const varExpr = this._getLocal(ast.name);
if (varExpr) {
throw new Error('Cannot assign to a reference or variable!');
}
}
return convertToStatementIfNeeded(
mode, receiver.prop(ast.name).set(this.visit(ast.value, _Mode.Expression)));
}
visitSafePropertyRead(ast: cdAst.SafePropertyRead, mode: _Mode): any {
return this.convertSafeAccess(ast, this.leftMostSafeNode(ast), mode);
}
visitSafeMethodCall(ast: cdAst.SafeMethodCall, mode: _Mode): any {
return this.convertSafeAccess(ast, this.leftMostSafeNode(ast), mode);
}
visitAll(asts: cdAst.AST[], mode: _Mode): any { return asts.map(ast => this.visit(ast, mode)); }
visitQuote(ast: cdAst.Quote, mode: _Mode): any {
throw new Error('Quotes are not supported for evaluation!');
}
private visit(ast: cdAst.AST, mode: _Mode): any {
const result = this._resultMap.get(ast);
if (result) return result;
return (this._nodeMap.get(ast) || ast).visit(this, mode);
}
private convertSafeAccess(
ast: cdAst.AST, leftMostSafe: cdAst.SafeMethodCall|cdAst.SafePropertyRead, mode: _Mode): any {
// If the expression contains a safe access node on the left it needs to be converted to
// an expression that guards the access to the member by checking the receiver for blank. As
// execution proceeds from left to right, the left most part of the expression must be guarded
// first but, because member access is left associative, the right side of the expression is at
// the top of the AST. The desired result requires lifting a copy of the the left part of the
// expression up to test it for blank before generating the unguarded version.
// Consider, for example the following expression: a?.b.c?.d.e
// This results in the ast:
// .
// / \
// ?. e
// / \
// . d
// / \
// ?. c
// / \
// a b
// The following tree should be generated:
//
// /---- ? ----\
// / | \
// a /--- ? ---\ null
// / | \
// . . null
// / \ / \
// . c . e
// / \ / \
// a b , d
// / \
// . c
// / \
// a b
//
// Notice that the first guard condition is the left hand of the left most safe access node
// which comes in as leftMostSafe to this routine.
let guardedExpression = this.visit(leftMostSafe.receiver, _Mode.Expression);
let temporary: o.ReadVarExpr;
if (this.needsTemporary(leftMostSafe.receiver)) {
// If the expression has method calls or pipes then we need to save the result into a
// temporary variable to avoid calling stateful or impure code more than once.
temporary = this.allocateTemporary();
// Preserve the result in the temporary variable
guardedExpression = temporary.set(guardedExpression);
// Ensure all further references to the guarded expression refer to the temporary instead.
this._resultMap.set(leftMostSafe.receiver, temporary);
}
const condition = guardedExpression.isBlank();
// Convert the ast to an unguarded access to the receiver's member. The map will substitute
// leftMostNode with its unguarded version in the call to `this.visit()`.
if (leftMostSafe instanceof cdAst.SafeMethodCall) {
this._nodeMap.set(
leftMostSafe,
new cdAst.MethodCall(
leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name, leftMostSafe.args));
} else {
this._nodeMap.set(
leftMostSafe,
new cdAst.PropertyRead(leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name));
}
// Recursively convert the node now without the guarded member access.
const access = this.visit(ast, _Mode.Expression);
// Remove the mapping. This is not strictly required as the converter only traverses each node
// once but is safer if the conversion is changed to traverse the nodes more than once.
this._nodeMap.delete(leftMostSafe);
// If we allcoated a temporary, release it.
if (temporary) {
this.releaseTemporary(temporary);
}
// Produce the conditional
return convertToStatementIfNeeded(mode, condition.conditional(o.literal(null), access));
}
// Given a expression of the form a?.b.c?.d.e the the left most safe node is
// the (a?.b). The . and ?. are left associative thus can be rewritten as:
// ((((a?.c).b).c)?.d).e. This returns the most deeply nested safe read or
// safe method call as this needs be transform initially to:
// a == null ? null : a.c.b.c?.d.e
// then to:
// a == null ? null : a.b.c == null ? null : a.b.c.d.e
private leftMostSafeNode(ast: cdAst.AST): cdAst.SafePropertyRead|cdAst.SafeMethodCall {
const visit = (visitor: cdAst.AstVisitor, ast: cdAst.AST): any => {
return (this._nodeMap.get(ast) || ast).visit(visitor);
};
return ast.visit({
visitBinary(ast: cdAst.Binary) { return null; },
visitChain(ast: cdAst.Chain) { return null; },
visitConditional(ast: cdAst.Conditional) { return null; },
visitFunctionCall(ast: cdAst.FunctionCall) { return null; },
visitImplicitReceiver(ast: cdAst.ImplicitReceiver) { return null; },
visitInterpolation(ast: cdAst.Interpolation) { return null; },
visitKeyedRead(ast: cdAst.KeyedRead) { return visit(this, ast.obj); },
visitKeyedWrite(ast: cdAst.KeyedWrite) { return null; },
visitLiteralArray(ast: cdAst.LiteralArray) { return null; },
visitLiteralMap(ast: cdAst.LiteralMap) { return null; },
visitLiteralPrimitive(ast: cdAst.LiteralPrimitive) { return null; },
visitMethodCall(ast: cdAst.MethodCall) { return visit(this, ast.receiver); },
visitPipe(ast: cdAst.BindingPipe) { return null; },
visitPrefixNot(ast: cdAst.PrefixNot) { return null; },
visitPropertyRead(ast: cdAst.PropertyRead) { return visit(this, ast.receiver); },
visitPropertyWrite(ast: cdAst.PropertyWrite) { return null; },
visitQuote(ast: cdAst.Quote) { return null; },
visitSafeMethodCall(ast: cdAst.SafeMethodCall) { return visit(this, ast.receiver) || ast; },
visitSafePropertyRead(ast: cdAst.SafePropertyRead) {
return visit(this, ast.receiver) || ast;
}
});
}
// Returns true of the AST includes a method or a pipe indicating that, if the
// expression is used as the target of a safe property or method access then
// the expression should be stored into a temporary variable.
private needsTemporary(ast: cdAst.AST): boolean {
const visit = (visitor: cdAst.AstVisitor, ast: cdAst.AST): boolean => {
return ast && (this._nodeMap.get(ast) || ast).visit(visitor);
};
const visitSome = (visitor: cdAst.AstVisitor, ast: cdAst.AST[]): boolean => {
return ast.some(ast => visit(visitor, ast));
};
return ast.visit({
visitBinary(ast: cdAst.Binary):
boolean{return visit(this, ast.left) || visit(this, ast.right);},
visitChain(ast: cdAst.Chain) { return false; },
visitConditional(ast: cdAst.Conditional):
boolean{return visit(this, ast.condition) || visit(this, ast.trueExp) ||
visit(this, ast.falseExp);},
visitFunctionCall(ast: cdAst.FunctionCall) { return true; },
visitImplicitReceiver(ast: cdAst.ImplicitReceiver) { return false; },
visitInterpolation(ast: cdAst.Interpolation) { return visitSome(this, ast.expressions); },
visitKeyedRead(ast: cdAst.KeyedRead) { return false; },
visitKeyedWrite(ast: cdAst.KeyedWrite) { return false; },
visitLiteralArray(ast: cdAst.LiteralArray) { return true; },
visitLiteralMap(ast: cdAst.LiteralMap) { return true; },
visitLiteralPrimitive(ast: cdAst.LiteralPrimitive) { return false; },
visitMethodCall(ast: cdAst.MethodCall) { return true; },
visitPipe(ast: cdAst.BindingPipe) { return true; },
visitPrefixNot(ast: cdAst.PrefixNot) { return visit(this, ast.expression); },
visitPropertyRead(ast: cdAst.PropertyRead) { return false; },
visitPropertyWrite(ast: cdAst.PropertyWrite) { return false; },
visitQuote(ast: cdAst.Quote) { return false; },
visitSafeMethodCall(ast: cdAst.SafeMethodCall) { return true; },
visitSafePropertyRead(ast: cdAst.SafePropertyRead) { return false; }
});
}
private allocateTemporary(): o.ReadVarExpr {
const tempNumber = this._currentTemporary++;
this.temporaryCount = Math.max(this._currentTemporary, this.temporaryCount);
return new o.ReadVarExpr(temporaryName(this.bindingId, tempNumber));
}
private releaseTemporary(temporary: o.ReadVarExpr) {
this._currentTemporary--;
if (temporary.name != temporaryName(this.bindingId, this._currentTemporary)) {
throw new Error(`Temporary ${temporary.name} released out of order`);
}
}
}
function flattenStatements(arg: any, output: o.Statement[]) {
if (Array.isArray(arg)) {
(<any[]>arg).forEach((entry) => flattenStatements(entry, output));
} else {
output.push(arg);
}
}
class DefaultLocalResolver implements LocalResolver {
getLocal(name: string): o.Expression {
if (name === EventHandlerVars.event.name) {
return EventHandlerVars.event;
}
return null;
}
}
function createCurrValueExpr(bindingId: string): o.ReadVarExpr {
return o.variable(`currVal_${bindingId}`); // fix syntax highlighting: `
}
function createPreventDefaultVar(bindingId: string): o.ReadVarExpr {
return o.variable(`pd_${bindingId}`);
}
function convertStmtIntoExpression(stmt: o.Statement): o.Expression {
if (stmt instanceof o.ExpressionStatement) {
return stmt.expr;
} else if (stmt instanceof o.ReturnStatement) {
return stmt.value;
}
return null;
}
class BuiltinFunctionCall extends cdAst.FunctionCall {
constructor(span: cdAst.ParseSpan, public args: cdAst.AST[], public converter: BuiltinConverter) {
super(span, null, args);
}
}

View File

@ -0,0 +1,36 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {InjectionToken, MissingTranslationStrategy, ViewEncapsulation, isDevMode} from '@angular/core';
import {CompileIdentifierMetadata} from './compile_metadata';
import {Identifiers, createIdentifier} from './identifiers';
export class CompilerConfig {
public defaultEncapsulation: ViewEncapsulation;
// Whether to support the `<template>` tag and the `template` attribute to define angular
// templates. They have been deprecated in 4.x, `<ng-template>` should be used instead.
public enableLegacyTemplate: boolean;
public useJit: boolean;
public missingTranslation: MissingTranslationStrategy;
constructor(
{defaultEncapsulation = ViewEncapsulation.Emulated, useJit = true, missingTranslation,
enableLegacyTemplate}: {
defaultEncapsulation?: ViewEncapsulation,
useJit?: boolean,
missingTranslation?: MissingTranslationStrategy,
enableLegacyTemplate?: boolean,
} = {}) {
this.defaultEncapsulation = defaultEncapsulation;
this.useJit = useJit;
this.missingTranslation = missingTranslation;
this.enableLegacyTemplate = enableLegacyTemplate !== false;
}
}

View File

@ -0,0 +1,248 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseLocation, ParseSourceSpan} from '../parse_util';
import {CssToken, CssTokenType} from './css_lexer';
export enum BlockType {
Import,
Charset,
Namespace,
Supports,
Keyframes,
MediaQuery,
Selector,
FontFace,
Page,
Document,
Viewport,
Unsupported
}
export interface CssAstVisitor {
visitCssValue(ast: CssStyleValueAst, context?: any): any;
visitCssInlineRule(ast: CssInlineRuleAst, context?: any): any;
visitCssAtRulePredicate(ast: CssAtRulePredicateAst, context?: any): any;
visitCssKeyframeRule(ast: CssKeyframeRuleAst, context?: any): any;
visitCssKeyframeDefinition(ast: CssKeyframeDefinitionAst, context?: any): any;
visitCssMediaQueryRule(ast: CssMediaQueryRuleAst, context?: any): any;
visitCssSelectorRule(ast: CssSelectorRuleAst, context?: any): any;
visitCssSelector(ast: CssSelectorAst, context?: any): any;
visitCssSimpleSelector(ast: CssSimpleSelectorAst, context?: any): any;
visitCssPseudoSelector(ast: CssPseudoSelectorAst, context?: any): any;
visitCssDefinition(ast: CssDefinitionAst, context?: any): any;
visitCssBlock(ast: CssBlockAst, context?: any): any;
visitCssStylesBlock(ast: CssStylesBlockAst, context?: any): any;
visitCssStyleSheet(ast: CssStyleSheetAst, context?: any): any;
visitCssUnknownRule(ast: CssUnknownRuleAst, context?: any): any;
visitCssUnknownTokenList(ast: CssUnknownTokenListAst, context?: any): any;
}
export abstract class CssAst {
constructor(public location: ParseSourceSpan) {}
get start(): ParseLocation { return this.location.start; }
get end(): ParseLocation { return this.location.end; }
abstract visit(visitor: CssAstVisitor, context?: any): any;
}
export class CssStyleValueAst extends CssAst {
constructor(location: ParseSourceSpan, public tokens: CssToken[], public strValue: string) {
super(location);
}
visit(visitor: CssAstVisitor, context?: any): any { return visitor.visitCssValue(this); }
}
export abstract class CssRuleAst extends CssAst {
constructor(location: ParseSourceSpan) { super(location); }
}
export class CssBlockRuleAst extends CssRuleAst {
constructor(
public location: ParseSourceSpan, public type: BlockType, public block: CssBlockAst,
public name: CssToken = null) {
super(location);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssBlock(this.block, context);
}
}
export class CssKeyframeRuleAst extends CssBlockRuleAst {
constructor(location: ParseSourceSpan, name: CssToken, block: CssBlockAst) {
super(location, BlockType.Keyframes, block, name);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssKeyframeRule(this, context);
}
}
export class CssKeyframeDefinitionAst extends CssBlockRuleAst {
constructor(location: ParseSourceSpan, public steps: CssToken[], block: CssBlockAst) {
super(location, BlockType.Keyframes, block, mergeTokens(steps, ','));
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssKeyframeDefinition(this, context);
}
}
export class CssBlockDefinitionRuleAst extends CssBlockRuleAst {
constructor(
location: ParseSourceSpan, public strValue: string, type: BlockType,
public query: CssAtRulePredicateAst, block: CssBlockAst) {
super(location, type, block);
const firstCssToken: CssToken = query.tokens[0];
this.name = new CssToken(
firstCssToken.index, firstCssToken.column, firstCssToken.line, CssTokenType.Identifier,
this.strValue);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssBlock(this.block, context);
}
}
export class CssMediaQueryRuleAst extends CssBlockDefinitionRuleAst {
constructor(
location: ParseSourceSpan, strValue: string, query: CssAtRulePredicateAst,
block: CssBlockAst) {
super(location, strValue, BlockType.MediaQuery, query, block);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssMediaQueryRule(this, context);
}
}
export class CssAtRulePredicateAst extends CssAst {
constructor(location: ParseSourceSpan, public strValue: string, public tokens: CssToken[]) {
super(location);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssAtRulePredicate(this, context);
}
}
export class CssInlineRuleAst extends CssRuleAst {
constructor(location: ParseSourceSpan, public type: BlockType, public value: CssStyleValueAst) {
super(location);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssInlineRule(this, context);
}
}
export class CssSelectorRuleAst extends CssBlockRuleAst {
public strValue: string;
constructor(location: ParseSourceSpan, public selectors: CssSelectorAst[], block: CssBlockAst) {
super(location, BlockType.Selector, block);
this.strValue = selectors.map(selector => selector.strValue).join(',');
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssSelectorRule(this, context);
}
}
export class CssDefinitionAst extends CssAst {
constructor(
location: ParseSourceSpan, public property: CssToken, public value: CssStyleValueAst) {
super(location);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssDefinition(this, context);
}
}
export abstract class CssSelectorPartAst extends CssAst {
constructor(location: ParseSourceSpan) { super(location); }
}
export class CssSelectorAst extends CssSelectorPartAst {
public strValue: string;
constructor(location: ParseSourceSpan, public selectorParts: CssSimpleSelectorAst[]) {
super(location);
this.strValue = selectorParts.map(part => part.strValue).join('');
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssSelector(this, context);
}
}
export class CssSimpleSelectorAst extends CssSelectorPartAst {
constructor(
location: ParseSourceSpan, public tokens: CssToken[], public strValue: string,
public pseudoSelectors: CssPseudoSelectorAst[], public operator: CssToken) {
super(location);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssSimpleSelector(this, context);
}
}
export class CssPseudoSelectorAst extends CssSelectorPartAst {
constructor(
location: ParseSourceSpan, public strValue: string, public name: string,
public tokens: CssToken[], public innerSelectors: CssSelectorAst[]) {
super(location);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssPseudoSelector(this, context);
}
}
export class CssBlockAst extends CssAst {
constructor(location: ParseSourceSpan, public entries: CssAst[]) { super(location); }
visit(visitor: CssAstVisitor, context?: any): any { return visitor.visitCssBlock(this, context); }
}
/*
a style block is different from a standard block because it contains
css prop:value definitions. A regular block can contain a list of Ast entries.
*/
export class CssStylesBlockAst extends CssBlockAst {
constructor(location: ParseSourceSpan, public definitions: CssDefinitionAst[]) {
super(location, definitions);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssStylesBlock(this, context);
}
}
export class CssStyleSheetAst extends CssAst {
constructor(location: ParseSourceSpan, public rules: CssAst[]) { super(location); }
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssStyleSheet(this, context);
}
}
export class CssUnknownRuleAst extends CssRuleAst {
constructor(location: ParseSourceSpan, public ruleName: string, public tokens: CssToken[]) {
super(location);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssUnknownRule(this, context);
}
}
export class CssUnknownTokenListAst extends CssRuleAst {
constructor(location: ParseSourceSpan, public name: string, public tokens: CssToken[]) {
super(location);
}
visit(visitor: CssAstVisitor, context?: any): any {
return visitor.visitCssUnknownTokenList(this, context);
}
}
export function mergeTokens(tokens: CssToken[], separator: string = ''): CssToken {
const mainToken = tokens[0];
let str = mainToken.strValue;
for (let i = 1; i < tokens.length; i++) {
str += separator + tokens[i].strValue;
}
return new CssToken(mainToken.index, mainToken.column, mainToken.line, mainToken.type, str);
}

View File

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

View File

@ -0,0 +1,900 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as chars from '../chars';
import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from '../parse_util';
import {BlockType, CssAst, CssAtRulePredicateAst, CssBlockAst, CssBlockDefinitionRuleAst, CssBlockRuleAst, CssDefinitionAst, CssInlineRuleAst, CssKeyframeDefinitionAst, CssKeyframeRuleAst, CssMediaQueryRuleAst, CssPseudoSelectorAst, CssRuleAst, CssSelectorAst, CssSelectorRuleAst, CssSimpleSelectorAst, CssStyleSheetAst, CssStyleValueAst, CssStylesBlockAst, CssUnknownRuleAst, CssUnknownTokenListAst, mergeTokens} from './css_ast';
import {CssLexer, CssLexerMode, CssScanner, CssToken, CssTokenType, generateErrorMessage, getRawMessage, isNewline} from './css_lexer';
const SPACE_OPERATOR = ' ';
export {CssToken} from './css_lexer';
export {BlockType} from './css_ast';
const SLASH_CHARACTER = '/';
const GT_CHARACTER = '>';
const TRIPLE_GT_OPERATOR_STR = '>>>';
const DEEP_OPERATOR_STR = '/deep/';
const EOF_DELIM_FLAG = 1;
const RBRACE_DELIM_FLAG = 2;
const LBRACE_DELIM_FLAG = 4;
const COMMA_DELIM_FLAG = 8;
const COLON_DELIM_FLAG = 16;
const SEMICOLON_DELIM_FLAG = 32;
const NEWLINE_DELIM_FLAG = 64;
const RPAREN_DELIM_FLAG = 128;
const LPAREN_DELIM_FLAG = 256;
const SPACE_DELIM_FLAG = 512;
function _pseudoSelectorSupportsInnerSelectors(name: string): boolean {
return ['not', 'host', 'host-context'].indexOf(name) >= 0;
}
function isSelectorOperatorCharacter(code: number): boolean {
switch (code) {
case chars.$SLASH:
case chars.$TILDA:
case chars.$PLUS:
case chars.$GT:
return true;
default:
return chars.isWhitespace(code);
}
}
function getDelimFromCharacter(code: number): number {
switch (code) {
case chars.$EOF:
return EOF_DELIM_FLAG;
case chars.$COMMA:
return COMMA_DELIM_FLAG;
case chars.$COLON:
return COLON_DELIM_FLAG;
case chars.$SEMICOLON:
return SEMICOLON_DELIM_FLAG;
case chars.$RBRACE:
return RBRACE_DELIM_FLAG;
case chars.$LBRACE:
return LBRACE_DELIM_FLAG;
case chars.$RPAREN:
return RPAREN_DELIM_FLAG;
case chars.$SPACE:
case chars.$TAB:
return SPACE_DELIM_FLAG;
default:
return isNewline(code) ? NEWLINE_DELIM_FLAG : 0;
}
}
function characterContainsDelimiter(code: number, delimiters: number): boolean {
return (getDelimFromCharacter(code) & delimiters) > 0;
}
export class ParsedCssResult {
constructor(public errors: CssParseError[], public ast: CssStyleSheetAst) {}
}
export class CssParser {
private _errors: CssParseError[] = [];
private _file: ParseSourceFile;
private _scanner: CssScanner;
private _lastToken: CssToken;
/**
* @param css the CSS code that will be parsed
* @param url the name of the CSS file containing the CSS source code
*/
parse(css: string, url: string): ParsedCssResult {
const lexer = new CssLexer();
this._file = new ParseSourceFile(css, url);
this._scanner = lexer.scan(css, false);
const ast = this._parseStyleSheet(EOF_DELIM_FLAG);
const errors = this._errors;
this._errors = [];
const result = new ParsedCssResult(errors, ast);
this._file = null;
this._scanner = null;
return result;
}
/** @internal */
_parseStyleSheet(delimiters: number): CssStyleSheetAst {
const results: CssRuleAst[] = [];
this._scanner.consumeEmptyStatements();
while (this._scanner.peek != chars.$EOF) {
this._scanner.setMode(CssLexerMode.BLOCK);
results.push(this._parseRule(delimiters));
}
let span: ParseSourceSpan = null;
if (results.length > 0) {
const firstRule = results[0];
// we collect the last token like so incase there was an
// EOF token that was emitted sometime during the lexing
span = this._generateSourceSpan(firstRule, this._lastToken);
}
return new CssStyleSheetAst(span, results);
}
/** @internal */
_getSourceContent(): string { return this._scanner != null ? this._scanner.input : ''; }
/** @internal */
_extractSourceContent(start: number, end: number): string {
return this._getSourceContent().substring(start, end + 1);
}
/** @internal */
_generateSourceSpan(start: CssToken|CssAst, end: CssToken|CssAst = null): ParseSourceSpan {
let startLoc: ParseLocation;
if (start instanceof CssAst) {
startLoc = start.location.start;
} else {
let token = start;
if (token == null) {
// the data here is invalid, however, if and when this does
// occur, any other errors associated with this will be collected
token = this._lastToken;
}
startLoc = new ParseLocation(this._file, token.index, token.line, token.column);
}
if (end == null) {
end = this._lastToken;
}
let endLine: number;
let endColumn: number;
let endIndex: number;
if (end instanceof CssAst) {
endLine = end.location.end.line;
endColumn = end.location.end.col;
endIndex = end.location.end.offset;
} else if (end instanceof CssToken) {
endLine = end.line;
endColumn = end.column;
endIndex = end.index;
}
const endLoc = new ParseLocation(this._file, endIndex, endLine, endColumn);
return new ParseSourceSpan(startLoc, endLoc);
}
/** @internal */
_resolveBlockType(token: CssToken): BlockType {
switch (token.strValue) {
case '@-o-keyframes':
case '@-moz-keyframes':
case '@-webkit-keyframes':
case '@keyframes':
return BlockType.Keyframes;
case '@charset':
return BlockType.Charset;
case '@import':
return BlockType.Import;
case '@namespace':
return BlockType.Namespace;
case '@page':
return BlockType.Page;
case '@document':
return BlockType.Document;
case '@media':
return BlockType.MediaQuery;
case '@font-face':
return BlockType.FontFace;
case '@viewport':
return BlockType.Viewport;
case '@supports':
return BlockType.Supports;
default:
return BlockType.Unsupported;
}
}
/** @internal */
_parseRule(delimiters: number): CssRuleAst {
if (this._scanner.peek == chars.$AT) {
return this._parseAtRule(delimiters);
}
return this._parseSelectorRule(delimiters);
}
/** @internal */
_parseAtRule(delimiters: number): CssRuleAst {
const start = this._getScannerIndex();
this._scanner.setMode(CssLexerMode.BLOCK);
const token = this._scan();
const startToken = token;
this._assertCondition(
token.type == CssTokenType.AtKeyword,
`The CSS Rule ${token.strValue} is not a valid [@] rule.`, token);
let block: CssBlockAst;
const type = this._resolveBlockType(token);
let span: ParseSourceSpan;
let tokens: CssToken[];
let endToken: CssToken;
let end: number;
let strValue: string;
let query: CssAtRulePredicateAst;
switch (type) {
case BlockType.Charset:
case BlockType.Namespace:
case BlockType.Import:
let value = this._parseValue(delimiters);
this._scanner.setMode(CssLexerMode.BLOCK);
this._scanner.consumeEmptyStatements();
span = this._generateSourceSpan(startToken, value);
return new CssInlineRuleAst(span, type, value);
case BlockType.Viewport:
case BlockType.FontFace:
block = this._parseStyleBlock(delimiters);
span = this._generateSourceSpan(startToken, block);
return new CssBlockRuleAst(span, type, block);
case BlockType.Keyframes:
tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG);
// keyframes only have one identifier name
let name = tokens[0];
block = this._parseKeyframeBlock(delimiters);
span = this._generateSourceSpan(startToken, block);
return new CssKeyframeRuleAst(span, name, block);
case BlockType.MediaQuery:
this._scanner.setMode(CssLexerMode.MEDIA_QUERY);
tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG);
endToken = tokens[tokens.length - 1];
// we do not track the whitespace after the mediaQuery predicate ends
// so we have to calculate the end string value on our own
end = endToken.index + endToken.strValue.length - 1;
strValue = this._extractSourceContent(start, end);
span = this._generateSourceSpan(startToken, endToken);
query = new CssAtRulePredicateAst(span, strValue, tokens);
block = this._parseBlock(delimiters);
strValue = this._extractSourceContent(start, this._getScannerIndex() - 1);
span = this._generateSourceSpan(startToken, block);
return new CssMediaQueryRuleAst(span, strValue, query, block);
case BlockType.Document:
case BlockType.Supports:
case BlockType.Page:
this._scanner.setMode(CssLexerMode.AT_RULE_QUERY);
tokens = this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG);
endToken = tokens[tokens.length - 1];
// we do not track the whitespace after this block rule predicate ends
// so we have to calculate the end string value on our own
end = endToken.index + endToken.strValue.length - 1;
strValue = this._extractSourceContent(start, end);
span = this._generateSourceSpan(startToken, tokens[tokens.length - 1]);
query = new CssAtRulePredicateAst(span, strValue, tokens);
block = this._parseBlock(delimiters);
strValue = this._extractSourceContent(start, block.end.offset);
span = this._generateSourceSpan(startToken, block);
return new CssBlockDefinitionRuleAst(span, strValue, type, query, block);
// if a custom @rule { ... } is used it should still tokenize the insides
default:
let listOfTokens: CssToken[] = [];
let tokenName = token.strValue;
this._scanner.setMode(CssLexerMode.ALL);
this._error(
generateErrorMessage(
this._getSourceContent(),
`The CSS "at" rule "${tokenName}" is not allowed to used here`, token.strValue,
token.index, token.line, token.column),
token);
this._collectUntilDelim(delimiters | LBRACE_DELIM_FLAG | SEMICOLON_DELIM_FLAG)
.forEach((token) => { listOfTokens.push(token); });
if (this._scanner.peek == chars.$LBRACE) {
listOfTokens.push(this._consume(CssTokenType.Character, '{'));
this._collectUntilDelim(delimiters | RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG)
.forEach((token) => { listOfTokens.push(token); });
listOfTokens.push(this._consume(CssTokenType.Character, '}'));
}
endToken = listOfTokens[listOfTokens.length - 1];
span = this._generateSourceSpan(startToken, endToken);
return new CssUnknownRuleAst(span, tokenName, listOfTokens);
}
}
/** @internal */
_parseSelectorRule(delimiters: number): CssRuleAst {
const start = this._getScannerIndex();
const selectors = this._parseSelectors(delimiters);
const block = this._parseStyleBlock(delimiters);
let ruleAst: CssRuleAst;
let span: ParseSourceSpan;
const startSelector = selectors[0];
if (block != null) {
span = this._generateSourceSpan(startSelector, block);
ruleAst = new CssSelectorRuleAst(span, selectors, block);
} else {
const name = this._extractSourceContent(start, this._getScannerIndex() - 1);
const innerTokens: CssToken[] = [];
selectors.forEach((selector: CssSelectorAst) => {
selector.selectorParts.forEach((part: CssSimpleSelectorAst) => {
part.tokens.forEach((token: CssToken) => { innerTokens.push(token); });
});
});
const endToken = innerTokens[innerTokens.length - 1];
span = this._generateSourceSpan(startSelector, endToken);
ruleAst = new CssUnknownTokenListAst(span, name, innerTokens);
}
this._scanner.setMode(CssLexerMode.BLOCK);
this._scanner.consumeEmptyStatements();
return ruleAst;
}
/** @internal */
_parseSelectors(delimiters: number): CssSelectorAst[] {
delimiters |= LBRACE_DELIM_FLAG | SEMICOLON_DELIM_FLAG;
const selectors: CssSelectorAst[] = [];
let isParsingSelectors = true;
while (isParsingSelectors) {
selectors.push(this._parseSelector(delimiters));
isParsingSelectors = !characterContainsDelimiter(this._scanner.peek, delimiters);
if (isParsingSelectors) {
this._consume(CssTokenType.Character, ',');
isParsingSelectors = !characterContainsDelimiter(this._scanner.peek, delimiters);
if (isParsingSelectors) {
this._scanner.consumeWhitespace();
}
}
}
return selectors;
}
/** @internal */
_scan(): CssToken {
const output = this._scanner.scan();
const token = output.token;
const error = output.error;
if (error != null) {
this._error(getRawMessage(error), token);
}
this._lastToken = token;
return token;
}
/** @internal */
_getScannerIndex(): number { return this._scanner.index; }
/** @internal */
_consume(type: CssTokenType, value: string = null): CssToken {
const output = this._scanner.consume(type, value);
const token = output.token;
const error = output.error;
if (error != null) {
this._error(getRawMessage(error), token);
}
this._lastToken = token;
return token;
}
/** @internal */
_parseKeyframeBlock(delimiters: number): CssBlockAst {
delimiters |= RBRACE_DELIM_FLAG;
this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK);
const startToken = this._consume(CssTokenType.Character, '{');
const definitions: CssKeyframeDefinitionAst[] = [];
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
definitions.push(this._parseKeyframeDefinition(delimiters));
}
const endToken = this._consume(CssTokenType.Character, '}');
const span = this._generateSourceSpan(startToken, endToken);
return new CssBlockAst(span, definitions);
}
/** @internal */
_parseKeyframeDefinition(delimiters: number): CssKeyframeDefinitionAst {
const start = this._getScannerIndex();
const stepTokens: CssToken[] = [];
delimiters |= LBRACE_DELIM_FLAG;
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
stepTokens.push(this._parseKeyframeLabel(delimiters | COMMA_DELIM_FLAG));
if (this._scanner.peek != chars.$LBRACE) {
this._consume(CssTokenType.Character, ',');
}
}
const stylesBlock = this._parseStyleBlock(delimiters | RBRACE_DELIM_FLAG);
const span = this._generateSourceSpan(stepTokens[0], stylesBlock);
const ast = new CssKeyframeDefinitionAst(span, stepTokens, stylesBlock);
this._scanner.setMode(CssLexerMode.BLOCK);
return ast;
}
/** @internal */
_parseKeyframeLabel(delimiters: number): CssToken {
this._scanner.setMode(CssLexerMode.KEYFRAME_BLOCK);
return mergeTokens(this._collectUntilDelim(delimiters));
}
/** @internal */
_parsePseudoSelector(delimiters: number): CssPseudoSelectorAst {
const start = this._getScannerIndex();
delimiters &= ~COMMA_DELIM_FLAG;
// we keep the original value since we may use it to recurse when :not, :host are used
const startingDelims = delimiters;
const startToken = this._consume(CssTokenType.Character, ':');
const tokens = [startToken];
if (this._scanner.peek == chars.$COLON) { // ::something
tokens.push(this._consume(CssTokenType.Character, ':'));
}
const innerSelectors: CssSelectorAst[] = [];
this._scanner.setMode(CssLexerMode.PSEUDO_SELECTOR);
// host, host-context, lang, not, nth-child are all identifiers
const pseudoSelectorToken = this._consume(CssTokenType.Identifier);
const pseudoSelectorName = pseudoSelectorToken.strValue;
tokens.push(pseudoSelectorToken);
// host(), lang(), nth-child(), etc...
if (this._scanner.peek == chars.$LPAREN) {
this._scanner.setMode(CssLexerMode.PSEUDO_SELECTOR_WITH_ARGUMENTS);
const openParenToken = this._consume(CssTokenType.Character, '(');
tokens.push(openParenToken);
// :host(innerSelector(s)), :not(selector), etc...
if (_pseudoSelectorSupportsInnerSelectors(pseudoSelectorName)) {
let innerDelims = startingDelims | LPAREN_DELIM_FLAG | RPAREN_DELIM_FLAG;
if (pseudoSelectorName == 'not') {
// the inner selector inside of :not(...) can only be one
// CSS selector (no commas allowed) ... This is according
// to the CSS specification
innerDelims |= COMMA_DELIM_FLAG;
}
// :host(a, b, c) {
this._parseSelectors(innerDelims).forEach((selector, index) => {
innerSelectors.push(selector);
});
} else {
// this branch is for things like "en-us, 2k + 1, etc..."
// which all end up in pseudoSelectors like :lang, :nth-child, etc..
const innerValueDelims = delimiters | LBRACE_DELIM_FLAG | COLON_DELIM_FLAG |
RPAREN_DELIM_FLAG | LPAREN_DELIM_FLAG;
while (!characterContainsDelimiter(this._scanner.peek, innerValueDelims)) {
const token = this._scan();
tokens.push(token);
}
}
const closeParenToken = this._consume(CssTokenType.Character, ')');
tokens.push(closeParenToken);
}
const end = this._getScannerIndex() - 1;
const strValue = this._extractSourceContent(start, end);
const endToken = tokens[tokens.length - 1];
const span = this._generateSourceSpan(startToken, endToken);
return new CssPseudoSelectorAst(span, strValue, pseudoSelectorName, tokens, innerSelectors);
}
/** @internal */
_parseSimpleSelector(delimiters: number): CssSimpleSelectorAst {
const start = this._getScannerIndex();
delimiters |= COMMA_DELIM_FLAG;
this._scanner.setMode(CssLexerMode.SELECTOR);
const selectorCssTokens: CssToken[] = [];
const pseudoSelectors: CssPseudoSelectorAst[] = [];
let previousToken: CssToken;
const selectorPartDelimiters = delimiters | SPACE_DELIM_FLAG;
let loopOverSelector = !characterContainsDelimiter(this._scanner.peek, selectorPartDelimiters);
let hasAttributeError = false;
while (loopOverSelector) {
const peek = this._scanner.peek;
switch (peek) {
case chars.$COLON:
let innerPseudo = this._parsePseudoSelector(delimiters);
pseudoSelectors.push(innerPseudo);
this._scanner.setMode(CssLexerMode.SELECTOR);
break;
case chars.$LBRACKET:
// we set the mode after the scan because attribute mode does not
// allow attribute [] values. And this also will catch any errors
// if an extra "[" is used inside.
selectorCssTokens.push(this._scan());
this._scanner.setMode(CssLexerMode.ATTRIBUTE_SELECTOR);
break;
case chars.$RBRACKET:
if (this._scanner.getMode() != CssLexerMode.ATTRIBUTE_SELECTOR) {
hasAttributeError = true;
}
// we set the mode early because attribute mode does not
// allow attribute [] values
this._scanner.setMode(CssLexerMode.SELECTOR);
selectorCssTokens.push(this._scan());
break;
default:
if (isSelectorOperatorCharacter(peek)) {
loopOverSelector = false;
continue;
}
let token = this._scan();
previousToken = token;
selectorCssTokens.push(token);
break;
}
loopOverSelector = !characterContainsDelimiter(this._scanner.peek, selectorPartDelimiters);
}
hasAttributeError =
hasAttributeError || this._scanner.getMode() == CssLexerMode.ATTRIBUTE_SELECTOR;
if (hasAttributeError) {
this._error(
`Unbalanced CSS attribute selector at column ${previousToken.line}:${previousToken.column}`,
previousToken);
}
let end = this._getScannerIndex() - 1;
// this happens if the selector is not directly followed by
// a comma or curly brace without a space in between
let operator: CssToken = null;
let operatorScanCount = 0;
let lastOperatorToken: CssToken = null;
if (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
while (operator == null && !characterContainsDelimiter(this._scanner.peek, delimiters) &&
isSelectorOperatorCharacter(this._scanner.peek)) {
let token = this._scan();
const tokenOperator = token.strValue;
operatorScanCount++;
lastOperatorToken = token;
if (tokenOperator != SPACE_OPERATOR) {
switch (tokenOperator) {
case SLASH_CHARACTER:
// /deep/ operator
let deepToken = this._consume(CssTokenType.Identifier);
let deepSlash = this._consume(CssTokenType.Character);
let index = lastOperatorToken.index;
let line = lastOperatorToken.line;
let column = lastOperatorToken.column;
if (deepToken != null && deepToken.strValue.toLowerCase() == 'deep' &&
deepSlash.strValue == SLASH_CHARACTER) {
token = new CssToken(
lastOperatorToken.index, lastOperatorToken.column, lastOperatorToken.line,
CssTokenType.Identifier, DEEP_OPERATOR_STR);
} else {
const text = SLASH_CHARACTER + deepToken.strValue + deepSlash.strValue;
this._error(
generateErrorMessage(
this._getSourceContent(), `${text} is an invalid CSS operator`, text, index,
line, column),
lastOperatorToken);
token = new CssToken(index, column, line, CssTokenType.Invalid, text);
}
break;
case GT_CHARACTER:
// >>> operator
if (this._scanner.peek == chars.$GT && this._scanner.peekPeek == chars.$GT) {
this._consume(CssTokenType.Character, GT_CHARACTER);
this._consume(CssTokenType.Character, GT_CHARACTER);
token = new CssToken(
lastOperatorToken.index, lastOperatorToken.column, lastOperatorToken.line,
CssTokenType.Identifier, TRIPLE_GT_OPERATOR_STR);
}
break;
}
operator = token;
}
}
// so long as there is an operator then we can have an
// ending value that is beyond the selector value ...
// otherwise it's just a bunch of trailing whitespace
if (operator != null) {
end = operator.index;
}
}
this._scanner.consumeWhitespace();
const strValue = this._extractSourceContent(start, end);
// if we do come across one or more spaces inside of
// the operators loop then an empty space is still a
// valid operator to use if something else was not found
if (operator == null && operatorScanCount > 0 && this._scanner.peek != chars.$LBRACE) {
operator = lastOperatorToken;
}
// please note that `endToken` is reassigned multiple times below
// so please do not optimize the if statements into if/elseif
let startTokenOrAst: CssToken|CssAst = null;
let endTokenOrAst: CssToken|CssAst = null;
if (selectorCssTokens.length > 0) {
startTokenOrAst = startTokenOrAst || selectorCssTokens[0];
endTokenOrAst = selectorCssTokens[selectorCssTokens.length - 1];
}
if (pseudoSelectors.length > 0) {
startTokenOrAst = startTokenOrAst || pseudoSelectors[0];
endTokenOrAst = pseudoSelectors[pseudoSelectors.length - 1];
}
if (operator != null) {
startTokenOrAst = startTokenOrAst || operator;
endTokenOrAst = operator;
}
const span = this._generateSourceSpan(startTokenOrAst, endTokenOrAst);
return new CssSimpleSelectorAst(span, selectorCssTokens, strValue, pseudoSelectors, operator);
}
/** @internal */
_parseSelector(delimiters: number): CssSelectorAst {
delimiters |= COMMA_DELIM_FLAG;
this._scanner.setMode(CssLexerMode.SELECTOR);
const simpleSelectors: CssSimpleSelectorAst[] = [];
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
simpleSelectors.push(this._parseSimpleSelector(delimiters));
this._scanner.consumeWhitespace();
}
const firstSelector = simpleSelectors[0];
const lastSelector = simpleSelectors[simpleSelectors.length - 1];
const span = this._generateSourceSpan(firstSelector, lastSelector);
return new CssSelectorAst(span, simpleSelectors);
}
/** @internal */
_parseValue(delimiters: number): CssStyleValueAst {
delimiters |= RBRACE_DELIM_FLAG | SEMICOLON_DELIM_FLAG | NEWLINE_DELIM_FLAG;
this._scanner.setMode(CssLexerMode.STYLE_VALUE);
const start = this._getScannerIndex();
const tokens: CssToken[] = [];
let wsStr = '';
let previous: CssToken;
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
let token: CssToken;
if (previous != null && previous.type == CssTokenType.Identifier &&
this._scanner.peek == chars.$LPAREN) {
token = this._consume(CssTokenType.Character, '(');
tokens.push(token);
this._scanner.setMode(CssLexerMode.STYLE_VALUE_FUNCTION);
token = this._scan();
tokens.push(token);
this._scanner.setMode(CssLexerMode.STYLE_VALUE);
token = this._consume(CssTokenType.Character, ')');
tokens.push(token);
} else {
token = this._scan();
if (token.type == CssTokenType.Whitespace) {
wsStr += token.strValue;
} else {
wsStr = '';
tokens.push(token);
}
}
previous = token;
}
const end = this._getScannerIndex() - 1;
this._scanner.consumeWhitespace();
const code = this._scanner.peek;
if (code == chars.$SEMICOLON) {
this._consume(CssTokenType.Character, ';');
} else if (code != chars.$RBRACE) {
this._error(
generateErrorMessage(
this._getSourceContent(), `The CSS key/value definition did not end with a semicolon`,
previous.strValue, previous.index, previous.line, previous.column),
previous);
}
const strValue = this._extractSourceContent(start, end);
const startToken = tokens[0];
const endToken = tokens[tokens.length - 1];
const span = this._generateSourceSpan(startToken, endToken);
return new CssStyleValueAst(span, tokens, strValue);
}
/** @internal */
_collectUntilDelim(delimiters: number, assertType: CssTokenType = null): CssToken[] {
const tokens: CssToken[] = [];
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
const val = assertType != null ? this._consume(assertType) : this._scan();
tokens.push(val);
}
return tokens;
}
/** @internal */
_parseBlock(delimiters: number): CssBlockAst {
delimiters |= RBRACE_DELIM_FLAG;
this._scanner.setMode(CssLexerMode.BLOCK);
const startToken = this._consume(CssTokenType.Character, '{');
this._scanner.consumeEmptyStatements();
const results: CssRuleAst[] = [];
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
results.push(this._parseRule(delimiters));
}
const endToken = this._consume(CssTokenType.Character, '}');
this._scanner.setMode(CssLexerMode.BLOCK);
this._scanner.consumeEmptyStatements();
const span = this._generateSourceSpan(startToken, endToken);
return new CssBlockAst(span, results);
}
/** @internal */
_parseStyleBlock(delimiters: number): CssStylesBlockAst {
delimiters |= RBRACE_DELIM_FLAG | LBRACE_DELIM_FLAG;
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
const startToken = this._consume(CssTokenType.Character, '{');
if (startToken.numValue != chars.$LBRACE) {
return null;
}
const definitions: CssDefinitionAst[] = [];
this._scanner.consumeEmptyStatements();
while (!characterContainsDelimiter(this._scanner.peek, delimiters)) {
definitions.push(this._parseDefinition(delimiters));
this._scanner.consumeEmptyStatements();
}
const endToken = this._consume(CssTokenType.Character, '}');
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
this._scanner.consumeEmptyStatements();
const span = this._generateSourceSpan(startToken, endToken);
return new CssStylesBlockAst(span, definitions);
}
/** @internal */
_parseDefinition(delimiters: number): CssDefinitionAst {
this._scanner.setMode(CssLexerMode.STYLE_BLOCK);
let prop = this._consume(CssTokenType.Identifier);
let parseValue: boolean = false;
let value: CssStyleValueAst = null;
let endToken: CssToken|CssStyleValueAst = prop;
// the colon value separates the prop from the style.
// there are a few cases as to what could happen if it
// is missing
switch (this._scanner.peek) {
case chars.$SEMICOLON:
case chars.$RBRACE:
case chars.$EOF:
parseValue = false;
break;
default:
let propStr = [prop.strValue];
if (this._scanner.peek != chars.$COLON) {
// this will throw the error
const nextValue = this._consume(CssTokenType.Character, ':');
propStr.push(nextValue.strValue);
const remainingTokens = this._collectUntilDelim(
delimiters | COLON_DELIM_FLAG | SEMICOLON_DELIM_FLAG, CssTokenType.Identifier);
if (remainingTokens.length > 0) {
remainingTokens.forEach((token) => { propStr.push(token.strValue); });
}
endToken = prop =
new CssToken(prop.index, prop.column, prop.line, prop.type, propStr.join(' '));
}
// this means we've reached the end of the definition and/or block
if (this._scanner.peek == chars.$COLON) {
this._consume(CssTokenType.Character, ':');
parseValue = true;
}
break;
}
if (parseValue) {
value = this._parseValue(delimiters);
endToken = value;
} else {
this._error(
generateErrorMessage(
this._getSourceContent(), `The CSS property was not paired with a style value`,
prop.strValue, prop.index, prop.line, prop.column),
prop);
}
const span = this._generateSourceSpan(prop, endToken);
return new CssDefinitionAst(span, prop, value);
}
/** @internal */
_assertCondition(status: boolean, errorMessage: string, problemToken: CssToken): boolean {
if (!status) {
this._error(errorMessage, problemToken);
return true;
}
return false;
}
/** @internal */
_error(message: string, problemToken: CssToken) {
const length = problemToken.strValue.length;
const error = CssParseError.create(
this._file, 0, problemToken.line, problemToken.column, length, message);
this._errors.push(error);
}
}
export class CssParseError extends ParseError {
static create(
file: ParseSourceFile, offset: number, line: number, col: number, length: number,
errMsg: string): CssParseError {
const start = new ParseLocation(file, offset, line, col);
const end = new ParseLocation(file, offset, line, col + length);
const span = new ParseSourceSpan(start, end);
return new CssParseError(span, 'CSS Parse Error: ' + errMsg);
}
constructor(span: ParseSourceSpan, message: string) { super(span, message); }
}

View File

@ -0,0 +1,247 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ViewEncapsulation, ɵstringify as stringify} from '@angular/core';
import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata} from './compile_metadata';
import {CompilerConfig} from './config';
import {CompilerInjectable} from './injectable';
import * as html from './ml_parser/ast';
import {HtmlParser} from './ml_parser/html_parser';
import {InterpolationConfig} from './ml_parser/interpolation_config';
import {ResourceLoader} from './resource_loader';
import {extractStyleUrls, isStyleUrlResolvable} from './style_url_resolver';
import {PreparsedElementType, preparseElement} from './template_parser/template_preparser';
import {UrlResolver} from './url_resolver';
import {SyncAsyncResult, syntaxError} from './util';
export interface PrenormalizedTemplateMetadata {
componentType: any;
moduleUrl: string;
template?: string;
templateUrl?: string;
styles?: string[];
styleUrls?: string[];
interpolation?: [string, string];
encapsulation?: ViewEncapsulation;
animations?: CompileAnimationEntryMetadata[];
}
@CompilerInjectable()
export class DirectiveNormalizer {
private _resourceLoaderCache = new Map<string, Promise<string>>();
constructor(
private _resourceLoader: ResourceLoader, private _urlResolver: UrlResolver,
private _htmlParser: HtmlParser, private _config: CompilerConfig) {}
clearCache(): void { this._resourceLoaderCache.clear(); }
clearCacheFor(normalizedDirective: CompileDirectiveMetadata): void {
if (!normalizedDirective.isComponent) {
return;
}
this._resourceLoaderCache.delete(normalizedDirective.template.templateUrl);
normalizedDirective.template.externalStylesheets.forEach(
(stylesheet) => { this._resourceLoaderCache.delete(stylesheet.moduleUrl); });
}
private _fetch(url: string): Promise<string> {
let result = this._resourceLoaderCache.get(url);
if (!result) {
result = this._resourceLoader.get(url);
this._resourceLoaderCache.set(url, result);
}
return result;
}
normalizeTemplate(prenormData: PrenormalizedTemplateMetadata):
SyncAsyncResult<CompileTemplateMetadata> {
let normalizedTemplateSync: CompileTemplateMetadata = null;
let normalizedTemplateAsync: Promise<CompileTemplateMetadata>;
if (prenormData.template != null) {
if (typeof prenormData.template !== 'string') {
throw syntaxError(
`The template specified for component ${stringify(prenormData.componentType)} is not a string`);
}
normalizedTemplateSync = this.normalizeTemplateSync(prenormData);
normalizedTemplateAsync = Promise.resolve(normalizedTemplateSync);
} else if (prenormData.templateUrl) {
if (typeof prenormData.templateUrl !== 'string') {
throw syntaxError(
`The templateUrl specified for component ${stringify(prenormData.componentType)} is not a string`);
}
normalizedTemplateAsync = this.normalizeTemplateAsync(prenormData);
} else {
throw syntaxError(
`No template specified for component ${stringify(prenormData.componentType)}`);
}
if (normalizedTemplateSync && normalizedTemplateSync.styleUrls.length === 0) {
// sync case
return new SyncAsyncResult(normalizedTemplateSync);
} else {
// async case
return new SyncAsyncResult(
null, normalizedTemplateAsync.then(
(normalizedTemplate) => this.normalizeExternalStylesheets(normalizedTemplate)));
}
}
normalizeTemplateSync(prenomData: PrenormalizedTemplateMetadata): CompileTemplateMetadata {
return this.normalizeLoadedTemplate(prenomData, prenomData.template, prenomData.moduleUrl);
}
normalizeTemplateAsync(prenomData: PrenormalizedTemplateMetadata):
Promise<CompileTemplateMetadata> {
const templateUrl = this._urlResolver.resolve(prenomData.moduleUrl, prenomData.templateUrl);
return this._fetch(templateUrl)
.then((value) => this.normalizeLoadedTemplate(prenomData, value, templateUrl));
}
normalizeLoadedTemplate(
prenomData: PrenormalizedTemplateMetadata, template: string,
templateAbsUrl: string): CompileTemplateMetadata {
const interpolationConfig = InterpolationConfig.fromArray(prenomData.interpolation);
const rootNodesAndErrors = this._htmlParser.parse(
template, stringify(prenomData.componentType), true, interpolationConfig);
if (rootNodesAndErrors.errors.length > 0) {
const errorString = rootNodesAndErrors.errors.join('\n');
throw syntaxError(`Template parse errors:\n${errorString}`);
}
const templateMetadataStyles = this.normalizeStylesheet(new CompileStylesheetMetadata({
styles: prenomData.styles,
styleUrls: prenomData.styleUrls,
moduleUrl: prenomData.moduleUrl
}));
const visitor = new TemplatePreparseVisitor();
html.visitAll(visitor, rootNodesAndErrors.rootNodes);
const templateStyles = this.normalizeStylesheet(new CompileStylesheetMetadata(
{styles: visitor.styles, styleUrls: visitor.styleUrls, moduleUrl: templateAbsUrl}));
let encapsulation = prenomData.encapsulation;
if (encapsulation == null) {
encapsulation = this._config.defaultEncapsulation;
}
const styles = templateMetadataStyles.styles.concat(templateStyles.styles);
const styleUrls = templateMetadataStyles.styleUrls.concat(templateStyles.styleUrls);
if (encapsulation === ViewEncapsulation.Emulated && styles.length === 0 &&
styleUrls.length === 0) {
encapsulation = ViewEncapsulation.None;
}
return new CompileTemplateMetadata({
encapsulation,
template,
templateUrl: templateAbsUrl, styles, styleUrls,
ngContentSelectors: visitor.ngContentSelectors,
animations: prenomData.animations,
interpolation: prenomData.interpolation,
});
}
normalizeExternalStylesheets(templateMeta: CompileTemplateMetadata):
Promise<CompileTemplateMetadata> {
return this._loadMissingExternalStylesheets(templateMeta.styleUrls)
.then((externalStylesheets) => new CompileTemplateMetadata({
encapsulation: templateMeta.encapsulation,
template: templateMeta.template,
templateUrl: templateMeta.templateUrl,
styles: templateMeta.styles,
styleUrls: templateMeta.styleUrls,
externalStylesheets: externalStylesheets,
ngContentSelectors: templateMeta.ngContentSelectors,
animations: templateMeta.animations,
interpolation: templateMeta.interpolation
}));
}
private _loadMissingExternalStylesheets(
styleUrls: string[],
loadedStylesheets:
Map<string, CompileStylesheetMetadata> = new Map<string, CompileStylesheetMetadata>()):
Promise<CompileStylesheetMetadata[]> {
return Promise
.all(styleUrls.filter((styleUrl) => !loadedStylesheets.has(styleUrl))
.map(styleUrl => this._fetch(styleUrl).then((loadedStyle) => {
const stylesheet = this.normalizeStylesheet(
new CompileStylesheetMetadata({styles: [loadedStyle], moduleUrl: styleUrl}));
loadedStylesheets.set(styleUrl, stylesheet);
return this._loadMissingExternalStylesheets(
stylesheet.styleUrls, loadedStylesheets);
})))
.then((_) => Array.from(loadedStylesheets.values()));
}
normalizeStylesheet(stylesheet: CompileStylesheetMetadata): CompileStylesheetMetadata {
const allStyleUrls = stylesheet.styleUrls.filter(isStyleUrlResolvable)
.map(url => this._urlResolver.resolve(stylesheet.moduleUrl, url));
const allStyles = stylesheet.styles.map(style => {
const styleWithImports = extractStyleUrls(this._urlResolver, stylesheet.moduleUrl, style);
allStyleUrls.push(...styleWithImports.styleUrls);
return styleWithImports.style;
});
return new CompileStylesheetMetadata(
{styles: allStyles, styleUrls: allStyleUrls, moduleUrl: stylesheet.moduleUrl});
}
}
class TemplatePreparseVisitor implements html.Visitor {
ngContentSelectors: string[] = [];
styles: string[] = [];
styleUrls: string[] = [];
ngNonBindableStackCount: number = 0;
visitElement(ast: html.Element, context: any): any {
const preparsedElement = preparseElement(ast);
switch (preparsedElement.type) {
case PreparsedElementType.NG_CONTENT:
if (this.ngNonBindableStackCount === 0) {
this.ngContentSelectors.push(preparsedElement.selectAttr);
}
break;
case PreparsedElementType.STYLE:
let textContent = '';
ast.children.forEach(child => {
if (child instanceof html.Text) {
textContent += child.value;
}
});
this.styles.push(textContent);
break;
case PreparsedElementType.STYLESHEET:
this.styleUrls.push(preparsedElement.hrefAttr);
break;
default:
break;
}
if (preparsedElement.nonBindable) {
this.ngNonBindableStackCount++;
}
html.visitAll(this, ast.children);
if (preparsedElement.nonBindable) {
this.ngNonBindableStackCount--;
}
return null;
}
visitExpansion(ast: html.Expansion, context: any): any { html.visitAll(this, ast.cases); }
visitExpansionCase(ast: html.ExpansionCase, context: any): any {
html.visitAll(this, ast.expression);
}
visitComment(ast: html.Comment, context: any): any { return null; }
visitAttribute(ast: html.Attribute, context: any): any { return null; }
visitText(ast: html.Text, context: any): any { return null; }
}

View File

@ -0,0 +1,176 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, HostBinding, HostListener, Input, Output, Query, Type, resolveForwardRef, ɵReflectorReader, ɵmerge as merge, ɵreflector, ɵstringify as stringify} from '@angular/core';
import {CompilerInjectable} from './injectable';
import {splitAtColon} from './util';
/*
* Resolve a `Type` for {@link Directive}.
*
* This interface can be overridden by the application developer to create custom behavior.
*
* See {@link Compiler}
*/
@CompilerInjectable()
export class DirectiveResolver {
constructor(private _reflector: ɵReflectorReader = ɵreflector) {}
isDirective(type: Type<any>) {
const typeMetadata = this._reflector.annotations(resolveForwardRef(type));
return typeMetadata && typeMetadata.some(isDirectiveMetadata);
}
/**
* Return {@link Directive} for a given `Type`.
*/
resolve(type: Type<any>, throwIfNotFound = true): Directive {
const typeMetadata = this._reflector.annotations(resolveForwardRef(type));
if (typeMetadata) {
const metadata = findLast(typeMetadata, isDirectiveMetadata);
if (metadata) {
const propertyMetadata = this._reflector.propMetadata(type);
return this._mergeWithPropertyMetadata(metadata, propertyMetadata, type);
}
}
if (throwIfNotFound) {
throw new Error(`No Directive annotation found on ${stringify(type)}`);
}
return null;
}
private _mergeWithPropertyMetadata(
dm: Directive, propertyMetadata: {[key: string]: any[]},
directiveType: Type<any>): Directive {
const inputs: string[] = [];
const outputs: string[] = [];
const host: {[key: string]: string} = {};
const queries: {[key: string]: any} = {};
Object.keys(propertyMetadata).forEach((propName: string) => {
const input = findLast(propertyMetadata[propName], (a) => a instanceof Input);
if (input) {
if (input.bindingPropertyName) {
inputs.push(`${propName}: ${input.bindingPropertyName}`);
} else {
inputs.push(propName);
}
}
const output = findLast(propertyMetadata[propName], (a) => a instanceof Output);
if (output) {
if (output.bindingPropertyName) {
outputs.push(`${propName}: ${output.bindingPropertyName}`);
} else {
outputs.push(propName);
}
}
const hostBindings = propertyMetadata[propName].filter(a => a && a instanceof HostBinding);
hostBindings.forEach(hostBinding => {
if (hostBinding.hostPropertyName) {
const startWith = hostBinding.hostPropertyName[0];
if (startWith === '(') {
throw new Error(`@HostBinding can not bind to events. Use @HostListener instead.`);
} else if (startWith === '[') {
throw new Error(
`@HostBinding parameter should be a property name, 'class.<name>', or 'attr.<name>'.`);
}
host[`[${hostBinding.hostPropertyName}]`] = propName;
} else {
host[`[${propName}]`] = propName;
}
});
const hostListeners = propertyMetadata[propName].filter(a => a && a instanceof HostListener);
hostListeners.forEach(hostListener => {
const args = hostListener.args || [];
host[`(${hostListener.eventName})`] = `${propName}(${args.join(',')})`;
});
const query = findLast(propertyMetadata[propName], (a) => a instanceof Query);
if (query) {
queries[propName] = query;
}
});
return this._merge(dm, inputs, outputs, host, queries, directiveType);
}
private _extractPublicName(def: string) { return splitAtColon(def, [null, def])[1].trim(); }
private _dedupeBindings(bindings: string[]): string[] {
const names = new Set<string>();
const reversedResult: string[] = [];
// go last to first to allow later entries to overwrite previous entries
for (let i = bindings.length - 1; i >= 0; i--) {
const binding = bindings[i];
const name = this._extractPublicName(binding);
if (!names.has(name)) {
names.add(name);
reversedResult.push(binding);
}
}
return reversedResult.reverse();
}
private _merge(
directive: Directive, inputs: string[], outputs: string[], host: {[key: string]: string},
queries: {[key: string]: any}, directiveType: Type<any>): Directive {
const mergedInputs =
this._dedupeBindings(directive.inputs ? directive.inputs.concat(inputs) : inputs);
const mergedOutputs =
this._dedupeBindings(directive.outputs ? directive.outputs.concat(outputs) : outputs);
const mergedHost = directive.host ? merge(directive.host, host) : host;
const mergedQueries = directive.queries ? merge(directive.queries, queries) : queries;
if (directive instanceof Component) {
return new Component({
selector: directive.selector,
inputs: mergedInputs,
outputs: mergedOutputs,
host: mergedHost,
exportAs: directive.exportAs,
moduleId: directive.moduleId,
queries: mergedQueries,
changeDetection: directive.changeDetection,
providers: directive.providers,
viewProviders: directive.viewProviders,
entryComponents: directive.entryComponents,
template: directive.template,
templateUrl: directive.templateUrl,
styles: directive.styles,
styleUrls: directive.styleUrls,
encapsulation: directive.encapsulation,
animations: directive.animations,
interpolation: directive.interpolation
});
} else {
return new Directive({
selector: directive.selector,
inputs: mergedInputs,
outputs: mergedOutputs,
host: mergedHost,
exportAs: directive.exportAs,
queries: mergedQueries,
providers: directive.providers
});
}
}
}
function isDirectiveMetadata(type: any): type is Directive {
return type instanceof Directive;
}
export function findLast<T>(arr: T[], condition: (value: T) => boolean): T {
for (let i = arr.length - 1; i >= 0; i--) {
if (condition(arr[i])) {
return arr[i];
}
}
return null;
}

View File

@ -0,0 +1,392 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export class ParserError {
public message: string;
constructor(
message: string, public input: string, public errLocation: string, public ctxLocation?: any) {
this.message = `Parser Error: ${message} ${errLocation} [${input}] in ${ctxLocation}`;
}
}
export class ParseSpan {
constructor(public start: number, public end: number) {}
}
export class AST {
constructor(public span: ParseSpan) {}
visit(visitor: AstVisitor, context: any = null): any { return null; }
toString(): string { return 'AST'; }
}
/**
* Represents a quoted expression of the form:
*
* quote = prefix `:` uninterpretedExpression
* prefix = identifier
* uninterpretedExpression = arbitrary string
*
* A quoted expression is meant to be pre-processed by an AST transformer that
* converts it into another AST that no longer contains quoted expressions.
* It is meant to allow third-party developers to extend Angular template
* expression language. The `uninterpretedExpression` part of the quote is
* therefore not interpreted by the Angular's own expression parser.
*/
export class Quote extends AST {
constructor(
span: ParseSpan, public prefix: string, public uninterpretedExpression: string,
public location: any) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitQuote(this, context); }
toString(): string { return 'Quote'; }
}
export class EmptyExpr extends AST {
visit(visitor: AstVisitor, context: any = null) {
// do nothing
}
}
export class ImplicitReceiver extends AST {
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitImplicitReceiver(this, context);
}
}
/**
* Multiple expressions separated by a semicolon.
*/
export class Chain extends AST {
constructor(span: ParseSpan, public expressions: any[]) { super(span); }
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitChain(this, context); }
}
export class Conditional extends AST {
constructor(span: ParseSpan, public condition: AST, public trueExp: AST, public falseExp: AST) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitConditional(this, context);
}
}
export class PropertyRead extends AST {
constructor(span: ParseSpan, public receiver: AST, public name: string) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitPropertyRead(this, context);
}
}
export class PropertyWrite extends AST {
constructor(span: ParseSpan, public receiver: AST, public name: string, public value: AST) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitPropertyWrite(this, context);
}
}
export class SafePropertyRead extends AST {
constructor(span: ParseSpan, public receiver: AST, public name: string) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitSafePropertyRead(this, context);
}
}
export class KeyedRead extends AST {
constructor(span: ParseSpan, public obj: AST, public key: AST) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitKeyedRead(this, context);
}
}
export class KeyedWrite extends AST {
constructor(span: ParseSpan, public obj: AST, public key: AST, public value: AST) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitKeyedWrite(this, context);
}
}
export class BindingPipe extends AST {
constructor(span: ParseSpan, public exp: AST, public name: string, public args: any[]) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitPipe(this, context); }
}
export class LiteralPrimitive extends AST {
constructor(span: ParseSpan, public value: any) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitLiteralPrimitive(this, context);
}
}
export class LiteralArray extends AST {
constructor(span: ParseSpan, public expressions: any[]) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitLiteralArray(this, context);
}
}
export class LiteralMap extends AST {
constructor(span: ParseSpan, public keys: any[], public values: any[]) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitLiteralMap(this, context);
}
}
export class Interpolation extends AST {
constructor(span: ParseSpan, public strings: any[], public expressions: any[]) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitInterpolation(this, context);
}
}
export class Binary extends AST {
constructor(span: ParseSpan, public operation: string, public left: AST, public right: AST) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitBinary(this, context);
}
}
export class PrefixNot extends AST {
constructor(span: ParseSpan, public expression: AST) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitPrefixNot(this, context);
}
}
export class MethodCall extends AST {
constructor(span: ParseSpan, public receiver: AST, public name: string, public args: any[]) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitMethodCall(this, context);
}
}
export class SafeMethodCall extends AST {
constructor(span: ParseSpan, public receiver: AST, public name: string, public args: any[]) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitSafeMethodCall(this, context);
}
}
export class FunctionCall extends AST {
constructor(span: ParseSpan, public target: AST, public args: any[]) { super(span); }
visit(visitor: AstVisitor, context: any = null): any {
return visitor.visitFunctionCall(this, context);
}
}
export class ASTWithSource extends AST {
constructor(
public ast: AST, public source: string, public location: string,
public errors: ParserError[]) {
super(new ParseSpan(0, source == null ? 0 : source.length));
}
visit(visitor: AstVisitor, context: any = null): any { return this.ast.visit(visitor, context); }
toString(): string { return `${this.source} in ${this.location}`; }
}
export class TemplateBinding {
constructor(
public span: ParseSpan, public key: string, public keyIsVar: boolean, public name: string,
public expression: ASTWithSource) {}
}
export interface AstVisitor {
visitBinary(ast: Binary, context: any): any;
visitChain(ast: Chain, context: any): any;
visitConditional(ast: Conditional, context: any): any;
visitFunctionCall(ast: FunctionCall, context: any): any;
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any;
visitInterpolation(ast: Interpolation, context: any): any;
visitKeyedRead(ast: KeyedRead, context: any): any;
visitKeyedWrite(ast: KeyedWrite, context: any): any;
visitLiteralArray(ast: LiteralArray, context: any): any;
visitLiteralMap(ast: LiteralMap, context: any): any;
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any;
visitMethodCall(ast: MethodCall, context: any): any;
visitPipe(ast: BindingPipe, context: any): any;
visitPrefixNot(ast: PrefixNot, context: any): any;
visitPropertyRead(ast: PropertyRead, context: any): any;
visitPropertyWrite(ast: PropertyWrite, context: any): any;
visitQuote(ast: Quote, context: any): any;
visitSafeMethodCall(ast: SafeMethodCall, context: any): any;
visitSafePropertyRead(ast: SafePropertyRead, context: any): any;
}
export class RecursiveAstVisitor implements AstVisitor {
visitBinary(ast: Binary, context: any): any {
ast.left.visit(this);
ast.right.visit(this);
return null;
}
visitChain(ast: Chain, context: any): any { return this.visitAll(ast.expressions, context); }
visitConditional(ast: Conditional, context: any): any {
ast.condition.visit(this);
ast.trueExp.visit(this);
ast.falseExp.visit(this);
return null;
}
visitPipe(ast: BindingPipe, context: any): any {
ast.exp.visit(this);
this.visitAll(ast.args, context);
return null;
}
visitFunctionCall(ast: FunctionCall, context: any): any {
ast.target.visit(this);
this.visitAll(ast.args, context);
return null;
}
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any { return null; }
visitInterpolation(ast: Interpolation, context: any): any {
return this.visitAll(ast.expressions, context);
}
visitKeyedRead(ast: KeyedRead, context: any): any {
ast.obj.visit(this);
ast.key.visit(this);
return null;
}
visitKeyedWrite(ast: KeyedWrite, context: any): any {
ast.obj.visit(this);
ast.key.visit(this);
ast.value.visit(this);
return null;
}
visitLiteralArray(ast: LiteralArray, context: any): any {
return this.visitAll(ast.expressions, context);
}
visitLiteralMap(ast: LiteralMap, context: any): any { return this.visitAll(ast.values, context); }
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any { return null; }
visitMethodCall(ast: MethodCall, context: any): any {
ast.receiver.visit(this);
return this.visitAll(ast.args, context);
}
visitPrefixNot(ast: PrefixNot, context: any): any {
ast.expression.visit(this);
return null;
}
visitPropertyRead(ast: PropertyRead, context: any): any {
ast.receiver.visit(this);
return null;
}
visitPropertyWrite(ast: PropertyWrite, context: any): any {
ast.receiver.visit(this);
ast.value.visit(this);
return null;
}
visitSafePropertyRead(ast: SafePropertyRead, context: any): any {
ast.receiver.visit(this);
return null;
}
visitSafeMethodCall(ast: SafeMethodCall, context: any): any {
ast.receiver.visit(this);
return this.visitAll(ast.args, context);
}
visitAll(asts: AST[], context: any): any {
asts.forEach(ast => ast.visit(this, context));
return null;
}
visitQuote(ast: Quote, context: any): any { return null; }
}
export class AstTransformer implements AstVisitor {
visitImplicitReceiver(ast: ImplicitReceiver, context: any): AST { return ast; }
visitInterpolation(ast: Interpolation, context: any): AST {
return new Interpolation(ast.span, ast.strings, this.visitAll(ast.expressions));
}
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): AST {
return new LiteralPrimitive(ast.span, ast.value);
}
visitPropertyRead(ast: PropertyRead, context: any): AST {
return new PropertyRead(ast.span, ast.receiver.visit(this), ast.name);
}
visitPropertyWrite(ast: PropertyWrite, context: any): AST {
return new PropertyWrite(ast.span, ast.receiver.visit(this), ast.name, ast.value.visit(this));
}
visitSafePropertyRead(ast: SafePropertyRead, context: any): AST {
return new SafePropertyRead(ast.span, ast.receiver.visit(this), ast.name);
}
visitMethodCall(ast: MethodCall, context: any): AST {
return new MethodCall(ast.span, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
}
visitSafeMethodCall(ast: SafeMethodCall, context: any): AST {
return new SafeMethodCall(
ast.span, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
}
visitFunctionCall(ast: FunctionCall, context: any): AST {
return new FunctionCall(ast.span, ast.target.visit(this), this.visitAll(ast.args));
}
visitLiteralArray(ast: LiteralArray, context: any): AST {
return new LiteralArray(ast.span, this.visitAll(ast.expressions));
}
visitLiteralMap(ast: LiteralMap, context: any): AST {
return new LiteralMap(ast.span, ast.keys, this.visitAll(ast.values));
}
visitBinary(ast: Binary, context: any): AST {
return new Binary(ast.span, ast.operation, ast.left.visit(this), ast.right.visit(this));
}
visitPrefixNot(ast: PrefixNot, context: any): AST {
return new PrefixNot(ast.span, ast.expression.visit(this));
}
visitConditional(ast: Conditional, context: any): AST {
return new Conditional(
ast.span, ast.condition.visit(this), ast.trueExp.visit(this), ast.falseExp.visit(this));
}
visitPipe(ast: BindingPipe, context: any): AST {
return new BindingPipe(ast.span, ast.exp.visit(this), ast.name, this.visitAll(ast.args));
}
visitKeyedRead(ast: KeyedRead, context: any): AST {
return new KeyedRead(ast.span, ast.obj.visit(this), ast.key.visit(this));
}
visitKeyedWrite(ast: KeyedWrite, context: any): AST {
return new KeyedWrite(
ast.span, ast.obj.visit(this), ast.key.visit(this), ast.value.visit(this));
}
visitAll(asts: any[]): any[] {
const res = new Array(asts.length);
for (let i = 0; i < asts.length; ++i) {
res[i] = asts[i].visit(this);
}
return res;
}
visitChain(ast: Chain, context: any): AST {
return new Chain(ast.span, this.visitAll(ast.expressions));
}
visitQuote(ast: Quote, context: any): AST {
return new Quote(ast.span, ast.prefix, ast.uninterpretedExpression, ast.location);
}
}

View File

@ -0,0 +1,392 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as chars from '../chars';
import {CompilerInjectable} from '../injectable';
export enum TokenType {
Character,
Identifier,
Keyword,
String,
Operator,
Number,
Error
}
const KEYWORDS = ['var', 'let', 'null', 'undefined', 'true', 'false', 'if', 'else', 'this'];
@CompilerInjectable()
export class Lexer {
tokenize(text: string): Token[] {
const scanner = new _Scanner(text);
const tokens: Token[] = [];
let token = scanner.scanToken();
while (token != null) {
tokens.push(token);
token = scanner.scanToken();
}
return tokens;
}
}
export class Token {
constructor(
public index: number, public type: TokenType, public numValue: number,
public strValue: string) {}
isCharacter(code: number): boolean {
return this.type == TokenType.Character && this.numValue == code;
}
isNumber(): boolean { return this.type == TokenType.Number; }
isString(): boolean { return this.type == TokenType.String; }
isOperator(operater: string): boolean {
return this.type == TokenType.Operator && this.strValue == operater;
}
isIdentifier(): boolean { return this.type == TokenType.Identifier; }
isKeyword(): boolean { return this.type == TokenType.Keyword; }
isKeywordLet(): boolean { return this.type == TokenType.Keyword && this.strValue == 'let'; }
isKeywordNull(): boolean { return this.type == TokenType.Keyword && this.strValue == 'null'; }
isKeywordUndefined(): boolean {
return this.type == TokenType.Keyword && this.strValue == 'undefined';
}
isKeywordTrue(): boolean { return this.type == TokenType.Keyword && this.strValue == 'true'; }
isKeywordFalse(): boolean { return this.type == TokenType.Keyword && this.strValue == 'false'; }
isKeywordThis(): boolean { return this.type == TokenType.Keyword && this.strValue == 'this'; }
isError(): boolean { return this.type == TokenType.Error; }
toNumber(): number { return this.type == TokenType.Number ? this.numValue : -1; }
toString(): string {
switch (this.type) {
case TokenType.Character:
case TokenType.Identifier:
case TokenType.Keyword:
case TokenType.Operator:
case TokenType.String:
case TokenType.Error:
return this.strValue;
case TokenType.Number:
return this.numValue.toString();
default:
return null;
}
}
}
function newCharacterToken(index: number, code: number): Token {
return new Token(index, TokenType.Character, code, String.fromCharCode(code));
}
function newIdentifierToken(index: number, text: string): Token {
return new Token(index, TokenType.Identifier, 0, text);
}
function newKeywordToken(index: number, text: string): Token {
return new Token(index, TokenType.Keyword, 0, text);
}
function newOperatorToken(index: number, text: string): Token {
return new Token(index, TokenType.Operator, 0, text);
}
function newStringToken(index: number, text: string): Token {
return new Token(index, TokenType.String, 0, text);
}
function newNumberToken(index: number, n: number): Token {
return new Token(index, TokenType.Number, n, '');
}
function newErrorToken(index: number, message: string): Token {
return new Token(index, TokenType.Error, 0, message);
}
export const EOF: Token = new Token(-1, TokenType.Character, 0, '');
class _Scanner {
length: number;
peek: number = 0;
index: number = -1;
constructor(public input: string) {
this.length = input.length;
this.advance();
}
advance() {
this.peek = ++this.index >= this.length ? chars.$EOF : this.input.charCodeAt(this.index);
}
scanToken(): Token {
const input = this.input, length = this.length;
let peek = this.peek, index = this.index;
// Skip whitespace.
while (peek <= chars.$SPACE) {
if (++index >= length) {
peek = chars.$EOF;
break;
} else {
peek = input.charCodeAt(index);
}
}
this.peek = peek;
this.index = index;
if (index >= length) {
return null;
}
// Handle identifiers and numbers.
if (isIdentifierStart(peek)) return this.scanIdentifier();
if (chars.isDigit(peek)) return this.scanNumber(index);
const start: number = index;
switch (peek) {
case chars.$PERIOD:
this.advance();
return chars.isDigit(this.peek) ? this.scanNumber(start) :
newCharacterToken(start, chars.$PERIOD);
case chars.$LPAREN:
case chars.$RPAREN:
case chars.$LBRACE:
case chars.$RBRACE:
case chars.$LBRACKET:
case chars.$RBRACKET:
case chars.$COMMA:
case chars.$COLON:
case chars.$SEMICOLON:
return this.scanCharacter(start, peek);
case chars.$SQ:
case chars.$DQ:
return this.scanString();
case chars.$HASH:
case chars.$PLUS:
case chars.$MINUS:
case chars.$STAR:
case chars.$SLASH:
case chars.$PERCENT:
case chars.$CARET:
return this.scanOperator(start, String.fromCharCode(peek));
case chars.$QUESTION:
return this.scanComplexOperator(start, '?', chars.$PERIOD, '.');
case chars.$LT:
case chars.$GT:
return this.scanComplexOperator(start, String.fromCharCode(peek), chars.$EQ, '=');
case chars.$BANG:
case chars.$EQ:
return this.scanComplexOperator(
start, String.fromCharCode(peek), chars.$EQ, '=', chars.$EQ, '=');
case chars.$AMPERSAND:
return this.scanComplexOperator(start, '&', chars.$AMPERSAND, '&');
case chars.$BAR:
return this.scanComplexOperator(start, '|', chars.$BAR, '|');
case chars.$NBSP:
while (chars.isWhitespace(this.peek)) this.advance();
return this.scanToken();
}
this.advance();
return this.error(`Unexpected character [${String.fromCharCode(peek)}]`, 0);
}
scanCharacter(start: number, code: number): Token {
this.advance();
return newCharacterToken(start, code);
}
scanOperator(start: number, str: string): Token {
this.advance();
return newOperatorToken(start, str);
}
/**
* Tokenize a 2/3 char long operator
*
* @param start start index in the expression
* @param one first symbol (always part of the operator)
* @param twoCode code point for the second symbol
* @param two second symbol (part of the operator when the second code point matches)
* @param threeCode code point for the third symbol
* @param three third symbol (part of the operator when provided and matches source expression)
* @returns {Token}
*/
scanComplexOperator(
start: number, one: string, twoCode: number, two: string, threeCode?: number,
three?: string): Token {
this.advance();
let str: string = one;
if (this.peek == twoCode) {
this.advance();
str += two;
}
if (threeCode != null && this.peek == threeCode) {
this.advance();
str += three;
}
return newOperatorToken(start, str);
}
scanIdentifier(): Token {
const start: number = this.index;
this.advance();
while (isIdentifierPart(this.peek)) this.advance();
const str: string = this.input.substring(start, this.index);
return KEYWORDS.indexOf(str) > -1 ? newKeywordToken(start, str) :
newIdentifierToken(start, str);
}
scanNumber(start: number): Token {
let simple: boolean = (this.index === start);
this.advance(); // Skip initial digit.
while (true) {
if (chars.isDigit(this.peek)) {
// Do nothing.
} else if (this.peek == chars.$PERIOD) {
simple = false;
} else if (isExponentStart(this.peek)) {
this.advance();
if (isExponentSign(this.peek)) this.advance();
if (!chars.isDigit(this.peek)) return this.error('Invalid exponent', -1);
simple = false;
} else {
break;
}
this.advance();
}
const str: string = this.input.substring(start, this.index);
const value: number = simple ? parseIntAutoRadix(str) : parseFloat(str);
return newNumberToken(start, value);
}
scanString(): Token {
const start: number = this.index;
const quote: number = this.peek;
this.advance(); // Skip initial quote.
let buffer: string = '';
let marker: number = this.index;
const input: string = this.input;
while (this.peek != quote) {
if (this.peek == chars.$BACKSLASH) {
buffer += input.substring(marker, this.index);
this.advance();
let unescapedCode: number;
// Workaround for TS2.1-introduced type strictness
this.peek = this.peek;
if (this.peek == chars.$u) {
// 4 character hex code for unicode character.
const hex: string = input.substring(this.index + 1, this.index + 5);
if (/^[0-9a-f]+$/i.test(hex)) {
unescapedCode = parseInt(hex, 16);
} else {
return this.error(`Invalid unicode escape [\\u${hex}]`, 0);
}
for (let i: number = 0; i < 5; i++) {
this.advance();
}
} else {
unescapedCode = unescape(this.peek);
this.advance();
}
buffer += String.fromCharCode(unescapedCode);
marker = this.index;
} else if (this.peek == chars.$EOF) {
return this.error('Unterminated quote', 0);
} else {
this.advance();
}
}
const last: string = input.substring(marker, this.index);
this.advance(); // Skip terminating quote.
return newStringToken(start, buffer + last);
}
error(message: string, offset: number): Token {
const position: number = this.index + offset;
return newErrorToken(
position, `Lexer Error: ${message} at column ${position} in expression [${this.input}]`);
}
}
function isIdentifierStart(code: number): boolean {
return (chars.$a <= code && code <= chars.$z) || (chars.$A <= code && code <= chars.$Z) ||
(code == chars.$_) || (code == chars.$$);
}
export function isIdentifier(input: string): boolean {
if (input.length == 0) return false;
const scanner = new _Scanner(input);
if (!isIdentifierStart(scanner.peek)) return false;
scanner.advance();
while (scanner.peek !== chars.$EOF) {
if (!isIdentifierPart(scanner.peek)) return false;
scanner.advance();
}
return true;
}
function isIdentifierPart(code: number): boolean {
return chars.isAsciiLetter(code) || chars.isDigit(code) || (code == chars.$_) ||
(code == chars.$$);
}
function isExponentStart(code: number): boolean {
return code == chars.$e || code == chars.$E;
}
function isExponentSign(code: number): boolean {
return code == chars.$MINUS || code == chars.$PLUS;
}
export function isQuote(code: number): boolean {
return code === chars.$SQ || code === chars.$DQ || code === chars.$BT;
}
function unescape(code: number): number {
switch (code) {
case chars.$n:
return chars.$LF;
case chars.$f:
return chars.$FF;
case chars.$r:
return chars.$CR;
case chars.$t:
return chars.$TAB;
case chars.$v:
return chars.$VTAB;
default:
return code;
}
}
function parseIntAutoRadix(text: string): number {
const result: number = parseInt(text);
if (isNaN(result)) {
throw new Error('Invalid integer literal when parsing ' + text);
}
return result;
}

View File

@ -0,0 +1,812 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as chars from '../chars';
import {CompilerInjectable} from '../injectable';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
import {escapeRegExp} from '../util';
import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, ParserError, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding} from './ast';
import {EOF, Lexer, Token, TokenType, isIdentifier, isQuote} from './lexer';
export class SplitInterpolation {
constructor(public strings: string[], public expressions: string[], public offsets: number[]) {}
}
export class TemplateBindingParseResult {
constructor(
public templateBindings: TemplateBinding[], public warnings: string[],
public errors: ParserError[]) {}
}
function _createInterpolateRegExp(config: InterpolationConfig): RegExp {
const pattern = escapeRegExp(config.start) + '([\\s\\S]*?)' + escapeRegExp(config.end);
return new RegExp(pattern, 'g');
}
@CompilerInjectable()
export class Parser {
private errors: ParserError[] = [];
constructor(private _lexer: Lexer) {}
parseAction(
input: string, location: any,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
this._checkNoInterpolation(input, location, interpolationConfig);
const sourceToLex = this._stripComments(input);
const tokens = this._lexer.tokenize(this._stripComments(input));
const ast = new _ParseAST(
input, location, tokens, sourceToLex.length, true, this.errors,
input.length - sourceToLex.length)
.parseChain();
return new ASTWithSource(ast, input, location, this.errors);
}
parseBinding(
input: string, location: any,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
const ast = this._parseBindingAst(input, location, interpolationConfig);
return new ASTWithSource(ast, input, location, this.errors);
}
parseSimpleBinding(
input: string, location: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
const ast = this._parseBindingAst(input, location, interpolationConfig);
const errors = SimpleExpressionChecker.check(ast);
if (errors.length > 0) {
this._reportError(
`Host binding expression cannot contain ${errors.join(' ')}`, input, location);
}
return new ASTWithSource(ast, input, location, this.errors);
}
private _reportError(message: string, input: string, errLocation: string, ctxLocation?: any) {
this.errors.push(new ParserError(message, input, errLocation, ctxLocation));
}
private _parseBindingAst(
input: string, location: string, interpolationConfig: InterpolationConfig): AST {
// Quotes expressions use 3rd-party expression language. We don't want to use
// our lexer or parser for that, so we check for that ahead of time.
const quote = this._parseQuote(input, location);
if (quote != null) {
return quote;
}
this._checkNoInterpolation(input, location, interpolationConfig);
const sourceToLex = this._stripComments(input);
const tokens = this._lexer.tokenize(sourceToLex);
return new _ParseAST(
input, location, tokens, sourceToLex.length, false, this.errors,
input.length - sourceToLex.length)
.parseChain();
}
private _parseQuote(input: string, location: any): AST {
if (input == null) return null;
const prefixSeparatorIndex = input.indexOf(':');
if (prefixSeparatorIndex == -1) return null;
const prefix = input.substring(0, prefixSeparatorIndex).trim();
if (!isIdentifier(prefix)) return null;
const uninterpretedExpression = input.substring(prefixSeparatorIndex + 1);
return new Quote(new ParseSpan(0, input.length), prefix, uninterpretedExpression, location);
}
parseTemplateBindings(prefixToken: string, input: string, location: any):
TemplateBindingParseResult {
const tokens = this._lexer.tokenize(input);
if (prefixToken) {
// Prefix the tokens with the tokens from prefixToken but have them take no space (0 index).
const prefixTokens = this._lexer.tokenize(prefixToken).map(t => {
t.index = 0;
return t;
});
tokens.unshift(...prefixTokens);
}
return new _ParseAST(input, location, tokens, input.length, false, this.errors, 0)
.parseTemplateBindings();
}
parseInterpolation(
input: string, location: any,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
const split = this.splitInterpolation(input, location, interpolationConfig);
if (split == null) return null;
const expressions: AST[] = [];
for (let i = 0; i < split.expressions.length; ++i) {
const expressionText = split.expressions[i];
const sourceToLex = this._stripComments(expressionText);
const tokens = this._lexer.tokenize(this._stripComments(split.expressions[i]));
const ast = new _ParseAST(
input, location, tokens, sourceToLex.length, false, this.errors,
split.offsets[i] + (expressionText.length - sourceToLex.length))
.parseChain();
expressions.push(ast);
}
return new ASTWithSource(
new Interpolation(
new ParseSpan(0, input == null ? 0 : input.length), split.strings, expressions),
input, location, this.errors);
}
splitInterpolation(
input: string, location: string,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation {
const regexp = _createInterpolateRegExp(interpolationConfig);
const parts = input.split(regexp);
if (parts.length <= 1) {
return null;
}
const strings: string[] = [];
const expressions: string[] = [];
const offsets: number[] = [];
let offset = 0;
for (let i = 0; i < parts.length; i++) {
const part: string = parts[i];
if (i % 2 === 0) {
// fixed string
strings.push(part);
offset += part.length;
} else if (part.trim().length > 0) {
offset += interpolationConfig.start.length;
expressions.push(part);
offsets.push(offset);
offset += part.length + interpolationConfig.end.length;
} else {
this._reportError(
'Blank expressions are not allowed in interpolated strings', input,
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
location);
expressions.push('$implict');
offsets.push(offset);
}
}
return new SplitInterpolation(strings, expressions, offsets);
}
wrapLiteralPrimitive(input: string, location: any): ASTWithSource {
return new ASTWithSource(
new LiteralPrimitive(new ParseSpan(0, input == null ? 0 : input.length), input), input,
location, this.errors);
}
private _stripComments(input: string): string {
const i = this._commentStart(input);
return i != null ? input.substring(0, i).trim() : input;
}
private _commentStart(input: string): number {
let outerQuote: number = null;
for (let i = 0; i < input.length - 1; i++) {
const char = input.charCodeAt(i);
const nextChar = input.charCodeAt(i + 1);
if (char === chars.$SLASH && nextChar == chars.$SLASH && outerQuote == null) return i;
if (outerQuote === char) {
outerQuote = null;
} else if (outerQuote == null && isQuote(char)) {
outerQuote = char;
}
}
return null;
}
private _checkNoInterpolation(
input: string, location: any, interpolationConfig: InterpolationConfig): void {
const regexp = _createInterpolateRegExp(interpolationConfig);
const parts = input.split(regexp);
if (parts.length > 1) {
this._reportError(
`Got interpolation (${interpolationConfig.start}${interpolationConfig.end}) where expression was expected`,
input,
`at column ${this._findInterpolationErrorColumn(parts, 1, interpolationConfig)} in`,
location);
}
}
private _findInterpolationErrorColumn(
parts: string[], partInErrIdx: number, interpolationConfig: InterpolationConfig): number {
let errLocation = '';
for (let j = 0; j < partInErrIdx; j++) {
errLocation += j % 2 === 0 ?
parts[j] :
`${interpolationConfig.start}${parts[j]}${interpolationConfig.end}`;
}
return errLocation.length;
}
}
export class _ParseAST {
private rparensExpected = 0;
private rbracketsExpected = 0;
private rbracesExpected = 0;
index: number = 0;
constructor(
public input: string, public location: any, public tokens: Token[],
public inputLength: number, public parseAction: boolean, private errors: ParserError[],
private offset: number) {}
peek(offset: number): Token {
const i = this.index + offset;
return i < this.tokens.length ? this.tokens[i] : EOF;
}
get next(): Token { return this.peek(0); }
get inputIndex(): number {
return (this.index < this.tokens.length) ? this.next.index + this.offset :
this.inputLength + this.offset;
}
span(start: number) { return new ParseSpan(start, this.inputIndex); }
advance() { this.index++; }
optionalCharacter(code: number): boolean {
if (this.next.isCharacter(code)) {
this.advance();
return true;
} else {
return false;
}
}
peekKeywordLet(): boolean { return this.next.isKeywordLet(); }
expectCharacter(code: number) {
if (this.optionalCharacter(code)) return;
this.error(`Missing expected ${String.fromCharCode(code)}`);
}
optionalOperator(op: string): boolean {
if (this.next.isOperator(op)) {
this.advance();
return true;
} else {
return false;
}
}
expectOperator(operator: string) {
if (this.optionalOperator(operator)) return;
this.error(`Missing expected operator ${operator}`);
}
expectIdentifierOrKeyword(): string {
const n = this.next;
if (!n.isIdentifier() && !n.isKeyword()) {
this.error(`Unexpected token ${n}, expected identifier or keyword`);
return '';
}
this.advance();
return n.toString();
}
expectIdentifierOrKeywordOrString(): string {
const n = this.next;
if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) {
this.error(`Unexpected token ${n}, expected identifier, keyword, or string`);
return '';
}
this.advance();
return n.toString();
}
parseChain(): AST {
const exprs: AST[] = [];
const start = this.inputIndex;
while (this.index < this.tokens.length) {
const expr = this.parsePipe();
exprs.push(expr);
if (this.optionalCharacter(chars.$SEMICOLON)) {
if (!this.parseAction) {
this.error('Binding expression cannot contain chained expression');
}
while (this.optionalCharacter(chars.$SEMICOLON)) {
} // read all semicolons
} else if (this.index < this.tokens.length) {
this.error(`Unexpected token '${this.next}'`);
}
}
if (exprs.length == 0) return new EmptyExpr(this.span(start));
if (exprs.length == 1) return exprs[0];
return new Chain(this.span(start), exprs);
}
parsePipe(): AST {
let result = this.parseExpression();
if (this.optionalOperator('|')) {
if (this.parseAction) {
this.error('Cannot have a pipe in an action expression');
}
do {
const name = this.expectIdentifierOrKeyword();
const args: AST[] = [];
while (this.optionalCharacter(chars.$COLON)) {
args.push(this.parseExpression());
}
result = new BindingPipe(this.span(result.span.start), result, name, args);
} while (this.optionalOperator('|'));
}
return result;
}
parseExpression(): AST { return this.parseConditional(); }
parseConditional(): AST {
const start = this.inputIndex;
const result = this.parseLogicalOr();
if (this.optionalOperator('?')) {
const yes = this.parsePipe();
let no: AST;
if (!this.optionalCharacter(chars.$COLON)) {
const end = this.inputIndex;
const expression = this.input.substring(start, end);
this.error(`Conditional expression ${expression} requires all 3 expressions`);
no = new EmptyExpr(this.span(start));
} else {
no = this.parsePipe();
}
return new Conditional(this.span(start), result, yes, no);
} else {
return result;
}
}
parseLogicalOr(): AST {
// '||'
let result = this.parseLogicalAnd();
while (this.optionalOperator('||')) {
const right = this.parseLogicalAnd();
result = new Binary(this.span(result.span.start), '||', result, right);
}
return result;
}
parseLogicalAnd(): AST {
// '&&'
let result = this.parseEquality();
while (this.optionalOperator('&&')) {
const right = this.parseEquality();
result = new Binary(this.span(result.span.start), '&&', result, right);
}
return result;
}
parseEquality(): AST {
// '==','!=','===','!=='
let result = this.parseRelational();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '==':
case '===':
case '!=':
case '!==':
this.advance();
const right = this.parseRelational();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
parseRelational(): AST {
// '<', '>', '<=', '>='
let result = this.parseAdditive();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '<':
case '>':
case '<=':
case '>=':
this.advance();
const right = this.parseAdditive();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
parseAdditive(): AST {
// '+', '-'
let result = this.parseMultiplicative();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '+':
case '-':
this.advance();
let right = this.parseMultiplicative();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
parseMultiplicative(): AST {
// '*', '%', '/'
let result = this.parsePrefix();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '*':
case '%':
case '/':
this.advance();
let right = this.parsePrefix();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
parsePrefix(): AST {
if (this.next.type == TokenType.Operator) {
const start = this.inputIndex;
const operator = this.next.strValue;
let result: AST;
switch (operator) {
case '+':
this.advance();
return this.parsePrefix();
case '-':
this.advance();
result = this.parsePrefix();
return new Binary(
this.span(start), operator, new LiteralPrimitive(new ParseSpan(start, start), 0),
result);
case '!':
this.advance();
result = this.parsePrefix();
return new PrefixNot(this.span(start), result);
}
}
return this.parseCallChain();
}
parseCallChain(): AST {
let result = this.parsePrimary();
while (true) {
if (this.optionalCharacter(chars.$PERIOD)) {
result = this.parseAccessMemberOrMethodCall(result, false);
} else if (this.optionalOperator('?.')) {
result = this.parseAccessMemberOrMethodCall(result, true);
} else if (this.optionalCharacter(chars.$LBRACKET)) {
this.rbracketsExpected++;
const key = this.parsePipe();
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
if (this.optionalOperator('=')) {
const value = this.parseConditional();
result = new KeyedWrite(this.span(result.span.start), result, key, value);
} else {
result = new KeyedRead(this.span(result.span.start), result, key);
}
} else if (this.optionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const args = this.parseCallArguments();
this.rparensExpected--;
this.expectCharacter(chars.$RPAREN);
result = new FunctionCall(this.span(result.span.start), result, args);
} else {
return result;
}
}
}
parsePrimary(): AST {
const start = this.inputIndex;
if (this.optionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const result = this.parsePipe();
this.rparensExpected--;
this.expectCharacter(chars.$RPAREN);
return result;
} else if (this.next.isKeywordNull()) {
this.advance();
return new LiteralPrimitive(this.span(start), null);
} else if (this.next.isKeywordUndefined()) {
this.advance();
return new LiteralPrimitive(this.span(start), void 0);
} else if (this.next.isKeywordTrue()) {
this.advance();
return new LiteralPrimitive(this.span(start), true);
} else if (this.next.isKeywordFalse()) {
this.advance();
return new LiteralPrimitive(this.span(start), false);
} else if (this.next.isKeywordThis()) {
this.advance();
return new ImplicitReceiver(this.span(start));
} else if (this.optionalCharacter(chars.$LBRACKET)) {
this.rbracketsExpected++;
const elements = this.parseExpressionList(chars.$RBRACKET);
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
return new LiteralArray(this.span(start), elements);
} else if (this.next.isCharacter(chars.$LBRACE)) {
return this.parseLiteralMap();
} else if (this.next.isIdentifier()) {
return this.parseAccessMemberOrMethodCall(new ImplicitReceiver(this.span(start)), false);
} else if (this.next.isNumber()) {
const value = this.next.toNumber();
this.advance();
return new LiteralPrimitive(this.span(start), value);
} else if (this.next.isString()) {
const literalValue = this.next.toString();
this.advance();
return new LiteralPrimitive(this.span(start), literalValue);
} else if (this.index >= this.tokens.length) {
this.error(`Unexpected end of expression: ${this.input}`);
return new EmptyExpr(this.span(start));
} else {
this.error(`Unexpected token ${this.next}`);
return new EmptyExpr(this.span(start));
}
}
parseExpressionList(terminator: number): AST[] {
const result: AST[] = [];
if (!this.next.isCharacter(terminator)) {
do {
result.push(this.parsePipe());
} while (this.optionalCharacter(chars.$COMMA));
}
return result;
}
parseLiteralMap(): LiteralMap {
const keys: string[] = [];
const values: AST[] = [];
const start = this.inputIndex;
this.expectCharacter(chars.$LBRACE);
if (!this.optionalCharacter(chars.$RBRACE)) {
this.rbracesExpected++;
do {
const key = this.expectIdentifierOrKeywordOrString();
keys.push(key);
this.expectCharacter(chars.$COLON);
values.push(this.parsePipe());
} while (this.optionalCharacter(chars.$COMMA));
this.rbracesExpected--;
this.expectCharacter(chars.$RBRACE);
}
return new LiteralMap(this.span(start), keys, values);
}
parseAccessMemberOrMethodCall(receiver: AST, isSafe: boolean = false): AST {
const start = receiver.span.start;
const id = this.expectIdentifierOrKeyword();
if (this.optionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const args = this.parseCallArguments();
this.expectCharacter(chars.$RPAREN);
this.rparensExpected--;
const span = this.span(start);
return isSafe ? new SafeMethodCall(span, receiver, id, args) :
new MethodCall(span, receiver, id, args);
} else {
if (isSafe) {
if (this.optionalOperator('=')) {
this.error('The \'?.\' operator cannot be used in the assignment');
return new EmptyExpr(this.span(start));
} else {
return new SafePropertyRead(this.span(start), receiver, id);
}
} else {
if (this.optionalOperator('=')) {
if (!this.parseAction) {
this.error('Bindings cannot contain assignments');
return new EmptyExpr(this.span(start));
}
const value = this.parseConditional();
return new PropertyWrite(this.span(start), receiver, id, value);
} else {
return new PropertyRead(this.span(start), receiver, id);
}
}
}
}
parseCallArguments(): BindingPipe[] {
if (this.next.isCharacter(chars.$RPAREN)) return [];
const positionals: AST[] = [];
do {
positionals.push(this.parsePipe());
} while (this.optionalCharacter(chars.$COMMA));
return positionals as BindingPipe[];
}
/**
* An identifier, a keyword, a string with an optional `-` inbetween.
*/
expectTemplateBindingKey(): string {
let result = '';
let operatorFound = false;
do {
result += this.expectIdentifierOrKeywordOrString();
operatorFound = this.optionalOperator('-');
if (operatorFound) {
result += '-';
}
} while (operatorFound);
return result.toString();
}
parseTemplateBindings(): TemplateBindingParseResult {
const bindings: TemplateBinding[] = [];
let prefix: string = null;
const warnings: string[] = [];
while (this.index < this.tokens.length) {
const start = this.inputIndex;
const keyIsVar: boolean = this.peekKeywordLet();
if (keyIsVar) {
this.advance();
}
let key = this.expectTemplateBindingKey();
if (!keyIsVar) {
if (prefix == null) {
prefix = key;
} else {
key = prefix + key[0].toUpperCase() + key.substring(1);
}
}
this.optionalCharacter(chars.$COLON);
let name: string = null;
let expression: ASTWithSource = null;
if (keyIsVar) {
if (this.optionalOperator('=')) {
name = this.expectTemplateBindingKey();
} else {
name = '\$implicit';
}
} else if (this.next !== EOF && !this.peekKeywordLet()) {
const start = this.inputIndex;
const ast = this.parsePipe();
const source = this.input.substring(start - this.offset, this.inputIndex - this.offset);
expression = new ASTWithSource(ast, source, this.location, this.errors);
}
bindings.push(new TemplateBinding(this.span(start), key, keyIsVar, name, expression));
if (!this.optionalCharacter(chars.$SEMICOLON)) {
this.optionalCharacter(chars.$COMMA);
}
}
return new TemplateBindingParseResult(bindings, warnings, this.errors);
}
error(message: string, index: number = null) {
this.errors.push(new ParserError(message, this.input, this.locationText(index), this.location));
this.skip();
}
private locationText(index: number = null) {
if (index == null) index = this.index;
return (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` :
`at the end of the expression`;
}
// Error recovery should skip tokens until it encounters a recovery point. skip() treats
// the end of input and a ';' as unconditionally a recovery point. It also treats ')',
// '}' and ']' as conditional recovery points if one of calling productions is expecting
// one of these symbols. This allows skip() to recover from errors such as '(a.) + 1' allowing
// more of the AST to be retained (it doesn't skip any tokens as the ')' is retained because
// of the '(' begins an '(' <expr> ')' production). The recovery points of grouping symbols
// must be conditional as they must be skipped if none of the calling productions are not
// expecting the closing token else we will never make progress in the case of an
// extraneous group closing symbol (such as a stray ')'). This is not the case for ';' because
// parseChain() is always the root production and it expects a ';'.
// If a production expects one of these token it increments the corresponding nesting count,
// and then decrements it just prior to checking if the token is in the input.
private skip() {
let n = this.next;
while (this.index < this.tokens.length && !n.isCharacter(chars.$SEMICOLON) &&
(this.rparensExpected <= 0 || !n.isCharacter(chars.$RPAREN)) &&
(this.rbracesExpected <= 0 || !n.isCharacter(chars.$RBRACE)) &&
(this.rbracketsExpected <= 0 || !n.isCharacter(chars.$RBRACKET))) {
if (this.next.isError()) {
this.errors.push(
new ParserError(this.next.toString(), this.input, this.locationText(), this.location));
}
this.advance();
n = this.next;
}
}
}
class SimpleExpressionChecker implements AstVisitor {
static check(ast: AST): string[] {
const s = new SimpleExpressionChecker();
ast.visit(s);
return s.errors;
}
errors: string[] = [];
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
visitInterpolation(ast: Interpolation, context: any) {}
visitLiteralPrimitive(ast: LiteralPrimitive, context: any) {}
visitPropertyRead(ast: PropertyRead, context: any) {}
visitPropertyWrite(ast: PropertyWrite, context: any) {}
visitSafePropertyRead(ast: SafePropertyRead, context: any) {}
visitMethodCall(ast: MethodCall, context: any) {}
visitSafeMethodCall(ast: SafeMethodCall, context: any) {}
visitFunctionCall(ast: FunctionCall, context: any) {}
visitLiteralArray(ast: LiteralArray, context: any) { this.visitAll(ast.expressions); }
visitLiteralMap(ast: LiteralMap, context: any) { this.visitAll(ast.values); }
visitBinary(ast: Binary, context: any) {}
visitPrefixNot(ast: PrefixNot, context: any) {}
visitConditional(ast: Conditional, context: any) {}
visitPipe(ast: BindingPipe, context: any) { this.errors.push('pipes'); }
visitKeyedRead(ast: KeyedRead, context: any) {}
visitKeyedWrite(ast: KeyedWrite, context: any) {}
visitAll(asts: any[]): any[] { return asts.map(node => node.visit(this)); }
visitChain(ast: Chain, context: any) {}
visitQuote(ast: Quote, context: any) {}
}

View File

@ -0,0 +1,376 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as i18n from './i18n_ast';
export function digest(message: i18n.Message): string {
return message.id || sha1(serializeNodes(message.nodes).join('') + `[${message.meaning}]`);
}
export function decimalDigest(message: i18n.Message): string {
if (message.id) {
return message.id;
}
const visitor = new _SerializerIgnoreIcuExpVisitor();
const parts = message.nodes.map(a => a.visit(visitor, null));
return computeMsgId(parts.join(''), message.meaning);
}
/**
* Serialize the i18n ast to something xml-like in order to generate an UID.
*
* The visitor is also used in the i18n parser tests
*
* @internal
*/
class _SerializerVisitor implements i18n.Visitor {
visitText(text: i18n.Text, context: any): any { return text.value; }
visitContainer(container: i18n.Container, context: any): any {
return `[${container.children.map(child => child.visit(this)).join(', ')}]`;
}
visitIcu(icu: i18n.Icu, context: any): any {
const strCases =
Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
return `{${icu.expression}, ${icu.type}, ${strCases.join(', ')}}`;
}
visitTagPlaceholder(ph: i18n.TagPlaceholder, context: any): any {
return ph.isVoid ?
`<ph tag name="${ph.startName}"/>` :
`<ph tag name="${ph.startName}">${ph.children.map(child => child.visit(this)).join(', ')}</ph name="${ph.closeName}">`;
}
visitPlaceholder(ph: i18n.Placeholder, context: any): any {
return ph.value ? `<ph name="${ph.name}">${ph.value}</ph>` : `<ph name="${ph.name}"/>`;
}
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
return `<ph icu name="${ph.name}">${ph.value.visit(this)}</ph>`;
}
}
const serializerVisitor = new _SerializerVisitor();
export function serializeNodes(nodes: i18n.Node[]): string[] {
return nodes.map(a => a.visit(serializerVisitor, null));
}
/**
* Serialize the i18n ast to something xml-like in order to generate an UID.
*
* Ignore the ICU expressions so that message IDs stays identical if only the expression changes.
*
* @internal
*/
class _SerializerIgnoreIcuExpVisitor extends _SerializerVisitor {
visitIcu(icu: i18n.Icu, context: any): any {
let strCases = Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
// Do not take the expression into account
return `{${icu.type}, ${strCases.join(', ')}}`;
}
}
/**
* Compute the SHA1 of the given string
*
* see http://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf
*
* WARNING: this function has not been designed not tested with security in mind.
* DO NOT USE IT IN A SECURITY SENSITIVE CONTEXT.
*/
export function sha1(str: string): string {
const utf8 = utf8Encode(str);
const words32 = stringToWords32(utf8, Endian.Big);
const len = utf8.length * 8;
const w = new Array(80);
let [a, b, c, d, e]: number[] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
words32[len >> 5] |= 0x80 << (24 - len % 32);
words32[((len + 64 >> 9) << 4) + 15] = len;
for (let i = 0; i < words32.length; i += 16) {
const [h0, h1, h2, h3, h4]: number[] = [a, b, c, d, e];
for (let j = 0; j < 80; j++) {
if (j < 16) {
w[j] = words32[i + j];
} else {
w[j] = rol32(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
}
const [f, k] = fk(j, b, c, d);
const temp = [rol32(a, 5), f, e, k, w[j]].reduce(add32);
[e, d, c, b, a] = [d, c, rol32(b, 30), a, temp];
}
[a, b, c, d, e] = [add32(a, h0), add32(b, h1), add32(c, h2), add32(d, h3), add32(e, h4)];
}
return byteStringToHexString(words32ToByteString([a, b, c, d, e]));
}
function fk(index: number, b: number, c: number, d: number): [number, number] {
if (index < 20) {
return [(b & c) | (~b & d), 0x5a827999];
}
if (index < 40) {
return [b ^ c ^ d, 0x6ed9eba1];
}
if (index < 60) {
return [(b & c) | (b & d) | (c & d), 0x8f1bbcdc];
}
return [b ^ c ^ d, 0xca62c1d6];
}
/**
* Compute the fingerprint of the given string
*
* The output is 64 bit number encoded as a decimal string
*
* based on:
* https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/GoogleJsMessageIdGenerator.java
*/
export function fingerprint(str: string): [number, number] {
const utf8 = utf8Encode(str);
let [hi, lo] = [hash32(utf8, 0), hash32(utf8, 102072)];
if (hi == 0 && (lo == 0 || lo == 1)) {
hi = hi ^ 0x130f9bef;
lo = lo ^ -0x6b5f56d8;
}
return [hi, lo];
}
export function computeMsgId(msg: string, meaning: string): string {
let [hi, lo] = fingerprint(msg);
if (meaning) {
const [him, lom] = fingerprint(meaning);
[hi, lo] = add64(rol64([hi, lo], 1), [him, lom]);
}
return byteStringToDecString(words32ToByteString([hi & 0x7fffffff, lo]));
}
function hash32(str: string, c: number): number {
let [a, b] = [0x9e3779b9, 0x9e3779b9];
let i: number;
const len = str.length;
for (i = 0; i + 12 <= len; i += 12) {
a = add32(a, wordAt(str, i, Endian.Little));
b = add32(b, wordAt(str, i + 4, Endian.Little));
c = add32(c, wordAt(str, i + 8, Endian.Little));
[a, b, c] = mix([a, b, c]);
}
a = add32(a, wordAt(str, i, Endian.Little));
b = add32(b, wordAt(str, i + 4, Endian.Little));
// the first byte of c is reserved for the length
c = add32(c, len);
c = add32(c, wordAt(str, i + 8, Endian.Little) << 8);
return mix([a, b, c])[2];
}
// clang-format off
function mix([a, b, c]: [number, number, number]): [number, number, number] {
a = sub32(a, b); a = sub32(a, c); a ^= c >>> 13;
b = sub32(b, c); b = sub32(b, a); b ^= a << 8;
c = sub32(c, a); c = sub32(c, b); c ^= b >>> 13;
a = sub32(a, b); a = sub32(a, c); a ^= c >>> 12;
b = sub32(b, c); b = sub32(b, a); b ^= a << 16;
c = sub32(c, a); c = sub32(c, b); c ^= b >>> 5;
a = sub32(a, b); a = sub32(a, c); a ^= c >>> 3;
b = sub32(b, c); b = sub32(b, a); b ^= a << 10;
c = sub32(c, a); c = sub32(c, b); c ^= b >>> 15;
return [a, b, c];
}
// clang-format on
// Utils
enum Endian {
Little,
Big,
}
function utf8Encode(str: string): string {
let encoded: string = '';
for (let index = 0; index < str.length; index++) {
const codePoint = decodeSurrogatePairs(str, index);
if (codePoint <= 0x7f) {
encoded += String.fromCharCode(codePoint);
} else if (codePoint <= 0x7ff) {
encoded += String.fromCharCode(0xc0 | codePoint >>> 6, 0x80 | codePoint & 0x3f);
} else if (codePoint <= 0xffff) {
encoded += String.fromCharCode(
0xe0 | codePoint >>> 12, 0x80 | codePoint >>> 6 & 0x3f, 0x80 | codePoint & 0x3f);
} else if (codePoint <= 0x1fffff) {
encoded += String.fromCharCode(
0xf0 | codePoint >>> 18, 0x80 | codePoint >>> 12 & 0x3f, 0x80 | codePoint >>> 6 & 0x3f,
0x80 | codePoint & 0x3f);
}
}
return encoded;
}
// see https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
function decodeSurrogatePairs(str: string, index: number): number {
if (index < 0 || index >= str.length) {
throw new Error(`index=${index} is out of range in "${str}"`);
}
const high = str.charCodeAt(index);
if (high >= 0xd800 && high <= 0xdfff && str.length > index + 1) {
const low = byteAt(str, index + 1);
if (low >= 0xdc00 && low <= 0xdfff) {
return (high - 0xd800) * 0x400 + low - 0xdc00 + 0x10000;
}
}
return high;
}
function add32(a: number, b: number): number {
return add32to64(a, b)[1];
}
function add32to64(a: number, b: number): [number, number] {
const low = (a & 0xffff) + (b & 0xffff);
const high = (a >>> 16) + (b >>> 16) + (low >>> 16);
return [high >>> 16, (high << 16) | (low & 0xffff)];
}
function add64([ah, al]: [number, number], [bh, bl]: [number, number]): [number, number] {
const [carry, l] = add32to64(al, bl);
const h = add32(add32(ah, bh), carry);
return [h, l];
}
function sub32(a: number, b: number): number {
const low = (a & 0xffff) - (b & 0xffff);
const high = (a >> 16) - (b >> 16) + (low >> 16);
return (high << 16) | (low & 0xffff);
}
// Rotate a 32b number left `count` position
function rol32(a: number, count: number): number {
return (a << count) | (a >>> (32 - count));
}
// Rotate a 64b number left `count` position
function rol64([hi, lo]: [number, number], count: number): [number, number] {
const h = (hi << count) | (lo >>> (32 - count));
const l = (lo << count) | (hi >>> (32 - count));
return [h, l];
}
function stringToWords32(str: string, endian: Endian): number[] {
const words32 = Array((str.length + 3) >>> 2);
for (let i = 0; i < words32.length; i++) {
words32[i] = wordAt(str, i * 4, endian);
}
return words32;
}
function byteAt(str: string, index: number): number {
return index >= str.length ? 0 : str.charCodeAt(index) & 0xff;
}
function wordAt(str: string, index: number, endian: Endian): number {
let word = 0;
if (endian === Endian.Big) {
for (let i = 0; i < 4; i++) {
word += byteAt(str, index + i) << (24 - 8 * i);
}
} else {
for (let i = 0; i < 4; i++) {
word += byteAt(str, index + i) << 8 * i;
}
}
return word;
}
function words32ToByteString(words32: number[]): string {
return words32.reduce((str, word) => str + word32ToByteString(word), '');
}
function word32ToByteString(word: number): string {
let str = '';
for (let i = 0; i < 4; i++) {
str += String.fromCharCode((word >>> 8 * (3 - i)) & 0xff);
}
return str;
}
function byteStringToHexString(str: string): string {
let hex: string = '';
for (let i = 0; i < str.length; i++) {
const b = byteAt(str, i);
hex += (b >>> 4).toString(16) + (b & 0x0f).toString(16);
}
return hex.toLowerCase();
}
// based on http://www.danvk.org/hex2dec.html (JS can not handle more than 56b)
function byteStringToDecString(str: string): string {
let decimal = '';
let toThePower = '1';
for (let i = str.length - 1; i >= 0; i--) {
decimal = addBigInt(decimal, numberTimesBigInt(byteAt(str, i), toThePower));
toThePower = numberTimesBigInt(256, toThePower);
}
return decimal.split('').reverse().join('');
}
// x and y decimal, lowest significant digit first
function addBigInt(x: string, y: string): string {
let sum = '';
const len = Math.max(x.length, y.length);
for (let i = 0, carry = 0; i < len || carry; i++) {
const tmpSum = carry + +(x[i] || 0) + +(y[i] || 0);
if (tmpSum >= 10) {
carry = 1;
sum += tmpSum - 10;
} else {
carry = 0;
sum += tmpSum;
}
}
return sum;
}
function numberTimesBigInt(num: number, b: string): string {
let product = '';
let bToThePower = b;
for (; num !== 0; num = num >>> 1) {
if (num & 1) product = addBigInt(product, bToThePower);
bToThePower = addBigInt(bToThePower, bToThePower);
}
return product;
}

View File

@ -0,0 +1,117 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Extract i18n messages from source code
*/
import {ViewEncapsulation} from '@angular/core';
import {analyzeAndValidateNgModules, extractProgramSymbols} from '../aot/compiler';
import {StaticAndDynamicReflectionCapabilities} from '../aot/static_reflection_capabilities';
import {StaticReflector} from '../aot/static_reflector';
import {StaticSymbolCache} from '../aot/static_symbol';
import {StaticSymbolResolver, StaticSymbolResolverHost} from '../aot/static_symbol_resolver';
import {AotSummaryResolver, AotSummaryResolverHost} from '../aot/summary_resolver';
import {CompileDirectiveMetadata} from '../compile_metadata';
import {CompilerConfig} from '../config';
import {DirectiveNormalizer} from '../directive_normalizer';
import {DirectiveResolver} from '../directive_resolver';
import {CompileMetadataResolver} from '../metadata_resolver';
import {HtmlParser} from '../ml_parser/html_parser';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {NgModuleResolver} from '../ng_module_resolver';
import {ParseError} from '../parse_util';
import {PipeResolver} from '../pipe_resolver';
import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry';
import {createOfflineCompileUrlResolver} from '../url_resolver';
import {I18NHtmlParser} from './i18n_html_parser';
import {MessageBundle} from './message_bundle';
/**
* The host of the Extractor disconnects the implementation from TypeScript / other language
* services and from underlying file systems.
*/
export interface ExtractorHost extends StaticSymbolResolverHost, AotSummaryResolverHost {
/**
* Loads a resource (e.g. html / css)
*/
loadResource(path: string): Promise<string>;
}
export class Extractor {
constructor(
public host: ExtractorHost, private staticSymbolResolver: StaticSymbolResolver,
private messageBundle: MessageBundle, private metadataResolver: CompileMetadataResolver) {}
extract(rootFiles: string[]): Promise<MessageBundle> {
const programSymbols = extractProgramSymbols(this.staticSymbolResolver, rootFiles, this.host);
const {files, ngModules} =
analyzeAndValidateNgModules(programSymbols, this.host, this.metadataResolver);
return Promise
.all(ngModules.map(
ngModule => this.metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
ngModule.type.reference, false)))
.then(() => {
const errors: ParseError[] = [];
files.forEach(file => {
const compMetas: CompileDirectiveMetadata[] = [];
file.directives.forEach(directiveType => {
const dirMeta = this.metadataResolver.getDirectiveMetadata(directiveType);
if (dirMeta && dirMeta.isComponent) {
compMetas.push(dirMeta);
}
});
compMetas.forEach(compMeta => {
const html = compMeta.template.template;
const interpolationConfig =
InterpolationConfig.fromArray(compMeta.template.interpolation);
errors.push(
...this.messageBundle.updateFromTemplate(html, file.srcUrl, interpolationConfig));
});
});
if (errors.length) {
throw new Error(errors.map(e => e.toString()).join('\n'));
}
return this.messageBundle;
});
}
static create(host: ExtractorHost, locale: string|null):
{extractor: Extractor, staticReflector: StaticReflector} {
const htmlParser = new I18NHtmlParser(new HtmlParser());
const urlResolver = createOfflineCompileUrlResolver();
const symbolCache = new StaticSymbolCache();
const summaryResolver = new AotSummaryResolver(host, symbolCache);
const staticSymbolResolver = new StaticSymbolResolver(host, symbolCache, summaryResolver);
const staticReflector = new StaticReflector(staticSymbolResolver);
StaticAndDynamicReflectionCapabilities.install(staticReflector);
const config =
new CompilerConfig({defaultEncapsulation: ViewEncapsulation.Emulated, useJit: false});
const normalizer = new DirectiveNormalizer(
{get: (url: string) => host.loadResource(url)}, urlResolver, htmlParser, config);
const elementSchemaRegistry = new DomElementSchemaRegistry();
const resolver = new CompileMetadataResolver(
config, new NgModuleResolver(staticReflector), new DirectiveResolver(staticReflector),
new PipeResolver(staticReflector), summaryResolver, elementSchemaRegistry, normalizer,
symbolCache, staticReflector);
// TODO(vicb): implicit tags & attributes
const messageBundle = new MessageBundle(htmlParser, [], {}, locale);
const extractor = new Extractor(host, staticSymbolResolver, messageBundle, resolver);
return {extractor, staticReflector};
}
}

View File

@ -0,0 +1,497 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as html from '../ml_parser/ast';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseTreeResult} from '../ml_parser/parser';
import * as i18n from './i18n_ast';
import {createI18nMessageFactory} from './i18n_parser';
import {I18nError} from './parse_util';
import {TranslationBundle} from './translation_bundle';
const _I18N_ATTR = 'i18n';
const _I18N_ATTR_PREFIX = 'i18n-';
const _I18N_COMMENT_PREFIX_REGEXP = /^i18n:?/;
const MEANING_SEPARATOR = '|';
const ID_SEPARATOR = '@@';
/**
* Extract translatable messages from an html AST
*/
export function extractMessages(
nodes: html.Node[], interpolationConfig: InterpolationConfig, implicitTags: string[],
implicitAttrs: {[k: string]: string[]}): ExtractionResult {
const visitor = new _Visitor(implicitTags, implicitAttrs);
return visitor.extract(nodes, interpolationConfig);
}
export function mergeTranslations(
nodes: html.Node[], translations: TranslationBundle, interpolationConfig: InterpolationConfig,
implicitTags: string[], implicitAttrs: {[k: string]: string[]}): ParseTreeResult {
const visitor = new _Visitor(implicitTags, implicitAttrs);
return visitor.merge(nodes, translations, interpolationConfig);
}
export class ExtractionResult {
constructor(public messages: i18n.Message[], public errors: I18nError[]) {}
}
enum _VisitorMode {
Extract,
Merge
}
/**
* This Visitor is used:
* 1. to extract all the translatable strings from an html AST (see `extract()`),
* 2. to replace the translatable strings with the actual translations (see `merge()`)
*
* @internal
*/
class _Visitor implements html.Visitor {
private _depth: number;
// <el i18n>...</el>
private _inI18nNode: boolean;
private _inImplicitNode: boolean;
// <!--i18n-->...<!--/i18n-->
private _inI18nBlock: boolean;
private _blockMeaningAndDesc: string;
private _blockChildren: html.Node[];
private _blockStartDepth: number;
// {<icu message>}
private _inIcu: boolean;
// set to void 0 when not in a section
private _msgCountAtSectionStart: number;
private _errors: I18nError[];
private _mode: _VisitorMode;
// _VisitorMode.Extract only
private _messages: i18n.Message[];
// _VisitorMode.Merge only
private _translations: TranslationBundle;
private _createI18nMessage:
(msg: html.Node[], meaning: string, description: string, id: string) => i18n.Message;
constructor(private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {}
/**
* Extracts the messages from the tree
*/
extract(nodes: html.Node[], interpolationConfig: InterpolationConfig): ExtractionResult {
this._init(_VisitorMode.Extract, interpolationConfig);
nodes.forEach(node => node.visit(this, null));
if (this._inI18nBlock) {
this._reportError(nodes[nodes.length - 1], 'Unclosed block');
}
return new ExtractionResult(this._messages, this._errors);
}
/**
* Returns a tree where all translatable nodes are translated
*/
merge(
nodes: html.Node[], translations: TranslationBundle,
interpolationConfig: InterpolationConfig): ParseTreeResult {
this._init(_VisitorMode.Merge, interpolationConfig);
this._translations = translations;
// Construct a single fake root element
const wrapper = new html.Element('wrapper', [], nodes, null, null, null);
const translatedNode = wrapper.visit(this, null);
if (this._inI18nBlock) {
this._reportError(nodes[nodes.length - 1], 'Unclosed block');
}
return new ParseTreeResult(translatedNode.children, this._errors);
}
visitExpansionCase(icuCase: html.ExpansionCase, context: any): any {
// Parse cases for translatable html attributes
const expression = html.visitAll(this, icuCase.expression, context);
if (this._mode === _VisitorMode.Merge) {
return new html.ExpansionCase(
icuCase.value, expression, icuCase.sourceSpan, icuCase.valueSourceSpan,
icuCase.expSourceSpan);
}
}
visitExpansion(icu: html.Expansion, context: any): html.Expansion {
this._mayBeAddBlockChildren(icu);
const wasInIcu = this._inIcu;
if (!this._inIcu) {
// nested ICU messages should not be extracted but top-level translated as a whole
if (this._isInTranslatableSection) {
this._addMessage([icu]);
}
this._inIcu = true;
}
const cases = html.visitAll(this, icu.cases, context);
if (this._mode === _VisitorMode.Merge) {
icu = new html.Expansion(
icu.switchValue, icu.type, cases, icu.sourceSpan, icu.switchValueSourceSpan);
}
this._inIcu = wasInIcu;
return icu;
}
visitComment(comment: html.Comment, context: any): any {
const isOpening = _isOpeningComment(comment);
if (isOpening && this._isInTranslatableSection) {
this._reportError(comment, 'Could not start a block inside a translatable section');
return;
}
const isClosing = _isClosingComment(comment);
if (isClosing && !this._inI18nBlock) {
this._reportError(comment, 'Trying to close an unopened block');
return;
}
if (!this._inI18nNode && !this._inIcu) {
if (!this._inI18nBlock) {
if (isOpening) {
this._inI18nBlock = true;
this._blockStartDepth = this._depth;
this._blockChildren = [];
this._blockMeaningAndDesc = comment.value.replace(_I18N_COMMENT_PREFIX_REGEXP, '').trim();
this._openTranslatableSection(comment);
}
} else {
if (isClosing) {
if (this._depth == this._blockStartDepth) {
this._closeTranslatableSection(comment, this._blockChildren);
this._inI18nBlock = false;
const message = this._addMessage(this._blockChildren, this._blockMeaningAndDesc);
// merge attributes in sections
const nodes = this._translateMessage(comment, message);
return html.visitAll(this, nodes);
} else {
this._reportError(comment, 'I18N blocks should not cross element boundaries');
return;
}
}
}
}
}
visitText(text: html.Text, context: any): html.Text {
if (this._isInTranslatableSection) {
this._mayBeAddBlockChildren(text);
}
return text;
}
visitElement(el: html.Element, context: any): html.Element {
this._mayBeAddBlockChildren(el);
this._depth++;
const wasInI18nNode = this._inI18nNode;
const wasInImplicitNode = this._inImplicitNode;
let childNodes: html.Node[] = [];
let translatedChildNodes: html.Node[];
// Extract:
// - top level nodes with the (implicit) "i18n" attribute if not already in a section
// - ICU messages
const i18nAttr = _getI18nAttr(el);
const i18nMeta = i18nAttr ? i18nAttr.value : '';
const isImplicit = this._implicitTags.some(tag => el.name === tag) && !this._inIcu &&
!this._isInTranslatableSection;
const isTopLevelImplicit = !wasInImplicitNode && isImplicit;
this._inImplicitNode = wasInImplicitNode || isImplicit;
if (!this._isInTranslatableSection && !this._inIcu) {
if (i18nAttr || isTopLevelImplicit) {
this._inI18nNode = true;
const message = this._addMessage(el.children, i18nMeta);
translatedChildNodes = this._translateMessage(el, message);
}
if (this._mode == _VisitorMode.Extract) {
const isTranslatable = i18nAttr || isTopLevelImplicit;
if (isTranslatable) this._openTranslatableSection(el);
html.visitAll(this, el.children);
if (isTranslatable) this._closeTranslatableSection(el, el.children);
}
} else {
if (i18nAttr || isTopLevelImplicit) {
this._reportError(
el, 'Could not mark an element as translatable inside a translatable section');
}
if (this._mode == _VisitorMode.Extract) {
// Descend into child nodes for extraction
html.visitAll(this, el.children);
}
}
if (this._mode === _VisitorMode.Merge) {
const visitNodes = translatedChildNodes || el.children;
visitNodes.forEach(child => {
const visited = child.visit(this, context);
if (visited && !this._isInTranslatableSection) {
// Do not add the children from translatable sections (= i18n blocks here)
// They will be added later in this loop when the block closes (i.e. on `<!-- /i18n -->`)
childNodes = childNodes.concat(visited);
}
});
}
this._visitAttributesOf(el);
this._depth--;
this._inI18nNode = wasInI18nNode;
this._inImplicitNode = wasInImplicitNode;
if (this._mode === _VisitorMode.Merge) {
const translatedAttrs = this._translateAttributes(el);
return new html.Element(
el.name, translatedAttrs, childNodes, el.sourceSpan, el.startSourceSpan,
el.endSourceSpan);
}
}
visitAttribute(attribute: html.Attribute, context: any): any {
throw new Error('unreachable code');
}
private _init(mode: _VisitorMode, interpolationConfig: InterpolationConfig): void {
this._mode = mode;
this._inI18nBlock = false;
this._inI18nNode = false;
this._depth = 0;
this._inIcu = false;
this._msgCountAtSectionStart = void 0;
this._errors = [];
this._messages = [];
this._inImplicitNode = false;
this._createI18nMessage = createI18nMessageFactory(interpolationConfig);
}
// looks for translatable attributes
private _visitAttributesOf(el: html.Element): void {
const explicitAttrNameToValue: {[k: string]: string} = {};
const implicitAttrNames: string[] = this._implicitAttrs[el.name] || [];
el.attrs.filter(attr => attr.name.startsWith(_I18N_ATTR_PREFIX))
.forEach(
attr => explicitAttrNameToValue[attr.name.slice(_I18N_ATTR_PREFIX.length)] =
attr.value);
el.attrs.forEach(attr => {
if (attr.name in explicitAttrNameToValue) {
this._addMessage([attr], explicitAttrNameToValue[attr.name]);
} else if (implicitAttrNames.some(name => attr.name === name)) {
this._addMessage([attr]);
}
});
}
// add a translatable message
private _addMessage(ast: html.Node[], msgMeta?: string): i18n.Message {
if (ast.length == 0 ||
ast.length == 1 && ast[0] instanceof html.Attribute && !(<html.Attribute>ast[0]).value) {
// Do not create empty messages
return;
}
const {meaning, description, id} = _parseMessageMeta(msgMeta);
const message = this._createI18nMessage(ast, meaning, description, id);
this._messages.push(message);
return message;
}
// Translates the given message given the `TranslationBundle`
// This is used for translating elements / blocks - see `_translateAttributes` for attributes
// no-op when called in extraction mode (returns [])
private _translateMessage(el: html.Node, message: i18n.Message): html.Node[] {
if (message && this._mode === _VisitorMode.Merge) {
const nodes = this._translations.get(message);
if (nodes) {
return nodes;
}
this._reportError(
el, `Translation unavailable for message id="${this._translations.digest(message)}"`);
}
return [];
}
// translate the attributes of an element and remove i18n specific attributes
private _translateAttributes(el: html.Element): html.Attribute[] {
const attributes = el.attrs;
const i18nAttributeMeanings: {[name: string]: string} = {};
attributes.forEach(attr => {
if (attr.name.startsWith(_I18N_ATTR_PREFIX)) {
i18nAttributeMeanings[attr.name.slice(_I18N_ATTR_PREFIX.length)] =
_parseMessageMeta(attr.value).meaning;
}
});
const translatedAttributes: html.Attribute[] = [];
attributes.forEach((attr) => {
if (attr.name === _I18N_ATTR || attr.name.startsWith(_I18N_ATTR_PREFIX)) {
// strip i18n specific attributes
return;
}
if (attr.value && attr.value != '' && i18nAttributeMeanings.hasOwnProperty(attr.name)) {
const meaning = i18nAttributeMeanings[attr.name];
const message: i18n.Message = this._createI18nMessage([attr], meaning, '', '');
const nodes = this._translations.get(message);
if (nodes) {
if (nodes.length == 0) {
translatedAttributes.push(new html.Attribute(attr.name, '', attr.sourceSpan));
} else if (nodes[0] instanceof html.Text) {
const value = (nodes[0] as html.Text).value;
translatedAttributes.push(new html.Attribute(attr.name, value, attr.sourceSpan));
} else {
this._reportError(
el,
`Unexpected translation for attribute "${attr.name}" (id="${this._translations.digest(message)}")`);
}
} else {
this._reportError(
el,
`Translation unavailable for attribute "${attr.name}" (id="${this._translations.digest(message)}")`);
}
} else {
translatedAttributes.push(attr);
}
});
return translatedAttributes;
}
/**
* Add the node as a child of the block when:
* - we are in a block,
* - we are not inside a ICU message (those are handled separately),
* - the node is a "direct child" of the block
*/
private _mayBeAddBlockChildren(node: html.Node): void {
if (this._inI18nBlock && !this._inIcu && this._depth == this._blockStartDepth) {
this._blockChildren.push(node);
}
}
/**
* Marks the start of a section, see `_closeTranslatableSection`
*/
private _openTranslatableSection(node: html.Node): void {
if (this._isInTranslatableSection) {
this._reportError(node, 'Unexpected section start');
} else {
this._msgCountAtSectionStart = this._messages.length;
}
}
/**
* A translatable section could be:
* - the content of translatable element,
* - nodes between `<!-- i18n -->` and `<!-- /i18n -->` comments
*/
private get _isInTranslatableSection(): boolean {
return this._msgCountAtSectionStart !== void 0;
}
/**
* Terminates a section.
*
* If a section has only one significant children (comments not significant) then we should not
* keep the message from this children:
*
* `<p i18n="meaning|description">{ICU message}</p>` would produce two messages:
* - one for the <p> content with meaning and description,
* - another one for the ICU message.
*
* In this case the last message is discarded as it contains less information (the AST is
* otherwise identical).
*
* Note that we should still keep messages extracted from attributes inside the section (ie in the
* ICU message here)
*/
private _closeTranslatableSection(node: html.Node, directChildren: html.Node[]): void {
if (!this._isInTranslatableSection) {
this._reportError(node, 'Unexpected section end');
return;
}
const startIndex = this._msgCountAtSectionStart;
const significantChildren: number = directChildren.reduce(
(count: number, node: html.Node): number => count + (node instanceof html.Comment ? 0 : 1),
0);
if (significantChildren == 1) {
for (let i = this._messages.length - 1; i >= startIndex; i--) {
const ast = this._messages[i].nodes;
if (!(ast.length == 1 && ast[0] instanceof i18n.Text)) {
this._messages.splice(i, 1);
break;
}
}
}
this._msgCountAtSectionStart = void 0;
}
private _reportError(node: html.Node, msg: string): void {
this._errors.push(new I18nError(node.sourceSpan, msg));
}
}
function _isOpeningComment(n: html.Node): boolean {
return n instanceof html.Comment && n.value && n.value.startsWith('i18n');
}
function _isClosingComment(n: html.Node): boolean {
return n instanceof html.Comment && n.value && n.value === '/i18n';
}
function _getI18nAttr(p: html.Element): html.Attribute {
return p.attrs.find(attr => attr.name === _I18N_ATTR) || null;
}
function _parseMessageMeta(i18n: string): {meaning: string, description: string, id: string} {
if (!i18n) return {meaning: '', description: '', id: ''};
const idIndex = i18n.indexOf(ID_SEPARATOR);
const descIndex = i18n.indexOf(MEANING_SEPARATOR);
const [meaningAndDesc, id] =
(idIndex > -1) ? [i18n.slice(0, idIndex), i18n.slice(idIndex + 2)] : [i18n, ''];
const [meaning, description] = (descIndex > -1) ?
[meaningAndDesc.slice(0, descIndex), meaningAndDesc.slice(descIndex + 1)] :
['', meaningAndDesc];
return {meaning, description, id};
}

View File

@ -0,0 +1,134 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseSourceSpan} from '../parse_util';
export class Message {
/**
* @param nodes message AST
* @param placeholders maps placeholder names to static content
* @param placeholderToMessage maps placeholder names to messages (used for nested ICU messages)
* @param meaning
* @param description
* @param id
*/
constructor(
public nodes: Node[], public placeholders: {[phName: string]: string},
public placeholderToMessage: {[phName: string]: Message}, public meaning: string,
public description: string, public id: string) {}
}
export interface Node {
sourceSpan: ParseSourceSpan;
visit(visitor: Visitor, context?: any): any;
}
export class Text implements Node {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitText(this, context); }
}
// TODO(vicb): do we really need this node (vs an array) ?
export class Container implements Node {
constructor(public children: Node[], public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitContainer(this, context); }
}
export class Icu implements Node {
public expressionPlaceholder: string;
constructor(
public expression: string, public type: string, public cases: {[k: string]: Node},
public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitIcu(this, context); }
}
export class TagPlaceholder implements Node {
constructor(
public tag: string, public attrs: {[k: string]: string}, public startName: string,
public closeName: string, public children: Node[], public isVoid: boolean,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitTagPlaceholder(this, context); }
}
export class Placeholder implements Node {
constructor(public value: string, public name: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitPlaceholder(this, context); }
}
export class IcuPlaceholder implements Node {
constructor(public value: Icu, public name: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitIcuPlaceholder(this, context); }
}
export interface Visitor {
visitText(text: Text, context?: any): any;
visitContainer(container: Container, context?: any): any;
visitIcu(icu: Icu, context?: any): any;
visitTagPlaceholder(ph: TagPlaceholder, context?: any): any;
visitPlaceholder(ph: Placeholder, context?: any): any;
visitIcuPlaceholder(ph: IcuPlaceholder, context?: any): any;
}
// Clone the AST
export class CloneVisitor implements Visitor {
visitText(text: Text, context?: any): Text { return new Text(text.value, text.sourceSpan); }
visitContainer(container: Container, context?: any): Container {
const children = container.children.map(n => n.visit(this, context));
return new Container(children, container.sourceSpan);
}
visitIcu(icu: Icu, context?: any): Icu {
const cases: {[k: string]: Node} = {};
Object.keys(icu.cases).forEach(key => cases[key] = icu.cases[key].visit(this, context));
const msg = new Icu(icu.expression, icu.type, cases, icu.sourceSpan);
msg.expressionPlaceholder = icu.expressionPlaceholder;
return msg;
}
visitTagPlaceholder(ph: TagPlaceholder, context?: any): TagPlaceholder {
const children = ph.children.map(n => n.visit(this, context));
return new TagPlaceholder(
ph.tag, ph.attrs, ph.startName, ph.closeName, children, ph.isVoid, ph.sourceSpan);
}
visitPlaceholder(ph: Placeholder, context?: any): Placeholder {
return new Placeholder(ph.value, ph.name, ph.sourceSpan);
}
visitIcuPlaceholder(ph: IcuPlaceholder, context?: any): IcuPlaceholder {
return new IcuPlaceholder(ph.value, ph.name, ph.sourceSpan);
}
}
// Visit all the nodes recursively
export class RecurseVisitor implements Visitor {
visitText(text: Text, context?: any): any{};
visitContainer(container: Container, context?: any): any {
container.children.forEach(child => child.visit(this));
}
visitIcu(icu: Icu, context?: any): any {
Object.keys(icu.cases).forEach(k => { icu.cases[k].visit(this); });
}
visitTagPlaceholder(ph: TagPlaceholder, context?: any): any {
ph.children.forEach(child => child.visit(this));
}
visitPlaceholder(ph: Placeholder, context?: any): any{};
visitIcuPlaceholder(ph: IcuPlaceholder, context?: any): any{};
}

View File

@ -0,0 +1,70 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {MissingTranslationStrategy, ɵConsole as Console} from '@angular/core';
import {HtmlParser} from '../ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseTreeResult} from '../ml_parser/parser';
import {mergeTranslations} from './extractor_merger';
import {Serializer} from './serializers/serializer';
import {Xliff} from './serializers/xliff';
import {Xmb} from './serializers/xmb';
import {Xtb} from './serializers/xtb';
import {TranslationBundle} from './translation_bundle';
export class I18NHtmlParser implements HtmlParser {
// @override
getTagDefinition: any;
private _translationBundle: TranslationBundle;
constructor(
private _htmlParser: HtmlParser, translations?: string, translationsFormat?: string,
missingTranslation: MissingTranslationStrategy = MissingTranslationStrategy.Warning,
console?: Console) {
if (translations) {
const serializer = createSerializer(translationsFormat);
this._translationBundle =
TranslationBundle.load(translations, 'i18n', serializer, missingTranslation, console);
}
}
parse(
source: string, url: string, parseExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ParseTreeResult {
const parseResult =
this._htmlParser.parse(source, url, parseExpansionForms, interpolationConfig);
if (!this._translationBundle) {
// Do not enable i18n when no translation bundle is provided
return parseResult;
}
if (parseResult.errors.length) {
return new ParseTreeResult(parseResult.rootNodes, parseResult.errors);
}
return mergeTranslations(
parseResult.rootNodes, this._translationBundle, interpolationConfig, [], {});
}
}
function createSerializer(format?: string): Serializer {
format = (format || 'xlf').toLowerCase();
switch (format) {
case 'xmb':
return new Xmb();
case 'xtb':
return new Xtb();
case 'xliff':
case 'xlf':
default:
return new Xliff();
}
}

View File

@ -0,0 +1,168 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Lexer as ExpressionLexer} from '../expression_parser/lexer';
import {Parser as ExpressionParser} from '../expression_parser/parser';
import * as html from '../ml_parser/ast';
import {getHtmlTagDefinition} from '../ml_parser/html_tags';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseSourceSpan} from '../parse_util';
import * as i18n from './i18n_ast';
import {PlaceholderRegistry} from './serializers/placeholder';
const _expParser = new ExpressionParser(new ExpressionLexer());
/**
* Returns a function converting html nodes to an i18n Message given an interpolationConfig
*/
export function createI18nMessageFactory(interpolationConfig: InterpolationConfig): (
nodes: html.Node[], meaning: string, description: string, id: string) => i18n.Message {
const visitor = new _I18nVisitor(_expParser, interpolationConfig);
return (nodes: html.Node[], meaning: string, description: string, id: string) =>
visitor.toI18nMessage(nodes, meaning, description, id);
}
class _I18nVisitor implements html.Visitor {
private _isIcu: boolean;
private _icuDepth: number;
private _placeholderRegistry: PlaceholderRegistry;
private _placeholderToContent: {[phName: string]: string};
private _placeholderToMessage: {[phName: string]: i18n.Message};
constructor(
private _expressionParser: ExpressionParser,
private _interpolationConfig: InterpolationConfig) {}
public toI18nMessage(nodes: html.Node[], meaning: string, description: string, id: string):
i18n.Message {
this._isIcu = nodes.length == 1 && nodes[0] instanceof html.Expansion;
this._icuDepth = 0;
this._placeholderRegistry = new PlaceholderRegistry();
this._placeholderToContent = {};
this._placeholderToMessage = {};
const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {});
return new i18n.Message(
i18nodes, this._placeholderToContent, this._placeholderToMessage, meaning, description, id);
}
visitElement(el: html.Element, context: any): i18n.Node {
const children = html.visitAll(this, el.children);
const attrs: {[k: string]: string} = {};
el.attrs.forEach(attr => {
// Do not visit the attributes, translatable ones are top-level ASTs
attrs[attr.name] = attr.value;
});
const isVoid: boolean = getHtmlTagDefinition(el.name).isVoid;
const startPhName =
this._placeholderRegistry.getStartTagPlaceholderName(el.name, attrs, isVoid);
this._placeholderToContent[startPhName] = el.sourceSpan.toString();
let closePhName = '';
if (!isVoid) {
closePhName = this._placeholderRegistry.getCloseTagPlaceholderName(el.name);
this._placeholderToContent[closePhName] = `</${el.name}>`;
}
return new i18n.TagPlaceholder(
el.name, attrs, startPhName, closePhName, children, isVoid, el.sourceSpan);
}
visitAttribute(attribute: html.Attribute, context: any): i18n.Node {
return this._visitTextWithInterpolation(attribute.value, attribute.sourceSpan);
}
visitText(text: html.Text, context: any): i18n.Node {
return this._visitTextWithInterpolation(text.value, text.sourceSpan);
}
visitComment(comment: html.Comment, context: any): i18n.Node { return null; }
visitExpansion(icu: html.Expansion, context: any): i18n.Node {
this._icuDepth++;
const i18nIcuCases: {[k: string]: i18n.Node} = {};
const i18nIcu = new i18n.Icu(icu.switchValue, icu.type, i18nIcuCases, icu.sourceSpan);
icu.cases.forEach((caze): void => {
i18nIcuCases[caze.value] = new i18n.Container(
caze.expression.map((node) => node.visit(this, {})), caze.expSourceSpan);
});
this._icuDepth--;
if (this._isIcu || this._icuDepth > 0) {
// Returns an ICU node when:
// - the message (vs a part of the message) is an ICU message, or
// - the ICU message is nested.
const expPh = this._placeholderRegistry.getUniquePlaceholder(`VAR_${icu.type}`);
i18nIcu.expressionPlaceholder = expPh;
this._placeholderToContent[expPh] = icu.switchValue;
return i18nIcu;
}
// Else returns a placeholder
// ICU placeholders should not be replaced with their original content but with the their
// translations. We need to create a new visitor (they are not re-entrant) to compute the
// message id.
// TODO(vicb): add a html.Node -> i18n.Message cache to avoid having to re-create the msg
const phName = this._placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
const visitor = new _I18nVisitor(this._expressionParser, this._interpolationConfig);
this._placeholderToMessage[phName] = visitor.toI18nMessage([icu], '', '', '');
return new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
}
visitExpansionCase(icuCase: html.ExpansionCase, context: any): i18n.Node {
throw new Error('Unreachable code');
}
private _visitTextWithInterpolation(text: string, sourceSpan: ParseSourceSpan): i18n.Node {
const splitInterpolation = this._expressionParser.splitInterpolation(
text, sourceSpan.start.toString(), this._interpolationConfig);
if (!splitInterpolation) {
// No expression, return a single text
return new i18n.Text(text, sourceSpan);
}
// Return a group of text + expressions
const nodes: i18n.Node[] = [];
const container = new i18n.Container(nodes, sourceSpan);
const {start: sDelimiter, end: eDelimiter} = this._interpolationConfig;
for (let i = 0; i < splitInterpolation.strings.length - 1; i++) {
const expression = splitInterpolation.expressions[i];
const baseName = _extractPlaceholderName(expression) || 'INTERPOLATION';
const phName = this._placeholderRegistry.getPlaceholderName(baseName, expression);
if (splitInterpolation.strings[i].length) {
// No need to add empty strings
nodes.push(new i18n.Text(splitInterpolation.strings[i], sourceSpan));
}
nodes.push(new i18n.Placeholder(expression, phName, sourceSpan));
this._placeholderToContent[phName] = sDelimiter + expression + eDelimiter;
}
// The last index contains no expression
const lastStringIdx = splitInterpolation.strings.length - 1;
if (splitInterpolation.strings[lastStringIdx].length) {
nodes.push(new i18n.Text(splitInterpolation.strings[lastStringIdx], sourceSpan));
}
return container;
}
}
const _CUSTOM_PH_EXP = /\/\/[\s\S]*i18n[\s\S]*\([\s\S]*ph[\s\S]*=[\s\S]*"([\s\S]*?)"[\s\S]*\)/g;
function _extractPlaceholderName(input: string): string {
return input.split(_CUSTOM_PH_EXP)[1];
}

View File

@ -0,0 +1,15 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export {Extractor, ExtractorHost} from './extractor';
export {I18NHtmlParser} from './i18n_html_parser';
export {MessageBundle} from './message_bundle';
export {Serializer} from './serializers/serializer';
export {Xliff} from './serializers/xliff';
export {Xmb} from './serializers/xmb';
export {Xtb} from './serializers/xtb';

View File

@ -0,0 +1,95 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {HtmlParser} from '../ml_parser/html_parser';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseError} from '../parse_util';
import {extractMessages} from './extractor_merger';
import * as i18n from './i18n_ast';
import {PlaceholderMapper, Serializer} from './serializers/serializer';
/**
* A container for message extracted from the templates.
*/
export class MessageBundle {
private _messages: i18n.Message[] = [];
constructor(
private _htmlParser: HtmlParser, private _implicitTags: string[],
private _implicitAttrs: {[k: string]: string[]}, private _locale: string|null = null) {}
updateFromTemplate(html: string, url: string, interpolationConfig: InterpolationConfig):
ParseError[] {
const htmlParserResult = this._htmlParser.parse(html, url, true, interpolationConfig);
if (htmlParserResult.errors.length) {
return htmlParserResult.errors;
}
const i18nParserResult = extractMessages(
htmlParserResult.rootNodes, interpolationConfig, this._implicitTags, this._implicitAttrs);
if (i18nParserResult.errors.length) {
return i18nParserResult.errors;
}
this._messages.push(...i18nParserResult.messages);
}
// Return the message in the internal format
// The public (serialized) format might be different, see the `write` method.
getMessages(): i18n.Message[] { return this._messages; }
write(serializer: Serializer): string {
const messages: {[id: string]: i18n.Message} = {};
const mapperVisitor = new MapPlaceholderNames();
// Deduplicate messages based on their ID
this._messages.forEach(message => {
const id = serializer.digest(message);
if (!messages.hasOwnProperty(id)) {
messages[id] = message;
}
});
// Transform placeholder names using the serializer mapping
const msgList = Object.keys(messages).map(id => {
const mapper = serializer.createNameMapper(messages[id]);
const src = messages[id];
const nodes = mapper ? mapperVisitor.convert(src.nodes, mapper) : src.nodes;
return new i18n.Message(nodes, {}, {}, src.meaning, src.description, id);
});
return serializer.write(msgList, this._locale);
}
}
// Transform an i18n AST by renaming the placeholder nodes with the given mapper
class MapPlaceholderNames extends i18n.CloneVisitor {
convert(nodes: i18n.Node[], mapper: PlaceholderMapper): i18n.Node[] {
return mapper ? nodes.map(n => n.visit(this, mapper)) : nodes;
}
visitTagPlaceholder(ph: i18n.TagPlaceholder, mapper: PlaceholderMapper): i18n.TagPlaceholder {
const startName = mapper.toPublicName(ph.startName);
const closeName = ph.closeName ? mapper.toPublicName(ph.closeName) : ph.closeName;
const children = ph.children.map(n => n.visit(this, mapper));
return new i18n.TagPlaceholder(
ph.tag, ph.attrs, startName, closeName, children, ph.isVoid, ph.sourceSpan);
}
visitPlaceholder(ph: i18n.Placeholder, mapper: PlaceholderMapper): i18n.Placeholder {
return new i18n.Placeholder(ph.value, mapper.toPublicName(ph.name), ph.sourceSpan);
}
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, mapper: PlaceholderMapper): i18n.IcuPlaceholder {
return new i18n.IcuPlaceholder(ph.value, mapper.toPublicName(ph.name), ph.sourceSpan);
}
}

View File

@ -0,0 +1,16 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseError, ParseSourceSpan} from '../parse_util';
/**
* An i18n error.
*/
export class I18nError extends ParseError {
constructor(span: ParseSourceSpan, msg: string) { super(span, msg); }
}

View File

@ -0,0 +1,124 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const TAG_TO_PLACEHOLDER_NAMES: {[k: string]: string} = {
'A': 'LINK',
'B': 'BOLD_TEXT',
'BR': 'LINE_BREAK',
'EM': 'EMPHASISED_TEXT',
'H1': 'HEADING_LEVEL1',
'H2': 'HEADING_LEVEL2',
'H3': 'HEADING_LEVEL3',
'H4': 'HEADING_LEVEL4',
'H5': 'HEADING_LEVEL5',
'H6': 'HEADING_LEVEL6',
'HR': 'HORIZONTAL_RULE',
'I': 'ITALIC_TEXT',
'LI': 'LIST_ITEM',
'LINK': 'MEDIA_LINK',
'OL': 'ORDERED_LIST',
'P': 'PARAGRAPH',
'Q': 'QUOTATION',
'S': 'STRIKETHROUGH_TEXT',
'SMALL': 'SMALL_TEXT',
'SUB': 'SUBSTRIPT',
'SUP': 'SUPERSCRIPT',
'TBODY': 'TABLE_BODY',
'TD': 'TABLE_CELL',
'TFOOT': 'TABLE_FOOTER',
'TH': 'TABLE_HEADER_CELL',
'THEAD': 'TABLE_HEADER',
'TR': 'TABLE_ROW',
'TT': 'MONOSPACED_TEXT',
'U': 'UNDERLINED_TEXT',
'UL': 'UNORDERED_LIST',
};
/**
* Creates unique names for placeholder with different content.
*
* Returns the same placeholder name when the content is identical.
*
* @internal
*/
export class PlaceholderRegistry {
// Count the occurrence of the base name top generate a unique name
private _placeHolderNameCounts: {[k: string]: number} = {};
// Maps signature to placeholder names
private _signatureToName: {[k: string]: string} = {};
getStartTagPlaceholderName(tag: string, attrs: {[k: string]: string}, isVoid: boolean): string {
const signature = this._hashTag(tag, attrs, isVoid);
if (this._signatureToName[signature]) {
return this._signatureToName[signature];
}
const upperTag = tag.toUpperCase();
const baseName = TAG_TO_PLACEHOLDER_NAMES[upperTag] || `TAG_${upperTag}`;
const name = this._generateUniqueName(isVoid ? baseName : `START_${baseName}`);
this._signatureToName[signature] = name;
return name;
}
getCloseTagPlaceholderName(tag: string): string {
const signature = this._hashClosingTag(tag);
if (this._signatureToName[signature]) {
return this._signatureToName[signature];
}
const upperTag = tag.toUpperCase();
const baseName = TAG_TO_PLACEHOLDER_NAMES[upperTag] || `TAG_${upperTag}`;
const name = this._generateUniqueName(`CLOSE_${baseName}`);
this._signatureToName[signature] = name;
return name;
}
getPlaceholderName(name: string, content: string): string {
const upperName = name.toUpperCase();
const signature = `PH: ${upperName}=${content}`;
if (this._signatureToName[signature]) {
return this._signatureToName[signature];
}
const uniqueName = this._generateUniqueName(upperName);
this._signatureToName[signature] = uniqueName;
return uniqueName;
}
getUniquePlaceholder(name: string): string {
return this._generateUniqueName(name.toUpperCase());
}
// Generate a hash for a tag - does not take attribute order into account
private _hashTag(tag: string, attrs: {[k: string]: string}, isVoid: boolean): string {
const start = `<${tag}`;
const strAttrs = Object.keys(attrs).sort().map((name) => ` ${name}=${attrs[name]}`).join('');
const end = isVoid ? '/>' : `></${tag}>`;
return start + strAttrs + end;
}
private _hashClosingTag(tag: string): string { return this._hashTag(`/${tag}`, {}, false); }
private _generateUniqueName(base: string): string {
const seen = this._placeHolderNameCounts.hasOwnProperty(base);
if (!seen) {
this._placeHolderNameCounts[base] = 1;
return base;
}
const id = this._placeHolderNameCounts[base];
this._placeHolderNameCounts[base] = id + 1;
return `${base}_${id}`;
}
}

View File

@ -0,0 +1,98 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as i18n from '../i18n_ast';
export abstract class Serializer {
// - The `placeholders` and `placeholderToMessage` properties are irrelevant in the input messages
// - The `id` contains the message id that the serializer is expected to use
// - Placeholder names are already map to public names using the provided mapper
abstract write(messages: i18n.Message[], locale: string|null): string;
abstract load(content: string, url: string):
{locale: string | null, i18nNodesByMsgId: {[msgId: string]: i18n.Node[]}};
abstract digest(message: i18n.Message): string;
// Creates a name mapper, see `PlaceholderMapper`
// Returning `null` means that no name mapping is used.
createNameMapper(message: i18n.Message): PlaceholderMapper { return null; }
}
/**
* A `PlaceholderMapper` converts placeholder names from internal to serialized representation and
* back.
*
* It should be used for serialization format that put constraints on the placeholder names.
*/
export interface PlaceholderMapper {
toPublicName(internalName: string): string;
toInternalName(publicName: string): string;
}
/**
* A simple mapper that take a function to transform an internal name to a public name
*/
export class SimplePlaceholderMapper extends i18n.RecurseVisitor implements PlaceholderMapper {
private internalToPublic: {[k: string]: string} = {};
private publicToNextId: {[k: string]: number} = {};
private publicToInternal: {[k: string]: string} = {};
// create a mapping from the message
constructor(message: i18n.Message, private mapName: (name: string) => string) {
super();
message.nodes.forEach(node => node.visit(this));
}
toPublicName(internalName: string): string {
return this.internalToPublic.hasOwnProperty(internalName) ?
this.internalToPublic[internalName] :
null;
}
toInternalName(publicName: string): string {
return this.publicToInternal.hasOwnProperty(publicName) ? this.publicToInternal[publicName] :
null;
}
visitText(text: i18n.Text, context?: any): any { return null; }
visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): any {
this.visitPlaceholderName(ph.startName);
super.visitTagPlaceholder(ph, context);
this.visitPlaceholderName(ph.closeName);
}
visitPlaceholder(ph: i18n.Placeholder, context?: any): any { this.visitPlaceholderName(ph.name); }
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
this.visitPlaceholderName(ph.name);
}
// XMB placeholders could only contains A-Z, 0-9 and _
private visitPlaceholderName(internalName: string): void {
if (!internalName || this.internalToPublic.hasOwnProperty(internalName)) {
return;
}
let publicName = this.mapName(internalName);
if (this.publicToInternal.hasOwnProperty(publicName)) {
// Create a new XMB when it has already been used
const nextId = this.publicToNextId[publicName];
this.publicToNextId[publicName] = nextId + 1;
publicName = `${publicName}_${nextId}`;
} else {
this.publicToNextId[publicName] = 1;
}
this.internalToPublic[internalName] = publicName;
this.publicToInternal[publicName] = internalName;
}
}

View File

@ -0,0 +1,290 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ml from '../../ml_parser/ast';
import {XmlParser} from '../../ml_parser/xml_parser';
import {digest} from '../digest';
import * as i18n from '../i18n_ast';
import {I18nError} from '../parse_util';
import {Serializer} from './serializer';
import * as xml from './xml_helper';
const _VERSION = '1.2';
const _XMLNS = 'urn:oasis:names:tc:xliff:document:1.2';
// TODO(vicb): make this a param (s/_/-/)
const _DEFAULT_SOURCE_LANG = 'en';
const _PLACEHOLDER_TAG = 'x';
const _FILE_TAG = 'file';
const _SOURCE_TAG = 'source';
const _TARGET_TAG = 'target';
const _UNIT_TAG = 'trans-unit';
// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html
// http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
export class Xliff extends Serializer {
write(messages: i18n.Message[], locale: string|null): string {
const visitor = new _WriteVisitor();
const transUnits: xml.Node[] = [];
messages.forEach(message => {
const transUnit = new xml.Tag(_UNIT_TAG, {id: message.id, datatype: 'html'});
transUnit.children.push(
new xml.CR(8), new xml.Tag(_SOURCE_TAG, {}, visitor.serialize(message.nodes)),
new xml.CR(8), new xml.Tag(_TARGET_TAG));
if (message.description) {
transUnit.children.push(
new xml.CR(8),
new xml.Tag(
'note', {priority: '1', from: 'description'}, [new xml.Text(message.description)]));
}
if (message.meaning) {
transUnit.children.push(
new xml.CR(8),
new xml.Tag('note', {priority: '1', from: 'meaning'}, [new xml.Text(message.meaning)]));
}
transUnit.children.push(new xml.CR(6));
transUnits.push(new xml.CR(6), transUnit);
});
const body = new xml.Tag('body', {}, [...transUnits, new xml.CR(4)]);
const file = new xml.Tag(
'file', {
'source-language': locale || _DEFAULT_SOURCE_LANG,
datatype: 'plaintext',
original: 'ng2.template',
},
[new xml.CR(4), body, new xml.CR(2)]);
const xliff = new xml.Tag(
'xliff', {version: _VERSION, xmlns: _XMLNS}, [new xml.CR(2), file, new xml.CR()]);
return xml.serialize([
new xml.Declaration({version: '1.0', encoding: 'UTF-8'}), new xml.CR(), xliff, new xml.CR()
]);
}
load(content: string, url: string):
{locale: string, i18nNodesByMsgId: {[msgId: string]: i18n.Node[]}} {
// xliff to xml nodes
const xliffParser = new XliffParser();
const {locale, mlNodesByMsgId, errors} = xliffParser.parse(content, url);
// xml nodes to i18n nodes
const i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {};
const converter = new XmlToI18n();
Object.keys(mlNodesByMsgId).forEach(msgId => {
const {i18nNodes, errors: e} = converter.convert(mlNodesByMsgId[msgId]);
errors.push(...e);
i18nNodesByMsgId[msgId] = i18nNodes;
});
if (errors.length) {
throw new Error(`xliff parse errors:\n${errors.join('\n')}`);
}
return {locale, i18nNodesByMsgId};
}
digest(message: i18n.Message): string { return digest(message); }
}
class _WriteVisitor implements i18n.Visitor {
private _isInIcu: boolean;
visitText(text: i18n.Text, context?: any): xml.Node[] { return [new xml.Text(text.value)]; }
visitContainer(container: i18n.Container, context?: any): xml.Node[] {
const nodes: xml.Node[] = [];
container.children.forEach((node: i18n.Node) => nodes.push(...node.visit(this)));
return nodes;
}
visitIcu(icu: i18n.Icu, context?: any): xml.Node[] {
if (this._isInIcu) {
// nested ICU is not supported
throw new Error('xliff does not support nested ICU messages');
}
this._isInIcu = true;
// TODO(vicb): support ICU messages
// https://lists.oasis-open.org/archives/xliff/201201/msg00028.html
// http://docs.oasis-open.org/xliff/v1.2/xliff-profile-po/xliff-profile-po-1.2-cd02.html
const nodes: xml.Node[] = [];
this._isInIcu = false;
return nodes;
}
visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): xml.Node[] {
const ctype = getCtypeForTag(ph.tag);
const startTagPh = new xml.Tag(_PLACEHOLDER_TAG, {id: ph.startName, ctype});
if (ph.isVoid) {
// void tags have no children nor closing tags
return [startTagPh];
}
const closeTagPh = new xml.Tag(_PLACEHOLDER_TAG, {id: ph.closeName, ctype});
return [startTagPh, ...this.serialize(ph.children), closeTagPh];
}
visitPlaceholder(ph: i18n.Placeholder, context?: any): xml.Node[] {
return [new xml.Tag(_PLACEHOLDER_TAG, {id: ph.name})];
}
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): xml.Node[] {
return [new xml.Tag(_PLACEHOLDER_TAG, {id: ph.name})];
}
serialize(nodes: i18n.Node[]): xml.Node[] {
this._isInIcu = false;
return [].concat(...nodes.map(node => node.visit(this)));
}
}
// TODO(vicb): add error management (structure)
// Extract messages as xml nodes from the xliff file
class XliffParser implements ml.Visitor {
private _unitMlNodes: ml.Node[];
private _errors: I18nError[];
private _mlNodesByMsgId: {[msgId: string]: ml.Node[]};
private _locale: string|null = null;
parse(xliff: string, url: string) {
this._unitMlNodes = [];
this._mlNodesByMsgId = {};
const xml = new XmlParser().parse(xliff, url, false);
this._errors = xml.errors;
ml.visitAll(this, xml.rootNodes, null);
return {
mlNodesByMsgId: this._mlNodesByMsgId,
errors: this._errors,
locale: this._locale,
};
}
visitElement(element: ml.Element, context: any): any {
switch (element.name) {
case _UNIT_TAG:
this._unitMlNodes = null;
const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!idAttr) {
this._addError(element, `<${_UNIT_TAG}> misses the "id" attribute`);
} else {
const id = idAttr.value;
if (this._mlNodesByMsgId.hasOwnProperty(id)) {
this._addError(element, `Duplicated translations for msg ${id}`);
} else {
ml.visitAll(this, element.children, null);
if (this._unitMlNodes) {
this._mlNodesByMsgId[id] = this._unitMlNodes;
} else {
this._addError(element, `Message ${id} misses a translation`);
}
}
}
break;
case _SOURCE_TAG:
// ignore source message
break;
case _TARGET_TAG:
this._unitMlNodes = element.children;
break;
case _FILE_TAG:
const localeAttr = element.attrs.find((attr) => attr.name === 'target-language');
if (localeAttr) {
this._locale = localeAttr.value;
}
ml.visitAll(this, element.children, null);
break;
default:
// TODO(vicb): assert file structure, xliff version
// For now only recurse on unhandled nodes
ml.visitAll(this, element.children, null);
}
}
visitAttribute(attribute: ml.Attribute, context: any): any {}
visitText(text: ml.Text, context: any): any {}
visitComment(comment: ml.Comment, context: any): any {}
visitExpansion(expansion: ml.Expansion, context: any): any {}
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
}
}
// Convert ml nodes (xliff syntax) to i18n nodes
class XmlToI18n implements ml.Visitor {
private _errors: I18nError[];
convert(nodes: ml.Node[]) {
this._errors = [];
return {
i18nNodes: ml.visitAll(this, nodes),
errors: this._errors,
};
}
visitText(text: ml.Text, context: any) { return new i18n.Text(text.value, text.sourceSpan); }
visitElement(el: ml.Element, context: any): i18n.Placeholder {
if (el.name === _PLACEHOLDER_TAG) {
const nameAttr = el.attrs.find((attr) => attr.name === 'id');
if (nameAttr) {
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan);
}
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "id" attribute`);
} else {
this._addError(el, `Unexpected tag`);
}
}
visitExpansion(icu: ml.Expansion, context: any) {}
visitExpansionCase(icuCase: ml.ExpansionCase, context: any): any {}
visitComment(comment: ml.Comment, context: any) {}
visitAttribute(attribute: ml.Attribute, context: any) {}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
}
}
function getCtypeForTag(tag: string): string {
switch (tag.toLowerCase()) {
case 'br':
return 'lb';
case 'img':
return 'image';
default:
return `x-${tag}`;
}
}

View File

@ -0,0 +1,164 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {decimalDigest} from '../digest';
import * as i18n from '../i18n_ast';
import {PlaceholderMapper, Serializer, SimplePlaceholderMapper} from './serializer';
import * as xml from './xml_helper';
const _MESSAGES_TAG = 'messagebundle';
const _MESSAGE_TAG = 'msg';
const _PLACEHOLDER_TAG = 'ph';
const _EXEMPLE_TAG = 'ex';
const _DOCTYPE = `<!ELEMENT messagebundle (msg)*>
<!ATTLIST messagebundle class CDATA #IMPLIED>
<!ELEMENT msg (#PCDATA|ph|source)*>
<!ATTLIST msg id CDATA #IMPLIED>
<!ATTLIST msg seq CDATA #IMPLIED>
<!ATTLIST msg name CDATA #IMPLIED>
<!ATTLIST msg desc CDATA #IMPLIED>
<!ATTLIST msg meaning CDATA #IMPLIED>
<!ATTLIST msg obsolete (obsolete) #IMPLIED>
<!ATTLIST msg xml:space (default|preserve) "default">
<!ATTLIST msg is_hidden CDATA #IMPLIED>
<!ELEMENT source (#PCDATA)>
<!ELEMENT ph (#PCDATA|ex)*>
<!ATTLIST ph name CDATA #REQUIRED>
<!ELEMENT ex (#PCDATA)>`;
export class Xmb extends Serializer {
write(messages: i18n.Message[], locale: string|null): string {
const exampleVisitor = new ExampleVisitor();
const visitor = new _Visitor();
let rootNode = new xml.Tag(_MESSAGES_TAG);
messages.forEach(message => {
const attrs: {[k: string]: string} = {id: message.id};
if (message.description) {
attrs['desc'] = message.description;
}
if (message.meaning) {
attrs['meaning'] = message.meaning;
}
rootNode.children.push(
new xml.CR(2), new xml.Tag(_MESSAGE_TAG, attrs, visitor.serialize(message.nodes)));
});
rootNode.children.push(new xml.CR());
return xml.serialize([
new xml.Declaration({version: '1.0', encoding: 'UTF-8'}),
new xml.CR(),
new xml.Doctype(_MESSAGES_TAG, _DOCTYPE),
new xml.CR(),
exampleVisitor.addDefaultExamples(rootNode),
new xml.CR(),
]);
}
load(content: string, url: string):
{locale: string, i18nNodesByMsgId: {[msgId: string]: i18n.Node[]}} {
throw new Error('Unsupported');
}
digest(message: i18n.Message): string { return digest(message); }
createNameMapper(message: i18n.Message): PlaceholderMapper {
return new SimplePlaceholderMapper(message, toPublicName);
}
}
class _Visitor implements i18n.Visitor {
visitText(text: i18n.Text, context?: any): xml.Node[] { return [new xml.Text(text.value)]; }
visitContainer(container: i18n.Container, context: any): xml.Node[] {
const nodes: xml.Node[] = [];
container.children.forEach((node: i18n.Node) => nodes.push(...node.visit(this)));
return nodes;
}
visitIcu(icu: i18n.Icu, context?: any): xml.Node[] {
const nodes = [new xml.Text(`{${icu.expressionPlaceholder}, ${icu.type}, `)];
Object.keys(icu.cases).forEach((c: string) => {
nodes.push(new xml.Text(`${c} {`), ...icu.cases[c].visit(this), new xml.Text(`} `));
});
nodes.push(new xml.Text(`}`));
return nodes;
}
visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): xml.Node[] {
const startEx = new xml.Tag(_EXEMPLE_TAG, {}, [new xml.Text(`<${ph.tag}>`)]);
const startTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name: ph.startName}, [startEx]);
if (ph.isVoid) {
// void tags have no children nor closing tags
return [startTagPh];
}
const closeEx = new xml.Tag(_EXEMPLE_TAG, {}, [new xml.Text(`</${ph.tag}>`)]);
const closeTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name: ph.closeName}, [closeEx]);
return [startTagPh, ...this.serialize(ph.children), closeTagPh];
}
visitPlaceholder(ph: i18n.Placeholder, context?: any): xml.Node[] {
return [new xml.Tag(_PLACEHOLDER_TAG, {name: ph.name})];
}
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): xml.Node[] {
return [new xml.Tag(_PLACEHOLDER_TAG, {name: ph.name})];
}
serialize(nodes: i18n.Node[]): xml.Node[] {
return [].concat(...nodes.map(node => node.visit(this)));
}
}
export function digest(message: i18n.Message): string {
return decimalDigest(message);
}
// TC requires at least one non-empty example on placeholders
class ExampleVisitor implements xml.IVisitor {
addDefaultExamples(node: xml.Node): xml.Node {
node.visit(this);
return node;
}
visitTag(tag: xml.Tag): void {
if (tag.name === _PLACEHOLDER_TAG) {
if (!tag.children || tag.children.length == 0) {
const exText = new xml.Text(tag.attrs['name'] || '...');
tag.children = [new xml.Tag(_EXEMPLE_TAG, {}, [exText])];
}
} else if (tag.children) {
tag.children.forEach(node => node.visit(this));
}
}
visitText(text: xml.Text): void {}
visitDeclaration(decl: xml.Declaration): void {}
visitDoctype(doctype: xml.Doctype): void {}
}
// XMB/XTB placeholders can only contain A-Z, 0-9 and _
export function toPublicName(internalName: string): string {
return internalName.toUpperCase().replace(/[^A-Z0-9_]/g, '_');
}

View File

@ -0,0 +1,106 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export interface IVisitor {
visitTag(tag: Tag): any;
visitText(text: Text): any;
visitDeclaration(decl: Declaration): any;
visitDoctype(doctype: Doctype): any;
}
class _Visitor implements IVisitor {
visitTag(tag: Tag): string {
const strAttrs = this._serializeAttributes(tag.attrs);
if (tag.children.length == 0) {
return `<${tag.name}${strAttrs}/>`;
}
const strChildren = tag.children.map(node => node.visit(this));
return `<${tag.name}${strAttrs}>${strChildren.join('')}</${tag.name}>`;
}
visitText(text: Text): string { return text.value; }
visitDeclaration(decl: Declaration): string {
return `<?xml${this._serializeAttributes(decl.attrs)} ?>`;
}
private _serializeAttributes(attrs: {[k: string]: string}) {
const strAttrs = Object.keys(attrs).map((name: string) => `${name}="${attrs[name]}"`).join(' ');
return strAttrs.length > 0 ? ' ' + strAttrs : '';
}
visitDoctype(doctype: Doctype): any {
return `<!DOCTYPE ${doctype.rootTag} [\n${doctype.dtd}\n]>`;
}
}
const _visitor = new _Visitor();
export function serialize(nodes: Node[]): string {
return nodes.map((node: Node): string => node.visit(_visitor)).join('');
}
export interface Node { visit(visitor: IVisitor): any; }
export class Declaration implements Node {
public attrs: {[k: string]: string} = {};
constructor(unescapedAttrs: {[k: string]: string}) {
Object.keys(unescapedAttrs).forEach((k: string) => {
this.attrs[k] = _escapeXml(unescapedAttrs[k]);
});
}
visit(visitor: IVisitor): any { return visitor.visitDeclaration(this); }
}
export class Doctype implements Node {
constructor(public rootTag: string, public dtd: string){};
visit(visitor: IVisitor): any { return visitor.visitDoctype(this); }
}
export class Tag implements Node {
public attrs: {[k: string]: string} = {};
constructor(
public name: string, unescapedAttrs: {[k: string]: string} = {},
public children: Node[] = []) {
Object.keys(unescapedAttrs).forEach((k: string) => {
this.attrs[k] = _escapeXml(unescapedAttrs[k]);
});
}
visit(visitor: IVisitor): any { return visitor.visitTag(this); }
}
export class Text implements Node {
value: string;
constructor(unescapedValue: string) { this.value = _escapeXml(unescapedValue); };
visit(visitor: IVisitor): any { return visitor.visitText(this); }
}
export class CR extends Text {
constructor(ws: number = 0) { super(`\n${new Array(ws + 1).join(' ')}`); }
}
const _ESCAPED_CHARS: [RegExp, string][] = [
[/&/g, '&amp;'],
[/"/g, '&quot;'],
[/'/g, '&apos;'],
[/</g, '&lt;'],
[/>/g, '&gt;'],
];
function _escapeXml(text: string): string {
return _ESCAPED_CHARS.reduce(
(text: string, entry: [RegExp, string]) => text.replace(entry[0], entry[1]), text);
}

View File

@ -0,0 +1,210 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ml from '../../ml_parser/ast';
import {XmlParser} from '../../ml_parser/xml_parser';
import * as i18n from '../i18n_ast';
import {I18nError} from '../parse_util';
import {PlaceholderMapper, Serializer, SimplePlaceholderMapper} from './serializer';
import {digest, toPublicName} from './xmb';
const _TRANSLATIONS_TAG = 'translationbundle';
const _TRANSLATION_TAG = 'translation';
const _PLACEHOLDER_TAG = 'ph';
export class Xtb extends Serializer {
write(messages: i18n.Message[], locale: string|null): string { throw new Error('Unsupported'); }
load(content: string, url: string):
{locale: string, i18nNodesByMsgId: {[msgId: string]: i18n.Node[]}} {
// xtb to xml nodes
const xtbParser = new XtbParser();
const {locale, msgIdToHtml, errors} = xtbParser.parse(content, url);
// xml nodes to i18n nodes
const i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {};
const converter = new XmlToI18n();
// Because we should be able to load xtb files that rely on features not supported by angular,
// we need to delay the conversion of html to i18n nodes so that non angular messages are not
// converted
Object.keys(msgIdToHtml).forEach(msgId => {
const valueFn = function() {
const {i18nNodes, errors} = converter.convert(msgIdToHtml[msgId], url);
if (errors.length) {
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
}
return i18nNodes;
};
createLazyProperty(i18nNodesByMsgId, msgId, valueFn);
});
if (errors.length) {
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
}
return {locale, i18nNodesByMsgId};
}
digest(message: i18n.Message): string { return digest(message); }
createNameMapper(message: i18n.Message): PlaceholderMapper {
return new SimplePlaceholderMapper(message, toPublicName);
}
}
function createLazyProperty(messages: any, id: string, valueFn: () => any) {
Object.defineProperty(messages, id, {
configurable: true,
enumerable: true,
get: function() {
const value = valueFn();
Object.defineProperty(messages, id, {enumerable: true, value});
return value;
},
set: _ => { throw new Error('Could not overwrite an XTB translation'); },
});
}
// Extract messages as xml nodes from the xtb file
class XtbParser implements ml.Visitor {
private _bundleDepth: number;
private _errors: I18nError[];
private _msgIdToHtml: {[msgId: string]: string};
private _locale: string|null = null;
parse(xtb: string, url: string) {
this._bundleDepth = 0;
this._msgIdToHtml = {};
// We can not parse the ICU messages at this point as some messages might not originate
// from Angular that could not be lex'd.
const xml = new XmlParser().parse(xtb, url, false);
this._errors = xml.errors;
ml.visitAll(this, xml.rootNodes);
return {
msgIdToHtml: this._msgIdToHtml,
errors: this._errors,
locale: this._locale,
};
}
visitElement(element: ml.Element, context: any): any {
switch (element.name) {
case _TRANSLATIONS_TAG:
this._bundleDepth++;
if (this._bundleDepth > 1) {
this._addError(element, `<${_TRANSLATIONS_TAG}> elements can not be nested`);
}
const langAttr = element.attrs.find((attr) => attr.name === 'lang');
if (langAttr) {
this._locale = langAttr.value;
}
ml.visitAll(this, element.children, null);
this._bundleDepth--;
break;
case _TRANSLATION_TAG:
const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!idAttr) {
this._addError(element, `<${_TRANSLATION_TAG}> misses the "id" attribute`);
} else {
const id = idAttr.value;
if (this._msgIdToHtml.hasOwnProperty(id)) {
this._addError(element, `Duplicated translations for msg ${id}`);
} else {
const innerTextStart = element.startSourceSpan.end.offset;
const innerTextEnd = element.endSourceSpan.start.offset;
const content = element.startSourceSpan.start.file.content;
const innerText = content.slice(innerTextStart, innerTextEnd);
this._msgIdToHtml[id] = innerText;
}
}
break;
default:
this._addError(element, 'Unexpected tag');
}
}
visitAttribute(attribute: ml.Attribute, context: any): any {}
visitText(text: ml.Text, context: any): any {}
visitComment(comment: ml.Comment, context: any): any {}
visitExpansion(expansion: ml.Expansion, context: any): any {}
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
}
}
// Convert ml nodes (xtb syntax) to i18n nodes
class XmlToI18n implements ml.Visitor {
private _errors: I18nError[];
convert(message: string, url: string) {
const xmlIcu = new XmlParser().parse(message, url, true);
this._errors = xmlIcu.errors;
const i18nNodes = this._errors.length > 0 || xmlIcu.rootNodes.length == 0 ?
[] :
ml.visitAll(this, xmlIcu.rootNodes);
return {
i18nNodes,
errors: this._errors,
};
}
visitText(text: ml.Text, context: any) { return new i18n.Text(text.value, text.sourceSpan); }
visitExpansion(icu: ml.Expansion, context: any) {
const caseMap: {[value: string]: i18n.Node} = {};
ml.visitAll(this, icu.cases).forEach(c => {
caseMap[c.value] = new i18n.Container(c.nodes, icu.sourceSpan);
});
return new i18n.Icu(icu.switchValue, icu.type, caseMap, icu.sourceSpan);
}
visitExpansionCase(icuCase: ml.ExpansionCase, context: any): any {
return {
value: icuCase.value,
nodes: ml.visitAll(this, icuCase.expression),
};
}
visitElement(el: ml.Element, context: any): i18n.Placeholder {
if (el.name === _PLACEHOLDER_TAG) {
const nameAttr = el.attrs.find((attr) => attr.name === 'name');
if (nameAttr) {
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan);
}
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
} else {
this._addError(el, `Unexpected tag`);
}
}
visitComment(comment: ml.Comment, context: any) {}
visitAttribute(attribute: ml.Attribute, context: any) {}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
}
}

View File

@ -0,0 +1,187 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {MissingTranslationStrategy, ɵConsole as Console} from '@angular/core';
import * as html from '../ml_parser/ast';
import {HtmlParser} from '../ml_parser/html_parser';
import * as i18n from './i18n_ast';
import {I18nError} from './parse_util';
import {PlaceholderMapper, Serializer} from './serializers/serializer';
/**
* A container for translated messages
*/
export class TranslationBundle {
private _i18nToHtml: I18nToHtmlVisitor;
constructor(
private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {}, locale: string|null,
public digest: (m: i18n.Message) => string,
public mapperFactory?: (m: i18n.Message) => PlaceholderMapper,
missingTranslationStrategy: MissingTranslationStrategy = MissingTranslationStrategy.Warning,
console?: Console) {
this._i18nToHtml = new I18nToHtmlVisitor(
_i18nNodesByMsgId, locale, digest, mapperFactory, missingTranslationStrategy, console);
}
// Creates a `TranslationBundle` by parsing the given `content` with the `serializer`.
static load(
content: string, url: string, serializer: Serializer,
missingTranslationStrategy: MissingTranslationStrategy,
console?: Console): TranslationBundle {
const {locale, i18nNodesByMsgId} = serializer.load(content, url);
const digestFn = (m: i18n.Message) => serializer.digest(m);
const mapperFactory = (m: i18n.Message) => serializer.createNameMapper(m);
return new TranslationBundle(
i18nNodesByMsgId, locale, digestFn, mapperFactory, missingTranslationStrategy, console);
}
// Returns the translation as HTML nodes from the given source message.
get(srcMsg: i18n.Message): html.Node[] {
const html = this._i18nToHtml.convert(srcMsg);
if (html.errors.length) {
throw new Error(html.errors.join('\n'));
}
return html.nodes;
}
has(srcMsg: i18n.Message): boolean { return this.digest(srcMsg) in this._i18nNodesByMsgId; }
}
class I18nToHtmlVisitor implements i18n.Visitor {
private _srcMsg: i18n.Message;
private _contextStack: {msg: i18n.Message, mapper: (name: string) => string}[] = [];
private _errors: I18nError[] = [];
private _mapper: (name: string) => string;
constructor(
private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {}, private _locale: string|null,
private _digest: (m: i18n.Message) => string,
private _mapperFactory: (m: i18n.Message) => PlaceholderMapper,
private _missingTranslationStrategy: MissingTranslationStrategy, private _console?: Console) {
}
convert(srcMsg: i18n.Message): {nodes: html.Node[], errors: I18nError[]} {
this._contextStack.length = 0;
this._errors.length = 0;
// i18n to text
const text = this._convertToText(srcMsg);
// text to html
const url = srcMsg.nodes[0].sourceSpan.start.file.url;
const html = new HtmlParser().parse(text, url, true);
return {
nodes: html.rootNodes,
errors: [...this._errors, ...html.errors],
};
}
visitText(text: i18n.Text, context?: any): string { return text.value; }
visitContainer(container: i18n.Container, context?: any): any {
return container.children.map(n => n.visit(this)).join('');
}
visitIcu(icu: i18n.Icu, context?: any): any {
const cases = Object.keys(icu.cases).map(k => `${k} {${icu.cases[k].visit(this)}}`);
// TODO(vicb): Once all format switch to using expression placeholders
// we should throw when the placeholder is not in the source message
const exp = this._srcMsg.placeholders.hasOwnProperty(icu.expression) ?
this._srcMsg.placeholders[icu.expression] :
icu.expression;
return `{${exp}, ${icu.type}, ${cases.join(' ')}}`;
}
visitPlaceholder(ph: i18n.Placeholder, context?: any): string {
const phName = this._mapper(ph.name);
if (this._srcMsg.placeholders.hasOwnProperty(phName)) {
return this._srcMsg.placeholders[phName];
}
if (this._srcMsg.placeholderToMessage.hasOwnProperty(phName)) {
return this._convertToText(this._srcMsg.placeholderToMessage[phName]);
}
this._addError(ph, `Unknown placeholder "${ph.name}"`);
return '';
}
// Loaded message contains only placeholders (vs tag and icu placeholders).
// However when a translation can not be found, we need to serialize the source message
// which can contain tag placeholders
visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): string {
const tag = `${ph.tag}`;
const attrs = Object.keys(ph.attrs).map(name => `${name}="${ph.attrs[name]}"`).join(' ');
if (ph.isVoid) {
return `<${tag} ${attrs}/>`;
}
const children = ph.children.map((c: i18n.Node) => c.visit(this)).join('');
return `<${tag} ${attrs}>${children}</${tag}>`;
}
// Loaded message contains only placeholders (vs tag and icu placeholders).
// However when a translation can not be found, we need to serialize the source message
// which can contain tag placeholders
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): string {
// An ICU placeholder references the source message to be serialized
return this._convertToText(this._srcMsg.placeholderToMessage[ph.name]);
}
/**
* Convert a source message to a translated text string:
* - text nodes are replaced with their translation,
* - placeholders are replaced with their content,
* - ICU nodes are converted to ICU expressions.
*/
private _convertToText(srcMsg: i18n.Message): string {
const id = this._digest(srcMsg);
const mapper = this._mapperFactory ? this._mapperFactory(srcMsg) : null;
let nodes: i18n.Node[];
this._contextStack.push({msg: this._srcMsg, mapper: this._mapper});
this._srcMsg = srcMsg;
if (this._i18nNodesByMsgId.hasOwnProperty(id)) {
// When there is a translation use its nodes as the source
// And create a mapper to convert serialized placeholder names to internal names
nodes = this._i18nNodesByMsgId[id];
this._mapper = (name: string) => mapper ? mapper.toInternalName(name) : name;
} else {
// When no translation has been found
// - report an error / a warning / nothing,
// - use the nodes from the original message
// - placeholders are already internal and need no mapper
if (this._missingTranslationStrategy === MissingTranslationStrategy.Error) {
const ctx = this._locale ? ` for locale "${this._locale}"` : '';
this._addError(srcMsg.nodes[0], `Missing translation for message "${id}"${ctx}`);
} else if (
this._console &&
this._missingTranslationStrategy === MissingTranslationStrategy.Warning) {
const ctx = this._locale ? ` for locale "${this._locale}"` : '';
this._console.warn(`Missing translation for message "${id}"${ctx}`);
}
nodes = srcMsg.nodes;
this._mapper = (name: string) => name;
}
const text = nodes.map(node => node.visit(this)).join('');
const context = this._contextStack.pop();
this._srcMsg = context.msg;
this._mapper = context.mapper;
return text;
}
private _addError(el: i18n.Node, msg: string) {
this._errors.push(new I18nError(el.sourceSpan, msg));
}
}

View File

@ -0,0 +1,144 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ANALYZE_FOR_ENTRY_COMPONENTS, ChangeDetectionStrategy, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, LOCALE_ID, NgModuleFactory, QueryList, RenderComponentType, Renderer, SecurityContext, SimpleChange, TRANSLATIONS_FORMAT, TemplateRef, ViewContainerRef, ViewEncapsulation, ɵChangeDetectorStatus, ɵCodegenComponentFactoryResolver, ɵEMPTY_ARRAY, ɵEMPTY_MAP, ɵNgModuleInjector, ɵValueUnwrapper, ɵand, ɵccf, ɵcrt, ɵdevModeEqual, ɵdid, ɵeld, ɵinlineInterpolate, ɵinterpolate, ɵncd, ɵnov, ɵpad, ɵpid, ɵpod, ɵppd, ɵprd, ɵqud, ɵreflector, ɵregisterModuleFactory, ɵted, ɵunv, ɵvid} from '@angular/core';
import {CompileIdentifierMetadata, CompileTokenMetadata} from './compile_metadata';
const CORE = assetUrl('core');
const VIEW_UTILS_MODULE_URL = assetUrl('core', 'linker/view_utils');
export interface IdentifierSpec {
name: string;
moduleUrl: string;
runtime: any;
}
export class Identifiers {
static ANALYZE_FOR_ENTRY_COMPONENTS: IdentifierSpec = {
name: 'ANALYZE_FOR_ENTRY_COMPONENTS',
moduleUrl: CORE,
runtime: ANALYZE_FOR_ENTRY_COMPONENTS
};
static ElementRef: IdentifierSpec = {name: 'ElementRef', moduleUrl: CORE, runtime: ElementRef};
static ViewContainerRef:
IdentifierSpec = {name: 'ViewContainerRef', moduleUrl: CORE, runtime: ViewContainerRef};
static ChangeDetectorRef:
IdentifierSpec = {name: 'ChangeDetectorRef', moduleUrl: CORE, runtime: ChangeDetectorRef};
static QueryList: IdentifierSpec = {name: 'QueryList', moduleUrl: CORE, runtime: QueryList};
static TemplateRef: IdentifierSpec = {name: 'TemplateRef', moduleUrl: CORE, runtime: TemplateRef};
static CodegenComponentFactoryResolver: IdentifierSpec = {
name: 'ɵCodegenComponentFactoryResolver',
moduleUrl: CORE,
runtime: ɵCodegenComponentFactoryResolver
};
static ComponentFactoryResolver: IdentifierSpec = {
name: 'ComponentFactoryResolver',
moduleUrl: CORE,
runtime: ComponentFactoryResolver
};
static ComponentFactory:
IdentifierSpec = {name: 'ComponentFactory', moduleUrl: CORE, runtime: ComponentFactory};
static ComponentRef:
IdentifierSpec = {name: 'ComponentRef', moduleUrl: CORE, runtime: ComponentRef};
static NgModuleFactory:
IdentifierSpec = {name: 'NgModuleFactory', moduleUrl: CORE, runtime: NgModuleFactory};
static NgModuleInjector: IdentifierSpec = {
name: 'ɵNgModuleInjector',
moduleUrl: CORE,
runtime: ɵNgModuleInjector,
};
static RegisterModuleFactoryFn: IdentifierSpec = {
name: 'ɵregisterModuleFactory',
moduleUrl: CORE,
runtime: ɵregisterModuleFactory,
};
static Injector: IdentifierSpec = {name: 'Injector', moduleUrl: CORE, runtime: Injector};
static ViewEncapsulation:
IdentifierSpec = {name: 'ViewEncapsulation', moduleUrl: CORE, runtime: ViewEncapsulation};
static ChangeDetectionStrategy: IdentifierSpec = {
name: 'ChangeDetectionStrategy',
moduleUrl: CORE,
runtime: ChangeDetectionStrategy
};
static SecurityContext: IdentifierSpec = {
name: 'SecurityContext',
moduleUrl: CORE,
runtime: SecurityContext,
};
static LOCALE_ID: IdentifierSpec = {name: 'LOCALE_ID', moduleUrl: CORE, runtime: LOCALE_ID};
static TRANSLATIONS_FORMAT:
IdentifierSpec = {name: 'TRANSLATIONS_FORMAT', moduleUrl: CORE, runtime: TRANSLATIONS_FORMAT};
static inlineInterpolate:
IdentifierSpec = {name: 'ɵinlineInterpolate', moduleUrl: CORE, runtime: ɵinlineInterpolate};
static interpolate:
IdentifierSpec = {name: 'ɵinterpolate', moduleUrl: CORE, runtime: ɵinterpolate};
static EMPTY_ARRAY:
IdentifierSpec = {name: 'ɵEMPTY_ARRAY', moduleUrl: CORE, runtime: ɵEMPTY_ARRAY};
static EMPTY_MAP: IdentifierSpec = {name: 'ɵEMPTY_MAP', moduleUrl: CORE, runtime: ɵEMPTY_MAP};
static Renderer: IdentifierSpec = {name: 'Renderer', moduleUrl: CORE, runtime: Renderer};
static viewDef: IdentifierSpec = {name: 'ɵvid', moduleUrl: CORE, runtime: ɵvid};
static elementDef: IdentifierSpec = {name: 'ɵeld', moduleUrl: CORE, runtime: ɵeld};
static anchorDef: IdentifierSpec = {name: 'ɵand', moduleUrl: CORE, runtime: ɵand};
static textDef: IdentifierSpec = {name: 'ɵted', moduleUrl: CORE, runtime: ɵted};
static directiveDef: IdentifierSpec = {name: 'ɵdid', moduleUrl: CORE, runtime: ɵdid};
static providerDef: IdentifierSpec = {name: 'ɵprd', moduleUrl: CORE, runtime: ɵprd};
static queryDef: IdentifierSpec = {name: 'ɵqud', moduleUrl: CORE, runtime: ɵqud};
static pureArrayDef: IdentifierSpec = {name: 'ɵpad', moduleUrl: CORE, runtime: ɵpad};
static pureObjectDef: IdentifierSpec = {name: 'ɵpod', moduleUrl: CORE, runtime: ɵpod};
static purePipeDef: IdentifierSpec = {name: 'ɵppd', moduleUrl: CORE, runtime: ɵppd};
static pipeDef: IdentifierSpec = {name: 'ɵpid', moduleUrl: CORE, runtime: ɵpid};
static nodeValue: IdentifierSpec = {name: 'ɵnov', moduleUrl: CORE, runtime: ɵnov};
static ngContentDef: IdentifierSpec = {name: 'ɵncd', moduleUrl: CORE, runtime: ɵncd};
static unwrapValue: IdentifierSpec = {name: 'ɵunv', moduleUrl: CORE, runtime: ɵunv};
static createRendererType2: IdentifierSpec = {name: 'ɵcrt', moduleUrl: CORE, runtime: ɵcrt};
static RendererType2: IdentifierSpec = {
name: 'RendererType2',
moduleUrl: CORE,
// type only
runtime: null
};
static ViewDefinition: IdentifierSpec = {
name: 'ɵViewDefinition',
moduleUrl: CORE,
// type only
runtime: null
};
static createComponentFactory: IdentifierSpec = {name: 'ɵccf', moduleUrl: CORE, runtime: ɵccf};
}
export function assetUrl(pkg: string, path: string = null, type: string = 'src'): string {
if (path == null) {
return `@angular/${pkg}`;
} else {
return `@angular/${pkg}/${type}/${path}`;
}
}
export function resolveIdentifier(identifier: IdentifierSpec) {
let name = identifier.name;
return ɵreflector.resolveIdentifier(name, identifier.moduleUrl, null, identifier.runtime);
}
export function createIdentifier(identifier: IdentifierSpec): CompileIdentifierMetadata {
return {reference: resolveIdentifier(identifier)};
}
export function identifierToken(identifier: CompileIdentifierMetadata): CompileTokenMetadata {
return {identifier: identifier};
}
export function createIdentifierToken(identifier: IdentifierSpec): CompileTokenMetadata {
return identifierToken(createIdentifier(identifier));
}
export function createEnumIdentifier(
enumType: IdentifierSpec, name: string): CompileIdentifierMetadata {
const resolvedEnum = ɵreflector.resolveEnum(resolveIdentifier(enumType), name);
return {reference: resolvedEnum};
}

View File

@ -0,0 +1,17 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* A replacement for @Injectable to be used in the compiler, so that
* we don't try to evaluate the metadata in the compiler during AoT.
* This decorator is enough to make the compiler work with the ReflectiveInjector though.
* @Annotation
*/
export function CompilerInjectable(): (data: any) => any {
return (x) => x;
}

View File

@ -0,0 +1,361 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Compiler, ComponentFactory, Inject, Injector, ModuleWithComponentFactories, NgModuleFactory, Type, ɵgetComponentViewDefinitionFactory as getComponentViewDefinitionFactory, ɵstringify as stringify} from '@angular/core';
import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, ProviderMeta, ProxyClass, createHostComponentMeta, identifierName} from '../compile_metadata';
import {CompilerConfig} from '../config';
import {CompilerInjectable} from '../injectable';
import {CompileMetadataResolver} from '../metadata_resolver';
import {NgModuleCompiler} from '../ng_module_compiler';
import * as ir from '../output/output_ast';
import {interpretStatements} from '../output/output_interpreter';
import {jitStatements} from '../output/output_jit';
import {CompiledStylesheet, StyleCompiler} from '../style_compiler';
import {TemplateParser} from '../template_parser/template_parser';
import {SyncAsyncResult} from '../util';
import {ViewCompiler} from '../view_compiler/view_compiler';
/**
* An internal module of the Angular compiler that begins with component types,
* extracts templates, and eventually produces a compiled version of the component
* ready for linking into an application.
*
* @security When compiling templates at runtime, you must ensure that the entire template comes
* from a trusted source. Attacker-controlled data introduced by a template could expose your
* application to XSS risks. For more detail, see the [Security Guide](http://g.co/ng/security).
*/
@CompilerInjectable()
export class JitCompiler implements Compiler {
private _compiledTemplateCache = new Map<Type<any>, CompiledTemplate>();
private _compiledHostTemplateCache = new Map<Type<any>, CompiledTemplate>();
private _compiledDirectiveWrapperCache = new Map<Type<any>, Type<any>>();
private _compiledNgModuleCache = new Map<Type<any>, NgModuleFactory<any>>();
constructor(
private _injector: Injector, private _metadataResolver: CompileMetadataResolver,
private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler,
private _viewCompiler: ViewCompiler, private _ngModuleCompiler: NgModuleCompiler,
private _compilerConfig: CompilerConfig) {}
get injector(): Injector { return this._injector; }
compileModuleSync<T>(moduleType: Type<T>): NgModuleFactory<T> {
return this._compileModuleAndComponents(moduleType, true).syncResult;
}
compileModuleAsync<T>(moduleType: Type<T>): Promise<NgModuleFactory<T>> {
return this._compileModuleAndComponents(moduleType, false).asyncResult;
}
compileModuleAndAllComponentsSync<T>(moduleType: Type<T>): ModuleWithComponentFactories<T> {
return this._compileModuleAndAllComponents(moduleType, true).syncResult;
}
compileModuleAndAllComponentsAsync<T>(moduleType: Type<T>):
Promise<ModuleWithComponentFactories<T>> {
return this._compileModuleAndAllComponents(moduleType, false).asyncResult;
}
getNgContentSelectors(component: Type<any>): string[] {
const template = this._compiledTemplateCache.get(component);
if (!template) {
throw new Error(`The component ${stringify(component)} is not yet compiled!`);
}
return template.compMeta.template.ngContentSelectors;
}
private _compileModuleAndComponents<T>(moduleType: Type<T>, isSync: boolean):
SyncAsyncResult<NgModuleFactory<T>> {
const loadingPromise = this._loadModules(moduleType, isSync);
const createResult = () => {
this._compileComponents(moduleType, null);
return this._compileModule(moduleType);
};
if (isSync) {
return new SyncAsyncResult(createResult());
} else {
return new SyncAsyncResult(null, loadingPromise.then(createResult));
}
}
private _compileModuleAndAllComponents<T>(moduleType: Type<T>, isSync: boolean):
SyncAsyncResult<ModuleWithComponentFactories<T>> {
const loadingPromise = this._loadModules(moduleType, isSync);
const createResult = () => {
const componentFactories: ComponentFactory<any>[] = [];
this._compileComponents(moduleType, componentFactories);
return new ModuleWithComponentFactories(this._compileModule(moduleType), componentFactories);
};
if (isSync) {
return new SyncAsyncResult(createResult());
} else {
return new SyncAsyncResult(null, loadingPromise.then(createResult));
}
}
private _loadModules(mainModule: any, isSync: boolean): Promise<any> {
const loadingPromises: Promise<any>[] = [];
const ngModule = this._metadataResolver.getNgModuleMetadata(mainModule);
// Note: the loadingPromise for a module only includes the loading of the exported directives
// of imported modules.
// However, for runtime compilation, we want to transitively compile all modules,
// so we also need to call loadNgModuleDirectiveAndPipeMetadata for all nested modules.
ngModule.transitiveModule.modules.forEach((localModuleMeta) => {
loadingPromises.push(this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
localModuleMeta.reference, isSync));
});
return Promise.all(loadingPromises);
}
private _compileModule<T>(moduleType: Type<T>): NgModuleFactory<T> {
let ngModuleFactory = this._compiledNgModuleCache.get(moduleType);
if (!ngModuleFactory) {
const moduleMeta = this._metadataResolver.getNgModuleMetadata(moduleType);
// Always provide a bound Compiler
const extraProviders = [this._metadataResolver.getProviderMetadata(new ProviderMeta(
Compiler, {useFactory: () => new ModuleBoundCompiler(this, moduleMeta.type.reference)}))];
const compileResult = this._ngModuleCompiler.compile(moduleMeta, extraProviders);
if (!this._compilerConfig.useJit) {
ngModuleFactory =
interpretStatements(compileResult.statements, [compileResult.ngModuleFactoryVar])[0];
} else {
ngModuleFactory = jitStatements(
`/${identifierName(moduleMeta.type)}/module.ngfactory.js`, compileResult.statements,
[compileResult.ngModuleFactoryVar])[0];
}
this._compiledNgModuleCache.set(moduleMeta.type.reference, ngModuleFactory);
}
return ngModuleFactory;
}
/**
* @internal
*/
_compileComponents(mainModule: Type<any>, allComponentFactories: ComponentFactory<any>[]) {
const ngModule = this._metadataResolver.getNgModuleMetadata(mainModule);
const moduleByDirective = new Map<any, CompileNgModuleMetadata>();
const templates = new Set<CompiledTemplate>();
ngModule.transitiveModule.modules.forEach((localModuleSummary) => {
const localModuleMeta =
this._metadataResolver.getNgModuleMetadata(localModuleSummary.reference);
localModuleMeta.declaredDirectives.forEach((dirIdentifier) => {
moduleByDirective.set(dirIdentifier.reference, localModuleMeta);
const dirMeta = this._metadataResolver.getDirectiveMetadata(dirIdentifier.reference);
if (dirMeta.isComponent) {
templates.add(this._createCompiledTemplate(dirMeta, localModuleMeta));
if (allComponentFactories) {
const template =
this._createCompiledHostTemplate(dirMeta.type.reference, localModuleMeta);
templates.add(template);
allComponentFactories.push(<ComponentFactory<any>>dirMeta.componentFactory);
}
}
});
});
ngModule.transitiveModule.modules.forEach((localModuleSummary) => {
const localModuleMeta =
this._metadataResolver.getNgModuleMetadata(localModuleSummary.reference);
localModuleMeta.declaredDirectives.forEach((dirIdentifier) => {
const dirMeta = this._metadataResolver.getDirectiveMetadata(dirIdentifier.reference);
if (dirMeta.isComponent) {
dirMeta.entryComponents.forEach((entryComponentType) => {
const moduleMeta = moduleByDirective.get(entryComponentType.componentType);
templates.add(
this._createCompiledHostTemplate(entryComponentType.componentType, moduleMeta));
});
}
});
localModuleMeta.entryComponents.forEach((entryComponentType) => {
const moduleMeta = moduleByDirective.get(entryComponentType.componentType);
templates.add(
this._createCompiledHostTemplate(entryComponentType.componentType, moduleMeta));
});
});
templates.forEach((template) => this._compileTemplate(template));
}
clearCacheFor(type: Type<any>) {
this._compiledNgModuleCache.delete(type);
this._metadataResolver.clearCacheFor(type);
this._compiledHostTemplateCache.delete(type);
const compiledTemplate = this._compiledTemplateCache.get(type);
if (compiledTemplate) {
this._compiledTemplateCache.delete(type);
}
}
clearCache(): void {
this._metadataResolver.clearCache();
this._compiledTemplateCache.clear();
this._compiledHostTemplateCache.clear();
this._compiledNgModuleCache.clear();
}
private _createCompiledHostTemplate(compType: Type<any>, ngModule: CompileNgModuleMetadata):
CompiledTemplate {
if (!ngModule) {
throw new Error(
`Component ${stringify(compType)} is not part of any NgModule or the module has not been imported into your module.`);
}
let compiledTemplate = this._compiledHostTemplateCache.get(compType);
if (!compiledTemplate) {
const compMeta = this._metadataResolver.getDirectiveMetadata(compType);
assertComponent(compMeta);
const componentFactory = <ComponentFactory<any>>compMeta.componentFactory;
const hostClass = this._metadataResolver.getHostComponentType(compType);
const hostMeta = createHostComponentMeta(
hostClass, compMeta, <any>getComponentViewDefinitionFactory(componentFactory));
compiledTemplate =
new CompiledTemplate(true, compMeta.type, hostMeta, ngModule, [compMeta.type]);
this._compiledHostTemplateCache.set(compType, compiledTemplate);
}
return compiledTemplate;
}
private _createCompiledTemplate(
compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata): CompiledTemplate {
let compiledTemplate = this._compiledTemplateCache.get(compMeta.type.reference);
if (!compiledTemplate) {
assertComponent(compMeta);
compiledTemplate = new CompiledTemplate(
false, compMeta.type, compMeta, ngModule, ngModule.transitiveModule.directives);
this._compiledTemplateCache.set(compMeta.type.reference, compiledTemplate);
}
return compiledTemplate;
}
private _compileTemplate(template: CompiledTemplate) {
if (template.isCompiled) {
return;
}
const compMeta = template.compMeta;
const externalStylesheetsByModuleUrl = new Map<string, CompiledStylesheet>();
const stylesCompileResult = this._styleCompiler.compileComponent(compMeta);
stylesCompileResult.externalStylesheets.forEach(
(r) => { externalStylesheetsByModuleUrl.set(r.meta.moduleUrl, r); });
this._resolveStylesCompileResult(
stylesCompileResult.componentStylesheet, externalStylesheetsByModuleUrl);
const directives =
template.directives.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference));
const pipes = template.ngModule.transitiveModule.pipes.map(
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
compMeta, compMeta.template.template, directives, pipes, template.ngModule.schemas,
identifierName(compMeta.type));
const compileResult = this._viewCompiler.compileComponent(
compMeta, parsedTemplate, ir.variable(stylesCompileResult.componentStylesheet.stylesVar),
usedPipes);
const statements =
stylesCompileResult.componentStylesheet.statements.concat(compileResult.statements);
let viewClass: any;
let rendererType: any;
if (!this._compilerConfig.useJit) {
[viewClass, rendererType] = interpretStatements(
statements, [compileResult.viewClassVar, compileResult.rendererTypeVar]);
} else {
const sourceUrl =
`/${identifierName(template.ngModule.type)}/${identifierName(template.compType)}/${template.isHost?'host':'component'}.ngfactory.js`;
[viewClass, rendererType] = jitStatements(
sourceUrl, statements, [compileResult.viewClassVar, compileResult.rendererTypeVar]);
}
template.compiled(viewClass, rendererType);
}
private _resolveStylesCompileResult(
result: CompiledStylesheet, externalStylesheetsByModuleUrl: Map<string, CompiledStylesheet>) {
result.dependencies.forEach((dep, i) => {
const nestedCompileResult = externalStylesheetsByModuleUrl.get(dep.moduleUrl);
const nestedStylesArr = this._resolveAndEvalStylesCompileResult(
nestedCompileResult, externalStylesheetsByModuleUrl);
dep.valuePlaceholder.reference = nestedStylesArr;
});
}
private _resolveAndEvalStylesCompileResult(
result: CompiledStylesheet,
externalStylesheetsByModuleUrl: Map<string, CompiledStylesheet>): string[] {
this._resolveStylesCompileResult(result, externalStylesheetsByModuleUrl);
if (!this._compilerConfig.useJit) {
return interpretStatements(result.statements, [result.stylesVar])[0];
} else {
return jitStatements(
`/${result.meta.moduleUrl}.ngstyle.js`, result.statements, [result.stylesVar])[0];
}
}
}
class CompiledTemplate {
private _viewClass: Function = null;
isCompiled = false;
constructor(
public isHost: boolean, public compType: CompileIdentifierMetadata,
public compMeta: CompileDirectiveMetadata, public ngModule: CompileNgModuleMetadata,
public directives: CompileIdentifierMetadata[]) {}
compiled(viewClass: Function, rendererType: any) {
this._viewClass = viewClass;
(<ProxyClass>this.compMeta.componentViewType).setDelegate(viewClass);
for (let prop in rendererType) {
(<any>this.compMeta.rendererType)[prop] = rendererType[prop];
}
this.isCompiled = true;
}
}
function assertComponent(meta: CompileDirectiveMetadata) {
if (!meta.isComponent) {
throw new Error(
`Could not compile '${identifierName(meta.type)}' because it is not a component.`);
}
}
/**
* Implements `Compiler` by delegating to the JitCompiler using a known module.
*/
class ModuleBoundCompiler implements Compiler {
constructor(private _delegate: JitCompiler, private _ngModule: Type<any>) {}
get _injector(): Injector { return this._delegate.injector; }
compileModuleSync<T>(moduleType: Type<T>): NgModuleFactory<T> {
return this._delegate.compileModuleSync(moduleType);
}
compileModuleAsync<T>(moduleType: Type<T>): Promise<NgModuleFactory<T>> {
return this._delegate.compileModuleAsync(moduleType);
}
compileModuleAndAllComponentsSync<T>(moduleType: Type<T>): ModuleWithComponentFactories<T> {
return this._delegate.compileModuleAndAllComponentsSync(moduleType);
}
compileModuleAndAllComponentsAsync<T>(moduleType: Type<T>):
Promise<ModuleWithComponentFactories<T>> {
return this._delegate.compileModuleAndAllComponentsAsync(moduleType);
}
getNgContentSelectors(component: Type<any>): string[] {
return this._delegate.getNgContentSelectors(component);
}
/**
* Clears all caches
*/
clearCache(): void { this._delegate.clearCache(); }
/**
* Clears the cache for the given component/ngModule.
*/
clearCacheFor(type: Type<any>) { this._delegate.clearCacheFor(type); }
}

View File

@ -0,0 +1,169 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {COMPILER_OPTIONS, Compiler, CompilerFactory, CompilerOptions, Inject, InjectionToken, MissingTranslationStrategy, Optional, PLATFORM_INITIALIZER, PlatformRef, Provider, ReflectiveInjector, TRANSLATIONS, TRANSLATIONS_FORMAT, Type, ViewEncapsulation, createPlatformFactory, isDevMode, platformCore, ɵConsole as Console, ɵReflectionCapabilities as ReflectionCapabilities, ɵReflector as Reflector, ɵReflectorReader as ReflectorReader, ɵreflector as reflector} from '@angular/core';
import {CompilerConfig} from '../config';
import {DirectiveNormalizer} from '../directive_normalizer';
import {DirectiveResolver} from '../directive_resolver';
import {Lexer} from '../expression_parser/lexer';
import {Parser} from '../expression_parser/parser';
import * as i18n from '../i18n/index';
import {CompilerInjectable} from '../injectable';
import {CompileMetadataResolver} from '../metadata_resolver';
import {HtmlParser} from '../ml_parser/html_parser';
import {NgModuleCompiler} from '../ng_module_compiler';
import {NgModuleResolver} from '../ng_module_resolver';
import {PipeResolver} from '../pipe_resolver';
import {ResourceLoader} from '../resource_loader';
import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {StyleCompiler} from '../style_compiler';
import {SummaryResolver} from '../summary_resolver';
import {TemplateParser} from '../template_parser/template_parser';
import {DEFAULT_PACKAGE_URL_PROVIDER, UrlResolver} from '../url_resolver';
import {ViewCompiler} from '../view_compiler/view_compiler';
import {JitCompiler} from './compiler';
const _NO_RESOURCE_LOADER: ResourceLoader = {
get(url: string): Promise<string>{
throw new Error(
`No ResourceLoader implementation has been provided. Can't read the url "${url}"`);}
};
const baseHtmlParser = new InjectionToken('HtmlParser');
/**
* A set of providers that provide `JitCompiler` and its dependencies to use for
* template compilation.
*/
export const COMPILER_PROVIDERS: Array<any|Type<any>|{[k: string]: any}|any[]> = [
{provide: Reflector, useValue: reflector},
{provide: ReflectorReader, useExisting: Reflector},
{provide: ResourceLoader, useValue: _NO_RESOURCE_LOADER},
SummaryResolver,
Console,
Lexer,
Parser,
{
provide: baseHtmlParser,
useClass: HtmlParser,
},
{
provide: i18n.I18NHtmlParser,
useFactory: (parser: HtmlParser, translations: string, format: string, config: CompilerConfig,
console: Console) =>
new i18n.I18NHtmlParser(
parser, translations, format, config.missingTranslation, console),
deps: [
baseHtmlParser,
[new Optional(), new Inject(TRANSLATIONS)],
[new Optional(), new Inject(TRANSLATIONS_FORMAT)],
[CompilerConfig],
[Console],
]
},
{
provide: HtmlParser,
useExisting: i18n.I18NHtmlParser,
},
TemplateParser,
DirectiveNormalizer,
CompileMetadataResolver,
DEFAULT_PACKAGE_URL_PROVIDER,
StyleCompiler,
ViewCompiler,
NgModuleCompiler,
{provide: CompilerConfig, useValue: new CompilerConfig()},
JitCompiler,
{provide: Compiler, useExisting: JitCompiler},
DomElementSchemaRegistry,
{provide: ElementSchemaRegistry, useExisting: DomElementSchemaRegistry},
UrlResolver,
DirectiveResolver,
PipeResolver,
NgModuleResolver,
];
@CompilerInjectable()
export class JitCompilerFactory implements CompilerFactory {
private _defaultOptions: CompilerOptions[];
constructor(@Inject(COMPILER_OPTIONS) defaultOptions: CompilerOptions[]) {
const compilerOptions: CompilerOptions = {
useDebug: isDevMode(),
useJit: true,
defaultEncapsulation: ViewEncapsulation.Emulated,
missingTranslation: MissingTranslationStrategy.Warning,
enableLegacyTemplate: true,
};
this._defaultOptions = [compilerOptions, ...defaultOptions];
}
createCompiler(options: CompilerOptions[] = []): Compiler {
const opts = _mergeOptions(this._defaultOptions.concat(options));
const injector = ReflectiveInjector.resolveAndCreate([
COMPILER_PROVIDERS, {
provide: CompilerConfig,
useFactory: () => {
return new CompilerConfig({
// let explicit values from the compiler options overwrite options
// from the app providers
useJit: opts.useJit,
// let explicit values from the compiler options overwrite options
// from the app providers
defaultEncapsulation: opts.defaultEncapsulation,
missingTranslation: opts.missingTranslation,
enableLegacyTemplate: opts.enableLegacyTemplate,
});
},
deps: []
},
opts.providers
]);
return injector.get(Compiler);
}
}
function _initReflector() {
reflector.reflectionCapabilities = new ReflectionCapabilities();
}
/**
* A platform that included corePlatform and the compiler.
*
* @experimental
*/
export const platformCoreDynamic = createPlatformFactory(platformCore, 'coreDynamic', [
{provide: COMPILER_OPTIONS, useValue: {}, multi: true},
{provide: CompilerFactory, useClass: JitCompilerFactory},
{provide: PLATFORM_INITIALIZER, useValue: _initReflector, multi: true},
]);
function _mergeOptions(optionsArr: CompilerOptions[]): CompilerOptions {
return {
useJit: _lastDefined(optionsArr.map(options => options.useJit)),
defaultEncapsulation: _lastDefined(optionsArr.map(options => options.defaultEncapsulation)),
providers: _mergeArrays(optionsArr.map(options => options.providers)),
missingTranslation: _lastDefined(optionsArr.map(options => options.missingTranslation)),
};
}
function _lastDefined<T>(args: T[]): T {
for (let i = args.length - 1; i >= 0; i--) {
if (args[i] !== undefined) {
return args[i];
}
}
return undefined;
}
function _mergeArrays(parts: any[][]): any[] {
const result: any[] = [];
parts.forEach((part) => part && result.push(...part));
return result;
}

View File

@ -0,0 +1,35 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵLifecycleHooks, ɵreflector} from '@angular/core';
export function hasLifecycleHook(hook: ɵLifecycleHooks, token: any): boolean {
return ɵreflector.hasLifecycleHook(token, getHookName(hook));
}
function getHookName(hook: ɵLifecycleHooks): string {
switch (hook) {
case ɵLifecycleHooks.OnInit:
return 'ngOnInit';
case ɵLifecycleHooks.OnDestroy:
return 'ngOnDestroy';
case ɵLifecycleHooks.DoCheck:
return 'ngDoCheck';
case ɵLifecycleHooks.OnChanges:
return 'ngOnChanges';
case ɵLifecycleHooks.AfterContentInit:
return 'ngAfterContentInit';
case ɵLifecycleHooks.AfterContentChecked:
return 'ngAfterContentChecked';
case ɵLifecycleHooks.AfterViewInit:
return 'ngAfterViewInit';
case ɵLifecycleHooks.AfterViewChecked:
return 'ngAfterViewChecked';
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,82 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseSourceSpan} from '../parse_util';
export interface Node {
sourceSpan: ParseSourceSpan;
visit(visitor: Visitor, context: any): any;
}
export class Text implements Node {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitText(this, context); }
}
export class Expansion implements Node {
constructor(
public switchValue: string, public type: string, public cases: ExpansionCase[],
public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitExpansion(this, context); }
}
export class ExpansionCase implements Node {
constructor(
public value: string, public expression: Node[], public sourceSpan: ParseSourceSpan,
public valueSourceSpan: ParseSourceSpan, public expSourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitExpansionCase(this, context); }
}
export class Attribute implements Node {
constructor(
public name: string, public value: string, public sourceSpan: ParseSourceSpan,
public valueSpan?: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitAttribute(this, context); }
}
export class Element implements Node {
constructor(
public name: string, public attrs: Attribute[], public children: Node[],
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
public endSourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitElement(this, context); }
}
export class Comment implements Node {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context: any): any { return visitor.visitComment(this, context); }
}
export interface Visitor {
// Returning a truthy value from `visit()` will prevent `visitAll()` from the call to the typed
// method and result returned will become the result included in `visitAll()`s result array.
visit?(node: Node, context: any): any;
visitElement(element: Element, context: any): any;
visitAttribute(attribute: Attribute, context: any): any;
visitText(text: Text, context: any): any;
visitComment(comment: Comment, context: any): any;
visitExpansion(expansion: Expansion, context: any): any;
visitExpansionCase(expansionCase: ExpansionCase, context: any): any;
}
export function visitAll(visitor: Visitor, nodes: Node[], context: any = null): any[] {
const result: any[] = [];
const visit = visitor.visit ?
(ast: Node) => visitor.visit(ast, context) || ast.visit(visitor, context) :
(ast: Node) => ast.visit(visitor, context);
nodes.forEach(ast => {
const astResult = visit(ast);
if (astResult) {
result.push(astResult);
}
});
return result;
}

View File

@ -0,0 +1,26 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CompilerInjectable} from '../injectable';
import {getHtmlTagDefinition} from './html_tags';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
import {ParseTreeResult, Parser} from './parser';
export {ParseTreeResult, TreeError} from './parser';
@CompilerInjectable()
export class HtmlParser extends Parser {
constructor() { super(getHtmlTagDefinition); }
parse(
source: string, url: string, parseExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ParseTreeResult {
return super.parse(source, url, parseExpansionForms, interpolationConfig);
}
}

View File

@ -0,0 +1,129 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {TagContentType, TagDefinition} from './tags';
export class HtmlTagDefinition implements TagDefinition {
private closedByChildren: {[key: string]: boolean} = {};
closedByParent: boolean = false;
requiredParents: {[key: string]: boolean};
parentToAdd: string;
implicitNamespacePrefix: string;
contentType: TagContentType;
isVoid: boolean;
ignoreFirstLf: boolean;
canSelfClose: boolean = false;
constructor(
{closedByChildren, requiredParents, implicitNamespacePrefix,
contentType = TagContentType.PARSABLE_DATA, closedByParent = false, isVoid = false,
ignoreFirstLf = false}: {
closedByChildren?: string[],
closedByParent?: boolean,
requiredParents?: string[],
implicitNamespacePrefix?: string,
contentType?: TagContentType,
isVoid?: boolean,
ignoreFirstLf?: boolean
} = {}) {
if (closedByChildren && closedByChildren.length > 0) {
closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true);
}
this.isVoid = isVoid;
this.closedByParent = closedByParent || isVoid;
if (requiredParents && requiredParents.length > 0) {
this.requiredParents = {};
// The first parent is the list is automatically when none of the listed parents are present
this.parentToAdd = requiredParents[0];
requiredParents.forEach(tagName => this.requiredParents[tagName] = true);
}
this.implicitNamespacePrefix = implicitNamespacePrefix;
this.contentType = contentType;
this.ignoreFirstLf = ignoreFirstLf;
}
requireExtraParent(currentParent: string): boolean {
if (!this.requiredParents) {
return false;
}
if (!currentParent) {
return true;
}
const lcParent = currentParent.toLowerCase();
const isParentTemplate = lcParent === 'template' || currentParent === 'ng-template';
return !isParentTemplate && this.requiredParents[lcParent] != true;
}
isClosedByChild(name: string): boolean {
return this.isVoid || name.toLowerCase() in this.closedByChildren;
}
}
// see http://www.w3.org/TR/html51/syntax.html#optional-tags
// This implementation does not fully conform to the HTML5 spec.
const TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
'base': new HtmlTagDefinition({isVoid: true}),
'meta': new HtmlTagDefinition({isVoid: true}),
'area': new HtmlTagDefinition({isVoid: true}),
'embed': new HtmlTagDefinition({isVoid: true}),
'link': new HtmlTagDefinition({isVoid: true}),
'img': new HtmlTagDefinition({isVoid: true}),
'input': new HtmlTagDefinition({isVoid: true}),
'param': new HtmlTagDefinition({isVoid: true}),
'hr': new HtmlTagDefinition({isVoid: true}),
'br': new HtmlTagDefinition({isVoid: true}),
'source': new HtmlTagDefinition({isVoid: true}),
'track': new HtmlTagDefinition({isVoid: true}),
'wbr': new HtmlTagDefinition({isVoid: true}),
'p': new HtmlTagDefinition({
closedByChildren: [
'address', 'article', 'aside', 'blockquote', 'div', 'dl', 'fieldset', 'footer', 'form',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr',
'main', 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul'
],
closedByParent: true
}),
'thead': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot']}),
'tbody': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot'], closedByParent: true}),
'tfoot': new HtmlTagDefinition({closedByChildren: ['tbody'], closedByParent: true}),
'tr': new HtmlTagDefinition({
closedByChildren: ['tr'],
requiredParents: ['tbody', 'tfoot', 'thead'],
closedByParent: true
}),
'td': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
'th': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
'col': new HtmlTagDefinition({requiredParents: ['colgroup'], isVoid: true}),
'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}),
'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}),
'li': new HtmlTagDefinition({closedByChildren: ['li'], closedByParent: true}),
'dt': new HtmlTagDefinition({closedByChildren: ['dt', 'dd']}),
'dd': new HtmlTagDefinition({closedByChildren: ['dt', 'dd'], closedByParent: true}),
'rb': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'rt': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'rtc': new HtmlTagDefinition({closedByChildren: ['rb', 'rtc', 'rp'], closedByParent: true}),
'rp': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
'optgroup': new HtmlTagDefinition({closedByChildren: ['optgroup'], closedByParent: true}),
'option': new HtmlTagDefinition({closedByChildren: ['option', 'optgroup'], closedByParent: true}),
'pre': new HtmlTagDefinition({ignoreFirstLf: true}),
'listing': new HtmlTagDefinition({ignoreFirstLf: true}),
'style': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}),
'script': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}),
'title': new HtmlTagDefinition({contentType: TagContentType.ESCAPABLE_RAW_TEXT}),
'textarea':
new HtmlTagDefinition({contentType: TagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true}),
};
const _DEFAULT_TAG_DEFINITION = new HtmlTagDefinition();
export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition {
return TAG_DEFINITIONS[tagName.toLowerCase()] || _DEFAULT_TAG_DEFINITION;
}

View File

@ -0,0 +1,125 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseError, ParseSourceSpan} from '../parse_util';
import * as html from './ast';
// http://cldr.unicode.org/index/cldr-spec/plural-rules
const PLURAL_CASES: string[] = ['zero', 'one', 'two', 'few', 'many', 'other'];
/**
* Expands special forms into elements.
*
* For example,
*
* ```
* { messages.length, plural,
* =0 {zero}
* =1 {one}
* other {more than one}
* }
* ```
*
* will be expanded into
*
* ```
* <ng-container [ngPlural]="messages.length">
* <ng-template ngPluralCase="=0">zero</ng-template>
* <ng-template ngPluralCase="=1">one</ng-template>
* <ng-template ngPluralCase="other">more than one</ng-template>
* </ng-container>
* ```
*/
export function expandNodes(nodes: html.Node[]): ExpansionResult {
const expander = new _Expander();
return new ExpansionResult(html.visitAll(expander, nodes), expander.isExpanded, expander.errors);
}
export class ExpansionResult {
constructor(public nodes: html.Node[], public expanded: boolean, public errors: ParseError[]) {}
}
export class ExpansionError extends ParseError {
constructor(span: ParseSourceSpan, errorMsg: string) { super(span, errorMsg); }
}
/**
* Expand expansion forms (plural, select) to directives
*
* @internal
*/
class _Expander implements html.Visitor {
isExpanded: boolean = false;
errors: ParseError[] = [];
visitElement(element: html.Element, context: any): any {
return new html.Element(
element.name, element.attrs, html.visitAll(this, element.children), element.sourceSpan,
element.startSourceSpan, element.endSourceSpan);
}
visitAttribute(attribute: html.Attribute, context: any): any { return attribute; }
visitText(text: html.Text, context: any): any { return text; }
visitComment(comment: html.Comment, context: any): any { return comment; }
visitExpansion(icu: html.Expansion, context: any): any {
this.isExpanded = true;
return icu.type == 'plural' ? _expandPluralForm(icu, this.errors) :
_expandDefaultForm(icu, this.errors);
}
visitExpansionCase(icuCase: html.ExpansionCase, context: any): any {
throw new Error('Should not be reached');
}
}
// Plural forms are expanded to `NgPlural` and `NgPluralCase`s
function _expandPluralForm(ast: html.Expansion, errors: ParseError[]): html.Element {
const children = ast.cases.map(c => {
if (PLURAL_CASES.indexOf(c.value) == -1 && !c.value.match(/^=\d+$/)) {
errors.push(new ExpansionError(
c.valueSourceSpan,
`Plural cases should be "=<number>" or one of ${PLURAL_CASES.join(", ")}`));
}
const expansionResult = expandNodes(c.expression);
errors.push(...expansionResult.errors);
return new html.Element(
`ng-template`, [new html.Attribute('ngPluralCase', `${c.value}`, c.valueSourceSpan)],
expansionResult.nodes, c.sourceSpan, c.sourceSpan, c.sourceSpan);
});
const switchAttr = new html.Attribute('[ngPlural]', ast.switchValue, ast.switchValueSourceSpan);
return new html.Element(
'ng-container', [switchAttr], children, ast.sourceSpan, ast.sourceSpan, ast.sourceSpan);
}
// ICU messages (excluding plural form) are expanded to `NgSwitch` and `NgSwitychCase`s
function _expandDefaultForm(ast: html.Expansion, errors: ParseError[]): html.Element {
const children = ast.cases.map(c => {
const expansionResult = expandNodes(c.expression);
errors.push(...expansionResult.errors);
if (c.value === 'other') {
// other is the default case when no values match
return new html.Element(
`ng-template`, [new html.Attribute('ngSwitchDefault', '', c.valueSourceSpan)],
expansionResult.nodes, c.sourceSpan, c.sourceSpan, c.sourceSpan);
}
return new html.Element(
`ng-template`, [new html.Attribute('ngSwitchCase', `${c.value}`, c.valueSourceSpan)],
expansionResult.nodes, c.sourceSpan, c.sourceSpan, c.sourceSpan);
});
const switchAttr = new html.Attribute('[ngSwitch]', ast.switchValue, ast.switchValueSourceSpan);
return new html.Element(
'ng-container', [switchAttr], children, ast.sourceSpan, ast.sourceSpan, ast.sourceSpan);
}

View File

@ -0,0 +1,25 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {assertInterpolationSymbols} from '../assertions';
export class InterpolationConfig {
static fromArray(markers: [string, string]): InterpolationConfig {
if (!markers) {
return DEFAULT_INTERPOLATION_CONFIG;
}
assertInterpolationSymbols('interpolation', markers);
return new InterpolationConfig(markers[0], markers[1]);
}
constructor(public start: string, public end: string){};
}
export const DEFAULT_INTERPOLATION_CONFIG: InterpolationConfig =
new InterpolationConfig('{{', '}}');

View File

@ -0,0 +1,713 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as chars from '../chars';
import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from '../parse_util';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
import {NAMED_ENTITIES, TagContentType, TagDefinition} from './tags';
export enum TokenType {
TAG_OPEN_START,
TAG_OPEN_END,
TAG_OPEN_END_VOID,
TAG_CLOSE,
TEXT,
ESCAPABLE_RAW_TEXT,
RAW_TEXT,
COMMENT_START,
COMMENT_END,
CDATA_START,
CDATA_END,
ATTR_NAME,
ATTR_VALUE,
DOC_TYPE,
EXPANSION_FORM_START,
EXPANSION_CASE_VALUE,
EXPANSION_CASE_EXP_START,
EXPANSION_CASE_EXP_END,
EXPANSION_FORM_END,
EOF
}
export class Token {
constructor(public type: TokenType, public parts: string[], public sourceSpan: ParseSourceSpan) {}
}
export class TokenError extends ParseError {
constructor(errorMsg: string, public tokenType: TokenType, span: ParseSourceSpan) {
super(span, errorMsg);
}
}
export class TokenizeResult {
constructor(public tokens: Token[], public errors: TokenError[]) {}
}
export function tokenize(
source: string, url: string, getTagDefinition: (tagName: string) => TagDefinition,
tokenizeExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): TokenizeResult {
return new _Tokenizer(
new ParseSourceFile(source, url), getTagDefinition, tokenizeExpansionForms,
interpolationConfig)
.tokenize();
}
const _CR_OR_CRLF_REGEXP = /\r\n?/g;
function _unexpectedCharacterErrorMsg(charCode: number): string {
const char = charCode === chars.$EOF ? 'EOF' : String.fromCharCode(charCode);
return `Unexpected character "${char}"`;
}
function _unknownEntityErrorMsg(entitySrc: string): string {
return `Unknown entity "${entitySrc}" - use the "&#<decimal>;" or "&#x<hex>;" syntax`;
}
class _ControlFlowError {
constructor(public error: TokenError) {}
}
// See http://www.w3.org/TR/html51/syntax.html#writing
class _Tokenizer {
private _input: string;
private _length: number;
// Note: this is always lowercase!
private _peek: number = -1;
private _nextPeek: number = -1;
private _index: number = -1;
private _line: number = 0;
private _column: number = -1;
private _currentTokenStart: ParseLocation;
private _currentTokenType: TokenType;
private _expansionCaseStack: TokenType[] = [];
private _inInterpolation: boolean = false;
tokens: Token[] = [];
errors: TokenError[] = [];
/**
* @param _file The html source
* @param _getTagDefinition
* @param _tokenizeIcu Whether to tokenize ICU messages (considered as text nodes when false)
* @param _interpolationConfig
*/
constructor(
private _file: ParseSourceFile, private _getTagDefinition: (tagName: string) => TagDefinition,
private _tokenizeIcu: boolean,
private _interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
this._input = _file.content;
this._length = _file.content.length;
this._advance();
}
private _processCarriageReturns(content: string): string {
// http://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream
// In order to keep the original position in the source, we can not
// pre-process it.
// Instead CRs are processed right before instantiating the tokens.
return content.replace(_CR_OR_CRLF_REGEXP, '\n');
}
tokenize(): TokenizeResult {
while (this._peek !== chars.$EOF) {
const start = this._getLocation();
try {
if (this._attemptCharCode(chars.$LT)) {
if (this._attemptCharCode(chars.$BANG)) {
if (this._attemptCharCode(chars.$LBRACKET)) {
this._consumeCdata(start);
} else if (this._attemptCharCode(chars.$MINUS)) {
this._consumeComment(start);
} else {
this._consumeDocType(start);
}
} else if (this._attemptCharCode(chars.$SLASH)) {
this._consumeTagClose(start);
} else {
this._consumeTagOpen(start);
}
} else if (!(this._tokenizeIcu && this._tokenizeExpansionForm())) {
this._consumeText();
}
} catch (e) {
if (e instanceof _ControlFlowError) {
this.errors.push(e.error);
} else {
throw e;
}
}
}
this._beginToken(TokenType.EOF);
this._endToken([]);
return new TokenizeResult(mergeTextTokens(this.tokens), this.errors);
}
/**
* @returns {boolean} whether an ICU token has been created
* @internal
*/
private _tokenizeExpansionForm(): boolean {
if (isExpansionFormStart(this._input, this._index, this._interpolationConfig)) {
this._consumeExpansionFormStart();
return true;
}
if (isExpansionCaseStart(this._peek) && this._isInExpansionForm()) {
this._consumeExpansionCaseStart();
return true;
}
if (this._peek === chars.$RBRACE) {
if (this._isInExpansionCase()) {
this._consumeExpansionCaseEnd();
return true;
}
if (this._isInExpansionForm()) {
this._consumeExpansionFormEnd();
return true;
}
}
return false;
}
private _getLocation(): ParseLocation {
return new ParseLocation(this._file, this._index, this._line, this._column);
}
private _getSpan(
start: ParseLocation = this._getLocation(),
end: ParseLocation = this._getLocation()): ParseSourceSpan {
return new ParseSourceSpan(start, end);
}
private _beginToken(type: TokenType, start: ParseLocation = this._getLocation()) {
this._currentTokenStart = start;
this._currentTokenType = type;
}
private _endToken(parts: string[], end: ParseLocation = this._getLocation()): Token {
const token =
new Token(this._currentTokenType, parts, new ParseSourceSpan(this._currentTokenStart, end));
this.tokens.push(token);
this._currentTokenStart = null;
this._currentTokenType = null;
return token;
}
private _createError(msg: string, span: ParseSourceSpan): _ControlFlowError {
if (this._isInExpansionForm()) {
msg += ` (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)`;
}
const error = new TokenError(msg, this._currentTokenType, span);
this._currentTokenStart = null;
this._currentTokenType = null;
return new _ControlFlowError(error);
}
private _advance() {
if (this._index >= this._length) {
throw this._createError(_unexpectedCharacterErrorMsg(chars.$EOF), this._getSpan());
}
if (this._peek === chars.$LF) {
this._line++;
this._column = 0;
} else if (this._peek !== chars.$LF && this._peek !== chars.$CR) {
this._column++;
}
this._index++;
this._peek = this._index >= this._length ? chars.$EOF : this._input.charCodeAt(this._index);
this._nextPeek =
this._index + 1 >= this._length ? chars.$EOF : this._input.charCodeAt(this._index + 1);
}
private _attemptCharCode(charCode: number): boolean {
if (this._peek === charCode) {
this._advance();
return true;
}
return false;
}
private _attemptCharCodeCaseInsensitive(charCode: number): boolean {
if (compareCharCodeCaseInsensitive(this._peek, charCode)) {
this._advance();
return true;
}
return false;
}
private _requireCharCode(charCode: number) {
const location = this._getLocation();
if (!this._attemptCharCode(charCode)) {
throw this._createError(
_unexpectedCharacterErrorMsg(this._peek), this._getSpan(location, location));
}
}
private _attemptStr(chars: string): boolean {
const len = chars.length;
if (this._index + len > this._length) {
return false;
}
const initialPosition = this._savePosition();
for (let i = 0; i < len; i++) {
if (!this._attemptCharCode(chars.charCodeAt(i))) {
// If attempting to parse the string fails, we want to reset the parser
// to where it was before the attempt
this._restorePosition(initialPosition);
return false;
}
}
return true;
}
private _attemptStrCaseInsensitive(chars: string): boolean {
for (let i = 0; i < chars.length; i++) {
if (!this._attemptCharCodeCaseInsensitive(chars.charCodeAt(i))) {
return false;
}
}
return true;
}
private _requireStr(chars: string) {
const location = this._getLocation();
if (!this._attemptStr(chars)) {
throw this._createError(_unexpectedCharacterErrorMsg(this._peek), this._getSpan(location));
}
}
private _attemptCharCodeUntilFn(predicate: (code: number) => boolean) {
while (!predicate(this._peek)) {
this._advance();
}
}
private _requireCharCodeUntilFn(predicate: (code: number) => boolean, len: number) {
const start = this._getLocation();
this._attemptCharCodeUntilFn(predicate);
if (this._index - start.offset < len) {
throw this._createError(
_unexpectedCharacterErrorMsg(this._peek), this._getSpan(start, start));
}
}
private _attemptUntilChar(char: number) {
while (this._peek !== char) {
this._advance();
}
}
private _readChar(decodeEntities: boolean): string {
if (decodeEntities && this._peek === chars.$AMPERSAND) {
return this._decodeEntity();
} else {
const index = this._index;
this._advance();
return this._input[index];
}
}
private _decodeEntity(): string {
const start = this._getLocation();
this._advance();
if (this._attemptCharCode(chars.$HASH)) {
const isHex = this._attemptCharCode(chars.$x) || this._attemptCharCode(chars.$X);
const numberStart = this._getLocation().offset;
this._attemptCharCodeUntilFn(isDigitEntityEnd);
if (this._peek != chars.$SEMICOLON) {
throw this._createError(_unexpectedCharacterErrorMsg(this._peek), this._getSpan());
}
this._advance();
const strNum = this._input.substring(numberStart, this._index - 1);
try {
const charCode = parseInt(strNum, isHex ? 16 : 10);
return String.fromCharCode(charCode);
} catch (e) {
const entity = this._input.substring(start.offset + 1, this._index - 1);
throw this._createError(_unknownEntityErrorMsg(entity), this._getSpan(start));
}
} else {
const startPosition = this._savePosition();
this._attemptCharCodeUntilFn(isNamedEntityEnd);
if (this._peek != chars.$SEMICOLON) {
this._restorePosition(startPosition);
return '&';
}
this._advance();
const name = this._input.substring(start.offset + 1, this._index - 1);
const char = NAMED_ENTITIES[name];
if (!char) {
throw this._createError(_unknownEntityErrorMsg(name), this._getSpan(start));
}
return char;
}
}
private _consumeRawText(
decodeEntities: boolean, firstCharOfEnd: number, attemptEndRest: () => boolean): Token {
let tagCloseStart: ParseLocation;
const textStart = this._getLocation();
this._beginToken(decodeEntities ? TokenType.ESCAPABLE_RAW_TEXT : TokenType.RAW_TEXT, textStart);
const parts: string[] = [];
while (true) {
tagCloseStart = this._getLocation();
if (this._attemptCharCode(firstCharOfEnd) && attemptEndRest()) {
break;
}
if (this._index > tagCloseStart.offset) {
// add the characters consumed by the previous if statement to the output
parts.push(this._input.substring(tagCloseStart.offset, this._index));
}
while (this._peek !== firstCharOfEnd) {
parts.push(this._readChar(decodeEntities));
}
}
return this._endToken([this._processCarriageReturns(parts.join(''))], tagCloseStart);
}
private _consumeComment(start: ParseLocation) {
this._beginToken(TokenType.COMMENT_START, start);
this._requireCharCode(chars.$MINUS);
this._endToken([]);
const textToken = this._consumeRawText(false, chars.$MINUS, () => this._attemptStr('->'));
this._beginToken(TokenType.COMMENT_END, textToken.sourceSpan.end);
this._endToken([]);
}
private _consumeCdata(start: ParseLocation) {
this._beginToken(TokenType.CDATA_START, start);
this._requireStr('CDATA[');
this._endToken([]);
const textToken = this._consumeRawText(false, chars.$RBRACKET, () => this._attemptStr(']>'));
this._beginToken(TokenType.CDATA_END, textToken.sourceSpan.end);
this._endToken([]);
}
private _consumeDocType(start: ParseLocation) {
this._beginToken(TokenType.DOC_TYPE, start);
this._attemptUntilChar(chars.$GT);
this._advance();
this._endToken([this._input.substring(start.offset + 2, this._index - 1)]);
}
private _consumePrefixAndName(): string[] {
const nameOrPrefixStart = this._index;
let prefix: string = null;
while (this._peek !== chars.$COLON && !isPrefixEnd(this._peek)) {
this._advance();
}
let nameStart: number;
if (this._peek === chars.$COLON) {
this._advance();
prefix = this._input.substring(nameOrPrefixStart, this._index - 1);
nameStart = this._index;
} else {
nameStart = nameOrPrefixStart;
}
this._requireCharCodeUntilFn(isNameEnd, this._index === nameStart ? 1 : 0);
const name = this._input.substring(nameStart, this._index);
return [prefix, name];
}
private _consumeTagOpen(start: ParseLocation) {
const savedPos = this._savePosition();
let tagName: string;
let lowercaseTagName: string;
try {
if (!chars.isAsciiLetter(this._peek)) {
throw this._createError(_unexpectedCharacterErrorMsg(this._peek), this._getSpan());
}
const nameStart = this._index;
this._consumeTagOpenStart(start);
tagName = this._input.substring(nameStart, this._index);
lowercaseTagName = tagName.toLowerCase();
this._attemptCharCodeUntilFn(isNotWhitespace);
while (this._peek !== chars.$SLASH && this._peek !== chars.$GT) {
this._consumeAttributeName();
this._attemptCharCodeUntilFn(isNotWhitespace);
if (this._attemptCharCode(chars.$EQ)) {
this._attemptCharCodeUntilFn(isNotWhitespace);
this._consumeAttributeValue();
}
this._attemptCharCodeUntilFn(isNotWhitespace);
}
this._consumeTagOpenEnd();
} catch (e) {
if (e instanceof _ControlFlowError) {
// When the start tag is invalid, assume we want a "<"
this._restorePosition(savedPos);
// Back to back text tokens are merged at the end
this._beginToken(TokenType.TEXT, start);
this._endToken(['<']);
return;
}
throw e;
}
const contentTokenType = this._getTagDefinition(tagName).contentType;
if (contentTokenType === TagContentType.RAW_TEXT) {
this._consumeRawTextWithTagClose(lowercaseTagName, false);
} else if (contentTokenType === TagContentType.ESCAPABLE_RAW_TEXT) {
this._consumeRawTextWithTagClose(lowercaseTagName, true);
}
}
private _consumeRawTextWithTagClose(lowercaseTagName: string, decodeEntities: boolean) {
const textToken = this._consumeRawText(decodeEntities, chars.$LT, () => {
if (!this._attemptCharCode(chars.$SLASH)) return false;
this._attemptCharCodeUntilFn(isNotWhitespace);
if (!this._attemptStrCaseInsensitive(lowercaseTagName)) return false;
this._attemptCharCodeUntilFn(isNotWhitespace);
return this._attemptCharCode(chars.$GT);
});
this._beginToken(TokenType.TAG_CLOSE, textToken.sourceSpan.end);
this._endToken([null, lowercaseTagName]);
}
private _consumeTagOpenStart(start: ParseLocation) {
this._beginToken(TokenType.TAG_OPEN_START, start);
const parts = this._consumePrefixAndName();
this._endToken(parts);
}
private _consumeAttributeName() {
this._beginToken(TokenType.ATTR_NAME);
const prefixAndName = this._consumePrefixAndName();
this._endToken(prefixAndName);
}
private _consumeAttributeValue() {
this._beginToken(TokenType.ATTR_VALUE);
let value: string;
if (this._peek === chars.$SQ || this._peek === chars.$DQ) {
const quoteChar = this._peek;
this._advance();
const parts: string[] = [];
while (this._peek !== quoteChar) {
parts.push(this._readChar(true));
}
value = parts.join('');
this._advance();
} else {
const valueStart = this._index;
this._requireCharCodeUntilFn(isNameEnd, 1);
value = this._input.substring(valueStart, this._index);
}
this._endToken([this._processCarriageReturns(value)]);
}
private _consumeTagOpenEnd() {
const tokenType =
this._attemptCharCode(chars.$SLASH) ? TokenType.TAG_OPEN_END_VOID : TokenType.TAG_OPEN_END;
this._beginToken(tokenType);
this._requireCharCode(chars.$GT);
this._endToken([]);
}
private _consumeTagClose(start: ParseLocation) {
this._beginToken(TokenType.TAG_CLOSE, start);
this._attemptCharCodeUntilFn(isNotWhitespace);
const prefixAndName = this._consumePrefixAndName();
this._attemptCharCodeUntilFn(isNotWhitespace);
this._requireCharCode(chars.$GT);
this._endToken(prefixAndName);
}
private _consumeExpansionFormStart() {
this._beginToken(TokenType.EXPANSION_FORM_START, this._getLocation());
this._requireCharCode(chars.$LBRACE);
this._endToken([]);
this._expansionCaseStack.push(TokenType.EXPANSION_FORM_START);
this._beginToken(TokenType.RAW_TEXT, this._getLocation());
const condition = this._readUntil(chars.$COMMA);
this._endToken([condition], this._getLocation());
this._requireCharCode(chars.$COMMA);
this._attemptCharCodeUntilFn(isNotWhitespace);
this._beginToken(TokenType.RAW_TEXT, this._getLocation());
const type = this._readUntil(chars.$COMMA);
this._endToken([type], this._getLocation());
this._requireCharCode(chars.$COMMA);
this._attemptCharCodeUntilFn(isNotWhitespace);
}
private _consumeExpansionCaseStart() {
this._beginToken(TokenType.EXPANSION_CASE_VALUE, this._getLocation());
const value = this._readUntil(chars.$LBRACE).trim();
this._endToken([value], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this._beginToken(TokenType.EXPANSION_CASE_EXP_START, this._getLocation());
this._requireCharCode(chars.$LBRACE);
this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this._expansionCaseStack.push(TokenType.EXPANSION_CASE_EXP_START);
}
private _consumeExpansionCaseEnd() {
this._beginToken(TokenType.EXPANSION_CASE_EXP_END, this._getLocation());
this._requireCharCode(chars.$RBRACE);
this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace);
this._expansionCaseStack.pop();
}
private _consumeExpansionFormEnd() {
this._beginToken(TokenType.EXPANSION_FORM_END, this._getLocation());
this._requireCharCode(chars.$RBRACE);
this._endToken([]);
this._expansionCaseStack.pop();
}
private _consumeText() {
const start = this._getLocation();
this._beginToken(TokenType.TEXT, start);
const parts: string[] = [];
do {
if (this._interpolationConfig && this._attemptStr(this._interpolationConfig.start)) {
parts.push(this._interpolationConfig.start);
this._inInterpolation = true;
} else if (
this._interpolationConfig && this._inInterpolation &&
this._attemptStr(this._interpolationConfig.end)) {
parts.push(this._interpolationConfig.end);
this._inInterpolation = false;
} else {
parts.push(this._readChar(true));
}
} while (!this._isTextEnd());
this._endToken([this._processCarriageReturns(parts.join(''))]);
}
private _isTextEnd(): boolean {
if (this._peek === chars.$LT || this._peek === chars.$EOF) {
return true;
}
if (this._tokenizeIcu && !this._inInterpolation) {
if (isExpansionFormStart(this._input, this._index, this._interpolationConfig)) {
// start of an expansion form
return true;
}
if (this._peek === chars.$RBRACE && this._isInExpansionCase()) {
// end of and expansion case
return true;
}
}
return false;
}
private _savePosition(): [number, number, number, number, number] {
return [this._peek, this._index, this._column, this._line, this.tokens.length];
}
private _readUntil(char: number): string {
const start = this._index;
this._attemptUntilChar(char);
return this._input.substring(start, this._index);
}
private _restorePosition(position: [number, number, number, number, number]): void {
this._peek = position[0];
this._index = position[1];
this._column = position[2];
this._line = position[3];
const nbTokens = position[4];
if (nbTokens < this.tokens.length) {
// remove any extra tokens
this.tokens = this.tokens.slice(0, nbTokens);
}
}
private _isInExpansionCase(): boolean {
return this._expansionCaseStack.length > 0 &&
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
TokenType.EXPANSION_CASE_EXP_START;
}
private _isInExpansionForm(): boolean {
return this._expansionCaseStack.length > 0 &&
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
TokenType.EXPANSION_FORM_START;
}
}
function isNotWhitespace(code: number): boolean {
return !chars.isWhitespace(code) || code === chars.$EOF;
}
function isNameEnd(code: number): boolean {
return chars.isWhitespace(code) || code === chars.$GT || code === chars.$SLASH ||
code === chars.$SQ || code === chars.$DQ || code === chars.$EQ;
}
function isPrefixEnd(code: number): boolean {
return (code < chars.$a || chars.$z < code) && (code < chars.$A || chars.$Z < code) &&
(code < chars.$0 || code > chars.$9);
}
function isDigitEntityEnd(code: number): boolean {
return code == chars.$SEMICOLON || code == chars.$EOF || !chars.isAsciiHexDigit(code);
}
function isNamedEntityEnd(code: number): boolean {
return code == chars.$SEMICOLON || code == chars.$EOF || !chars.isAsciiLetter(code);
}
function isExpansionFormStart(
input: string, offset: number, interpolationConfig: InterpolationConfig): boolean {
const isInterpolationStart =
interpolationConfig ? input.indexOf(interpolationConfig.start, offset) == offset : false;
return input.charCodeAt(offset) == chars.$LBRACE && !isInterpolationStart;
}
function isExpansionCaseStart(peek: number): boolean {
return peek === chars.$EQ || chars.isAsciiLetter(peek);
}
function compareCharCodeCaseInsensitive(code1: number, code2: number): boolean {
return toUpperCaseCharCode(code1) == toUpperCaseCharCode(code2);
}
function toUpperCaseCharCode(code: number): number {
return code >= chars.$a && code <= chars.$z ? code - chars.$a + chars.$A : code;
}
function mergeTextTokens(srcTokens: Token[]): Token[] {
const dstTokens: Token[] = [];
let lastDstToken: Token;
for (let i = 0; i < srcTokens.length; i++) {
const token = srcTokens[i];
if (lastDstToken && lastDstToken.type == TokenType.TEXT && token.type == TokenType.TEXT) {
lastDstToken.parts[0] += token.parts[0];
lastDstToken.sourceSpan.end = token.sourceSpan.end;
} else {
lastDstToken = token;
dstTokens.push(lastDstToken);
}
}
return dstTokens;
}

View File

@ -0,0 +1,414 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseError, ParseSourceSpan} from '../parse_util';
import * as html from './ast';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
import * as lex from './lexer';
import {TagDefinition, getNsPrefix, mergeNsAndName} from './tags';
export class TreeError extends ParseError {
static create(elementName: string, span: ParseSourceSpan, msg: string): TreeError {
return new TreeError(elementName, span, msg);
}
constructor(public elementName: string, span: ParseSourceSpan, msg: string) { super(span, msg); }
}
export class ParseTreeResult {
constructor(public rootNodes: html.Node[], public errors: ParseError[]) {}
}
export class Parser {
constructor(public getTagDefinition: (tagName: string) => TagDefinition) {}
parse(
source: string, url: string, parseExpansionForms: boolean = false,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ParseTreeResult {
const tokensAndErrors =
lex.tokenize(source, url, this.getTagDefinition, parseExpansionForms, interpolationConfig);
const treeAndErrors = new _TreeBuilder(tokensAndErrors.tokens, this.getTagDefinition).build();
return new ParseTreeResult(
treeAndErrors.rootNodes,
(<ParseError[]>tokensAndErrors.errors).concat(treeAndErrors.errors));
}
}
class _TreeBuilder {
private _index: number = -1;
private _peek: lex.Token;
private _rootNodes: html.Node[] = [];
private _errors: TreeError[] = [];
private _elementStack: html.Element[] = [];
constructor(
private tokens: lex.Token[], private getTagDefinition: (tagName: string) => TagDefinition) {
this._advance();
}
build(): ParseTreeResult {
while (this._peek.type !== lex.TokenType.EOF) {
if (this._peek.type === lex.TokenType.TAG_OPEN_START) {
this._consumeStartTag(this._advance());
} else if (this._peek.type === lex.TokenType.TAG_CLOSE) {
this._consumeEndTag(this._advance());
} else if (this._peek.type === lex.TokenType.CDATA_START) {
this._closeVoidElement();
this._consumeCdata(this._advance());
} else if (this._peek.type === lex.TokenType.COMMENT_START) {
this._closeVoidElement();
this._consumeComment(this._advance());
} else if (
this._peek.type === lex.TokenType.TEXT || this._peek.type === lex.TokenType.RAW_TEXT ||
this._peek.type === lex.TokenType.ESCAPABLE_RAW_TEXT) {
this._closeVoidElement();
this._consumeText(this._advance());
} else if (this._peek.type === lex.TokenType.EXPANSION_FORM_START) {
this._consumeExpansion(this._advance());
} else {
// Skip all other tokens...
this._advance();
}
}
return new ParseTreeResult(this._rootNodes, this._errors);
}
private _advance(): lex.Token {
const prev = this._peek;
if (this._index < this.tokens.length - 1) {
// Note: there is always an EOF token at the end
this._index++;
}
this._peek = this.tokens[this._index];
return prev;
}
private _advanceIf(type: lex.TokenType): lex.Token {
if (this._peek.type === type) {
return this._advance();
}
return null;
}
private _consumeCdata(startToken: lex.Token) {
this._consumeText(this._advance());
this._advanceIf(lex.TokenType.CDATA_END);
}
private _consumeComment(token: lex.Token) {
const text = this._advanceIf(lex.TokenType.RAW_TEXT);
this._advanceIf(lex.TokenType.COMMENT_END);
const value = text != null ? text.parts[0].trim() : null;
this._addToParent(new html.Comment(value, token.sourceSpan));
}
private _consumeExpansion(token: lex.Token) {
const switchValue = this._advance();
const type = this._advance();
const cases: html.ExpansionCase[] = [];
// read =
while (this._peek.type === lex.TokenType.EXPANSION_CASE_VALUE) {
const expCase = this._parseExpansionCase();
if (!expCase) return; // error
cases.push(expCase);
}
// read the final }
if (this._peek.type !== lex.TokenType.EXPANSION_FORM_END) {
this._errors.push(
TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '}'.`));
return;
}
const sourceSpan = new ParseSourceSpan(token.sourceSpan.start, this._peek.sourceSpan.end);
this._addToParent(new html.Expansion(
switchValue.parts[0], type.parts[0], cases, sourceSpan, switchValue.sourceSpan));
this._advance();
}
private _parseExpansionCase(): html.ExpansionCase {
const value = this._advance();
// read {
if (this._peek.type !== lex.TokenType.EXPANSION_CASE_EXP_START) {
this._errors.push(
TreeError.create(null, this._peek.sourceSpan, `Invalid ICU message. Missing '{'.`));
return null;
}
// read until }
const start = this._advance();
const exp = this._collectExpansionExpTokens(start);
if (!exp) return null;
const end = this._advance();
exp.push(new lex.Token(lex.TokenType.EOF, [], end.sourceSpan));
// parse everything in between { and }
const parsedExp = new _TreeBuilder(exp, this.getTagDefinition).build();
if (parsedExp.errors.length > 0) {
this._errors = this._errors.concat(<TreeError[]>parsedExp.errors);
return null;
}
const sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end);
const expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end);
return new html.ExpansionCase(
value.parts[0], parsedExp.rootNodes, sourceSpan, value.sourceSpan, expSourceSpan);
}
private _collectExpansionExpTokens(start: lex.Token): lex.Token[] {
const exp: lex.Token[] = [];
const expansionFormStack = [lex.TokenType.EXPANSION_CASE_EXP_START];
while (true) {
if (this._peek.type === lex.TokenType.EXPANSION_FORM_START ||
this._peek.type === lex.TokenType.EXPANSION_CASE_EXP_START) {
expansionFormStack.push(this._peek.type);
}
if (this._peek.type === lex.TokenType.EXPANSION_CASE_EXP_END) {
if (lastOnStack(expansionFormStack, lex.TokenType.EXPANSION_CASE_EXP_START)) {
expansionFormStack.pop();
if (expansionFormStack.length == 0) return exp;
} else {
this._errors.push(
TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
}
if (this._peek.type === lex.TokenType.EXPANSION_FORM_END) {
if (lastOnStack(expansionFormStack, lex.TokenType.EXPANSION_FORM_START)) {
expansionFormStack.pop();
} else {
this._errors.push(
TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
}
if (this._peek.type === lex.TokenType.EOF) {
this._errors.push(
TreeError.create(null, start.sourceSpan, `Invalid ICU message. Missing '}'.`));
return null;
}
exp.push(this._advance());
}
}
private _consumeText(token: lex.Token) {
let text = token.parts[0];
if (text.length > 0 && text[0] == '\n') {
const parent = this._getParentElement();
if (parent != null && parent.children.length == 0 &&
this.getTagDefinition(parent.name).ignoreFirstLf) {
text = text.substring(1);
}
}
if (text.length > 0) {
this._addToParent(new html.Text(text, token.sourceSpan));
}
}
private _closeVoidElement(): void {
if (this._elementStack.length > 0) {
const el = this._elementStack[this._elementStack.length - 1];
if (this.getTagDefinition(el.name).isVoid) {
this._elementStack.pop();
}
}
}
private _consumeStartTag(startTagToken: lex.Token) {
const prefix = startTagToken.parts[0];
const name = startTagToken.parts[1];
const attrs: html.Attribute[] = [];
while (this._peek.type === lex.TokenType.ATTR_NAME) {
attrs.push(this._consumeAttr(this._advance()));
}
const fullName = this._getElementFullName(prefix, name, this._getParentElement());
let selfClosing = false;
// Note: There could have been a tokenizer error
// so that we don't get a token for the end tag...
if (this._peek.type === lex.TokenType.TAG_OPEN_END_VOID) {
this._advance();
selfClosing = true;
const tagDef = this.getTagDefinition(fullName);
if (!(tagDef.canSelfClose || getNsPrefix(fullName) !== null || tagDef.isVoid)) {
this._errors.push(TreeError.create(
fullName, startTagToken.sourceSpan,
`Only void and foreign elements can be self closed "${startTagToken.parts[1]}"`));
}
} else if (this._peek.type === lex.TokenType.TAG_OPEN_END) {
this._advance();
selfClosing = false;
}
const end = this._peek.sourceSpan.start;
const span = new ParseSourceSpan(startTagToken.sourceSpan.start, end);
const el = new html.Element(fullName, attrs, [], span, span, null);
this._pushElement(el);
if (selfClosing) {
this._popElement(fullName);
el.endSourceSpan = span;
}
}
private _pushElement(el: html.Element) {
if (this._elementStack.length > 0) {
const parentEl = this._elementStack[this._elementStack.length - 1];
if (this.getTagDefinition(parentEl.name).isClosedByChild(el.name)) {
this._elementStack.pop();
}
}
const tagDef = this.getTagDefinition(el.name);
const {parent, container} = this._getParentElementSkippingContainers();
if (parent && tagDef.requireExtraParent(parent.name)) {
const newParent = new html.Element(
tagDef.parentToAdd, [], [], el.sourceSpan, el.startSourceSpan, el.endSourceSpan);
this._insertBeforeContainer(parent, container, newParent);
}
this._addToParent(el);
this._elementStack.push(el);
}
private _consumeEndTag(endTagToken: lex.Token) {
const fullName = this._getElementFullName(
endTagToken.parts[0], endTagToken.parts[1], this._getParentElement());
if (this._getParentElement()) {
this._getParentElement().endSourceSpan = endTagToken.sourceSpan;
}
if (this.getTagDefinition(fullName).isVoid) {
this._errors.push(TreeError.create(
fullName, endTagToken.sourceSpan,
`Void elements do not have end tags "${endTagToken.parts[1]}"`));
} else if (!this._popElement(fullName)) {
this._errors.push(TreeError.create(
fullName, endTagToken.sourceSpan, `Unexpected closing tag "${endTagToken.parts[1]}"`));
}
}
private _popElement(fullName: string): boolean {
for (let stackIndex = this._elementStack.length - 1; stackIndex >= 0; stackIndex--) {
const el = this._elementStack[stackIndex];
if (el.name == fullName) {
this._elementStack.splice(stackIndex, this._elementStack.length - stackIndex);
return true;
}
if (!this.getTagDefinition(el.name).closedByParent) {
return false;
}
}
return false;
}
private _consumeAttr(attrName: lex.Token): html.Attribute {
const fullName = mergeNsAndName(attrName.parts[0], attrName.parts[1]);
let end = attrName.sourceSpan.end;
let value = '';
let valueSpan: ParseSourceSpan;
if (this._peek.type === lex.TokenType.ATTR_VALUE) {
const valueToken = this._advance();
value = valueToken.parts[0];
end = valueToken.sourceSpan.end;
valueSpan = valueToken.sourceSpan;
}
return new html.Attribute(
fullName, value, new ParseSourceSpan(attrName.sourceSpan.start, end), valueSpan);
}
private _getParentElement(): html.Element {
return this._elementStack.length > 0 ? this._elementStack[this._elementStack.length - 1] : null;
}
/**
* Returns the parent in the DOM and the container.
*
* `<ng-container>` elements are skipped as they are not rendered as DOM element.
*/
private _getParentElementSkippingContainers(): {parent: html.Element, container: html.Element} {
let container: html.Element = null;
for (let i = this._elementStack.length - 1; i >= 0; i--) {
if (this._elementStack[i].name !== 'ng-container') {
return {parent: this._elementStack[i], container};
}
container = this._elementStack[i];
}
return {parent: this._elementStack[this._elementStack.length - 1], container};
}
private _addToParent(node: html.Node) {
const parent = this._getParentElement();
if (parent != null) {
parent.children.push(node);
} else {
this._rootNodes.push(node);
}
}
/**
* Insert a node between the parent and the container.
* When no container is given, the node is appended as a child of the parent.
* Also updates the element stack accordingly.
*
* @internal
*/
private _insertBeforeContainer(
parent: html.Element, container: html.Element, node: html.Element) {
if (!container) {
this._addToParent(node);
this._elementStack.push(node);
} else {
if (parent) {
// replace the container with the new node in the children
const index = parent.children.indexOf(container);
parent.children[index] = node;
} else {
this._rootNodes.push(node);
}
node.children.push(container);
this._elementStack.splice(this._elementStack.indexOf(container), 0, node);
}
}
private _getElementFullName(prefix: string, localName: string, parentElement: html.Element):
string {
if (prefix == null) {
prefix = this.getTagDefinition(localName).implicitNamespacePrefix;
if (prefix == null && parentElement != null) {
prefix = getNsPrefix(parentElement.name);
}
}
return mergeNsAndName(prefix, localName);
}
}
function lastOnStack(stack: any[], element: any): boolean {
return stack.length > 0 && stack[stack.length - 1] === element;
}

View File

@ -0,0 +1,310 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export enum TagContentType {
RAW_TEXT,
ESCAPABLE_RAW_TEXT,
PARSABLE_DATA
}
// TODO(vicb): read-only when TS supports it
export interface TagDefinition {
closedByParent: boolean;
requiredParents: {[key: string]: boolean};
parentToAdd: string;
implicitNamespacePrefix: string;
contentType: TagContentType;
isVoid: boolean;
ignoreFirstLf: boolean;
canSelfClose: boolean;
requireExtraParent(currentParent: string): boolean;
isClosedByChild(name: string): boolean;
}
export function splitNsName(elementName: string): [string, string] {
if (elementName[0] != ':') {
return [null, elementName];
}
const colonIndex = elementName.indexOf(':', 1);
if (colonIndex == -1) {
throw new Error(`Unsupported format "${elementName}" expecting ":namespace:name"`);
}
return [elementName.slice(1, colonIndex), elementName.slice(colonIndex + 1)];
}
export function getNsPrefix(fullName: string): string {
return fullName === null ? null : splitNsName(fullName)[0];
}
export function mergeNsAndName(prefix: string, localName: string): string {
return prefix ? `:${prefix}:${localName}` : localName;
}
// see http://www.w3.org/TR/html51/syntax.html#named-character-references
// see https://html.spec.whatwg.org/multipage/entities.json
// This list is not exhaustive to keep the compiler footprint low.
// The `&#123;` / `&#x1ab;` syntax should be used when the named character reference does not exist.
export const NAMED_ENTITIES: {[k: string]: string} = {
'Aacute': '\u00C1',
'aacute': '\u00E1',
'Acirc': '\u00C2',
'acirc': '\u00E2',
'acute': '\u00B4',
'AElig': '\u00C6',
'aelig': '\u00E6',
'Agrave': '\u00C0',
'agrave': '\u00E0',
'alefsym': '\u2135',
'Alpha': '\u0391',
'alpha': '\u03B1',
'amp': '&',
'and': '\u2227',
'ang': '\u2220',
'apos': '\u0027',
'Aring': '\u00C5',
'aring': '\u00E5',
'asymp': '\u2248',
'Atilde': '\u00C3',
'atilde': '\u00E3',
'Auml': '\u00C4',
'auml': '\u00E4',
'bdquo': '\u201E',
'Beta': '\u0392',
'beta': '\u03B2',
'brvbar': '\u00A6',
'bull': '\u2022',
'cap': '\u2229',
'Ccedil': '\u00C7',
'ccedil': '\u00E7',
'cedil': '\u00B8',
'cent': '\u00A2',
'Chi': '\u03A7',
'chi': '\u03C7',
'circ': '\u02C6',
'clubs': '\u2663',
'cong': '\u2245',
'copy': '\u00A9',
'crarr': '\u21B5',
'cup': '\u222A',
'curren': '\u00A4',
'dagger': '\u2020',
'Dagger': '\u2021',
'darr': '\u2193',
'dArr': '\u21D3',
'deg': '\u00B0',
'Delta': '\u0394',
'delta': '\u03B4',
'diams': '\u2666',
'divide': '\u00F7',
'Eacute': '\u00C9',
'eacute': '\u00E9',
'Ecirc': '\u00CA',
'ecirc': '\u00EA',
'Egrave': '\u00C8',
'egrave': '\u00E8',
'empty': '\u2205',
'emsp': '\u2003',
'ensp': '\u2002',
'Epsilon': '\u0395',
'epsilon': '\u03B5',
'equiv': '\u2261',
'Eta': '\u0397',
'eta': '\u03B7',
'ETH': '\u00D0',
'eth': '\u00F0',
'Euml': '\u00CB',
'euml': '\u00EB',
'euro': '\u20AC',
'exist': '\u2203',
'fnof': '\u0192',
'forall': '\u2200',
'frac12': '\u00BD',
'frac14': '\u00BC',
'frac34': '\u00BE',
'frasl': '\u2044',
'Gamma': '\u0393',
'gamma': '\u03B3',
'ge': '\u2265',
'gt': '>',
'harr': '\u2194',
'hArr': '\u21D4',
'hearts': '\u2665',
'hellip': '\u2026',
'Iacute': '\u00CD',
'iacute': '\u00ED',
'Icirc': '\u00CE',
'icirc': '\u00EE',
'iexcl': '\u00A1',
'Igrave': '\u00CC',
'igrave': '\u00EC',
'image': '\u2111',
'infin': '\u221E',
'int': '\u222B',
'Iota': '\u0399',
'iota': '\u03B9',
'iquest': '\u00BF',
'isin': '\u2208',
'Iuml': '\u00CF',
'iuml': '\u00EF',
'Kappa': '\u039A',
'kappa': '\u03BA',
'Lambda': '\u039B',
'lambda': '\u03BB',
'lang': '\u27E8',
'laquo': '\u00AB',
'larr': '\u2190',
'lArr': '\u21D0',
'lceil': '\u2308',
'ldquo': '\u201C',
'le': '\u2264',
'lfloor': '\u230A',
'lowast': '\u2217',
'loz': '\u25CA',
'lrm': '\u200E',
'lsaquo': '\u2039',
'lsquo': '\u2018',
'lt': '<',
'macr': '\u00AF',
'mdash': '\u2014',
'micro': '\u00B5',
'middot': '\u00B7',
'minus': '\u2212',
'Mu': '\u039C',
'mu': '\u03BC',
'nabla': '\u2207',
'nbsp': '\u00A0',
'ndash': '\u2013',
'ne': '\u2260',
'ni': '\u220B',
'not': '\u00AC',
'notin': '\u2209',
'nsub': '\u2284',
'Ntilde': '\u00D1',
'ntilde': '\u00F1',
'Nu': '\u039D',
'nu': '\u03BD',
'Oacute': '\u00D3',
'oacute': '\u00F3',
'Ocirc': '\u00D4',
'ocirc': '\u00F4',
'OElig': '\u0152',
'oelig': '\u0153',
'Ograve': '\u00D2',
'ograve': '\u00F2',
'oline': '\u203E',
'Omega': '\u03A9',
'omega': '\u03C9',
'Omicron': '\u039F',
'omicron': '\u03BF',
'oplus': '\u2295',
'or': '\u2228',
'ordf': '\u00AA',
'ordm': '\u00BA',
'Oslash': '\u00D8',
'oslash': '\u00F8',
'Otilde': '\u00D5',
'otilde': '\u00F5',
'otimes': '\u2297',
'Ouml': '\u00D6',
'ouml': '\u00F6',
'para': '\u00B6',
'permil': '\u2030',
'perp': '\u22A5',
'Phi': '\u03A6',
'phi': '\u03C6',
'Pi': '\u03A0',
'pi': '\u03C0',
'piv': '\u03D6',
'plusmn': '\u00B1',
'pound': '\u00A3',
'prime': '\u2032',
'Prime': '\u2033',
'prod': '\u220F',
'prop': '\u221D',
'Psi': '\u03A8',
'psi': '\u03C8',
'quot': '\u0022',
'radic': '\u221A',
'rang': '\u27E9',
'raquo': '\u00BB',
'rarr': '\u2192',
'rArr': '\u21D2',
'rceil': '\u2309',
'rdquo': '\u201D',
'real': '\u211C',
'reg': '\u00AE',
'rfloor': '\u230B',
'Rho': '\u03A1',
'rho': '\u03C1',
'rlm': '\u200F',
'rsaquo': '\u203A',
'rsquo': '\u2019',
'sbquo': '\u201A',
'Scaron': '\u0160',
'scaron': '\u0161',
'sdot': '\u22C5',
'sect': '\u00A7',
'shy': '\u00AD',
'Sigma': '\u03A3',
'sigma': '\u03C3',
'sigmaf': '\u03C2',
'sim': '\u223C',
'spades': '\u2660',
'sub': '\u2282',
'sube': '\u2286',
'sum': '\u2211',
'sup': '\u2283',
'sup1': '\u00B9',
'sup2': '\u00B2',
'sup3': '\u00B3',
'supe': '\u2287',
'szlig': '\u00DF',
'Tau': '\u03A4',
'tau': '\u03C4',
'there4': '\u2234',
'Theta': '\u0398',
'theta': '\u03B8',
'thetasym': '\u03D1',
'thinsp': '\u2009',
'THORN': '\u00DE',
'thorn': '\u00FE',
'tilde': '\u02DC',
'times': '\u00D7',
'trade': '\u2122',
'Uacute': '\u00DA',
'uacute': '\u00FA',
'uarr': '\u2191',
'uArr': '\u21D1',
'Ucirc': '\u00DB',
'ucirc': '\u00FB',
'Ugrave': '\u00D9',
'ugrave': '\u00F9',
'uml': '\u00A8',
'upsih': '\u03D2',
'Upsilon': '\u03A5',
'upsilon': '\u03C5',
'Uuml': '\u00DC',
'uuml': '\u00FC',
'weierp': '\u2118',
'Xi': '\u039E',
'xi': '\u03BE',
'Yacute': '\u00DD',
'yacute': '\u00FD',
'yen': '\u00A5',
'yuml': '\u00FF',
'Yuml': '\u0178',
'Zeta': '\u0396',
'zeta': '\u03B6',
'zwj': '\u200D',
'zwnj': '\u200C',
};

View File

@ -0,0 +1,20 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseTreeResult, Parser} from './parser';
import {getXmlTagDefinition} from './xml_tags';
export {ParseTreeResult, TreeError} from './parser';
export class XmlParser extends Parser {
constructor() { super(getXmlTagDefinition); }
parse(source: string, url: string, parseExpansionForms: boolean = false): ParseTreeResult {
return super.parse(source, url, parseExpansionForms, null);
}
}

View File

@ -0,0 +1,30 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {TagContentType, TagDefinition} from './tags';
export class XmlTagDefinition implements TagDefinition {
closedByParent: boolean = false;
requiredParents: {[key: string]: boolean};
parentToAdd: string;
implicitNamespacePrefix: string;
contentType: TagContentType = TagContentType.PARSABLE_DATA;
isVoid: boolean = false;
ignoreFirstLf: boolean = false;
canSelfClose: boolean = true;
requireExtraParent(currentParent: string): boolean { return false; }
isClosedByChild(name: string): boolean { return false; }
}
const _TAG_DEFINITION = new XmlTagDefinition();
export function getXmlTagDefinition(tagName: string): XmlTagDefinition {
return _TAG_DEFINITION;
}

View File

@ -0,0 +1,255 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵLifecycleHooks} from '@angular/core';
import {CompileDiDependencyMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileProviderMetadata, CompileTokenMetadata, identifierModuleUrl, identifierName, tokenName, tokenReference} from './compile_metadata';
import {Identifiers, createIdentifier, resolveIdentifier} from './identifiers';
import {CompilerInjectable} from './injectable';
import {ClassBuilder, createClassStmt} from './output/class_builder';
import * as o from './output/output_ast';
import {convertValueToOutputAst} from './output/value_util';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from './parse_util';
import {NgModuleProviderAnalyzer} from './provider_analyzer';
import {ProviderAst} from './template_parser/template_ast';
/**
* This is currently not read, but will probably be used in the future.
* We keep it as we already pass it through all the rigth places...
*/
export class ComponentFactoryDependency {
constructor(public compType: any) {}
}
export class NgModuleCompileResult {
constructor(
public statements: o.Statement[], public ngModuleFactoryVar: string,
public dependencies: ComponentFactoryDependency[]) {}
}
@CompilerInjectable()
export class NgModuleCompiler {
compile(ngModuleMeta: CompileNgModuleMetadata, extraProviders: CompileProviderMetadata[]):
NgModuleCompileResult {
const moduleUrl = identifierModuleUrl(ngModuleMeta.type);
const sourceFileName = moduleUrl != null ?
`in NgModule ${identifierName(ngModuleMeta.type)} in ${moduleUrl}` :
`in NgModule ${identifierName(ngModuleMeta.type)}`;
const sourceFile = new ParseSourceFile('', sourceFileName);
const sourceSpan = new ParseSourceSpan(
new ParseLocation(sourceFile, null, null, null),
new ParseLocation(sourceFile, null, null, null));
const deps: ComponentFactoryDependency[] = [];
const bootstrapComponentFactories: CompileIdentifierMetadata[] = [];
const entryComponentFactories =
ngModuleMeta.transitiveModule.entryComponents.map((entryComponent) => {
if (ngModuleMeta.bootstrapComponents.some(
(id) => id.reference === entryComponent.componentType)) {
bootstrapComponentFactories.push({reference: entryComponent.componentFactory});
}
deps.push(new ComponentFactoryDependency(entryComponent.componentType));
return {reference: entryComponent.componentFactory};
});
const builder = new _InjectorBuilder(
ngModuleMeta, entryComponentFactories, bootstrapComponentFactories, sourceSpan);
const providerParser = new NgModuleProviderAnalyzer(ngModuleMeta, extraProviders, sourceSpan);
providerParser.parse().forEach((provider) => builder.addProvider(provider));
const injectorClass = builder.build();
const ngModuleFactoryVar = `${identifierName(ngModuleMeta.type)}NgFactory`;
const ngModuleFactoryStmt =
o.variable(ngModuleFactoryVar)
.set(o.importExpr(createIdentifier(Identifiers.NgModuleFactory))
.instantiate(
[o.variable(injectorClass.name), o.importExpr(ngModuleMeta.type)],
o.importType(
createIdentifier(Identifiers.NgModuleFactory),
[o.importType(ngModuleMeta.type)], [o.TypeModifier.Const])))
.toDeclStmt(null, [o.StmtModifier.Final]);
const stmts: o.Statement[] = [injectorClass, ngModuleFactoryStmt];
if (ngModuleMeta.id) {
const registerFactoryStmt =
o.importExpr(createIdentifier(Identifiers.RegisterModuleFactoryFn))
.callFn([o.literal(ngModuleMeta.id), o.variable(ngModuleFactoryVar)])
.toStmt();
stmts.push(registerFactoryStmt);
}
return new NgModuleCompileResult(stmts, ngModuleFactoryVar, deps);
}
}
class _InjectorBuilder implements ClassBuilder {
fields: o.ClassField[] = [];
getters: o.ClassGetter[] = [];
methods: o.ClassMethod[] = [];
ctorStmts: o.Statement[] = [];
private _tokens: CompileTokenMetadata[] = [];
private _instances = new Map<any, o.Expression>();
private _createStmts: o.Statement[] = [];
private _destroyStmts: o.Statement[] = [];
constructor(
private _ngModuleMeta: CompileNgModuleMetadata,
private _entryComponentFactories: CompileIdentifierMetadata[],
private _bootstrapComponentFactories: CompileIdentifierMetadata[],
private _sourceSpan: ParseSourceSpan) {}
addProvider(resolvedProvider: ProviderAst) {
const providerValueExpressions =
resolvedProvider.providers.map((provider) => this._getProviderValue(provider));
const propName = `_${tokenName(resolvedProvider.token)}_${this._instances.size}`;
const instance = this._createProviderProperty(
propName, resolvedProvider, providerValueExpressions, resolvedProvider.multiProvider,
resolvedProvider.eager);
if (resolvedProvider.lifecycleHooks.indexOf(ɵLifecycleHooks.OnDestroy) !== -1) {
this._destroyStmts.push(instance.callMethod('ngOnDestroy', []).toStmt());
}
this._tokens.push(resolvedProvider.token);
this._instances.set(tokenReference(resolvedProvider.token), instance);
}
build(): o.ClassStmt {
const getMethodStmts: o.Statement[] = this._tokens.map((token) => {
const providerExpr = this._instances.get(tokenReference(token));
return new o.IfStmt(
InjectMethodVars.token.identical(createDiTokenExpression(token)),
[new o.ReturnStatement(providerExpr)]);
});
const methods = [
new o.ClassMethod(
'createInternal', [], this._createStmts.concat(new o.ReturnStatement(
this._instances.get(this._ngModuleMeta.type.reference))),
o.importType(this._ngModuleMeta.type)),
new o.ClassMethod(
'getInternal',
[
new o.FnParam(InjectMethodVars.token.name, o.DYNAMIC_TYPE),
new o.FnParam(InjectMethodVars.notFoundResult.name, o.DYNAMIC_TYPE)
],
getMethodStmts.concat([new o.ReturnStatement(InjectMethodVars.notFoundResult)]),
o.DYNAMIC_TYPE),
new o.ClassMethod('destroyInternal', [], this._destroyStmts),
];
const parentArgs = [
o.variable(InjectorProps.parent.name),
o.literalArr(
this._entryComponentFactories.map((componentFactory) => o.importExpr(componentFactory))),
o.literalArr(this._bootstrapComponentFactories.map(
(componentFactory) => o.importExpr(componentFactory)))
];
const injClassName = `${identifierName(this._ngModuleMeta.type)}Injector`;
return createClassStmt({
name: injClassName,
ctorParams: [new o.FnParam(
InjectorProps.parent.name, o.importType(createIdentifier(Identifiers.Injector)))],
parent: o.importExpr(
createIdentifier(Identifiers.NgModuleInjector), [o.importType(this._ngModuleMeta.type)]),
parentArgs: parentArgs,
builders: [{methods}, this]
});
}
private _getProviderValue(provider: CompileProviderMetadata): o.Expression {
let result: o.Expression;
if (provider.useExisting != null) {
result = this._getDependency({token: provider.useExisting});
} else if (provider.useFactory != null) {
const deps = provider.deps || provider.useFactory.diDeps;
const depsExpr = deps.map((dep) => this._getDependency(dep));
result = o.importExpr(provider.useFactory).callFn(depsExpr);
} else if (provider.useClass != null) {
const deps = provider.deps || provider.useClass.diDeps;
const depsExpr = deps.map((dep) => this._getDependency(dep));
result =
o.importExpr(provider.useClass).instantiate(depsExpr, o.importType(provider.useClass));
} else {
result = convertValueToOutputAst(provider.useValue);
}
return result;
}
private _createProviderProperty(
propName: string, provider: ProviderAst, providerValueExpressions: o.Expression[],
isMulti: boolean, isEager: boolean): o.Expression {
let resolvedProviderValueExpr: o.Expression;
let type: o.Type;
if (isMulti) {
resolvedProviderValueExpr = o.literalArr(providerValueExpressions);
type = new o.ArrayType(o.DYNAMIC_TYPE);
} else {
resolvedProviderValueExpr = providerValueExpressions[0];
type = providerValueExpressions[0].type;
}
if (!type) {
type = o.DYNAMIC_TYPE;
}
if (isEager) {
this.fields.push(new o.ClassField(propName, type));
this._createStmts.push(o.THIS_EXPR.prop(propName).set(resolvedProviderValueExpr).toStmt());
} else {
const internalField = `_${propName}`;
this.fields.push(new o.ClassField(internalField, type));
// Note: Equals is important for JS so that it also checks the undefined case!
const getterStmts = [
new o.IfStmt(
o.THIS_EXPR.prop(internalField).isBlank(),
[o.THIS_EXPR.prop(internalField).set(resolvedProviderValueExpr).toStmt()]),
new o.ReturnStatement(o.THIS_EXPR.prop(internalField))
];
this.getters.push(new o.ClassGetter(propName, getterStmts, type));
}
return o.THIS_EXPR.prop(propName);
}
private _getDependency(dep: CompileDiDependencyMetadata): o.Expression {
let result: o.Expression = null;
if (dep.isValue) {
result = o.literal(dep.value);
}
if (!dep.isSkipSelf) {
if (dep.token &&
(tokenReference(dep.token) === resolveIdentifier(Identifiers.Injector) ||
tokenReference(dep.token) === resolveIdentifier(Identifiers.ComponentFactoryResolver))) {
result = o.THIS_EXPR;
}
if (!result) {
result = this._instances.get(tokenReference(dep.token));
}
}
if (!result) {
const args = [createDiTokenExpression(dep.token)];
if (dep.isOptional) {
args.push(o.NULL_EXPR);
}
result = InjectorProps.parent.callMethod('get', args);
}
return result;
}
}
function createDiTokenExpression(token: CompileTokenMetadata): o.Expression {
if (token.value != null) {
return o.literal(token.value);
} else {
return o.importExpr(token.identifier);
}
}
class InjectorProps {
static parent = o.THIS_EXPR.prop('parent');
}
class InjectMethodVars {
static token = o.variable('token');
static notFoundResult = o.variable('notFoundResult');
}

View File

@ -0,0 +1,38 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NgModule, Type, ɵReflectorReader, ɵreflector, ɵstringify as stringify} from '@angular/core';
import {findLast} from './directive_resolver';
import {CompilerInjectable} from './injectable';
function _isNgModuleMetadata(obj: any): obj is NgModule {
return obj instanceof NgModule;
}
/**
* Resolves types to {@link NgModule}.
*/
@CompilerInjectable()
export class NgModuleResolver {
constructor(private _reflector: ɵReflectorReader = ɵreflector) {}
isNgModule(type: any) { return this._reflector.annotations(type).some(_isNgModuleMetadata); }
resolve(type: Type<any>, throwIfNotFound = true): NgModule {
const ngModuleMeta: NgModule = findLast(this._reflector.annotations(type), _isNgModuleMetadata);
if (ngModuleMeta) {
return ngModuleMeta;
} else {
if (throwIfNotFound) {
throw new Error(`No NgModule metadata found for '${stringify(type)}'.`);
}
return null;
}
}
}

View File

@ -0,0 +1,476 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseSourceSpan} from '../parse_util';
import * as o from './output_ast';
import {SourceMapGenerator} from './source_map';
const _SINGLE_QUOTE_ESCAPE_STRING_RE = /'|\\|\n|\r|\$/g;
const _LEGAL_IDENTIFIER_RE = /^[$A-Z_][0-9A-Z_$]*$/i;
const _INDENT_WITH = ' ';
export const CATCH_ERROR_VAR = o.variable('error');
export const CATCH_STACK_VAR = o.variable('stack');
export abstract class OutputEmitter {
abstract emitStatements(moduleUrl: string, stmts: o.Statement[], exportedVars: string[]): string;
}
class _EmittedLine {
parts: string[] = [];
srcSpans: ParseSourceSpan[] = [];
constructor(public indent: number) {}
}
export class EmitterVisitorContext {
static createRoot(exportedVars: string[]): EmitterVisitorContext {
return new EmitterVisitorContext(exportedVars, 0);
}
private _lines: _EmittedLine[];
private _classes: o.ClassStmt[] = [];
constructor(private _exportedVars: string[], private _indent: number) {
this._lines = [new _EmittedLine(_indent)];
}
private get _currentLine(): _EmittedLine { return this._lines[this._lines.length - 1]; }
isExportedVar(varName: string): boolean { return this._exportedVars.indexOf(varName) !== -1; }
println(from?: {sourceSpan?: ParseSourceSpan}|null, lastPart: string = ''): void {
this.print(from, lastPart, true);
}
lineIsEmpty(): boolean { return this._currentLine.parts.length === 0; }
print(from: {sourceSpan?: ParseSourceSpan}|null, part: string, newLine: boolean = false) {
if (part.length > 0) {
this._currentLine.parts.push(part);
this._currentLine.srcSpans.push(from && from.sourceSpan || null);
}
if (newLine) {
this._lines.push(new _EmittedLine(this._indent));
}
}
removeEmptyLastLine() {
if (this.lineIsEmpty()) {
this._lines.pop();
}
}
incIndent() {
this._indent++;
this._currentLine.indent = this._indent;
}
decIndent() {
this._indent--;
this._currentLine.indent = this._indent;
}
pushClass(clazz: o.ClassStmt) { this._classes.push(clazz); }
popClass(): o.ClassStmt { return this._classes.pop(); }
get currentClass(): o.ClassStmt {
return this._classes.length > 0 ? this._classes[this._classes.length - 1] : null;
}
toSource(): string {
return this.sourceLines
.map(l => l.parts.length > 0 ? _createIndent(l.indent) + l.parts.join('') : '')
.join('\n');
}
toSourceMapGenerator(file: string|null = null, startsAtLine: number = 0): SourceMapGenerator {
const map = new SourceMapGenerator(file);
for (let i = 0; i < startsAtLine; i++) {
map.addLine();
}
this.sourceLines.forEach(line => {
map.addLine();
const spans = line.srcSpans;
const parts = line.parts;
let col0 = line.indent * _INDENT_WITH.length;
let spanIdx = 0;
// skip leading parts without source spans
while (spanIdx < spans.length && !spans[spanIdx]) {
col0 += parts[spanIdx].length;
spanIdx++;
}
while (spanIdx < spans.length) {
const span = spans[spanIdx];
const source = span.start.file;
const sourceLine = span.start.line;
const sourceCol = span.start.col;
map.addSource(source.url, source.content)
.addMapping(col0, source.url, sourceLine, sourceCol);
col0 += parts[spanIdx].length;
spanIdx++;
// assign parts without span or the same span to the previous segment
while (spanIdx < spans.length && (span === spans[spanIdx] || !spans[spanIdx])) {
col0 += parts[spanIdx].length;
spanIdx++;
}
}
});
return map;
}
private get sourceLines(): _EmittedLine[] {
if (this._lines.length && this._lines[this._lines.length - 1].parts.length === 0) {
return this._lines.slice(0, -1);
}
return this._lines;
}
}
export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.ExpressionVisitor {
constructor(private _escapeDollarInStrings: boolean) {}
visitExpressionStmt(stmt: o.ExpressionStatement, ctx: EmitterVisitorContext): any {
stmt.expr.visitExpression(this, ctx);
ctx.println(stmt, ';');
return null;
}
visitReturnStmt(stmt: o.ReturnStatement, ctx: EmitterVisitorContext): any {
ctx.print(stmt, `return `);
stmt.value.visitExpression(this, ctx);
ctx.println(stmt, ';');
return null;
}
abstract visitCastExpr(ast: o.CastExpr, context: any): any;
abstract visitDeclareClassStmt(stmt: o.ClassStmt, ctx: EmitterVisitorContext): any;
visitIfStmt(stmt: o.IfStmt, ctx: EmitterVisitorContext): any {
ctx.print(stmt, `if (`);
stmt.condition.visitExpression(this, ctx);
ctx.print(stmt, `) {`);
const hasElseCase = stmt.falseCase != null && stmt.falseCase.length > 0;
if (stmt.trueCase.length <= 1 && !hasElseCase) {
ctx.print(stmt, ` `);
this.visitAllStatements(stmt.trueCase, ctx);
ctx.removeEmptyLastLine();
ctx.print(stmt, ` `);
} else {
ctx.println();
ctx.incIndent();
this.visitAllStatements(stmt.trueCase, ctx);
ctx.decIndent();
if (hasElseCase) {
ctx.println(stmt, `} else {`);
ctx.incIndent();
this.visitAllStatements(stmt.falseCase, ctx);
ctx.decIndent();
}
}
ctx.println(stmt, `}`);
return null;
}
abstract visitTryCatchStmt(stmt: o.TryCatchStmt, ctx: EmitterVisitorContext): any;
visitThrowStmt(stmt: o.ThrowStmt, ctx: EmitterVisitorContext): any {
ctx.print(stmt, `throw `);
stmt.error.visitExpression(this, ctx);
ctx.println(stmt, `;`);
return null;
}
visitCommentStmt(stmt: o.CommentStmt, ctx: EmitterVisitorContext): any {
const lines = stmt.comment.split('\n');
lines.forEach((line) => { ctx.println(stmt, `// ${line}`); });
return null;
}
abstract visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any;
visitWriteVarExpr(expr: o.WriteVarExpr, ctx: EmitterVisitorContext): any {
const lineWasEmpty = ctx.lineIsEmpty();
if (!lineWasEmpty) {
ctx.print(expr, '(');
}
ctx.print(expr, `${expr.name} = `);
expr.value.visitExpression(this, ctx);
if (!lineWasEmpty) {
ctx.print(expr, ')');
}
return null;
}
visitWriteKeyExpr(expr: o.WriteKeyExpr, ctx: EmitterVisitorContext): any {
const lineWasEmpty = ctx.lineIsEmpty();
if (!lineWasEmpty) {
ctx.print(expr, '(');
}
expr.receiver.visitExpression(this, ctx);
ctx.print(expr, `[`);
expr.index.visitExpression(this, ctx);
ctx.print(expr, `] = `);
expr.value.visitExpression(this, ctx);
if (!lineWasEmpty) {
ctx.print(expr, ')');
}
return null;
}
visitWritePropExpr(expr: o.WritePropExpr, ctx: EmitterVisitorContext): any {
const lineWasEmpty = ctx.lineIsEmpty();
if (!lineWasEmpty) {
ctx.print(expr, '(');
}
expr.receiver.visitExpression(this, ctx);
ctx.print(expr, `.${expr.name} = `);
expr.value.visitExpression(this, ctx);
if (!lineWasEmpty) {
ctx.print(expr, ')');
}
return null;
}
visitInvokeMethodExpr(expr: o.InvokeMethodExpr, ctx: EmitterVisitorContext): any {
expr.receiver.visitExpression(this, ctx);
let name = expr.name;
if (expr.builtin != null) {
name = this.getBuiltinMethodName(expr.builtin);
if (name == null) {
// some builtins just mean to skip the call.
return null;
}
}
ctx.print(expr, `.${name}(`);
this.visitAllExpressions(expr.args, ctx, `,`);
ctx.print(expr, `)`);
return null;
}
abstract getBuiltinMethodName(method: o.BuiltinMethod): string;
visitInvokeFunctionExpr(expr: o.InvokeFunctionExpr, ctx: EmitterVisitorContext): any {
expr.fn.visitExpression(this, ctx);
ctx.print(expr, `(`);
this.visitAllExpressions(expr.args, ctx, ',');
ctx.print(expr, `)`);
return null;
}
visitReadVarExpr(ast: o.ReadVarExpr, ctx: EmitterVisitorContext): any {
let varName = ast.name;
if (ast.builtin != null) {
switch (ast.builtin) {
case o.BuiltinVar.Super:
varName = 'super';
break;
case o.BuiltinVar.This:
varName = 'this';
break;
case o.BuiltinVar.CatchError:
varName = CATCH_ERROR_VAR.name;
break;
case o.BuiltinVar.CatchStack:
varName = CATCH_STACK_VAR.name;
break;
default:
throw new Error(`Unknown builtin variable ${ast.builtin}`);
}
}
ctx.print(ast, varName);
return null;
}
visitInstantiateExpr(ast: o.InstantiateExpr, ctx: EmitterVisitorContext): any {
ctx.print(ast, `new `);
ast.classExpr.visitExpression(this, ctx);
ctx.print(ast, `(`);
this.visitAllExpressions(ast.args, ctx, ',');
ctx.print(ast, `)`);
return null;
}
visitLiteralExpr(ast: o.LiteralExpr, ctx: EmitterVisitorContext): any {
const value = ast.value;
if (typeof value === 'string') {
ctx.print(ast, escapeIdentifier(value, this._escapeDollarInStrings));
} else {
ctx.print(ast, `${value}`);
}
return null;
}
abstract visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any;
visitConditionalExpr(ast: o.ConditionalExpr, ctx: EmitterVisitorContext): any {
ctx.print(ast, `(`);
ast.condition.visitExpression(this, ctx);
ctx.print(ast, '? ');
ast.trueCase.visitExpression(this, ctx);
ctx.print(ast, ': ');
ast.falseCase.visitExpression(this, ctx);
ctx.print(ast, `)`);
return null;
}
visitNotExpr(ast: o.NotExpr, ctx: EmitterVisitorContext): any {
ctx.print(ast, '!');
ast.condition.visitExpression(this, ctx);
return null;
}
abstract visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any;
abstract visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, context: any): any;
visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, ctx: EmitterVisitorContext): any {
let opStr: string;
switch (ast.operator) {
case o.BinaryOperator.Equals:
opStr = '==';
break;
case o.BinaryOperator.Identical:
opStr = '===';
break;
case o.BinaryOperator.NotEquals:
opStr = '!=';
break;
case o.BinaryOperator.NotIdentical:
opStr = '!==';
break;
case o.BinaryOperator.And:
opStr = '&&';
break;
case o.BinaryOperator.Or:
opStr = '||';
break;
case o.BinaryOperator.Plus:
opStr = '+';
break;
case o.BinaryOperator.Minus:
opStr = '-';
break;
case o.BinaryOperator.Divide:
opStr = '/';
break;
case o.BinaryOperator.Multiply:
opStr = '*';
break;
case o.BinaryOperator.Modulo:
opStr = '%';
break;
case o.BinaryOperator.Lower:
opStr = '<';
break;
case o.BinaryOperator.LowerEquals:
opStr = '<=';
break;
case o.BinaryOperator.Bigger:
opStr = '>';
break;
case o.BinaryOperator.BiggerEquals:
opStr = '>=';
break;
default:
throw new Error(`Unknown operator ${ast.operator}`);
}
ctx.print(ast, `(`);
ast.lhs.visitExpression(this, ctx);
ctx.print(ast, ` ${opStr} `);
ast.rhs.visitExpression(this, ctx);
ctx.print(ast, `)`);
return null;
}
visitReadPropExpr(ast: o.ReadPropExpr, ctx: EmitterVisitorContext): any {
ast.receiver.visitExpression(this, ctx);
ctx.print(ast, `.`);
ctx.print(ast, ast.name);
return null;
}
visitReadKeyExpr(ast: o.ReadKeyExpr, ctx: EmitterVisitorContext): any {
ast.receiver.visitExpression(this, ctx);
ctx.print(ast, `[`);
ast.index.visitExpression(this, ctx);
ctx.print(ast, `]`);
return null;
}
visitLiteralArrayExpr(ast: o.LiteralArrayExpr, ctx: EmitterVisitorContext): any {
const useNewLine = ast.entries.length > 1;
ctx.print(ast, `[`, useNewLine);
ctx.incIndent();
this.visitAllExpressions(ast.entries, ctx, ',', useNewLine);
ctx.decIndent();
ctx.print(ast, `]`, useNewLine);
return null;
}
visitLiteralMapExpr(ast: o.LiteralMapExpr, ctx: EmitterVisitorContext): any {
const useNewLine = ast.entries.length > 1;
ctx.print(ast, `{`, useNewLine);
ctx.incIndent();
this.visitAllObjects(entry => {
ctx.print(ast, `${escapeIdentifier(entry.key, this._escapeDollarInStrings, entry.quoted)}: `);
entry.value.visitExpression(this, ctx);
}, ast.entries, ctx, ',', useNewLine);
ctx.decIndent();
ctx.print(ast, `}`, useNewLine);
return null;
}
visitAllExpressions(
expressions: o.Expression[], ctx: EmitterVisitorContext, separator: string,
newLine: boolean = false): void {
this.visitAllObjects(
expr => expr.visitExpression(this, ctx), expressions, ctx, separator, newLine);
}
visitAllObjects<T>(
handler: (t: T) => void, expressions: T[], ctx: EmitterVisitorContext, separator: string,
newLine: boolean = false): void {
for (let i = 0; i < expressions.length; i++) {
if (i > 0) {
ctx.print(null, separator, newLine);
}
handler(expressions[i]);
}
if (newLine) {
ctx.println();
}
}
visitAllStatements(statements: o.Statement[], ctx: EmitterVisitorContext): void {
statements.forEach((stmt) => stmt.visitStatement(this, ctx));
}
}
export function escapeIdentifier(
input: string, escapeDollar: boolean, alwaysQuote: boolean = true): any {
if (input == null) {
return null;
}
const body = input.replace(_SINGLE_QUOTE_ESCAPE_STRING_RE, (...match: string[]) => {
if (match[0] == '$') {
return escapeDollar ? '\\$' : '$';
} else if (match[0] == '\n') {
return '\\n';
} else if (match[0] == '\r') {
return '\\r';
} else {
return `\\${match[0]}`;
}
});
const requiresQuotes = alwaysQuote || !_LEGAL_IDENTIFIER_RE.test(body);
return requiresQuotes ? `'${body}'` : body;
}
function _createIndent(count: number): string {
let res = '';
for (let i = 0; i < count; i++) {
res += _INDENT_WITH;
}
return res;
}

View File

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

View File

@ -0,0 +1,65 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ParseSourceSpan} from '../parse_util';
import * as o from './output_ast';
/**
* Create a new class stmts based on the given data.
*/
export function createClassStmt(config: {
name: string,
parent?: o.Expression,
parentArgs?: o.Expression[],
ctorParams?: o.FnParam[],
builders: ClassBuilderPart | ClassBuilderPart[],
modifiers?: o.StmtModifier[],
sourceSpan?: ParseSourceSpan
}): o.ClassStmt {
const parentArgs = config.parentArgs || [];
const superCtorStmts = config.parent ? [o.SUPER_EXPR.callFn(parentArgs).toStmt()] : [];
const builder =
concatClassBuilderParts(Array.isArray(config.builders) ? config.builders : [config.builders]);
const ctor =
new o.ClassMethod(null, config.ctorParams || [], superCtorStmts.concat(builder.ctorStmts));
return new o.ClassStmt(
config.name, config.parent, builder.fields, builder.getters, ctor, builder.methods,
config.modifiers || [], config.sourceSpan);
}
function concatClassBuilderParts(builders: ClassBuilderPart[]) {
return {
fields: [].concat(...builders.map(builder => builder.fields || [])),
methods: [].concat(...builders.map(builder => builder.methods || [])),
getters: [].concat(...builders.map(builder => builder.getters || [])),
ctorStmts: [].concat(...builders.map(builder => builder.ctorStmts || [])),
};
}
/**
* Collects data for a generated class.
*/
export interface ClassBuilderPart {
fields?: o.ClassField[];
methods?: o.ClassMethod[];
getters?: o.ClassGetter[];
ctorStmts?: o.Statement[];
}
/**
* Collects data for a generated class.
*/
export interface ClassBuilder {
fields: o.ClassField[];
methods: o.ClassMethod[];
getters: o.ClassGetter[];
ctorStmts: o.Statement[];
}

View File

@ -0,0 +1,97 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StaticSymbol} from '../aot/static_symbol';
import {CompileIdentifierMetadata} from '../compile_metadata';
import {EmitterVisitorContext, OutputEmitter} from './abstract_emitter';
import {AbstractJsEmitterVisitor} from './abstract_js_emitter';
import * as o from './output_ast';
import {ImportResolver} from './path_util';
export class JavaScriptEmitter implements OutputEmitter {
constructor(private _importResolver: ImportResolver) {}
emitStatements(genFilePath: string, stmts: o.Statement[], exportedVars: string[]): string {
const converter = new JsEmitterVisitor(genFilePath, this._importResolver);
const ctx = EmitterVisitorContext.createRoot(exportedVars);
converter.visitAllStatements(stmts, ctx);
const srcParts: string[] = [];
converter.importsWithPrefixes.forEach((prefix, importedFilePath) => {
// Note: can't write the real word for import as it screws up system.js auto detection...
srcParts.push(
`var ${prefix} = req` +
`uire('${this._importResolver.fileNameToModuleName(importedFilePath, genFilePath)}');`);
});
srcParts.push(ctx.toSource());
const prefixLines = converter.importsWithPrefixes.size;
const sm = ctx.toSourceMapGenerator(null, prefixLines).toJsComment();
if (sm) {
srcParts.push(sm);
}
return srcParts.join('\n');
}
}
class JsEmitterVisitor extends AbstractJsEmitterVisitor {
importsWithPrefixes = new Map<string, string>();
constructor(private _genFilePath: string, private _importResolver: ImportResolver) { super(); }
private _resolveStaticSymbol(value: CompileIdentifierMetadata): StaticSymbol {
const reference = value.reference;
if (!(reference instanceof StaticSymbol)) {
throw new Error(`Internal error: unknown identifier ${JSON.stringify(value)}`);
}
return this._importResolver.getImportAs(reference) || reference;
}
visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any {
const {name, filePath} = this._resolveStaticSymbol(ast.value);
if (filePath != this._genFilePath) {
let prefix = this.importsWithPrefixes.get(filePath);
if (prefix == null) {
prefix = `import${this.importsWithPrefixes.size}`;
this.importsWithPrefixes.set(filePath, prefix);
}
ctx.print(ast, `${prefix}.`);
}
ctx.print(ast, name);
return null;
}
visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any {
super.visitDeclareVarStmt(stmt, ctx);
if (ctx.isExportedVar(stmt.name)) {
ctx.println(stmt, exportVar(stmt.name));
}
return null;
}
visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, ctx: EmitterVisitorContext): any {
super.visitDeclareFunctionStmt(stmt, ctx);
if (ctx.isExportedVar(stmt.name)) {
ctx.println(stmt, exportVar(stmt.name));
}
return null;
}
visitDeclareClassStmt(stmt: o.ClassStmt, ctx: EmitterVisitorContext): any {
super.visitDeclareClassStmt(stmt, ctx);
if (ctx.isExportedVar(stmt.name)) {
ctx.println(stmt, exportVar(stmt.name));
}
return null;
}
}
function exportVar(varName: string): string {
return `Object.defineProperty(exports, '${varName}', { get: function() { return ${varName}; }});`;
}

View File

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

View File

@ -0,0 +1,331 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as o from './output_ast';
import {debugOutputAstAsTypeScript} from './ts_emitter';
export function interpretStatements(statements: o.Statement[], resultVars: string[]): any[] {
const stmtsWithReturn = statements.concat(
[new o.ReturnStatement(o.literalArr(resultVars.map(resultVar => o.variable(resultVar))))]);
const ctx = new _ExecutionContext(null, null, null, new Map<string, any>());
const visitor = new StatementInterpreter();
const result = visitor.visitAllStatements(stmtsWithReturn, ctx);
return result != null ? result.value : null;
}
function _executeFunctionStatements(
varNames: string[], varValues: any[], statements: o.Statement[], ctx: _ExecutionContext,
visitor: StatementInterpreter): any {
const childCtx = ctx.createChildWihtLocalVars();
for (let i = 0; i < varNames.length; i++) {
childCtx.vars.set(varNames[i], varValues[i]);
}
const result = visitor.visitAllStatements(statements, childCtx);
return result ? result.value : null;
}
class _ExecutionContext {
constructor(
public parent: _ExecutionContext, public instance: any, public className: string,
public vars: Map<string, any>) {}
createChildWihtLocalVars(): _ExecutionContext {
return new _ExecutionContext(this, this.instance, this.className, new Map<string, any>());
}
}
class ReturnValue {
constructor(public value: any) {}
}
function createDynamicClass(
_classStmt: o.ClassStmt, _ctx: _ExecutionContext, _visitor: StatementInterpreter): Function {
const propertyDescriptors: {[key: string]: any} = {};
_classStmt.getters.forEach((getter: o.ClassGetter) => {
// Note: use `function` instead of arrow function to capture `this`
propertyDescriptors[getter.name] = {
configurable: false,
get: function() {
const instanceCtx = new _ExecutionContext(_ctx, this, _classStmt.name, _ctx.vars);
return _executeFunctionStatements([], [], getter.body, instanceCtx, _visitor);
}
};
});
_classStmt.methods.forEach(function(method: o.ClassMethod) {
const paramNames = method.params.map(param => param.name);
// Note: use `function` instead of arrow function to capture `this`
propertyDescriptors[method.name] = {
writable: false,
configurable: false,
value: function(...args: any[]) {
const instanceCtx = new _ExecutionContext(_ctx, this, _classStmt.name, _ctx.vars);
return _executeFunctionStatements(paramNames, args, method.body, instanceCtx, _visitor);
}
};
});
const ctorParamNames = _classStmt.constructorMethod.params.map(param => param.name);
// Note: use `function` instead of arrow function to capture `this`
const ctor = function(...args: any[]) {
const instanceCtx = new _ExecutionContext(_ctx, this, _classStmt.name, _ctx.vars);
_classStmt.fields.forEach((field) => { this[field.name] = undefined; });
_executeFunctionStatements(
ctorParamNames, args, _classStmt.constructorMethod.body, instanceCtx, _visitor);
};
const superClass = _classStmt.parent ? _classStmt.parent.visitExpression(_visitor, _ctx) : Object;
ctor.prototype = Object.create(superClass.prototype, propertyDescriptors);
return ctor;
}
class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor {
debugAst(ast: o.Expression|o.Statement|o.Type): string { return debugOutputAstAsTypeScript(ast); }
visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: _ExecutionContext): any {
ctx.vars.set(stmt.name, stmt.value.visitExpression(this, ctx));
return null;
}
visitWriteVarExpr(expr: o.WriteVarExpr, ctx: _ExecutionContext): any {
const value = expr.value.visitExpression(this, ctx);
let currCtx = ctx;
while (currCtx != null) {
if (currCtx.vars.has(expr.name)) {
currCtx.vars.set(expr.name, value);
return value;
}
currCtx = currCtx.parent;
}
throw new Error(`Not declared variable ${expr.name}`);
}
visitReadVarExpr(ast: o.ReadVarExpr, ctx: _ExecutionContext): any {
let varName = ast.name;
if (ast.builtin != null) {
switch (ast.builtin) {
case o.BuiltinVar.Super:
return ctx.instance.__proto__;
case o.BuiltinVar.This:
return ctx.instance;
case o.BuiltinVar.CatchError:
varName = CATCH_ERROR_VAR;
break;
case o.BuiltinVar.CatchStack:
varName = CATCH_STACK_VAR;
break;
default:
throw new Error(`Unknown builtin variable ${ast.builtin}`);
}
}
let currCtx = ctx;
while (currCtx != null) {
if (currCtx.vars.has(varName)) {
return currCtx.vars.get(varName);
}
currCtx = currCtx.parent;
}
throw new Error(`Not declared variable ${varName}`);
}
visitWriteKeyExpr(expr: o.WriteKeyExpr, ctx: _ExecutionContext): any {
const receiver = expr.receiver.visitExpression(this, ctx);
const index = expr.index.visitExpression(this, ctx);
const value = expr.value.visitExpression(this, ctx);
receiver[index] = value;
return value;
}
visitWritePropExpr(expr: o.WritePropExpr, ctx: _ExecutionContext): any {
const receiver = expr.receiver.visitExpression(this, ctx);
const value = expr.value.visitExpression(this, ctx);
receiver[expr.name] = value;
return value;
}
visitInvokeMethodExpr(expr: o.InvokeMethodExpr, ctx: _ExecutionContext): any {
const receiver = expr.receiver.visitExpression(this, ctx);
const args = this.visitAllExpressions(expr.args, ctx);
let result: any;
if (expr.builtin != null) {
switch (expr.builtin) {
case o.BuiltinMethod.ConcatArray:
result = receiver.concat(...args);
break;
case o.BuiltinMethod.SubscribeObservable:
result = receiver.subscribe({next: args[0]});
break;
case o.BuiltinMethod.Bind:
result = receiver.bind(...args);
break;
default:
throw new Error(`Unknown builtin method ${expr.builtin}`);
}
} else {
result = receiver[expr.name].apply(receiver, args);
}
return result;
}
visitInvokeFunctionExpr(stmt: o.InvokeFunctionExpr, ctx: _ExecutionContext): any {
const args = this.visitAllExpressions(stmt.args, ctx);
const fnExpr = stmt.fn;
if (fnExpr instanceof o.ReadVarExpr && fnExpr.builtin === o.BuiltinVar.Super) {
ctx.instance.constructor.prototype.constructor.apply(ctx.instance, args);
return null;
} else {
const fn = stmt.fn.visitExpression(this, ctx);
return fn.apply(null, args);
}
}
visitReturnStmt(stmt: o.ReturnStatement, ctx: _ExecutionContext): any {
return new ReturnValue(stmt.value.visitExpression(this, ctx));
}
visitDeclareClassStmt(stmt: o.ClassStmt, ctx: _ExecutionContext): any {
const clazz = createDynamicClass(stmt, ctx, this);
ctx.vars.set(stmt.name, clazz);
return null;
}
visitExpressionStmt(stmt: o.ExpressionStatement, ctx: _ExecutionContext): any {
return stmt.expr.visitExpression(this, ctx);
}
visitIfStmt(stmt: o.IfStmt, ctx: _ExecutionContext): any {
const condition = stmt.condition.visitExpression(this, ctx);
if (condition) {
return this.visitAllStatements(stmt.trueCase, ctx);
} else if (stmt.falseCase != null) {
return this.visitAllStatements(stmt.falseCase, ctx);
}
return null;
}
visitTryCatchStmt(stmt: o.TryCatchStmt, ctx: _ExecutionContext): any {
try {
return this.visitAllStatements(stmt.bodyStmts, ctx);
} catch (e) {
const childCtx = ctx.createChildWihtLocalVars();
childCtx.vars.set(CATCH_ERROR_VAR, e);
childCtx.vars.set(CATCH_STACK_VAR, e.stack);
return this.visitAllStatements(stmt.catchStmts, childCtx);
}
}
visitThrowStmt(stmt: o.ThrowStmt, ctx: _ExecutionContext): any {
throw stmt.error.visitExpression(this, ctx);
}
visitCommentStmt(stmt: o.CommentStmt, context?: any): any { return null; }
visitInstantiateExpr(ast: o.InstantiateExpr, ctx: _ExecutionContext): any {
const args = this.visitAllExpressions(ast.args, ctx);
const clazz = ast.classExpr.visitExpression(this, ctx);
return new clazz(...args);
}
visitLiteralExpr(ast: o.LiteralExpr, ctx: _ExecutionContext): any { return ast.value; }
visitExternalExpr(ast: o.ExternalExpr, ctx: _ExecutionContext): any {
return ast.value.reference;
}
visitConditionalExpr(ast: o.ConditionalExpr, ctx: _ExecutionContext): any {
if (ast.condition.visitExpression(this, ctx)) {
return ast.trueCase.visitExpression(this, ctx);
} else if (ast.falseCase != null) {
return ast.falseCase.visitExpression(this, ctx);
}
return null;
}
visitNotExpr(ast: o.NotExpr, ctx: _ExecutionContext): any {
return !ast.condition.visitExpression(this, ctx);
}
visitCastExpr(ast: o.CastExpr, ctx: _ExecutionContext): any {
return ast.value.visitExpression(this, ctx);
}
visitFunctionExpr(ast: o.FunctionExpr, ctx: _ExecutionContext): any {
const paramNames = ast.params.map((param) => param.name);
return _declareFn(paramNames, ast.statements, ctx, this);
}
visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, ctx: _ExecutionContext): any {
const paramNames = stmt.params.map((param) => param.name);
ctx.vars.set(stmt.name, _declareFn(paramNames, stmt.statements, ctx, this));
return null;
}
visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, ctx: _ExecutionContext): any {
const lhs = () => ast.lhs.visitExpression(this, ctx);
const rhs = () => ast.rhs.visitExpression(this, ctx);
switch (ast.operator) {
case o.BinaryOperator.Equals:
return lhs() == rhs();
case o.BinaryOperator.Identical:
return lhs() === rhs();
case o.BinaryOperator.NotEquals:
return lhs() != rhs();
case o.BinaryOperator.NotIdentical:
return lhs() !== rhs();
case o.BinaryOperator.And:
return lhs() && rhs();
case o.BinaryOperator.Or:
return lhs() || rhs();
case o.BinaryOperator.Plus:
return lhs() + rhs();
case o.BinaryOperator.Minus:
return lhs() - rhs();
case o.BinaryOperator.Divide:
return lhs() / rhs();
case o.BinaryOperator.Multiply:
return lhs() * rhs();
case o.BinaryOperator.Modulo:
return lhs() % rhs();
case o.BinaryOperator.Lower:
return lhs() < rhs();
case o.BinaryOperator.LowerEquals:
return lhs() <= rhs();
case o.BinaryOperator.Bigger:
return lhs() > rhs();
case o.BinaryOperator.BiggerEquals:
return lhs() >= rhs();
default:
throw new Error(`Unknown operator ${ast.operator}`);
}
}
visitReadPropExpr(ast: o.ReadPropExpr, ctx: _ExecutionContext): any {
let result: any;
const receiver = ast.receiver.visitExpression(this, ctx);
result = receiver[ast.name];
return result;
}
visitReadKeyExpr(ast: o.ReadKeyExpr, ctx: _ExecutionContext): any {
const receiver = ast.receiver.visitExpression(this, ctx);
const prop = ast.index.visitExpression(this, ctx);
return receiver[prop];
}
visitLiteralArrayExpr(ast: o.LiteralArrayExpr, ctx: _ExecutionContext): any {
return this.visitAllExpressions(ast.entries, ctx);
}
visitLiteralMapExpr(ast: o.LiteralMapExpr, ctx: _ExecutionContext): any {
const result = {};
ast.entries.forEach(
(entry) => (result as any)[entry.key] = entry.value.visitExpression(this, ctx));
return result;
}
visitAllExpressions(expressions: o.Expression[], ctx: _ExecutionContext): any {
return expressions.map((expr) => expr.visitExpression(this, ctx));
}
visitAllStatements(statements: o.Statement[], ctx: _ExecutionContext): ReturnValue {
for (let i = 0; i < statements.length; i++) {
const stmt = statements[i];
const val = stmt.visitStatement(this, ctx);
if (val instanceof ReturnValue) {
return val;
}
}
return null;
}
}
function _declareFn(
varNames: string[], statements: o.Statement[], ctx: _ExecutionContext,
visitor: StatementInterpreter): Function {
return (...args: any[]) => _executeFunctionStatements(varNames, args, statements, ctx, visitor);
}
const CATCH_ERROR_VAR = 'error';
const CATCH_STACK_VAR = 'stack';

View File

@ -0,0 +1,62 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {identifierName} from '../compile_metadata';
import {EmitterVisitorContext} from './abstract_emitter';
import {AbstractJsEmitterVisitor} from './abstract_js_emitter';
import * as o from './output_ast';
function evalExpression(
sourceUrl: string, ctx: EmitterVisitorContext, vars: {[key: string]: any}): any {
const fnBody =
`${ctx.toSource()}\n//# sourceURL=${sourceUrl}\n${ctx.toSourceMapGenerator().toJsComment()}`;
const fnArgNames: string[] = [];
const fnArgValues: any[] = [];
for (const argName in vars) {
fnArgNames.push(argName);
fnArgValues.push(vars[argName]);
}
return new Function(...fnArgNames.concat(fnBody))(...fnArgValues);
}
export function jitStatements(
sourceUrl: string, statements: o.Statement[], resultVars: string[]): any[] {
const converter = new JitEmitterVisitor();
const ctx = EmitterVisitorContext.createRoot(resultVars);
const returnStmt =
new o.ReturnStatement(o.literalArr(resultVars.map(resultVar => o.variable(resultVar))));
converter.visitAllStatements(statements.concat([returnStmt]), ctx);
return evalExpression(sourceUrl, ctx, converter.getArgs());
}
class JitEmitterVisitor extends AbstractJsEmitterVisitor {
private _evalArgNames: string[] = [];
private _evalArgValues: any[] = [];
getArgs(): {[key: string]: any} {
const result: {[key: string]: any} = {};
for (let i = 0; i < this._evalArgNames.length; i++) {
result[this._evalArgNames[i]] = this._evalArgValues[i];
}
return result;
}
visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any {
const value = ast.value.reference;
let id = this._evalArgValues.indexOf(value);
if (id === -1) {
id = this._evalArgValues.length;
this._evalArgValues.push(value);
const name = identifierName(ast.value) || 'val';
this._evalArgNames.push(`jit_${name}${id}`);
}
ctx.print(ast, this._evalArgNames[id]);
return null;
}
}

View File

@ -0,0 +1,32 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StaticSymbol} from '../aot/static_symbol';
/**
* Interface that defines how import statements should be generated.
*/
export abstract class ImportResolver {
/**
* Converts a file path to a module name that can be used as an `import.
* I.e. `path/to/importedFile.ts` should be imported by `path/to/containingFile.ts`.
*/
abstract fileNameToModuleName(importedFilePath: string, containingFilePath: string): string
/*|null*/;
/**
* Converts the given StaticSymbol into another StaticSymbol that should be used
* to generate the import from.
*/
abstract getImportAs(symbol: StaticSymbol): StaticSymbol /*|null*/;
/**
* Determine the airty of a type.
*/
abstract getTypeArity(symbol: StaticSymbol): number /*|null*/;
}

View File

@ -0,0 +1,184 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
const VERSION = 3;
const JS_B64_PREFIX = '# sourceMappingURL=data:application/json;base64,';
type Segment = {
col0: number,
sourceUrl?: string,
sourceLine0?: number,
sourceCol0?: number,
};
export type SourceMap = {
version: number,
file?: string,
sourceRoot: string,
sources: string[],
sourcesContent: string[],
mappings: string,
};
export class SourceMapGenerator {
private sourcesContent: Map<string, string> = new Map();
private lines: Segment[][] = [];
private lastCol0: number = 0;
private hasMappings = false;
constructor(private file: string|null = null) {}
// The content is `null` when the content is expected to be loaded using the URL
addSource(url: string, content: string|null = null): this {
if (!this.sourcesContent.has(url)) {
this.sourcesContent.set(url, content);
}
return this;
}
addLine(): this {
this.lines.push([]);
this.lastCol0 = 0;
return this;
}
addMapping(col0: number, sourceUrl?: string, sourceLine0?: number, sourceCol0?: number): this {
if (!this.currentLine) {
throw new Error(`A line must be added before mappings can be added`);
}
if (sourceUrl != null && !this.sourcesContent.has(sourceUrl)) {
throw new Error(`Unknown source file "${sourceUrl}"`);
}
if (col0 == null) {
throw new Error(`The column in the generated code must be provided`);
}
if (col0 < this.lastCol0) {
throw new Error(`Mapping should be added in output order`);
}
if (sourceUrl && (sourceLine0 == null || sourceCol0 == null)) {
throw new Error(`The source location must be provided when a source url is provided`);
}
this.hasMappings = true;
this.lastCol0 = col0;
this.currentLine.push({col0, sourceUrl, sourceLine0, sourceCol0});
return this;
}
private get currentLine(): Segment[]|null { return this.lines.slice(-1)[0]; }
toJSON(): SourceMap|null {
if (!this.hasMappings) {
return null;
}
const sourcesIndex = new Map<string, number>();
const sources: string[] = [];
const sourcesContent: string[] = [];
Array.from(this.sourcesContent.keys()).forEach((url: string, i: number) => {
sourcesIndex.set(url, i);
sources.push(url);
sourcesContent.push(this.sourcesContent.get(url) || null);
});
let mappings: string = '';
let lastCol0: number = 0;
let lastSourceIndex: number = 0;
let lastSourceLine0: number = 0;
let lastSourceCol0: number = 0;
this.lines.forEach(segments => {
lastCol0 = 0;
mappings += segments
.map(segment => {
// zero-based starting column of the line in the generated code
let segAsStr = toBase64VLQ(segment.col0 - lastCol0);
lastCol0 = segment.col0;
if (segment.sourceUrl != null) {
// zero-based index into the “sources” list
segAsStr +=
toBase64VLQ(sourcesIndex.get(segment.sourceUrl) - lastSourceIndex);
lastSourceIndex = sourcesIndex.get(segment.sourceUrl);
// the zero-based starting line in the original source
segAsStr += toBase64VLQ(segment.sourceLine0 - lastSourceLine0);
lastSourceLine0 = segment.sourceLine0;
// the zero-based starting column in the original source
segAsStr += toBase64VLQ(segment.sourceCol0 - lastSourceCol0);
lastSourceCol0 = segment.sourceCol0;
}
return segAsStr;
})
.join(',');
mappings += ';';
});
mappings = mappings.slice(0, -1);
return {
'file': this.file || '',
'version': VERSION,
'sourceRoot': '',
'sources': sources,
'sourcesContent': sourcesContent,
'mappings': mappings,
};
}
toJsComment(): string {
return this.hasMappings ? '//' + JS_B64_PREFIX + toBase64String(JSON.stringify(this, null, 0)) :
'';
}
}
export function toBase64String(value: string): string {
let b64 = '';
for (let i = 0; i < value.length;) {
const i1 = value.charCodeAt(i++);
const i2 = value.charCodeAt(i++);
const i3 = value.charCodeAt(i++);
b64 += toBase64Digit(i1 >> 2);
b64 += toBase64Digit(((i1 & 3) << 4) | (isNaN(i2) ? 0 : i2 >> 4));
b64 += isNaN(i2) ? '=' : toBase64Digit(((i2 & 15) << 2) | (i3 >> 6));
b64 += isNaN(i2) || isNaN(i3) ? '=' : toBase64Digit(i3 & 63);
}
return b64;
}
function toBase64VLQ(value: number): string {
value = value < 0 ? ((-value) << 1) + 1 : value << 1;
let out = '';
do {
let digit = value & 31;
value = value >> 5;
if (value > 0) {
digit = digit | 32;
}
out += toBase64Digit(digit);
} while (value > 0);
return out;
}
const B64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function toBase64Digit(value: number): string {
if (value < 0 || value >= 64) {
throw new Error(`Can only encode value in the range [0, 63]`);
}
return B64_DIGITS[value];
}

View File

@ -0,0 +1,437 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {StaticSymbol} from '../aot/static_symbol';
import {CompileIdentifierMetadata} from '../compile_metadata';
import {AbstractEmitterVisitor, CATCH_ERROR_VAR, CATCH_STACK_VAR, EmitterVisitorContext, OutputEmitter} from './abstract_emitter';
import * as o from './output_ast';
import {ImportResolver} from './path_util';
const _debugFilePath = '/debug/lib';
export function debugOutputAstAsTypeScript(ast: o.Statement | o.Expression | o.Type | any[]):
string {
const converter = new _TsEmitterVisitor(_debugFilePath, {
fileNameToModuleName(filePath: string, containingFilePath: string) { return filePath; },
getImportAs(symbol: StaticSymbol) { return null; },
getTypeArity: symbol => null
});
const ctx = EmitterVisitorContext.createRoot([]);
const asts: any[] = Array.isArray(ast) ? ast : [ast];
asts.forEach((ast) => {
if (ast instanceof o.Statement) {
ast.visitStatement(converter, ctx);
} else if (ast instanceof o.Expression) {
ast.visitExpression(converter, ctx);
} else if (ast instanceof o.Type) {
ast.visitType(converter, ctx);
} else {
throw new Error(`Don't know how to print debug info for ${ast}`);
}
});
return ctx.toSource();
}
export class TypeScriptEmitter implements OutputEmitter {
constructor(private _importResolver: ImportResolver) {}
emitStatements(genFilePath: string, stmts: o.Statement[], exportedVars: string[]): string {
const converter = new _TsEmitterVisitor(genFilePath, this._importResolver);
const ctx = EmitterVisitorContext.createRoot(exportedVars);
converter.visitAllStatements(stmts, ctx);
const srcParts: string[] = [];
converter.reexports.forEach((reexports, exportedFilePath) => {
const reexportsCode =
reexports.map(reexport => `${reexport.name} as ${reexport.as}`).join(',');
srcParts.push(
`export {${reexportsCode}} from '${this._importResolver.fileNameToModuleName(exportedFilePath, genFilePath)}';`);
});
converter.importsWithPrefixes.forEach((prefix, importedFilePath) => {
// Note: can't write the real word for import as it screws up system.js auto detection...
srcParts.push(
`imp` +
`ort * as ${prefix} from '${this._importResolver.fileNameToModuleName(importedFilePath, genFilePath)}';`);
});
srcParts.push(ctx.toSource());
const prefixLines = converter.reexports.size + converter.importsWithPrefixes.size;
const sm = ctx.toSourceMapGenerator(null, prefixLines).toJsComment();
if (sm) {
srcParts.push(sm);
}
return srcParts.join('\n');
}
}
class _TsEmitterVisitor extends AbstractEmitterVisitor implements o.TypeVisitor {
private typeExpression = 0;
constructor(private _genFilePath: string, private _importResolver: ImportResolver) {
super(false);
}
importsWithPrefixes = new Map<string, string>();
reexports = new Map<string, {name: string, as: string}[]>();
visitType(t: o.Type, ctx: EmitterVisitorContext, defaultType: string = 'any') {
if (t != null) {
this.typeExpression++;
t.visitType(this, ctx);
this.typeExpression--;
} else {
ctx.print(null, defaultType);
}
}
visitLiteralExpr(ast: o.LiteralExpr, ctx: EmitterVisitorContext): any {
const value = ast.value;
if (value == null && ast.type != o.INFERRED_TYPE) {
ctx.print(ast, `(${value} as any)`);
return null;
}
return super.visitLiteralExpr(ast, ctx);
}
// Temporary workaround to support strictNullCheck enabled consumers of ngc emit.
// In SNC mode, [] have the type never[], so we cast here to any[].
// TODO: narrow the cast to a more explicit type, or use a pattern that does not
// start with [].concat. see https://github.com/angular/angular/pull/11846
visitLiteralArrayExpr(ast: o.LiteralArrayExpr, ctx: EmitterVisitorContext): any {
if (ast.entries.length === 0) {
ctx.print(ast, '(');
}
const result = super.visitLiteralArrayExpr(ast, ctx);
if (ast.entries.length === 0) {
ctx.print(ast, ' as any[])');
}
return result;
}
visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any {
this._visitIdentifier(ast.value, ast.typeParams, ctx);
return null;
}
visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any {
if (ctx.isExportedVar(stmt.name) && stmt.value instanceof o.ExternalExpr && !stmt.type) {
// check for a reexport
const {name, filePath, members} = this._resolveStaticSymbol(stmt.value.value);
if (members.length === 0 && filePath !== this._genFilePath) {
let reexports = this.reexports.get(filePath);
if (!reexports) {
reexports = [];
this.reexports.set(filePath, reexports);
}
reexports.push({name, as: stmt.name});
return null;
}
}
if (ctx.isExportedVar(stmt.name)) {
ctx.print(stmt, `export `);
}
if (stmt.hasModifier(o.StmtModifier.Final)) {
ctx.print(stmt, `const`);
} else {
ctx.print(stmt, `var`);
}
ctx.print(stmt, ` ${stmt.name}`);
this._printColonType(stmt.type, ctx);
ctx.print(stmt, ` = `);
stmt.value.visitExpression(this, ctx);
ctx.println(stmt, `;`);
return null;
}
visitCastExpr(ast: o.CastExpr, ctx: EmitterVisitorContext): any {
ctx.print(ast, `(<`);
ast.type.visitType(this, ctx);
ctx.print(ast, `>`);
ast.value.visitExpression(this, ctx);
ctx.print(ast, `)`);
return null;
}
visitInstantiateExpr(ast: o.InstantiateExpr, ctx: EmitterVisitorContext): any {
ctx.print(ast, `new `);
this.typeExpression++;
ast.classExpr.visitExpression(this, ctx);
this.typeExpression--;
ctx.print(ast, `(`);
this.visitAllExpressions(ast.args, ctx, ',');
ctx.print(ast, `)`);
return null;
}
visitDeclareClassStmt(stmt: o.ClassStmt, ctx: EmitterVisitorContext): any {
ctx.pushClass(stmt);
if (ctx.isExportedVar(stmt.name)) {
ctx.print(stmt, `export `);
}
ctx.print(stmt, `class ${stmt.name}`);
if (stmt.parent != null) {
ctx.print(stmt, ` extends `);
this.typeExpression++;
stmt.parent.visitExpression(this, ctx);
this.typeExpression--;
}
ctx.println(stmt, ` {`);
ctx.incIndent();
stmt.fields.forEach((field) => this._visitClassField(field, ctx));
if (stmt.constructorMethod != null) {
this._visitClassConstructor(stmt, ctx);
}
stmt.getters.forEach((getter) => this._visitClassGetter(getter, ctx));
stmt.methods.forEach((method) => this._visitClassMethod(method, ctx));
ctx.decIndent();
ctx.println(stmt, `}`);
ctx.popClass();
return null;
}
private _visitClassField(field: o.ClassField, ctx: EmitterVisitorContext) {
if (field.hasModifier(o.StmtModifier.Private)) {
// comment out as a workaround for #10967
ctx.print(null, `/*private*/ `);
}
ctx.print(null, field.name);
this._printColonType(field.type, ctx);
ctx.println(null, `;`);
}
private _visitClassGetter(getter: o.ClassGetter, ctx: EmitterVisitorContext) {
if (getter.hasModifier(o.StmtModifier.Private)) {
ctx.print(null, `private `);
}
ctx.print(null, `get ${getter.name}()`);
this._printColonType(getter.type, ctx);
ctx.println(null, ` {`);
ctx.incIndent();
this.visitAllStatements(getter.body, ctx);
ctx.decIndent();
ctx.println(null, `}`);
}
private _visitClassConstructor(stmt: o.ClassStmt, ctx: EmitterVisitorContext) {
ctx.print(stmt, `constructor(`);
this._visitParams(stmt.constructorMethod.params, ctx);
ctx.println(stmt, `) {`);
ctx.incIndent();
this.visitAllStatements(stmt.constructorMethod.body, ctx);
ctx.decIndent();
ctx.println(stmt, `}`);
}
private _visitClassMethod(method: o.ClassMethod, ctx: EmitterVisitorContext) {
if (method.hasModifier(o.StmtModifier.Private)) {
ctx.print(null, `private `);
}
ctx.print(null, `${method.name}(`);
this._visitParams(method.params, ctx);
ctx.print(null, `)`);
this._printColonType(method.type, ctx, 'void');
ctx.println(null, ` {`);
ctx.incIndent();
this.visitAllStatements(method.body, ctx);
ctx.decIndent();
ctx.println(null, `}`);
}
visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any {
ctx.print(ast, `(`);
this._visitParams(ast.params, ctx);
ctx.print(ast, `)`);
this._printColonType(ast.type, ctx, 'void');
ctx.println(ast, ` => {`);
ctx.incIndent();
this.visitAllStatements(ast.statements, ctx);
ctx.decIndent();
ctx.print(ast, `}`);
return null;
}
visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, ctx: EmitterVisitorContext): any {
if (ctx.isExportedVar(stmt.name)) {
ctx.print(stmt, `export `);
}
ctx.print(stmt, `function ${stmt.name}(`);
this._visitParams(stmt.params, ctx);
ctx.print(stmt, `)`);
this._printColonType(stmt.type, ctx, 'void');
ctx.println(stmt, ` {`);
ctx.incIndent();
this.visitAllStatements(stmt.statements, ctx);
ctx.decIndent();
ctx.println(stmt, `}`);
return null;
}
visitTryCatchStmt(stmt: o.TryCatchStmt, ctx: EmitterVisitorContext): any {
ctx.println(stmt, `try {`);
ctx.incIndent();
this.visitAllStatements(stmt.bodyStmts, ctx);
ctx.decIndent();
ctx.println(stmt, `} catch (${CATCH_ERROR_VAR.name}) {`);
ctx.incIndent();
const catchStmts =
[<o.Statement>CATCH_STACK_VAR.set(CATCH_ERROR_VAR.prop('stack')).toDeclStmt(null, [
o.StmtModifier.Final
])].concat(stmt.catchStmts);
this.visitAllStatements(catchStmts, ctx);
ctx.decIndent();
ctx.println(stmt, `}`);
return null;
}
visitBuiltintType(type: o.BuiltinType, ctx: EmitterVisitorContext): any {
let typeStr: string;
switch (type.name) {
case o.BuiltinTypeName.Bool:
typeStr = 'boolean';
break;
case o.BuiltinTypeName.Dynamic:
typeStr = 'any';
break;
case o.BuiltinTypeName.Function:
typeStr = 'Function';
break;
case o.BuiltinTypeName.Number:
typeStr = 'number';
break;
case o.BuiltinTypeName.Int:
typeStr = 'number';
break;
case o.BuiltinTypeName.String:
typeStr = 'string';
break;
default:
throw new Error(`Unsupported builtin type ${type.name}`);
}
ctx.print(null, typeStr);
return null;
}
visitExpressionType(ast: o.ExpressionType, ctx: EmitterVisitorContext): any {
ast.value.visitExpression(this, ctx);
return null;
}
visitArrayType(type: o.ArrayType, ctx: EmitterVisitorContext): any {
this.visitType(type.of, ctx);
ctx.print(null, `[]`);
return null;
}
visitMapType(type: o.MapType, ctx: EmitterVisitorContext): any {
ctx.print(null, `{[key: string]:`);
this.visitType(type.valueType, ctx);
ctx.print(null, `}`);
return null;
}
getBuiltinMethodName(method: o.BuiltinMethod): string {
let name: string;
switch (method) {
case o.BuiltinMethod.ConcatArray:
name = 'concat';
break;
case o.BuiltinMethod.SubscribeObservable:
name = 'subscribe';
break;
case o.BuiltinMethod.Bind:
name = 'bind';
break;
default:
throw new Error(`Unknown builtin method: ${method}`);
}
return name;
}
private _visitParams(params: o.FnParam[], ctx: EmitterVisitorContext): void {
this.visitAllObjects(param => {
ctx.print(null, param.name);
this._printColonType(param.type, ctx);
}, params, ctx, ',');
}
private _resolveStaticSymbol(value: CompileIdentifierMetadata):
{name: string, filePath: string, members?: string[], arity?: number} {
const reference = value.reference;
if (!(reference instanceof StaticSymbol)) {
throw new Error(`Internal error: unknown identifier ${JSON.stringify(value)}`);
}
const arity = this._importResolver.getTypeArity(reference) || undefined;
const importReference = this._importResolver.getImportAs(reference) || reference;
return {
name: importReference.name,
filePath: importReference.filePath,
members: importReference.members, arity
};
}
private _visitIdentifier(
value: CompileIdentifierMetadata, typeParams: o.Type[], ctx: EmitterVisitorContext): void {
const {name, filePath, members, arity} = this._resolveStaticSymbol(value);
if (filePath != this._genFilePath) {
let prefix = this.importsWithPrefixes.get(filePath);
if (prefix == null) {
prefix = `import${this.importsWithPrefixes.size}`;
this.importsWithPrefixes.set(filePath, prefix);
}
ctx.print(null, `${prefix}.`);
}
if (members.length) {
ctx.print(null, name);
ctx.print(null, '.');
ctx.print(null, members.join('.'));
} else {
ctx.print(null, name);
}
if (this.typeExpression > 0) {
// If we are in a type expression that refers to a generic type then supply
// the required type parameters. If there were not enough type parameters
// supplied, supply any as the type. Outside a type expression the reference
// should not supply type parameters and be treated as a simple value reference
// to the constructor function itself.
const suppliedParameters = (typeParams && typeParams.length) || 0;
const additionalParameters = (arity || 0) - suppliedParameters;
if (suppliedParameters > 0 || additionalParameters > 0) {
ctx.print(null, `<`);
if (suppliedParameters > 0) {
this.visitAllObjects(type => type.visitType(this, ctx), typeParams, ctx, ',');
}
if (additionalParameters > 0) {
for (let i = 0; i < additionalParameters; i++) {
if (i > 0 || suppliedParameters > 0) ctx.print(null, ',');
ctx.print(null, 'any');
}
}
ctx.print(null, `>`);
}
}
}
private _printColonType(type: o.Type, ctx: EmitterVisitorContext, defaultType?: string) {
if (type !== o.INFERRED_TYPE) {
ctx.print(null, ':');
this.visitType(type, ctx, defaultType);
}
}
}

View File

@ -0,0 +1,44 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ValueTransformer, visitValue} from '../util';
import * as o from './output_ast';
export const QUOTED_KEYS = '$quoted$';
export function convertValueToOutputAst(value: any, type: o.Type = null): o.Expression {
return visitValue(value, new _ValueOutputAstTransformer(), type);
}
class _ValueOutputAstTransformer implements ValueTransformer {
visitArray(arr: any[], type: o.Type): o.Expression {
return o.literalArr(arr.map(value => visitValue(value, this, null)), type);
}
visitStringMap(map: {[key: string]: any}, type: o.MapType): o.Expression {
const entries: o.LiteralMapEntry[] = [];
const quotedSet = new Set<string>(map && map[QUOTED_KEYS]);
Object.keys(map).forEach(key => {
entries.push(
new o.LiteralMapEntry(key, visitValue(map[key], this, null), quotedSet.has(key)));
});
return new o.LiteralMapExpr(entries, type);
}
visitPrimitive(value: any, type: o.Type): o.Expression { return o.literal(value, type); }
visitOther(value: any, type: o.Type): o.Expression {
if (value instanceof o.Expression) {
return value;
} else {
return o.importExpr({reference: value});
}
}
}

View File

@ -0,0 +1,126 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as chars from './chars';
export class ParseLocation {
constructor(
public file: ParseSourceFile, public offset: number, public line: number,
public col: number) {}
toString(): string {
return this.offset != null ? `${this.file.url}@${this.line}:${this.col}` : this.file.url;
}
moveBy(delta: number): ParseLocation {
const source = this.file.content;
const len = source.length;
let offset = this.offset;
let line = this.line;
let col = this.col;
while (offset > 0 && delta < 0) {
offset--;
delta++;
const ch = source.charCodeAt(offset);
if (ch == chars.$LF) {
line--;
const priorLine = source.substr(0, offset - 1).lastIndexOf(String.fromCharCode(chars.$LF));
col = priorLine > 0 ? offset - priorLine : offset;
} else {
col--;
}
}
while (offset < len && delta > 0) {
const ch = source.charCodeAt(offset);
offset++;
delta--;
if (ch == chars.$LF) {
line++;
col = 0;
} else {
col++;
}
}
return new ParseLocation(this.file, offset, line, col);
}
// Return the source around the location
// Up to `maxChars` or `maxLines` on each side of the location
getContext(maxChars: number, maxLines: number): {before: string, after: string} {
const content = this.file.content;
let startOffset = this.offset;
if (startOffset != null) {
if (startOffset > content.length - 1) {
startOffset = content.length - 1;
}
let endOffset = startOffset;
let ctxChars = 0;
let ctxLines = 0;
while (ctxChars < maxChars && startOffset > 0) {
startOffset--;
ctxChars++;
if (content[startOffset] == '\n') {
if (++ctxLines == maxLines) {
break;
}
}
}
ctxChars = 0;
ctxLines = 0;
while (ctxChars < maxChars && endOffset < content.length - 1) {
endOffset++;
ctxChars++;
if (content[endOffset] == '\n') {
if (++ctxLines == maxLines) {
break;
}
}
}
return {
before: content.substring(startOffset, this.offset),
after: content.substring(this.offset, endOffset + 1),
};
}
return null;
}
}
export class ParseSourceFile {
constructor(public content: string, public url: string) {}
}
export class ParseSourceSpan {
constructor(
public start: ParseLocation, public end: ParseLocation, public details: string = null) {}
toString(): string {
return this.start.file.content.substring(this.start.offset, this.end.offset);
}
}
export enum ParseErrorLevel {
WARNING,
FATAL
}
export class ParseError {
constructor(
public span: ParseSourceSpan, public msg: string,
public level: ParseErrorLevel = ParseErrorLevel.FATAL) {}
toString(): string {
const ctx = this.span.start.getContext(100, 3);
const contextStr = ctx ? ` ("${ctx.before}[ERROR ->]${ctx.after}")` : '';
const details = this.span.details ? `, ${this.span.details}` : '';
return `${this.msg}${contextStr}: ${this.span.start}${details}`;
}
}

View File

@ -0,0 +1,49 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Pipe, Type, resolveForwardRef, ɵReflectorReader, ɵreflector, ɵstringify as stringify} from '@angular/core';
import {findLast} from './directive_resolver';
import {CompilerInjectable} from './injectable';
function _isPipeMetadata(type: any): boolean {
return type instanceof Pipe;
}
/**
* Resolve a `Type` for {@link Pipe}.
*
* This interface can be overridden by the application developer to create custom behavior.
*
* See {@link Compiler}
*/
@CompilerInjectable()
export class PipeResolver {
constructor(private _reflector: ɵReflectorReader = ɵreflector) {}
isPipe(type: Type<any>) {
const typeMetadata = this._reflector.annotations(resolveForwardRef(type));
return typeMetadata && typeMetadata.some(_isPipeMetadata);
}
/**
* Return {@link Pipe} for a given `Type`.
*/
resolve(type: Type<any>, throwIfNotFound = true): Pipe {
const metas = this._reflector.annotations(resolveForwardRef(type));
if (metas) {
const annotation = findLast(metas, _isPipeMetadata);
if (annotation) {
return annotation;
}
}
if (throwIfNotFound) {
throw new Error(`No Pipe decorator found on ${stringify(type)}`);
}
return null;
}
}

View File

@ -0,0 +1,512 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompileNgModuleMetadata, CompileProviderMetadata, CompileQueryMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenName, tokenReference} from './compile_metadata';
import {Identifiers, createIdentifierToken, resolveIdentifier} from './identifiers';
import {ParseError, ParseSourceSpan} from './parse_util';
import {AttrAst, DirectiveAst, ProviderAst, ProviderAstType, QueryMatch, ReferenceAst} from './template_parser/template_ast';
export class ProviderError extends ParseError {
constructor(message: string, span: ParseSourceSpan) { super(span, message); }
}
export interface QueryWithId {
meta: CompileQueryMetadata;
queryId: number;
}
export class ProviderViewContext {
/**
* @internal
*/
viewQueries: Map<any, QueryWithId[]>;
/**
* @internal
*/
viewProviders: Map<any, boolean>;
errors: ProviderError[] = [];
constructor(public component: CompileDirectiveMetadata, public sourceSpan: ParseSourceSpan) {
this.viewQueries = _getViewQueries(component);
this.viewProviders = new Map<any, boolean>();
component.viewProviders.forEach((provider) => {
if (this.viewProviders.get(tokenReference(provider.token)) == null) {
this.viewProviders.set(tokenReference(provider.token), true);
}
});
}
}
export class ProviderElementContext {
private _contentQueries: Map<any, QueryWithId[]>;
private _transformedProviders = new Map<any, ProviderAst>();
private _seenProviders = new Map<any, boolean>();
private _allProviders: Map<any, ProviderAst>;
private _attrs: {[key: string]: string};
private _hasViewContainer: boolean = false;
private _queriedTokens = new Map<any, QueryMatch[]>();
constructor(
public viewContext: ProviderViewContext, private _parent: ProviderElementContext,
private _isViewRoot: boolean, private _directiveAsts: DirectiveAst[], attrs: AttrAst[],
refs: ReferenceAst[], isTemplate: boolean, contentQueryStartId: number,
private _sourceSpan: ParseSourceSpan) {
this._attrs = {};
attrs.forEach((attrAst) => this._attrs[attrAst.name] = attrAst.value);
const directivesMeta = _directiveAsts.map(directiveAst => directiveAst.directive);
this._allProviders =
_resolveProvidersFromDirectives(directivesMeta, _sourceSpan, viewContext.errors);
this._contentQueries = _getContentQueries(contentQueryStartId, directivesMeta);
Array.from(this._allProviders.values()).forEach((provider) => {
this._addQueryReadsTo(provider.token, provider.token, this._queriedTokens);
});
if (isTemplate) {
const templateRefId = createIdentifierToken(Identifiers.TemplateRef);
this._addQueryReadsTo(templateRefId, templateRefId, this._queriedTokens);
}
refs.forEach((refAst) => {
let defaultQueryValue = refAst.value || createIdentifierToken(Identifiers.ElementRef);
this._addQueryReadsTo({value: refAst.name}, defaultQueryValue, this._queriedTokens);
});
if (this._queriedTokens.get(resolveIdentifier(Identifiers.ViewContainerRef))) {
this._hasViewContainer = true;
}
// create the providers that we know are eager first
Array.from(this._allProviders.values()).forEach((provider) => {
const eager = provider.eager || this._queriedTokens.get(tokenReference(provider.token));
if (eager) {
this._getOrCreateLocalProvider(provider.providerType, provider.token, true);
}
});
}
afterElement() {
// collect lazy providers
Array.from(this._allProviders.values()).forEach((provider) => {
this._getOrCreateLocalProvider(provider.providerType, provider.token, false);
});
}
get transformProviders(): ProviderAst[] {
return Array.from(this._transformedProviders.values());
}
get transformedDirectiveAsts(): DirectiveAst[] {
const sortedProviderTypes = this.transformProviders.map(provider => provider.token.identifier);
const sortedDirectives = this._directiveAsts.slice();
sortedDirectives.sort(
(dir1, dir2) => sortedProviderTypes.indexOf(dir1.directive.type) -
sortedProviderTypes.indexOf(dir2.directive.type));
return sortedDirectives;
}
get transformedHasViewContainer(): boolean { return this._hasViewContainer; }
get queryMatches(): QueryMatch[] {
const allMatches: QueryMatch[] = [];
this._queriedTokens.forEach((matches: QueryMatch[]) => { allMatches.push(...matches); });
return allMatches;
}
private _addQueryReadsTo(
token: CompileTokenMetadata, defaultValue: CompileTokenMetadata,
queryReadTokens: Map<any, QueryMatch[]>) {
this._getQueriesFor(token).forEach((query) => {
const queryValue = query.meta.read || defaultValue;
const tokenRef = tokenReference(queryValue);
let queryMatches = queryReadTokens.get(tokenRef);
if (!queryMatches) {
queryMatches = [];
queryReadTokens.set(tokenRef, queryMatches);
}
queryMatches.push({queryId: query.queryId, value: queryValue});
});
}
private _getQueriesFor(token: CompileTokenMetadata): QueryWithId[] {
const result: QueryWithId[] = [];
let currentEl: ProviderElementContext = this;
let distance = 0;
let queries: QueryWithId[];
while (currentEl !== null) {
queries = currentEl._contentQueries.get(tokenReference(token));
if (queries) {
result.push(...queries.filter((query) => query.meta.descendants || distance <= 1));
}
if (currentEl._directiveAsts.length > 0) {
distance++;
}
currentEl = currentEl._parent;
}
queries = this.viewContext.viewQueries.get(tokenReference(token));
if (queries) {
result.push(...queries);
}
return result;
}
private _getOrCreateLocalProvider(
requestingProviderType: ProviderAstType, token: CompileTokenMetadata,
eager: boolean): ProviderAst {
const resolvedProvider = this._allProviders.get(tokenReference(token));
if (!resolvedProvider || ((requestingProviderType === ProviderAstType.Directive ||
requestingProviderType === ProviderAstType.PublicService) &&
resolvedProvider.providerType === ProviderAstType.PrivateService) ||
((requestingProviderType === ProviderAstType.PrivateService ||
requestingProviderType === ProviderAstType.PublicService) &&
resolvedProvider.providerType === ProviderAstType.Builtin)) {
return null;
}
let transformedProviderAst = this._transformedProviders.get(tokenReference(token));
if (transformedProviderAst) {
return transformedProviderAst;
}
if (this._seenProviders.get(tokenReference(token)) != null) {
this.viewContext.errors.push(new ProviderError(
`Cannot instantiate cyclic dependency! ${tokenName(token)}`, this._sourceSpan));
return null;
}
this._seenProviders.set(tokenReference(token), true);
const transformedProviders = resolvedProvider.providers.map((provider) => {
let transformedUseValue = provider.useValue;
let transformedUseExisting = provider.useExisting;
let transformedDeps: CompileDiDependencyMetadata[];
if (provider.useExisting != null) {
const existingDiDep = this._getDependency(
resolvedProvider.providerType, {token: provider.useExisting}, eager);
if (existingDiDep.token != null) {
transformedUseExisting = existingDiDep.token;
} else {
transformedUseExisting = null;
transformedUseValue = existingDiDep.value;
}
} else if (provider.useFactory) {
const deps = provider.deps || provider.useFactory.diDeps;
transformedDeps =
deps.map((dep) => this._getDependency(resolvedProvider.providerType, dep, eager));
} else if (provider.useClass) {
const deps = provider.deps || provider.useClass.diDeps;
transformedDeps =
deps.map((dep) => this._getDependency(resolvedProvider.providerType, dep, eager));
}
return _transformProvider(provider, {
useExisting: transformedUseExisting,
useValue: transformedUseValue,
deps: transformedDeps
});
});
transformedProviderAst =
_transformProviderAst(resolvedProvider, {eager: eager, providers: transformedProviders});
this._transformedProviders.set(tokenReference(token), transformedProviderAst);
return transformedProviderAst;
}
private _getLocalDependency(
requestingProviderType: ProviderAstType, dep: CompileDiDependencyMetadata,
eager: boolean = null): CompileDiDependencyMetadata {
if (dep.isAttribute) {
const attrValue = this._attrs[dep.token.value];
return {isValue: true, value: attrValue == null ? null : attrValue};
}
if (dep.token != null) {
// access builtints
if ((requestingProviderType === ProviderAstType.Directive ||
requestingProviderType === ProviderAstType.Component)) {
if (tokenReference(dep.token) === resolveIdentifier(Identifiers.Renderer) ||
tokenReference(dep.token) === resolveIdentifier(Identifiers.ElementRef) ||
tokenReference(dep.token) === resolveIdentifier(Identifiers.ChangeDetectorRef) ||
tokenReference(dep.token) === resolveIdentifier(Identifiers.TemplateRef)) {
return dep;
}
if (tokenReference(dep.token) === resolveIdentifier(Identifiers.ViewContainerRef)) {
this._hasViewContainer = true;
}
}
// access the injector
if (tokenReference(dep.token) === resolveIdentifier(Identifiers.Injector)) {
return dep;
}
// access providers
if (this._getOrCreateLocalProvider(requestingProviderType, dep.token, eager) != null) {
return dep;
}
}
return null;
}
private _getDependency(
requestingProviderType: ProviderAstType, dep: CompileDiDependencyMetadata,
eager: boolean = null): CompileDiDependencyMetadata {
let currElement: ProviderElementContext = this;
let currEager: boolean = eager;
let result: CompileDiDependencyMetadata = null;
if (!dep.isSkipSelf) {
result = this._getLocalDependency(requestingProviderType, dep, eager);
}
if (dep.isSelf) {
if (!result && dep.isOptional) {
result = {isValue: true, value: null};
}
} else {
// check parent elements
while (!result && currElement._parent) {
const prevElement = currElement;
currElement = currElement._parent;
if (prevElement._isViewRoot) {
currEager = false;
}
result = currElement._getLocalDependency(ProviderAstType.PublicService, dep, currEager);
}
// check @Host restriction
if (!result) {
if (!dep.isHost || this.viewContext.component.isHost ||
this.viewContext.component.type.reference === tokenReference(dep.token) ||
this.viewContext.viewProviders.get(tokenReference(dep.token)) != null) {
result = dep;
} else {
result = dep.isOptional ? result = {isValue: true, value: null} : null;
}
}
}
if (!result) {
this.viewContext.errors.push(
new ProviderError(`No provider for ${tokenName(dep.token)}`, this._sourceSpan));
}
return result;
}
}
export class NgModuleProviderAnalyzer {
private _transformedProviders = new Map<any, ProviderAst>();
private _seenProviders = new Map<any, boolean>();
private _allProviders: Map<any, ProviderAst>;
private _errors: ProviderError[] = [];
constructor(
ngModule: CompileNgModuleMetadata, extraProviders: CompileProviderMetadata[],
sourceSpan: ParseSourceSpan) {
this._allProviders = new Map<any, ProviderAst>();
ngModule.transitiveModule.modules.forEach((ngModuleType: CompileTypeMetadata) => {
const ngModuleProvider = {token: {identifier: ngModuleType}, useClass: ngModuleType};
_resolveProviders(
[ngModuleProvider], ProviderAstType.PublicService, true, sourceSpan, this._errors,
this._allProviders);
});
_resolveProviders(
ngModule.transitiveModule.providers.map(entry => entry.provider).concat(extraProviders),
ProviderAstType.PublicService, false, sourceSpan, this._errors, this._allProviders);
}
parse(): ProviderAst[] {
Array.from(this._allProviders.values()).forEach((provider) => {
this._getOrCreateLocalProvider(provider.token, provider.eager);
});
if (this._errors.length > 0) {
const errorString = this._errors.join('\n');
throw new Error(`Provider parse errors:\n${errorString}`);
}
return Array.from(this._transformedProviders.values());
}
private _getOrCreateLocalProvider(token: CompileTokenMetadata, eager: boolean): ProviderAst {
const resolvedProvider = this._allProviders.get(tokenReference(token));
if (!resolvedProvider) {
return null;
}
let transformedProviderAst = this._transformedProviders.get(tokenReference(token));
if (transformedProviderAst) {
return transformedProviderAst;
}
if (this._seenProviders.get(tokenReference(token)) != null) {
this._errors.push(new ProviderError(
`Cannot instantiate cyclic dependency! ${tokenName(token)}`,
resolvedProvider.sourceSpan));
return null;
}
this._seenProviders.set(tokenReference(token), true);
const transformedProviders = resolvedProvider.providers.map((provider) => {
let transformedUseValue = provider.useValue;
let transformedUseExisting = provider.useExisting;
let transformedDeps: CompileDiDependencyMetadata[];
if (provider.useExisting != null) {
const existingDiDep =
this._getDependency({token: provider.useExisting}, eager, resolvedProvider.sourceSpan);
if (existingDiDep.token != null) {
transformedUseExisting = existingDiDep.token;
} else {
transformedUseExisting = null;
transformedUseValue = existingDiDep.value;
}
} else if (provider.useFactory) {
const deps = provider.deps || provider.useFactory.diDeps;
transformedDeps =
deps.map((dep) => this._getDependency(dep, eager, resolvedProvider.sourceSpan));
} else if (provider.useClass) {
const deps = provider.deps || provider.useClass.diDeps;
transformedDeps =
deps.map((dep) => this._getDependency(dep, eager, resolvedProvider.sourceSpan));
}
return _transformProvider(provider, {
useExisting: transformedUseExisting,
useValue: transformedUseValue,
deps: transformedDeps
});
});
transformedProviderAst =
_transformProviderAst(resolvedProvider, {eager: eager, providers: transformedProviders});
this._transformedProviders.set(tokenReference(token), transformedProviderAst);
return transformedProviderAst;
}
private _getDependency(
dep: CompileDiDependencyMetadata, eager: boolean = null,
requestorSourceSpan: ParseSourceSpan): CompileDiDependencyMetadata {
let foundLocal = false;
if (!dep.isSkipSelf && dep.token != null) {
// access the injector
if (tokenReference(dep.token) === resolveIdentifier(Identifiers.Injector) ||
tokenReference(dep.token) === resolveIdentifier(Identifiers.ComponentFactoryResolver)) {
foundLocal = true;
// access providers
} else if (this._getOrCreateLocalProvider(dep.token, eager) != null) {
foundLocal = true;
}
}
let result: CompileDiDependencyMetadata = dep;
if (dep.isSelf && !foundLocal) {
if (dep.isOptional) {
result = {isValue: true, value: null};
} else {
this._errors.push(
new ProviderError(`No provider for ${tokenName(dep.token)}`, requestorSourceSpan));
}
}
return result;
}
}
function _transformProvider(
provider: CompileProviderMetadata,
{useExisting, useValue, deps}:
{useExisting: CompileTokenMetadata, useValue: any, deps: CompileDiDependencyMetadata[]}) {
return {
token: provider.token,
useClass: provider.useClass,
useExisting: useExisting,
useFactory: provider.useFactory,
useValue: useValue,
deps: deps,
multi: provider.multi
};
}
function _transformProviderAst(
provider: ProviderAst,
{eager, providers}: {eager: boolean, providers: CompileProviderMetadata[]}): ProviderAst {
return new ProviderAst(
provider.token, provider.multiProvider, provider.eager || eager, providers,
provider.providerType, provider.lifecycleHooks, provider.sourceSpan);
}
function _resolveProvidersFromDirectives(
directives: CompileDirectiveSummary[], sourceSpan: ParseSourceSpan,
targetErrors: ParseError[]): Map<any, ProviderAst> {
const providersByToken = new Map<any, ProviderAst>();
directives.forEach((directive) => {
const dirProvider:
CompileProviderMetadata = {token: {identifier: directive.type}, useClass: directive.type};
_resolveProviders(
[dirProvider],
directive.isComponent ? ProviderAstType.Component : ProviderAstType.Directive, true,
sourceSpan, targetErrors, providersByToken);
});
// Note: directives need to be able to overwrite providers of a component!
const directivesWithComponentFirst =
directives.filter(dir => dir.isComponent).concat(directives.filter(dir => !dir.isComponent));
directivesWithComponentFirst.forEach((directive) => {
_resolveProviders(
directive.providers, ProviderAstType.PublicService, false, sourceSpan, targetErrors,
providersByToken);
_resolveProviders(
directive.viewProviders, ProviderAstType.PrivateService, false, sourceSpan, targetErrors,
providersByToken);
});
return providersByToken;
}
function _resolveProviders(
providers: CompileProviderMetadata[], providerType: ProviderAstType, eager: boolean,
sourceSpan: ParseSourceSpan, targetErrors: ParseError[],
targetProvidersByToken: Map<any, ProviderAst>) {
providers.forEach((provider) => {
let resolvedProvider = targetProvidersByToken.get(tokenReference(provider.token));
if (resolvedProvider != null && !!resolvedProvider.multiProvider !== !!provider.multi) {
targetErrors.push(new ProviderError(
`Mixing multi and non multi provider is not possible for token ${tokenName(resolvedProvider.token)}`,
sourceSpan));
}
if (!resolvedProvider) {
const lifecycleHooks = provider.token.identifier &&
(<CompileTypeMetadata>provider.token.identifier).lifecycleHooks ?
(<CompileTypeMetadata>provider.token.identifier).lifecycleHooks :
[];
resolvedProvider = new ProviderAst(
provider.token, provider.multi, eager || lifecycleHooks.length > 0, [provider],
providerType, lifecycleHooks, sourceSpan);
targetProvidersByToken.set(tokenReference(provider.token), resolvedProvider);
} else {
if (!provider.multi) {
resolvedProvider.providers.length = 0;
}
resolvedProvider.providers.push(provider);
}
});
}
function _getViewQueries(component: CompileDirectiveMetadata): Map<any, QueryWithId[]> {
// Note: queries start with id 1 so we can use the number in a Bloom filter!
let viewQueryId = 1;
const viewQueries = new Map<any, QueryWithId[]>();
if (component.viewQueries) {
component.viewQueries.forEach(
(query) => _addQueryToTokenMap(viewQueries, {meta: query, queryId: viewQueryId++}));
}
return viewQueries;
}
function _getContentQueries(
contentQueryStartId: number, directives: CompileDirectiveSummary[]): Map<any, QueryWithId[]> {
let contentQueryId = contentQueryStartId;
const contentQueries = new Map<any, QueryWithId[]>();
directives.forEach((directive, directiveIndex) => {
if (directive.queries) {
directive.queries.forEach(
(query) => _addQueryToTokenMap(contentQueries, {meta: query, queryId: contentQueryId++}));
}
});
return contentQueries;
}
function _addQueryToTokenMap(map: Map<any, QueryWithId[]>, query: QueryWithId) {
query.meta.selectors.forEach((token: CompileTokenMetadata) => {
let entry = map.get(tokenReference(token));
if (!entry) {
entry = [];
map.set(tokenReference(token), entry);
}
entry.push(query);
});
}

View File

@ -0,0 +1,15 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* An interface for retrieving documents by URL that the compiler uses
* to load templates.
*/
export class ResourceLoader {
get(url: string): Promise<string> { return null; }
}

View File

@ -0,0 +1,440 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AUTO_STYLE, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata, SecurityContext} from '@angular/core';
import {CompilerInjectable} from '../injectable';
import {dashCaseToCamelCase} from '../util';
import {SECURITY_SCHEMA} from './dom_security_schema';
import {ElementSchemaRegistry} from './element_schema_registry';
const BOOLEAN = 'boolean';
const NUMBER = 'number';
const STRING = 'string';
const OBJECT = 'object';
/**
* This array represents the DOM schema. It encodes inheritance, properties, and events.
*
* ## Overview
*
* Each line represents one kind of element. The `element_inheritance` and properties are joined
* using `element_inheritance|properties` syntax.
*
* ## Element Inheritance
*
* The `element_inheritance` can be further subdivided as `element1,element2,...^parentElement`.
* Here the individual elements are separated by `,` (commas). Every element in the list
* has identical properties.
*
* An `element` may inherit additional properties from `parentElement` If no `^parentElement` is
* specified then `""` (blank) element is assumed.
*
* NOTE: The blank element inherits from root `[Element]` element, the super element of all
* elements.
*
* NOTE an element prefix such as `:svg:` has no special meaning to the schema.
*
* ## Properties
*
* Each element has a set of properties separated by `,` (commas). Each property can be prefixed
* by a special character designating its type:
*
* - (no prefix): property is a string.
* - `*`: property represents an event.
* - `!`: property is a boolean.
* - `#`: property is a number.
* - `%`: property is an object.
*
* ## Query
*
* The class creates an internal squas representation which allows to easily answer the query of
* if a given property exist on a given element.
*
* NOTE: We don't yet support querying for types or events.
* NOTE: This schema is auto extracted from `schema_extractor.ts` located in the test folder,
* see dom_element_schema_registry_spec.ts
*/
// =================================================================================================
// =================================================================================================
// =========== S T O P - S T O P - S T O P - S T O P - S T O P - S T O P ===========
// =================================================================================================
// =================================================================================================
//
// DO NOT EDIT THIS DOM SCHEMA WITHOUT A SECURITY REVIEW!
//
// Newly added properties must be security reviewed and assigned an appropriate SecurityContext in
// dom_security_schema.ts. Reach out to mprobst & rjamet for details.
//
// =================================================================================================
const SCHEMA:
string[] =
[
'[Element]|textContent,%classList,className,id,innerHTML,*beforecopy,*beforecut,*beforepaste,*copy,*cut,*paste,*search,*selectstart,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerHTML,#scrollLeft,#scrollTop',
'[HTMLElement]^[Element]|accessKey,contentEditable,dir,!draggable,!hidden,innerText,lang,*abort,*beforecopy,*beforecut,*beforepaste,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*message,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*mozfullscreenchange,*mozfullscreenerror,*mozpointerlockchange,*mozpointerlockerror,*paste,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*search,*seeked,*seeking,*select,*selectstart,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,*webglcontextcreationerror,*webglcontextlost,*webglcontextrestored,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate',
'abbr,address,article,aside,b,bdi,bdo,cite,code,dd,dfn,dt,em,figcaption,figure,footer,header,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,section,small,strong,sub,sup,u,var,wbr^[HTMLElement]|accessKey,contentEditable,dir,!draggable,!hidden,innerText,lang,*abort,*beforecopy,*beforecut,*beforepaste,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*message,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*mozfullscreenchange,*mozfullscreenerror,*mozpointerlockchange,*mozpointerlockerror,*paste,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*search,*seeked,*seeking,*select,*selectstart,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,*webglcontextcreationerror,*webglcontextlost,*webglcontextrestored,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate',
'media^[HTMLElement]|!autoplay,!controls,%crossOrigin,#currentTime,!defaultMuted,#defaultPlaybackRate,!disableRemotePlayback,!loop,!muted,*encrypted,#playbackRate,preload,src,%srcObject,#volume',
':svg:^[HTMLElement]|*abort,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*cuechange,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*seeked,*seeking,*select,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,%style,#tabIndex',
':svg:graphics^:svg:|',
':svg:animation^:svg:|*begin,*end,*repeat',
':svg:geometry^:svg:|',
':svg:componentTransferFunction^:svg:|',
':svg:gradient^:svg:|',
':svg:textContent^:svg:graphics|',
':svg:textPositioning^:svg:textContent|',
'a^[HTMLElement]|charset,coords,download,hash,host,hostname,href,hreflang,name,password,pathname,ping,port,protocol,referrerPolicy,rel,rev,search,shape,target,text,type,username',
'area^[HTMLElement]|alt,coords,hash,host,hostname,href,!noHref,password,pathname,ping,port,protocol,referrerPolicy,search,shape,target,username',
'audio^media|',
'br^[HTMLElement]|clear',
'base^[HTMLElement]|href,target',
'body^[HTMLElement]|aLink,background,bgColor,link,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,text,vLink',
'button^[HTMLElement]|!autofocus,!disabled,formAction,formEnctype,formMethod,!formNoValidate,formTarget,name,type,value',
'canvas^[HTMLElement]|#height,#width',
'content^[HTMLElement]|select',
'dl^[HTMLElement]|!compact',
'datalist^[HTMLElement]|',
'details^[HTMLElement]|!open',
'dialog^[HTMLElement]|!open,returnValue',
'dir^[HTMLElement]|!compact',
'div^[HTMLElement]|align',
'embed^[HTMLElement]|align,height,name,src,type,width',
'fieldset^[HTMLElement]|!disabled,name',
'font^[HTMLElement]|color,face,size',
'form^[HTMLElement]|acceptCharset,action,autocomplete,encoding,enctype,method,name,!noValidate,target',
'frame^[HTMLElement]|frameBorder,longDesc,marginHeight,marginWidth,name,!noResize,scrolling,src',
'frameset^[HTMLElement]|cols,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,rows',
'hr^[HTMLElement]|align,color,!noShade,size,width',
'head^[HTMLElement]|',
'h1,h2,h3,h4,h5,h6^[HTMLElement]|align',
'html^[HTMLElement]|version',
'iframe^[HTMLElement]|align,!allowFullscreen,frameBorder,height,longDesc,marginHeight,marginWidth,name,referrerPolicy,%sandbox,scrolling,src,srcdoc,width',
'img^[HTMLElement]|align,alt,border,%crossOrigin,#height,#hspace,!isMap,longDesc,lowsrc,name,referrerPolicy,sizes,src,srcset,useMap,#vspace,#width',
'input^[HTMLElement]|accept,align,alt,autocapitalize,autocomplete,!autofocus,!checked,!defaultChecked,defaultValue,dirName,!disabled,%files,formAction,formEnctype,formMethod,!formNoValidate,formTarget,#height,!incremental,!indeterminate,max,#maxLength,min,#minLength,!multiple,name,pattern,placeholder,!readOnly,!required,selectionDirection,#selectionEnd,#selectionStart,#size,src,step,type,useMap,value,%valueAsDate,#valueAsNumber,#width',
'keygen^[HTMLElement]|!autofocus,challenge,!disabled,keytype,name',
'li^[HTMLElement]|type,#value',
'label^[HTMLElement]|htmlFor',
'legend^[HTMLElement]|align',
'link^[HTMLElement]|as,charset,%crossOrigin,!disabled,href,hreflang,integrity,media,rel,%relList,rev,%sizes,target,type',
'map^[HTMLElement]|name',
'marquee^[HTMLElement]|behavior,bgColor,direction,height,#hspace,#loop,#scrollAmount,#scrollDelay,!trueSpeed,#vspace,width',
'menu^[HTMLElement]|!compact',
'meta^[HTMLElement]|content,httpEquiv,name,scheme',
'meter^[HTMLElement]|#high,#low,#max,#min,#optimum,#value',
'ins,del^[HTMLElement]|cite,dateTime',
'ol^[HTMLElement]|!compact,!reversed,#start,type',
'object^[HTMLElement]|align,archive,border,code,codeBase,codeType,data,!declare,height,#hspace,name,standby,type,useMap,#vspace,width',
'optgroup^[HTMLElement]|!disabled,label',
'option^[HTMLElement]|!defaultSelected,!disabled,label,!selected,text,value',
'output^[HTMLElement]|defaultValue,%htmlFor,name,value',
'p^[HTMLElement]|align',
'param^[HTMLElement]|name,type,value,valueType',
'picture^[HTMLElement]|',
'pre^[HTMLElement]|#width',
'progress^[HTMLElement]|#max,#value',
'q,blockquote,cite^[HTMLElement]|',
'script^[HTMLElement]|!async,charset,%crossOrigin,!defer,event,htmlFor,integrity,src,text,type',
'select^[HTMLElement]|!autofocus,!disabled,#length,!multiple,name,!required,#selectedIndex,#size,value',
'shadow^[HTMLElement]|',
'source^[HTMLElement]|media,sizes,src,srcset,type',
'span^[HTMLElement]|',
'style^[HTMLElement]|!disabled,media,type',
'caption^[HTMLElement]|align',
'th,td^[HTMLElement]|abbr,align,axis,bgColor,ch,chOff,#colSpan,headers,height,!noWrap,#rowSpan,scope,vAlign,width',
'col,colgroup^[HTMLElement]|align,ch,chOff,#span,vAlign,width',
'table^[HTMLElement]|align,bgColor,border,%caption,cellPadding,cellSpacing,frame,rules,summary,%tFoot,%tHead,width',
'tr^[HTMLElement]|align,bgColor,ch,chOff,vAlign',
'tfoot,thead,tbody^[HTMLElement]|align,ch,chOff,vAlign',
'template^[HTMLElement]|',
'textarea^[HTMLElement]|autocapitalize,!autofocus,#cols,defaultValue,dirName,!disabled,#maxLength,#minLength,name,placeholder,!readOnly,!required,#rows,selectionDirection,#selectionEnd,#selectionStart,value,wrap',
'title^[HTMLElement]|text',
'track^[HTMLElement]|!default,kind,label,src,srclang',
'ul^[HTMLElement]|!compact,type',
'unknown^[HTMLElement]|',
'video^media|#height,poster,#width',
':svg:a^:svg:graphics|',
':svg:animate^:svg:animation|',
':svg:animateMotion^:svg:animation|',
':svg:animateTransform^:svg:animation|',
':svg:circle^:svg:geometry|',
':svg:clipPath^:svg:graphics|',
':svg:cursor^:svg:|',
':svg:defs^:svg:graphics|',
':svg:desc^:svg:|',
':svg:discard^:svg:|',
':svg:ellipse^:svg:geometry|',
':svg:feBlend^:svg:|',
':svg:feColorMatrix^:svg:|',
':svg:feComponentTransfer^:svg:|',
':svg:feComposite^:svg:|',
':svg:feConvolveMatrix^:svg:|',
':svg:feDiffuseLighting^:svg:|',
':svg:feDisplacementMap^:svg:|',
':svg:feDistantLight^:svg:|',
':svg:feDropShadow^:svg:|',
':svg:feFlood^:svg:|',
':svg:feFuncA^:svg:componentTransferFunction|',
':svg:feFuncB^:svg:componentTransferFunction|',
':svg:feFuncG^:svg:componentTransferFunction|',
':svg:feFuncR^:svg:componentTransferFunction|',
':svg:feGaussianBlur^:svg:|',
':svg:feImage^:svg:|',
':svg:feMerge^:svg:|',
':svg:feMergeNode^:svg:|',
':svg:feMorphology^:svg:|',
':svg:feOffset^:svg:|',
':svg:fePointLight^:svg:|',
':svg:feSpecularLighting^:svg:|',
':svg:feSpotLight^:svg:|',
':svg:feTile^:svg:|',
':svg:feTurbulence^:svg:|',
':svg:filter^:svg:|',
':svg:foreignObject^:svg:graphics|',
':svg:g^:svg:graphics|',
':svg:image^:svg:graphics|',
':svg:line^:svg:geometry|',
':svg:linearGradient^:svg:gradient|',
':svg:mpath^:svg:|',
':svg:marker^:svg:|',
':svg:mask^:svg:|',
':svg:metadata^:svg:|',
':svg:path^:svg:geometry|',
':svg:pattern^:svg:|',
':svg:polygon^:svg:geometry|',
':svg:polyline^:svg:geometry|',
':svg:radialGradient^:svg:gradient|',
':svg:rect^:svg:geometry|',
':svg:svg^:svg:graphics|#currentScale,#zoomAndPan',
':svg:script^:svg:|type',
':svg:set^:svg:animation|',
':svg:stop^:svg:|',
':svg:style^:svg:|!disabled,media,title,type',
':svg:switch^:svg:graphics|',
':svg:symbol^:svg:|',
':svg:tspan^:svg:textPositioning|',
':svg:text^:svg:textPositioning|',
':svg:textPath^:svg:textContent|',
':svg:title^:svg:|',
':svg:use^:svg:graphics|',
':svg:view^:svg:|#zoomAndPan',
'data^[HTMLElement]|value',
'menuitem^[HTMLElement]|type,label,icon,!disabled,!checked,radiogroup,!default',
'summary^[HTMLElement]|',
'time^[HTMLElement]|dateTime',
];
const _ATTR_TO_PROP: {[name: string]: string} = {
'class': 'className',
'for': 'htmlFor',
'formaction': 'formAction',
'innerHtml': 'innerHTML',
'readonly': 'readOnly',
'tabindex': 'tabIndex',
};
@CompilerInjectable()
export class DomElementSchemaRegistry extends ElementSchemaRegistry {
private _schema: {[element: string]: {[property: string]: string}} = {};
constructor() {
super();
SCHEMA.forEach(encodedType => {
const type: {[property: string]: string} = {};
const [strType, strProperties] = encodedType.split('|');
const properties = strProperties.split(',');
const [typeNames, superName] = strType.split('^');
typeNames.split(',').forEach(tag => this._schema[tag.toLowerCase()] = type);
const superType = superName && this._schema[superName.toLowerCase()];
if (superType) {
Object.keys(superType).forEach((prop: string) => { type[prop] = superType[prop]; });
}
properties.forEach((property: string) => {
if (property.length > 0) {
switch (property[0]) {
case '*':
// We don't yet support events.
// If ever allowing to bind to events, GO THROUGH A SECURITY REVIEW, allowing events
// will
// almost certainly introduce bad XSS vulnerabilities.
// type[property.substring(1)] = EVENT;
break;
case '!':
type[property.substring(1)] = BOOLEAN;
break;
case '#':
type[property.substring(1)] = NUMBER;
break;
case '%':
type[property.substring(1)] = OBJECT;
break;
default:
type[property] = STRING;
}
}
});
});
}
hasProperty(tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean {
if (schemaMetas.some((schema) => schema.name === NO_ERRORS_SCHEMA.name)) {
return true;
}
if (tagName.indexOf('-') > -1) {
if (tagName === 'ng-container' || tagName === 'ng-content') {
return false;
}
if (schemaMetas.some((schema) => schema.name === CUSTOM_ELEMENTS_SCHEMA.name)) {
// Can't tell now as we don't know which properties a custom element will get
// once it is instantiated
return true;
}
}
const elementProperties = this._schema[tagName.toLowerCase()] || this._schema['unknown'];
return !!elementProperties[propName];
}
hasElement(tagName: string, schemaMetas: SchemaMetadata[]): boolean {
if (schemaMetas.some((schema) => schema.name === NO_ERRORS_SCHEMA.name)) {
return true;
}
if (tagName.indexOf('-') > -1) {
if (tagName === 'ng-container' || tagName === 'ng-content') {
return true;
}
if (schemaMetas.some((schema) => schema.name === CUSTOM_ELEMENTS_SCHEMA.name)) {
// Allow any custom elements
return true;
}
}
return !!this._schema[tagName.toLowerCase()];
}
/**
* securityContext returns the security context for the given property on the given DOM tag.
*
* Tag and property name are statically known and cannot change at runtime, i.e. it is not
* possible to bind a value into a changing attribute or tag name.
*
* The filtering is white list based. All attributes in the schema above are assumed to have the
* 'NONE' security context, i.e. that they are safe inert string values. Only specific well known
* attack vectors are assigned their appropriate context.
*/
securityContext(tagName: string, propName: string, isAttribute: boolean): SecurityContext {
if (isAttribute) {
// NB: For security purposes, use the mapped property name, not the attribute name.
propName = this.getMappedPropName(propName);
}
// Make sure comparisons are case insensitive, so that case differences between attribute and
// property names do not have a security impact.
tagName = tagName.toLowerCase();
propName = propName.toLowerCase();
let ctx = SECURITY_SCHEMA[tagName + '|' + propName];
if (ctx) {
return ctx;
}
ctx = SECURITY_SCHEMA['*|' + propName];
return ctx ? ctx : SecurityContext.NONE;
}
getMappedPropName(propName: string): string { return _ATTR_TO_PROP[propName] || propName; }
getDefaultComponentElementName(): string { return 'ng-component'; }
validateProperty(name: string): {error: boolean, msg?: string} {
if (name.toLowerCase().startsWith('on')) {
const msg = `Binding to event property '${name}' is disallowed for security reasons, ` +
`please use (${name.slice(2)})=...` +
`\nIf '${name}' is a directive input, make sure the directive is imported by the` +
` current module.`;
return {error: true, msg: msg};
} else {
return {error: false};
}
}
validateAttribute(name: string): {error: boolean, msg?: string} {
if (name.toLowerCase().startsWith('on')) {
const msg = `Binding to event attribute '${name}' is disallowed for security reasons, ` +
`please use (${name.slice(2)})=...`;
return {error: true, msg: msg};
} else {
return {error: false};
}
}
allKnownElementNames(): string[] { return Object.keys(this._schema); }
normalizeAnimationStyleProperty(propName: string): string {
return dashCaseToCamelCase(propName);
}
normalizeAnimationStyleValue(camelCaseProp: string, userProvidedProp: string, val: string|number):
{error: string, value: string} {
let unit: string = '';
const strVal = val.toString().trim();
let errorMsg: string = null;
if (_isPixelDimensionStyle(camelCaseProp) && val !== 0 && val !== '0') {
if (typeof val === 'number') {
unit = 'px';
} else {
const valAndSuffixMatch = val.match(/^[+-]?[\d\.]+([a-z]*)$/);
if (valAndSuffixMatch && valAndSuffixMatch[1].length == 0) {
errorMsg = `Please provide a CSS unit value for ${userProvidedProp}:${val}`;
}
}
}
return {error: errorMsg, value: strVal + unit};
}
}
function _isPixelDimensionStyle(prop: string): boolean {
switch (prop) {
case 'width':
case 'height':
case 'minWidth':
case 'minHeight':
case 'maxWidth':
case 'maxHeight':
case 'left':
case 'top':
case 'bottom':
case 'right':
case 'fontSize':
case 'outlineWidth':
case 'outlineOffset':
case 'paddingTop':
case 'paddingLeft':
case 'paddingBottom':
case 'paddingRight':
case 'marginTop':
case 'marginLeft':
case 'marginBottom':
case 'marginRight':
case 'borderRadius':
case 'borderWidth':
case 'borderTopWidth':
case 'borderLeftWidth':
case 'borderRightWidth':
case 'borderBottomWidth':
case 'textIndent':
return true;
default:
return false;
}
}

View File

@ -0,0 +1,58 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SecurityContext} from '@angular/core';
// =================================================================================================
// =================================================================================================
// =========== S T O P - S T O P - S T O P - S T O P - S T O P - S T O P ===========
// =================================================================================================
// =================================================================================================
//
// DO NOT EDIT THIS LIST OF SECURITY SENSITIVE PROPERTIES WITHOUT A SECURITY REVIEW!
// Reach out to mprobst for details.
//
// =================================================================================================
/** Map from tagName|propertyName SecurityContext. Properties applying to all tags use '*'. */
export const SECURITY_SCHEMA: {[k: string]: SecurityContext} = {};
function registerContext(ctx: SecurityContext, specs: string[]) {
for (const spec of specs) SECURITY_SCHEMA[spec.toLowerCase()] = ctx;
}
// Case is insignificant below, all element and attribute names are lower-cased for lookup.
registerContext(SecurityContext.HTML, [
'iframe|srcdoc',
'*|innerHTML',
'*|outerHTML',
]);
registerContext(SecurityContext.STYLE, ['*|style']);
// NB: no SCRIPT contexts here, they are never allowed due to the parser stripping them.
registerContext(SecurityContext.URL, [
'*|formAction', 'area|href', 'area|ping', 'audio|src', 'a|href',
'a|ping', 'blockquote|cite', 'body|background', 'del|cite', 'form|action',
'img|src', 'img|srcset', 'input|src', 'ins|cite', 'q|cite',
'source|src', 'source|srcset', 'track|src', 'video|poster', 'video|src',
]);
registerContext(SecurityContext.RESOURCE_URL, [
'applet|code',
'applet|codebase',
'base|href',
'embed|src',
'frame|src',
'head|profile',
'html|manifest',
'iframe|src',
'link|href',
'media|src',
'object|codebase',
'object|data',
'script|src',
]);

View File

@ -0,0 +1,25 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SchemaMetadata, SecurityContext} from '@angular/core';
export abstract class ElementSchemaRegistry {
abstract hasProperty(tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean;
abstract hasElement(tagName: string, schemaMetas: SchemaMetadata[]): boolean;
abstract securityContext(elementName: string, propName: string, isAttribute: boolean):
SecurityContext;
abstract allKnownElementNames(): string[];
abstract getMappedPropName(propName: string): string;
abstract getDefaultComponentElementName(): string;
abstract validateProperty(name: string): {error: boolean, msg?: string};
abstract validateAttribute(name: string): {error: boolean, msg?: string};
abstract normalizeAnimationStyleProperty(propName: string): string;
abstract normalizeAnimationStyleValue(
camelCaseProp: string, userProvidedProp: string,
val: string|number): {error: string, value: string};
}

View File

@ -0,0 +1,370 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {getHtmlTagDefinition} from './ml_parser/html_tags';
const _SELECTOR_REGEXP = new RegExp(
'(\\:not\\()|' + //":not("
'([-\\w]+)|' + // "tag"
'(?:\\.([-\\w]+))|' + // ".class"
// "-" should appear first in the regexp below as FF31 parses "[.-\w]" as a range
'(?:\\[([-.\\w*]+)(?:=([\"\']?)([^\\]\"\']*)\\5)?\\])|' + // "[name]", "[name=value]",
// "[name="value"]",
// "[name='value']"
'(\\))|' + // ")"
'(\\s*,\\s*)', // ","
'g');
/**
* 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[] {
const results: CssSelector[] = [];
const _addResult = (res: CssSelector[], cssSel: CssSelector) => {
if (cssSel.notSelectors.length > 0 && !cssSel.element && cssSel.classNames.length == 0 &&
cssSel.attrs.length == 0) {
cssSel.element = '*';
}
res.push(cssSel);
};
let cssSelector = new CssSelector();
let match: string[];
let current = cssSelector;
let inNot = false;
_SELECTOR_REGEXP.lastIndex = 0;
while (match = _SELECTOR_REGEXP.exec(selector)) {
if (match[1]) {
if (inNot) {
throw new Error('Nesting :not is not allowed in a selector');
}
inNot = true;
current = new CssSelector();
cssSelector.notSelectors.push(current);
}
if (match[2]) {
current.setElement(match[2]);
}
if (match[3]) {
current.addClassName(match[3]);
}
if (match[4]) {
current.addAttribute(match[4], match[6]);
}
if (match[7]) {
inNot = false;
current = cssSelector;
}
if (match[8]) {
if (inNot) {
throw new Error('Multiple selectors in :not are not supported');
}
_addResult(results, cssSelector);
cssSelector = current = new CssSelector();
}
}
_addResult(results, cssSelector);
return results;
}
isElementSelector(): boolean {
return this.hasElementSelector() && this.classNames.length == 0 && this.attrs.length == 0 &&
this.notSelectors.length === 0;
}
hasElementSelector(): boolean { return !!this.element; }
setElement(element: string = null) { this.element = element; }
/** Gets a template string for an element that matches the selector. */
getMatchingElementTemplate(): string {
const tagName = this.element || 'div';
const classAttr = this.classNames.length > 0 ? ` class="${this.classNames.join(' ')}"` : '';
let attrs = '';
for (let i = 0; i < this.attrs.length; i += 2) {
const attrName = this.attrs[i];
const attrValue = this.attrs[i + 1] !== '' ? `="${this.attrs[i + 1]}"` : '';
attrs += ` ${attrName}${attrValue}`;
}
return getHtmlTagDefinition(tagName).isVoid ? `<${tagName}${classAttr}${attrs}/>` :
`<${tagName}${classAttr}${attrs}></${tagName}>`;
}
addAttribute(name: string, value: string = '') {
this.attrs.push(name, value && value.toLowerCase() || '');
}
addClassName(name: string) { this.classNames.push(name.toLowerCase()); }
toString(): string {
let res: string = this.element || '';
if (this.classNames) {
this.classNames.forEach(klass => res += `.${klass}`);
}
if (this.attrs) {
for (let i = 0; i < this.attrs.length; i += 2) {
const name = this.attrs[i];
const value = this.attrs[i + 1];
res += `[${name}${value ? '=' + value : ''}]`;
}
}
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 {
const 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) {
let listContext: SelectorListContext = null;
if (cssSelectors.length > 1) {
listContext = new SelectorListContext(cssSelectors);
this._listContexts.push(listContext);
}
for (let 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) {
let matcher: SelectorMatcher = this;
const element = cssSelector.element;
const classNames = cssSelector.classNames;
const attrs = cssSelector.attrs;
const selectable = new SelectorContext(cssSelector, callbackCtxt, listContext);
if (element) {
const isTerminal = attrs.length === 0 && classNames.length === 0;
if (isTerminal) {
this._addTerminal(matcher._elementMap, element, selectable);
} else {
matcher = this._addPartial(matcher._elementPartialMap, element);
}
}
if (classNames) {
for (let i = 0; i < classNames.length; i++) {
const isTerminal = attrs.length === 0 && i === classNames.length - 1;
const className = classNames[i];
if (isTerminal) {
this._addTerminal(matcher._classMap, className, selectable);
} else {
matcher = this._addPartial(matcher._classPartialMap, className);
}
}
}
if (attrs) {
for (let i = 0; i < attrs.length; i += 2) {
const isTerminal = i === attrs.length - 2;
const name = attrs[i];
const value = attrs[i + 1];
if (isTerminal) {
const terminalMap = matcher._attrValueMap;
let terminalValuesMap = terminalMap.get(name);
if (!terminalValuesMap) {
terminalValuesMap = new Map<string, SelectorContext[]>();
terminalMap.set(name, terminalValuesMap);
}
this._addTerminal(terminalValuesMap, value, selectable);
} else {
const partialMap = matcher._attrValuePartialMap;
let partialValuesMap = partialMap.get(name);
if (!partialValuesMap) {
partialValuesMap = new Map<string, SelectorMatcher>();
partialMap.set(name, partialValuesMap);
}
matcher = this._addPartial(partialValuesMap, value);
}
}
}
}
private _addTerminal(
map: Map<string, SelectorContext[]>, name: string, selectable: SelectorContext) {
let terminalList = map.get(name);
if (!terminalList) {
terminalList = [];
map.set(name, terminalList);
}
terminalList.push(selectable);
}
private _addPartial(map: Map<string, SelectorMatcher>, name: string): SelectorMatcher {
let matcher = map.get(name);
if (!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 {
let result = false;
const element = cssSelector.element;
const classNames = cssSelector.classNames;
const attrs = cssSelector.attrs;
for (let 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 (classNames) {
for (let i = 0; i < classNames.length; i++) {
const className = classNames[i];
result =
this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result;
result =
this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback) ||
result;
}
}
if (attrs) {
for (let i = 0; i < attrs.length; i += 2) {
const name = attrs[i];
const value = attrs[i + 1];
const terminalValuesMap = this._attrValueMap.get(name);
if (value) {
result =
this._matchTerminal(terminalValuesMap, '', cssSelector, matchedCallback) || result;
}
result =
this._matchTerminal(terminalValuesMap, value, cssSelector, matchedCallback) || result;
const partialValuesMap = this._attrValuePartialMap.get(name);
if (value) {
result = this._matchPartial(partialValuesMap, '', cssSelector, matchedCallback) || result;
}
result =
this._matchPartial(partialValuesMap, value, cssSelector, matchedCallback) || result;
}
}
return result;
}
/** @internal */
_matchTerminal(
map: Map<string, SelectorContext[]>, name: string, cssSelector: CssSelector,
matchedCallback: (c: CssSelector, a: any) => void): boolean {
if (!map || typeof name !== 'string') {
return false;
}
let selectables: SelectorContext[] = map.get(name) || [];
const starSelectables: SelectorContext[] = map.get('*');
if (starSelectables) {
selectables = selectables.concat(starSelectables);
}
if (selectables.length === 0) {
return false;
}
let selectable: SelectorContext;
let result = false;
for (let i = 0; i < selectables.length; i++) {
selectable = selectables[i];
result = selectable.finalize(cssSelector, matchedCallback) || result;
}
return result;
}
/** @internal */
_matchPartial(
map: Map<string, SelectorMatcher>, name: string, cssSelector: CssSelector,
matchedCallback: (c: CssSelector, a: any) => void): boolean {
if (!map || typeof name !== 'string') {
return false;
}
const nestedSelector = map.get(name);
if (!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 {
let result = true;
if (this.notSelectors.length > 0 && (!this.listContext || !this.listContext.alreadyMatched)) {
const notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors);
result = !notMatcher.match(cssSelector, null);
}
if (result && callback && (!this.listContext || !this.listContext.alreadyMatched)) {
if (this.listContext) {
this.listContext.alreadyMatched = true;
}
callback(this.selector, this.cbContext);
}
return result;
}
}

View File

@ -0,0 +1,598 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* 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;
}
* encapsulation: Styles defined within ShadowDOM, apply only to
dom inside the ShadowDOM. Polymer uses one of two techniques to implement
this feature.
By default, rules are prefixed with the host element tag name
as a descendant selector. This ensures styling does not leak out of the 'top'
of the element's ShadowDOM. For example,
div {
font-weight: bold;
}
becomes:
x-foo div {
font-weight: bold;
}
becomes:
Alternatively, if WebComponents.ShadowCSS.strictStyling is set to true then
selectors are scoped by adding an attribute selector suffix to each
simple selector that contains the host element tag name. Each element
in the element's ShadowDOM template is also given the scope attribute.
Thus, these rules match only elements that have the scope attribute.
For example, given a scope name of x-foo, a rule like this:
div {
font-weight: bold;
}
becomes:
div[x-foo] {
font-weight: bold;
}
Note that elements that are dynamically added to a scope must have the scope
selector added to them manually.
* upper/lower bound encapsulation: Styles which are defined outside a
shadowRoot should not cross the ShadowDOM boundary and should not apply
inside a shadowRoot.
This styling behavior is not emulated. Some possible ways to do this that
were rejected due to complexity and/or performance concerns include: (1) reset
every possible property for every possible selector for a given scope name;
(2) re-implement css in javascript.
As an alternative, users should make sure to use selectors
specific to the scope in which they are working.
* ::distributed: This behavior is not emulated. It's often not necessary
to style the contents of a specific insertion point and instead, descendants
of the host element can be styled selectively. Users can also create an
extra node around an insertion point and style that node's contents
via descendent selectors. For example, with a shadowRoot like this:
<style>
::content(div) {
background: red;
}
</style>
<content></content>
could become:
<style>
/ *@polyfill .content-container div * /
::content(div) {
background: red;
}
</style>
<div class="content-container">
<content></content>
</div>
Note the use of @polyfill in the comment above a ShadowDOM specific style
declaration. This is a directive to the styling shim to use the selector
in comments in lieu of the next selector when running under polyfill.
*/
export class ShadowCss {
strictStyling: boolean = true;
constructor() {}
/*
* Shim some cssText with the given selector. Returns cssText that can
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
*
* When strictStyling is true:
* - selector is the attribute added to all elements inside the host,
* - hostSelector is the attribute added to the host itself.
*/
shimCssText(cssText: string, selector: string, hostSelector: string = ''): string {
const sourceMappingUrl: string = extractSourceMappingUrl(cssText);
cssText = stripComments(cssText);
cssText = this._insertDirectives(cssText);
return this._scopeCssText(cssText, selector, hostSelector) + sourceMappingUrl;
}
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 cssText.replace(
_cssContentNextSelectorRe, function(...m: string[]) { return m[2] + '{'; });
}
/*
* 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 cssText.replace(_cssContentRuleRe, (...m: string[]) => {
const rule = m[0].replace(m[1], '').replace(m[2], '');
return m[4] + 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 {
const unscopedRules = this._extractUnscopedRulesFromCssText(cssText);
// replace :host and :host-context -shadowcsshost and -shadowcsshost respectively
cssText = this._insertPolyfillHostInCssText(cssText);
cssText = this._convertColonHost(cssText);
cssText = this._convertColonHostContext(cssText);
cssText = this._convertShadowDOMSelectors(cssText);
if (scopeSelector) {
cssText = this._scopeSelectors(cssText, scopeSelector, hostSelector);
}
cssText = cssText + '\n' + unscopedRules;
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
let r = '';
let m: RegExpExecArray;
_cssContentUnscopedRuleRe.lastIndex = 0;
while ((m = _cssContentUnscopedRuleRe.exec(cssText)) !== null) {
const rule = m[0].replace(m[2], '').replace(m[1], m[4]);
r += rule + '\n\n';
}
return r;
}
/*
* convert a rule like :host(.foo) > .bar { }
*
* to
*
* .foo<scopeName> > .bar
*/
private _convertColonHost(cssText: string): string {
return this._convertColonRule(cssText, _cssColonHostRe, this._colonHostPartReplacer);
}
/*
* convert a rule like :host-context(.foo) > .bar { }
*
* to
*
* .foo<scopeName> > .bar, .foo scopeName > .bar { }
*
* and
*
* :host-context(.foo:host) .bar { ... }
*
* to
*
* .foo<scopeName> .bar { ... }
*/
private _convertColonHostContext(cssText: string): string {
return this._convertColonRule(
cssText, _cssColonHostContextRe, this._colonHostContextPartReplacer);
}
private _convertColonRule(cssText: string, regExp: RegExp, partReplacer: Function): string {
// m[1] = :host(-context), m[2] = contents of (), m[3] rest of rule
return cssText.replace(regExp, function(...m: string[]) {
if (m[2]) {
const parts = m[2].split(',');
const r: string[] = [];
for (let i = 0; i < parts.length; i++) {
const p = parts[i].trim();
if (!p) break;
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 (part.indexOf(_polyfillHost) > -1) {
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 + part.replace(_polyfillHost, '') + suffix;
}
/*
* Convert combinators like ::shadow and pseudo-elements like ::content
* by replacing with space.
*/
private _convertShadowDOMSelectors(cssText: string): string {
return _shadowDOMSelectorsRe.reduce((result, pattern) => result.replace(pattern, ' '), cssText);
}
// change a selector like 'div' to 'name div'
private _scopeSelectors(cssText: string, scopeSelector: string, hostSelector: string): string {
return processRules(cssText, (rule: CssRule) => {
let selector = rule.selector;
let content = rule.content;
if (rule.selector[0] != '@') {
selector =
this._scopeSelector(rule.selector, scopeSelector, hostSelector, this.strictStyling);
} else if (
rule.selector.startsWith('@media') || rule.selector.startsWith('@supports') ||
rule.selector.startsWith('@page') || rule.selector.startsWith('@document')) {
content = this._scopeSelectors(rule.content, scopeSelector, hostSelector);
}
return new CssRule(selector, content);
});
}
private _scopeSelector(
selector: string, scopeSelector: string, hostSelector: string, strict: boolean): string {
return selector.split(',')
.map(part => part.trim().split(_shadowDeepSelectors))
.map((deepParts) => {
const [shallowPart, ...otherParts] = deepParts;
const applyScope = (shallowPart: string) => {
if (this._selectorNeedsScoping(shallowPart, scopeSelector)) {
return strict ?
this._applyStrictSelectorScope(shallowPart, scopeSelector, hostSelector) :
this._applySelectorScope(shallowPart, scopeSelector, hostSelector);
} else {
return shallowPart;
}
};
return [applyScope(shallowPart), ...otherParts].join(' ');
})
.join(', ');
}
private _selectorNeedsScoping(selector: string, scopeSelector: string): boolean {
const re = this._makeScopeMatcher(scopeSelector);
return !re.test(selector);
}
private _makeScopeMatcher(scopeSelector: string): RegExp {
const lre = /\[/g;
const rre = /\]/g;
scopeSelector = scopeSelector.replace(lre, '\\[').replace(rre, '\\]');
return new RegExp('^(' + scopeSelector + ')' + _selectorReSuffix, 'm');
}
private _applySelectorScope(selector: string, scopeSelector: string, hostSelector: string):
string {
// Difference from webcomponents.js: 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 {
// In Android browser, the lastIndex is not reset when the regex is used in String.replace()
_polyfillHostRe.lastIndex = 0;
if (_polyfillHostRe.test(selector)) {
const replaceBy = this.strictStyling ? `[${hostSelector}]` : scopeSelector;
return selector
.replace(
_polyfillHostNoCombinatorRe,
(hnc, selector) => {
return selector.replace(
/([^:]*)(:*)(.*)/,
(_: string, before: string, colon: string, after: string) => {
return before + replaceBy + colon + after;
});
})
.replace(_polyfillHostRe, replaceBy + ' ');
}
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, hostSelector: string):
string {
const isRe = /\[is=([^\]]*)\]/g;
scopeSelector = scopeSelector.replace(isRe, (_: string, ...parts: string[]) => parts[0]);
const attrName = '[' + scopeSelector + ']';
const _scopeSelectorPart = (p: string) => {
let scopedP = p.trim();
if (!scopedP) {
return '';
}
if (p.indexOf(_polyfillHostNoCombinator) > -1) {
scopedP = this._applySimpleSelectorScope(p, scopeSelector, hostSelector);
} else {
// remove :host since it should be unnecessary
const t = p.replace(_polyfillHostRe, '');
if (t.length > 0) {
const matches = t.match(/([^:]*)(:*)(.*)/);
if (matches) {
scopedP = matches[1] + attrName + matches[2] + matches[3];
}
}
}
return scopedP;
};
const safeContent = new SafeSelector(selector);
selector = safeContent.content();
let scopedSelector = '';
let startIndex = 0;
let res: RegExpExecArray;
const sep = /( |>|\+|~(?!=))\s*/g;
const scopeAfter = selector.indexOf(_polyfillHostNoCombinator);
while ((res = sep.exec(selector)) !== null) {
const separator = res[1];
const part = selector.slice(startIndex, res.index).trim();
// if a selector appears before :host-context it should not be shimmed as it
// matches on ancestor elements and not on elements in the host's shadow
const scopedPart = startIndex >= scopeAfter ? _scopeSelectorPart(part) : part;
scopedSelector += `${scopedPart} ${separator} `;
startIndex = sep.lastIndex;
}
scopedSelector += _scopeSelectorPart(selector.substring(startIndex));
// replace the placeholders with their original values
return safeContent.restore(scopedSelector);
}
private _insertPolyfillHostInCssText(selector: string): string {
return selector.replace(_colonHostContextRe, _polyfillHostContext)
.replace(_colonHostRe, _polyfillHost);
}
}
class SafeSelector {
private placeholders: string[] = [];
private index = 0;
private _content: string;
constructor(selector: string) {
// Replaces attribute selectors with placeholders.
// The WS in [attr="va lue"] would otherwise be interpreted as a selector separator.
selector = selector.replace(/(\[[^\]]*\])/g, (_, keep) => {
const replaceBy = `__ph-${this.index}__`;
this.placeholders.push(keep);
this.index++;
return replaceBy;
});
// Replaces the expression in `:nth-child(2n + 1)` with a placeholder.
// WS and "+" would otherwise be interpreted as selector separators.
this._content = selector.replace(/(:nth-[-\w]+)(\([^)]+\))/g, (_, pseudo, exp) => {
const replaceBy = `__ph-${this.index}__`;
this.placeholders.push(exp);
this.index++;
return pseudo + replaceBy;
});
};
restore(content: string): string {
return content.replace(/__ph-(\d+)__/g, (ph, index) => this.placeholders[+index]);
}
content(): string { return this._content; }
}
const _cssContentNextSelectorRe =
/polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim;
const _cssContentRuleRe = /(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;
const _cssContentUnscopedRuleRe =
/(polyfill-unscoped-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim;
const _polyfillHost = '-shadowcsshost';
// note: :host-context pre-processed to -shadowcsshostcontext.
const _polyfillHostContext = '-shadowcsscontext';
const _parenSuffix = ')(?:\\((' +
'(?:\\([^)(]*\\)|[^)(]*)+?' +
')\\))?([^,{]*)';
const _cssColonHostRe = new RegExp('(' + _polyfillHost + _parenSuffix, 'gim');
const _cssColonHostContextRe = new RegExp('(' + _polyfillHostContext + _parenSuffix, 'gim');
const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator';
const _polyfillHostNoCombinatorRe = /-shadowcsshost-no-combinator([^\s]*)/;
const _shadowDOMSelectorsRe = [
/::shadow/g,
/::content/g,
// Deprecated selectors
/\/shadow-deep\//g,
/\/shadow\//g,
];
const _shadowDeepSelectors = /(?:>>>)|(?:\/deep\/)/g;
const _selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$';
const _polyfillHostRe = /-shadowcsshost/gim;
const _colonHostRe = /:host/gim;
const _colonHostContextRe = /:host-context/gim;
const _commentRe = /\/\*\s*[\s\S]*?\*\//g;
function stripComments(input: string): string {
return input.replace(_commentRe, '');
}
// all comments except inline source mapping
const _sourceMappingUrlRe = /\/\*\s*#\s*sourceMappingURL=[\s\S]+?\*\//;
function extractSourceMappingUrl(input: string): string {
const matcher = input.match(_sourceMappingUrlRe);
return matcher ? matcher[0] : '';
}
const _ruleRe = /(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g;
const _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: (rule: CssRule) => CssRule): string {
const inputWithEscapedBlocks = escapeBlocks(input);
let nextBlockIndex = 0;
return inputWithEscapedBlocks.escapedString.replace(_ruleRe, function(...m: string[]) {
const selector = m[2];
let content = '';
let suffix = m[4];
let contentPrefix = '';
if (suffix && suffix.startsWith('{' + BLOCK_PLACEHOLDER)) {
content = inputWithEscapedBlocks.blocks[nextBlockIndex++];
suffix = suffix.substring(BLOCK_PLACEHOLDER.length + 1);
contentPrefix = '{';
}
const 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 {
const inputParts = input.split(_curlyRe);
const resultParts: string[] = [];
const escapedBlocks: string[] = [];
let bracketCount = 0;
let currentBlockParts: string[] = [];
for (let partIndex = 0; partIndex < inputParts.length; partIndex++) {
const part = inputParts[partIndex];
if (part == CLOSE_CURLY) {
bracketCount--;
}
if (bracketCount > 0) {
currentBlockParts.push(part);
} else {
if (currentBlockParts.length > 0) {
escapedBlocks.push(currentBlockParts.join(''));
resultParts.push(BLOCK_PLACEHOLDER);
currentBlockParts = [];
}
resultParts.push(part);
}
if (part == OPEN_CURLY) {
bracketCount++;
}
}
if (currentBlockParts.length > 0) {
escapedBlocks.push(currentBlockParts.join(''));
resultParts.push(BLOCK_PLACEHOLDER);
}
return new StringWithEscapedBlocks(resultParts.join(''), escapedBlocks);
}

View File

@ -0,0 +1,96 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ViewEncapsulation} from '@angular/core';
import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileStylesheetMetadata, identifierModuleUrl, identifierName} from './compile_metadata';
import {CompilerInjectable} from './injectable';
import * as o from './output/output_ast';
import {ShadowCss} from './shadow_css';
import {UrlResolver} from './url_resolver';
const COMPONENT_VARIABLE = '%COMP%';
const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`;
const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`;
export class StylesCompileDependency {
constructor(
public name: string, public moduleUrl: string, public isShimmed: boolean,
public valuePlaceholder: CompileIdentifierMetadata) {}
}
export class StylesCompileResult {
constructor(
public componentStylesheet: CompiledStylesheet,
public externalStylesheets: CompiledStylesheet[]) {}
}
export class CompiledStylesheet {
constructor(
public statements: o.Statement[], public stylesVar: string,
public dependencies: StylesCompileDependency[], public isShimmed: boolean,
public meta: CompileStylesheetMetadata) {}
}
@CompilerInjectable()
export class StyleCompiler {
private _shadowCss: ShadowCss = new ShadowCss();
constructor(private _urlResolver: UrlResolver) {}
compileComponent(comp: CompileDirectiveMetadata): StylesCompileResult {
const externalStylesheets: CompiledStylesheet[] = [];
const componentStylesheet: CompiledStylesheet = this._compileStyles(
comp, new CompileStylesheetMetadata({
styles: comp.template.styles,
styleUrls: comp.template.styleUrls,
moduleUrl: identifierModuleUrl(comp.type)
}),
true);
comp.template.externalStylesheets.forEach((stylesheetMeta) => {
const compiledStylesheet = this._compileStyles(comp, stylesheetMeta, false);
externalStylesheets.push(compiledStylesheet);
});
return new StylesCompileResult(componentStylesheet, externalStylesheets);
}
private _compileStyles(
comp: CompileDirectiveMetadata, stylesheet: CompileStylesheetMetadata,
isComponentStylesheet: boolean): CompiledStylesheet {
const shim = comp.template.encapsulation === ViewEncapsulation.Emulated;
const styleExpressions =
stylesheet.styles.map(plainStyle => o.literal(this._shimIfNeeded(plainStyle, shim)));
const dependencies: StylesCompileDependency[] = [];
for (let i = 0; i < stylesheet.styleUrls.length; i++) {
const identifier: CompileIdentifierMetadata = {reference: null};
dependencies.push(new StylesCompileDependency(
getStylesVarName(null), stylesheet.styleUrls[i], shim, identifier));
styleExpressions.push(new o.ExternalExpr(identifier));
}
// styles variable contains plain strings and arrays of other styles arrays (recursive),
// so we set its type to dynamic.
const stylesVar = getStylesVarName(isComponentStylesheet ? comp : null);
const stmt = o.variable(stylesVar)
.set(o.literalArr(
styleExpressions, new o.ArrayType(o.DYNAMIC_TYPE, [o.TypeModifier.Const])))
.toDeclStmt(null, [o.StmtModifier.Final]);
return new CompiledStylesheet([stmt], stylesVar, dependencies, shim, stylesheet);
}
private _shimIfNeeded(style: string, shim: boolean): string {
return shim ? this._shadowCss.shimCssText(style, CONTENT_ATTR, HOST_ATTR) : style;
}
}
function getStylesVarName(component: CompileDirectiveMetadata): string {
let result = `styles`;
if (component) {
result += `_${identifierName(component.type)}`;
}
return result;
}

View File

@ -0,0 +1,47 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// Some of the code comes from WebComponents.JS
// https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js
import {UrlResolver} from './url_resolver';
export class StyleWithImports {
constructor(public style: string, public styleUrls: string[]) {}
}
export function isStyleUrlResolvable(url: string): boolean {
if (url == null || url.length === 0 || url[0] == '/') return false;
const schemeMatch = url.match(URL_WITH_SCHEMA_REGEXP);
return schemeMatch === null || 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 {
const foundUrls: string[] = [];
const modifiedCssText =
cssText.replace(CSS_COMMENT_REGEXP, '').replace(CSS_IMPORT_REGEXP, (...m: string[]) => {
const url = 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);
}
const CSS_IMPORT_REGEXP = /@import\s+(?:url\()?\s*(?:(?:['"]([^'"]*))|([^;\)\s]*))[^;]*;?/g;
const CSS_COMMENT_REGEXP = /\/\*.+?\*\//g;
const URL_WITH_SCHEMA_REGEXP = /^([^:/?#]+):/;

View File

@ -0,0 +1,24 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CompileTypeSummary} from './compile_metadata';
import {CompilerInjectable} from './injectable';
export interface Summary<T> {
symbol: T;
metadata: any;
type?: CompileTypeSummary;
}
@CompilerInjectable()
export class SummaryResolver<T> {
isLibraryFile(fileName: string): boolean { return false; };
getLibraryFileName(fileName: string): string { return null; }
resolveSummary(reference: T): Summary<T> { return null; };
getSymbolsOf(filePath: string): T[] { return []; }
getImportAs(reference: T): T { return reference; }
}

View File

@ -0,0 +1,443 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SecurityContext} from '@angular/core';
import {CompileDirectiveSummary, CompilePipeSummary} from '../compile_metadata';
import {ASTWithSource, BindingPipe, EmptyExpr, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {mergeNsAndName} from '../ml_parser/tags';
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector} from '../selector';
import {splitAtColon, splitAtPeriod} from '../util';
import {BoundElementPropertyAst, BoundEventAst, PropertyBindingType, VariableAst} from './template_ast';
const PROPERTY_PARTS_SEPARATOR = '.';
const ATTRIBUTE_PREFIX = 'attr';
const CLASS_PREFIX = 'class';
const STYLE_PREFIX = 'style';
const ANIMATE_PROP_PREFIX = 'animate-';
export enum BoundPropertyType {
DEFAULT,
LITERAL_ATTR,
ANIMATION
}
/**
* Represents a parsed property.
*/
export class BoundProperty {
constructor(
public name: string, public expression: ASTWithSource, public type: BoundPropertyType,
public sourceSpan: ParseSourceSpan) {}
get isLiteral() { return this.type === BoundPropertyType.LITERAL_ATTR; }
get isAnimation() { return this.type === BoundPropertyType.ANIMATION; }
}
/**
* Parses bindings in templates and in the directive host area.
*/
export class BindingParser {
pipesByName: Map<string, CompilePipeSummary> = new Map();
private _usedPipes: Map<string, CompilePipeSummary> = new Map();
constructor(
private _exprParser: Parser, private _interpolationConfig: InterpolationConfig,
private _schemaRegistry: ElementSchemaRegistry, pipes: CompilePipeSummary[],
private _targetErrors: ParseError[]) {
pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe));
}
getUsedPipes(): CompilePipeSummary[] { return Array.from(this._usedPipes.values()); }
createDirectiveHostPropertyAsts(
dirMeta: CompileDirectiveSummary, elementSelector: string,
sourceSpan: ParseSourceSpan): BoundElementPropertyAst[] {
if (dirMeta.hostProperties) {
const boundProps: BoundProperty[] = [];
Object.keys(dirMeta.hostProperties).forEach(propName => {
const expression = dirMeta.hostProperties[propName];
if (typeof expression === 'string') {
this.parsePropertyBinding(propName, expression, true, sourceSpan, [], boundProps);
} else {
this._reportError(
`Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
sourceSpan);
}
});
return boundProps.map((prop) => this.createElementPropertyAst(elementSelector, prop));
}
}
createDirectiveHostEventAsts(dirMeta: CompileDirectiveSummary, sourceSpan: ParseSourceSpan):
BoundEventAst[] {
if (dirMeta.hostListeners) {
const targetEventAsts: BoundEventAst[] = [];
Object.keys(dirMeta.hostListeners).forEach(propName => {
const expression = dirMeta.hostListeners[propName];
if (typeof expression === 'string') {
this.parseEvent(propName, expression, sourceSpan, [], targetEventAsts);
} else {
this._reportError(
`Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`,
sourceSpan);
}
});
return targetEventAsts;
}
}
parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
const sourceInfo = sourceSpan.start.toString();
try {
const ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig);
if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
this._checkPipes(ast, sourceSpan);
return ast;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
}
}
parseInlineTemplateBinding(
prefixToken: string, value: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetProps: BoundProperty[], targetVars: VariableAst[]) {
const bindings = this._parseTemplateBindings(prefixToken, value, sourceSpan);
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i];
if (binding.keyIsVar) {
targetVars.push(new VariableAst(binding.key, binding.name, sourceSpan));
} else if (binding.expression) {
this._parsePropertyAst(
binding.key, binding.expression, sourceSpan, targetMatchableAttrs, targetProps);
} else {
targetMatchableAttrs.push([binding.key, '']);
this.parseLiteralAttr(binding.key, null, sourceSpan, targetMatchableAttrs, targetProps);
}
}
}
private _parseTemplateBindings(prefixToken: string, value: string, sourceSpan: ParseSourceSpan):
TemplateBinding[] {
const sourceInfo = sourceSpan.start.toString();
try {
const bindingsResult = this._exprParser.parseTemplateBindings(prefixToken, value, sourceInfo);
this._reportExpressionParserErrors(bindingsResult.errors, sourceSpan);
bindingsResult.templateBindings.forEach((binding) => {
if (binding.expression) {
this._checkPipes(binding.expression, sourceSpan);
}
});
bindingsResult.warnings.forEach(
(warning) => { this._reportError(warning, sourceSpan, ParseErrorLevel.WARNING); });
return bindingsResult.templateBindings;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return [];
}
}
parseLiteralAttr(
name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][],
targetProps: BoundProperty[]) {
if (_isAnimationLabel(name)) {
name = name.substring(1);
if (value) {
this._reportError(
`Assigning animation triggers via @prop="exp" attributes with an expression is invalid.` +
` Use property bindings (e.g. [@prop]="exp") or use an attribute without a value (e.g. @prop) instead.`,
sourceSpan, ParseErrorLevel.FATAL);
}
this._parseAnimation(name, value, sourceSpan, targetMatchableAttrs, targetProps);
} else {
targetProps.push(new BoundProperty(
name, this._exprParser.wrapLiteralPrimitive(value, ''), BoundPropertyType.LITERAL_ATTR,
sourceSpan));
}
}
parsePropertyBinding(
name: string, expression: string, isHost: boolean, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetProps: BoundProperty[]) {
let isAnimationProp = false;
if (name.startsWith(ANIMATE_PROP_PREFIX)) {
isAnimationProp = true;
name = name.substring(ANIMATE_PROP_PREFIX.length);
} else if (_isAnimationLabel(name)) {
isAnimationProp = true;
name = name.substring(1);
}
if (isAnimationProp) {
this._parseAnimation(name, expression, sourceSpan, targetMatchableAttrs, targetProps);
} else {
this._parsePropertyAst(
name, this._parseBinding(expression, isHost, sourceSpan), sourceSpan,
targetMatchableAttrs, targetProps);
}
}
parsePropertyInterpolation(
name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][],
targetProps: BoundProperty[]): boolean {
const expr = this.parseInterpolation(value, sourceSpan);
if (expr) {
this._parsePropertyAst(name, expr, sourceSpan, targetMatchableAttrs, targetProps);
return true;
}
return false;
}
private _parsePropertyAst(
name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetProps: BoundProperty[]) {
targetMatchableAttrs.push([name, ast.source]);
targetProps.push(new BoundProperty(name, ast, BoundPropertyType.DEFAULT, sourceSpan));
}
private _parseAnimation(
name: string, expression: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetProps: BoundProperty[]) {
// This will occur when a @trigger is not paired with an expression.
// For animations it is valid to not have an expression since */void
// states will be applied by angular when the element is attached/detached
const ast = this._parseBinding(expression || 'null', false, sourceSpan);
targetMatchableAttrs.push([name, ast.source]);
targetProps.push(new BoundProperty(name, ast, BoundPropertyType.ANIMATION, sourceSpan));
}
private _parseBinding(value: string, isHostBinding: boolean, sourceSpan: ParseSourceSpan):
ASTWithSource {
const sourceInfo = sourceSpan.start.toString();
try {
const ast = isHostBinding ?
this._exprParser.parseSimpleBinding(value, sourceInfo, this._interpolationConfig) :
this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig);
if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan);
this._checkPipes(ast, sourceSpan);
return ast;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
}
}
createElementPropertyAst(elementSelector: string, boundProp: BoundProperty):
BoundElementPropertyAst {
if (boundProp.isAnimation) {
return new BoundElementPropertyAst(
boundProp.name, PropertyBindingType.Animation, SecurityContext.NONE, boundProp.expression,
null, boundProp.sourceSpan);
}
let unit: string = null;
let bindingType: PropertyBindingType;
let boundPropertyName: string = null;
const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR);
let securityContexts: SecurityContext[];
// Check check for special cases (prefix style, attr, class)
if (parts.length > 1) {
if (parts[0] == ATTRIBUTE_PREFIX) {
boundPropertyName = parts[1];
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true);
securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry, elementSelector, boundPropertyName, true);
const nsSeparatorIdx = boundPropertyName.indexOf(':');
if (nsSeparatorIdx > -1) {
const ns = boundPropertyName.substring(0, nsSeparatorIdx);
const name = boundPropertyName.substring(nsSeparatorIdx + 1);
boundPropertyName = mergeNsAndName(ns, name);
}
bindingType = PropertyBindingType.Attribute;
} else if (parts[0] == CLASS_PREFIX) {
boundPropertyName = parts[1];
bindingType = PropertyBindingType.Class;
securityContexts = [SecurityContext.NONE];
} else if (parts[0] == STYLE_PREFIX) {
unit = parts.length > 2 ? parts[2] : null;
boundPropertyName = parts[1];
bindingType = PropertyBindingType.Style;
securityContexts = [SecurityContext.STYLE];
}
}
// If not a special case, use the full property name
if (boundPropertyName === null) {
boundPropertyName = this._schemaRegistry.getMappedPropName(boundProp.name);
securityContexts = calcPossibleSecurityContexts(
this._schemaRegistry, elementSelector, boundPropertyName, false);
bindingType = PropertyBindingType.Property;
this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false);
}
return new BoundElementPropertyAst(
boundPropertyName, bindingType, securityContexts[0], boundProp.expression, unit,
boundProp.sourceSpan);
}
parseEvent(
name: string, expression: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
if (_isAnimationLabel(name)) {
name = name.substr(1);
this._parseAnimationEvent(name, expression, sourceSpan, targetEvents);
} else {
this._parseEvent(name, expression, sourceSpan, targetMatchableAttrs, targetEvents);
}
}
private _parseAnimationEvent(
name: string, expression: string, sourceSpan: ParseSourceSpan,
targetEvents: BoundEventAst[]) {
const matches = splitAtPeriod(name, [name, '']);
const eventName = matches[0];
const phase = matches[1].toLowerCase();
if (phase) {
switch (phase) {
case 'start':
case 'done':
const ast = this._parseAction(expression, sourceSpan);
targetEvents.push(new BoundEventAst(eventName, null, phase, ast, sourceSpan));
break;
default:
this._reportError(
`The provided animation output phase value "${phase}" for "@${eventName}" is not supported (use start or done)`,
sourceSpan);
break;
}
} else {
this._reportError(
`The animation trigger output event (@${eventName}) is missing its phase value name (start or done are currently supported)`,
sourceSpan);
}
}
private _parseEvent(
name: string, expression: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
// long format: 'target: eventName'
const [target, eventName] = splitAtColon(name, [null, name]);
const ast = this._parseAction(expression, sourceSpan);
targetMatchableAttrs.push([name, ast.source]);
targetEvents.push(new BoundEventAst(eventName, target, null, ast, sourceSpan));
// Don't detect directives for event names for now,
// so don't add the event name to the matchableAttrs
}
private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
const sourceInfo = sourceSpan.start.toString();
try {
const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
if (ast) {
this._reportExpressionParserErrors(ast.errors, sourceSpan);
}
if (!ast || ast.ast instanceof EmptyExpr) {
this._reportError(`Empty expressions are not allowed`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
}
this._checkPipes(ast, sourceSpan);
return ast;
} catch (e) {
this._reportError(`${e}`, sourceSpan);
return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo);
}
}
private _reportError(
message: string, sourceSpan: ParseSourceSpan,
level: ParseErrorLevel = ParseErrorLevel.FATAL) {
this._targetErrors.push(new ParseError(sourceSpan, message, level));
}
private _reportExpressionParserErrors(errors: ParserError[], sourceSpan: ParseSourceSpan) {
for (const error of errors) {
this._reportError(error.message, sourceSpan);
}
}
private _checkPipes(ast: ASTWithSource, sourceSpan: ParseSourceSpan) {
if (ast) {
const collector = new PipeCollector();
ast.visit(collector);
collector.pipes.forEach((ast, pipeName) => {
const pipeMeta = this.pipesByName.get(pipeName);
if (!pipeMeta) {
this._reportError(
`The pipe '${pipeName}' could not be found`,
new ParseSourceSpan(
sourceSpan.start.moveBy(ast.span.start), sourceSpan.start.moveBy(ast.span.end)));
} else {
this._usedPipes.set(pipeName, pipeMeta);
}
});
}
}
/**
* @param propName the name of the property / attribute
* @param sourceSpan
* @param isAttr true when binding to an attribute
* @private
*/
private _validatePropertyOrAttributeName(
propName: string, sourceSpan: ParseSourceSpan, isAttr: boolean): void {
const report = isAttr ? this._schemaRegistry.validateAttribute(propName) :
this._schemaRegistry.validateProperty(propName);
if (report.error) {
this._reportError(report.msg, sourceSpan, ParseErrorLevel.FATAL);
}
}
}
export class PipeCollector extends RecursiveAstVisitor {
pipes = new Map<string, BindingPipe>();
visitPipe(ast: BindingPipe, context: any): any {
this.pipes.set(ast.name, ast);
ast.exp.visit(this);
this.visitAll(ast.args, context);
return null;
}
}
function _isAnimationLabel(name: string): boolean {
return name[0] == '@';
}
export function calcPossibleSecurityContexts(
registry: ElementSchemaRegistry, selector: string, propName: string,
isAttribute: boolean): SecurityContext[] {
const ctxs: SecurityContext[] = [];
CssSelector.parse(selector).forEach((selector) => {
const elementNames = selector.element ? [selector.element] : registry.allKnownElementNames();
const notElementNames =
new Set(selector.notSelectors.filter(selector => selector.isElementSelector())
.map((selector) => selector.element));
const possibleElementNames =
elementNames.filter(elementName => !notElementNames.has(elementName));
ctxs.push(...possibleElementNames.map(
elementName => registry.securityContext(elementName, propName, isAttribute)));
});
return ctxs.length === 0 ? [SecurityContext.NONE] : Array.from(new Set(ctxs)).sort();
}

View File

@ -0,0 +1,287 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SecurityContext, ɵLifecycleHooks as LifecycleHooks} from '@angular/core';
import {CompileDirectiveSummary, CompileProviderMetadata, CompileTokenMetadata} from '../compile_metadata';
import {AST} from '../expression_parser/ast';
import {ParseSourceSpan} from '../parse_util';
/**
* An Abstract Syntax Tree node representing part of a parsed Angular template.
*/
export interface TemplateAst {
/**
* The source span from which this node was parsed.
*/
sourceSpan: ParseSourceSpan;
/**
* Visit this node and possibly transform it.
*/
visit(visitor: TemplateAstVisitor, context: any): any;
}
/**
* A segment of text within the template.
*/
export class TextAst implements TemplateAst {
constructor(
public value: string, public ngContentIndex: number, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitText(this, context); }
}
/**
* A bound expression within the text of a template.
*/
export class BoundTextAst implements TemplateAst {
constructor(
public value: AST, public ngContentIndex: number, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitBoundText(this, context);
}
}
/**
* A plain attribute on an element.
*/
export class AttrAst implements TemplateAst {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
}
/**
* A binding for an element property (e.g. `[property]="expression"`) or an animation trigger (e.g.
* `[@trigger]="stateExp"`)
*/
export class BoundElementPropertyAst implements TemplateAst {
constructor(
public name: string, public type: PropertyBindingType,
public securityContext: SecurityContext, public value: AST, public unit: string,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElementProperty(this, context);
}
get isAnimation(): boolean { return this.type === PropertyBindingType.Animation; }
}
/**
* A binding for an element event (e.g. `(event)="handler()"`) or an animation trigger event (e.g.
* `(@trigger.phase)="callback($event)"`).
*/
export class BoundEventAst implements TemplateAst {
static calcFullName(name: string, target: string, phase: string): string {
if (target) {
return `${target}:${name}`;
} else if (phase) {
return `@${name}.${phase}`;
} else {
return name;
}
}
constructor(
public name: string, public target: string, public phase: string, public handler: AST,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitEvent(this, context);
}
get fullName() { return BoundEventAst.calcFullName(this.name, this.target, this.phase); }
get isAnimation(): boolean { return !!this.phase; }
}
/**
* A reference declaration on an element (e.g. `let someName="expression"`).
*/
export class ReferenceAst implements TemplateAst {
constructor(
public name: string, public value: CompileTokenMetadata, public sourceSpan: ParseSourceSpan) {
}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitReference(this, context);
}
}
/**
* A variable declaration on a <ng-template> (e.g. `var-someName="someLocalName"`).
*/
export class VariableAst implements TemplateAst {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitVariable(this, context);
}
}
/**
* An element declaration in a template.
*/
export class ElementAst implements TemplateAst {
constructor(
public name: string, public attrs: AttrAst[], public inputs: BoundElementPropertyAst[],
public outputs: BoundEventAst[], public references: ReferenceAst[],
public directives: DirectiveAst[], public providers: ProviderAst[],
public hasViewContainer: boolean, public queryMatches: QueryMatch[],
public children: TemplateAst[], public ngContentIndex: number,
public sourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElement(this, context);
}
}
/**
* A `<ng-template>` element included in an Angular template.
*/
export class EmbeddedTemplateAst implements TemplateAst {
constructor(
public attrs: AttrAst[], public outputs: BoundEventAst[], public references: ReferenceAst[],
public variables: VariableAst[], public directives: DirectiveAst[],
public providers: ProviderAst[], public hasViewContainer: boolean,
public queryMatches: QueryMatch[], public children: TemplateAst[],
public ngContentIndex: number, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitEmbeddedTemplate(this, context);
}
}
/**
* A directive property with a bound value (e.g. `*ngIf="condition").
*/
export class BoundDirectivePropertyAst implements TemplateAst {
constructor(
public directiveName: string, public templateName: string, public value: AST,
public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitDirectiveProperty(this, context);
}
}
/**
* A directive declared on an element.
*/
export class DirectiveAst implements TemplateAst {
constructor(
public directive: CompileDirectiveSummary, public inputs: BoundDirectivePropertyAst[],
public hostProperties: BoundElementPropertyAst[], public hostEvents: BoundEventAst[],
public contentQueryStartId: number, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitDirective(this, context);
}
}
/**
* A provider declared on an element
*/
export class ProviderAst implements TemplateAst {
constructor(
public token: CompileTokenMetadata, public multiProvider: boolean, public eager: boolean,
public providers: CompileProviderMetadata[], public providerType: ProviderAstType,
public lifecycleHooks: LifecycleHooks[], public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
// No visit method in the visitor for now...
return null;
}
}
export enum ProviderAstType {
PublicService,
PrivateService,
Component,
Directive,
Builtin
}
/**
* Position where content is to be projected (instance of `<ng-content>` in a template).
*/
export class NgContentAst implements TemplateAst {
constructor(
public index: number, public ngContentIndex: number, public sourceSpan: ParseSourceSpan) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitNgContent(this, context);
}
}
/**
* Enumeration of types of property bindings.
*/
export enum PropertyBindingType {
/**
* A normal binding to a property (e.g. `[property]="expression"`).
*/
Property,
/**
* A binding to an element attribute (e.g. `[attr.name]="expression"`).
*/
Attribute,
/**
* A binding to a CSS class (e.g. `[class.name]="condition"`).
*/
Class,
/**
* A binding to a style rule (e.g. `[style.rule]="expression"`).
*/
Style,
/**
* A binding to an animation reference (e.g. `[animate.key]="expression"`).
*/
Animation
}
export interface QueryMatch {
queryId: number;
value: CompileTokenMetadata;
}
/**
* A visitor for {@link TemplateAst} trees that will process each node.
*/
export interface TemplateAstVisitor {
// Returning a truthy value from `visit()` will prevent `templateVisitAll()` from the call to
// the typed method and result returned will become the result included in `visitAll()`s
// result array.
visit?(ast: TemplateAst, context: any): any;
visitNgContent(ast: NgContentAst, context: any): any;
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any;
visitElement(ast: ElementAst, context: any): any;
visitReference(ast: ReferenceAst, context: any): any;
visitVariable(ast: VariableAst, context: any): any;
visitEvent(ast: BoundEventAst, context: any): any;
visitElementProperty(ast: BoundElementPropertyAst, context: any): any;
visitAttr(ast: AttrAst, context: any): any;
visitBoundText(ast: BoundTextAst, context: any): any;
visitText(ast: TextAst, context: any): any;
visitDirective(ast: DirectiveAst, context: any): any;
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any;
}
/**
* Visit every node in a list of {@link TemplateAst}s with the given {@link TemplateAstVisitor}.
*/
export function templateVisitAll(
visitor: TemplateAstVisitor, asts: TemplateAst[], context: any = null): any[] {
const result: any[] = [];
const visit = visitor.visit ?
(ast: TemplateAst) => visitor.visit(ast, context) || ast.visit(visitor, context) :
(ast: TemplateAst) => ast.visit(visitor, context);
asts.forEach(ast => {
const astResult = visit(ast);
if (astResult) {
result.push(astResult);
}
});
return result;
}

View File

@ -0,0 +1,884 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Inject, InjectionToken, Optional, SchemaMetadata, ɵConsole as Console} from '@angular/core';
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CompileTemplateSummary, CompileTokenMetadata, CompileTypeMetadata, identifierName} from '../compile_metadata';
import {CompilerConfig} from '../config';
import {AST, ASTWithSource, EmptyExpr} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser';
import {I18NHtmlParser} from '../i18n/i18n_html_parser';
import {Identifiers, createIdentifierToken, identifierToken} from '../identifiers';
import {CompilerInjectable} from '../injectable';
import * as html from '../ml_parser/ast';
import {ParseTreeResult} from '../ml_parser/html_parser';
import {expandNodes} from '../ml_parser/icu_ast_expander';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {splitNsName} from '../ml_parser/tags';
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {ProviderElementContext, ProviderViewContext} from '../provider_analyzer';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector, SelectorMatcher} from '../selector';
import {isStyleUrlResolvable} from '../style_url_resolver';
import {syntaxError} from '../util';
import {BindingParser, BoundProperty} from './binding_parser';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from './template_ast';
import {PreparsedElementType, preparseElement} from './template_preparser';
const BIND_NAME_REGEXP =
/^(?:(?:(?:(bind-)|(let-)|(ref-|#)|(on-)|(bindon-)|(@))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/;
// Group 1 = "bind-"
const KW_BIND_IDX = 1;
// Group 2 = "let-"
const KW_LET_IDX = 2;
// Group 3 = "ref-/#"
const KW_REF_IDX = 3;
// Group 4 = "on-"
const KW_ON_IDX = 4;
// Group 5 = "bindon-"
const KW_BINDON_IDX = 5;
// Group 6 = "@"
const KW_AT_IDX = 6;
// Group 7 = the identifier after "bind-", "let-", "ref-/#", "on-", "bindon-" or "@"
const IDENT_KW_IDX = 7;
// Group 8 = identifier inside [()]
const IDENT_BANANA_BOX_IDX = 8;
// Group 9 = identifier inside []
const IDENT_PROPERTY_IDX = 9;
// Group 10 = identifier inside ()
const IDENT_EVENT_IDX = 10;
const NG_TEMPLATE_ELEMENT = 'ng-template';
// deprecated in 4.x
const TEMPLATE_ELEMENT = 'template';
// deprecated in 4.x
const TEMPLATE_ATTR = 'template';
const TEMPLATE_ATTR_PREFIX = '*';
const CLASS_ATTR = 'class';
const TEXT_CSS_SELECTOR = CssSelector.parse('*')[0];
/**
* Provides an array of {@link TemplateAstVisitor}s which will be used to transform
* parsed templates before compilation is invoked, allowing custom expression syntax
* and other advanced transformations.
*
* This is currently an internal-only feature and not meant for general use.
*/
export const TEMPLATE_TRANSFORMS = new InjectionToken('TemplateTransforms');
export class TemplateParseError extends ParseError {
constructor(message: string, span: ParseSourceSpan, level: ParseErrorLevel) {
super(span, message, level);
}
}
export class TemplateParseResult {
constructor(
public templateAst?: TemplateAst[], public usedPipes?: CompilePipeSummary[],
public errors?: ParseError[]) {}
}
@CompilerInjectable()
export class TemplateParser {
constructor(
private _config: CompilerConfig, private _exprParser: Parser,
private _schemaRegistry: ElementSchemaRegistry, private _htmlParser: I18NHtmlParser,
private _console: Console,
@Optional() @Inject(TEMPLATE_TRANSFORMS) public transforms: TemplateAstVisitor[]) {}
parse(
component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[],
pipes: CompilePipeSummary[], schemas: SchemaMetadata[],
templateUrl: string): {template: TemplateAst[], pipes: CompilePipeSummary[]} {
const result = this.tryParse(component, template, directives, pipes, schemas, templateUrl);
const warnings = result.errors.filter(error => error.level === ParseErrorLevel.WARNING);
const errors = result.errors.filter(error => error.level === ParseErrorLevel.FATAL);
if (warnings.length > 0) {
this._console.warn(`Template parse warnings:\n${warnings.join('\n')}`);
}
if (errors.length > 0) {
const errorString = errors.join('\n');
throw syntaxError(`Template parse errors:\n${errorString}`);
}
return {template: result.templateAst, pipes: result.usedPipes};
}
tryParse(
component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[],
pipes: CompilePipeSummary[], schemas: SchemaMetadata[],
templateUrl: string): TemplateParseResult {
return this.tryParseHtml(
this.expandHtml(this._htmlParser.parse(
template, templateUrl, true, this.getInterpolationConfig(component))),
component, template, directives, pipes, schemas, templateUrl);
}
tryParseHtml(
htmlAstWithErrors: ParseTreeResult, component: CompileDirectiveMetadata, template: string,
directives: CompileDirectiveSummary[], pipes: CompilePipeSummary[], schemas: SchemaMetadata[],
templateUrl: string): TemplateParseResult {
let result: TemplateAst[];
const errors = htmlAstWithErrors.errors;
const usedPipes: CompilePipeSummary[] = [];
if (htmlAstWithErrors.rootNodes.length > 0) {
const uniqDirectives = removeSummaryDuplicates(directives);
const uniqPipes = removeSummaryDuplicates(pipes);
const providerViewContext =
new ProviderViewContext(component, htmlAstWithErrors.rootNodes[0].sourceSpan);
let interpolationConfig: InterpolationConfig;
if (component.template && component.template.interpolation) {
interpolationConfig = {
start: component.template.interpolation[0],
end: component.template.interpolation[1]
};
}
const bindingParser = new BindingParser(
this._exprParser, interpolationConfig, this._schemaRegistry, uniqPipes, errors);
const parseVisitor = new TemplateParseVisitor(
this._config, providerViewContext, uniqDirectives, bindingParser, this._schemaRegistry,
schemas, errors);
result = html.visitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT);
errors.push(...providerViewContext.errors);
usedPipes.push(...bindingParser.getUsedPipes());
} else {
result = [];
}
this._assertNoReferenceDuplicationOnTemplate(result, errors);
if (errors.length > 0) {
return new TemplateParseResult(result, usedPipes, errors);
}
if (this.transforms) {
this.transforms.forEach(
(transform: TemplateAstVisitor) => { result = templateVisitAll(transform, result); });
}
return new TemplateParseResult(result, usedPipes, errors);
}
expandHtml(htmlAstWithErrors: ParseTreeResult, forced: boolean = false): ParseTreeResult {
const errors: ParseError[] = htmlAstWithErrors.errors;
if (errors.length == 0 || forced) {
// Transform ICU messages to angular directives
const expandedHtmlAst = expandNodes(htmlAstWithErrors.rootNodes);
errors.push(...expandedHtmlAst.errors);
htmlAstWithErrors = new ParseTreeResult(expandedHtmlAst.nodes, errors);
}
return htmlAstWithErrors;
}
getInterpolationConfig(component: CompileDirectiveMetadata): InterpolationConfig {
if (component.template) {
return InterpolationConfig.fromArray(component.template.interpolation);
}
}
/** @internal */
_assertNoReferenceDuplicationOnTemplate(result: TemplateAst[], errors: TemplateParseError[]):
void {
const existingReferences: string[] = [];
result.filter(element => !!(<any>element).references)
.forEach(element => (<any>element).references.forEach((reference: ReferenceAst) => {
const name = reference.name;
if (existingReferences.indexOf(name) < 0) {
existingReferences.push(name);
} else {
const error = new TemplateParseError(
`Reference "#${name}" is defined several times`, reference.sourceSpan,
ParseErrorLevel.FATAL);
errors.push(error);
}
}));
}
}
class TemplateParseVisitor implements html.Visitor {
selectorMatcher = new SelectorMatcher();
directivesIndex = new Map<CompileDirectiveSummary, number>();
ngContentCount = 0;
contentQueryStartId: number;
constructor(
private config: CompilerConfig, public providerViewContext: ProviderViewContext,
directives: CompileDirectiveSummary[], private _bindingParser: BindingParser,
private _schemaRegistry: ElementSchemaRegistry, private _schemas: SchemaMetadata[],
private _targetErrors: TemplateParseError[]) {
// Note: queries start with id 1 so we can use the number in a Bloom filter!
this.contentQueryStartId = providerViewContext.component.viewQueries.length + 1;
directives.forEach((directive, index) => {
const selector = CssSelector.parse(directive.selector);
this.selectorMatcher.addSelectables(selector, directive);
this.directivesIndex.set(directive, index);
});
}
visitExpansion(expansion: html.Expansion, context: any): any { return null; }
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return null; }
visitText(text: html.Text, parent: ElementContext): any {
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
const expr = this._bindingParser.parseInterpolation(text.value, text.sourceSpan);
return expr ? new BoundTextAst(expr, ngContentIndex, text.sourceSpan) :
new TextAst(text.value, ngContentIndex, text.sourceSpan);
}
visitAttribute(attribute: html.Attribute, context: any): any {
return new AttrAst(attribute.name, attribute.value, attribute.sourceSpan);
}
visitComment(comment: html.Comment, context: any): any { return null; }
visitElement(element: html.Element, parent: ElementContext): any {
const queryStartIndex = this.contentQueryStartId;
const nodeName = element.name;
const 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;
}
const matchableAttrs: [string, string][] = [];
const elementOrDirectiveProps: BoundProperty[] = [];
const elementOrDirectiveRefs: ElementOrDirectiveRef[] = [];
const elementVars: VariableAst[] = [];
const events: BoundEventAst[] = [];
const templateElementOrDirectiveProps: BoundProperty[] = [];
const templateMatchableAttrs: [string, string][] = [];
const templateElementVars: VariableAst[] = [];
let hasInlineTemplates = false;
const attrs: AttrAst[] = [];
const isTemplateElement = isTemplate(
element, this.config.enableLegacyTemplate,
(m: string, span: ParseSourceSpan) => this._reportError(m, span, ParseErrorLevel.WARNING));
element.attrs.forEach(attr => {
const hasBinding = this._parseAttr(
isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, events,
elementOrDirectiveRefs, elementVars);
let templateBindingsSource: string|undefined;
let prefixToken: string|undefined;
let normalizedName = this._normalizeAttributeName(attr.name);
if (this.config.enableLegacyTemplate && normalizedName == TEMPLATE_ATTR) {
this._reportError(
`The template attribute is deprecated. Use an ng-template element instead.`,
attr.sourceSpan, ParseErrorLevel.WARNING);
templateBindingsSource = attr.value;
} else if (normalizedName.startsWith(TEMPLATE_ATTR_PREFIX)) {
templateBindingsSource = attr.value;
prefixToken = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length) + ':';
}
const hasTemplateBinding = templateBindingsSource != null;
if (hasTemplateBinding) {
if (hasInlineTemplates) {
this._reportError(
`Can't have multiple template bindings on one element. Use only one attribute named 'template' or prefixed with *`,
attr.sourceSpan);
}
hasInlineTemplates = true;
this._bindingParser.parseInlineTemplateBinding(
prefixToken, templateBindingsSource, attr.sourceSpan, templateMatchableAttrs,
templateElementOrDirectiveProps, templateElementVars);
}
if (!hasBinding && !hasTemplateBinding) {
// don't include the bindings as attributes as well in the AST
attrs.push(this.visitAttribute(attr, null));
matchableAttrs.push([attr.name, attr.value]);
}
});
const elementCssSelector = createElementCssSelector(nodeName, matchableAttrs);
const {directives: directiveMetas, matchElement} =
this._parseDirectives(this.selectorMatcher, elementCssSelector);
const references: ReferenceAst[] = [];
const boundDirectivePropNames = new Set<string>();
const directiveAsts = this._createDirectiveAsts(
isTemplateElement, element.name, directiveMetas, elementOrDirectiveProps,
elementOrDirectiveRefs, element.sourceSpan, references, boundDirectivePropNames);
const elementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts(
element.name, elementOrDirectiveProps, boundDirectivePropNames);
const isViewRoot = parent.isTemplateElement || hasInlineTemplates;
const providerContext = new ProviderElementContext(
this.providerViewContext, parent.providerContext, isViewRoot, directiveAsts, attrs,
references, isTemplateElement, queryStartIndex, element.sourceSpan);
const children: TemplateAst[] = html.visitAll(
preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this, element.children,
ElementContext.create(
isTemplateElement, directiveAsts,
isTemplateElement ? parent.providerContext : providerContext));
providerContext.afterElement();
// Override the actual selector when the `ngProjectAs` attribute is provided
const projectionSelector = preparsedElement.projectAs != null ?
CssSelector.parse(preparsedElement.projectAs)[0] :
elementCssSelector;
const ngContentIndex = parent.findNgContentIndex(projectionSelector);
let parsedElement: TemplateAst;
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
if (element.children && !element.children.every(_isEmptyTextNode)) {
this._reportError(`<ng-content> element cannot have content.`, element.sourceSpan);
}
parsedElement = new NgContentAst(
this.ngContentCount++, hasInlineTemplates ? null : ngContentIndex, element.sourceSpan);
} else if (isTemplateElement) {
this._assertAllEventsPublishedByDirectives(directiveAsts, events);
this._assertNoComponentsNorElementBindingsOnTemplate(
directiveAsts, elementProps, element.sourceSpan);
parsedElement = new EmbeddedTemplateAst(
attrs, events, references, elementVars, providerContext.transformedDirectiveAsts,
providerContext.transformProviders, providerContext.transformedHasViewContainer,
providerContext.queryMatches, children, hasInlineTemplates ? null : ngContentIndex,
element.sourceSpan);
} else {
this._assertElementExists(matchElement, element);
this._assertOnlyOneComponent(directiveAsts, element.sourceSpan);
const ngContentIndex =
hasInlineTemplates ? null : parent.findNgContentIndex(projectionSelector);
parsedElement = new ElementAst(
nodeName, attrs, elementProps, events, references,
providerContext.transformedDirectiveAsts, providerContext.transformProviders,
providerContext.transformedHasViewContainer, providerContext.queryMatches, children,
hasInlineTemplates ? null : ngContentIndex, element.sourceSpan, element.endSourceSpan);
}
if (hasInlineTemplates) {
const templateQueryStartIndex = this.contentQueryStartId;
const templateSelector = createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs);
const {directives: templateDirectiveMetas} =
this._parseDirectives(this.selectorMatcher, templateSelector);
const templateBoundDirectivePropNames = new Set<string>();
const templateDirectiveAsts = this._createDirectiveAsts(
true, element.name, templateDirectiveMetas, templateElementOrDirectiveProps, [],
element.sourceSpan, [], templateBoundDirectivePropNames);
const templateElementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts(
element.name, templateElementOrDirectiveProps, templateBoundDirectivePropNames);
this._assertNoComponentsNorElementBindingsOnTemplate(
templateDirectiveAsts, templateElementProps, element.sourceSpan);
const templateProviderContext = new ProviderElementContext(
this.providerViewContext, parent.providerContext, parent.isTemplateElement,
templateDirectiveAsts, [], [], true, templateQueryStartIndex, element.sourceSpan);
templateProviderContext.afterElement();
parsedElement = new EmbeddedTemplateAst(
[], [], [], templateElementVars, templateProviderContext.transformedDirectiveAsts,
templateProviderContext.transformProviders,
templateProviderContext.transformedHasViewContainer, templateProviderContext.queryMatches,
[parsedElement], ngContentIndex, element.sourceSpan);
}
return parsedElement;
}
private _parseAttr(
isTemplateElement: boolean, attr: html.Attribute, targetMatchableAttrs: string[][],
targetProps: BoundProperty[], targetEvents: BoundEventAst[],
targetRefs: ElementOrDirectiveRef[], targetVars: VariableAst[]): boolean {
const name = this._normalizeAttributeName(attr.name);
const value = attr.value;
const srcSpan = attr.sourceSpan;
const bindParts = name.match(BIND_NAME_REGEXP);
let hasBinding = false;
if (bindParts !== null) {
hasBinding = true;
if (bindParts[KW_BIND_IDX] != null) {
this._bindingParser.parsePropertyBinding(
bindParts[IDENT_KW_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps);
} else if (bindParts[KW_LET_IDX]) {
if (isTemplateElement) {
const identifier = bindParts[IDENT_KW_IDX];
this._parseVariable(identifier, value, srcSpan, targetVars);
} else {
this._reportError(`"let-" is only supported on template elements.`, srcSpan);
}
} else if (bindParts[KW_REF_IDX]) {
const identifier = bindParts[IDENT_KW_IDX];
this._parseReference(identifier, value, srcSpan, targetRefs);
} else if (bindParts[KW_ON_IDX]) {
this._bindingParser.parseEvent(
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
} else if (bindParts[KW_BINDON_IDX]) {
this._bindingParser.parsePropertyBinding(
bindParts[IDENT_KW_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps);
this._parseAssignmentEvent(
bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
} else if (bindParts[KW_AT_IDX]) {
this._bindingParser.parseLiteralAttr(
name, value, srcSpan, targetMatchableAttrs, targetProps);
} else if (bindParts[IDENT_BANANA_BOX_IDX]) {
this._bindingParser.parsePropertyBinding(
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, targetMatchableAttrs,
targetProps);
this._parseAssignmentEvent(
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
} else if (bindParts[IDENT_PROPERTY_IDX]) {
this._bindingParser.parsePropertyBinding(
bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, targetMatchableAttrs,
targetProps);
} else if (bindParts[IDENT_EVENT_IDX]) {
this._bindingParser.parseEvent(
bindParts[IDENT_EVENT_IDX], value, srcSpan, targetMatchableAttrs, targetEvents);
}
} else {
hasBinding = this._bindingParser.parsePropertyInterpolation(
name, value, srcSpan, targetMatchableAttrs, targetProps);
}
if (!hasBinding) {
this._bindingParser.parseLiteralAttr(name, value, srcSpan, targetMatchableAttrs, targetProps);
}
return hasBinding;
}
private _normalizeAttributeName(attrName: string): string {
return /^data-/i.test(attrName) ? attrName.substring(5) : attrName;
}
private _parseVariable(
identifier: string, value: string, sourceSpan: ParseSourceSpan, targetVars: VariableAst[]) {
if (identifier.indexOf('-') > -1) {
this._reportError(`"-" is not allowed in variable names`, sourceSpan);
}
targetVars.push(new VariableAst(identifier, value, sourceSpan));
}
private _parseReference(
identifier: string, value: string, sourceSpan: ParseSourceSpan,
targetRefs: ElementOrDirectiveRef[]) {
if (identifier.indexOf('-') > -1) {
this._reportError(`"-" is not allowed in reference names`, sourceSpan);
}
targetRefs.push(new ElementOrDirectiveRef(identifier, value, sourceSpan));
}
private _parseAssignmentEvent(
name: string, expression: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) {
this._bindingParser.parseEvent(
`${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, targetEvents);
}
private _parseDirectives(selectorMatcher: SelectorMatcher, elementCssSelector: CssSelector):
{directives: CompileDirectiveSummary[], matchElement: boolean} {
// Need to sort the directives so that we get consistent results throughout,
// as selectorMatcher uses Maps inside.
// Also deduplicate directives as they might match more than one time!
const directives = new Array(this.directivesIndex.size);
// Whether any directive selector matches on the element name
let matchElement = false;
selectorMatcher.match(elementCssSelector, (selector, directive) => {
directives[this.directivesIndex.get(directive)] = directive;
matchElement = matchElement || selector.hasElementSelector();
});
return {
directives: directives.filter(dir => !!dir),
matchElement,
};
}
private _createDirectiveAsts(
isTemplateElement: boolean, elementName: string, directives: CompileDirectiveSummary[],
props: BoundProperty[], elementOrDirectiveRefs: ElementOrDirectiveRef[],
elementSourceSpan: ParseSourceSpan, targetReferences: ReferenceAst[],
targetBoundDirectivePropNames: Set<string>): DirectiveAst[] {
const matchedReferences = new Set<string>();
let component: CompileDirectiveSummary = null;
const directiveAsts = directives.map((directive) => {
const sourceSpan = new ParseSourceSpan(
elementSourceSpan.start, elementSourceSpan.end,
`Directive ${identifierName(directive.type)}`);
if (directive.isComponent) {
component = directive;
}
const directiveProperties: BoundDirectivePropertyAst[] = [];
let hostProperties =
this._bindingParser.createDirectiveHostPropertyAsts(directive, elementName, sourceSpan);
// Note: We need to check the host properties here as well,
// as we don't know the element name in the DirectiveWrapperCompiler yet.
hostProperties = this._checkPropertiesInSchema(elementName, hostProperties);
const hostEvents = this._bindingParser.createDirectiveHostEventAsts(directive, sourceSpan);
this._createDirectivePropertyAsts(
directive.inputs, props, directiveProperties, targetBoundDirectivePropNames);
elementOrDirectiveRefs.forEach((elOrDirRef) => {
if ((elOrDirRef.value.length === 0 && directive.isComponent) ||
(directive.exportAs == elOrDirRef.value)) {
targetReferences.push(new ReferenceAst(
elOrDirRef.name, identifierToken(directive.type), elOrDirRef.sourceSpan));
matchedReferences.add(elOrDirRef.name);
}
});
const contentQueryStartId = this.contentQueryStartId;
this.contentQueryStartId += directive.queries.length;
return new DirectiveAst(
directive, directiveProperties, hostProperties, hostEvents, contentQueryStartId,
sourceSpan);
});
elementOrDirectiveRefs.forEach((elOrDirRef) => {
if (elOrDirRef.value.length > 0) {
if (!matchedReferences.has(elOrDirRef.name)) {
this._reportError(
`There is no directive with "exportAs" set to "${elOrDirRef.value}"`,
elOrDirRef.sourceSpan);
}
} else if (!component) {
let refToken: CompileTokenMetadata = null;
if (isTemplateElement) {
refToken = createIdentifierToken(Identifiers.TemplateRef);
}
targetReferences.push(new ReferenceAst(elOrDirRef.name, refToken, elOrDirRef.sourceSpan));
}
});
return directiveAsts;
}
private _createDirectivePropertyAsts(
directiveProperties: {[key: string]: string}, boundProps: BoundProperty[],
targetBoundDirectiveProps: BoundDirectivePropertyAst[],
targetBoundDirectivePropNames: Set<string>) {
if (directiveProperties) {
const boundPropsByName = new Map<string, BoundProperty>();
boundProps.forEach(boundProp => {
const prevValue = boundPropsByName.get(boundProp.name);
if (!prevValue || prevValue.isLiteral) {
// give [a]="b" a higher precedence than a="b" on the same element
boundPropsByName.set(boundProp.name, boundProp);
}
});
Object.keys(directiveProperties).forEach(dirProp => {
const elProp = directiveProperties[dirProp];
const boundProp = boundPropsByName.get(elProp);
// Bindings are optional, so this binding only needs to be set up if an expression is given.
if (boundProp) {
targetBoundDirectivePropNames.add(boundProp.name);
if (!isEmptyExpression(boundProp.expression)) {
targetBoundDirectiveProps.push(new BoundDirectivePropertyAst(
dirProp, boundProp.name, boundProp.expression, boundProp.sourceSpan));
}
}
});
}
}
private _createElementPropertyAsts(
elementName: string, props: BoundProperty[],
boundDirectivePropNames: Set<string>): BoundElementPropertyAst[] {
const boundElementProps: BoundElementPropertyAst[] = [];
props.forEach((prop: BoundProperty) => {
if (!prop.isLiteral && !boundDirectivePropNames.has(prop.name)) {
boundElementProps.push(this._bindingParser.createElementPropertyAst(elementName, prop));
}
});
return this._checkPropertiesInSchema(elementName, boundElementProps);
}
private _findComponentDirectives(directives: DirectiveAst[]): DirectiveAst[] {
return directives.filter(directive => directive.directive.isComponent);
}
private _findComponentDirectiveNames(directives: DirectiveAst[]): string[] {
return this._findComponentDirectives(directives)
.map(directive => identifierName(directive.directive.type));
}
private _assertOnlyOneComponent(directives: DirectiveAst[], sourceSpan: ParseSourceSpan) {
const componentTypeNames = this._findComponentDirectiveNames(directives);
if (componentTypeNames.length > 1) {
this._reportError(
`More than one component matched on this element.\n` +
`Make sure that only one component's selector can match a given element.\n` +
`Conflicting components: ${componentTypeNames.join(',')}`,
sourceSpan);
}
}
/**
* Make sure that non-angular tags conform to the schemas.
*
* Note: An element is considered an angular tag when at least one directive selector matches the
* tag name.
*
* @param matchElement Whether any directive has matched on the tag name
* @param element the html element
*/
private _assertElementExists(matchElement: boolean, element: html.Element) {
const elName = element.name.replace(/^:xhtml:/, '');
if (!matchElement && !this._schemaRegistry.hasElement(elName, this._schemas)) {
let errorMsg = `'${elName}' is not a known element:\n`;
errorMsg +=
`1. If '${elName}' is an Angular component, then verify that it is part of this module.\n`;
if (elName.indexOf('-') > -1) {
errorMsg +=
`2. If '${elName}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.`;
} else {
errorMsg +=
`2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`;
}
this._reportError(errorMsg, element.sourceSpan);
}
}
private _assertNoComponentsNorElementBindingsOnTemplate(
directives: DirectiveAst[], elementProps: BoundElementPropertyAst[],
sourceSpan: ParseSourceSpan) {
const componentTypeNames: string[] = this._findComponentDirectiveNames(directives);
if (componentTypeNames.length > 0) {
this._reportError(
`Components on an embedded template: ${componentTypeNames.join(',')}`, sourceSpan);
}
elementProps.forEach(prop => {
this._reportError(
`Property binding ${prop.name} not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "@NgModule.declarations".`,
sourceSpan);
});
}
private _assertAllEventsPublishedByDirectives(
directives: DirectiveAst[], events: BoundEventAst[]) {
const allDirectiveEvents = new Set<string>();
directives.forEach(directive => {
Object.keys(directive.directive.outputs).forEach(k => {
const eventName = directive.directive.outputs[k];
allDirectiveEvents.add(eventName);
});
});
events.forEach(event => {
if (event.target != null || !allDirectiveEvents.has(event.name)) {
this._reportError(
`Event binding ${event.fullName} not emitted by any directive on an embedded template. Make sure that the event name is spelled correctly and all directives are listed in the "@NgModule.declarations".`,
event.sourceSpan);
}
});
}
private _checkPropertiesInSchema(elementName: string, boundProps: BoundElementPropertyAst[]):
BoundElementPropertyAst[] {
// Note: We can't filter out empty expressions before this method,
// as we still want to validate them!
return boundProps.filter((boundProp) => {
if (boundProp.type === PropertyBindingType.Property &&
!this._schemaRegistry.hasProperty(elementName, boundProp.name, this._schemas)) {
let errorMsg =
`Can't bind to '${boundProp.name}' since it isn't a known property of '${elementName}'.`;
if (elementName.startsWith('ng-')) {
errorMsg +=
`\n1. If '${boundProp.name}' is an Angular directive, then add 'CommonModule' to the '@NgModule.imports' of this component.` +
`\n2. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`;
} else if (elementName.indexOf('-') > -1) {
errorMsg +=
`\n1. If '${elementName}' is an Angular component and it has '${boundProp.name}' input, then verify that it is part of this module.` +
`\n2. If '${elementName}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.` +
`\n3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`;
}
this._reportError(errorMsg, boundProp.sourceSpan);
}
return !isEmptyExpression(boundProp.value);
});
}
private _reportError(
message: string, sourceSpan: ParseSourceSpan,
level: ParseErrorLevel = ParseErrorLevel.FATAL) {
this._targetErrors.push(new ParseError(sourceSpan, message, level));
}
}
class NonBindableVisitor implements html.Visitor {
visitElement(ast: html.Element, parent: ElementContext): ElementAst {
const 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;
}
const attrNameAndValues = ast.attrs.map((attr): [string, string] => [attr.name, attr.value]);
const selector = createElementCssSelector(ast.name, attrNameAndValues);
const ngContentIndex = parent.findNgContentIndex(selector);
const children: TemplateAst[] = html.visitAll(this, ast.children, EMPTY_ELEMENT_CONTEXT);
return new ElementAst(
ast.name, html.visitAll(this, ast.attrs), [], [], [], [], [], false, [], children,
ngContentIndex, ast.sourceSpan, ast.endSourceSpan);
}
visitComment(comment: html.Comment, context: any): any { return null; }
visitAttribute(attribute: html.Attribute, context: any): AttrAst {
return new AttrAst(attribute.name, attribute.value, attribute.sourceSpan);
}
visitText(text: html.Text, parent: ElementContext): TextAst {
const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
return new TextAst(text.value, ngContentIndex, text.sourceSpan);
}
visitExpansion(expansion: html.Expansion, context: any): any { return expansion; }
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
}
class ElementOrDirectiveRef {
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
}
export function splitClasses(classAttrValue: string): string[] {
return classAttrValue.trim().split(/\s+/g);
}
class ElementContext {
static create(
isTemplateElement: boolean, directives: DirectiveAst[],
providerContext: ProviderElementContext): ElementContext {
const matcher = new SelectorMatcher();
let wildcardNgContentIndex: number = null;
const component = directives.find(directive => directive.directive.isComponent);
if (component) {
const ngContentSelectors = component.directive.template.ngContentSelectors;
for (let i = 0; i < ngContentSelectors.length; i++) {
const selector = ngContentSelectors[i];
if (selector === '*') {
wildcardNgContentIndex = i;
} else {
matcher.addSelectables(CssSelector.parse(ngContentSelectors[i]), i);
}
}
}
return new ElementContext(isTemplateElement, matcher, wildcardNgContentIndex, providerContext);
}
constructor(
public isTemplateElement: boolean, private _ngContentIndexMatcher: SelectorMatcher,
private _wildcardNgContentIndex: number, public providerContext: ProviderElementContext) {}
findNgContentIndex(selector: CssSelector): number {
const ngContentIndices: number[] = [];
this._ngContentIndexMatcher.match(
selector, (selector, ngContentIndex) => { ngContentIndices.push(ngContentIndex); });
ngContentIndices.sort();
if (this._wildcardNgContentIndex != null) {
ngContentIndices.push(this._wildcardNgContentIndex);
}
return ngContentIndices.length > 0 ? ngContentIndices[0] : null;
}
}
export function createElementCssSelector(
elementName: string, attributes: [string, string][]): CssSelector {
const cssSelector = new CssSelector();
const elNameNoNs = splitNsName(elementName)[1];
cssSelector.setElement(elNameNoNs);
for (let i = 0; i < attributes.length; i++) {
const attrName = attributes[i][0];
const attrNameNoNs = splitNsName(attrName)[1];
const attrValue = attributes[i][1];
cssSelector.addAttribute(attrNameNoNs, attrValue);
if (attrName.toLowerCase() == CLASS_ATTR) {
const classes = splitClasses(attrValue);
classes.forEach(className => cssSelector.addClassName(className));
}
}
return cssSelector;
}
const EMPTY_ELEMENT_CONTEXT = new ElementContext(true, new SelectorMatcher(), null, null);
const NON_BINDABLE_VISITOR = new NonBindableVisitor();
function _isEmptyTextNode(node: html.Node): boolean {
return node instanceof html.Text && node.value.trim().length == 0;
}
export function removeSummaryDuplicates<T extends{type: CompileTypeMetadata}>(items: T[]): T[] {
const map = new Map<any, T>();
items.forEach((item) => {
if (!map.get(item.type.reference)) {
map.set(item.type.reference, item);
}
});
return Array.from(map.values());
}
function isEmptyExpression(ast: AST): boolean {
if (ast instanceof ASTWithSource) {
ast = ast.ast;
}
return ast instanceof EmptyExpr;
}
// `template` is deprecated in 4.x
function isTemplate(
el: html.Element, enableLegacyTemplate: boolean,
reportDeprecation: (m: string, span: ParseSourceSpan) => void): boolean {
const tagNoNs = splitNsName(el.name)[1];
// `<ng-template>` is an angular construct and is lower case
if (tagNoNs === NG_TEMPLATE_ELEMENT) return true;
// `<template>` is HTML and case insensitive
if (tagNoNs.toLowerCase() === TEMPLATE_ELEMENT) {
if (enableLegacyTemplate && tagNoNs.toLowerCase() === TEMPLATE_ELEMENT) {
reportDeprecation(
`The <template> element is deprecated. Use <ng-template> instead`, el.sourceSpan);
return true;
}
return false;
}
}

View File

@ -0,0 +1,80 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as html from '../ml_parser/ast';
import {splitNsName} from '../ml_parser/tags';
const NG_CONTENT_SELECT_ATTR = 'select';
const NG_CONTENT_ELEMENT = 'ng-content';
const LINK_ELEMENT = 'link';
const LINK_STYLE_REL_ATTR = 'rel';
const LINK_STYLE_HREF_ATTR = 'href';
const LINK_STYLE_REL_VALUE = 'stylesheet';
const STYLE_ELEMENT = 'style';
const SCRIPT_ELEMENT = 'script';
const NG_NON_BINDABLE_ATTR = 'ngNonBindable';
const NG_PROJECT_AS = 'ngProjectAs';
export function preparseElement(ast: html.Element): PreparsedElement {
let selectAttr: string = null;
let hrefAttr: string = null;
let relAttr: string = null;
let nonBindable = false;
let projectAs: string = null;
ast.attrs.forEach(attr => {
const lcAttrName = attr.name.toLowerCase();
if (lcAttrName == NG_CONTENT_SELECT_ATTR) {
selectAttr = attr.value;
} else if (lcAttrName == LINK_STYLE_HREF_ATTR) {
hrefAttr = attr.value;
} else if (lcAttrName == LINK_STYLE_REL_ATTR) {
relAttr = attr.value;
} else if (attr.name == NG_NON_BINDABLE_ATTR) {
nonBindable = true;
} else if (attr.name == NG_PROJECT_AS) {
if (attr.value.length > 0) {
projectAs = attr.value;
}
}
});
selectAttr = normalizeNgContentSelect(selectAttr);
const nodeName = ast.name.toLowerCase();
let type = PreparsedElementType.OTHER;
if (splitNsName(nodeName)[1] == NG_CONTENT_ELEMENT) {
type = PreparsedElementType.NG_CONTENT;
} else if (nodeName == STYLE_ELEMENT) {
type = PreparsedElementType.STYLE;
} else if (nodeName == SCRIPT_ELEMENT) {
type = PreparsedElementType.SCRIPT;
} else if (nodeName == LINK_ELEMENT && relAttr == LINK_STYLE_REL_VALUE) {
type = PreparsedElementType.STYLESHEET;
}
return new PreparsedElement(type, selectAttr, hrefAttr, nonBindable, projectAs);
}
export enum PreparsedElementType {
NG_CONTENT,
STYLE,
STYLESHEET,
SCRIPT,
OTHER
}
export class PreparsedElement {
constructor(
public type: PreparsedElementType, public selectAttr: string, public hrefAttr: string,
public nonBindable: boolean, public projectAs: string) {}
}
function normalizeNgContentSelect(selectAttr: string): string {
if (selectAttr === null || selectAttr.length === 0) {
return '*';
}
return selectAttr;
}

View File

@ -0,0 +1,351 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Inject, InjectionToken, PACKAGE_ROOT_URL} from '@angular/core';
import {CompilerInjectable} from './injectable';
/**
* Create a {@link UrlResolver} with no package prefix.
*/
export function createUrlResolverWithoutPackagePrefix(): UrlResolver {
return new UrlResolver();
}
export function createOfflineCompileUrlResolver(): UrlResolver {
return new UrlResolver('.');
}
/**
* A default provider for {@link PACKAGE_ROOT_URL} that maps to '/'.
*/
export const DEFAULT_PACKAGE_URL_PROVIDER = {
provide: PACKAGE_ROOT_URL,
useValue: '/'
};
/**
* Used by the {@link Compiler} when resolving HTML and CSS template URLs.
*
* This class can be overridden by the application developer to create custom behavior.
*
* See {@link Compiler}
*
* ## Example
*
* {@example compiler/ts/url_resolver/url_resolver.ts region='url_resolver'}
*
* @security When compiling templates at runtime, you must
* ensure that the entire template comes from a trusted source.
* Attacker-controlled data introduced by a template could expose your
* application to XSS risks. For more detail, see the [Security Guide](http://g.co/ng/security).
*/
@CompilerInjectable()
export class UrlResolver {
constructor(@Inject(PACKAGE_ROOT_URL) private _packagePrefix: string = null) {}
/**
* Resolves the `url` given the `baseUrl`:
* - when the `url` is null, the `baseUrl` is returned,
* - if `url` is relative ('path/to/here', './path/to/here'), the resolved url is a combination of
* `baseUrl` and `url`,
* - if `url` is absolute (it has a scheme: 'http://', 'https://' or start with '/'), the `url` is
* returned as is (ignoring the `baseUrl`)
*/
resolve(baseUrl: string, url: string): string {
let resolvedUrl = url;
if (baseUrl != null && baseUrl.length > 0) {
resolvedUrl = _resolveUrl(baseUrl, resolvedUrl);
}
const resolvedParts = _split(resolvedUrl);
let prefix = this._packagePrefix;
if (prefix != null && resolvedParts != null &&
resolvedParts[_ComponentIndex.Scheme] == 'package') {
let path = resolvedParts[_ComponentIndex.Path];
prefix = prefix.replace(/\/+$/, '');
path = path.replace(/^\/+/, '');
return `${prefix}/${path}`;
}
return resolvedUrl;
}
}
/**
* Extract the scheme of a URL.
*/
export function getUrlScheme(url: string): string {
const match = _split(url);
return (match && match[_ComponentIndex.Scheme]) || '';
}
// The code below is adapted from Traceur:
// https://github.com/google/traceur-compiler/blob/9511c1dafa972bf0de1202a8a863bad02f0f95a8/src/runtime/url.js
/**
* Builds a URI string from already-encoded parts.
*
* No encoding is performed. Any component may be omitted as either null or
* undefined.
*
* @param opt_scheme The scheme such as 'http'.
* @param opt_userInfo The user name before the '@'.
* @param opt_domain The domain such as 'www.google.com', already
* URI-encoded.
* @param opt_port The port number.
* @param opt_path The path, already URI-encoded. If it is not
* empty, it must begin with a slash.
* @param opt_queryData The URI-encoded query data.
* @param opt_fragment The URI-encoded fragment identifier.
* @return 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 {
const out: string[] = [];
if (opt_scheme != null) {
out.push(opt_scheme + ':');
}
if (opt_domain != null) {
out.push('//');
if (opt_userInfo != null) {
out.push(opt_userInfo + '@');
}
out.push(opt_domain);
if (opt_port != null) {
out.push(':' + opt_port);
}
}
if (opt_path != null) {
out.push(opt_path);
}
if (opt_queryData != null) {
out.push('?' + opt_queryData);
}
if (opt_fragment != null) {
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
*/
const _splitRe = new RegExp(
'^' +
'(?:' +
'([^:/?#.]+)' + // 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 uri The URI string to examine.
* @return 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 uri.match(_splitRe);
}
/**
* Removes dot segments in given path component, as described in
* RFC 3986, section 5.2.4.
*
* @param path A non-empty path component.
* @return Path component with removed dot segments.
*/
function _removeDotSegments(path: string): string {
if (path == '/') return '/';
const leadingSlash = path[0] == '/' ? '/' : '';
const trailingSlash = path[path.length - 1] === '/' ? '/' : '';
const segments = path.split('/');
const out: string[] = [];
let up = 0;
for (let pos = 0; pos < segments.length; pos++) {
const 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.
*/
function _joinAndCanonicalizePath(parts: any[]): string {
let path = parts[_ComponentIndex.Path];
path = path == null ? '' : _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 base The URL acting as the base URL.
* @param to The URL to resolve.
*/
function _resolveUrl(base: string, url: string): string {
const parts = _split(encodeURI(url));
const baseParts = _split(base);
if (parts[_ComponentIndex.Scheme] != null) {
return _joinAndCanonicalizePath(parts);
} else {
parts[_ComponentIndex.Scheme] = baseParts[_ComponentIndex.Scheme];
}
for (let i = _ComponentIndex.Scheme; i <= _ComponentIndex.Port; i++) {
if (parts[i] == null) {
parts[i] = baseParts[i];
}
}
if (parts[_ComponentIndex.Path][0] == '/') {
return _joinAndCanonicalizePath(parts);
}
let path = baseParts[_ComponentIndex.Path];
if (path == null) path = '/';
const index = path.lastIndexOf('/');
path = path.substring(0, index + 1) + parts[_ComponentIndex.Path];
parts[_ComponentIndex.Path] = path;
return _joinAndCanonicalizePath(parts);
}

View File

@ -0,0 +1,100 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export const MODULE_SUFFIX = '';
const CAMEL_CASE_REGEXP = /([A-Z])/g;
const DASH_CASE_REGEXP = /-+([a-z0-9])/g;
export function camelCaseToDashCase(input: string): string {
return input.replace(CAMEL_CASE_REGEXP, (...m: any[]) => '-' + m[1].toLowerCase());
}
export function dashCaseToCamelCase(input: string): string {
return input.replace(DASH_CASE_REGEXP, (...m: any[]) => m[1].toUpperCase());
}
export function splitAtColon(input: string, defaultValues: string[]): string[] {
return _splitAt(input, ':', defaultValues);
}
export function splitAtPeriod(input: string, defaultValues: string[]): string[] {
return _splitAt(input, '.', defaultValues);
}
function _splitAt(input: string, character: string, defaultValues: string[]): string[] {
const characterIndex = input.indexOf(character);
if (characterIndex == -1) return defaultValues;
return [input.slice(0, characterIndex).trim(), input.slice(characterIndex + 1).trim()];
}
export function visitValue(value: any, visitor: ValueVisitor, context: any): any {
if (Array.isArray(value)) {
return visitor.visitArray(<any[]>value, context);
}
if (isStrictStringMap(value)) {
return visitor.visitStringMap(<{[key: string]: any}>value, context);
}
if (value == null || typeof value == 'string' || typeof value == 'number' ||
typeof value == 'boolean') {
return visitor.visitPrimitive(value, context);
}
return visitor.visitOther(value, context);
}
export interface ValueVisitor {
visitArray(arr: any[], context: any): any;
visitStringMap(map: {[key: string]: any}, context: any): any;
visitPrimitive(value: any, context: any): any;
visitOther(value: any, context: any): any;
}
export class ValueTransformer implements ValueVisitor {
visitArray(arr: any[], context: any): any {
return arr.map(value => visitValue(value, this, context));
}
visitStringMap(map: {[key: string]: any}, context: any): any {
const result: {[key: string]: any} = {};
Object.keys(map).forEach(key => { result[key] = visitValue(map[key], this, context); });
return result;
}
visitPrimitive(value: any, context: any): any { return value; }
visitOther(value: any, context: any): any { return value; }
}
export class SyncAsyncResult<T> {
constructor(public syncResult: T, public asyncResult: Promise<T> = null) {
if (!asyncResult) {
this.asyncResult = Promise.resolve(syncResult);
}
}
}
export function syntaxError(msg: string): Error {
const error = Error(msg);
(error as any)[ERROR_SYNTAX_ERROR] = true;
return error;
}
const ERROR_SYNTAX_ERROR = 'ngSyntaxError';
export function isSyntaxError(error: Error): boolean {
return (error as any)[ERROR_SYNTAX_ERROR];
}
export function escapeRegExp(s: string): string {
return s.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
}
const STRING_MAP_PROTO = Object.getPrototypeOf({});
function isStrictStringMap(obj: any): boolean {
return typeof obj === 'object' && obj !== null && Object.getPrototypeOf(obj) === STRING_MAP_PROTO;
}

View File

@ -0,0 +1,19 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* @module
* @description
* Entry point for all public APIs of the common package.
*/
import {Version} from '@angular/core';
/**
* @stable
*/
export const VERSION = new Version('0.0.0-PLACEHOLDER');

File diff suppressed because it is too large Load Diff