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

@ -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({