2016-06-23 12:47:54 -04:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
2016-12-22 13:17:41 -05:00
|
|
|
import {ViewEncapsulation} from '@angular/core';
|
2016-07-21 16:56:58 -04:00
|
|
|
|
2016-11-10 17:07:30 -05:00
|
|
|
import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from './compile_metadata';
|
2016-05-26 19:43:15 -04:00
|
|
|
import {CompilerConfig} from './config';
|
2016-12-11 05:43:51 -05:00
|
|
|
import {stringify} from './facade/lang';
|
2016-12-15 12:12:40 -05:00
|
|
|
import {CompilerInjectable} from './injectable';
|
2016-08-01 15:19:09 -04:00
|
|
|
import * as html from './ml_parser/ast';
|
|
|
|
import {HtmlParser} from './ml_parser/html_parser';
|
|
|
|
import {InterpolationConfig} from './ml_parser/interpolation_config';
|
2016-08-17 12:24:44 -04:00
|
|
|
import {ResourceLoader} from './resource_loader';
|
2016-06-24 11:46:43 -04:00
|
|
|
import {extractStyleUrls, isStyleUrlResolvable} from './style_url_resolver';
|
2016-07-21 14:41:25 -04:00
|
|
|
import {PreparsedElementType, preparseElement} from './template_parser/template_preparser';
|
2016-06-24 11:46:43 -04:00
|
|
|
import {UrlResolver} from './url_resolver';
|
2016-12-15 16:07:12 -05:00
|
|
|
import {SyncAsyncResult, SyntaxError} from './util';
|
2015-08-25 18:36:02 -04:00
|
|
|
|
2016-11-10 17:07:30 -05:00
|
|
|
export interface PrenormalizedTemplateMetadata {
|
|
|
|
componentType: any;
|
|
|
|
moduleUrl: string;
|
|
|
|
template?: string;
|
|
|
|
templateUrl?: string;
|
|
|
|
styles?: string[];
|
|
|
|
styleUrls?: string[];
|
|
|
|
interpolation?: [string, string];
|
|
|
|
encapsulation?: ViewEncapsulation;
|
|
|
|
animations?: CompileAnimationEntryMetadata[];
|
|
|
|
}
|
|
|
|
|
2016-12-15 12:12:40 -05:00
|
|
|
@CompilerInjectable()
|
2016-01-06 17:13:44 -05:00
|
|
|
export class DirectiveNormalizer {
|
2016-08-17 12:24:44 -04:00
|
|
|
private _resourceLoaderCache = new Map<string, Promise<string>>();
|
2016-06-24 11:46:43 -04:00
|
|
|
|
2016-06-08 19:38:52 -04:00
|
|
|
constructor(
|
2016-08-17 12:24:44 -04:00
|
|
|
private _resourceLoader: ResourceLoader, private _urlResolver: UrlResolver,
|
|
|
|
private _htmlParser: HtmlParser, private _config: CompilerConfig) {}
|
2015-08-25 18:36:02 -04:00
|
|
|
|
2016-12-11 05:43:51 -05:00
|
|
|
clearCache(): void { this._resourceLoaderCache.clear(); }
|
2016-06-24 11:46:43 -04:00
|
|
|
|
2016-12-11 05:43:51 -05:00
|
|
|
clearCacheFor(normalizedDirective: CompileDirectiveMetadata): void {
|
2016-06-24 11:46:43 -04:00
|
|
|
if (!normalizedDirective.isComponent) {
|
|
|
|
return;
|
|
|
|
}
|
2016-08-17 12:24:44 -04:00
|
|
|
this._resourceLoaderCache.delete(normalizedDirective.template.templateUrl);
|
2016-06-24 11:46:43 -04:00
|
|
|
normalizedDirective.template.externalStylesheets.forEach(
|
2016-08-17 12:24:44 -04:00
|
|
|
(stylesheet) => { this._resourceLoaderCache.delete(stylesheet.moduleUrl); });
|
2016-06-24 11:46:43 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
private _fetch(url: string): Promise<string> {
|
2016-11-12 08:08:58 -05:00
|
|
|
let result = this._resourceLoaderCache.get(url);
|
2016-06-24 11:46:43 -04:00
|
|
|
if (!result) {
|
2016-08-17 12:24:44 -04:00
|
|
|
result = this._resourceLoader.get(url);
|
|
|
|
this._resourceLoaderCache.set(url, result);
|
2016-06-24 11:46:43 -04:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2016-11-10 17:07:30 -05:00
|
|
|
normalizeTemplate(prenormData: PrenormalizedTemplateMetadata):
|
|
|
|
SyncAsyncResult<CompileTemplateMetadata> {
|
2016-06-24 11:46:43 -04:00
|
|
|
let normalizedTemplateSync: CompileTemplateMetadata = null;
|
|
|
|
let normalizedTemplateAsync: Promise<CompileTemplateMetadata>;
|
2016-12-11 05:49:03 -05:00
|
|
|
if (prenormData.template != null) {
|
|
|
|
if (typeof prenormData.template !== 'string') {
|
|
|
|
throw new SyntaxError(
|
|
|
|
`The template specified for component ${stringify(prenormData.componentType)} is not a string`);
|
|
|
|
}
|
2016-11-10 17:07:30 -05:00
|
|
|
normalizedTemplateSync = this.normalizeTemplateSync(prenormData);
|
2016-06-24 11:46:43 -04:00
|
|
|
normalizedTemplateAsync = Promise.resolve(normalizedTemplateSync);
|
2016-11-10 17:07:30 -05:00
|
|
|
} else if (prenormData.templateUrl) {
|
2016-12-11 05:49:03 -05:00
|
|
|
if (typeof prenormData.templateUrl !== 'string') {
|
|
|
|
throw new SyntaxError(
|
|
|
|
`The templateUrl specified for component ${stringify(prenormData.componentType)} is not a string`);
|
|
|
|
}
|
2016-11-10 17:07:30 -05:00
|
|
|
normalizedTemplateAsync = this.normalizeTemplateAsync(prenormData);
|
2016-06-24 11:46:43 -04:00
|
|
|
} else {
|
2016-12-15 16:07:12 -05:00
|
|
|
throw new SyntaxError(
|
2016-11-10 17:07:30 -05:00
|
|
|
`No template specified for component ${stringify(prenormData.componentType)}`);
|
2016-06-24 11:46:43 -04:00
|
|
|
}
|
2016-11-10 17:07:30 -05:00
|
|
|
|
2016-06-24 11:46:43 -04:00
|
|
|
if (normalizedTemplateSync && normalizedTemplateSync.styleUrls.length === 0) {
|
|
|
|
// sync case
|
2016-11-10 17:07:30 -05:00
|
|
|
return new SyncAsyncResult(normalizedTemplateSync);
|
2016-06-24 11:46:43 -04:00
|
|
|
} else {
|
|
|
|
// async case
|
2016-06-28 12:54:42 -04:00
|
|
|
return new SyncAsyncResult(
|
2016-11-10 17:07:30 -05:00
|
|
|
null, normalizedTemplateAsync.then(
|
|
|
|
(normalizedTemplate) => this.normalizeExternalStylesheets(normalizedTemplate)));
|
2016-06-24 11:46:43 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-10 17:07:30 -05:00
|
|
|
normalizeTemplateSync(prenomData: PrenormalizedTemplateMetadata): CompileTemplateMetadata {
|
|
|
|
return this.normalizeLoadedTemplate(prenomData, prenomData.template, prenomData.moduleUrl);
|
2016-01-06 17:13:44 -05:00
|
|
|
}
|
|
|
|
|
2016-11-10 17:07:30 -05:00
|
|
|
normalizeTemplateAsync(prenomData: PrenormalizedTemplateMetadata):
|
2016-06-08 19:38:52 -04:00
|
|
|
Promise<CompileTemplateMetadata> {
|
2016-11-12 08:08:58 -05:00
|
|
|
const templateUrl = this._urlResolver.resolve(prenomData.moduleUrl, prenomData.templateUrl);
|
2016-06-24 11:46:43 -04:00
|
|
|
return this._fetch(templateUrl)
|
2016-11-10 17:07:30 -05:00
|
|
|
.then((value) => this.normalizeLoadedTemplate(prenomData, value, templateUrl));
|
2015-08-25 18:36:02 -04:00
|
|
|
}
|
|
|
|
|
2016-06-08 19:38:52 -04:00
|
|
|
normalizeLoadedTemplate(
|
2016-11-10 17:07:30 -05:00
|
|
|
prenomData: PrenormalizedTemplateMetadata, template: string,
|
2016-06-08 19:38:52 -04:00
|
|
|
templateAbsUrl: string): CompileTemplateMetadata {
|
2016-11-10 17:07:30 -05:00
|
|
|
const interpolationConfig = InterpolationConfig.fromArray(prenomData.interpolation);
|
|
|
|
const rootNodesAndErrors = this._htmlParser.parse(
|
2016-12-22 13:17:41 -05:00
|
|
|
template, stringify(prenomData.componentType), true, interpolationConfig);
|
2015-10-07 12:34:21 -04:00
|
|
|
if (rootNodesAndErrors.errors.length > 0) {
|
2016-07-09 13:12:39 -04:00
|
|
|
const errorString = rootNodesAndErrors.errors.join('\n');
|
2016-12-15 16:07:12 -05:00
|
|
|
throw new SyntaxError(`Template parse errors:\n${errorString}`);
|
2015-10-07 12:34:21 -04:00
|
|
|
}
|
2016-12-22 13:17:41 -05:00
|
|
|
|
2016-07-09 13:12:39 -04:00
|
|
|
const templateMetadataStyles = this.normalizeStylesheet(new CompileStylesheetMetadata({
|
2016-11-10 17:07:30 -05:00
|
|
|
styles: prenomData.styles,
|
|
|
|
styleUrls: prenomData.styleUrls,
|
|
|
|
moduleUrl: prenomData.moduleUrl
|
2016-06-24 11:46:43 -04:00
|
|
|
}));
|
2015-10-07 12:34:21 -04:00
|
|
|
|
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(
|
2016-06-24 11:46:43 -04:00
|
|
|
{styles: visitor.styles, styleUrls: visitor.styleUrls, moduleUrl: templateAbsUrl}));
|
2015-09-14 18:59:09 -04:00
|
|
|
|
2016-11-10 17:07:30 -05:00
|
|
|
let encapsulation = prenomData.encapsulation;
|
2016-12-11 05:43:51 -05:00
|
|
|
if (encapsulation == null) {
|
2016-04-02 19:34:44 -04:00
|
|
|
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) {
|
2015-09-18 13:33:23 -04:00
|
|
|
encapsulation = ViewEncapsulation.None;
|
|
|
|
}
|
2016-09-20 17:14:57 -04:00
|
|
|
|
2015-09-18 13:33:23 -04:00
|
|
|
return new CompileTemplateMetadata({
|
2016-06-22 20:25:42 -04:00
|
|
|
encapsulation,
|
2016-09-20 17:14:57 -04:00
|
|
|
template,
|
|
|
|
templateUrl: templateAbsUrl, styles, styleUrls,
|
2016-05-25 15:46:22 -04:00
|
|
|
ngContentSelectors: visitor.ngContentSelectors,
|
2016-11-10 17:07:30 -05:00
|
|
|
animations: prenomData.animations,
|
|
|
|
interpolation: prenomData.interpolation,
|
2015-08-25 18:36:02 -04:00
|
|
|
});
|
|
|
|
}
|
2016-06-24 11:46:43 -04:00
|
|
|
|
|
|
|
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) => {
|
2016-11-12 08:08:58 -05:00
|
|
|
const stylesheet = this.normalizeStylesheet(
|
2016-06-24 11:46:43 -04:00
|
|
|
new CompileStylesheetMetadata({styles: [loadedStyle], moduleUrl: styleUrl}));
|
|
|
|
loadedStylesheets.set(styleUrl, stylesheet);
|
|
|
|
return this._loadMissingExternalStylesheets(
|
|
|
|
stylesheet.styleUrls, loadedStylesheets);
|
|
|
|
})))
|
2016-11-03 19:58:27 -04:00
|
|
|
.then((_) => Array.from(loadedStylesheets.values()));
|
2016-06-24 11:46:43 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
normalizeStylesheet(stylesheet: CompileStylesheetMetadata): CompileStylesheetMetadata {
|
2016-11-12 08:08:58 -05:00
|
|
|
const allStyleUrls = stylesheet.styleUrls.filter(isStyleUrlResolvable)
|
|
|
|
.map(url => this._urlResolver.resolve(stylesheet.moduleUrl, url));
|
2016-06-24 11:46:43 -04:00
|
|
|
|
2016-11-12 08:08:58 -05:00
|
|
|
const allStyles = stylesheet.styles.map(style => {
|
|
|
|
const styleWithImports = extractStyleUrls(this._urlResolver, stylesheet.moduleUrl, style);
|
2016-06-24 11:46:43 -04:00
|
|
|
allStyleUrls.push(...styleWithImports.styleUrls);
|
|
|
|
return styleWithImports.style;
|
|
|
|
});
|
|
|
|
|
|
|
|
return new CompileStylesheetMetadata(
|
|
|
|
{styles: allStyles, styleUrls: allStyleUrls, moduleUrl: stylesheet.moduleUrl});
|
|
|
|
}
|
2015-08-25 18:36:02 -04:00
|
|
|
}
|
|
|
|
|
2016-07-21 16:56:58 -04:00
|
|
|
class TemplatePreparseVisitor implements html.Visitor {
|
2015-08-25 18:36:02 -04:00
|
|
|
ngContentSelectors: string[] = [];
|
|
|
|
styles: string[] = [];
|
|
|
|
styleUrls: string[] = [];
|
2015-09-18 13:33:23 -04:00
|
|
|
ngNonBindableStackCount: number = 0;
|
2015-08-25 18:36:02 -04:00
|
|
|
|
2016-07-21 16:56:58 -04:00
|
|
|
visitElement(ast: html.Element, context: any): any {
|
2016-11-12 08:08:58 -05:00
|
|
|
const preparsedElement = preparseElement(ast);
|
2015-09-18 13:33:23 -04:00
|
|
|
switch (preparsedElement.type) {
|
|
|
|
case PreparsedElementType.NG_CONTENT:
|
2015-09-18 13:33:23 -04:00
|
|
|
if (this.ngNonBindableStackCount === 0) {
|
|
|
|
this.ngContentSelectors.push(preparsedElement.selectAttr);
|
|
|
|
}
|
2015-09-18 13:33:23 -04:00
|
|
|
break;
|
|
|
|
case PreparsedElementType.STYLE:
|
2016-11-12 08:08:58 -05:00
|
|
|
let textContent = '';
|
2015-09-18 13:33:23 -04:00
|
|
|
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;
|
2015-09-18 13:33:23 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
this.styles.push(textContent);
|
|
|
|
break;
|
|
|
|
case PreparsedElementType.STYLESHEET:
|
|
|
|
this.styleUrls.push(preparsedElement.hrefAttr);
|
|
|
|
break;
|
2016-01-25 20:57:29 -05:00
|
|
|
default:
|
|
|
|
break;
|
2015-08-25 18:36:02 -04:00
|
|
|
}
|
2015-09-18 13:33:23 -04:00
|
|
|
if (preparsedElement.nonBindable) {
|
|
|
|
this.ngNonBindableStackCount++;
|
|
|
|
}
|
2016-07-21 16:56:58 -04:00
|
|
|
html.visitAll(this, ast.children);
|
2015-09-18 13:33:23 -04:00
|
|
|
if (preparsedElement.nonBindable) {
|
|
|
|
this.ngNonBindableStackCount--;
|
2015-08-25 18:36:02 -04:00
|
|
|
}
|
2015-09-18 13:33:23 -04:00
|
|
|
return null;
|
2015-08-25 18:36:02 -04:00
|
|
|
}
|
2016-08-12 00:00:35 -04:00
|
|
|
|
2016-12-22 13:17:41 -05:00
|
|
|
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; }
|
2015-08-25 18:36:02 -04:00
|
|
|
}
|