perf(core): add option to remove blank text nodes from compiled templates
This commit is contained in:
@ -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(
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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';
|
||||
|
||||
|
@ -97,4 +97,4 @@ const serializerVisitor = new _SerializerVisitor();
|
||||
|
||||
export function serializeNodes(nodes: html.Node[]): string[] {
|
||||
return nodes.map(node => node.visit(serializerVisitor, null));
|
||||
}
|
||||
}
|
||||
|
118
packages/compiler/test/ml_parser/html_whitespaces_spec.ts
Normal file
118
packages/compiler/test/ml_parser/html_whitespaces_spec.ts
Normal 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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
@ -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({
|
||||
|
Reference in New Issue
Block a user