angular-cn/packages/compiler/src/directive_normalizer.ts

260 lines
10 KiB
TypeScript
Raw Normal View History

/**
* @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
*/
2017-03-02 12:37:01 -05:00
import {ViewEncapsulation, ɵstringify as stringify} from '@angular/core';
import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, templateSourceUrl} 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';
2016-07-21 14:41:25 -04:00
import {PreparsedElementType, preparseElement} from './template_parser/template_preparser';
import {UrlResolver} from './url_resolver';
import {SyncAsync, isDefined, syntaxError} from './util';
export interface PrenormalizedTemplateMetadata {
ngModuleType: any;
componentType: any;
moduleUrl: string;
template: string|null;
templateUrl: string|null;
styles: string[];
styleUrls: string[];
interpolation: [string, string]|null;
encapsulation: ViewEncapsulation|null;
animations: CompileAnimationEntryMetadata[];
}
@CompilerInjectable()
export class DirectiveNormalizer {
private _resourceLoaderCache = new Map<string, SyncAsync<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;
}
const template = normalizedDirective.template !;
this._resourceLoaderCache.delete(template.templateUrl !);
template.externalStylesheets.forEach(
(stylesheet) => { this._resourceLoaderCache.delete(stylesheet.moduleUrl !); });
}
private _fetch(url: string): SyncAsync<string> {
let result = this._resourceLoaderCache.get(url);
if (!result) {
result = this._resourceLoader.get(url);
this._resourceLoaderCache.set(url, result);
}
return result;
}
normalizeTemplate(prenormData: PrenormalizedTemplateMetadata):
SyncAsync<CompileTemplateMetadata> {
if (isDefined(prenormData.template)) {
if (isDefined(prenormData.templateUrl)) {
throw syntaxError(
`'${stringify(prenormData.componentType)}' component cannot define both template and templateUrl`);
}
if (typeof prenormData.template !== 'string') {
throw syntaxError(
`The template specified for component ${stringify(prenormData.componentType)} is not a string`);
}
} else if (isDefined(prenormData.templateUrl)) {
if (typeof prenormData.templateUrl !== 'string') {
throw syntaxError(
`The templateUrl specified for component ${stringify(prenormData.componentType)} is not a string`);
}
} else {
throw syntaxError(
`No template specified for component ${stringify(prenormData.componentType)}`);
}
return SyncAsync.then(
this.normalizeTemplateOnly(prenormData),
(result: CompileTemplateMetadata) => this.normalizeExternalStylesheets(result));
}
normalizeTemplateOnly(prenomData: PrenormalizedTemplateMetadata):
SyncAsync<CompileTemplateMetadata> {
let template: SyncAsync<string>;
let templateUrl: string;
if (prenomData.template != null) {
template = prenomData.template;
templateUrl = prenomData.moduleUrl;
} else {
templateUrl = this._urlResolver.resolve(prenomData.moduleUrl, prenomData.templateUrl !);
template = this._fetch(templateUrl);
}
return SyncAsync.then(
template, (template) => this.normalizeLoadedTemplate(prenomData, template, templateUrl));
}
normalizeLoadedTemplate(
prenormData: PrenormalizedTemplateMetadata, template: string,
templateAbsUrl: string): CompileTemplateMetadata {
const isInline = !!prenormData.template;
const interpolationConfig = InterpolationConfig.fromArray(prenormData.interpolation !);
const rootNodesAndErrors = this._htmlParser.parse(
template,
templateSourceUrl(
{reference: prenormData.ngModuleType}, {type: {reference: prenormData.componentType}},
{isInline, templateUrl: templateAbsUrl}),
true, interpolationConfig);
if (rootNodesAndErrors.errors.length > 0) {
2016-07-09 13:12:39 -04:00
const errorString = rootNodesAndErrors.errors.join('\n');
throw syntaxError(`Template parse errors:\n${errorString}`);
}
2016-07-09 13:12:39 -04:00
const templateMetadataStyles = this.normalizeStylesheet(new CompileStylesheetMetadata({
styles: prenormData.styles,
styleUrls: prenormData.styleUrls,
moduleUrl: prenormData.moduleUrl
}));
2016-07-09 13:12:39 -04:00
const visitor = new TemplatePreparseVisitor();
2016-07-21 16:56:58 -04:00
html.visitAll(visitor, rootNodesAndErrors.rootNodes);
2016-07-09 13:12:39 -04:00
const templateStyles = this.normalizeStylesheet(new CompileStylesheetMetadata(
{styles: visitor.styles, styleUrls: visitor.styleUrls, moduleUrl: templateAbsUrl}));
let encapsulation = prenormData.encapsulation;
if (encapsulation == null) {
encapsulation = this._config.defaultEncapsulation;
}
2016-09-20 17:14:57 -04:00
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;
}
2016-09-20 17:14:57 -04:00
return new CompileTemplateMetadata({
encapsulation,
2016-09-20 17:14:57 -04:00
template,
templateUrl: templateAbsUrl, styles, styleUrls,
ngContentSelectors: visitor.ngContentSelectors,
animations: prenormData.animations,
interpolation: prenormData.interpolation, isInline,
externalStylesheets: []
});
}
normalizeExternalStylesheets(templateMeta: CompileTemplateMetadata):
SyncAsync<CompileTemplateMetadata> {
return SyncAsync.then(
this._loadMissingExternalStylesheets(templateMeta.styleUrls),
(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,
isInline: templateMeta.isInline,
}));
}
private _loadMissingExternalStylesheets(
styleUrls: string[],
loadedStylesheets:
Map<string, CompileStylesheetMetadata> = new Map<string, CompileStylesheetMetadata>()):
SyncAsync<CompileStylesheetMetadata[]> {
return SyncAsync.then(
SyncAsync.all(styleUrls.filter((styleUrl) => !loadedStylesheets.has(styleUrl))
.map(
styleUrl => SyncAsync.then(
this._fetch(styleUrl),
(loadedStyle) => {
const stylesheet =
this.normalizeStylesheet(new CompileStylesheetMetadata(
{styles: [loadedStyle], moduleUrl: styleUrl}));
loadedStylesheets.set(styleUrl, stylesheet);
return this._loadMissingExternalStylesheets(
stylesheet.styleUrls, loadedStylesheets);
}))),
(_) => Array.from(loadedStylesheets.values()));
}
normalizeStylesheet(stylesheet: CompileStylesheetMetadata): CompileStylesheetMetadata {
const moduleUrl = stylesheet.moduleUrl !;
const allStyleUrls = stylesheet.styleUrls.filter(isStyleUrlResolvable)
.map(url => this._urlResolver.resolve(moduleUrl, url));
const allStyles = stylesheet.styles.map(style => {
const styleWithImports = extractStyleUrls(this._urlResolver, moduleUrl, style);
allStyleUrls.push(...styleWithImports.styleUrls);
return styleWithImports.style;
});
return new CompileStylesheetMetadata(
{styles: allStyles, styleUrls: allStyleUrls, moduleUrl: moduleUrl});
}
}
2016-07-21 16:56:58 -04:00
class TemplatePreparseVisitor implements html.Visitor {
ngContentSelectors: string[] = [];
styles: string[] = [];
styleUrls: string[] = [];
ngNonBindableStackCount: number = 0;
2016-07-21 16:56:58 -04:00
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 => {
2016-07-21 16:56:58 -04:00
if (child instanceof html.Text) {
2016-07-13 14:01:32 -04:00
textContent += child.value;
}
});
this.styles.push(textContent);
break;
case PreparsedElementType.STYLESHEET:
this.styleUrls.push(preparsedElement.hrefAttr);
break;
default:
break;
}
if (preparsedElement.nonBindable) {
this.ngNonBindableStackCount++;
}
2016-07-21 16:56:58 -04:00
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);
}
2016-07-21 16:56:58 -04:00
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; }
}