Skip to content

Commit d3f7f3a

Browse files
committed
add new --allow-override-fill option
1 parent 4b16d13 commit d3f7f3a

24 files changed

Lines changed: 723 additions & 39 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ Options
3636
--with-web-for-typescript, -rnwts Output code for DOM with TypeScript. If --with-native is also used, will be output as .web.tsx files
3737
--remove-fill, -rf Remove all 'fill' properties from SVGs, convenient for icons
3838
--remove-stroke, -rs Remove all 'stroke' properties from SVGs, convenient for icons
39+
--allow-override-fill, -aof Replace all 'fill' properties by a dynamic prop (fills) in SVGs, e.g. fill={fills[N]}. If fills[N] is undefined, fallback to the original value. Useful to dynamically control icon color(s).
3940

4041
Example
4142
$ react-from-svg assets/svgs src/Svgs --with-native --remove-fill
43+
$ react-from-svg assets/svgs src/Svgs --with-web --allow-override-fill
4244
```
4345

4446
Generated components will allow you to inject all the props you could use on an `<svg>`/`<Svg>`, such as:

src/adjust-svg.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,31 @@ const transformStyleAttributes = (svg: string): string => {
7373
});
7474
};
7575

76+
// Replace each existing fill with fill={fills[N]} (N = index of occurrence)
77+
// If fills[N] is strictly undefined, keep the original value unless removeFill is true, then fallback to undefined
78+
function allowOverrideFillWithProp(
79+
svg: string,
80+
removeFill: boolean = false,
81+
): string {
82+
let fillIndex = 0;
83+
return svg.replace(
84+
/(<(?!svg)[^\s>]+)([^>]*?)\sfill="([^"]*)"([^>]*>)/g,
85+
function (
86+
match: string,
87+
startTag: string,
88+
beforeFill: string,
89+
fillValue: string,
90+
afterFill: string,
91+
) {
92+
const idx = fillIndex++;
93+
// If removeFill: no fallback to the original value
94+
// Otherwise: fallback to the original value
95+
const fallback = removeFill ? "undefined" : `"${fillValue}"`;
96+
return `${startTag}${beforeFill} fill={typeof fills !== 'undefined' && typeof fills[${idx}] !== 'undefined' ? fills[${idx}] : ${fallback}}${afterFill}`;
97+
},
98+
);
99+
}
100+
76101
export {
77102
cleanupStart,
78103
prepareSvgProps,
@@ -83,4 +108,5 @@ export {
83108
deleteFill,
84109
deleteStroke,
85110
transformStyleAttributes,
111+
allowOverrideFillWithProp,
86112
};

src/all-combinations.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const __dirname = path.dirname(__filename);
1111
const optionalFlags = [
1212
{ name: "remove-fill", alias: "rf", bool: true },
1313
{ name: "remove-stroke", alias: "rs", bool: true },
14+
{ name: "allow-override-fill", alias: "aof", bool: true, value: '["#f00"]' },
1415
];
1516

1617
// Create specific core combinations as requested

src/bin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const cli = meow(
1515
--with-web-for-typescript, -rnwts Output code for DOM with TypeScript. If --with-native is also used, will be output as .web.tsx files
1616
--remove-fill, -rf Remove all 'fill' properties from SVGs, convenient for icons
1717
--remove-stroke, -rs Remove all 'stroke' properties from SVGs, convenient for icons
18+
--allow-override-fill, -aof Replace all 'fill' properties from SVGs
1819
1920
Example
2021
$ react-from-svg assets/svgs src/Svgs --with-native --remove-fill
@@ -28,6 +29,7 @@ const cli = meow(
2829
withWebForTypescript: { type: "boolean", shortFlag: "rnwts" },
2930
removeFill: { type: "boolean", shortFlag: "rf" },
3031
removeStroke: { type: "boolean", shortFlag: "rs" },
32+
allowOverrideFill: { type: "boolean", shortFlag: "aof" },
3133
},
3234
},
3335
);

src/templates.ts

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
11
const sep = ";\n";
22

3+
// Helper for prop signature
4+
function getPropsSignature(
5+
allowOverrideFill: boolean,
6+
typescript: boolean,
7+
native: boolean,
8+
): string {
9+
const fillsType = `{fills?: (${native ? "ColorValue" : "string"} | undefined)[]}`;
10+
const optionalPropsType = typescript
11+
? ` : ${
12+
native ? "SvgProps" : "React.SVGProps<SVGSVGElement>"
13+
}${allowOverrideFill ? " & " + fillsType : ""}`
14+
: "";
15+
return `(${allowOverrideFill ? "{ fills, ...props }" : "props"}${optionalPropsType})`;
16+
}
17+
318
const importReact = (): string => {
419
return "import React from 'react'";
520
};
621

7-
const jsExport = (name: string, svgOutput: string): string => {
22+
const jsExport = (
23+
name: string,
24+
svgOutput: string,
25+
allowOverrideFill: boolean = false,
26+
): string => {
827
const content =
9-
`const ${name} = (props) => {
28+
`const ${name} = ${getPropsSignature(allowOverrideFill, false, false)} => {
1029
return (${svgOutput});
1130
}` + sep;
1231
return `${content}
@@ -17,19 +36,24 @@ const tsxExport = (
1736
name: string,
1837
svgOutput: string,
1938
native: boolean,
39+
allowOverrideFill: boolean = false,
2040
): string => {
2141
const content =
22-
`const ${name} = (props: ${
23-
native ? "SvgProps" : "React.SVGProps<SVGSVGElement>"
24-
}) => {
42+
`const ${name} = ${getPropsSignature(allowOverrideFill, true, native)} => {
2543
return (${svgOutput});
2644
}` + sep;
2745
return `${content}
2846
export default ${name}`;
2947
};
3048

31-
const web = (svgOutput: string, name: string): string => {
32-
return importReact() + sep + jsExport(name, svgOutput) + sep;
49+
const web = (
50+
svgOutput: string,
51+
name: string,
52+
allowOverrideFill: boolean = false,
53+
): string => {
54+
return (
55+
importReact() + sep + jsExport(name, svgOutput, allowOverrideFill) + sep
56+
);
3357
};
3458

3559
const RNSvgModules = [
@@ -58,37 +82,61 @@ const RNSvgModules = [
5882
"Use",
5983
];
6084

61-
const importReactNativeSvg = (typescript: boolean): string => {
85+
const importReactNativeSvg = (
86+
typescript: boolean,
87+
allowOverrideFill: boolean,
88+
): string => {
6289
return `import Svg, {
6390
${RNSvgModules.join(",")}
64-
} from 'react-native-svg'
65-
${typescript ? "import type { SvgProps } from 'react-native-svg'" : ""}`;
91+
} from 'react-native-svg';${
92+
typescript && allowOverrideFill
93+
? `import type { ColorValue } from 'react-native';`
94+
: ""
95+
}${typescript ? `import type { SvgProps } from 'react-native-svg';` : ""}
96+
`;
6697
};
6798

68-
const native = (svgOutput: string, name: string): string => {
99+
const native = (
100+
svgOutput: string,
101+
name: string,
102+
allowOverrideFill: boolean = false,
103+
): string => {
69104
return (
70105
importReact() +
71106
sep +
72-
importReactNativeSvg(false) +
107+
importReactNativeSvg(false, allowOverrideFill) +
73108
sep +
74-
jsExport(name, svgOutput) +
109+
jsExport(name, svgOutput, allowOverrideFill) +
75110
sep
76111
);
77112
};
78113

79-
const nativeForTypescript = (svgOutput: string, name: string): string => {
114+
const nativeForTypescript = (
115+
svgOutput: string,
116+
name: string,
117+
allowOverrideFill: boolean = false,
118+
): string => {
80119
return (
81120
importReact() +
82121
sep +
83-
importReactNativeSvg(true) +
122+
importReactNativeSvg(true, allowOverrideFill) +
84123
sep +
85-
tsxExport(name, svgOutput, true) +
124+
tsxExport(name, svgOutput, true, allowOverrideFill) +
86125
sep
87126
);
88127
};
89128

90-
const webForTypescript = (svgOutput: string, name: string): string => {
91-
return importReact() + sep + tsxExport(name, svgOutput, false) + sep;
129+
const webForTypescript = (
130+
svgOutput: string,
131+
name: string,
132+
allowOverrideFill: boolean = false,
133+
): string => {
134+
return (
135+
importReact() +
136+
sep +
137+
tsxExport(name, svgOutput, false, allowOverrideFill) +
138+
sep
139+
);
92140
};
93141

94142
export { web, native, nativeForTypescript, webForTypescript };

src/transformer.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
deleteFill,
1414
deleteStroke,
1515
transformStyleAttributes,
16+
allowOverrideFillWithProp,
1617
} from "./adjust-svg.js";
1718
import {
1819
web,
@@ -33,6 +34,7 @@ type Flags = {
3334
withWebForTypescript?: boolean;
3435
removeFill?: boolean;
3536
removeStroke?: boolean;
37+
allowOverrideFill?: boolean;
3638
};
3739

3840
const get = async (globPattern: string): Promise<File[]> => {
@@ -65,11 +67,18 @@ export const transformSvg = (
6567
options: {
6668
removeFill: boolean;
6769
removeStroke: boolean;
70+
allowOverrideFill: boolean;
6871
pascalCaseTag: boolean;
6972
template: (svg: string) => string;
7073
},
7174
): string => {
72-
const { removeFill, removeStroke, pascalCaseTag, template } = options;
75+
const {
76+
removeFill,
77+
removeStroke,
78+
allowOverrideFill,
79+
pascalCaseTag,
80+
template,
81+
} = options;
7382

7483
let result = cleanupStart(svg);
7584
result = prepareSvgProps(result);
@@ -78,6 +87,9 @@ export const transformSvg = (
7887
result = transformStyleAttributes(result);
7988
result = pascalCaseTag ? tagToPascalCase(result) : result;
8089
result = cleanupEndWithoutSpace(result);
90+
if (allowOverrideFill) {
91+
result = allowOverrideFillWithProp(result, removeFill);
92+
}
8193
result = removeFill ? deleteFill(result) : result;
8294
result = removeStroke ? deleteStroke(result) : result;
8395

@@ -95,6 +107,7 @@ const transformFiles = (
95107
withWebForTypescript: boolean;
96108
removeFill: boolean;
97109
removeStroke: boolean;
110+
allowOverrideFill: boolean;
98111
},
99112
): File[] => {
100113
const {
@@ -104,6 +117,7 @@ const transformFiles = (
104117
withWebForTypescript,
105118
removeFill,
106119
removeStroke,
120+
allowOverrideFill,
107121
} = options;
108122

109123
return files.reduce((acc, file) => {
@@ -112,34 +126,40 @@ const transformFiles = (
112126
js: boolean;
113127
pascalCaseTag: boolean;
114128
template: (svg: string) => string;
115-
}) => transformSvg(file.content, { removeFill, removeStroke, ...options });
129+
}) =>
130+
transformSvg(file.content, {
131+
removeFill,
132+
removeStroke,
133+
allowOverrideFill,
134+
...options,
135+
});
116136

117137
const trsfNative = () =>
118138
trsf({
119139
js: true,
120140
pascalCaseTag: true,
121-
template: (svg) => native(svg, name),
141+
template: (svg) => native(svg, name, allowOverrideFill),
122142
});
123143

124144
const trsfNativeForTs = () =>
125145
trsf({
126146
js: true,
127147
pascalCaseTag: true,
128-
template: (svg) => nativeForTypescript(svg, name),
148+
template: (svg) => nativeForTypescript(svg, name, allowOverrideFill),
129149
});
130150

131151
const trsfWeb = () =>
132152
trsf({
133153
js: true,
134154
pascalCaseTag: false,
135-
template: (svg) => web(svg, name),
155+
template: (svg) => web(svg, name, allowOverrideFill),
136156
});
137157

138158
const trsfWebForTs = () =>
139159
trsf({
140160
js: true,
141161
pascalCaseTag: false,
142-
template: (svg) => webForTypescript(svg, name),
162+
template: (svg) => webForTypescript(svg, name, allowOverrideFill),
143163
});
144164

145165
if (
@@ -210,6 +230,7 @@ export const make = async (
210230
withWebForTypescript: flags.withWebForTypescript ?? false,
211231
removeFill: flags.removeFill ?? false,
212232
removeStroke: flags.removeStroke ?? false,
233+
allowOverrideFill: flags.allowOverrideFill ?? false,
213234
});
214235

215236
console.log("Files transformed", transformedFiles.length);

tests/components/with-native-for-typescript_remove-fill_remove-stroke/SVGClean.tsx renamed to tests/components/with-native-for-typescript_allow-override-fill/SVGClean.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ import Svg, {
2424
TSpan as Tspan,
2525
Use,
2626
} from "react-native-svg";
27+
import type { ColorValue } from "react-native";
2728
import type { SvgProps } from "react-native-svg";
28-
const SVGClean = (props: SvgProps) => {
29+
const SVGClean = ({
30+
fills,
31+
...props
32+
}: SvgProps & { fills?: (ColorValue | undefined)[] }) => {
2933
return (
3034
<Svg viewBox="0 0 30 30" {...props}>
3135
<Path d="m15 3c-6.627 0-12 5.373-12 12 0 5.623 3.872 10.328 9.092 11.63-.056-.162-.092-.35-.092-.583v-2.051c-.487 0-1.303 0-1.508 0-.821 0-1.551-.353-1.905-1.009-.393-.729-.461-1.844-1.435-2.526-.289-.227-.069-.486.264-.451.615.174 1.125.596 1.605 1.222.478.627.703.769 1.596.769.433 0 1.081-.025 1.691-.121.328-.833.895-1.6 1.588-1.962-3.996-.411-5.903-2.399-5.903-5.098 0-1.162.495-2.286 1.336-3.233-.276-.94-.623-2.857.106-3.587 1.798 0 2.885 1.166 3.146 1.481.896-.307 1.88-.481 2.914-.481 1.036 0 2.024.174 2.922.483.258-.313 1.346-1.483 3.148-1.483.732.731.381 2.656.102 3.594.836.945 1.328 2.066 1.328 3.226 0 2.697-1.904 4.684-5.894 5.097 1.098.573 1.899 2.183 1.899 3.396v2.734c0 .104-.023.179-.035.268 4.676-1.639 8.035-6.079 8.035-11.315 0-6.627-5.373-12-12-12z" />

tests/components/with-native-for-typescript_remove-fill_remove-stroke/SVGEdgeCaseWidth.tsx renamed to tests/components/with-native-for-typescript_allow-override-fill/SVGEdgeCaseWidth.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ import Svg, {
2424
TSpan as Tspan,
2525
Use,
2626
} from "react-native-svg";
27+
import type { ColorValue } from "react-native";
2728
import type { SvgProps } from "react-native-svg";
28-
const SVGEdgeCaseWidth = (props: SvgProps) => {
29+
const SVGEdgeCaseWidth = ({
30+
fills,
31+
...props
32+
}: SvgProps & { fills?: (ColorValue | undefined)[] }) => {
2933
return (
3034
<Svg viewBox="0 0 512 512" {...props}>
3135
<Path d="M336 192h40a40 40 0 0140 40v192a40 40 0 01-40 40H136a40 40 0 01-40-40V232a40 40 0 0140-40h40M336 128l-80-80-80 80M256 321V48" />

0 commit comments

Comments
 (0)