Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support css variables for SvgCss #2459

Merged
merged 10 commits into from
Oct 2, 2024
1 change: 1 addition & 0 deletions apps/test-examples/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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';
Expand Down
58 changes: 58 additions & 0 deletions apps/test-examples/src/Test2380.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
73 changes: 71 additions & 2 deletions src/css/css.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,57 @@ const parseProps = {
* @author strarsis <[email protected]>
* @author modified by: msand <[email protected]>
*/

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 = ['fill', 'color', 'stroke', 'stopColor', 'floodColor'];
bohdanprog marked this conversation as resolved.
Show resolved Hide resolved
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
) {
Expand All @@ -604,6 +655,7 @@ export const inlineStyles: Middleware = function inlineStyles(
}

const selectors: FlatSelectorList = [];
let variables = new Map<string, string>();

for (const element of styleElements) {
const { children } = element;
Expand All @@ -615,7 +667,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: ' +
Expand All @@ -636,6 +691,15 @@ export const inlineStyles: Middleware = function inlineStyles(
// stable sort selectors
const sortedSelectors = sortSelectors(selectorsPseudo).reverse();

const elementsWithColor = cssSelect(
'*[fill], *[color], *[stroke], *[stopColor], *[floodColor]',
document,
cssSelectOpts
);
for (const element of elementsWithColor) {
resolveElementVariables(element, variables);
}

// match selectors
for (const { rule, item } of sortedSelectors) {
if (rule === null) {
Expand Down Expand Up @@ -667,7 +731,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}`);
}
}
}
},
Expand Down
Loading