refactor(ivy): Switch styling to new reconcile algorithm (#34616)

NOTE: This change must be reverted with previous deletes so that it code remains in build-able state.

This change deletes old styling code and replaces it with a simplified styling algorithm.

The mental model for the new algorithm is:
- Create a linked list of styling bindings in the order of priority. All styling bindings ere executed in compiled order and than a linked list of bindings is created in priority order.
- Flush the style bindings at the end of `advance()` instruction. This implies that there are two flush events. One at the end of template `advance` instruction in the template. Second one at the end of `hostBindings` `advance` instruction when processing host bindings (if any).
- Each binding instructions effectively updates the string to represent the string at that location. Because most of the bindings are additive, this is a cheap strategy in most cases. In rare cases the strategy requires removing tokens from the styling up to this point. (We expect that to be rare case)S Because, the bindings are presorted in the order of priority, it is safe to resume the processing of the concatenated string from the last change binding.

PR Close #34616
This commit is contained in:
Miško Hevery
2019-12-17 15:40:37 -08:00
parent 1ccf3e54f7
commit 49e8028f26
60 changed files with 2439 additions and 1413 deletions

View File

@ -8,10 +8,10 @@
import {Injector, NgModuleRef, ViewEncapsulation} from '../../src/core';
import {ComponentFactory} from '../../src/linker/component_factory';
import {RendererFactory2} from '../../src/render/api';
import {RendererFactory2, RendererType2} from '../../src/render/api';
import {injectComponentFactoryResolver} from '../../src/render3/component_ref';
import {ɵɵdefineComponent} from '../../src/render3/index';
import {domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {AttributeMarker, ɵɵdefineComponent} from '../../src/render3/index';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {Sanitizer} from '../../src/sanitization/sanitizer';
describe('ComponentFactory', () => {
@ -97,6 +97,7 @@ describe('ComponentFactory', () => {
decls: 0,
vars: 0,
template: () => undefined,
hostAttrs: [AttributeMarker.Classes, 'HOST_COMPONENT']
});
}
@ -291,5 +292,24 @@ describe('ComponentFactory', () => {
expect(mSanitizerFactorySpy).toHaveBeenCalled();
});
});
it('should ensure that rendererFactory is called after initial styling is set', () => {
const myRendererFactory: RendererFactory3 = {
createRenderer: function(hostElement: RElement|null, rendererType: RendererType2|null):
Renderer3 {
if (hostElement) {
hostElement.classList.add('HOST_RENDERER');
}
return document;
}
};
const injector = Injector.create([
{provide: RendererFactory2, useValue: myRendererFactory},
]);
const hostNode = document.createElement('div');
const componentRef = cf.create(injector, undefined, hostNode);
expect(hostNode.className).toEqual('HOST_COMPONENT HOST_RENDERER');
});
});
});

View File

@ -0,0 +1,147 @@
/**
* @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 {TNodeDebug} from '@angular/core/src/render3/instructions/lview_debug';
import {createTNode, createTView} from '@angular/core/src/render3/instructions/shared';
import {TNodeType} from '@angular/core/src/render3/interfaces/node';
import {LView, TView, TViewType} from '@angular/core/src/render3/interfaces/view';
import {enterView, leaveView} from '@angular/core/src/render3/state';
import {CLASS_MAP_STYLING_KEY, STYLE_MAP_STYLING_KEY, insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list';
describe('lView_debug', () => {
const mockFirstUpdatePassLView: LView = [null, {firstUpdatePass: true}] as any;
beforeEach(() => enterView(mockFirstUpdatePassLView, null));
afterEach(() => leaveView());
describe('TNode', () => {
let tNode !: TNodeDebug;
let tView !: TView;
beforeEach(() => {
tView = createTView(TViewType.Component, 0, null, 0, 0, null, null, null, null, null);
tNode = createTNode(tView, null !, TNodeType.Element, 0, '', null) as TNodeDebug;
});
afterEach(() => tNode = tView = null !);
describe('styling', () => {
it('should decode no styling', () => {
expect(tNode.styleBindings_).toEqual([null]);
expect(tNode.classBindings_).toEqual([null]);
});
it('should decode static styling', () => {
tNode.styles = 'color: blue';
tNode.classes = 'STATIC';
expect(tNode.styleBindings_).toEqual(['color: blue']);
expect(tNode.classBindings_).toEqual(['STATIC']);
});
it('should decode no-template property binding', () => {
tNode.classes = 'STATIC';
insertTStylingBinding(tView.data, tNode, 'CLASS', 2, true, true);
insertTStylingBinding(tView.data, tNode, 'color', 4, true, false);
expect(tNode.styleBindings_).toEqual([
null, {
index: 4,
key: 'color',
isTemplate: false,
prevDuplicate: false,
nextDuplicate: false,
prevIndex: 0,
nextIndex: 0,
}
]);
expect(tNode.classBindings_).toEqual([
'STATIC', {
index: 2,
key: 'CLASS',
isTemplate: false,
prevDuplicate: false,
nextDuplicate: false,
prevIndex: 0,
nextIndex: 0,
}
]);
});
it('should decode template and directive property binding', () => {
tNode.classes = 'STATIC';
insertTStylingBinding(tView.data, tNode, 'CLASS', 2, false, true);
insertTStylingBinding(tView.data, tNode, 'color', 4, false, false);
expect(tNode.styleBindings_).toEqual([
null, {
index: 4,
key: 'color',
isTemplate: true,
prevDuplicate: false,
nextDuplicate: false,
prevIndex: 0,
nextIndex: 0,
}
]);
expect(tNode.classBindings_).toEqual([
'STATIC', {
index: 2,
key: 'CLASS',
isTemplate: true,
prevDuplicate: false,
nextDuplicate: false,
prevIndex: 0,
nextIndex: 0,
}
]);
insertTStylingBinding(tView.data, tNode, STYLE_MAP_STYLING_KEY, 6, true, true);
insertTStylingBinding(tView.data, tNode, CLASS_MAP_STYLING_KEY, 8, true, false);
expect(tNode.styleBindings_).toEqual([
null, {
index: 8,
key: CLASS_MAP_STYLING_KEY,
isTemplate: false,
prevDuplicate: false,
nextDuplicate: true,
prevIndex: 0,
nextIndex: 4,
},
{
index: 4,
key: 'color',
isTemplate: true,
prevDuplicate: true,
nextDuplicate: false,
prevIndex: 8,
nextIndex: 0,
}
]);
expect(tNode.classBindings_).toEqual([
'STATIC', {
index: 6,
key: STYLE_MAP_STYLING_KEY,
isTemplate: false,
prevDuplicate: true,
nextDuplicate: true,
prevIndex: 0,
nextIndex: 2,
},
{
index: 2,
key: 'CLASS',
isTemplate: true,
prevDuplicate: true,
nextDuplicate: false,
prevIndex: 6,
nextIndex: 0,
}
]);
});
});
});
});

View File

@ -7,11 +7,12 @@
*/
import {NgForOfContext} from '@angular/common';
import {getSortedClassName} from '@angular/core/testing/src/styling';
import {ɵɵdefineComponent} from '../../src/render3/definition';
import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
import {AttributeMarker} from '../../src/render3/interfaces/node';
import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, unwrapSafeValue} from '../../src/sanitization/bypass';
import {SafeValue, bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, unwrapSafeValue} from '../../src/sanitization/bypass';
import {ɵɵdefaultStyleSanitizer, ɵɵsanitizeHtml, ɵɵsanitizeResourceUrl, ɵɵsanitizeScript, ɵɵsanitizeStyle, ɵɵsanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer} from '../../src/sanitization/sanitizer';
import {SecurityContext} from '../../src/sanitization/security';
@ -137,18 +138,20 @@ describe('instructions', () => {
describe('styleProp', () => {
it('should automatically sanitize unless a bypass operation is applied', () => {
const t = new TemplateFixture(() => { return createDiv(); }, () => {}, 1);
t.update(() => {
ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer);
ɵɵstyleProp('background-image', 'url("http://server")');
});
let backgroundImage: string|SafeValue = 'url("http://server")';
const t = new TemplateFixture(
() => { return createDiv(); },
() => {
ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer);
ɵɵstyleProp('background-image', backgroundImage);
},
2, 2);
// nothing is set because sanitizer suppresses it.
expect(t.html).toEqual('<div></div>');
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('');
t.update(() => {
ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer);
ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('url("http://server2")'));
});
backgroundImage = bypassSanitizationTrustStyle('url("http://server2")');
t.update();
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('url("http://server2")');
});
@ -160,9 +163,10 @@ describe('instructions', () => {
function createDivWithStyle() { ɵɵelement(0, 'div', 0); }
it('should add style', () => {
const fixture = new TemplateFixture(
createDivWithStyle, () => {}, 1, 0, null, null, null, undefined, attrs);
fixture.update(() => { ɵɵstyleMap({'background-color': 'red'}); });
const fixture = new TemplateFixture(createDivWithStyle, () => {
ɵɵstyleMap({'background-color': 'red'});
}, 1, 2, null, null, null, undefined, attrs);
fixture.update();
expect(fixture.html).toEqual('<div style="background-color: red; height: 10px;"></div>');
});
@ -184,7 +188,7 @@ describe('instructions', () => {
'width': 'width'
});
},
1, 0, null, null, sanitizerInterceptor);
1, 2, null, null, sanitizerInterceptor);
const props = detectedValues.sort();
expect(props).toEqual([
@ -197,9 +201,10 @@ describe('instructions', () => {
function createDivWithStyling() { ɵɵelement(0, 'div'); }
it('should add class', () => {
const fixture =
new TemplateFixture(createDivWithStyling, () => { ɵɵclassMap('multiple classes'); }, 1);
expect(fixture.html).toEqual('<div class="classes multiple"></div>');
const fixture = new TemplateFixture(
createDivWithStyling, () => { ɵɵclassMap('multiple classes'); }, 1, 2);
const div = fixture.containerElement.querySelector('div.multiple') !;
expect(getSortedClassName(div)).toEqual('classes multiple');
});
});

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {classIndexOf, computeClassChanges, removeClass, splitClassList, toggleClass} from '../../../src/render3/styling/class_differ';
import {classIndexOf, computeClassChanges, splitClassList, toggleClass} from '../../../src/render3/styling/class_differ';
describe('class differ', () => {
describe('computeClassChanges', () => {
@ -81,25 +81,25 @@ describe('class differ', () => {
});
});
describe('removeClass', () => {
describe('toggleClass', () => {
it('should remove class name from a class-list string', () => {
expect(removeClass('', '')).toEqual('');
expect(removeClass('A', 'A')).toEqual('');
expect(removeClass('AB', 'AB')).toEqual('');
expect(removeClass('A B', 'A')).toEqual('B');
expect(removeClass('A B', 'A')).toEqual('B');
expect(toggleClass('', '', false)).toEqual('');
expect(toggleClass('A', 'A', false)).toEqual('');
expect(toggleClass('AB', 'AB', false)).toEqual('');
expect(toggleClass('A B', 'A', false)).toEqual('B');
expect(toggleClass('A B', 'A', false)).toEqual('B');
expect(toggleClass('A B', 'B', false)).toEqual('A');
expect(toggleClass(' B ', 'B', false)).toEqual('');
});
it('should not remove a sub-string', () => {
expect(removeClass('ABC', 'A')).toEqual('ABC');
expect(removeClass('ABC', 'B')).toEqual('ABC');
expect(removeClass('ABC', 'C')).toEqual('ABC');
expect(removeClass('ABC', 'AB')).toEqual('ABC');
expect(removeClass('ABC', 'BC')).toEqual('ABC');
expect(toggleClass('ABC', 'A', false)).toEqual('ABC');
expect(toggleClass('ABC', 'B', false)).toEqual('ABC');
expect(toggleClass('ABC', 'C', false)).toEqual('ABC');
expect(toggleClass('ABC', 'AB', false)).toEqual('ABC');
expect(toggleClass('ABC', 'BC', false)).toEqual('ABC');
});
});
describe('removeClass', () => {
it('should toggle a class', () => {
expect(toggleClass('', 'B', false)).toEqual('');
expect(toggleClass('', 'B', true)).toEqual('B');

View File

@ -8,6 +8,7 @@
import {Renderer3, domRendererFactory3} from '@angular/core/src/render3/interfaces/renderer';
import {writeAndReconcileClass, writeAndReconcileStyle} from '@angular/core/src/render3/styling/reconcile';
import {getSortedClassName, getSortedStyle} from '@angular/core/testing/src/styling';
describe('styling reconcile', () => {
[document, domRendererFactory3.createRenderer(null, null)].forEach((renderer: Renderer3) => {
@ -84,41 +85,3 @@ describe('styling reconcile', () => {
});
});
});
function getSortedClassName(element: HTMLElement): string {
const names: string[] = [];
const classList = element.classList || [];
for (let i = 0; i < classList.length; i++) {
const name = classList[i];
if (names.indexOf(name) === -1) {
names.push(name);
}
}
names.sort();
return names.join(' ');
}
function getSortedStyle(element: HTMLElement): string {
const names: string[] = [];
const style = element.style;
// reading `style.color` is a work around for a bug in Domino. The issue is that Domino has stale
// value for `style.length`. It seems that reading a property from the element causes the stale
// value to be updated. (As of Domino v 2.1.3)
style.color;
for (let i = 0; i < style.length; i++) {
const name = style.item(i);
if (names.indexOf(name) === -1) {
names.push(name);
}
}
names.sort();
let sorted = '';
names.forEach(key => {
const value = style.getPropertyValue(key);
if (value != null && value !== '') {
if (sorted !== '') sorted += ' ';
sorted += key + ': ' + value + ';';
}
});
return sorted;
}

View File

@ -12,7 +12,6 @@ import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangeNextDu
import {LView, TData} from '@angular/core/src/render3/interfaces/view';
import {enterView, leaveView} from '@angular/core/src/render3/state';
import {CLASS_MAP_STYLING_KEY, STYLE_MAP_STYLING_KEY, appendStyling, flushStyleBinding, insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list';
import {getStylingBindingHead} from '@angular/core/src/render3/styling/styling_debug';
import {newArray} from '@angular/core/src/util/array_utils';
describe('TNode styling linked list', () => {
@ -438,34 +437,47 @@ describe('TNode styling linked list', () => {
it('should write basic value', () => {
const fixture = new StylingFixture([['color']], false);
fixture.setBinding(0, 'red');
expect(fixture.flush(0)).toEqual('color: red');
expect(fixture.flush(0)).toEqual('color: red;');
});
it('should chain values and allow update mid list', () => {
const fixture = new StylingFixture([['color', {key: 'width', extra: 'px'}]], false);
fixture.setBinding(0, 'red');
fixture.setBinding(1, '100');
expect(fixture.flush(0)).toEqual('color: red; width: 100px');
expect(fixture.flush(0)).toEqual('color: red; width: 100px;');
fixture.setBinding(0, 'blue');
fixture.setBinding(1, '200');
expect(fixture.flush(1)).toEqual('color: red; width: 200px');
expect(fixture.flush(0)).toEqual('color: blue; width: 200px');
expect(fixture.flush(1)).toEqual('color: red; width: 200px;');
expect(fixture.flush(0)).toEqual('color: blue; width: 200px;');
});
it('should remove duplicates', () => {
const fixture = new StylingFixture([['color', 'color']], false);
fixture.setBinding(0, 'red');
fixture.setBinding(1, 'blue');
expect(fixture.flush(0)).toEqual('color: blue');
expect(fixture.flush(0)).toEqual('color: blue;');
});
it('should treat undefined values as previous value', () => {
const fixture = new StylingFixture([['color', 'color']], false);
fixture.setBinding(0, 'red');
fixture.setBinding(1, undefined);
expect(fixture.flush(0)).toEqual('color: red;');
});
it('should treat null value as removal', () => {
const fixture = new StylingFixture([['color']], false);
fixture.setBinding(0, null);
expect(fixture.flush(0)).toEqual('');
});
});
describe('appendStyling', () => {
it('should append simple style', () => {
expect(appendStyling('', 'color', 'red', null, false, false)).toEqual('color: red');
expect(appendStyling('', 'color', 'red', null, true, false)).toEqual('color: red');
expect(appendStyling('', 'color', 'red', null, false, false)).toEqual('color: red;');
expect(appendStyling('', 'color', 'red', null, true, false)).toEqual('color: red;');
expect(appendStyling('', 'color', 'red', null, false, true)).toEqual('color');
expect(appendStyling('', 'color', 'red', null, true, true)).toEqual('color');
expect(appendStyling('', 'color', true, null, true, true)).toEqual('color');
@ -476,25 +488,25 @@ describe('TNode styling linked list', () => {
it('should append simple style with suffix', () => {
expect(appendStyling('', {key: 'width', extra: 'px'}, 100, null, false, false))
.toEqual('width: 100px');
.toEqual('width: 100px;');
});
it('should append simple style with sanitizer', () => {
expect(
appendStyling('', {key: 'width', extra: (v: any) => `-${v}-`}, 100, null, false, false))
.toEqual('width: -100-');
.toEqual('width: -100-;');
});
it('should append class/style', () => {
expect(appendStyling('color: white', 'color', 'red', null, false, false))
.toEqual('color: white; color: red');
expect(appendStyling('color: white;', 'color', 'red', null, false, false))
.toEqual('color: white; color: red;');
expect(appendStyling('MY-CLASS', 'color', true, null, false, true)).toEqual('MY-CLASS color');
expect(appendStyling('MY-CLASS', 'color', false, null, true, true)).toEqual('MY-CLASS');
});
it('should remove existing', () => {
expect(appendStyling('color: white', 'color', 'blue', null, true, false))
.toEqual('color: blue');
expect(appendStyling('color: white;', 'color', 'blue', null, true, false))
.toEqual('color: blue;');
expect(appendStyling('A YES B', 'YES', false, null, true, true)).toEqual('A B');
});
@ -510,10 +522,10 @@ describe('TNode styling linked list', () => {
it('should support maps for styles', () => {
expect(appendStyling('', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false))
.toEqual('A: a; B: b');
.toEqual('A: a; B: b;');
expect(appendStyling(
'A:_; B:_; C:_', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false))
.toEqual('C:_; A: a; B: b');
'A:_; B:_; C:_;', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false))
.toEqual('C:_; A: a; B: b;');
});
it('should support strings for classes', () => {
@ -525,11 +537,11 @@ describe('TNode styling linked list', () => {
});
it('should support strings for styles', () => {
expect(appendStyling('A:a;B:b', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, false, false))
.toEqual('A:a;B:b; A : a ; B : b');
expect(
appendStyling('A:_; B:_; C:_', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, true, false))
.toEqual('C:_; A: a; B: b');
expect(appendStyling('A:a;B:b;', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, false, false))
.toEqual('A:a;B:b; A : a ; B : b;');
expect(appendStyling(
'A:_; B:_; C:_;', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, true, false))
.toEqual('C:_; A: a; B: b;');
});
it('should throw no arrays for styles', () => {
@ -560,7 +572,7 @@ describe('TNode styling linked list', () => {
'list-style: unsafe; ' +
'list-style-image: unsafe; ' +
'clip-path: unsafe; ' +
'width: url(javascript:evil())');
'width: url(javascript:evil());');
// verify string
expect(appendStyling(
'', STYLE_MAP_STYLING_KEY,
@ -571,7 +583,7 @@ describe('TNode styling linked list', () => {
'list-style: url(javascript:evil());' +
'list-style-image: url(javascript:evil());' +
'clip-path: url(javascript:evil());' +
'width: url(javascript:evil())' // should not sanitize
'width: url(javascript:evil());' // should not sanitize
,
null, true, false))
.toEqual(
@ -582,7 +594,7 @@ describe('TNode styling linked list', () => {
'list-style: unsafe; ' +
'list-style-image: unsafe; ' +
'clip-path: unsafe; ' +
'width: url(javascript:evil())');
'width: url(javascript:evil());');
});
});
});
@ -632,6 +644,24 @@ function expectPriorityOrder(tData: TData, tNode: TNode, isClassBinding: boolean
return expect(indexes);
}
/**
* Find the head of the styling binding linked list.
*/
export function getStylingBindingHead(tData: TData, tNode: TNode, isClassBinding: boolean): number {
let index = getTStylingRangePrev(isClassBinding ? tNode.classBindings : tNode.styleBindings);
while (true) {
const tStylingRange = tData[index + 1] as TStylingRange;
const prev = getTStylingRangePrev(tStylingRange);
if (prev === 0) {
// found head exit.
return index;
} else {
index = prev;
}
}
}
class StylingFixture {
tData: TData = [null, null];
lView: LView = [null, null !] as any;

View File

@ -7,8 +7,7 @@
*/
import {StyleChangesMap, parseKeyValue, removeStyle} from '@angular/core/src/render3/styling/style_differ';
import {consumeSeparatorWithWhitespace, consumeStyleValue} from '@angular/core/src/render3/styling/styling_parser';
import {CharCode} from '@angular/core/src/util/char_code';
import {getLastParsedValue, parseStyle} from '@angular/core/src/render3/styling/styling_parser';
import {sortedForEach} from './class_differ_spec';
describe('style differ', () => {
@ -31,6 +30,13 @@ describe('style differ', () => {
expectParseValue(': text1 text2 ;🛑').toBe('text1 text2');
});
it('should parse empty vale', () => {
expectParseValue(':').toBe('');
expectParseValue(': ').toBe('');
expectParseValue(': ;🛑').toBe('');
expectParseValue(':;🛑').toBe('');
});
it('should parse quoted values', () => {
expectParseValue(':""').toBe('""');
expectParseValue(':"\\\\"').toBe('"\\\\"');
@ -54,11 +60,16 @@ describe('style differ', () => {
});
describe('parseKeyValue', () => {
it('should parse empty value', () => {
it('should parse empty string', () => {
expectParseKeyValue('').toEqual([]);
expectParseKeyValue(' \n\t\r ').toEqual([]);
});
it('should parse empty value', () => {
expectParseKeyValue('key:').toEqual(['key', '', null]);
expectParseKeyValue('key: \n\t\r; ').toEqual(['key', '', null]);
});
it('should prase single style', () => {
expectParseKeyValue('width: 100px').toEqual(['width', '100px', null]);
expectParseKeyValue(' width : 100px ;').toEqual(['width', '100px', null]);
@ -79,27 +90,27 @@ describe('style differ', () => {
describe('removeStyle', () => {
it('should remove no style', () => {
expect(removeStyle('', 'foo')).toEqual('');
expect(removeStyle('abc: bar', 'a')).toEqual('abc: bar');
expect(removeStyle('abc: bar', 'b')).toEqual('abc: bar');
expect(removeStyle('abc: bar', 'c')).toEqual('abc: bar');
expect(removeStyle('abc: bar', 'bar')).toEqual('abc: bar');
expect(removeStyle('abc: bar;', 'a')).toEqual('abc: bar;');
expect(removeStyle('abc: bar;', 'b')).toEqual('abc: bar;');
expect(removeStyle('abc: bar;', 'c')).toEqual('abc: bar;');
expect(removeStyle('abc: bar;', 'bar')).toEqual('abc: bar;');
});
it('should remove all style', () => {
expect(removeStyle('foo: bar', 'foo')).toEqual('');
expect(removeStyle('foo: bar;', 'foo')).toEqual('');
expect(removeStyle('foo: bar; foo: bar;', 'foo')).toEqual('');
});
it('should remove some of the style', () => {
expect(removeStyle('a: a; foo: bar; b: b', 'foo')).toEqual('a: a; b: b');
expect(removeStyle('a: a; foo: bar; b: b', 'foo')).toEqual('a: a; b: b');
expect(removeStyle('a: a; foo: bar; b: b; foo: bar; c: c', 'foo'))
.toEqual('a: a; b: b; c: c');
expect(removeStyle('a: a; foo: bar; b: b;', 'foo')).toEqual('a: a; b: b;');
expect(removeStyle('a: a; foo: bar; b: b;', 'foo')).toEqual('a: a; b: b;');
expect(removeStyle('a: a; foo: bar; b: b; foo: bar; c: c;', 'foo'))
.toEqual('a: a; b: b; c: c;');
});
it('should remove trailing ;', () => {
expect(removeStyle('a: a; foo: bar', 'foo')).toEqual('a: a');
expect(removeStyle('a: a ; foo: bar ; ', 'foo')).toEqual('a: a');
expect(removeStyle('a: a; foo: bar;', 'foo')).toEqual('a: a;');
expect(removeStyle('a: a ; foo: bar ; ', 'foo')).toEqual('a: a ;');
});
});
});
@ -114,11 +125,9 @@ function expectParseValue(
text: string) {
let stopIndex = text.indexOf('🛑');
if (stopIndex < 0) stopIndex = text.length;
const valueStart = consumeSeparatorWithWhitespace(text, 0, text.length, CharCode.COLON);
const valueEnd = consumeStyleValue(text, valueStart, text.length);
const valueSep = consumeSeparatorWithWhitespace(text, valueEnd, text.length, CharCode.SEMI_COLON);
expect(valueSep).toBe(stopIndex);
return expect(text.substring(valueStart, valueEnd));
let i = parseStyle(text);
expect(i).toBe(stopIndex);
return expect(getLastParsedValue(text));
}
function expectParseKeyValue(text: string) {