refactor(ivy): abstract all styling-related compiler logic into a shared class (#27043)
PR Close #27043
This commit is contained in:
parent
8b9249a670
commit
095b6e8113
|
@ -121,7 +121,6 @@ describe('compiler compliance', () => {
|
|||
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
|
||||
expectEmit(result.source, factory, 'Incorrect factory');
|
||||
expectEmit(result.source, template, 'Incorrect template');
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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({});
|
Loading…
Reference in New Issue