Skip to content

Commit 993d870

Browse files
url() function support in background image parser
lint fix add local image example
1 parent 2b74f98 commit 993d870

File tree

8 files changed

+382
-8
lines changed

8 files changed

+382
-8
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,15 @@ type RadialGradientValue = {
777777
}>,
778778
};
779779

780-
export type BackgroundImageValue = LinearGradientValue | RadialGradientValue;
780+
type URLBackgroundImageValue = {
781+
type: 'url',
782+
uri: string | number,
783+
};
784+
785+
export type BackgroundImageValue =
786+
| LinearGradientValue
787+
| RadialGradientValue
788+
| URLBackgroundImageValue;
781789

782790
export type BackgroundSizeValue =
783791
| {

packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,10 @@ describe('processBackgroundImage', () => {
361361
{color: 'blue', positions: ['100%']},
362362
],
363363
},
364+
{
365+
type: 'url',
366+
uri: 'https://example.com',
367+
},
364368
];
365369
const result = processBackgroundImage(input);
366370
expect(result).toEqual([
@@ -372,6 +376,10 @@ describe('processBackgroundImage', () => {
372376
{color: processColor('blue'), position: '100%'},
373377
],
374378
},
379+
{
380+
type: 'url',
381+
uri: 'https://example.com',
382+
},
375383
]);
376384
});
377385

@@ -1153,4 +1161,104 @@ describe('processBackgroundImage', () => {
11531161
expect(result1).toEqual([]);
11541162
expect(result2).toEqual([]);
11551163
});
1164+
1165+
it('should parse url unquoted', () => {
1166+
const result = processBackgroundImage('url(https://example.com/image.png)');
1167+
expect(result).toEqual([
1168+
{type: 'url', uri: 'https://example.com/image.png'},
1169+
]);
1170+
});
1171+
1172+
it('should parse url double quoted', () => {
1173+
const result = processBackgroundImage(
1174+
'url("https://example.com/image.png")',
1175+
);
1176+
expect(result).toEqual([
1177+
{type: 'url', uri: 'https://example.com/image.png'},
1178+
]);
1179+
});
1180+
1181+
it('should parse url single quoted', () => {
1182+
const result = processBackgroundImage(
1183+
"url('https://example.com/image.png')",
1184+
);
1185+
expect(result).toEqual([
1186+
{type: 'url', uri: 'https://example.com/image.png'},
1187+
]);
1188+
});
1189+
1190+
it('should parse url case insensitive', () => {
1191+
const result = processBackgroundImage('UrL(https://example.com/image.png)');
1192+
expect(result).toEqual([
1193+
{type: 'url', uri: 'https://example.com/image.png'},
1194+
]);
1195+
});
1196+
1197+
it('should parse url with query params', () => {
1198+
const result = processBackgroundImage(
1199+
'url(https://example.com/image.png?size=Large&format=webp)',
1200+
);
1201+
expect(result).toEqual([
1202+
{
1203+
type: 'url',
1204+
uri: 'https://example.com/image.png?size=Large&format=webp',
1205+
},
1206+
]);
1207+
});
1208+
1209+
it('should parse url with whitespace', () => {
1210+
const result = processBackgroundImage(
1211+
'url( https://example.com/image.png )',
1212+
);
1213+
expect(result).toEqual([
1214+
{type: 'url', uri: 'https://example.com/image.png'},
1215+
]);
1216+
});
1217+
1218+
it('should parse multiple urls', () => {
1219+
const result = processBackgroundImage(
1220+
'url(https://example.com/bg1.png), url(https://example.com/bg2.png)',
1221+
);
1222+
expect(result).toEqual([
1223+
{type: 'url', uri: 'https://example.com/bg1.png'},
1224+
{type: 'url', uri: 'https://example.com/bg2.png'},
1225+
]);
1226+
});
1227+
1228+
it('should parse url mixed with gradients', () => {
1229+
const result = processBackgroundImage(
1230+
'radial-gradient(circle at top left, red, blue), url(https://example.com/image.png), linear-gradient(to bottom, green, yellow)',
1231+
);
1232+
expect(result).toEqual([
1233+
{
1234+
type: 'radial-gradient',
1235+
shape: 'circle',
1236+
size: 'farthest-corner',
1237+
position: {top: '0%', left: '0%'},
1238+
colorStops: [
1239+
{color: processColor('red'), position: null},
1240+
{color: processColor('blue'), position: null},
1241+
],
1242+
},
1243+
{type: 'url', uri: 'https://example.com/image.png'},
1244+
{
1245+
type: 'linear-gradient',
1246+
direction: {type: 'angle', value: 180},
1247+
colorStops: [
1248+
{color: processColor('green'), position: null},
1249+
{color: processColor('yellow'), position: null},
1250+
],
1251+
},
1252+
]);
1253+
});
1254+
1255+
it('should return empty for url empty', () => {
1256+
const result = processBackgroundImage('url()');
1257+
expect(result).toEqual([]);
1258+
});
1259+
1260+
it('should return empty for url empty quoted', () => {
1261+
const result = processBackgroundImage('url("")');
1262+
expect(result).toEqual([]);
1263+
});
11561264
});

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

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
RadialGradientSize,
1919
} from './StyleSheetTypes';
2020

21+
const resolveAssetSource = require('../Image/resolveAssetSource').default;
2122
const processColor = require('./processColor').default;
2223

2324
// Linear Gradient
@@ -63,14 +64,20 @@ type RadialGradientBackgroundImage = {
6364
}>,
6465
};
6566

67+
type URLBackgroundImage = {
68+
type: 'url',
69+
uri: string,
70+
};
71+
6672
// null color indicate that the transition hint syntax is used. e.g. red, 20%, blue
6773
type ColorStopColor = ProcessedColorValue | null;
6874
// percentage or pixel value
6975
type ColorStopPosition = number | string | null;
7076

7177
type ParsedBackgroundImageValue =
7278
| LinearGradientBackgroundImage
73-
| RadialGradientBackgroundImage;
79+
| RadialGradientBackgroundImage
80+
| URLBackgroundImage;
7481

7582
export default function processBackgroundImage(
7683
backgroundImage: ?($ReadOnlyArray<BackgroundImageValue> | string),
@@ -84,6 +91,28 @@ export default function processBackgroundImage(
8491
result = parseBackgroundImageCSSString(backgroundImage.replace(/\n/g, ' '));
8592
} else if (Array.isArray(backgroundImage)) {
8693
for (const bgImage of backgroundImage) {
94+
if (bgImage.type === 'url') {
95+
let uri: ?string = null;
96+
if (typeof bgImage.uri === 'string') {
97+
uri = bgImage.uri;
98+
} else if (typeof bgImage.uri === 'number') {
99+
const source = resolveAssetSource(bgImage.uri);
100+
if (source != null && source.uri != null) {
101+
uri = source.uri;
102+
}
103+
}
104+
if (uri != null) {
105+
result = result.concat({
106+
type: 'url',
107+
uri,
108+
});
109+
continue;
110+
} else {
111+
// If the URI is invalid, return an empty array. Same as web.
112+
return [];
113+
}
114+
}
115+
87116
const processedColorStops = processColorStops(bgImage);
88117
if (processedColorStops == null) {
89118
// If a color stop is invalid, return an empty array and do not apply any gradient. Same as web.
@@ -250,13 +279,29 @@ function processColorStops(bgImage: BackgroundImageValue): $ReadOnlyArray<{
250279
function parseBackgroundImageCSSString(
251280
cssString: string,
252281
): $ReadOnlyArray<ParsedBackgroundImageValue> {
253-
const gradients = [];
282+
const backgroundImages = [];
254283
const bgImageStrings = splitGradients(cssString);
255284

256285
for (const bgImageString of bgImageStrings) {
286+
const urlRegex = /^url\((.*)\)$/i;
287+
const urlMatch = urlRegex.exec(bgImageString);
288+
if (urlMatch) {
289+
let uri = urlMatch[1].trim();
290+
const first = uri[0];
291+
if ((first === '"' || first === "'") && uri.endsWith(first)) {
292+
uri = uri.slice(1, -1);
293+
}
294+
if (uri.length > 0) {
295+
backgroundImages.push({
296+
type: 'url',
297+
uri,
298+
});
299+
}
300+
continue;
301+
}
302+
257303
const bgImage = bgImageString.toLowerCase();
258304
const gradientRegex = /^(linear|radial)-gradient\(((?:\([^)]*\)|[^()])*)\)/;
259-
260305
const match = gradientRegex.exec(bgImage);
261306
if (match) {
262307
const [, type, gradientContent] = match;
@@ -266,11 +311,11 @@ function parseBackgroundImageCSSString(
266311
: parseLinearGradientCSSString(gradientContent);
267312

268313
if (gradient != null) {
269-
gradients.push(gradient);
314+
backgroundImages.push(gradient);
270315
}
271316
}
272317
}
273-
return gradients;
318+
return backgroundImages;
274319
}
275320

276321
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
@@ -210,6 +210,14 @@ void parseProcessedBackgroundImage(
210210
}
211211

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

@@ -415,6 +423,14 @@ void parseUnprocessedBackgroundImageList(
415423
}
416424

417425
backgroundImage.emplace_back(std::move(radialGradient));
426+
} else if (type == "url") {
427+
auto uriIt = rawBackgroundImageMap.find("uri");
428+
if (uriIt != rawBackgroundImageMap.end() &&
429+
uriIt->second.hasType<std::string>()) {
430+
URLBackgroundImage urlBackgroundImage;
431+
urlBackgroundImage.uri = (std::string)(uriIt->second);
432+
backgroundImage.emplace_back(std::move(urlBackgroundImage));
433+
}
418434
}
419435
}
420436

@@ -478,6 +494,13 @@ void fromCSSColorStop(
478494

479495
std::optional<BackgroundImage> fromCSSBackgroundImage(
480496
const CSSBackgroundImageVariant& cssBackgroundImage) {
497+
if (std::holds_alternative<CSSURLFunction>(cssBackgroundImage)) {
498+
const auto& urlFunc = std::get<CSSURLFunction>(cssBackgroundImage);
499+
URLBackgroundImage urlBackgroundImage;
500+
urlBackgroundImage.uri = urlFunc.url;
501+
return BackgroundImage{urlBackgroundImage};
502+
}
503+
481504
if (std::holds_alternative<CSSLinearGradientFunction>(cssBackgroundImage)) {
482505
const auto& gradient =
483506
std::get<CSSLinearGradientFunction>(cssBackgroundImage);

packages/react-native/ReactCommon/react/renderer/css/CSSBackgroundImage.h

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,11 +845,55 @@ struct CSSDataTypeParser<CSSLinearGradientFunction> {
845845

846846
static_assert(CSSDataType<CSSLinearGradientFunction>);
847847

848+
struct CSSURLFunction {
849+
std::string url{};
850+
851+
bool operator==(const CSSURLFunction &rhs) const = default;
852+
};
853+
854+
template <>
855+
struct CSSDataTypeParser<CSSURLFunction> {
856+
static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSSyntaxParser &parser)
857+
-> std::optional<CSSURLFunction>
858+
{
859+
if (!iequals(func.name, "url")) {
860+
return {};
861+
}
862+
863+
parser.consumeWhitespace();
864+
std::string url;
865+
while (auto token = parser.consumeComponentValue<std::optional<std::string>>(
866+
[](const CSSPreservedToken &t) -> std::optional<std::string> {
867+
return std::string(t.stringValue());
868+
})) {
869+
if (!token->empty()) {
870+
url += *token;
871+
}
872+
}
873+
874+
if (url.size() >= 2) {
875+
char first = url.front();
876+
char last = url.back();
877+
if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) {
878+
url = url.substr(1, url.size() - 2);
879+
}
880+
}
881+
882+
if (!url.empty()) {
883+
return CSSURLFunction{url};
884+
}
885+
886+
return {};
887+
}
888+
};
889+
890+
static_assert(CSSDataType<CSSURLFunction>);
891+
848892
/**
849893
* Representation of <background-image>
850894
* https://www.w3.org/TR/css-backgrounds-3/#background-image
851895
*/
852-
using CSSBackgroundImage = CSSCompoundDataType<CSSLinearGradientFunction, CSSRadialGradientFunction>;
896+
using CSSBackgroundImage = CSSCompoundDataType<CSSLinearGradientFunction, CSSRadialGradientFunction, CSSURLFunction>;
853897

854898
/**
855899
* Variant of possible CSS background image types

0 commit comments

Comments
 (0)