Skip to content

Commit 1829542

Browse files
alan-agius4atscott
authored andcommitted
fix(compiler): do not unquote CSS values (#49460)
Currently we are unsafely unquoting CSS values which in some cases causes valid values to become invalid and invalid values to become valid. Example: ```html <div style="width:&quot;1px;&quot;"></div> ``` In the above case, `width` has an invalid value of `"1px"`, however the compiler will transform it to `1px` which makes it valid. On the other hand, in the below case ```html <div style="content:&quot;foo&quot;"></div> ``` `content` has a valid value of `"foo"`, but since the compiler unwraps it to `foo` it becomes invalid. For correctness, we should not remove quotes. ```js const div = document.createElement('div'); div.style.width='"1px"'; div.style.content='foo'; div.style.width; // '' div.style.content; // '' div.style.width='1px'; div.style.content='"foo"'; div.style.width; // '1px' div.style.content; // '"foo"' ``` More information about values can be found https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier PR Close #49460
1 parent fdafdb7 commit 1829542

File tree

3 files changed

+40
-46
lines changed

3 files changed

+40
-46
lines changed

packages/compiler/src/render3/view/style_parser.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
// Any changes here should be ported to the Angular Domino fork.
10+
// https://github.com/angular/domino/blob/main/lib/style_parser.js
11+
912
const enum Char {
1013
OpenParen = 40,
1114
CloseParen = 41,
@@ -39,7 +42,6 @@ export function parse(value: string): string[] {
3942
let valueStart = 0;
4043
let propStart = 0;
4144
let currentProp: string|null = null;
42-
let valueHasQuotes = false;
4345
while (i < value.length) {
4446
const token = value.charCodeAt(i++) as Char;
4547
switch (token) {
@@ -52,7 +54,6 @@ export function parse(value: string): string[] {
5254
case Char.QuoteSingle:
5355
// valueStart needs to be there since prop values don't
5456
// have quotes in CSS
55-
valueHasQuotes = valueHasQuotes || valueStart > 0;
5657
if (quote === Char.QuoteNone) {
5758
quote = Char.QuoteSingle;
5859
} else if (quote === Char.QuoteSingle && value.charCodeAt(i - 1) !== Char.BackSlash) {
@@ -61,7 +62,6 @@ export function parse(value: string): string[] {
6162
break;
6263
case Char.QuoteDouble:
6364
// same logic as above
64-
valueHasQuotes = valueHasQuotes || valueStart > 0;
6565
if (quote === Char.QuoteNone) {
6666
quote = Char.QuoteDouble;
6767
} else if (quote === Char.QuoteDouble && value.charCodeAt(i - 1) !== Char.BackSlash) {
@@ -77,38 +77,23 @@ export function parse(value: string): string[] {
7777
case Char.Semicolon:
7878
if (currentProp && valueStart > 0 && parenDepth === 0 && quote === Char.QuoteNone) {
7979
const styleVal = value.substring(valueStart, i - 1).trim();
80-
styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal);
80+
styles.push(currentProp, styleVal);
8181
propStart = i;
8282
valueStart = 0;
8383
currentProp = null;
84-
valueHasQuotes = false;
8584
}
8685
break;
8786
}
8887
}
8988

9089
if (currentProp && valueStart) {
9190
const styleVal = value.slice(valueStart).trim();
92-
styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal);
91+
styles.push(currentProp, styleVal);
9392
}
9493

9594
return styles;
9695
}
9796

98-
export function stripUnnecessaryQuotes(value: string): string {
99-
const qS = value.charCodeAt(0);
100-
const qE = value.charCodeAt(value.length - 1);
101-
if (qS == qE && (qS == Char.QuoteSingle || qS == Char.QuoteDouble)) {
102-
const tempValue = value.substring(1, value.length - 1);
103-
// special case to avoid using a multi-quoted string that was just chomped
104-
// (e.g. `font-family: "Verdana", "sans-serif"`)
105-
if (tempValue.indexOf('\'') == -1 && tempValue.indexOf('"') == -1) {
106-
value = tempValue;
107-
}
108-
}
109-
return value;
110-
}
111-
11297
export function hyphenate(value: string): string {
11398
return value
11499
.replace(

packages/compiler/test/render3/style_parser_spec.ts

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {hyphenate, parse as parseStyle, stripUnnecessaryQuotes} from '../../src/render3/view/style_parser';
8+
import {hyphenate, parse as parseStyle} from '../../src/render3/view/style_parser';
99

1010
describe('style parsing', () => {
1111
it('should parse empty or blank strings', () => {
@@ -31,16 +31,9 @@ describe('style parsing', () => {
3131
expect(result).toEqual(['width', '333px', 'height', '666px', 'opacity', '0.5']);
3232
});
3333

34-
it('should chomp out start/end quotes', () => {
35-
const result = parseStyle(
36-
'content: "foo"; opacity: \'0.5\'; font-family: "Verdana", Helvetica, "sans-serif"');
37-
expect(result).toEqual(
38-
['content', 'foo', 'opacity', '0.5', 'font-family', '"Verdana", Helvetica, "sans-serif"']);
39-
});
40-
4134
it('should not mess up with quoted strings that contain [:;] values', () => {
4235
const result = parseStyle('content: "foo; man: guy"; width: 100px');
43-
expect(result).toEqual(['content', 'foo; man: guy', 'width', '100px']);
36+
expect(result).toEqual(['content', '"foo; man: guy"', 'width', '100px']);
4437
});
4538

4639
it('should not mess up with quoted strings that contain inner quote values', () => {
@@ -64,22 +57,15 @@ describe('style parsing', () => {
6457
expect(result).toEqual(['border-width', '200px']);
6558
});
6659

67-
describe('quote chomping', () => {
68-
it('should remove the start and end quotes', () => {
69-
expect(stripUnnecessaryQuotes('\'foo bar\'')).toEqual('foo bar');
70-
expect(stripUnnecessaryQuotes('"foo bar"')).toEqual('foo bar');
71-
});
72-
73-
it('should not remove quotes if the quotes are not at the start and end', () => {
74-
expect(stripUnnecessaryQuotes('foo bar')).toEqual('foo bar');
75-
expect(stripUnnecessaryQuotes(' foo bar ')).toEqual(' foo bar ');
76-
expect(stripUnnecessaryQuotes('\'foo\' bar')).toEqual('\'foo\' bar');
77-
expect(stripUnnecessaryQuotes('foo "bar"')).toEqual('foo "bar"');
60+
describe('should not remove quotes', () => {
61+
it('from string data types', () => {
62+
const result = parseStyle('content: "foo"');
63+
expect(result).toEqual(['content', '"foo"']);
7864
});
7965

80-
it('should not remove quotes if there are inner quotes', () => {
81-
const str = '"Verdana", "Helvetica"';
82-
expect(stripUnnecessaryQuotes(str)).toEqual(str);
66+
it('that changes the value context from invalid to valid', () => {
67+
const result = parseStyle('width: "1px"');
68+
expect(result).toEqual(['width', '"1px"']);
8369
});
8470
});
8571

packages/core/test/acceptance/styling_spec.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,15 @@ describe('styling', () => {
123123

124124
const div = fixture.nativeElement.querySelector('div');
125125
expect(getSortedClassName(div)).toEqual('HOST_STATIC_1 HOST_STATIC_2 STATIC');
126-
expect(getSortedStyle(div)).toEqual('color: blue; font-family: c2;');
126+
// Chrome will remove quotes but Firefox and Domino do not.
127+
expect(getSortedStyle(div)).toMatch(/color: blue; font-family: "?c2"?;/);
127128
});
128129

129130
it('should support hostBindings inheritance', () => {
130131
@Component({template: `<div my-host-bindings class="STATIC" style="color: blue;"></div>`})
131132
class Cmp {
132133
}
133-
@Directive({host: {'class': 'SUPER_STATIC', 'style': 'font-family: "super"; width: "1px";'}})
134+
@Directive({host: {'class': 'SUPER_STATIC', 'style': 'font-family: "super";'}})
134135
class SuperDir {
135136
}
136137
@Directive({
@@ -149,7 +150,29 @@ describe('styling', () => {
149150
// Browsers keep the '"' around the font name, but Domino removes it some we do search and
150151
// replace. Yes we could do `replace(/"/g, '')` but that fails on android.
151152
expect(getSortedStyle(div).replace('"', '').replace('"', ''))
152-
.toEqual('color: blue; font-family: host font; width: 1px;');
153+
.toEqual('color: blue; font-family: host font;');
154+
});
155+
156+
it('should apply style properties that require quote wrapping', () => {
157+
@Component({
158+
selector: 'test-style-quoting',
159+
template: `
160+
<div style="content: &quot;foo&quot;"></div>
161+
<div style='content: "foo"'></div>
162+
<div style="content: 'foo'"></div>
163+
`
164+
})
165+
class Cmp {
166+
}
167+
168+
TestBed.configureTestingModule({declarations: [Cmp]});
169+
const fixture = TestBed.createComponent(Cmp);
170+
fixture.detectChanges();
171+
172+
const divElements = fixture.nativeElement.querySelectorAll('div');
173+
expect(getSortedStyle(divElements[0])).toBe('content: "foo";');
174+
expect(getSortedStyle(divElements[1])).toBe('content: "foo";');
175+
expect(getSortedStyle(divElements[2])).toMatch(/content: ["']foo["'];/);
153176
});
154177

155178
it('should apply template classes in correct order', () => {

0 commit comments

Comments
 (0)