refactor(ivy): abstract all styling-related compiler logic into a shared class (#27043)

PR Close #27043
This commit is contained in:
Matias Niemelä 2018-11-09 18:03:32 -08:00 committed by Andrew Kushnir
parent 8b9249a670
commit 095b6e8113
5 changed files with 439 additions and 337 deletions

View File

@ -121,7 +121,6 @@ describe('compiler compliance', () => {
const result = compile(files, angularFiles);
expectEmit(result.source, factory, 'Incorrect factory');
expectEmit(result.source, template, 'Incorrect template');
});

View File

@ -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();
}

View File

@ -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<string, number>();
private _classesIndex = new Map<string, number>();
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<string, number>, 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<string, number>,
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<string, number>, 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';
}

View File

@ -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<void>, 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<void>, 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<void>, 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<void>, 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<void>, 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<void>, 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<void>, 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;
}

View File

@ -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({});