Skip to content

Commit e6f8c65

Browse files
url() function support in background image parser
lint fix add local image example
1 parent c6b137c commit e6f8c65

20 files changed

Lines changed: 653 additions & 96 deletions

‎packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,16 @@ type RadialGradientValue = {
769769
}>,
770770
};
771771

772-
export type BackgroundImageValue = LinearGradientValue | RadialGradientValue;
772+
type URLBackgroundImageValue = {
773+
type: 'url',
774+
uri: string | number,
775+
};
776+
777+
export type GradientBackgroundImageValue =
778+
LinearGradientValue | RadialGradientValue;
779+
780+
export type BackgroundImageValue =
781+
GradientBackgroundImageValue | URLBackgroundImageValue;
773782

774783
export type BackgroundSizeValue = {
775784
x: string | number,

‎packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js‎

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,10 @@ describe('processBackgroundImage', () => {
363363
{color: 'blue', positions: ['100%']},
364364
],
365365
},
366+
{
367+
type: 'url',
368+
uri: 'https://example.com',
369+
},
366370
];
367371
const result = processBackgroundImage(input);
368372
expect(result).toEqual([
@@ -374,6 +378,10 @@ describe('processBackgroundImage', () => {
374378
{color: processColor('blue'), position: '100%'},
375379
],
376380
},
381+
{
382+
type: 'url',
383+
uri: 'https://example.com',
384+
},
377385
]);
378386
});
379387

@@ -657,7 +665,11 @@ describe('processBackgroundImage', () => {
657665
],
658666
},
659667
]);
660-
expect(result[0].colorStops[0].color).toEqual({
668+
const layer = result[0];
669+
if (layer.type !== 'linear-gradient') {
670+
throw new Error(`Expected a linear-gradient, got ${layer.type}`);
671+
}
672+
expect(layer.colorStops[0].color).toEqual({
661673
semantic: ['systemRedColor'],
662674
});
663675
});
@@ -674,7 +686,11 @@ describe('processBackgroundImage', () => {
674686
],
675687
},
676688
]);
677-
expect(result[0].colorStops[0].color).toEqual({
689+
const layer = result[0];
690+
if (layer.type !== 'linear-gradient') {
691+
throw new Error(`Expected a linear-gradient, got ${layer.type}`);
692+
}
693+
expect(layer.colorStops[0].color).toEqual({
678694
dynamic: {light: 0xff000000, dark: 0xffffffff},
679695
});
680696
});
@@ -696,7 +712,11 @@ describe('processBackgroundImage', () => {
696712
],
697713
},
698714
]);
699-
expect(result[0].colorStops[0].color).toEqual({
715+
const layer = result[0];
716+
if (layer.type !== 'linear-gradient') {
717+
throw new Error(`Expected a linear-gradient, got ${layer.type}`);
718+
}
719+
expect(layer.colorStops[0].color).toEqual({
700720
resource_paths: ['?attr/colorPrimary'],
701721
});
702722
});
@@ -1188,4 +1208,104 @@ describe('processBackgroundImage', () => {
11881208
expect(result1).toEqual([]);
11891209
expect(result2).toEqual([]);
11901210
});
1211+
1212+
it('should parse url unquoted', () => {
1213+
const result = processBackgroundImage('url(https://example.com/image.png)');
1214+
expect(result).toEqual([
1215+
{type: 'url', uri: 'https://example.com/image.png'},
1216+
]);
1217+
});
1218+
1219+
it('should parse url double quoted', () => {
1220+
const result = processBackgroundImage(
1221+
'url("https://example.com/image.png")',
1222+
);
1223+
expect(result).toEqual([
1224+
{type: 'url', uri: 'https://example.com/image.png'},
1225+
]);
1226+
});
1227+
1228+
it('should parse url single quoted', () => {
1229+
const result = processBackgroundImage(
1230+
"url('https://example.com/image.png')",
1231+
);
1232+
expect(result).toEqual([
1233+
{type: 'url', uri: 'https://example.com/image.png'},
1234+
]);
1235+
});
1236+
1237+
it('should parse url case insensitive', () => {
1238+
const result = processBackgroundImage('UrL(https://example.com/image.png)');
1239+
expect(result).toEqual([
1240+
{type: 'url', uri: 'https://example.com/image.png'},
1241+
]);
1242+
});
1243+
1244+
it('should parse url with query params', () => {
1245+
const result = processBackgroundImage(
1246+
'url(https://example.com/image.png?size=Large&format=webp)',
1247+
);
1248+
expect(result).toEqual([
1249+
{
1250+
type: 'url',
1251+
uri: 'https://example.com/image.png?size=Large&format=webp',
1252+
},
1253+
]);
1254+
});
1255+
1256+
it('should parse url with whitespace', () => {
1257+
const result = processBackgroundImage(
1258+
'url( https://example.com/image.png )',
1259+
);
1260+
expect(result).toEqual([
1261+
{type: 'url', uri: 'https://example.com/image.png'},
1262+
]);
1263+
});
1264+
1265+
it('should parse multiple urls', () => {
1266+
const result = processBackgroundImage(
1267+
'url(https://example.com/bg1.png), url(https://example.com/bg2.png)',
1268+
);
1269+
expect(result).toEqual([
1270+
{type: 'url', uri: 'https://example.com/bg1.png'},
1271+
{type: 'url', uri: 'https://example.com/bg2.png'},
1272+
]);
1273+
});
1274+
1275+
it('should parse url mixed with gradients', () => {
1276+
const result = processBackgroundImage(
1277+
'radial-gradient(circle at top left, red, blue), url(https://example.com/image.png), linear-gradient(to bottom, green, yellow)',
1278+
);
1279+
expect(result).toEqual([
1280+
{
1281+
type: 'radial-gradient',
1282+
shape: 'circle',
1283+
size: 'farthest-corner',
1284+
position: {top: '0%', left: '0%'},
1285+
colorStops: [
1286+
{color: processColor('red'), position: null},
1287+
{color: processColor('blue'), position: null},
1288+
],
1289+
},
1290+
{type: 'url', uri: 'https://example.com/image.png'},
1291+
{
1292+
type: 'linear-gradient',
1293+
direction: {type: 'angle', value: 180},
1294+
colorStops: [
1295+
{color: processColor('green'), position: null},
1296+
{color: processColor('yellow'), position: null},
1297+
],
1298+
},
1299+
]);
1300+
});
1301+
1302+
it('should return empty for url empty', () => {
1303+
const result = processBackgroundImage('url()');
1304+
expect(result).toEqual([]);
1305+
});
1306+
1307+
it('should return empty for url empty quoted', () => {
1308+
const result = processBackgroundImage('url("")');
1309+
expect(result).toEqual([]);
1310+
});
11911311
});

‎packages/react-native/Libraries/StyleSheet/processBackgroundImage.js‎

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@
1313
import type {ProcessedColorValue} from './processColor';
1414
import type {
1515
BackgroundImageValue,
16+
GradientBackgroundImageValue,
1617
RadialGradientPosition,
1718
RadialGradientShape,
1819
RadialGradientSize,
1920
} from './StyleSheetTypes';
2021

22+
const resolveAssetSource = require('../Image/resolveAssetSource').default;
2123
const processColor = require('./processColor').default;
2224

2325
// Pre-compiled regex patterns for performance - avoids regex compilation on each call
2426
const NEWLINE_REGEX = /\n/g;
2527
const GRADIENT_REGEX = /^(linear|radial)-gradient\(((?:\([^)]*\)|[^()])*)\)/;
28+
const URL_REGEX = /^url\((.*)\)$/i;
2629
const COMMA_SPLIT_REGEX = /,(?![^(]*\))/;
2730
const WHITESPACE_SPLIT_REGEX = /\s+/;
2831
const COLOR_STOP_PARTS_REGEX = /\S+\([^)]*\)|\S+/g;
@@ -70,13 +73,20 @@ type RadialGradientBackgroundImage = {
7073
}>,
7174
};
7275

76+
type URLBackgroundImage = {
77+
type: 'url',
78+
uri: string,
79+
};
80+
7381
// null color indicate that the transition hint syntax is used. e.g. red, 20%, blue
7482
type ColorStopColor = ProcessedColorValue | null;
7583
// percentage or pixel value
7684
type ColorStopPosition = number | string | null;
7785

7886
type ParsedBackgroundImageValue =
79-
LinearGradientBackgroundImage | RadialGradientBackgroundImage;
87+
| LinearGradientBackgroundImage
88+
| RadialGradientBackgroundImage
89+
| URLBackgroundImage;
8090

8191
export default function processBackgroundImage(
8292
backgroundImage: ?(ReadonlyArray<BackgroundImageValue> | string),
@@ -92,6 +102,28 @@ export default function processBackgroundImage(
92102
);
93103
} else if (Array.isArray(backgroundImage)) {
94104
for (const bgImage of backgroundImage) {
105+
if (bgImage.type === 'url') {
106+
let uri: ?string = null;
107+
if (typeof bgImage.uri === 'string') {
108+
uri = bgImage.uri;
109+
} else if (typeof bgImage.uri === 'number') {
110+
const source = resolveAssetSource(bgImage.uri);
111+
if (source != null && source.uri != null) {
112+
uri = source.uri;
113+
}
114+
}
115+
if (uri != null) {
116+
result = result.concat({
117+
type: 'url',
118+
uri,
119+
});
120+
continue;
121+
} else {
122+
// If the URI is invalid, return an empty array. Same as web.
123+
return [];
124+
}
125+
}
126+
95127
const processedColorStops = processColorStops(bgImage);
96128
if (processedColorStops == null) {
97129
// If a color stop is invalid, return an empty array and do not apply any gradient. Same as web.
@@ -191,7 +223,9 @@ export default function processBackgroundImage(
191223
return result;
192224
}
193225

194-
function processColorStops(bgImage: BackgroundImageValue): ReadonlyArray<{
226+
function processColorStops(
227+
bgImage: GradientBackgroundImageValue,
228+
): ReadonlyArray<{
195229
color: ColorStopColor,
196230
position: ColorStopPosition,
197231
}> | null {
@@ -258,10 +292,26 @@ function processColorStops(bgImage: BackgroundImageValue): ReadonlyArray<{
258292
function parseBackgroundImageCSSString(
259293
cssString: string,
260294
): ReadonlyArray<ParsedBackgroundImageValue> {
261-
const gradients = [];
295+
const backgroundImages: Array<ParsedBackgroundImageValue> = [];
262296
const bgImageStrings = splitGradients(cssString);
263297

264298
for (const bgImageString of bgImageStrings) {
299+
const urlMatch = URL_REGEX.exec(bgImageString);
300+
if (urlMatch) {
301+
let uri = urlMatch[1].trim();
302+
const first = uri[0];
303+
if ((first === '"' || first === "'") && uri.endsWith(first)) {
304+
uri = uri.slice(1, -1);
305+
}
306+
if (uri.length > 0) {
307+
backgroundImages.push({
308+
type: 'url',
309+
uri,
310+
});
311+
}
312+
continue;
313+
}
314+
265315
const bgImage = bgImageString.toLowerCase();
266316
const match = GRADIENT_REGEX.exec(bgImage);
267317
if (match) {
@@ -272,11 +322,11 @@ function parseBackgroundImageCSSString(
272322
: parseLinearGradientCSSString(gradientContent);
273323

274324
if (gradient != null) {
275-
gradients.push(gradient);
325+
backgroundImages.push(gradient);
276326
}
277327
}
278328
}
279-
return gradients;
329+
return backgroundImages;
280330
}
281331

282332
function parseRadialGradientCSSString(

‎packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,14 @@ void parseProcessedBackgroundImage(
208208
}
209209

210210
backgroundImage.emplace_back(std::move(radialGradient));
211+
} else if (type == "url") {
212+
auto uriIt = rawBackgroundImageMap.find("uri");
213+
if (uriIt != rawBackgroundImageMap.end() &&
214+
uriIt->second.hasType<std::string>()) {
215+
URLBackgroundImage urlBackgroundImage;
216+
urlBackgroundImage.uri = (std::string)(uriIt->second);
217+
backgroundImage.emplace_back(std::move(urlBackgroundImage));
218+
}
211219
}
212220
}
213221

@@ -409,6 +417,14 @@ void parseUnprocessedBackgroundImageList(
409417
}
410418

411419
backgroundImage.emplace_back(std::move(radialGradient));
420+
} else if (type == "url") {
421+
auto uriIt = rawBackgroundImageMap.find("uri");
422+
if (uriIt != rawBackgroundImageMap.end() &&
423+
uriIt->second.hasType<std::string>()) {
424+
URLBackgroundImage urlBackgroundImage;
425+
urlBackgroundImage.uri = (std::string)(uriIt->second);
426+
backgroundImage.emplace_back(std::move(urlBackgroundImage));
427+
}
412428
}
413429
}
414430

@@ -472,6 +488,13 @@ void fromCSSColorStop(
472488

473489
std::optional<BackgroundImage> fromCSSBackgroundImage(
474490
const CSSBackgroundImage& cssBackgroundImage) {
491+
if (std::holds_alternative<CSSURLFunction>(cssBackgroundImage)) {
492+
const auto& urlFunc = std::get<CSSURLFunction>(cssBackgroundImage);
493+
URLBackgroundImage urlBackgroundImage;
494+
urlBackgroundImage.uri = urlFunc.url;
495+
return BackgroundImage{urlBackgroundImage};
496+
}
497+
475498
if (std::holds_alternative<CSSLinearGradientFunction>(cssBackgroundImage)) {
476499
const auto& gradient =
477500
std::get<CSSLinearGradientFunction>(cssBackgroundImage);

0 commit comments

Comments
 (0)