refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
411
packages/compiler/src/aot/compiler.ts
Normal file
411
packages/compiler/src/aot/compiler.ts
Normal 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};
|
||||
}
|
84
packages/compiler/src/aot/compiler_factory.ts
Normal file
84
packages/compiler/src/aot/compiler_factory.ts
Normal 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};
|
||||
}
|
30
packages/compiler/src/aot/compiler_host.ts
Normal file
30
packages/compiler/src/aot/compiler_host.ts
Normal 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>;
|
||||
}
|
14
packages/compiler/src/aot/compiler_options.ts
Normal file
14
packages/compiler/src/aot/compiler_options.ts
Normal 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;
|
||||
}
|
11
packages/compiler/src/aot/generated_file.ts
Normal file
11
packages/compiler/src/aot/generated_file.ts
Normal 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) {}
|
||||
}
|
59
packages/compiler/src/aot/static_reflection_capabilities.ts
Normal file
59
packages/compiler/src/aot/static_reflection_capabilities.ts
Normal 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;
|
||||
}
|
700
packages/compiler/src/aot/static_reflector.ts
Normal file
700
packages/compiler/src/aot/static_reflector.ts
Normal 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;
|
||||
}
|
43
packages/compiler/src/aot/static_symbol.ts
Normal file
43
packages/compiler/src/aot/static_symbol.ts
Normal 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;
|
||||
}
|
||||
}
|
391
packages/compiler/src/aot/static_symbol_resolver.ts
Normal file
391
packages/compiler/src/aot/static_symbol_resolver.ts
Normal 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;
|
||||
}
|
96
packages/compiler/src/aot/summary_resolver.ts
Normal file
96
packages/compiler/src/aot/summary_resolver.ts
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
192
packages/compiler/src/aot/summary_serializer.ts
Normal file
192
packages/compiler/src/aot/summary_serializer.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
42
packages/compiler/src/aot/util.ts
Normal file
42
packages/compiler/src/aot/util.ts
Normal 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`;
|
||||
}
|
47
packages/compiler/src/assertions.ts
Normal file
47
packages/compiler/src/assertions.ts
Normal 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.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
89
packages/compiler/src/chars.ts
Normal file
89
packages/compiler/src/chars.ts
Normal 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);
|
||||
}
|
740
packages/compiler/src/compile_metadata.ts
Normal file
740
packages/compiler/src/compile_metadata.ts
Normal 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);
|
||||
}, []);
|
||||
}
|
69
packages/compiler/src/compiler.ts
Normal file
69
packages/compiler/src/compiler.ts
Normal 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.
|
609
packages/compiler/src/compiler_util/expression_converter.ts
Normal file
609
packages/compiler/src/compiler_util/expression_converter.ts
Normal 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);
|
||||
}
|
||||
}
|
36
packages/compiler/src/config.ts
Normal file
36
packages/compiler/src/config.ts
Normal 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;
|
||||
}
|
||||
}
|
248
packages/compiler/src/css_parser/css_ast.ts
Normal file
248
packages/compiler/src/css_parser/css_ast.ts
Normal 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);
|
||||
}
|
720
packages/compiler/src/css_parser/css_lexer.ts
Normal file
720
packages/compiler/src/css_parser/css_lexer.ts
Normal 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;
|
||||
}
|
||||
}
|
900
packages/compiler/src/css_parser/css_parser.ts
Normal file
900
packages/compiler/src/css_parser/css_parser.ts
Normal 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); }
|
||||
}
|
247
packages/compiler/src/directive_normalizer.ts
Normal file
247
packages/compiler/src/directive_normalizer.ts
Normal 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; }
|
||||
}
|
176
packages/compiler/src/directive_resolver.ts
Normal file
176
packages/compiler/src/directive_resolver.ts
Normal 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;
|
||||
}
|
392
packages/compiler/src/expression_parser/ast.ts
Normal file
392
packages/compiler/src/expression_parser/ast.ts
Normal 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);
|
||||
}
|
||||
}
|
392
packages/compiler/src/expression_parser/lexer.ts
Normal file
392
packages/compiler/src/expression_parser/lexer.ts
Normal 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;
|
||||
}
|
812
packages/compiler/src/expression_parser/parser.ts
Normal file
812
packages/compiler/src/expression_parser/parser.ts
Normal 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) {}
|
||||
}
|
376
packages/compiler/src/i18n/digest.ts
Normal file
376
packages/compiler/src/i18n/digest.ts
Normal 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;
|
||||
}
|
117
packages/compiler/src/i18n/extractor.ts
Normal file
117
packages/compiler/src/i18n/extractor.ts
Normal 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};
|
||||
}
|
||||
}
|
497
packages/compiler/src/i18n/extractor_merger.ts
Normal file
497
packages/compiler/src/i18n/extractor_merger.ts
Normal 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};
|
||||
}
|
134
packages/compiler/src/i18n/i18n_ast.ts
Normal file
134
packages/compiler/src/i18n/i18n_ast.ts
Normal 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{};
|
||||
}
|
70
packages/compiler/src/i18n/i18n_html_parser.ts
Normal file
70
packages/compiler/src/i18n/i18n_html_parser.ts
Normal 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();
|
||||
}
|
||||
}
|
168
packages/compiler/src/i18n/i18n_parser.ts
Normal file
168
packages/compiler/src/i18n/i18n_parser.ts
Normal 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];
|
||||
}
|
15
packages/compiler/src/i18n/index.ts
Normal file
15
packages/compiler/src/i18n/index.ts
Normal 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';
|
95
packages/compiler/src/i18n/message_bundle.ts
Normal file
95
packages/compiler/src/i18n/message_bundle.ts
Normal 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);
|
||||
}
|
||||
}
|
16
packages/compiler/src/i18n/parse_util.ts
Normal file
16
packages/compiler/src/i18n/parse_util.ts
Normal 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); }
|
||||
}
|
124
packages/compiler/src/i18n/serializers/placeholder.ts
Normal file
124
packages/compiler/src/i18n/serializers/placeholder.ts
Normal 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}`;
|
||||
}
|
||||
}
|
98
packages/compiler/src/i18n/serializers/serializer.ts
Normal file
98
packages/compiler/src/i18n/serializers/serializer.ts
Normal 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;
|
||||
}
|
||||
}
|
290
packages/compiler/src/i18n/serializers/xliff.ts
Normal file
290
packages/compiler/src/i18n/serializers/xliff.ts
Normal 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}`;
|
||||
}
|
||||
}
|
164
packages/compiler/src/i18n/serializers/xmb.ts
Normal file
164
packages/compiler/src/i18n/serializers/xmb.ts
Normal 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, '_');
|
||||
}
|
106
packages/compiler/src/i18n/serializers/xml_helper.ts
Normal file
106
packages/compiler/src/i18n/serializers/xml_helper.ts
Normal 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, '&'],
|
||||
[/"/g, '"'],
|
||||
[/'/g, '''],
|
||||
[/</g, '<'],
|
||||
[/>/g, '>'],
|
||||
];
|
||||
|
||||
function _escapeXml(text: string): string {
|
||||
return _ESCAPED_CHARS.reduce(
|
||||
(text: string, entry: [RegExp, string]) => text.replace(entry[0], entry[1]), text);
|
||||
}
|
210
packages/compiler/src/i18n/serializers/xtb.ts
Normal file
210
packages/compiler/src/i18n/serializers/xtb.ts
Normal 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));
|
||||
}
|
||||
}
|
187
packages/compiler/src/i18n/translation_bundle.ts
Normal file
187
packages/compiler/src/i18n/translation_bundle.ts
Normal 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));
|
||||
}
|
||||
}
|
144
packages/compiler/src/identifiers.ts
Normal file
144
packages/compiler/src/identifiers.ts
Normal 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};
|
||||
}
|
17
packages/compiler/src/injectable.ts
Normal file
17
packages/compiler/src/injectable.ts
Normal 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;
|
||||
}
|
361
packages/compiler/src/jit/compiler.ts
Normal file
361
packages/compiler/src/jit/compiler.ts
Normal 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); }
|
||||
}
|
169
packages/compiler/src/jit/compiler_factory.ts
Normal file
169
packages/compiler/src/jit/compiler_factory.ts
Normal 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;
|
||||
}
|
35
packages/compiler/src/lifecycle_reflector.ts
Normal file
35
packages/compiler/src/lifecycle_reflector.ts
Normal 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';
|
||||
}
|
||||
}
|
1079
packages/compiler/src/metadata_resolver.ts
Normal file
1079
packages/compiler/src/metadata_resolver.ts
Normal file
File diff suppressed because it is too large
Load Diff
82
packages/compiler/src/ml_parser/ast.ts
Normal file
82
packages/compiler/src/ml_parser/ast.ts
Normal 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;
|
||||
}
|
26
packages/compiler/src/ml_parser/html_parser.ts
Normal file
26
packages/compiler/src/ml_parser/html_parser.ts
Normal 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);
|
||||
}
|
||||
}
|
129
packages/compiler/src/ml_parser/html_tags.ts
Normal file
129
packages/compiler/src/ml_parser/html_tags.ts
Normal 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;
|
||||
}
|
125
packages/compiler/src/ml_parser/icu_ast_expander.ts
Normal file
125
packages/compiler/src/ml_parser/icu_ast_expander.ts
Normal 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);
|
||||
}
|
25
packages/compiler/src/ml_parser/interpolation_config.ts
Normal file
25
packages/compiler/src/ml_parser/interpolation_config.ts
Normal 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('{{', '}}');
|
713
packages/compiler/src/ml_parser/lexer.ts
Normal file
713
packages/compiler/src/ml_parser/lexer.ts
Normal 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;
|
||||
}
|
414
packages/compiler/src/ml_parser/parser.ts
Normal file
414
packages/compiler/src/ml_parser/parser.ts
Normal 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;
|
||||
}
|
310
packages/compiler/src/ml_parser/tags.ts
Normal file
310
packages/compiler/src/ml_parser/tags.ts
Normal 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 `{` / `ƫ` 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',
|
||||
};
|
20
packages/compiler/src/ml_parser/xml_parser.ts
Normal file
20
packages/compiler/src/ml_parser/xml_parser.ts
Normal 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);
|
||||
}
|
||||
}
|
30
packages/compiler/src/ml_parser/xml_tags.ts
Normal file
30
packages/compiler/src/ml_parser/xml_tags.ts
Normal 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;
|
||||
}
|
255
packages/compiler/src/ng_module_compiler.ts
Normal file
255
packages/compiler/src/ng_module_compiler.ts
Normal 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');
|
||||
}
|
38
packages/compiler/src/ng_module_resolver.ts
Normal file
38
packages/compiler/src/ng_module_resolver.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
476
packages/compiler/src/output/abstract_emitter.ts
Normal file
476
packages/compiler/src/output/abstract_emitter.ts
Normal 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;
|
||||
}
|
167
packages/compiler/src/output/abstract_js_emitter.ts
Normal file
167
packages/compiler/src/output/abstract_js_emitter.ts
Normal 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;
|
||||
}
|
||||
}
|
65
packages/compiler/src/output/class_builder.ts
Normal file
65
packages/compiler/src/output/class_builder.ts
Normal 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[];
|
||||
}
|
97
packages/compiler/src/output/js_emitter.ts
Normal file
97
packages/compiler/src/output/js_emitter.ts
Normal 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}; }});`;
|
||||
}
|
987
packages/compiler/src/output/output_ast.ts
Normal file
987
packages/compiler/src/output/output_ast.ts
Normal 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);
|
||||
}
|
331
packages/compiler/src/output/output_interpreter.ts
Normal file
331
packages/compiler/src/output/output_interpreter.ts
Normal 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';
|
62
packages/compiler/src/output/output_jit.ts
Normal file
62
packages/compiler/src/output/output_jit.ts
Normal 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;
|
||||
}
|
||||
}
|
32
packages/compiler/src/output/path_util.ts
Normal file
32
packages/compiler/src/output/path_util.ts
Normal 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*/;
|
||||
}
|
184
packages/compiler/src/output/source_map.ts
Normal file
184
packages/compiler/src/output/source_map.ts
Normal 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];
|
||||
}
|
437
packages/compiler/src/output/ts_emitter.ts
Normal file
437
packages/compiler/src/output/ts_emitter.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
44
packages/compiler/src/output/value_util.ts
Normal file
44
packages/compiler/src/output/value_util.ts
Normal 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});
|
||||
}
|
||||
}
|
||||
}
|
126
packages/compiler/src/parse_util.ts
Normal file
126
packages/compiler/src/parse_util.ts
Normal 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}`;
|
||||
}
|
||||
}
|
49
packages/compiler/src/pipe_resolver.ts
Normal file
49
packages/compiler/src/pipe_resolver.ts
Normal 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;
|
||||
}
|
||||
}
|
512
packages/compiler/src/provider_analyzer.ts
Normal file
512
packages/compiler/src/provider_analyzer.ts
Normal 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);
|
||||
});
|
||||
}
|
15
packages/compiler/src/resource_loader.ts
Normal file
15
packages/compiler/src/resource_loader.ts
Normal 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; }
|
||||
}
|
440
packages/compiler/src/schema/dom_element_schema_registry.ts
Normal file
440
packages/compiler/src/schema/dom_element_schema_registry.ts
Normal 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;
|
||||
}
|
||||
}
|
58
packages/compiler/src/schema/dom_security_schema.ts
Normal file
58
packages/compiler/src/schema/dom_security_schema.ts
Normal 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',
|
||||
]);
|
25
packages/compiler/src/schema/element_schema_registry.ts
Normal file
25
packages/compiler/src/schema/element_schema_registry.ts
Normal 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};
|
||||
}
|
370
packages/compiler/src/selector.ts
Normal file
370
packages/compiler/src/selector.ts
Normal 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;
|
||||
}
|
||||
}
|
598
packages/compiler/src/shadow_css.ts
Normal file
598
packages/compiler/src/shadow_css.ts
Normal 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);
|
||||
}
|
96
packages/compiler/src/style_compiler.ts
Normal file
96
packages/compiler/src/style_compiler.ts
Normal 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;
|
||||
}
|
47
packages/compiler/src/style_url_resolver.ts
Normal file
47
packages/compiler/src/style_url_resolver.ts
Normal 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 = /^([^:/?#]+):/;
|
24
packages/compiler/src/summary_resolver.ts
Normal file
24
packages/compiler/src/summary_resolver.ts
Normal 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; }
|
||||
}
|
443
packages/compiler/src/template_parser/binding_parser.ts
Normal file
443
packages/compiler/src/template_parser/binding_parser.ts
Normal 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();
|
||||
}
|
287
packages/compiler/src/template_parser/template_ast.ts
Normal file
287
packages/compiler/src/template_parser/template_ast.ts
Normal 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;
|
||||
}
|
884
packages/compiler/src/template_parser/template_parser.ts
Normal file
884
packages/compiler/src/template_parser/template_parser.ts
Normal 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;
|
||||
}
|
||||
}
|
80
packages/compiler/src/template_parser/template_preparser.ts
Normal file
80
packages/compiler/src/template_parser/template_preparser.ts
Normal 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;
|
||||
}
|
351
packages/compiler/src/url_resolver.ts
Normal file
351
packages/compiler/src/url_resolver.ts
Normal 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);
|
||||
}
|
100
packages/compiler/src/util.ts
Normal file
100
packages/compiler/src/util.ts
Normal 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;
|
||||
}
|
19
packages/compiler/src/version.ts
Normal file
19
packages/compiler/src/version.ts
Normal 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');
|
1061
packages/compiler/src/view_compiler/view_compiler.ts
Normal file
1061
packages/compiler/src/view_compiler/view_compiler.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user