/** * @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 {ConstantPool} from '../../constant_pool'; import {AttributeMarker} from '../../core'; import {AST, BindingType, Interpolation} from '../../expression_parser/ast'; import * as o from '../../output/output_ast'; import {ParseSourceSpan} from '../../parse_util'; import {isEmptyExpression} from '../../template_parser/template_parser'; import {error} from '../../util'; import * as t from '../r3_ast'; import {Identifiers as R3} from '../r3_identifiers'; import {hyphenate, parse as parseStyle} from './style_parser'; import {ValueConverter} from './template'; import {getInterpolationArgsLength} from './util'; const IMPORTANT_FLAG = '!important'; /** * A styling expression summary that is to be processed by the compiler */ export interface StylingInstruction { sourceSpan: ParseSourceSpan|null; reference: o.ExternalReference; allocateBindingSlots: number; supportsInterpolation?: boolean; params: ((convertFn: (value: any) => o.Expression | o.Expression[]) => o.Expression[]); } /** * An internal record of the input data for a styling binding */ interface BoundStylingEntry { hasOverrideFlag: boolean; name: string|null; unit: string|null; sourceSpan: ParseSourceSpan; value: AST; } /** * Produces creation/update instructions for all styling bindings (class and style) * * It also produces the creation instruction to register all initial styling values * (which are all the static class="..." and style="..." attribute values that exist * on an element within a template). * * 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) { * styling(...) * } * if (updateMode) { * styleMap(...) * classMap(...) * styleProp(...) * classProp(...) * stylingApply(...) * } * * The creation/update methods within the builder class produce these instructions. */ export class StylingBuilder { /** Whether or not there are any static styling values present */ private _hasInitialValues = false; /** * Whether or not there are any styling bindings present * (i.e. `[style]`, `[class]`, `[style.prop]` or `[class.name]`) */ public hasBindings = false; /** the input for [class] (if it exists) */ private _classMapInput: BoundStylingEntry|null = null; /** the input for [style] (if it exists) */ private _styleMapInput: BoundStylingEntry|null = null; /** an array of each [style.prop] input */ private _singleStyleInputs: BoundStylingEntry[]|null = null; /** an array of each [class.name] input */ private _singleClassInputs: BoundStylingEntry[]|null = null; private _lastStylingInput: BoundStylingEntry|null = null; private _firstStylingInput: BoundStylingEntry|null = null; // maps are used instead of hash maps because a Map will // retain the ordering of the keys /** * Represents the location of each style binding in the template * (e.g. `
` implies * that `width=0` and `height=1`) */ private _stylesIndex = new Map(); /** * Represents the location of each class binding in the template * (e.g. `
` implies * that `big=0` and `hidden=1`) */ private _classesIndex = new Map(); private _initialStyleValues: string[] = []; private _initialClassValues: string[] = []; // certain style properties ALWAYS need sanitization // this is checked each time new styles are encountered private _useDefaultSanitizer = false; constructor(private _elementIndexExpr: o.Expression, private _directiveExpr: o.Expression|null) {} /** * Registers a given input to the styling builder to be later used when producing AOT code. * * The code below will only accept the input if it is somehow tied to styling (whether it be * style/class bindings or static style/class attributes). */ registerBoundInput(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 binding: BoundStylingEntry|null = null; let name = input.name; switch (input.type) { case BindingType.Property: binding = this.registerInputBasedOnName(name, input.value, input.sourceSpan); break; case BindingType.Style: binding = this.registerStyleInput(name, false, input.value, input.sourceSpan, input.unit); break; case BindingType.Class: binding = this.registerClassInput(name, false, input.value, input.sourceSpan); break; } return binding ? true : false; } registerInputBasedOnName(name: string, expression: AST, sourceSpan: ParseSourceSpan) { let binding: BoundStylingEntry|null = null; const prefix = name.substring(0, 6); const isStyle = name === 'style' || prefix === 'style.' || prefix === 'style!'; const isClass = !isStyle && (name === 'class' || name === 'className' || prefix === 'class.' || prefix === 'class!'); if (isStyle || isClass) { const isMapBased = name.charAt(5) !== '.'; // style.prop or class.prop makes this a no const property = name.substr(isMapBased ? 5 : 6); // the dot explains why there's a +1 if (isStyle) { binding = this.registerStyleInput(property, isMapBased, expression, sourceSpan); } else { binding = this.registerClassInput(property, isMapBased, expression, sourceSpan); } } return binding; } registerStyleInput( name: string, isMapBased: boolean, value: AST, sourceSpan: ParseSourceSpan, unit?: string|null): BoundStylingEntry|null { if (isEmptyExpression(value)) { return null; } name = normalizePropName(name); const {property, hasOverrideFlag, unit: bindingUnit} = parseProperty(name); const entry: BoundStylingEntry = { name: property, unit: unit || bindingUnit, value, sourceSpan, hasOverrideFlag }; if (isMapBased) { this._useDefaultSanitizer = true; this._styleMapInput = entry; } else { (this._singleStyleInputs = this._singleStyleInputs || []).push(entry); this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(name); registerIntoMap(this._stylesIndex, property); } this._lastStylingInput = entry; this._firstStylingInput = this._firstStylingInput || entry; this.hasBindings = true; return entry; } registerClassInput(name: string, isMapBased: boolean, value: AST, sourceSpan: ParseSourceSpan): BoundStylingEntry|null { if (isEmptyExpression(value)) { return null; } const {property, hasOverrideFlag} = parseProperty(name); const entry: BoundStylingEntry = {name: property, value, sourceSpan, hasOverrideFlag, unit: null}; if (isMapBased) { this._classMapInput = entry; } else { (this._singleClassInputs = this._singleClassInputs || []).push(entry); registerIntoMap(this._classesIndex, property); } this._lastStylingInput = entry; this._firstStylingInput = this._firstStylingInput || entry; this.hasBindings = true; return entry; } /** * Registers the element's static style string value to the builder. * * @param value the style string (e.g. `width:100px; height:200px;`) */ registerStyleAttr(value: string) { this._initialStyleValues = parseStyle(value); this._hasInitialValues = true; } /** * Registers the element's static class string value to the builder. * * @param value the className string (e.g. `disabled gold zoom`) */ registerClassAttr(value: string) { this._initialClassValues = value.trim().split(/\s+/g); this._hasInitialValues = true; } /** * Appends all styling-related expressions to the provided attrs array. * * @param attrs an existing array where each of the styling expressions * will be inserted into. */ populateInitialStylingAttrs(attrs: o.Expression[]): void { // [CLASS_MARKER, 'foo', 'bar', 'baz' ...] if (this._initialClassValues.length) { attrs.push(o.literal(AttributeMarker.Classes)); for (let i = 0; i < this._initialClassValues.length; i++) { attrs.push(o.literal(this._initialClassValues[i])); } } // [STYLE_MARKER, 'width', '200px', 'height', '100px', ...] if (this._initialStyleValues.length) { attrs.push(o.literal(AttributeMarker.Styles)); for (let i = 0; i < this._initialStyleValues.length; i += 2) { attrs.push( o.literal(this._initialStyleValues[i]), o.literal(this._initialStyleValues[i + 1])); } } } /** * Builds an instruction with all the expressions and parameters for `elementHostAttrs`. * * The instruction generation code below is used for producing the AOT statement code which is * responsible for registering initial styles (within a directive hostBindings' creation block), * as well as any of the provided attribute values, to the directive host element. */ buildHostAttrsInstruction( sourceSpan: ParseSourceSpan|null, attrs: o.Expression[], constantPool: ConstantPool): StylingInstruction|null { if (this._directiveExpr && (attrs.length || this._hasInitialValues)) { return { sourceSpan, reference: R3.elementHostAttrs, allocateBindingSlots: 0, params: () => { // params => elementHostAttrs(attrs) this.populateInitialStylingAttrs(attrs); const attrArray = !attrs.some(attr => attr instanceof o.WrappedNodeExpr) ? getConstantLiteralFromArray(constantPool, attrs) : o.literalArr(attrs); return [attrArray]; } }; } return null; } /** * Builds an instruction with all the expressions and parameters for `styling`. * * The instruction generation code below is used for producing the AOT statement code which is * responsible for registering style/class bindings to an element. */ buildStylingInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool): StylingInstruction|null { if (this.hasBindings) { return { sourceSpan, allocateBindingSlots: 0, reference: R3.styling, params: () => [], }; } return null; } /** * Builds an instruction with all the expressions and parameters for `classMap`. * * The instruction data will contain all expressions for `classMap` to function * which includes the `[class]` expression params. */ buildClassMapInstruction(valueConverter: ValueConverter): StylingInstruction|null { if (this._classMapInput) { return this._buildMapBasedInstruction(valueConverter, true, this._classMapInput); } return null; } /** * Builds an instruction with all the expressions and parameters for `styleMap`. * * The instruction data will contain all expressions for `styleMap` to function * which includes the `[style]` expression params. */ buildStyleMapInstruction(valueConverter: ValueConverter): StylingInstruction|null { if (this._styleMapInput) { return this._buildMapBasedInstruction(valueConverter, false, this._styleMapInput); } return null; } private _buildMapBasedInstruction( valueConverter: ValueConverter, isClassBased: boolean, stylingInput: BoundStylingEntry): StylingInstruction { // each styling binding value is stored in the LView let totalBindingSlotsRequired = 1; // these values must be outside of the update block so that they can // be evaluated (the AST visit call) during creation time so that any // pipes can be picked up in time before the template is built const mapValue = stylingInput.value.visit(valueConverter); let reference: o.ExternalReference; if (mapValue instanceof Interpolation && isClassBased) { totalBindingSlotsRequired += mapValue.expressions.length; reference = getClassMapInterpolationExpression(mapValue); } else { reference = isClassBased ? R3.classMap : R3.styleMap; } return { sourceSpan: stylingInput.sourceSpan, reference, allocateBindingSlots: totalBindingSlotsRequired, supportsInterpolation: isClassBased, params: (convertFn: (value: any) => o.Expression | o.Expression[]) => { const convertResult = convertFn(mapValue); return Array.isArray(convertResult) ? convertResult : [convertResult]; } }; } private _buildSingleInputs( reference: o.ExternalReference, inputs: BoundStylingEntry[], mapIndex: Map, allowUnits: boolean, valueConverter: ValueConverter, getInterpolationExpressionFn?: (value: Interpolation) => o.ExternalReference): StylingInstruction[] { let totalBindingSlotsRequired = 0; return inputs.map(input => { const value = input.value.visit(valueConverter); // each styling binding value is stored in the LView let totalBindingSlotsRequired = 1; if (value instanceof Interpolation) { totalBindingSlotsRequired += value.expressions.length; if (getInterpolationExpressionFn) { reference = getInterpolationExpressionFn(value); } } return { sourceSpan: input.sourceSpan, supportsInterpolation: !!getInterpolationExpressionFn, allocateBindingSlots: totalBindingSlotsRequired, reference, params: (convertFn: (value: any) => o.Expression | o.Expression[]) => { // params => stylingProp(propName, value) const params: o.Expression[] = []; params.push(o.literal(input.name)); const convertResult = convertFn(value); if (Array.isArray(convertResult)) { params.push(...convertResult); } else { params.push(convertResult); } if (allowUnits && input.unit) { params.push(o.literal(input.unit)); } return params; } }; }); } private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] { if (this._singleClassInputs) { return this._buildSingleInputs( R3.classProp, this._singleClassInputs, this._classesIndex, false, valueConverter); } return []; } private _buildStyleInputs(valueConverter: ValueConverter): StylingInstruction[] { if (this._singleStyleInputs) { return this._buildSingleInputs( R3.styleProp, this._singleStyleInputs, this._stylesIndex, true, valueConverter, getStylePropInterpolationExpression); } return []; } private _buildApplyFn(): StylingInstruction { return { sourceSpan: this._lastStylingInput ? this._lastStylingInput.sourceSpan : null, reference: R3.stylingApply, allocateBindingSlots: 0, params: () => { return []; } }; } private _buildSanitizerFn(): StylingInstruction { return { sourceSpan: this._firstStylingInput ? this._firstStylingInput.sourceSpan : null, reference: R3.styleSanitizer, allocateBindingSlots: 0, params: () => [o.importExpr(R3.defaultStyleSanitizer)] }; } /** * Constructs all instructions which contain the expressions that will be placed * into the update block of a template function or a directive hostBindings function. */ buildUpdateLevelInstructions(valueConverter: ValueConverter) { const instructions: StylingInstruction[] = []; if (this.hasBindings) { if (this._useDefaultSanitizer) { instructions.push(this._buildSanitizerFn()); } const styleMapInstruction = this.buildStyleMapInstruction(valueConverter); if (styleMapInstruction) { instructions.push(styleMapInstruction); } const classMapInstruction = this.buildClassMapInstruction(valueConverter); if (classMapInstruction) { instructions.push(classMapInstruction); } instructions.push(...this._buildStyleInputs(valueConverter)); instructions.push(...this._buildClassInputs(valueConverter)); instructions.push(this._buildApplyFn()); } return instructions; } } function registerIntoMap(map: Map, key: string) { if (!map.has(key)) { map.set(key, map.size); } } function isStyleSanitizable(prop: string): boolean { // Note that browsers support both the dash case and // camel case property names when setting through JS. return prop === 'background-image' || prop === 'backgroundImage' || prop === 'background' || prop === 'border-image' || prop === 'borderImage' || prop === 'filter' || prop === 'list-style' || prop === 'listStyle' || prop === 'list-style-image' || prop === 'listStyleImage' || prop === 'clip-path' || prop === 'clipPath'; } /** * Simple helper function to either provide the constant literal that will house the value * here or a null value if the provided values are empty. */ function getConstantLiteralFromArray( constantPool: ConstantPool, values: o.Expression[]): o.Expression { return values.length ? constantPool.getConstLiteral(o.literalArr(values), true) : o.NULL_EXPR; } /** * Simple helper function that adds a parameter or does nothing at all depending on the provided * predicate and totalExpectedArgs values */ function addParam( params: o.Expression[], predicate: any, value: o.Expression | null, argNumber: number, totalExpectedArgs: number) { if (predicate && value) { params.push(value); } else if (argNumber < totalExpectedArgs) { params.push(o.NULL_EXPR); } } export function parseProperty(name: string): {property: string, unit: string, hasOverrideFlag: boolean} { let hasOverrideFlag = false; const overrideIndex = name.indexOf(IMPORTANT_FLAG); if (overrideIndex !== -1) { name = overrideIndex > 0 ? name.substring(0, overrideIndex) : ''; hasOverrideFlag = true; } let unit = ''; let property = name; const unitIndex = name.lastIndexOf('.'); if (unitIndex > 0) { unit = name.substr(unitIndex + 1); property = name.substring(0, unitIndex); } return {property, unit, hasOverrideFlag}; } /** * Gets the instruction to generate for an interpolated class map. * @param interpolation An Interpolation AST */ function getClassMapInterpolationExpression(interpolation: Interpolation): o.ExternalReference { switch (getInterpolationArgsLength(interpolation)) { case 1: return R3.classMap; case 3: return R3.classMapInterpolate1; case 5: return R3.classMapInterpolate2; case 7: return R3.classMapInterpolate3; case 9: return R3.classMapInterpolate4; case 11: return R3.classMapInterpolate5; case 13: return R3.classMapInterpolate6; case 15: return R3.classMapInterpolate7; case 17: return R3.classMapInterpolate8; default: return R3.classMapInterpolateV; } } /** * Gets the instruction to generate for an interpolated style prop. * @param interpolation An Interpolation AST */ function getStylePropInterpolationExpression(interpolation: Interpolation) { switch (getInterpolationArgsLength(interpolation)) { case 1: return R3.styleProp; case 3: return R3.stylePropInterpolate1; case 5: return R3.stylePropInterpolate2; case 7: return R3.stylePropInterpolate3; case 9: return R3.stylePropInterpolate4; case 11: return R3.stylePropInterpolate5; case 13: return R3.stylePropInterpolate6; case 15: return R3.stylePropInterpolate7; case 17: return R3.stylePropInterpolate8; default: return R3.stylePropInterpolateV; } } function normalizePropName(prop: string): string { return hyphenate(prop); }