feat(ivy): properly apply style="", [style], [style.foo] and [attr.style] bindings (#24602)
PR Close #24602
This commit is contained in:

committed by
Miško Hevery

parent
52d43a99ef
commit
3980640d53
@ -379,3 +379,9 @@ export const enum RenderFlags {
|
||||
/* Whether to run the update block (e.g. refresh bindings) */
|
||||
Update = 0b10
|
||||
}
|
||||
|
||||
// Note this will expand once `class` is introduced to styling
|
||||
export const enum InitialStylingFlags {
|
||||
/** Mode for matching initial style values */
|
||||
INITIAL_STYLES = 0b00,
|
||||
}
|
||||
|
@ -37,9 +37,13 @@ export class Identifiers {
|
||||
|
||||
static elementClassNamed: o.ExternalReference = {name: 'ɵkn', moduleName: CORE};
|
||||
|
||||
static elementStyle: o.ExternalReference = {name: 'ɵs', moduleName: CORE};
|
||||
static elementStyling: o.ExternalReference = {name: 'ɵs', moduleName: CORE};
|
||||
|
||||
static elementStyleNamed: o.ExternalReference = {name: 'ɵsn', moduleName: CORE};
|
||||
static elementStyle: o.ExternalReference = {name: 'ɵsm', moduleName: CORE};
|
||||
|
||||
static elementStyleProp: o.ExternalReference = {name: 'ɵsp', moduleName: CORE};
|
||||
|
||||
static elementStylingApply: o.ExternalReference = {name: 'ɵsa', moduleName: CORE};
|
||||
|
||||
static containerCreate: o.ExternalReference = {name: 'ɵC', moduleName: CORE};
|
||||
|
||||
|
111
packages/compiler/src/render3/view/styling.ts
Normal file
111
packages/compiler/src/render3/view/styling.ts
Normal 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 parseStyle(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();
|
||||
}
|
@ -30,6 +30,7 @@ import {Identifiers as R3} from '../r3_identifiers';
|
||||
import {htmlAstToRender3Ast} from '../r3_template_transform';
|
||||
|
||||
import {R3QueryMetadata} from './api';
|
||||
import {parseStyle} from './styling';
|
||||
import {CONTEXT_NAME, I18N_ATTR, I18N_ATTR_PREFIX, ID_SEPARATOR, IMPLICIT_REFERENCE, MEANING_SEPARATOR, REFERENCE_PREFIX, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, getQueryPredicate, invalid, mapToExpression, noop, temporaryAllocator, trimTrailingNulls, unsupported} from './util';
|
||||
|
||||
function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined {
|
||||
@ -40,8 +41,6 @@ function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefin
|
||||
return R3.elementAttribute;
|
||||
case BindingType.Class:
|
||||
return R3.elementClassNamed;
|
||||
case BindingType.Style:
|
||||
return R3.elementStyleNamed;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@ -51,8 +50,7 @@ function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefin
|
||||
// code (where this map is used) deals with DOM element property values
|
||||
// (like elm.propName) and not component bindining properties (like [propName]).
|
||||
const SPECIAL_CASED_PROPERTIES_INSTRUCTION_MAP: {[index: string]: o.ExternalReference} = {
|
||||
'className': R3.elementClass,
|
||||
'style': R3.elementStyle
|
||||
'className': R3.elementClass
|
||||
};
|
||||
|
||||
export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver {
|
||||
@ -316,19 +314,70 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// Add the attributes
|
||||
const i18nMessages: o.Statement[] = [];
|
||||
const attributes: o.Expression[] = [];
|
||||
const initialStyleDeclarations: o.Expression[] = [];
|
||||
|
||||
Object.getOwnPropertyNames(outputAttrs).forEach(name => {
|
||||
const value = outputAttrs[name];
|
||||
attributes.push(o.literal(name));
|
||||
if (attrI18nMetas.hasOwnProperty(name)) {
|
||||
const meta = parseI18nMeta(attrI18nMetas[name]);
|
||||
const variable = this.constantPool.getTranslation(value, meta);
|
||||
attributes.push(variable);
|
||||
const styleInputs: t.BoundAttribute[] = [];
|
||||
const allOtherInputs: t.BoundAttribute[] = [];
|
||||
|
||||
element.inputs.forEach((input: t.BoundAttribute) => {
|
||||
// [attr.style] should not be treated as a styling-based
|
||||
// binding since it is intended to write directly to the attr
|
||||
// and therefore will skip all style resolution that is present
|
||||
// with style="", [style]="" and [style.prop]="" assignments
|
||||
if (input.name == 'style' && input.type == BindingType.Property) {
|
||||
// this should always go first in the compilation (for [style])
|
||||
styleInputs.splice(0, 0, input);
|
||||
} else if (input.type == BindingType.Style) {
|
||||
styleInputs.push(input);
|
||||
} else {
|
||||
attributes.push(o.literal(value));
|
||||
allOtherInputs.push(input);
|
||||
}
|
||||
});
|
||||
|
||||
let currStyleIndex = 0;
|
||||
let staticStylesMap: {[key: string]: any}|null = null;
|
||||
const stylesIndexMap: {[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 {
|
||||
attributes.push(o.literal(name));
|
||||
if (attrI18nMetas.hasOwnProperty(name)) {
|
||||
const meta = parseI18nMeta(attrI18nMetas[name]);
|
||||
const variable = this.constantPool.getTranslation(value, meta);
|
||||
attributes.push(variable);
|
||||
} else {
|
||||
attributes.push(o.literal(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0; i < styleInputs.length; i++) {
|
||||
const input = styleInputs[i];
|
||||
const isMapBasedStyleBinding = i === 0 && input.name === 'style';
|
||||
if (!isMapBasedStyleBinding && !stylesIndexMap.hasOwnProperty(input.name)) {
|
||||
stylesIndexMap[input.name] = currStyleIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 => {
|
||||
initialStyleDeclarations.push(o.literal(prop));
|
||||
});
|
||||
|
||||
if (staticStylesMap) {
|
||||
initialStyleDeclarations.push(o.literal(core.InitialStylingFlags.INITIAL_STYLES));
|
||||
|
||||
Object.keys(staticStylesMap).forEach(prop => {
|
||||
initialStyleDeclarations.push(o.literal(prop));
|
||||
const value = staticStylesMap ![prop];
|
||||
initialStyleDeclarations.push(o.literal(value));
|
||||
});
|
||||
}
|
||||
|
||||
const attrArg: o.Expression = attributes.length > 0 ?
|
||||
this.constantPool.getConstLiteral(o.literalArr(attributes), true) :
|
||||
o.TYPED_NULL_EXPR;
|
||||
@ -365,11 +414,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
this.addNamespaceInstruction(currentNamespace, element);
|
||||
}
|
||||
|
||||
const isEmptyElement = element.children.length === 0 && element.outputs.length === 0;
|
||||
|
||||
const implicit = o.variable(CONTEXT_NAME);
|
||||
|
||||
if (isEmptyElement) {
|
||||
const elementStyleIndex =
|
||||
(initialStyleDeclarations.length || styleInputs.length) ? this.allocateDataSlot() : 0;
|
||||
const createSelfClosingInstruction =
|
||||
elementStyleIndex === 0 && element.children.length === 0 && element.outputs.length === 0;
|
||||
|
||||
if (createSelfClosingInstruction) {
|
||||
this.instruction(
|
||||
this._creationCode, element.sourceSpan, R3.element, ...trimTrailingNulls(parameters));
|
||||
} else {
|
||||
@ -381,6 +433,20 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
this._creationCode, element.sourceSpan, R3.elementStart,
|
||||
...trimTrailingNulls(parameters));
|
||||
|
||||
// initial styling for static style="..." attributes
|
||||
if (elementStyleIndex > 0) {
|
||||
let paramsList: (o.Expression)[] = [o.literal(elementStyleIndex)];
|
||||
if (initialStyleDeclarations.length) {
|
||||
// the template compiler handles initial styling (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));
|
||||
}
|
||||
this._creationCode.push(o.importExpr(R3.elementStyling).callFn(paramsList).toStmt());
|
||||
}
|
||||
|
||||
// Generate Listeners (outputs)
|
||||
element.outputs.forEach((outputAst: t.BoundEvent) => {
|
||||
const elName = sanitizeIdentifier(element.name);
|
||||
@ -404,11 +470,33 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
});
|
||||
}
|
||||
|
||||
if (styleInputs.length && elementStyleIndex > 0) {
|
||||
const indexLiteral = o.literal(elementStyleIndex);
|
||||
styleInputs.forEach((input, i) => {
|
||||
const isMapBasedStyleBinding = i == 0 && input.name == 'style';
|
||||
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true);
|
||||
if (isMapBasedStyleBinding) {
|
||||
this.instruction(
|
||||
this._bindingCode, input.sourceSpan, R3.elementStyle, indexLiteral, convertedBinding);
|
||||
} else {
|
||||
const key = input.name;
|
||||
let styleIndex: number = stylesIndexMap[key] !;
|
||||
this.instruction(
|
||||
this._bindingCode, input.sourceSpan, R3.elementStyleProp, indexLiteral,
|
||||
o.literal(styleIndex), convertedBinding);
|
||||
}
|
||||
});
|
||||
|
||||
const spanEnd = styleInputs[styleInputs.length - 1].sourceSpan;
|
||||
this.instruction(this._bindingCode, spanEnd, R3.elementStylingApply, indexLiteral);
|
||||
}
|
||||
|
||||
// Generate element input bindings
|
||||
element.inputs.forEach((input: t.BoundAttribute) => {
|
||||
allOtherInputs.forEach((input: t.BoundAttribute) => {
|
||||
if (input.type === BindingType.Animation) {
|
||||
this._unsupported('animations');
|
||||
}
|
||||
|
||||
const convertedBinding = this.convertPropertyBinding(implicit, input.value);
|
||||
const specialInstruction = SPECIAL_CASED_PROPERTIES_INSTRUCTION_MAP[input.name];
|
||||
if (specialInstruction) {
|
||||
@ -442,7 +530,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
t.visitAll(this, element.children);
|
||||
}
|
||||
|
||||
if (!isEmptyElement) {
|
||||
if (!createSelfClosingInstruction) {
|
||||
// Finish element construction mode.
|
||||
this.instruction(
|
||||
this._creationCode, element.endSourceSpan || element.sourceSpan, R3.elementEnd);
|
||||
@ -568,7 +656,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
statements.push(o.importExpr(reference, null, span).callFn(params, span).toStmt());
|
||||
}
|
||||
|
||||
private convertPropertyBinding(implicit: o.Expression, value: AST): o.Expression {
|
||||
private convertPropertyBinding(implicit: o.Expression, value: AST, skipBindFn?: boolean):
|
||||
o.Expression {
|
||||
const pipesConvertedValue = value.visit(this._valueConverter);
|
||||
if (pipesConvertedValue instanceof Interpolation) {
|
||||
const convertedPropertyBinding = convertPropertyBinding(
|
||||
@ -581,7 +670,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
this, implicit, pipesConvertedValue, this.bindingContext(), BindingForm.TrySimple,
|
||||
() => error('Unexpected interpolation'));
|
||||
this._bindingCode.push(...convertedPropertyBinding.stmts);
|
||||
return o.importExpr(R3.bind).callFn([convertedPropertyBinding.currValExpr]);
|
||||
const valExpr = convertedPropertyBinding.currValExpr;
|
||||
return skipBindFn ? valExpr : o.importExpr(R3.bind).callFn([valExpr]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -321,15 +321,23 @@ describe('compiler compliance', () => {
|
||||
|
||||
const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }';
|
||||
const template = `
|
||||
template: function MyComponent_Template(rf: IDENT, ctx: IDENT) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵEe(0, 'div');
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵkn(0, 'error', $r3$.ɵb(ctx.error));
|
||||
$r3$.ɵsn(0, 'background-color', $r3$.ɵb(ctx.color));
|
||||
}
|
||||
}
|
||||
const _c0 = ['background-color'];
|
||||
class MyComponent {
|
||||
static ngComponentDef = i0.ɵdefineComponent({type:MyComponent,selectors:[['my-component']],
|
||||
factory:function MyComponent_Factory(){
|
||||
return new MyComponent();
|
||||
},template:function MyComponent_Template(rf:number,ctx:any){
|
||||
if (rf & 1) {
|
||||
$r3$.ɵE(0, 'div');
|
||||
$r3$.ɵs(1, _c0);
|
||||
$r3$.ɵe();
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵsp(1, 0, ctx.color);
|
||||
$r3$.ɵsa(1);
|
||||
$r3$.ɵkn(0, 'error', $r3$.ɵb(ctx.error));
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
@ -6,7 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {InitialStylingFlags} from '../../src/core';
|
||||
import {MockDirectory, setup} from '../aot/test_util';
|
||||
|
||||
import {compile, expectEmit} from './mock_compile';
|
||||
|
||||
describe('compiler compliance: styling', () => {
|
||||
@ -16,7 +18,7 @@ describe('compiler compliance: styling', () => {
|
||||
compileCommon: true,
|
||||
});
|
||||
|
||||
describe('[style]', () => {
|
||||
describe('[style] and [style.prop]', () => {
|
||||
it('should create style instructions on the element', () => {
|
||||
const files = {
|
||||
app: {
|
||||
@ -40,10 +42,13 @@ describe('compiler compliance: styling', () => {
|
||||
const template = `
|
||||
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵEe(0, 'div');
|
||||
$r3$.ɵE(0, 'div');
|
||||
$r3$.ɵs(1);
|
||||
$r3$.ɵe();
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵs(0,$r3$.ɵb($ctx$.myStyleExp));
|
||||
$r3$.ɵsm(1, $ctx$.myStyleExp);
|
||||
$r3$.ɵsa(1);
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -51,6 +56,63 @@ describe('compiler compliance: styling', () => {
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, template, 'Incorrect template');
|
||||
});
|
||||
|
||||
it('should place initial, multi, singular and application followed by attribute styling instructions in the template code in that order',
|
||||
() => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`<div style="opacity:1"
|
||||
[attr.style]="'border-width: 10px'"
|
||||
[style.width]="myWidth"
|
||||
[style]="myStyleExp"
|
||||
[style.height]="myHeight"></div>\`
|
||||
})
|
||||
export class MyComponent {
|
||||
myStyleExp = [{color:'red'}, {color:'blue', duration:1000}]
|
||||
myWidth = '100px';
|
||||
myHeight = '100px';
|
||||
}
|
||||
|
||||
@NgModule({declarations: [MyComponent]})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const template = `
|
||||
const _c0 = ['opacity','width','height',${InitialStylingFlags.INITIAL_STYLES},'opacity','1'];
|
||||
class MyComponent {
|
||||
static ngComponentDef = i0.ɵdefineComponent({
|
||||
type: MyComponent,
|
||||
selectors:[['my-component']],
|
||||
factory:function MyComponent_Factory(){
|
||||
return new MyComponent();
|
||||
},
|
||||
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵE(0, 'div');
|
||||
$r3$.ɵs(1, _c0);
|
||||
$r3$.ɵe();
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵsm(1, $ctx$.myStyleExp);
|
||||
$r3$.ɵsp(1, 1, $ctx$.myWidth);
|
||||
$r3$.ɵsp(1, 2, $ctx$.myHeight);
|
||||
$r3$.ɵsa(1);
|
||||
$r3$.ɵa(0, 'style', $r3$.ɵb('border-width: 10px'));
|
||||
}
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, template, 'Incorrect template');
|
||||
});
|
||||
});
|
||||
|
||||
describe('[class]', () => {
|
||||
|
93
packages/compiler/test/render3/styling_spec.ts
Normal file
93
packages/compiler/test/render3/styling_spec.ts
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @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 {hyphenate, parseStyle, stripUnnecessaryQuotes} from '../../src/render3/view/styling';
|
||||
|
||||
describe('inline css style parsing', () => {
|
||||
it('should parse empty or blank strings', () => {
|
||||
const result1 = parseStyle('');
|
||||
expect(result1).toEqual({});
|
||||
|
||||
const result2 = parseStyle(' ');
|
||||
expect(result2).toEqual({});
|
||||
});
|
||||
|
||||
it('should parse a string into a key/value map', () => {
|
||||
const result = parseStyle('width:100px;height:200px;opacity:0');
|
||||
expect(result).toEqual({width: '100px', height: '200px', opacity: '0'});
|
||||
});
|
||||
|
||||
it('should trim values and properties', () => {
|
||||
const result = parseStyle('width :333px ; height:666px ; opacity: 0.5;');
|
||||
expect(result).toEqual({width: '333px', height: '666px', opacity: '0.5'});
|
||||
});
|
||||
|
||||
it('should chomp out start/end quotes', () => {
|
||||
const result = parseStyle(
|
||||
'content: "foo"; opacity: \'0.5\'; font-family: "Verdana", Helvetica, "sans-serif"');
|
||||
expect(result).toEqual(
|
||||
{content: 'foo', opacity: '0.5', 'font-family': '"Verdana", Helvetica, "sans-serif"'});
|
||||
});
|
||||
|
||||
it('should not mess up with quoted strings that contain [:;] values', () => {
|
||||
const result = parseStyle('content: "foo; man: guy"; width: 100px');
|
||||
expect(result).toEqual({content: 'foo; man: guy', width: '100px'});
|
||||
});
|
||||
|
||||
it('should not mess up with quoted strings that contain inner quote values', () => {
|
||||
const quoteStr = '"one \'two\' three \"four\" five"';
|
||||
const result = parseStyle(`content: ${quoteStr}; width: 123px`);
|
||||
expect(result).toEqual({content: quoteStr, width: '123px'});
|
||||
});
|
||||
|
||||
it('should respect parenthesis that are placed within a style', () => {
|
||||
const result = parseStyle('background-image: url("foo.jpg")');
|
||||
expect(result).toEqual({'background-image': 'url("foo.jpg")'});
|
||||
});
|
||||
|
||||
it('should respect multi-level parenthesis that contain special [:;] characters', () => {
|
||||
const result = parseStyle('color: rgba(calc(50 * 4), var(--cool), :5;); height: 100px;');
|
||||
expect(result).toEqual({color: 'rgba(calc(50 * 4), var(--cool), :5;)', height: '100px'});
|
||||
});
|
||||
|
||||
it('should hyphenate style properties from camel case', () => {
|
||||
const result = parseStyle('borderWidth: 200px');
|
||||
expect(result).toEqual({
|
||||
'border-width': '200px',
|
||||
});
|
||||
});
|
||||
|
||||
describe('quote chomping', () => {
|
||||
it('should remove the start and end quotes', () => {
|
||||
expect(stripUnnecessaryQuotes('\'foo bar\'')).toEqual('foo bar');
|
||||
expect(stripUnnecessaryQuotes('"foo bar"')).toEqual('foo bar');
|
||||
});
|
||||
|
||||
it('should not remove quotes if the quotes are not at the start and end', () => {
|
||||
expect(stripUnnecessaryQuotes('foo bar')).toEqual('foo bar');
|
||||
expect(stripUnnecessaryQuotes(' foo bar ')).toEqual(' foo bar ');
|
||||
expect(stripUnnecessaryQuotes('\'foo\' bar')).toEqual('\'foo\' bar');
|
||||
expect(stripUnnecessaryQuotes('foo "bar"')).toEqual('foo "bar"');
|
||||
});
|
||||
|
||||
it('should not remove quotes if there are inner quotes', () => {
|
||||
const str = '"Verdana", "Helvetica"';
|
||||
expect(stripUnnecessaryQuotes(str)).toEqual(str);
|
||||
});
|
||||
});
|
||||
|
||||
describe('camelCasing => hyphenation', () => {
|
||||
it('should convert a camel-cased value to a hyphenated value', () => {
|
||||
expect(hyphenate('fooBar')).toEqual('foo-bar');
|
||||
expect(hyphenate('fooBarMan')).toEqual('foo-bar-man');
|
||||
expect(hyphenate('-fooBar-man')).toEqual('-foo-bar-man');
|
||||
});
|
||||
|
||||
it('should make everything lowercase',
|
||||
() => { expect(hyphenate('-WebkitAnimation')).toEqual('-webkit-animation'); });
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user