perf(core): add option to remove blank text nodes from compiled templates

This commit is contained in:
Pawel Kozlowski 2017-07-28 15:58:28 +02:00 committed by Hans
parent 088532bf2e
commit d2c0d986d4
27 changed files with 450 additions and 46 deletions

View File

@ -111,6 +111,10 @@ export interface CompilerOptions extends ts.CompilerOptions {
i18nInFile?: string;
// How to handle missing messages
i18nInMissingTranslations?: 'error'|'warning'|'ignore';
// Whether to remove blank text nodes from compiled templates. It is `true` by default
// in Angular 5 and will be re-visited in Angular 6.
preserveWhitespaces?: boolean;
}
export interface ModuleFilenameResolver {

View File

@ -347,6 +347,7 @@ function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions {
i18nFormat: options.i18nInFormat || options.i18nOutFormat, translations, missingTranslation,
enableLegacyTemplate: options.enableLegacyTemplate,
enableSummariesForJit: true,
preserveWhitespaces: options.preserveWhitespaces,
};
}
@ -480,4 +481,4 @@ function createProgramWithStubsHost(
fileExists = (fileName: string) =>
this.generatedFiles.has(fileName) || originalHost.fileExists(fileName);
};
}
}

View File

@ -322,9 +322,10 @@ export class AotCompiler {
const pipes = ngModule.transitiveModule.pipes.map(
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
compMeta, compMeta.template !.template !, directives, pipes, ngModule.schemas,
templateSourceUrl(ngModule.type, compMeta, compMeta.template !));
templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces);
const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]);
const viewResult = this._viewCompiler.compileComponent(
outputCtx, compMeta, parsedTemplate, stylesExpr, usedPipes);

View File

@ -54,6 +54,7 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom
useJit: false,
enableLegacyTemplate: options.enableLegacyTemplate !== false,
missingTranslation: options.missingTranslation,
preserveWhitespaces: options.preserveWhitespaces,
});
const normalizer = new DirectiveNormalizer(
{get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config);

View File

@ -15,4 +15,5 @@ export interface AotCompilerOptions {
missingTranslation?: MissingTranslationStrategy;
enableLegacyTemplate?: boolean;
enableSummariesForJit?: boolean;
preserveWhitespaces?: boolean;
}

View File

@ -252,8 +252,9 @@ export class CompileTemplateMetadata {
animations: any[];
ngContentSelectors: string[];
interpolation: [string, string]|null;
preserveWhitespaces: boolean;
constructor({encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets,
animations, ngContentSelectors, interpolation, isInline}: {
animations, ngContentSelectors, interpolation, isInline, preserveWhitespaces}: {
encapsulation: ViewEncapsulation | null,
template: string|null,
templateUrl: string|null,
@ -263,7 +264,8 @@ export class CompileTemplateMetadata {
ngContentSelectors: string[],
animations: any[],
interpolation: [string, string]|null,
isInline: boolean
isInline: boolean,
preserveWhitespaces: boolean
}) {
this.encapsulation = encapsulation;
this.template = template;
@ -278,6 +280,7 @@ export class CompileTemplateMetadata {
}
this.interpolation = interpolation;
this.isInline = isInline;
this.preserveWhitespaces = preserveWhitespaces;
}
toSummary(): CompileTemplateSummary {
@ -516,7 +519,8 @@ export function createHostComponentMeta(
animations: [],
isInline: true,
externalStylesheets: [],
interpolation: null
interpolation: null,
preserveWhitespaces: false,
}),
exportAs: null,
changeDetection: ChangeDetectionStrategy.Default,

View File

@ -24,7 +24,7 @@
export {VERSION} from './version';
export * from './template_parser/template_ast';
export {TEMPLATE_TRANSFORMS} from './template_parser/template_parser';
export {CompilerConfig} from './config';
export {CompilerConfig, preserveWhitespacesDefault} from './config';
export * from './compile_metadata';
export * from './aot/compiler_factory';
export * from './aot/compiler';

View File

@ -6,11 +6,8 @@
* 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} from './identifiers';
import {MissingTranslationStrategy, ViewEncapsulation} from '@angular/core';
import {noUndefined} from './util';
export class CompilerConfig {
public defaultEncapsulation: ViewEncapsulation|null;
@ -19,18 +16,26 @@ export class CompilerConfig {
public enableLegacyTemplate: boolean;
public useJit: boolean;
public missingTranslation: MissingTranslationStrategy|null;
public preserveWhitespaces: boolean;
constructor(
{defaultEncapsulation = ViewEncapsulation.Emulated, useJit = true, missingTranslation,
enableLegacyTemplate}: {
enableLegacyTemplate, preserveWhitespaces}: {
defaultEncapsulation?: ViewEncapsulation,
useJit?: boolean,
missingTranslation?: MissingTranslationStrategy,
enableLegacyTemplate?: boolean,
preserveWhitespaces?: boolean
} = {}) {
this.defaultEncapsulation = defaultEncapsulation;
this.useJit = !!useJit;
this.missingTranslation = missingTranslation || null;
this.enableLegacyTemplate = enableLegacyTemplate !== false;
this.preserveWhitespaces = preserveWhitespacesDefault(noUndefined(preserveWhitespaces));
}
}
export function preserveWhitespacesDefault(
preserveWhitespacesOption: boolean | null, defaultSetting = true): boolean {
return preserveWhitespacesOption === null ? defaultSetting : preserveWhitespacesOption;
}

View File

@ -9,7 +9,7 @@
import {ViewEncapsulation, ɵstringify as stringify} from '@angular/core';
import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, templateSourceUrl} from './compile_metadata';
import {CompilerConfig} from './config';
import {CompilerConfig, preserveWhitespacesDefault} from './config';
import {CompilerInjectable} from './injectable';
import * as html from './ml_parser/ast';
import {HtmlParser} from './ml_parser/html_parser';
@ -31,6 +31,7 @@ export interface PrenormalizedTemplateMetadata {
interpolation: [string, string]|null;
encapsulation: ViewEncapsulation|null;
animations: CompileAnimationEntryMetadata[];
preserveWhitespaces: boolean|null;
}
@CompilerInjectable()
@ -82,6 +83,13 @@ export class DirectiveNormalizer {
throw syntaxError(
`No template specified for component ${stringify(prenormData.componentType)}`);
}
if (isDefined(prenormData.preserveWhitespaces) &&
typeof prenormData.preserveWhitespaces !== 'boolean') {
throw syntaxError(
`The preserveWhitespaces option for component ${stringify(prenormData.componentType)} must be a boolean`);
}
return SyncAsync.then(
this.normalizeTemplateOnly(prenormData),
(result: CompileTemplateMetadata) => this.normalizeExternalStylesheets(result));
@ -149,7 +157,9 @@ export class DirectiveNormalizer {
ngContentSelectors: visitor.ngContentSelectors,
animations: prenormData.animations,
interpolation: prenormData.interpolation, isInline,
externalStylesheets: []
externalStylesheets: [],
preserveWhitespaces: preserveWhitespacesDefault(
prenormData.preserveWhitespaces, this._config.preserveWhitespaces),
});
}
@ -168,6 +178,7 @@ export class DirectiveNormalizer {
animations: templateMeta.animations,
interpolation: templateMeta.interpolation,
isInline: templateMeta.isInline,
preserveWhitespaces: templateMeta.preserveWhitespaces,
}));
}

View File

@ -152,7 +152,8 @@ export class DirectiveResolver {
styleUrls: directive.styleUrls,
encapsulation: directive.encapsulation,
animations: directive.animations,
interpolation: directive.interpolation
interpolation: directive.interpolation,
preserveWhitespaces: directive.preserveWhitespaces,
});
} else {
return new Directive({

View File

@ -262,6 +262,7 @@ export class JitCompiler implements Compiler {
const externalStylesheetsByModuleUrl = new Map<string, CompiledStylesheet>();
const outputContext = createOutputContext();
const componentStylesheet = this._styleCompiler.compileComponent(outputContext, compMeta);
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => {
const compiledStylesheet =
this._styleCompiler.compileStyles(createOutputContext(), compMeta, stylesheetMeta);
@ -274,7 +275,8 @@ export class JitCompiler implements Compiler {
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
compMeta, compMeta.template !.template !, directives, pipes, template.ngModule.schemas,
templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !));
templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !),
preserveWhitespaces);
const compileResult = this._viewCompiler.compileComponent(
outputContext, compMeta, parsedTemplate, ir.variable(componentStylesheet.stylesVar),
usedPipes);

View File

@ -123,6 +123,7 @@ export class JitCompilerFactory implements CompilerFactory {
defaultEncapsulation: ViewEncapsulation.Emulated,
missingTranslation: MissingTranslationStrategy.Warning,
enableLegacyTemplate: true,
preserveWhitespaces: true,
};
this._defaultOptions = [compilerOptions, ...defaultOptions];
@ -142,6 +143,7 @@ export class JitCompilerFactory implements CompilerFactory {
defaultEncapsulation: opts.defaultEncapsulation,
missingTranslation: opts.missingTranslation,
enableLegacyTemplate: opts.enableLegacyTemplate,
preserveWhitespaces: opts.preserveWhitespaces,
});
},
deps: []
@ -169,6 +171,7 @@ function _mergeOptions(optionsArr: CompilerOptions[]): CompilerOptions {
providers: _mergeArrays(optionsArr.map(options => options.providers !)),
missingTranslation: _lastDefined(optionsArr.map(options => options.missingTranslation)),
enableLegacyTemplate: _lastDefined(optionsArr.map(options => options.enableLegacyTemplate)),
preserveWhitespaces: _lastDefined(optionsArr.map(options => options.preserveWhitespaces)),
};
}

View File

@ -219,7 +219,8 @@ export class CompileMetadataResolver {
styles: template.styles,
styleUrls: template.styleUrls,
animations: template.animations,
interpolation: template.interpolation
interpolation: template.interpolation,
preserveWhitespaces: template.preserveWhitespaces
});
if (isPromise(templateMeta) && isSync) {
this._reportError(componentStillLoadingError(directiveType), directiveType);
@ -267,7 +268,8 @@ export class CompileMetadataResolver {
interpolation: noUndefined(dirMeta.interpolation),
isInline: !!dirMeta.template,
externalStylesheets: [],
ngContentSelectors: []
ngContentSelectors: [],
preserveWhitespaces: noUndefined(dirMeta.preserveWhitespaces),
});
}

View File

@ -0,0 +1,83 @@
/**
* @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 './ast';
import {ParseTreeResult} from './parser';
import {NGSP_UNICODE} from './tags';
export const PRESERVE_WS_ATTR_NAME = 'ngPreserveWhitespaces';
const SKIP_WS_TRIM_TAGS = new Set(['pre', 'template', 'textarea', 'script', 'style']);
function hasPreserveWhitespacesAttr(attrs: html.Attribute[]): boolean {
return attrs.some((attr: html.Attribute) => attr.name === PRESERVE_WS_ATTR_NAME);
}
/**
* This visitor can walk HTML parse tree and remove / trim text nodes using the following rules:
* - consider spaces, tabs and new lines as whitespace characters;
* - drop text nodes consisting of whitespace characters only;
* - for all other text nodes replace consecutive whitespace characters with one space;
* - convert &ngsp; pseudo-entity to a single space;
*
* The idea of using &ngsp; as a placeholder for non-removable space was originally introduced in
* Angular Dart, see:
* https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart#L25-L32
* In Angular Dart &ngsp; is converted to the 0xE500 PUA (Private Use Areas) unicode character
* and later on replaced by a space. We are re-implementing the same idea here.
*
* Removal and trimming of whitespaces have positive performance impact (less code to generate
* while compiling templates, faster view creation). At the same time it can be "destructive"
* in some cases (whitespaces can influence layout). Becouse of the potential of breaking layout
* this visitor is not activated by default in Angular 5 and people need to explicitly opt-in for
* whitespace removal. The default option for whitespace removal will be revisited in Angular 6
* and might be changed to "on" by default.
*/
class WhitespaceVisitor implements html.Visitor {
visitElement(element: html.Element, context: any): any {
if (SKIP_WS_TRIM_TAGS.has(element.name) || hasPreserveWhitespacesAttr(element.attrs)) {
// don't descent into elements where we need to preserve whitespaces
// but still visit all attributes to eliminate one used as a market to preserve WS
return new html.Element(
element.name, html.visitAll(this, element.attrs), element.children, element.sourceSpan,
element.startSourceSpan, element.endSourceSpan);
}
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.name !== PRESERVE_WS_ATTR_NAME ? attribute : null;
}
visitText(text: html.Text, context: any): any {
const isBlank = text.value.trim().length === 0;
if (!isBlank) {
// lexer is replacing the &ngsp; pseudo-entity with NGSP_UNICODE
return new html.Text(
text.value.replace(NGSP_UNICODE, ' ').replace(/\s\s+/g, ' '), text.sourceSpan);
}
return null;
}
visitComment(comment: html.Comment, context: any): any { return comment; }
visitExpansion(expansion: html.Expansion, context: any): any { return expansion; }
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
}
export function removeWhitespaces(htmlAstWithErrors: ParseTreeResult): ParseTreeResult {
return new ParseTreeResult(
html.visitAll(new WhitespaceVisitor(), htmlAstWithErrors.rootNodes),
htmlAstWithErrors.errors);
}

View File

@ -71,6 +71,7 @@ export function mergeNsAndName(prefix: string, localName: string): string {
// This list is not exhaustive to keep the compiler footprint low.
// The `&#123;` / `&#x1ab;` syntax should be used when the named character reference does not
// exist.
export const NAMED_ENTITIES: {[k: string]: string} = {
'Aacute': '\u00C1',
'aacute': '\u00E1',
@ -325,3 +326,9 @@ export const NAMED_ENTITIES: {[k: string]: string} = {
'zwj': '\u200D',
'zwnj': '\u200C',
};
// The &ngsp; pseudo-entity is denoting a space. see:
// https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart
export const NGSP_UNICODE = '\uE500';
NAMED_ENTITIES['ngsp'] = NGSP_UNICODE;

View File

@ -18,6 +18,7 @@ import {Identifiers, createTokenForExternalReference, createTokenForReference} f
import {CompilerInjectable} from '../injectable';
import * as html from '../ml_parser/ast';
import {ParseTreeResult} from '../ml_parser/html_parser';
import {removeWhitespaces} from '../ml_parser/html_whitespaces';
import {expandNodes} from '../ml_parser/icu_ast_expander';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {isNgTemplate, splitNsName} from '../ml_parser/tags';
@ -113,9 +114,10 @@ export class TemplateParser {
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);
pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string,
preserveWhitespaces: boolean): {template: TemplateAst[], pipes: CompilePipeSummary[]} {
const result = this.tryParse(
component, template, directives, pipes, schemas, templateUrl, preserveWhitespaces);
const warnings =
result.errors !.filter(error => error.level === ParseErrorLevel.WARNING)
.filter(warnOnlyOnce(
@ -137,12 +139,17 @@ export class TemplateParser {
tryParse(
component: CompileDirectiveMetadata, template: string, directives: CompileDirectiveSummary[],
pipes: CompilePipeSummary[], schemas: SchemaMetadata[],
templateUrl: string): TemplateParseResult {
pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string,
preserveWhitespaces: boolean): TemplateParseResult {
let htmlParseResult = this._htmlParser !.parse(
template, templateUrl, true, this.getInterpolationConfig(component));
if (!preserveWhitespaces) {
htmlParseResult = removeWhitespaces(htmlParseResult);
}
return this.tryParseHtml(
this.expandHtml(this._htmlParser !.parse(
template, templateUrl, true, this.getInterpolationConfig(component))),
component, directives, pipes, schemas);
this.expandHtml(htmlParseResult), component, directives, pipes, schemas);
}
tryParseHtml(

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CompileAnimationEntryMetadata} from '@angular/compiler';
import {CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, CompileTypeMetadata} from '@angular/compiler/src/compile_metadata';
import {CompilerConfig} from '@angular/compiler/src/config';
import {CompileStylesheetMetadata, CompileTemplateMetadata} from '@angular/compiler/src/compile_metadata';
import {CompilerConfig, preserveWhitespacesDefault} from '@angular/compiler/src/config';
import {DirectiveNormalizer} from '@angular/compiler/src/directive_normalizer';
import {ResourceLoader} from '@angular/compiler/src/resource_loader';
import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock';
@ -31,6 +31,7 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: {
interpolation?: [string, string] | null;
encapsulation?: ViewEncapsulation | null;
animations?: CompileAnimationEntryMetadata[];
preserveWhitespaces?: boolean | null;
}) {
return normalizer.normalizeTemplate({
ngModuleType: noUndefined(o.ngModuleType),
@ -42,7 +43,8 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: {
styleUrls: noUndefined(o.styleUrls),
interpolation: noUndefined(o.interpolation),
encapsulation: noUndefined(o.encapsulation),
animations: noUndefined(o.animations)
animations: noUndefined(o.animations),
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
});
}
@ -54,6 +56,7 @@ function normalizeTemplateOnly(normalizer: DirectiveNormalizer, o: {
interpolation?: [string, string] | null;
encapsulation?: ViewEncapsulation | null;
animations?: CompileAnimationEntryMetadata[];
preserveWhitespaces?: boolean | null;
}) {
return normalizer.normalizeTemplateOnly({
ngModuleType: noUndefined(o.ngModuleType),
@ -65,13 +68,14 @@ function normalizeTemplateOnly(normalizer: DirectiveNormalizer, o: {
styleUrls: noUndefined(o.styleUrls),
interpolation: noUndefined(o.interpolation),
encapsulation: noUndefined(o.encapsulation),
animations: noUndefined(o.animations)
animations: noUndefined(o.animations),
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
});
}
function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls,
externalStylesheets, animations, ngContentSelectors,
interpolation, isInline}: {
interpolation, isInline, preserveWhitespaces}: {
encapsulation?: ViewEncapsulation | null,
template?: string | null,
templateUrl?: string | null,
@ -81,7 +85,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
ngContentSelectors?: string[],
animations?: any[],
interpolation?: [string, string] | null,
isInline?: boolean
isInline?: boolean,
preserveWhitespaces?: boolean | null
}): CompileTemplateMetadata {
return new CompileTemplateMetadata({
encapsulation: encapsulation || null,
@ -94,6 +99,7 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
animations: animations || [],
interpolation: interpolation || null,
isInline: !!isInline,
preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)),
});
}
@ -106,6 +112,7 @@ function normalizeLoadedTemplate(
interpolation?: [string, string] | null;
encapsulation?: ViewEncapsulation | null;
animations?: CompileAnimationEntryMetadata[];
preserveWhitespaces?: boolean;
},
template: string, templateAbsUrl: string) {
return normalizer.normalizeLoadedTemplate(
@ -120,6 +127,7 @@ function normalizeLoadedTemplate(
interpolation: o.interpolation || null,
encapsulation: o.encapsulation || null,
animations: o.animations || [],
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
},
template, templateAbsUrl);
}
@ -169,6 +177,18 @@ export function main() {
}))
.toThrowError(`'SomeComp' component cannot define both template and templateUrl`);
}));
it('should throw if preserveWhitespaces is not a boolean',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
expect(() => normalizeTemplate(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
template: '',
preserveWhitespaces: <any>'WRONG',
}))
.toThrowError(
'The preserveWhitespaces option for component SomeComp must be a boolean');
}));
});
describe('normalizeTemplateOnly sync', () => {
@ -431,6 +451,28 @@ export function main() {
expect(template.encapsulation).toBe(viewEncapsulation);
}));
it('should use preserveWhitespaces setting from compiler config if none provided',
inject(
[DirectiveNormalizer, CompilerConfig],
(normalizer: DirectiveNormalizer, config: CompilerConfig) => {
const template = normalizeLoadedTemplate(normalizer, {}, '', '');
expect(template.preserveWhitespaces).toBe(config.preserveWhitespaces);
}));
it('should store the preserveWhitespaces=false in the result',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template =
normalizeLoadedTemplate(normalizer, {preserveWhitespaces: false}, '', '');
expect(template.preserveWhitespaces).toBe(false);
}));
it('should store the preserveWhitespaces=true in the result',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template =
normalizeLoadedTemplate(normalizer, {preserveWhitespaces: true}, '', '');
expect(template.preserveWhitespaces).toBe(true);
}));
it('should keep the template as html',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(

View File

@ -79,7 +79,12 @@ class SomeDirectiveWithViewChild {
c: any;
}
@Component({selector: 'sample', template: 'some template', styles: ['some styles']})
@Component({
selector: 'sample',
template: 'some template',
styles: ['some styles'],
preserveWhitespaces: true
})
class ComponentWithTemplate {
}
@ -439,6 +444,7 @@ export function main() {
const compMetadata: Component = resolver.resolve(ComponentWithTemplate);
expect(compMetadata.template).toEqual('some template');
expect(compMetadata.styles).toEqual(['some styles']);
expect(compMetadata.preserveWhitespaces).toBe(true);
});
});
});

View File

@ -9,7 +9,6 @@
import {Component, Directive, Input} from '@angular/core';
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers';

View File

@ -97,4 +97,4 @@ const serializerVisitor = new _SerializerVisitor();
export function serializeNodes(nodes: html.Node[]): string[] {
return nodes.map(node => node.visit(serializerVisitor, null));
}
}

View File

@ -0,0 +1,118 @@
/**
* @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 '../../src/ml_parser/ast';
import {HtmlParser} from '../../src/ml_parser/html_parser';
import {PRESERVE_WS_ATTR_NAME, removeWhitespaces} from '../../src/ml_parser/html_whitespaces';
import {humanizeDom} from './ast_spec_utils';
export function main() {
describe('removeWhitespaces', () => {
function parseAndRemoveWS(template: string): any[] {
return humanizeDom(removeWhitespaces(new HtmlParser().parse(template, 'TestComp')));
}
it('should remove blank text nodes', () => {
expect(parseAndRemoveWS(' ')).toEqual([]);
expect(parseAndRemoveWS('\n')).toEqual([]);
expect(parseAndRemoveWS('\t')).toEqual([]);
expect(parseAndRemoveWS(' \t \n ')).toEqual([]);
});
it('should remove whitespaces (space, tab, new line) between elements', () => {
expect(parseAndRemoveWS('<br> <br>\t<br>\n<br>')).toEqual([
[html.Element, 'br', 0],
[html.Element, 'br', 0],
[html.Element, 'br', 0],
[html.Element, 'br', 0],
]);
});
it('should remove whitespaces from child text nodes', () => {
expect(parseAndRemoveWS('<div><span> </span></div>')).toEqual([
[html.Element, 'div', 0],
[html.Element, 'span', 1],
]);
});
it('should remove whitespaces from the beginning and end of a template', () => {
expect(parseAndRemoveWS(` <br>\t`)).toEqual([
[html.Element, 'br', 0],
]);
});
it('should convert &ngsp; to a space and preserve it', () => {
expect(parseAndRemoveWS('<div><span>foo</span>&ngsp;<span>bar</span></div>')).toEqual([
[html.Element, 'div', 0],
[html.Element, 'span', 1],
[html.Text, 'foo', 2],
[html.Text, ' ', 1],
[html.Element, 'span', 1],
[html.Text, 'bar', 2],
]);
});
it('should replace multiple whitespaces with one space', () => {
expect(parseAndRemoveWS('\n\n\nfoo\t\t\t')).toEqual([[html.Text, ' foo ', 0]]);
expect(parseAndRemoveWS(' \n foo \t ')).toEqual([[html.Text, ' foo ', 0]]);
});
it('should not replace single tab and newline with spaces', () => {
expect(parseAndRemoveWS('\nfoo')).toEqual([[html.Text, '\nfoo', 0]]);
expect(parseAndRemoveWS('\tfoo')).toEqual([[html.Text, '\tfoo', 0]]);
});
it('should preserve single whitespaces between interpolations', () => {
expect(parseAndRemoveWS(`{{fooExp}} {{barExp}}`)).toEqual([
[html.Text, '{{fooExp}} {{barExp}}', 0],
]);
expect(parseAndRemoveWS(`{{fooExp}}\t{{barExp}}`)).toEqual([
[html.Text, '{{fooExp}}\t{{barExp}}', 0],
]);
expect(parseAndRemoveWS(`{{fooExp}}\n{{barExp}}`)).toEqual([
[html.Text, '{{fooExp}}\n{{barExp}}', 0],
]);
});
it('should preserve whitespaces around interpolations', () => {
expect(parseAndRemoveWS(` {{exp}} `)).toEqual([
[html.Text, ' {{exp}} ', 0],
]);
});
it('should preserve whitespaces inside <pre> elements', () => {
expect(parseAndRemoveWS(`<pre><strong>foo</strong>\n<strong>bar</strong></pre>`)).toEqual([
[html.Element, 'pre', 0],
[html.Element, 'strong', 1],
[html.Text, 'foo', 2],
[html.Text, '\n', 1],
[html.Element, 'strong', 1],
[html.Text, 'bar', 2],
]);
});
it('should skip whitespace trimming in <textarea>', () => {
expect(parseAndRemoveWS(`<textarea>foo\n\n bar</textarea>`)).toEqual([
[html.Element, 'textarea', 0],
[html.Text, 'foo\n\n bar', 1],
]);
});
it(`should preserve whitespaces inside elements annotated with ${PRESERVE_WS_ATTR_NAME}`,
() => {
expect(parseAndRemoveWS(`<div ${PRESERVE_WS_ATTR_NAME}><img> <img></div>`)).toEqual([
[html.Element, 'div', 0],
[html.Element, 'img', 1],
[html.Text, ' ', 1],
[html.Element, 'img', 1],
]);
});
});
}

View File

@ -5,7 +5,7 @@
* 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 {CompileQueryMetadata, CompilerConfig, JitReflector, ProxyClass, StaticSymbol} from '@angular/compiler';
import {CompileQueryMetadata, CompilerConfig, JitReflector, ProxyClass, StaticSymbol, preserveWhitespacesDefault} from '@angular/compiler';
import {CompileAnimationEntryMetadata, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata, tokenReference} from '@angular/compiler/src/compile_metadata';
import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry';
import {ElementSchemaRegistry} from '@angular/compiler/src/schema/element_schema_registry';
@ -84,7 +84,7 @@ function compileDirectiveMetadataCreate(
function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls,
externalStylesheets, animations, ngContentSelectors,
interpolation, isInline}: {
interpolation, isInline, preserveWhitespaces}: {
encapsulation?: ViewEncapsulation | null,
template?: string | null,
templateUrl?: string | null,
@ -94,7 +94,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
ngContentSelectors?: string[],
animations?: any[],
interpolation?: [string, string] | null,
isInline?: boolean
isInline?: boolean,
preserveWhitespaces?: boolean | null,
}): CompileTemplateMetadata {
return new CompileTemplateMetadata({
encapsulation: noUndefined(encapsulation),
@ -106,7 +107,8 @@ function compileTemplateMetadata({encapsulation, template, templateUrl, styles,
animations: animations || [],
ngContentSelectors: ngContentSelectors || [],
interpolation: noUndefined(interpolation),
isInline: !!isInline
isInline: !!isInline,
preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)),
});
}
@ -116,7 +118,7 @@ export function main() {
let ngIf: CompileDirectiveSummary;
let parse: (
template: string, directives: CompileDirectiveSummary[], pipes?: CompilePipeSummary[],
schemas?: SchemaMetadata[]) => TemplateAst[];
schemas?: SchemaMetadata[], preserveWhitespaces?: boolean) => TemplateAst[];
let console: ArrayConsole;
function commonBeforeEach() {
@ -148,12 +150,15 @@ export function main() {
parse =
(template: string, directives: CompileDirectiveSummary[],
pipes: CompilePipeSummary[] | null = null,
schemas: SchemaMetadata[] = []): TemplateAst[] => {
pipes: CompilePipeSummary[] | null = null, schemas: SchemaMetadata[] = [],
preserveWhitespaces = true): TemplateAst[] => {
if (pipes === null) {
pipes = [];
}
return parser.parse(component, template, directives, pipes, schemas, 'TestComp')
return parser
.parse(
component, template, directives, pipes, schemas, 'TestComp',
preserveWhitespaces)
.template;
};
}));
@ -398,7 +403,8 @@ export function main() {
externalStylesheets: [],
styleUrls: [],
styles: [],
encapsulation: null
encapsulation: null,
preserveWhitespaces: preserveWhitespacesDefault(null),
}),
isHost: false,
exportAs: null,
@ -417,7 +423,7 @@ export function main() {
});
expect(humanizeTplAst(
parser.parse(component, '{%a%}', [], [], [], 'TestComp').template,
parser.parse(component, '{%a%}', [], [], [], 'TestComp', true).template,
{start: '{%', end: '%}'}))
.toEqual([[BoundTextAst, '{% a %}']]);
}));
@ -2052,6 +2058,48 @@ The pipe 'test' could not be found ("{{[ERROR ->]a | test}}"): TestComp@0:2`);
});
});
describe('whitespaces removal', () => {
it('should not remove whitespaces by default', () => {
expect(humanizeTplAst(parse(' <br> <br>\t<br>\n<br> ', []))).toEqual([
[TextAst, ' '],
[ElementAst, 'br'],
[TextAst, ' '],
[ElementAst, 'br'],
[TextAst, '\t'],
[ElementAst, 'br'],
[TextAst, '\n'],
[ElementAst, 'br'],
[TextAst, ' '],
]);
});
it('should remove whitespaces when explicitly requested', () => {
expect(humanizeTplAst(parse(' <br> <br>\t<br>\n<br> ', [], [], [], false))).toEqual([
[ElementAst, 'br'],
[ElementAst, 'br'],
[ElementAst, 'br'],
[ElementAst, 'br'],
]);
});
it('should remove whitespace between ICU expansions when not preserving whitespaces', () => {
const shortForm = '{ count, plural, =0 {small} many {big} }';
const expandedForm = '<ng-container [ngPlural]="count">' +
'<ng-template ngPluralCase="=0">small</ng-template>' +
'<ng-template ngPluralCase="many">big</ng-template>' +
'</ng-container>';
const humanizedExpandedForm = humanizeTplAst(parse(expandedForm, []));
// ICU expansions are converted to `<ng-container>` tags and all blank text nodes are reomved
// so any whitespace between ICU exansions are removed as well
expect(humanizeTplAst(parse(`${shortForm} ${shortForm}`, [], [], [], false))).toEqual([
...humanizedExpandedForm, ...humanizedExpandedForm
]);
});
});
describe('Template Parser - opt-out `<template>` support', () => {
beforeEach(() => {
TestBed.configureCompiler({

View File

@ -85,7 +85,8 @@ export class MockDirectiveResolver extends DirectiveResolver {
styles: view.styles,
styleUrls: view.styleUrls,
encapsulation: view.encapsulation,
interpolation: view.interpolation
interpolation: view.interpolation,
preserveWhitespaces: view.preserveWhitespaces,
});
}

View File

@ -107,6 +107,7 @@ export type CompilerOptions = {
// 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.
enableLegacyTemplate?: boolean,
preserveWhitespaces?: boolean,
};
/**

View File

@ -675,6 +675,16 @@ export interface Component extends Directive {
* {@link ComponentFactoryResolver}.
*/
entryComponents?: Array<Type<any>|any[]>;
/**
* If preserveWhitespaces is set to `false` potentially superfluous blank characters (space, tab,
* new line) will be removed from compiled templates. This can greatly reduce generated code size
* as well as speed up components' creation. The whitespace removal algorithm will drop all
* the blank text nodes and collapse series of whitespaces to just one space.
* Those transformations can potentially influence layout of the generated markup so
* the `preserveWhitespaces` should be used with care.
*/
preserveWhitespaces?: boolean;
}
/**

View File

@ -1758,6 +1758,51 @@ function declareTests({useJit}: {useJit: boolean}) {
});
});
describe('whitespaces in templates', () => {
it('should not remove whitespaces by default', async(() => {
@Component({
selector: 'comp',
template: '<span>foo</span> <span>bar</span>',
})
class MyCmp {
}
const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp);
f.detectChanges();
expect(f.nativeElement.childNodes.length).toBe(3);
}));
it('should not remove whitespaces when explicitly requested not to do so', async(() => {
@Component({
selector: 'comp',
template: '<span>foo</span> <span>bar</span>',
preserveWhitespaces: true,
})
class MyCmp {
}
const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp);
f.detectChanges();
expect(f.nativeElement.childNodes.length).toBe(3);
}));
it('should remove whitespaces when explicitly requested to do so', async(() => {
@Component({
selector: 'comp',
template: '<span>foo</span> <span>bar</span>',
preserveWhitespaces: false,
})
class MyCmp {
}
const f = TestBed.configureTestingModule({declarations: [MyCmp]}).createComponent(MyCmp);
f.detectChanges();
expect(f.nativeElement.childNodes.length).toBe(2);
}));
});
if (getDOM().supportsDOMEvents()) {
describe('svg', () => {

View File

@ -201,6 +201,7 @@ export declare type CompilerOptions = {
providers?: StaticProvider[];
missingTranslation?: MissingTranslationStrategy;
enableLegacyTemplate?: boolean;
preserveWhitespaces?: boolean;
};
/** @stable */