feat: support css variables for SvgCss (#2459)

# Summary
Feature #2380 

We want to add support for CSS variables when passing them to parse the
SVG XML source function.

## Test Plan
Test app -> src -> Test2380

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |         		 |
| MacOS   |            |
| Android |         	|
| Web     |    		    |

---------

Co-authored-by: Jakub Grzywacz <jakub.grzywacz@swmansion.com>
This commit is contained in:
Bohdan Artiukhov
2024-10-02 09:23:34 +02:00
committed by GitHub
parent 9d9958264b
commit b4dc9756df
3 changed files with 137 additions and 2 deletions
+1
View File
@@ -27,6 +27,7 @@ import Test2327 from './src/Test2327';
import Test2233 from './src/Test2233';
import Test2363 from './src/Test2363';
import Test2366 from './src/Test2366';
import Test2380 from './src/Test2380';
import Test2397 from './src/Test2397';
import Test2403 from './src/Test2403';
import Test2407 from './src/Test2407';
+58
View File
@@ -0,0 +1,58 @@
import React from 'react';
import {View} from 'react-native';
import {SvgCss} from 'react-native-svg/css';
const xml = `
<svg width="200" height="200" id="mySVG" viewBox="0 0 200 200">
<style>
#mySVG {
--my-color: #ff0000;
--color: #3e3efe;
--green-color: green;
--my-gold-stop-color: gold;
--my-red-stop-color: red;
--my-feFlood-color: green;
}
#newId {
fill: purple;
}
</style>
<defs>
<linearGradient id="myGradient">
<stop offset="5%" stop-color="var(--my-gold-stop-color)" />
<stop offset="95%" stop-color="var(--my-red-stop-color)" />
</linearGradient>
<filter id="spotlight">
<feFlood
result="floodFill"
x="0"
y="0"
width="100%"
height="100%"
flood-color="var(--my-feFlood-color)"
flood-opacity="1" />
<feBlend in="SourceGraphic" in2="floodFill" mode="multiply" />
</filter>
</defs>
<rect fill="var(--color)" x="40" y="40" width="32" height="32" />
<rect id="newId" x="80" y="40" width="32" height="32" />
<text fill="var(--color)" x="20" y="100" stroke="var(--green-color)" stroke-width="1" font-size="16">Hello</text>
<circle cx="140" cy="55" r="15" fill="url(#myGradient)" />
<image
href="https://static-00.iconduck.com/assets.00/mdn-icon-2048x1806-enhibj42.png"
x="0"
y="40"
width="32"
height="32"
style="filter:url(#spotlight);" />
</svg>
`;
export default function SvgComponent() {
return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<SvgCss xml={xml} height="200" width="200" />
</View>
);
}
+78 -2
View File
@@ -592,6 +592,64 @@ const parseProps = {
* @author strarsis <strarsis@gmail.com>
* @author modified by: msand <msand@abo.fi>
*/
function extractVariables(stylesheet: CssNode): Map<string, string> {
const variables = new Map<string, string>();
csstree.walk(stylesheet, {
visit: 'Declaration',
enter(node) {
const { property, value } = node as Declaration;
if (property.startsWith('--')) {
const variableName = property.trim();
const variableValue = csstree.generate(value).trim();
variables.set(variableName, variableValue);
}
},
});
return variables;
}
function resolveVariables(
value: string | CssNode | undefined,
variables: Map<string, string>
): string {
if (value === undefined) {
return '';
}
const valueStr = typeof value === 'string' ? value : csstree.generate(value);
return valueStr.replace(
/var\((--[^,)]+)(?:,\s*([^)]+))?\)/g,
(_, variableName, fallback) => {
const resolvedValue = variables.get(variableName);
if (resolvedValue !== undefined) {
return resolveVariables(resolvedValue, variables);
}
return fallback ? resolveVariables(fallback, variables) : '';
}
);
}
const propsToResolve = [
'color',
'fill',
'floodColor',
'lightingColor',
'stopColor',
'stroke',
];
const resolveElementVariables = (
element: XmlAST,
variables: Map<string, string>
) =>
propsToResolve.forEach((prop) => {
const value = element.props[prop] as string;
if (value && value.startsWith('var(')) {
element.props[prop] = resolveVariables(value, variables);
}
});
export const inlineStyles: Middleware = function inlineStyles(
document: XmlAST
) {
@@ -604,6 +662,7 @@ export const inlineStyles: Middleware = function inlineStyles(
}
const selectors: FlatSelectorList = [];
let variables = new Map<string, string>();
for (const element of styleElements) {
const { children } = element;
@@ -615,7 +674,10 @@ export const inlineStyles: Middleware = function inlineStyles(
// collect <style/>s and their css ast
try {
const styleString = children.join('');
flattenToSelectors(csstree.parse(styleString, parseProps), selectors);
const stylesheet = csstree.parse(styleString, parseProps);
variables = extractVariables(stylesheet);
flattenToSelectors(stylesheet, selectors);
} catch (parseError) {
console.warn(
'Warning: Parse error of styles of <style/> element, skipped. Error details: ' +
@@ -636,6 +698,15 @@ export const inlineStyles: Middleware = function inlineStyles(
// stable sort selectors
const sortedSelectors = sortSelectors(selectorsPseudo).reverse();
const elementsWithColor = cssSelect(
'*[color], *[fill], *[floodColor], *[lightingColor], *[stopColor], *[stroke]',
document,
cssSelectOpts
);
for (const element of elementsWithColor) {
resolveElementVariables(element, variables);
}
// match selectors
for (const { rule, item } of sortedSelectors) {
if (rule === null) {
@@ -667,7 +738,12 @@ export const inlineStyles: Middleware = function inlineStyles(
const current = priority.get(name);
if (current === undefined || current < important) {
priority.set(name, important as boolean);
style[camel] = val;
// Handle if value is undefined
if (val !== undefined) {
style[camel] = val;
} else {
console.warn(`Undefined value for style property: ${camel}`);
}
}
}
},