diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 7ce0dbcd74..0e4f2a976d 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -121,7 +121,6 @@ describe('compiler compliance', () => { const result = compile(files, angularFiles); - expectEmit(result.source, factory, 'Incorrect factory'); expectEmit(result.source, template, 'Incorrect template'); }); diff --git a/packages/compiler/src/render3/view/style_parser.ts b/packages/compiler/src/render3/view/style_parser.ts new file mode 100644 index 0000000000..28d38c6f4f --- /dev/null +++ b/packages/compiler/src/render3/view/style_parser.ts @@ -0,0 +1,111 @@ +/** + * @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 + */ + +const enum Char { + OpenParen = 40, + CloseParen = 41, + Colon = 58, + Semicolon = 59, + BackSlash = 92, + QuoteNone = 0, // indicating we are not inside a quote + QuoteDouble = 34, + QuoteSingle = 39, +} + + +/** + * Parses string representation of a style and converts it into object literal. + * + * @param value string representation of style as used in the `style` attribute in HTML. + * Example: `color: red; height: auto`. + * @returns an object literal. `{ color: 'red', height: 'auto'}`. + */ +export function parse(value: string): {[key: string]: any} { + const styles: {[key: string]: any} = {}; + + let i = 0; + let parenDepth = 0; + let quote: Char = Char.QuoteNone; + let valueStart = 0; + let propStart = 0; + let currentProp: string|null = null; + let valueHasQuotes = false; + while (i < value.length) { + const token = value.charCodeAt(i++) as Char; + switch (token) { + case Char.OpenParen: + parenDepth++; + break; + case Char.CloseParen: + parenDepth--; + break; + case Char.QuoteSingle: + // valueStart needs to be there since prop values don't + // have quotes in CSS + valueHasQuotes = valueHasQuotes || valueStart > 0; + if (quote === Char.QuoteNone) { + quote = Char.QuoteSingle; + } else if (quote === Char.QuoteSingle && value.charCodeAt(i - 1) !== Char.BackSlash) { + quote = Char.QuoteNone; + } + break; + case Char.QuoteDouble: + // same logic as above + valueHasQuotes = valueHasQuotes || valueStart > 0; + if (quote === Char.QuoteNone) { + quote = Char.QuoteDouble; + } else if (quote === Char.QuoteDouble && value.charCodeAt(i - 1) !== Char.BackSlash) { + quote = Char.QuoteNone; + } + break; + case Char.Colon: + if (!currentProp && parenDepth === 0 && quote === Char.QuoteNone) { + currentProp = hyphenate(value.substring(propStart, i - 1).trim()); + valueStart = i; + } + break; + case Char.Semicolon: + if (currentProp && valueStart > 0 && parenDepth === 0 && quote === Char.QuoteNone) { + const styleVal = value.substring(valueStart, i - 1).trim(); + styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal; + propStart = i; + valueStart = 0; + currentProp = null; + valueHasQuotes = false; + } + break; + } + } + + if (currentProp && valueStart) { + const styleVal = value.substr(valueStart).trim(); + styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal; + } + + return styles; +} + +export function stripUnnecessaryQuotes(value: string): string { + const qS = value.charCodeAt(0); + const qE = value.charCodeAt(value.length - 1); + if (qS == qE && (qS == Char.QuoteSingle || qS == Char.QuoteDouble)) { + const tempValue = value.substring(1, value.length - 1); + // special case to avoid using a multi-quoted string that was just chomped + // (e.g. `font-family: "Verdana", "sans-serif"`) + if (tempValue.indexOf('\'') == -1 && tempValue.indexOf('"') == -1) { + value = tempValue; + } + } + return value; +} + +export function hyphenate(value: string): string { + return value.replace(/[a-z][A-Z]/g, v => { + return v.charAt(0) + '-' + v.charAt(1); + }).toLowerCase(); +} diff --git a/packages/compiler/src/render3/view/styling.ts b/packages/compiler/src/render3/view/styling.ts index d9b02f4b8e..a0ecaf3252 100644 --- a/packages/compiler/src/render3/view/styling.ts +++ b/packages/compiler/src/render3/view/styling.ts @@ -5,107 +5,305 @@ * 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 {ConstantPool} from '../../constant_pool'; +import {InitialStylingFlags} from '../../core'; +import {BindingType} from '../../expression_parser/ast'; +import * as o from '../../output/output_ast'; +import {ParseSourceSpan} from '../../parse_util'; +import * as t from '../r3_ast'; +import {Identifiers as R3} from '../r3_identifiers'; -const enum Char { - OpenParen = 40, - CloseParen = 41, - Colon = 58, - Semicolon = 59, - BackSlash = 92, - QuoteNone = 0, // indicating we are not inside a quote - QuoteDouble = 34, - QuoteSingle = 39, -} - +import {parse as parseStyle} from './style_parser'; +import {ValueConverter} from './template'; /** - * Parses string representation of a style and converts it into object literal. - * - * @param value string representation of style as used in the `style` attribute in HTML. - * Example: `color: red; height: auto`. - * @returns an object literal. `{ color: 'red', height: 'auto'}`. + * A styling expression summary that is to be processed by the compiler */ -export function parseStyle(value: string): {[key: string]: any} { - const styles: {[key: string]: any} = {}; +export interface StylingInstruction { + sourceSpan: ParseSourceSpan|null; + reference: o.ExternalReference; + buildParams(convertFn: (value: any) => o.Expression): o.Expression[]; +} - let i = 0; - let parenDepth = 0; - let quote: Char = Char.QuoteNone; - let valueStart = 0; - let propStart = 0; - let currentProp: string|null = null; - let valueHasQuotes = false; - while (i < value.length) { - const token = value.charCodeAt(i++) as Char; - switch (token) { - case Char.OpenParen: - parenDepth++; - break; - case Char.CloseParen: - parenDepth--; - break; - case Char.QuoteSingle: - // valueStart needs to be there since prop values don't - // have quotes in CSS - valueHasQuotes = valueHasQuotes || valueStart > 0; - if (quote === Char.QuoteNone) { - quote = Char.QuoteSingle; - } else if (quote === Char.QuoteSingle && value.charCodeAt(i - 1) !== Char.BackSlash) { - quote = Char.QuoteNone; +/** + * Produces creation/update instructions for all styling bindings (class and style) + * + * The builder class below handles producing instructions for the following cases: + * + * - Static style/class attributes (style="..." and class="...") + * - Dynamic style/class map bindings ([style]="map" and [class]="map|string") + * - Dynamic style/class property bindings ([style.prop]="exp" and [class.name]="exp") + * + * Due to the complex relationship of all of these cases, the instructions generated + * for these attributes/properties/bindings must be done so in the correct order. The + * order which these must be generated is as follows: + * + * if (createMode) { + * elementStyling(...) + * } + * if (updateMode) { + * elementStylingMap(...) + * elementStyleProp(...) + * elementClassProp(...) + * elementStylingApp(...) + * } + * + * The creation/update methods within the builder class produce these instructions. + */ +export class StylingBuilder { + public readonly hasBindingsOrInitialValues = false; + + private _indexLiteral: o.LiteralExpr; + private _classMapInput: t.BoundAttribute|null = null; + private _styleMapInput: t.BoundAttribute|null = null; + private _singleStyleInputs: t.BoundAttribute[]|null = null; + private _singleClassInputs: t.BoundAttribute[]|null = null; + private _lastStylingInput: t.BoundAttribute|null = null; + + // maps are used instead of hash maps because a Map will + // retain the ordering of the keys + private _stylesIndex = new Map(); + private _classesIndex = new Map(); + private _initialStyleValues: {[propName: string]: string} = {}; + private _initialClassValues: {[className: string]: boolean} = {}; + private _useDefaultSanitizer = false; + private _applyFnRequired = false; + + constructor(elementIndex: number) { this._indexLiteral = o.literal(elementIndex); } + + registerInput(input: t.BoundAttribute): boolean { + // [attr.style] or [attr.class] are skipped in the code below, + // they should not be treated as styling-based bindings since + // they are intended to be written directly to the attr and + // will therefore skip all style/class resolution that is present + // with style="", [style]="" and [style.prop]="", class="", + // [class.prop]="". [class]="" assignments + let registered = false; + const name = input.name; + switch (input.type) { + case BindingType.Property: + if (name == 'style') { + this._styleMapInput = input; + this._useDefaultSanitizer = true; + registered = true; + } else if (isClassBinding(input)) { + this._classMapInput = input; + registered = true; } break; - case Char.QuoteDouble: - // same logic as above - valueHasQuotes = valueHasQuotes || valueStart > 0; - if (quote === Char.QuoteNone) { - quote = Char.QuoteDouble; - } else if (quote === Char.QuoteDouble && value.charCodeAt(i - 1) !== Char.BackSlash) { - quote = Char.QuoteNone; - } + case BindingType.Style: + (this._singleStyleInputs = this._singleStyleInputs || []).push(input); + this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(name); + registerIntoMap(this._stylesIndex, name); + registered = true; break; - case Char.Colon: - if (!currentProp && parenDepth === 0 && quote === Char.QuoteNone) { - currentProp = hyphenate(value.substring(propStart, i - 1).trim()); - valueStart = i; - } - break; - case Char.Semicolon: - if (currentProp && valueStart > 0 && parenDepth === 0 && quote === Char.QuoteNone) { - const styleVal = value.substring(valueStart, i - 1).trim(); - styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal; - propStart = i; - valueStart = 0; - currentProp = null; - valueHasQuotes = false; - } + case BindingType.Class: + (this._singleClassInputs = this._singleClassInputs || []).push(input); + registerIntoMap(this._classesIndex, name); + registered = true; break; } - } - - if (currentProp && valueStart) { - const styleVal = value.substr(valueStart).trim(); - styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal; - } - - return styles; -} - -export function stripUnnecessaryQuotes(value: string): string { - const qS = value.charCodeAt(0); - const qE = value.charCodeAt(value.length - 1); - if (qS == qE && (qS == Char.QuoteSingle || qS == Char.QuoteDouble)) { - const tempValue = value.substring(1, value.length - 1); - // special case to avoid using a multi-quoted string that was just chomped - // (e.g. `font-family: "Verdana", "sans-serif"`) - if (tempValue.indexOf('\'') == -1 && tempValue.indexOf('"') == -1) { - value = tempValue; + if (registered) { + this._lastStylingInput = input; + (this as any).hasBindingsOrInitialValues = true; + this._applyFnRequired = true; } + return registered; + } + + registerStyleAttr(value: string) { + this._initialStyleValues = parseStyle(value); + Object.keys(this._initialStyleValues).forEach(prop => { + registerIntoMap(this._stylesIndex, prop); + (this as any).hasBindingsOrInitialValues = true; + }); + } + + registerClassAttr(value: string) { + this._initialClassValues = {}; + value.split(/\s+/g).forEach(className => { + this._initialClassValues[className] = true; + registerIntoMap(this._classesIndex, className); + (this as any).hasBindingsOrInitialValues = true; + }); + } + + private _buildInitExpr(registry: Map, initialValues: {[key: string]: any}): + o.Expression|null { + const exprs: o.Expression[] = []; + const nameAndValueExprs: o.Expression[] = []; + + // _c0 = [prop, prop2, prop3, ...] + registry.forEach((value, key) => { + const keyLiteral = o.literal(key); + exprs.push(keyLiteral); + const initialValue = initialValues[key]; + if (initialValue) { + nameAndValueExprs.push(keyLiteral, o.literal(initialValue)); + } + }); + + if (nameAndValueExprs.length) { + // _c0 = [... MARKER ...] + exprs.push(o.literal(InitialStylingFlags.VALUES_MODE)); + // _c0 = [prop, VALUE, prop2, VALUE2, ...] + exprs.push(...nameAndValueExprs); + } + + return exprs.length ? o.literalArr(exprs) : null; + } + + buildCreateLevelInstruction(sourceSpan: ParseSourceSpan, constantPool: ConstantPool): + StylingInstruction|null { + if (this.hasBindingsOrInitialValues) { + const initialClasses = this._buildInitExpr(this._classesIndex, this._initialClassValues); + const initialStyles = this._buildInitExpr(this._stylesIndex, this._initialStyleValues); + + // in the event that a [style] binding is used then sanitization will + // always be imported because it is not possible to know ahead of time + // whether style bindings will use or not use any sanitizable properties + // that isStyleSanitizable() will detect + const useSanitizer = this._useDefaultSanitizer; + const params: (o.Expression)[] = []; + + if (initialClasses) { + // the template compiler handles initial class styling (e.g. class="foo") values + // in a special command called `elementClass` so that the initial class + // can be processed during runtime. These initial class values are bound to + // a constant because the inital class values do not change (since they're static). + params.push(constantPool.getConstLiteral(initialClasses, true)); + } else if (initialStyles || useSanitizer) { + // no point in having an extra `null` value unless there are follow-up params + params.push(o.NULL_EXPR); + } + + if (initialStyles) { + // the template compiler handles initial style (e.g. style="foo") values + // in a special command called `elementStyle` so that the initial styles + // can be processed during runtime. These initial styles values are bound to + // a constant because the inital style values do not change (since they're static). + params.push(constantPool.getConstLiteral(initialStyles, true)); + } else if (useSanitizer) { + // no point in having an extra `null` value unless there are follow-up params + params.push(o.NULL_EXPR); + } + + if (useSanitizer) { + params.push(o.importExpr(R3.defaultStyleSanitizer)); + } + + return {sourceSpan, reference: R3.elementStyling, buildParams: () => params}; + } + return null; + } + + private _buildStylingMap(valueConverter: ValueConverter): StylingInstruction|null { + if (this._classMapInput || this._styleMapInput) { + const stylingInput = this._classMapInput ! || this._styleMapInput !; + + // these values must be outside of the update block so that they can + // be evaluted (the AST visit call) during creation time so that any + // pipes can be picked up in time before the template is built + const mapBasedClassValue = + this._classMapInput ? this._classMapInput.value.visit(valueConverter) : null; + const mapBasedStyleValue = + this._styleMapInput ? this._styleMapInput.value.visit(valueConverter) : null; + + return { + sourceSpan: stylingInput.sourceSpan, + reference: R3.elementStylingMap, + buildParams: (convertFn: (value: any) => o.Expression) => { + const params: o.Expression[] = [this._indexLiteral]; + + if (mapBasedClassValue) { + params.push(convertFn(mapBasedClassValue)); + } else if (this._styleMapInput) { + params.push(o.NULL_EXPR); + } + + if (mapBasedStyleValue) { + params.push(convertFn(mapBasedStyleValue)); + } + + return params; + } + }; + } + return null; + } + + private _buildSingleInputs( + reference: o.ExternalReference, inputs: t.BoundAttribute[], mapIndex: Map, + valueConverter: ValueConverter): StylingInstruction[] { + return inputs.map(input => { + const bindingIndex: number = mapIndex.get(input.name) !; + const value = input.value.visit(valueConverter); + return { + sourceSpan: input.sourceSpan, + reference, + buildParams: (convertFn: (value: any) => o.Expression) => { + const params = [this._indexLiteral, o.literal(bindingIndex), convertFn(value)]; + if (input.unit != null) { + params.push(o.literal(input.unit)); + } + return params; + } + }; + }); + } + + private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] { + if (this._singleClassInputs) { + return this._buildSingleInputs( + R3.elementClassProp, this._singleClassInputs, this._classesIndex, valueConverter); + } + return []; + } + + private _buildStyleInputs(valueConverter: ValueConverter): StylingInstruction[] { + if (this._singleStyleInputs) { + return this._buildSingleInputs( + R3.elementStyleProp, this._singleStyleInputs, this._stylesIndex, valueConverter); + } + return []; + } + + private _buildApplyFn(): StylingInstruction { + return { + sourceSpan: this._lastStylingInput ? this._lastStylingInput.sourceSpan : null, + reference: R3.elementStylingApply, + buildParams: () => [this._indexLiteral] + }; + } + + buildUpdateLevelInstructions(valueConverter: ValueConverter) { + const instructions: StylingInstruction[] = []; + if (this.hasBindingsOrInitialValues) { + const mapInstruction = this._buildStylingMap(valueConverter); + if (mapInstruction) { + instructions.push(mapInstruction); + } + instructions.push(...this._buildStyleInputs(valueConverter)); + instructions.push(...this._buildClassInputs(valueConverter)); + if (this._applyFnRequired) { + instructions.push(this._buildApplyFn()); + } + } + return instructions; } - return value; } -export function hyphenate(value: string): string { - return value.replace(/[a-z][A-Z]/g, v => { - return v.charAt(0) + '-' + v.charAt(1); - }).toLowerCase(); +function isClassBinding(input: t.BoundAttribute): boolean { + return input.name == 'className' || input.name == 'class'; +} + +function registerIntoMap(map: Map, key: string) { + if (!map.has(key)) { + map.set(key, map.size); + } +} + +function isStyleSanitizable(prop: string): boolean { + return prop === 'background-image' || prop === 'background' || prop === 'border-image' || + prop === 'filter' || prop === 'list-style' || prop === 'list-style-image'; } diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 82fe78e4c3..005839beac 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -30,7 +30,7 @@ import {htmlAstToRender3Ast} from '../r3_template_transform'; import {R3QueryMetadata} from './api'; import {I18N_ATTR, I18N_ATTR_PREFIX, I18nContext, assembleI18nBoundString} from './i18n'; -import {parseStyle} from './styling'; +import {StylingBuilder, StylingInstruction} from './styling'; import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util'; function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined { @@ -338,6 +338,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver visitElement(element: t.Element) { const elementIndex = this.allocateDataSlot(); + const stylingBuilder = new StylingBuilder(elementIndex); let isNonBindableMode: boolean = false; let isI18nRootElement: boolean = false; @@ -364,6 +365,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver i18nMeta = value; } else if (name.startsWith(I18N_ATTR_PREFIX)) { attrI18nMetas[name.slice(I18N_ATTR_PREFIX.length)] = value; + } else if (name == 'style') { + stylingBuilder.registerStyleAttr(value); + } else if (name == 'class') { + stylingBuilder.registerClassAttr(value); } else { outputAttrs[name] = value; } @@ -380,131 +385,33 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Add the attributes const attributes: o.Expression[] = []; - const initialStyleDeclarations: o.Expression[] = []; - const initialClassDeclarations: o.Expression[] = []; - - const styleInputs: t.BoundAttribute[] = []; - const classInputs: t.BoundAttribute[] = []; const allOtherInputs: t.BoundAttribute[] = []; const i18nAttrs: Array<{name: string, value: string | AST}> = []; - element.inputs.forEach((input: t.BoundAttribute) => { - switch (input.type) { - // [attr.style] or [attr.class] should not be treated as styling-based - // bindings since they are intended to be written directly to the attr - // and therefore will skip all style/class resolution that is present - // with style="", [style]="" and [style.prop]="", class="", - // [class.prop]="". [class]="" assignments - case BindingType.Property: - if (input.name == 'style') { - // this should always go first in the compilation (for [style]) - styleInputs.splice(0, 0, input); - } else if (isClassBinding(input)) { - // this should always go first in the compilation (for [class]) - classInputs.splice(0, 0, input); - } else if (attrI18nMetas.hasOwnProperty(input.name)) { + if (!stylingBuilder.registerInput(input)) { + if (input.type == BindingType.Property) { + if (attrI18nMetas.hasOwnProperty(input.name)) { i18nAttrs.push({name: input.name, value: input.value}); } else { allOtherInputs.push(input); } - break; - case BindingType.Style: - styleInputs.push(input); - break; - case BindingType.Class: - classInputs.push(input); - break; - default: - allOtherInputs.push(input); - break; - } - }); - - let currStyleIndex = 0; - let currClassIndex = 0; - let staticStylesMap: {[key: string]: any}|null = null; - let staticClassesMap: {[key: string]: boolean}|null = null; - const stylesIndexMap: {[key: string]: number} = {}; - const classesIndexMap: {[key: string]: number} = {}; - Object.getOwnPropertyNames(outputAttrs).forEach(name => { - const value = outputAttrs[name]; - if (name == 'style') { - staticStylesMap = parseStyle(value); - Object.keys(staticStylesMap).forEach(prop => { stylesIndexMap[prop] = currStyleIndex++; }); - } else if (name == 'class') { - staticClassesMap = {}; - value.split(/\s+/g).forEach(className => { - classesIndexMap[className] = currClassIndex++; - staticClassesMap ![className] = true; - }); - } else { - if (attrI18nMetas.hasOwnProperty(name)) { - i18nAttrs.push({name, value}); } else { - attributes.push(o.literal(name), o.literal(value)); + allOtherInputs.push(input); } } }); - let hasMapBasedStyling = false; - for (let i = 0; i < styleInputs.length; i++) { - const input = styleInputs[i]; - const isMapBasedStyleBinding = i === 0 && input.name === 'style'; - if (isMapBasedStyleBinding) { - hasMapBasedStyling = true; - } else if (!stylesIndexMap.hasOwnProperty(input.name)) { - stylesIndexMap[input.name] = currStyleIndex++; + Object.getOwnPropertyNames(outputAttrs).forEach(name => { + const value = outputAttrs[name]; + if (attrI18nMetas.hasOwnProperty(name)) { + i18nAttrs.push({name, value}); + } else { + attributes.push(o.literal(name), o.literal(value)); } - } - - for (let i = 0; i < classInputs.length; i++) { - const input = classInputs[i]; - const isMapBasedClassBinding = i === 0 && isClassBinding(input); - if (!isMapBasedClassBinding && !stylesIndexMap.hasOwnProperty(input.name)) { - classesIndexMap[input.name] = currClassIndex++; - } - } - - // in the event that a [style] binding is used then sanitization will - // always be imported because it is not possible to know ahead of time - // whether style bindings will use or not use any sanitizable properties - // that isStyleSanitizable() will detect - let useDefaultStyleSanitizer = hasMapBasedStyling; + }); // this will build the instructions so that they fall into the following syntax - // => [prop1, prop2, prop3, 0, prop1, value1, prop2, value2] - Object.keys(stylesIndexMap).forEach(prop => { - useDefaultStyleSanitizer = useDefaultStyleSanitizer || isStyleSanitizable(prop); - initialStyleDeclarations.push(o.literal(prop)); - }); - - if (staticStylesMap) { - initialStyleDeclarations.push(o.literal(core.InitialStylingFlags.VALUES_MODE)); - - Object.keys(staticStylesMap).forEach(prop => { - initialStyleDeclarations.push(o.literal(prop)); - const value = staticStylesMap ![prop]; - initialStyleDeclarations.push(o.literal(value)); - }); - } - - Object.keys(classesIndexMap).forEach(prop => { - initialClassDeclarations.push(o.literal(prop)); - }); - - if (staticClassesMap) { - initialClassDeclarations.push(o.literal(core.InitialStylingFlags.VALUES_MODE)); - - Object.keys(staticClassesMap).forEach(className => { - initialClassDeclarations.push(o.literal(className)); - initialClassDeclarations.push(o.literal(true)); - }); - } - - const hasStylingInstructions = initialStyleDeclarations.length || styleInputs.length || - initialClassDeclarations.length || classInputs.length; - // add attributes for directive matching purposes attributes.push(...this.prepareSyntheticAndSelectOnlyAttrs(allOtherInputs, element.outputs)); parameters.push(this.toAttrsParam(attributes)); @@ -537,8 +444,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver return element.children.length > 0; }; - const createSelfClosingInstruction = !hasStylingInstructions && !isNgContainer && - element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren(); + const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues && + !isNgContainer && element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren(); if (createSelfClosingInstruction) { this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters)); @@ -590,40 +497,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } } - // initial styling for static style="..." attributes - if (hasStylingInstructions) { - const paramsList: (o.Expression)[] = []; - - if (initialClassDeclarations.length) { - // the template compiler handles initial class styling (e.g. class="foo") values - // in a special command called `elementClass` so that the initial class - // can be processed during runtime. These initial class values are bound to - // a constant because the inital class values do not change (since they're static). - paramsList.push( - this.constantPool.getConstLiteral(o.literalArr(initialClassDeclarations), true)); - } else if (initialStyleDeclarations.length || useDefaultStyleSanitizer) { - // no point in having an extra `null` value unless there are follow-up params - paramsList.push(o.NULL_EXPR); - } - - if (initialStyleDeclarations.length) { - // the template compiler handles initial style (e.g. style="foo") values - // in a special command called `elementStyle` so that the initial styles - // can be processed during runtime. These initial styles values are bound to - // a constant because the inital style values do not change (since they're static). - paramsList.push( - this.constantPool.getConstLiteral(o.literalArr(initialStyleDeclarations), true)); - } else if (useDefaultStyleSanitizer) { - // no point in having an extra `null` value unless there are follow-up params - paramsList.push(o.NULL_EXPR); - } - - if (useDefaultStyleSanitizer) { - paramsList.push(o.importExpr(R3.defaultStyleSanitizer)); - } - - this.creationInstruction(null, R3.elementStyling, paramsList); - } + // initial styling for static style="..." and class="..." attributes + this.processStylingInstruction( + implicit, + stylingBuilder.buildCreateLevelInstruction(element.sourceSpan, this.constantPool), true); // Generate Listeners (outputs) element.outputs.forEach((outputAst: t.BoundEvent) => { @@ -633,88 +510,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); } - if ((styleInputs.length || classInputs.length) && hasStylingInstructions) { - const indexLiteral = o.literal(elementIndex); - - const firstStyle = styleInputs[0]; - const mapBasedStyleInput = firstStyle && firstStyle.name == 'style' ? firstStyle : null; - - const firstClass = classInputs[0]; - const mapBasedClassInput = firstClass && isClassBinding(firstClass) ? firstClass : null; - - const stylingInput = mapBasedStyleInput || mapBasedClassInput; - if (stylingInput) { - // these values must be outside of the update block so that they can - // be evaluted (the AST visit call) during creation time so that any - // pipes can be picked up in time before the template is built - const mapBasedClassValue = - mapBasedClassInput ? mapBasedClassInput.value.visit(this._valueConverter) : null; - const mapBasedStyleValue = - mapBasedStyleInput ? mapBasedStyleInput.value.visit(this._valueConverter) : null; - this.updateInstruction(stylingInput.sourceSpan, R3.elementStylingMap, () => { - const params: o.Expression[] = [indexLiteral]; - - if (mapBasedClassValue) { - params.push(this.convertPropertyBinding(implicit, mapBasedClassValue, true)); - } else if (mapBasedStyleInput) { - params.push(o.NULL_EXPR); - } - - if (mapBasedStyleValue) { - params.push(this.convertPropertyBinding(implicit, mapBasedStyleValue, true)); - } - - return params; - }); - } - - let lastInputCommand: t.BoundAttribute|null = null; - if (styleInputs.length) { - let i = mapBasedStyleInput ? 1 : 0; - for (i; i < styleInputs.length; i++) { - const input = styleInputs[i]; - const key = input.name; - const styleIndex: number = stylesIndexMap[key] !; - const value = input.value.visit(this._valueConverter); - this.updateInstruction(input.sourceSpan, R3.elementStyleProp, () => { - const params: o.Expression[] = [ - indexLiteral, o.literal(styleIndex), - this.convertPropertyBinding(implicit, value, true) - ]; - - if (input.unit != null) { - params.push(o.literal(input.unit)); - } - - return params; - }); - } - - lastInputCommand = styleInputs[styleInputs.length - 1]; - } - - if (classInputs.length) { - let i = mapBasedClassInput ? 1 : 0; - for (i; i < classInputs.length; i++) { - const input = classInputs[i]; - const params: any[] = []; - const sanitizationRef = resolveSanitizationFn(input, input.securityContext); - if (sanitizationRef) params.push(sanitizationRef); - - const key = input.name; - const classIndex: number = classesIndexMap[key] !; - const value = input.value.visit(this._valueConverter); - this.updateInstruction(input.sourceSpan, R3.elementClassProp, () => { - const valueLiteral = this.convertPropertyBinding(implicit, value, true); - return [indexLiteral, o.literal(classIndex), valueLiteral]; - }); - } - - lastInputCommand = classInputs[classInputs.length - 1]; - } - - this.updateInstruction(lastInputCommand !.sourceSpan, R3.elementStylingApply, [indexLiteral]); - } + stylingBuilder.buildUpdateLevelInstructions(this._valueConverter).forEach(instruction => { + this.processStylingInstruction(implicit, instruction, false); + }); // Generate element input bindings allOtherInputs.forEach((input: t.BoundAttribute) => { @@ -920,6 +718,19 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); } + private processStylingInstruction( + implicit: any, instruction: StylingInstruction|null, createMode: boolean) { + if (instruction) { + const paramsFn = () => + instruction.buildParams(value => this.convertPropertyBinding(implicit, value, true)); + if (createMode) { + this.creationInstruction(instruction.sourceSpan, instruction.reference, paramsFn); + } else { + this.updateInstruction(instruction.sourceSpan, instruction.reference, paramsFn); + } + } + } + private creationInstruction( span: ParseSourceSpan|null, reference: o.ExternalReference, paramsOrFn?: o.Expression[]|(() => o.Expression[])) { @@ -1511,10 +1322,6 @@ export function makeBindingParser(): BindingParser { []); } -function isClassBinding(input: t.BoundAttribute): boolean { - return input.name == 'className' || input.name == 'class'; -} - function resolveSanitizationFn(input: t.BoundAttribute, context: core.SecurityContext) { switch (context) { case core.SecurityContext.HTML: @@ -1535,19 +1342,6 @@ function resolveSanitizationFn(input: t.BoundAttribute, context: core.SecurityCo } } -function isStyleSanitizable(prop: string): boolean { - switch (prop) { - case 'background-image': - case 'background': - case 'border-image': - case 'filter': - case 'list-style': - case 'list-style-image': - return true; - } - return false; -} - function prepareSyntheticAttributeName(name: string) { return '@' + name; } diff --git a/packages/compiler/test/render3/styling_spec.ts b/packages/compiler/test/render3/style_parser_spec.ts similarity index 96% rename from packages/compiler/test/render3/styling_spec.ts rename to packages/compiler/test/render3/style_parser_spec.ts index 82d7dc71ff..d0f9e4ae11 100644 --- a/packages/compiler/test/render3/styling_spec.ts +++ b/packages/compiler/test/render3/style_parser_spec.ts @@ -5,9 +5,9 @@ * 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 {hyphenate, parseStyle, stripUnnecessaryQuotes} from '../../src/render3/view/styling'; +import {hyphenate, parse as parseStyle, stripUnnecessaryQuotes} from '../../src/render3/view/style_parser'; -describe('inline css style parsing', () => { +describe('style parsing', () => { it('should parse empty or blank strings', () => { const result1 = parseStyle(''); expect(result1).toEqual({});