@fortawesome/react-native-fontawesome 0.3.1 → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/lib/module/FontAwesomeIcon.js +204 -0
- package/lib/module/FontAwesomeIcon.js.map +1 -0
- package/lib/module/converter.js +49 -0
- package/lib/module/converter.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/logger.js +9 -0
- package/lib/module/logger.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/FontAwesomeIcon.d.ts +41 -0
- package/lib/typescript/src/FontAwesomeIcon.d.ts.map +1 -0
- package/lib/typescript/src/converter.d.ts +10 -0
- package/lib/typescript/src/converter.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/logger.d.ts +2 -0
- package/lib/typescript/src/logger.d.ts.map +1 -0
- package/package.json +169 -58
- package/src/FontAwesomeIcon.tsx +331 -0
- package/src/converter.ts +71 -0
- package/src/index.tsx +7 -0
- package/src/logger.ts +7 -0
- package/dist/components/FontAwesomeIcon.js +0 -188
- package/dist/converter.js +0 -81
- package/dist/logger.js +0 -15
- package/index.d.ts +0 -29
- package/index.js +0 -1
package/package.json
CHANGED
|
@@ -1,78 +1,189 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fortawesome/react-native-fontawesome",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
4
|
"description": "Official React Native component for Font Awesome",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"cpp",
|
|
21
|
+
"*.podspec",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!ios/build",
|
|
24
|
+
"!android/build",
|
|
25
|
+
"!android/gradle",
|
|
26
|
+
"!android/gradlew",
|
|
27
|
+
"!android/gradlew.bat",
|
|
28
|
+
"!android/local.properties",
|
|
29
|
+
"!**/__tests__",
|
|
30
|
+
"!**/__fixtures__",
|
|
31
|
+
"!**/__mocks__",
|
|
32
|
+
"!**/.*"
|
|
33
|
+
],
|
|
6
34
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
35
|
+
"example": "npm run --workspace=react-native-fontawesome-example",
|
|
36
|
+
"clean": "del-cli lib",
|
|
37
|
+
"prepare": "bob build",
|
|
38
|
+
"typecheck": "tsc",
|
|
39
|
+
"test": "NODE_OPTIONS='--localstorage-file=/tmp/jest-localstorage.db' jest",
|
|
40
|
+
"release": "release-it --only-version",
|
|
41
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\""
|
|
12
42
|
},
|
|
13
|
-
"
|
|
14
|
-
|
|
43
|
+
"keywords": [
|
|
44
|
+
"react-native",
|
|
45
|
+
"ios",
|
|
46
|
+
"android"
|
|
47
|
+
],
|
|
15
48
|
"repository": {
|
|
16
49
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/FortAwesome/react-native-fontawesome.git"
|
|
50
|
+
"url": "git+https://github.com/FortAwesome/react-native-fontawesome.git"
|
|
18
51
|
},
|
|
19
|
-
"
|
|
20
|
-
"Travis Chase <travis@fontawesome.com>",
|
|
21
|
-
"Rob Madole <rob@fontawesome.com>",
|
|
22
|
-
"Mike Wilkerson <mwilkerson@gmail.com>",
|
|
23
|
-
"Dizy <mhisf@vip.qq.com>",
|
|
24
|
-
"David Martin <github.com/iamdavidmartin>",
|
|
25
|
-
"Jeremy <github.com/puremana>",
|
|
26
|
-
"Michael Schonfeld <github.com/schonfeld>",
|
|
27
|
-
"Ádám Gólya <github.com/golya>",
|
|
28
|
-
"Edward Emanuel <github.com/ej2>",
|
|
29
|
-
"Jason Lundien <github.com/jasonlundien>",
|
|
30
|
-
"Greg Marut <github.com/gregmarut>"
|
|
31
|
-
],
|
|
52
|
+
"author": "Font Awesome Team <hello@fontawesome.com> (https://fontawesome.com)",
|
|
32
53
|
"license": "MIT",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/FortAwesome/react-native-fontawesome/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/FortAwesome/react-native-fontawesome#readme",
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"registry": "https://registry.npmjs.org/"
|
|
37
60
|
},
|
|
38
61
|
"devDependencies": {
|
|
39
|
-
"@
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
-
"@fortawesome/
|
|
45
|
-
"babel-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"react-
|
|
62
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
63
|
+
"@eslint/compat": "^1.3.2",
|
|
64
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
65
|
+
"@eslint/js": "^9.35.0",
|
|
66
|
+
"@fortawesome/fontawesome-svg-core": "^7.0.0",
|
|
67
|
+
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
|
68
|
+
"@react-native/babel-preset": "0.83.0",
|
|
69
|
+
"@react-native/eslint-config": "0.83.0",
|
|
70
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
71
|
+
"@types/humps": "^2.0.6",
|
|
72
|
+
"@types/jest": "^29.5.14",
|
|
73
|
+
"@types/lodash": "^4.17.23",
|
|
74
|
+
"@types/react": "^19.1.12",
|
|
75
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
76
|
+
"commitlint": "^19.8.1",
|
|
77
|
+
"del-cli": "^6.0.0",
|
|
78
|
+
"eslint": "^9.35.0",
|
|
79
|
+
"eslint-config-prettier": "^10.1.8",
|
|
80
|
+
"eslint-plugin-ft-flow": "^3.0.11",
|
|
81
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
82
|
+
"eslint-plugin-react-native": "^5.0.0",
|
|
83
|
+
"hermes-eslint": "^0.33.3",
|
|
84
|
+
"jest": "^29.7.0",
|
|
85
|
+
"lefthook": "^2.0.3",
|
|
86
|
+
"lodash": "^4.17.23",
|
|
87
|
+
"prettier": "^3.3.3",
|
|
88
|
+
"react": "19.1.0",
|
|
89
|
+
"react-native": "0.81.5",
|
|
90
|
+
"react-native-builder-bob": "^0.40.17",
|
|
91
|
+
"react-native-svg": "^15.15.1",
|
|
92
|
+
"react-test-renderer": "^19.1.0",
|
|
93
|
+
"release-it": "^19.0.4",
|
|
94
|
+
"typescript": "^5.9.2"
|
|
60
95
|
},
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
96
|
+
"peerDependencies": {
|
|
97
|
+
"@fortawesome/fontawesome-svg-core": "~7",
|
|
98
|
+
"react": "*",
|
|
99
|
+
"react-native": "*",
|
|
100
|
+
"react-native-svg": ">=11"
|
|
64
101
|
},
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"index.d.ts",
|
|
68
|
-
"dist/converter.js",
|
|
69
|
-
"dist/logger.js",
|
|
70
|
-
"dist/components/FontAwesomeIcon.js"
|
|
102
|
+
"workspaces": [
|
|
103
|
+
"example"
|
|
71
104
|
],
|
|
105
|
+
"react-native-builder-bob": {
|
|
106
|
+
"source": "src",
|
|
107
|
+
"output": "lib",
|
|
108
|
+
"targets": [
|
|
109
|
+
[
|
|
110
|
+
"module",
|
|
111
|
+
{
|
|
112
|
+
"esm": true
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
"typescript",
|
|
117
|
+
{
|
|
118
|
+
"project": "tsconfig.build.json"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
]
|
|
122
|
+
},
|
|
72
123
|
"jest": {
|
|
73
124
|
"preset": "react-native",
|
|
125
|
+
"testEnvironmentOptions": {
|
|
126
|
+
"customExportConditions": [
|
|
127
|
+
"react-native"
|
|
128
|
+
]
|
|
129
|
+
},
|
|
74
130
|
"modulePathIgnorePatterns": [
|
|
75
|
-
"<rootDir>/
|
|
131
|
+
"<rootDir>/example/node_modules",
|
|
132
|
+
"<rootDir>/lib/"
|
|
133
|
+
],
|
|
134
|
+
"testPathIgnorePatterns": [
|
|
135
|
+
"<rootDir>/node_modules/",
|
|
136
|
+
"<rootDir>/lib/",
|
|
137
|
+
"<rootDir>/src/__tests__/__fixtures__/",
|
|
138
|
+
"<rootDir>/src/__tests__/__snapshots__/"
|
|
139
|
+
],
|
|
140
|
+
"fakeTimers": {
|
|
141
|
+
"enableGlobally": false
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"commitlint": {
|
|
145
|
+
"extends": [
|
|
146
|
+
"@commitlint/config-conventional"
|
|
76
147
|
]
|
|
148
|
+
},
|
|
149
|
+
"release-it": {
|
|
150
|
+
"git": {
|
|
151
|
+
"commitMessage": "chore: release ${version}",
|
|
152
|
+
"tagName": "v${version}"
|
|
153
|
+
},
|
|
154
|
+
"npm": {
|
|
155
|
+
"publish": true
|
|
156
|
+
},
|
|
157
|
+
"github": {
|
|
158
|
+
"release": false
|
|
159
|
+
},
|
|
160
|
+
"plugins": {
|
|
161
|
+
"@release-it/conventional-changelog": {
|
|
162
|
+
"preset": {
|
|
163
|
+
"name": "angular"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"prettier": {
|
|
169
|
+
"quoteProps": "consistent",
|
|
170
|
+
"singleQuote": true,
|
|
171
|
+
"tabWidth": 2,
|
|
172
|
+
"trailingComma": "es5",
|
|
173
|
+
"useTabs": false
|
|
174
|
+
},
|
|
175
|
+
"create-react-native-library": {
|
|
176
|
+
"type": "library",
|
|
177
|
+
"languages": "js",
|
|
178
|
+
"tools": [
|
|
179
|
+
"jest",
|
|
180
|
+
"lefthook",
|
|
181
|
+
"release-it",
|
|
182
|
+
"eslint"
|
|
183
|
+
],
|
|
184
|
+
"version": "0.57.0"
|
|
185
|
+
},
|
|
186
|
+
"dependencies": {
|
|
187
|
+
"humps": "^2.0.1"
|
|
77
188
|
}
|
|
78
189
|
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import convert from './converter';
|
|
3
|
+
import { StyleSheet } from 'react-native';
|
|
4
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
5
|
+
import { icon, parse } from '@fortawesome/fontawesome-svg-core';
|
|
6
|
+
import type {
|
|
7
|
+
Transform,
|
|
8
|
+
IconProp,
|
|
9
|
+
IconPrefix,
|
|
10
|
+
IconName,
|
|
11
|
+
IconLookup as FAIconLookup,
|
|
12
|
+
IconDefinition as FAIconDefinition,
|
|
13
|
+
} from '@fortawesome/fontawesome-svg-core';
|
|
14
|
+
import log from './logger';
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_SIZE = 16;
|
|
17
|
+
export const DEFAULT_COLOR = '#000';
|
|
18
|
+
export const DEFAULT_SECONDARY_OPACITY = 0.4;
|
|
19
|
+
|
|
20
|
+
export type FontAwesomeIconStyle = StyleProp<ViewStyle> & {
|
|
21
|
+
color?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface Props {
|
|
25
|
+
icon: IconProp;
|
|
26
|
+
/** @deprecated Use size instead */
|
|
27
|
+
height?: number;
|
|
28
|
+
/** @deprecated Use size instead */
|
|
29
|
+
width?: number;
|
|
30
|
+
size?: number;
|
|
31
|
+
color?: string;
|
|
32
|
+
secondaryColor?: string;
|
|
33
|
+
secondaryOpacity?: number;
|
|
34
|
+
mask?: IconProp;
|
|
35
|
+
maskId?: string;
|
|
36
|
+
transform?: string | Transform;
|
|
37
|
+
style?: FontAwesomeIconStyle;
|
|
38
|
+
testID?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type IconLookupOrDefinition = FAIconLookup | FAIconDefinition;
|
|
42
|
+
|
|
43
|
+
interface AbstractElement {
|
|
44
|
+
tag: string;
|
|
45
|
+
attributes: Record<string, unknown>;
|
|
46
|
+
children?: (AbstractElement | string)[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function objectWithKey(
|
|
50
|
+
key: string,
|
|
51
|
+
value: unknown
|
|
52
|
+
): Record<string, unknown> | Record<string, never> {
|
|
53
|
+
return (Array.isArray(value) && value.length > 0) ||
|
|
54
|
+
(!Array.isArray(value) && value)
|
|
55
|
+
? { [key]: value }
|
|
56
|
+
: {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeIconArgs(
|
|
60
|
+
iconArg: IconProp | null | undefined
|
|
61
|
+
): IconLookupOrDefinition | null {
|
|
62
|
+
// If it's a full icon definition object (with prefix, iconName, and icon array),
|
|
63
|
+
// return it as-is so icon() can use it directly
|
|
64
|
+
if (
|
|
65
|
+
iconArg &&
|
|
66
|
+
typeof iconArg === 'object' &&
|
|
67
|
+
'prefix' in iconArg &&
|
|
68
|
+
'iconName' in iconArg &&
|
|
69
|
+
'icon' in iconArg
|
|
70
|
+
) {
|
|
71
|
+
return iconArg as FAIconDefinition;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if ((parse as { icon?: (icon: IconProp) => FAIconLookup }).icon) {
|
|
75
|
+
return (parse as { icon: (icon: IconProp) => FAIconLookup }).icon(
|
|
76
|
+
iconArg as IconProp
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (iconArg === null) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (Array.isArray(iconArg) && iconArg.length === 2) {
|
|
85
|
+
return {
|
|
86
|
+
prefix: iconArg[0] as IconPrefix,
|
|
87
|
+
iconName: iconArg[1] as IconName,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof iconArg === 'string') {
|
|
92
|
+
return { prefix: 'fas' as IconPrefix, iconName: iconArg as IconName };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default function FontAwesomeIcon(
|
|
99
|
+
props: Props
|
|
100
|
+
): React.ReactElement | null {
|
|
101
|
+
const {
|
|
102
|
+
icon: iconArgs,
|
|
103
|
+
mask: maskArgs,
|
|
104
|
+
maskId = null,
|
|
105
|
+
height,
|
|
106
|
+
width,
|
|
107
|
+
size = DEFAULT_SIZE,
|
|
108
|
+
style: styleInput = {},
|
|
109
|
+
color: colorInput = null,
|
|
110
|
+
secondaryColor: secondaryColorInput = null,
|
|
111
|
+
secondaryOpacity: secondaryOpacityInput = null,
|
|
112
|
+
transform: transformInput = null,
|
|
113
|
+
} = props;
|
|
114
|
+
const style = StyleSheet.flatten(styleInput) as Record<
|
|
115
|
+
string,
|
|
116
|
+
unknown
|
|
117
|
+
> | null;
|
|
118
|
+
|
|
119
|
+
const iconLookup = normalizeIconArgs(iconArgs);
|
|
120
|
+
const transform = objectWithKey(
|
|
121
|
+
'transform',
|
|
122
|
+
typeof transformInput === 'string'
|
|
123
|
+
? parse.transform(transformInput)
|
|
124
|
+
: transformInput
|
|
125
|
+
);
|
|
126
|
+
const mask = objectWithKey('mask', normalizeIconArgs(maskArgs));
|
|
127
|
+
|
|
128
|
+
const renderedIcon = icon(iconLookup as IconLookupOrDefinition, {
|
|
129
|
+
...transform,
|
|
130
|
+
...mask,
|
|
131
|
+
maskId: maskId ?? undefined,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!renderedIcon) {
|
|
135
|
+
log('ERROR: icon not found for icon = ', iconArgs);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { abstract } = renderedIcon;
|
|
140
|
+
|
|
141
|
+
// This is the color that will be passed to the "fill" prop of the Svg element
|
|
142
|
+
const color = colorInput || (style || {}).color || DEFAULT_COLOR;
|
|
143
|
+
|
|
144
|
+
// This is the color that will be passed to the "fill" prop of the secondary Path element child (in Duotone Icons)
|
|
145
|
+
// `null` value will result in using the primary color, at 40% opacity
|
|
146
|
+
const secondaryColor = secondaryColorInput || color;
|
|
147
|
+
|
|
148
|
+
// Secondary layer opacity should default to 0.4, unless a specific opacity value or a specific secondary color was given
|
|
149
|
+
const secondaryOpacity = secondaryOpacityInput || DEFAULT_SECONDARY_OPACITY;
|
|
150
|
+
|
|
151
|
+
// To avoid confusion down the line, we'll remove properties from the StyleSheet, like color, that are being overridden
|
|
152
|
+
// or resolved in other ways, to avoid ambiguity as to which inputs cause which outputs in the underlying rendering process.
|
|
153
|
+
// In other words, we don't want color (for example) to be specified via two different inputs.
|
|
154
|
+
// Intentionally extract and discard color from style to avoid ambiguity
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
156
|
+
const { color: _styleColor, ...modifiedStyle } = style || {};
|
|
157
|
+
|
|
158
|
+
let resolvedHeight: number;
|
|
159
|
+
let resolvedWidth: number;
|
|
160
|
+
|
|
161
|
+
if (height || width) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Prop height and width for component ${FontAwesomeIcon.displayName} have been deprecated. ` +
|
|
164
|
+
`Use the size prop instead like <${FontAwesomeIcon.displayName} size={${width}} />.`
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
resolvedHeight = resolvedWidth = size || DEFAULT_SIZE;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const rootAttributes = (abstract[0] as AbstractElement).attributes;
|
|
171
|
+
|
|
172
|
+
rootAttributes.height = resolvedHeight;
|
|
173
|
+
rootAttributes.width = resolvedWidth;
|
|
174
|
+
rootAttributes.style = modifiedStyle;
|
|
175
|
+
|
|
176
|
+
replaceCurrentColor(
|
|
177
|
+
abstract[0] as AbstractElement,
|
|
178
|
+
color as string,
|
|
179
|
+
secondaryColor as string,
|
|
180
|
+
secondaryOpacity
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Expand viewBox for FA7 overflow icon support
|
|
184
|
+
// This must happen before percentage replacement to ensure correct dimensions
|
|
185
|
+
if (rootAttributes.viewBox) {
|
|
186
|
+
rootAttributes.viewBox = expandViewBox(rootAttributes.viewBox as string);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Parse viewBox to get dimensions for percentage replacement
|
|
190
|
+
// viewBox format: "minX minY width height" e.g., "0 -32 512 576" (after expansion)
|
|
191
|
+
const viewBox = rootAttributes.viewBox as string | undefined;
|
|
192
|
+
if (viewBox) {
|
|
193
|
+
const parts = viewBox.split(' ').map(Number);
|
|
194
|
+
if (parts.length === 4) {
|
|
195
|
+
const vbWidth = parts[2];
|
|
196
|
+
const vbHeight = parts[3];
|
|
197
|
+
if (vbWidth !== undefined && vbHeight !== undefined) {
|
|
198
|
+
replacePercentages(abstract[0] as AbstractElement, vbWidth, vbHeight);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// AbstractElement input always produces a ReactElement (not string), so cast is safe
|
|
204
|
+
return convertCurry(abstract[0] as AbstractElement) as React.ReactElement;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
FontAwesomeIcon.displayName = 'FontAwesomeIcon';
|
|
208
|
+
|
|
209
|
+
const convertCurry = convert.bind(null, React.createElement);
|
|
210
|
+
|
|
211
|
+
function replaceCurrentColor(
|
|
212
|
+
obj: AbstractElement,
|
|
213
|
+
primaryColor: string,
|
|
214
|
+
secondaryColor: string,
|
|
215
|
+
secondaryOpacity: number
|
|
216
|
+
): void {
|
|
217
|
+
(obj.children || []).forEach((child) => {
|
|
218
|
+
if (typeof child === 'string') return;
|
|
219
|
+
|
|
220
|
+
replaceFill(child, primaryColor, secondaryColor, secondaryOpacity);
|
|
221
|
+
|
|
222
|
+
if (Object.prototype.hasOwnProperty.call(child, 'attributes')) {
|
|
223
|
+
replaceFill(
|
|
224
|
+
child.attributes,
|
|
225
|
+
primaryColor,
|
|
226
|
+
secondaryColor,
|
|
227
|
+
secondaryOpacity
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (Array.isArray(child.children) && child.children.length > 0) {
|
|
232
|
+
replaceCurrentColor(
|
|
233
|
+
child,
|
|
234
|
+
primaryColor,
|
|
235
|
+
secondaryColor,
|
|
236
|
+
secondaryOpacity
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function replaceFill(
|
|
243
|
+
obj: Record<string, unknown> | AbstractElement,
|
|
244
|
+
primaryColor: string,
|
|
245
|
+
secondaryColor: string,
|
|
246
|
+
secondaryOpacity: number
|
|
247
|
+
): void {
|
|
248
|
+
if (hasPropertySetToValue(obj, 'fill', 'currentColor')) {
|
|
249
|
+
if (hasPropertySetToValue(obj, 'class', 'fa-primary')) {
|
|
250
|
+
(obj as Record<string, unknown>).fill = primaryColor;
|
|
251
|
+
} else if (hasPropertySetToValue(obj, 'class', 'fa-secondary')) {
|
|
252
|
+
(obj as Record<string, unknown>).fill = secondaryColor;
|
|
253
|
+
(obj as Record<string, unknown>).fillOpacity = secondaryOpacity;
|
|
254
|
+
} else {
|
|
255
|
+
(obj as Record<string, unknown>).fill = primaryColor;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function hasPropertySetToValue(
|
|
261
|
+
obj: Record<string, unknown> | AbstractElement,
|
|
262
|
+
property: string,
|
|
263
|
+
value: unknown
|
|
264
|
+
): boolean {
|
|
265
|
+
return (
|
|
266
|
+
Object.prototype.hasOwnProperty.call(obj, property) &&
|
|
267
|
+
(obj as Record<string, unknown>)[property] === value
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Expands the viewBox to accommodate Font Awesome 7 icons that overflow their standard viewBox.
|
|
273
|
+
* FA7 introduced icons where paths extend beyond the declared viewBox boundaries.
|
|
274
|
+
* React Native clips content to the viewBox (unlike web browsers which support CSS overflow: visible).
|
|
275
|
+
* This expansion adds vertical space by subtracting 32 from minY and adding 64 to height.
|
|
276
|
+
*
|
|
277
|
+
* @param viewBox - The original viewBox string in format "minX minY width height"
|
|
278
|
+
* @returns The expanded viewBox string with adjusted minY and height
|
|
279
|
+
*/
|
|
280
|
+
export function expandViewBox(viewBox: string): string {
|
|
281
|
+
const parts = viewBox.split(' ').map(Number);
|
|
282
|
+
if (parts.length !== 4) return viewBox;
|
|
283
|
+
|
|
284
|
+
const minX = parts[0];
|
|
285
|
+
const minY = parts[1];
|
|
286
|
+
const width = parts[2];
|
|
287
|
+
const height = parts[3];
|
|
288
|
+
|
|
289
|
+
// Validate that all parts are valid numbers
|
|
290
|
+
if (
|
|
291
|
+
minX === undefined ||
|
|
292
|
+
minY === undefined ||
|
|
293
|
+
width === undefined ||
|
|
294
|
+
height === undefined ||
|
|
295
|
+
isNaN(minX) ||
|
|
296
|
+
isNaN(minY) ||
|
|
297
|
+
isNaN(width) ||
|
|
298
|
+
isNaN(height)
|
|
299
|
+
) {
|
|
300
|
+
return viewBox;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Expand for FA7 overflow icons: subtract 32 from minY, add 64 to height
|
|
304
|
+
return `${minX} ${minY - 32} ${width} ${height + 64}`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* react-native-svg has issues with percentage values like "100%" in masks and rects.
|
|
309
|
+
* This function replaces percentage values with actual numeric values based on the viewBox.
|
|
310
|
+
*/
|
|
311
|
+
function replacePercentages(
|
|
312
|
+
obj: AbstractElement,
|
|
313
|
+
viewBoxWidth: number,
|
|
314
|
+
viewBoxHeight: number
|
|
315
|
+
): void {
|
|
316
|
+
const attrs = obj.attributes as Record<string, unknown> | undefined;
|
|
317
|
+
if (attrs) {
|
|
318
|
+
if (attrs.width === '100%') {
|
|
319
|
+
attrs.width = viewBoxWidth;
|
|
320
|
+
}
|
|
321
|
+
if (attrs.height === '100%') {
|
|
322
|
+
attrs.height = viewBoxHeight;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
(obj.children || []).forEach((child) => {
|
|
327
|
+
if (typeof child !== 'string') {
|
|
328
|
+
replacePercentages(child, viewBoxWidth, viewBoxHeight);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
package/src/converter.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import humps from 'humps';
|
|
3
|
+
import { Svg, Path, Rect, Defs, Mask, G, ClipPath } from 'react-native-svg';
|
|
4
|
+
|
|
5
|
+
interface AbstractElement {
|
|
6
|
+
tag: string;
|
|
7
|
+
attributes?: Record<string, unknown>;
|
|
8
|
+
children?: (AbstractElement | string)[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type CreateElementFn = typeof import('react').createElement;
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- required for dynamic component lookup from FA abstract elements
|
|
14
|
+
const svgObjectMap: Record<string, React.ComponentType<any>> = {
|
|
15
|
+
svg: Svg,
|
|
16
|
+
path: Path,
|
|
17
|
+
rect: Rect,
|
|
18
|
+
defs: Defs,
|
|
19
|
+
mask: Mask,
|
|
20
|
+
g: G,
|
|
21
|
+
clipPath: ClipPath,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function convert(
|
|
25
|
+
createElement: CreateElementFn,
|
|
26
|
+
element: AbstractElement | string
|
|
27
|
+
): React.ReactNode {
|
|
28
|
+
if (typeof element === 'string') {
|
|
29
|
+
return element;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const children = (element.children || []).map((child) => {
|
|
33
|
+
return convert(createElement, child);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const mixins = Object.keys(element.attributes || {}).reduce(
|
|
37
|
+
(acc, key) => {
|
|
38
|
+
const val = (element.attributes as Record<string, unknown>)[key];
|
|
39
|
+
switch (key) {
|
|
40
|
+
case 'class':
|
|
41
|
+
case 'role':
|
|
42
|
+
case 'xmlns':
|
|
43
|
+
delete (element.attributes as Record<string, unknown>)[key];
|
|
44
|
+
break;
|
|
45
|
+
case 'focusable':
|
|
46
|
+
acc.attrs[key] = val === 'true';
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
if (
|
|
50
|
+
key.indexOf('aria-') === 0 ||
|
|
51
|
+
key.indexOf('data-') === 0 ||
|
|
52
|
+
(key === 'fill' && val === 'currentColor')
|
|
53
|
+
) {
|
|
54
|
+
delete (element.attributes as Record<string, unknown>)[key];
|
|
55
|
+
} else {
|
|
56
|
+
acc.attrs[humps.camelize(key)] = val;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return acc;
|
|
60
|
+
},
|
|
61
|
+
{ attrs: {} as Record<string, unknown> }
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return createElement(
|
|
65
|
+
svgObjectMap[element.tag] as React.ComponentType,
|
|
66
|
+
{ ...mixins.attrs },
|
|
67
|
+
...children
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default convert;
|
package/src/index.tsx
ADDED