Skip to content

Commit c8a0a3e

Browse files
vjeuxfacebook-github-bot-8
authored andcommitted
Reimplement color processing
Summary: **Problem:** As I was trying to document what color formats we supported, I realized that our current implementation based on the open source project tinycolor supported some crazy things. A few examples that were all valid: ``` tinycolor('abc') tinycolor(' #abc ') tinycolor('##abc') tinycolor('rgb 255 0 0') tinycolor('RGBA(0, 1, 2)') tinycolor('rgb (0, 1, 2)') tinycolor('hsv(0, 1, 2)') tinycolor({r: 10, g: 10, b: 10}) tinycolor('hsl(1%, 2, 3)') tinycolor('rgb(1.0, 2.0, 3.0)') tinycolor('rgb(1%, 2%, 3%)') ``` The integrations of tinycolor were also really bad. processColor added "support" for pure numbers and an array of colors!?? ColorPropTypes did some crazy trim().toString() and repeated a bad error message twice. **Solution:** While iteratively cleaning the file, I eventually ended up reimplementing it entierly. Major changes are: - The API is now dead simple: returns null if it doesn't parse or returns the int32 representation of the color - Stricter parsing of at Closes facebook#5529 Reviewed By: svcscm Differential Revision: D2872015 Pulled By: nicklockwood fb-gh-sync-id: df78244eefce6cf8e8ed2ea51f58d6b232de16f9
1 parent 715081c commit c8a0a3e

File tree

9 files changed

+529
-580
lines changed

9 files changed

+529
-580
lines changed

Libraries/Animated/src/Interpolation.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
* @providesModule Interpolation
1010
* @flow
1111
*/
12+
/* eslint no-bitwise: 0 */
1213
'use strict';
1314

14-
var tinycolor = require('tinycolor');
15+
var normalizeColor = require('normalizeColor');
1516

1617
// TODO(#7644673): fix this hack once github jest actually checks invariants
1718
var invariant = function(condition, message) {
@@ -164,16 +165,20 @@ function interpolate(
164165
return result;
165166
}
166167

167-
function colorToRgba(
168-
input: string
169-
): string {
170-
var color = tinycolor(input);
171-
if (color.isValid()) {
172-
var {r, g, b, a} = color.toRgb();
173-
return `rgba(${r}, ${g}, ${b}, ${a === undefined ? 1 : a})`;
174-
} else {
168+
function colorToRgba(input: string): string {
169+
var int32Color = normalizeColor(input);
170+
if (int32Color === null) {
175171
return input;
176172
}
173+
174+
int32Color = int32Color || 0; // $FlowIssue
175+
176+
var a = ((int32Color & 0xff000000) >>> 24) / 255;
177+
var r = (int32Color & 0x00ff0000) >>> 16;
178+
var g = (int32Color & 0x0000ff00) >>> 8;
179+
var b = int32Color & 0x000000ff;
180+
181+
return `rgba(${r}, ${g}, ${b}, ${a})`;
177182
}
178183

179184
var stringShapeRegex = /[0-9\.-]+/g;

Libraries/Animated/src/__tests__/Interpolation-test.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
jest
1212
.dontMock('Interpolation')
1313
.dontMock('Easing')
14-
.dontMock('tinycolor');
14+
.dontMock('normalizeColor');
1515

1616
var Interpolation = require('Interpolation');
1717
var Easing = require('Easing');
@@ -216,12 +216,12 @@ describe('Interpolation', () => {
216216
it('should work with output ranges as string', () => {
217217
var interpolation = Interpolation.create({
218218
inputRange: [0, 1],
219-
outputRange: ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)'],
219+
outputRange: ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.4)'],
220220
});
221221

222222
expect(interpolation(0)).toBe('rgba(0, 100, 200, 0)');
223-
expect(interpolation(0.5)).toBe('rgba(25, 125, 225, 0.25)');
224-
expect(interpolation(1)).toBe('rgba(50, 150, 250, 0.5)');
223+
expect(interpolation(0.5)).toBe('rgba(25, 125, 225, 0.2)');
224+
expect(interpolation(1)).toBe('rgba(50, 150, 250, 0.4)');
225225
});
226226

227227
it('should work with output ranges as short hex string', () => {
@@ -249,11 +249,11 @@ describe('Interpolation', () => {
249249
it('should work with output ranges with mixed hex and rgba strings', () => {
250250
var interpolation = Interpolation.create({
251251
inputRange: [0, 1],
252-
outputRange: ['rgba(100, 120, 140, .5)', '#87FC70'],
252+
outputRange: ['rgba(100, 120, 140, .4)', '#87FC70'],
253253
});
254254

255-
expect(interpolation(0)).toBe('rgba(100, 120, 140, 0.5)');
256-
expect(interpolation(0.5)).toBe('rgba(117.5, 186, 126, 0.75)');
255+
expect(interpolation(0)).toBe('rgba(100, 120, 140, 0.4)');
256+
expect(interpolation(0.5)).toBe('rgba(117.5, 186, 126, 0.7)');
257257
expect(interpolation(1)).toBe('rgba(135, 252, 112, 1)');
258258
});
259259

Libraries/ReactIOS/requireNativeComponent.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,16 @@ var TypeToDifferMap = {
106106
// (not yet implemented)
107107
};
108108

109+
function processColorArray(colors: []): [] {
110+
return colors && colors.map(processColor);
111+
}
112+
109113
var TypeToProcessorMap = {
110114
// iOS Types
111115
CGColor: processColor,
112-
CGColorArray: processColor,
116+
CGColorArray: processColorArray,
113117
UIColor: processColor,
114-
UIColorArray: processColor,
118+
UIColorArray: processColorArray,
115119
CGImage: resolveAssetSource,
116120
UIImage: resolveAssetSource,
117121
RCTImageSource: resolveAssetSource,

Libraries/StyleSheet/ColorPropType.js

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,38 @@
99
* @providesModule ColorPropType
1010
*/
1111
'use strict';
12+
1213
var ReactPropTypes = require('ReactPropTypes');
13-
var tinycolor = require('tinycolor');
1414

15-
var colorValidator = function (props, propName) {
16-
var selectedColor = props[propName];
17-
if (selectedColor === null || selectedColor === undefined || selectedColor.toString().trim() === '') {
18-
return new Error(
19-
`Invalid argument supplied to ${propName}.Expected a string like #123ADF or 'red'.`
20-
);
15+
var normalizeColor = require('normalizeColor');
16+
17+
var ColorPropType = function(props, propName) {
18+
var color = props[propName];
19+
if (color === undefined || color === null) {
20+
return;
2121
}
2222

23-
if (tinycolor(selectedColor.toString().trim()).isValid()) {
24-
return null;
23+
if (typeof color === 'number') {
24+
// Developers should not use a number, but we are using the prop type
25+
// both for user provided colors and for transformed ones. This isn't ideal
26+
// and should be fixed but will do for now...
27+
return;
2528
}
2629

27-
return new Error(
28-
`Invalid argument supplied to ${propName}.Expected a string like #123ADF or 'red'.`
29-
);
30+
if (normalizeColor(color) === null) {
31+
return new Error(
32+
`Invalid color supplied to ${propName}: ${color}. Valid color formats are
33+
- #f0f (#rgb)
34+
- #f0fc (#rgba)
35+
- #ff00ff (#rrggbb)
36+
- #ff00ff00 (#rrggbbaa)
37+
- rgb(255, 255, 255)
38+
- rgba(255, 255, 255, 1.0)
39+
- hsl(360, 100%, 100%)
40+
- hsla(360, 100%, 100%, 1.0)
41+
- transparent
42+
- red`);
43+
}
3044
};
3145

32-
var ColorPropType = ReactPropTypes.oneOfType([colorValidator, ReactPropTypes.number]);
33-
3446
module.exports = ColorPropType;
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
'use strict';
10+
11+
jest.dontMock('normalizeColor');
12+
13+
var normalizeColor = require('normalizeColor');
14+
15+
describe('normalizeColor', function() {
16+
it('should accept only spec compliant colors', function() {
17+
expect(normalizeColor('#abc')).not.toBe(null);
18+
expect(normalizeColor('#abcd')).not.toBe(null);
19+
expect(normalizeColor('#abcdef')).not.toBe(null);
20+
expect(normalizeColor('#abcdef01')).not.toBe(null);
21+
expect(normalizeColor('rgb(1,2,3)')).not.toBe(null);
22+
expect(normalizeColor('rgb(1, 2, 3)')).not.toBe(null);
23+
expect(normalizeColor('rgb( 1 , 2 , 3 )')).not.toBe(null);
24+
expect(normalizeColor('rgb(-1, -2, -3)')).not.toBe(null);
25+
expect(normalizeColor('rgba(0, 0, 0, 1)')).not.toBe(null);
26+
});
27+
28+
it('should refuse non spec compliant colors', function() {
29+
expect(normalizeColor('#00gg00')).toBe(null);
30+
expect(normalizeColor('rgb(1, 2, 3,)')).toBe(null);
31+
expect(normalizeColor('rgb(1, 2, 3')).toBe(null);
32+
33+
// Used to be accepted by normalizeColor
34+
expect(normalizeColor('abc')).toBe(null);
35+
expect(normalizeColor(' #abc ')).toBe(null);
36+
expect(normalizeColor('##abc')).toBe(null);
37+
expect(normalizeColor('rgb 255 0 0')).toBe(null);
38+
expect(normalizeColor('RGBA(0, 1, 2)')).toBe(null);
39+
expect(normalizeColor('rgb (0, 1, 2)')).toBe(null);
40+
expect(normalizeColor('hsv(0, 1, 2)')).toBe(null);
41+
expect(normalizeColor({r: 10, g: 10, b: 10})).toBe(null);
42+
expect(normalizeColor('hsl(1%, 2, 3)')).toBe(null);
43+
expect(normalizeColor('rgb(1.0, 2.0, 3.0)')).toBe(null);
44+
expect(normalizeColor('rgb(1%, 2%, 3%)')).toBe(null);
45+
});
46+
47+
it('should handle hex6 properly', function() {
48+
expect(normalizeColor('#000000')).toBe(0xff000000);
49+
expect(normalizeColor('#ffffff')).toBe(0xffffffff);
50+
expect(normalizeColor('#ff00ff')).toBe(0xffff00ff);
51+
expect(normalizeColor('#abcdef')).toBe(0xffabcdef);
52+
expect(normalizeColor('#012345')).toBe(0xff012345);
53+
});
54+
55+
it('should handle hex3 properly', function() {
56+
expect(normalizeColor('#000')).toBe(0xff000000);
57+
expect(normalizeColor('#fff')).toBe(0xffffffff);
58+
expect(normalizeColor('#f0f')).toBe(0xffff00ff);
59+
});
60+
61+
it('should handle hex8 properly', function() {
62+
expect(normalizeColor('#00000000')).toBe(0x00000000);
63+
expect(normalizeColor('#ffffffff')).toBe(0xffffffff);
64+
expect(normalizeColor('#ffff00ff')).toBe(0xffffff00);
65+
expect(normalizeColor('#abcdef01')).toBe(0x01abcdef);
66+
expect(normalizeColor('#01234567')).toBe(0x67012345);
67+
});
68+
69+
it('should handle rgb properly', function() {
70+
expect(normalizeColor('rgb(0, 0, 0)')).toBe(0xff000000);
71+
expect(normalizeColor('rgb(-1, -2, -3)')).toBe(0xff000000);
72+
expect(normalizeColor('rgb(0, 0, 255)')).toBe(0xff0000ff);
73+
expect(normalizeColor('rgb(100, 15, 69)')).toBe(0xff640f45);
74+
expect(normalizeColor('rgb(255, 255, 255)')).toBe(0xffffffff);
75+
expect(normalizeColor('rgb(256, 256, 256)')).toBe(0xffffffff);
76+
});
77+
78+
it('should handle rgba properly', function() {
79+
expect(normalizeColor('rgba(0, 0, 0, 0.0)')).toBe(0x00000000);
80+
expect(normalizeColor('rgba(0, 0, 0, 0)')).toBe(0x00000000);
81+
expect(normalizeColor('rgba(0, 0, 0, -0.5)')).toBe(0x00000000);
82+
expect(normalizeColor('rgba(0, 0, 0, 1.0)')).toBe(0xff000000);
83+
expect(normalizeColor('rgba(0, 0, 0, 1)')).toBe(0xff000000);
84+
expect(normalizeColor('rgba(0, 0, 0, 1.5)')).toBe(0xff000000);
85+
expect(normalizeColor('rgba(100, 15, 69, 0.5)')).toBe(0x80640f45);
86+
});
87+
88+
it('should handle hsl properly', function() {
89+
expect(normalizeColor('hsl(0, 0%, 0%)')).toBe(0xff000000);
90+
expect(normalizeColor('hsl(360, 100%, 100%)')).toBe(0xffffffff);
91+
expect(normalizeColor('hsl(180, 50%, 50%)')).toBe(0xff40bfbf);
92+
expect(normalizeColor('hsl(540, 50%, 50%)')).toBe(0xff40bfbf);
93+
expect(normalizeColor('hsl(70, 25%, 75%)')).toBe(0xffcacfaf);
94+
expect(normalizeColor('hsl(70, 100%, 75%)')).toBe(0xffeaff80);
95+
expect(normalizeColor('hsl(70, 110%, 75%)')).toBe(0xffeaff80);
96+
expect(normalizeColor('hsl(70, 0%, 75%)')).toBe(0xffbfbfbf);
97+
expect(normalizeColor('hsl(70, -10%, 75%)')).toBe(0xffbfbfbf);
98+
});
99+
100+
it('should handle hsla properly', function() {
101+
expect(normalizeColor('hsla(0, 0%, 0%, 0)')).toBe(0x00000000);
102+
expect(normalizeColor('hsla(360, 100%, 100%, 1)')).toBe(0xffffffff);
103+
expect(normalizeColor('hsla(360, 100%, 100%, 0)')).toBe(0x00ffffff);
104+
expect(normalizeColor('hsla(180, 50%, 50%, 0.2)')).toBe(0x3340bfbf);
105+
});
106+
107+
it('should handle named colors properly', function() {
108+
expect(normalizeColor('red')).toBe(0xffff0000);
109+
expect(normalizeColor('transparent')).toBe(0x00000000);
110+
expect(normalizeColor('peachpuff')).toBe(0xffffdab9);
111+
});
112+
});

Libraries/StyleSheet/__tests__/processColor-test.js

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,6 @@ describe('processColor', () => {
4949
expect(colorFromString).toEqual(expectedInt);
5050
});
5151

52-
it('should convert rgb x, y, z', () => {
53-
var colorFromString = processColor('rgb 10, 20, 30');
54-
var expectedInt = 0xFF0A141E;
55-
expect(colorFromString).toEqual(expectedInt);
56-
});
57-
5852
});
5953

6054
describe('RGBA strings', () => {
@@ -65,12 +59,6 @@ describe('processColor', () => {
6559
expect(colorFromString).toEqual(expectedInt);
6660
});
6761

68-
it('should convert rgba x, y, z, a', () => {
69-
var colorFromString = processColor('rgba 10, 20, 30, 0.4');
70-
var expectedInt = 0x660A141E;
71-
expect(colorFromString).toEqual(expectedInt);
72-
});
73-
7462
});
7563

7664
describe('HSL strings', () => {
@@ -81,12 +69,6 @@ describe('processColor', () => {
8169
expect(colorFromString).toEqual(expectedInt);
8270
});
8371

84-
it('should convert hsl x, y%, z%', () => {
85-
var colorFromString = processColor('hsl 318, 69%, 55%');
86-
var expectedInt = 0xFFDB3DAC;
87-
expect(colorFromString).toEqual(expectedInt);
88-
});
89-
9072
});
9173

9274
describe('HSL strings', () => {
@@ -97,12 +79,6 @@ describe('processColor', () => {
9779
expect(colorFromString).toEqual(expectedInt);
9880
});
9981

100-
it('should convert hsla x, y%, z%, a', () => {
101-
var colorFromString = processColor('hsla 318, 69%, 55%, 0.25');
102-
var expectedInt = 0x40DB3DAC;
103-
expect(colorFromString).toEqual(expectedInt);
104-
});
105-
10682
});
10783

10884
describe('hex strings', () => {

0 commit comments

Comments
 (0)