@elementor/editor-canvas 4.4.0-1070 → 4.4.0-1071

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/dist/index.js CHANGED
@@ -2132,6 +2132,8 @@ function processSvgContent(svgText) {
2132
2132
  var FONT_AWESOME_JSON = {
2133
2133
  width: 0,
2134
2134
  height: 1,
2135
+ aliases: 2,
2136
+ unicode: 3,
2135
2137
  path: 4
2136
2138
  };
2137
2139
  var fontAwesomeJsonCache = /* @__PURE__ */ new Map();
@@ -2152,7 +2154,10 @@ var iconTransformer = createTransformer(async (value, { signal }) => {
2152
2154
  return { html: null, url: null };
2153
2155
  }
2154
2156
  const svgText = buildFontAwesomeSvg(iconData);
2155
- const html = processSvgContent(svgText);
2157
+ if (!svgText) {
2158
+ return { html: null, url: null };
2159
+ }
2160
+ const html = processIconSvgContent(svgText);
2156
2161
  return { html, url: null };
2157
2162
  });
2158
2163
  function getFontAwesomeIconName(iconValue) {
@@ -2160,14 +2165,22 @@ function getFontAwesomeIconName(iconValue) {
2160
2165
  return match?.[1] ?? null;
2161
2166
  }
2162
2167
  function getFontAwesomeJsonFileName(library) {
2163
- if (!library.startsWith("fa-")) {
2168
+ const fileName = library.replace(/^fa-/, "");
2169
+ const config = getFontAwesome7EditorConfig();
2170
+ if (!config?.jsonFiles.includes(fileName)) {
2164
2171
  return null;
2165
2172
  }
2166
- return library.replace(/^fa-/, "");
2173
+ return fileName;
2167
2174
  }
2168
- function getAssetsBaseUrl() {
2169
- const assetsUrl = window.elementorCommon?.config?.urls?.assets;
2170
- return typeof assetsUrl === "string" && assetsUrl !== "" ? assetsUrl : null;
2175
+ function getFontAwesome7EditorConfig() {
2176
+ const config = window.elementorCommon?.config?.fontAwesome?.v7;
2177
+ if (!config || !Array.isArray(config.jsonFiles) || typeof config.jsonBaseUrl !== "string" || config.jsonBaseUrl === "") {
2178
+ return null;
2179
+ }
2180
+ return {
2181
+ jsonFiles: config.jsonFiles,
2182
+ jsonBaseUrl: config.jsonBaseUrl
2183
+ };
2171
2184
  }
2172
2185
  async function fetchFontAwesomeIcons(jsonFileName, signal) {
2173
2186
  const cached = fontAwesomeJsonCache.get(jsonFileName);
@@ -2181,26 +2194,83 @@ async function fetchFontAwesomeIcons(jsonFileName, signal) {
2181
2194
  return icons;
2182
2195
  }
2183
2196
  async function loadFontAwesomeIcons(jsonFileName, signal) {
2184
- const assetsUrl = getAssetsBaseUrl();
2185
- if (!assetsUrl) {
2197
+ const config = getFontAwesome7EditorConfig();
2198
+ if (!config?.jsonFiles.includes(jsonFileName)) {
2186
2199
  return null;
2187
2200
  }
2188
2201
  try {
2189
- const response = await fetch(`${assetsUrl}lib/font-awesome/json/${jsonFileName}.json`, { signal });
2202
+ const response = await fetch(`${config.jsonBaseUrl}${jsonFileName}.json`, {
2203
+ signal
2204
+ });
2190
2205
  if (!response.ok) {
2191
2206
  return null;
2192
2207
  }
2193
2208
  const data = await response.json();
2194
- return data.icons ?? null;
2209
+ const icons = data.icons;
2210
+ if (!icons || typeof icons !== "object") {
2211
+ return null;
2212
+ }
2213
+ return indexFontAwesomeIcons(icons);
2195
2214
  } catch {
2196
2215
  return null;
2197
2216
  }
2198
2217
  }
2218
+ function indexFontAwesomeIcons(icons) {
2219
+ const index = {};
2220
+ for (const [name, iconData] of Object.entries(icons)) {
2221
+ if (!isValidIconTuple(iconData)) {
2222
+ continue;
2223
+ }
2224
+ index[name] = iconData;
2225
+ for (const alias of iconData[FONT_AWESOME_JSON.aliases]) {
2226
+ if (typeof alias === "string" && alias !== "" && !index[alias]) {
2227
+ index[alias] = iconData;
2228
+ }
2229
+ }
2230
+ }
2231
+ return index;
2232
+ }
2233
+ function isValidIconTuple(iconData) {
2234
+ return Array.isArray(iconData) && iconData.length >= 5 && typeof iconData[FONT_AWESOME_JSON.width] === "number" && typeof iconData[FONT_AWESOME_JSON.height] === "number" && Array.isArray(iconData[FONT_AWESOME_JSON.aliases]);
2235
+ }
2199
2236
  function buildFontAwesomeSvg(iconData) {
2200
2237
  const width = iconData[FONT_AWESOME_JSON.width];
2201
2238
  const height = iconData[FONT_AWESOME_JSON.height];
2202
- const path = iconData[FONT_AWESOME_JSON.path];
2203
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"><path d="${path}"></path></svg>`;
2239
+ const paths = normalizePaths(iconData[FONT_AWESOME_JSON.path]);
2240
+ if (paths.length === 0) {
2241
+ return null;
2242
+ }
2243
+ const pathMarkup = paths.map((path) => `<path d="${escapeSvgPath(path)}"></path>`).join("");
2244
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${pathMarkup}</svg>`;
2245
+ }
2246
+ function normalizePaths(pathData) {
2247
+ if (typeof pathData === "string" && pathData !== "") {
2248
+ return [pathData];
2249
+ }
2250
+ if (!Array.isArray(pathData)) {
2251
+ return [];
2252
+ }
2253
+ return pathData.filter((path) => typeof path === "string" && path !== "");
2254
+ }
2255
+ function escapeSvgPath(path) {
2256
+ return path.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2257
+ }
2258
+ function processIconSvgContent(svgText) {
2259
+ const html = processSvgContent(svgText);
2260
+ if (!html) {
2261
+ return null;
2262
+ }
2263
+ const parser = new DOMParser();
2264
+ const doc = parser.parseFromString(html, "image/svg+xml");
2265
+ const svgElement = doc.querySelector("svg");
2266
+ if (!svgElement) {
2267
+ return null;
2268
+ }
2269
+ svgElement.setAttribute("aria-hidden", "true");
2270
+ svgElement.style.setProperty("width", "100%");
2271
+ svgElement.style.setProperty("height", "100%");
2272
+ svgElement.style.setProperty("overflow", "visible");
2273
+ return svgElement.outerHTML;
2204
2274
  }
2205
2275
 
2206
2276
  // src/transformers/shared/image-src-transformer.ts
package/dist/index.mjs CHANGED
@@ -2084,6 +2084,8 @@ function processSvgContent(svgText) {
2084
2084
  var FONT_AWESOME_JSON = {
2085
2085
  width: 0,
2086
2086
  height: 1,
2087
+ aliases: 2,
2088
+ unicode: 3,
2087
2089
  path: 4
2088
2090
  };
2089
2091
  var fontAwesomeJsonCache = /* @__PURE__ */ new Map();
@@ -2104,7 +2106,10 @@ var iconTransformer = createTransformer(async (value, { signal }) => {
2104
2106
  return { html: null, url: null };
2105
2107
  }
2106
2108
  const svgText = buildFontAwesomeSvg(iconData);
2107
- const html = processSvgContent(svgText);
2109
+ if (!svgText) {
2110
+ return { html: null, url: null };
2111
+ }
2112
+ const html = processIconSvgContent(svgText);
2108
2113
  return { html, url: null };
2109
2114
  });
2110
2115
  function getFontAwesomeIconName(iconValue) {
@@ -2112,14 +2117,22 @@ function getFontAwesomeIconName(iconValue) {
2112
2117
  return match?.[1] ?? null;
2113
2118
  }
2114
2119
  function getFontAwesomeJsonFileName(library) {
2115
- if (!library.startsWith("fa-")) {
2120
+ const fileName = library.replace(/^fa-/, "");
2121
+ const config = getFontAwesome7EditorConfig();
2122
+ if (!config?.jsonFiles.includes(fileName)) {
2116
2123
  return null;
2117
2124
  }
2118
- return library.replace(/^fa-/, "");
2125
+ return fileName;
2119
2126
  }
2120
- function getAssetsBaseUrl() {
2121
- const assetsUrl = window.elementorCommon?.config?.urls?.assets;
2122
- return typeof assetsUrl === "string" && assetsUrl !== "" ? assetsUrl : null;
2127
+ function getFontAwesome7EditorConfig() {
2128
+ const config = window.elementorCommon?.config?.fontAwesome?.v7;
2129
+ if (!config || !Array.isArray(config.jsonFiles) || typeof config.jsonBaseUrl !== "string" || config.jsonBaseUrl === "") {
2130
+ return null;
2131
+ }
2132
+ return {
2133
+ jsonFiles: config.jsonFiles,
2134
+ jsonBaseUrl: config.jsonBaseUrl
2135
+ };
2123
2136
  }
2124
2137
  async function fetchFontAwesomeIcons(jsonFileName, signal) {
2125
2138
  const cached = fontAwesomeJsonCache.get(jsonFileName);
@@ -2133,26 +2146,83 @@ async function fetchFontAwesomeIcons(jsonFileName, signal) {
2133
2146
  return icons;
2134
2147
  }
2135
2148
  async function loadFontAwesomeIcons(jsonFileName, signal) {
2136
- const assetsUrl = getAssetsBaseUrl();
2137
- if (!assetsUrl) {
2149
+ const config = getFontAwesome7EditorConfig();
2150
+ if (!config?.jsonFiles.includes(jsonFileName)) {
2138
2151
  return null;
2139
2152
  }
2140
2153
  try {
2141
- const response = await fetch(`${assetsUrl}lib/font-awesome/json/${jsonFileName}.json`, { signal });
2154
+ const response = await fetch(`${config.jsonBaseUrl}${jsonFileName}.json`, {
2155
+ signal
2156
+ });
2142
2157
  if (!response.ok) {
2143
2158
  return null;
2144
2159
  }
2145
2160
  const data = await response.json();
2146
- return data.icons ?? null;
2161
+ const icons = data.icons;
2162
+ if (!icons || typeof icons !== "object") {
2163
+ return null;
2164
+ }
2165
+ return indexFontAwesomeIcons(icons);
2147
2166
  } catch {
2148
2167
  return null;
2149
2168
  }
2150
2169
  }
2170
+ function indexFontAwesomeIcons(icons) {
2171
+ const index = {};
2172
+ for (const [name, iconData] of Object.entries(icons)) {
2173
+ if (!isValidIconTuple(iconData)) {
2174
+ continue;
2175
+ }
2176
+ index[name] = iconData;
2177
+ for (const alias of iconData[FONT_AWESOME_JSON.aliases]) {
2178
+ if (typeof alias === "string" && alias !== "" && !index[alias]) {
2179
+ index[alias] = iconData;
2180
+ }
2181
+ }
2182
+ }
2183
+ return index;
2184
+ }
2185
+ function isValidIconTuple(iconData) {
2186
+ return Array.isArray(iconData) && iconData.length >= 5 && typeof iconData[FONT_AWESOME_JSON.width] === "number" && typeof iconData[FONT_AWESOME_JSON.height] === "number" && Array.isArray(iconData[FONT_AWESOME_JSON.aliases]);
2187
+ }
2151
2188
  function buildFontAwesomeSvg(iconData) {
2152
2189
  const width = iconData[FONT_AWESOME_JSON.width];
2153
2190
  const height = iconData[FONT_AWESOME_JSON.height];
2154
- const path = iconData[FONT_AWESOME_JSON.path];
2155
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"><path d="${path}"></path></svg>`;
2191
+ const paths = normalizePaths(iconData[FONT_AWESOME_JSON.path]);
2192
+ if (paths.length === 0) {
2193
+ return null;
2194
+ }
2195
+ const pathMarkup = paths.map((path) => `<path d="${escapeSvgPath(path)}"></path>`).join("");
2196
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${pathMarkup}</svg>`;
2197
+ }
2198
+ function normalizePaths(pathData) {
2199
+ if (typeof pathData === "string" && pathData !== "") {
2200
+ return [pathData];
2201
+ }
2202
+ if (!Array.isArray(pathData)) {
2203
+ return [];
2204
+ }
2205
+ return pathData.filter((path) => typeof path === "string" && path !== "");
2206
+ }
2207
+ function escapeSvgPath(path) {
2208
+ return path.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2209
+ }
2210
+ function processIconSvgContent(svgText) {
2211
+ const html = processSvgContent(svgText);
2212
+ if (!html) {
2213
+ return null;
2214
+ }
2215
+ const parser = new DOMParser();
2216
+ const doc = parser.parseFromString(html, "image/svg+xml");
2217
+ const svgElement = doc.querySelector("svg");
2218
+ if (!svgElement) {
2219
+ return null;
2220
+ }
2221
+ svgElement.setAttribute("aria-hidden", "true");
2222
+ svgElement.style.setProperty("width", "100%");
2223
+ svgElement.style.setProperty("height", "100%");
2224
+ svgElement.style.setProperty("overflow", "visible");
2225
+ return svgElement.outerHTML;
2156
2226
  }
2157
2227
 
2158
2228
  // src/transformers/shared/image-src-transformer.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-canvas",
3
3
  "description": "Elementor Editor Canvas",
4
- "version": "4.4.0-1070",
4
+ "version": "4.4.0-1071",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,26 +37,26 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor": "4.4.0-1070",
41
- "@elementor/editor-controls": "4.4.0-1070",
42
- "@elementor/editor-documents": "4.4.0-1070",
43
- "@elementor/editor-elements": "4.4.0-1070",
44
- "@elementor/editor-interactions": "4.4.0-1070",
45
- "@elementor/editor-mcp": "4.4.0-1070",
46
- "@elementor/editor-notifications": "4.4.0-1070",
47
- "@elementor/editor-props": "4.4.0-1070",
48
- "@elementor/editor-responsive": "4.4.0-1070",
49
- "@elementor/editor-styles": "4.4.0-1070",
50
- "@elementor/editor-styles-repository": "4.4.0-1070",
51
- "@elementor/editor-ui": "4.4.0-1070",
52
- "@elementor/editor-v1-adapters": "4.4.0-1070",
53
- "@elementor/events": "4.4.0-1070",
54
- "@elementor/http-client": "4.4.0-1070",
55
- "@elementor/schema": "4.4.0-1070",
56
- "@elementor/twing": "4.4.0-1070",
40
+ "@elementor/editor": "4.4.0-1071",
41
+ "@elementor/editor-controls": "4.4.0-1071",
42
+ "@elementor/editor-documents": "4.4.0-1071",
43
+ "@elementor/editor-elements": "4.4.0-1071",
44
+ "@elementor/editor-interactions": "4.4.0-1071",
45
+ "@elementor/editor-mcp": "4.4.0-1071",
46
+ "@elementor/editor-notifications": "4.4.0-1071",
47
+ "@elementor/editor-props": "4.4.0-1071",
48
+ "@elementor/editor-responsive": "4.4.0-1071",
49
+ "@elementor/editor-styles": "4.4.0-1071",
50
+ "@elementor/editor-styles-repository": "4.4.0-1071",
51
+ "@elementor/editor-ui": "4.4.0-1071",
52
+ "@elementor/editor-v1-adapters": "4.4.0-1071",
53
+ "@elementor/events": "4.4.0-1071",
54
+ "@elementor/http-client": "4.4.0-1071",
55
+ "@elementor/schema": "4.4.0-1071",
56
+ "@elementor/twing": "4.4.0-1071",
57
57
  "@elementor/ui": "1.37.5",
58
- "@elementor/utils": "4.4.0-1070",
59
- "@elementor/wp-media": "4.4.0-1070",
58
+ "@elementor/utils": "4.4.0-1071",
59
+ "@elementor/wp-media": "4.4.0-1071",
60
60
  "@floating-ui/react": "^0.27.5",
61
61
  "@wordpress/i18n": "^5.13.0",
62
62
  "dompurify": "^3.2.6"
@@ -36,6 +36,17 @@ export function linkPropType() {
36
36
  } );
37
37
  }
38
38
 
39
+ export function iconPropType() {
40
+ return createMockPropType( {
41
+ kind: 'object',
42
+ key: 'icon',
43
+ shape: {
44
+ value: stringPropType(),
45
+ library: stringPropType(),
46
+ },
47
+ } );
48
+ }
49
+
39
50
  export function imagePropType() {
40
51
  return createMockPropType( {
41
52
  kind: 'object',
@@ -1,7 +1,13 @@
1
- import { iconTransformer, resetFontAwesomeIconsCache } from '../icon-transformer';
1
+ import { iconPropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
2
+
3
+ import { iconPropType } from '../../../__tests__/prop-types';
4
+ import { initSettingsTransformers } from '../../../init-settings-transformers';
5
+ import { createPropsResolver } from '../../../renderers/create-props-resolver';
6
+ import { settingsTransformersRegistry } from '../../../settings-transformers-registry';
7
+ import { resetFontAwesomeIconsCache } from '../icon-transformer';
2
8
 
3
9
  const STAR_PATH = 'M0 0h100v100H0z';
4
- const STAR_WIDTH = 512;
10
+ const STAR_WIDTH = 576;
5
11
  const STAR_HEIGHT = 512;
6
12
 
7
13
  const mockFetch = ( body: unknown, ok = true ) => {
@@ -11,17 +17,46 @@ const mockFetch = ( body: unknown, ok = true ) => {
11
17
  } );
12
18
  };
13
19
 
20
+ function createSavedIcon( iconClass: string, library: string ) {
21
+ return iconPropTypeUtil.create( {
22
+ value: stringPropTypeUtil.create( iconClass ),
23
+ library: stringPropTypeUtil.create( library ),
24
+ } );
25
+ }
26
+
27
+ async function resolveSavedIcon( iconClass: string, library: string ) {
28
+ const resolve = createPropsResolver( {
29
+ transformers: settingsTransformersRegistry,
30
+ schema: { svg: iconPropType() },
31
+ } );
32
+
33
+ const result = await resolve( {
34
+ props: {
35
+ svg: createSavedIcon( iconClass, library ),
36
+ },
37
+ } );
38
+
39
+ return result.svg;
40
+ }
41
+
14
42
  describe( 'iconTransformer', () => {
15
43
  const originalElementorCommon = window.elementorCommon;
16
44
 
17
45
  beforeEach( () => {
18
46
  jest.clearAllMocks();
19
47
  resetFontAwesomeIconsCache();
48
+ initSettingsTransformers();
20
49
  window.elementorCommon = {
21
50
  config: {
22
51
  urls: {
23
52
  assets: 'https://example.com/assets/',
24
53
  },
54
+ fontAwesome: {
55
+ v7: {
56
+ jsonFiles: [ 'solid', 'regular', 'brands' ],
57
+ jsonBaseUrl: 'https://example.com/assets/lib/font-awesome-7/json/',
58
+ },
59
+ },
25
60
  },
26
61
  };
27
62
  } );
@@ -40,10 +75,10 @@ describe( 'iconTransformer', () => {
40
75
  } );
41
76
 
42
77
  // Act.
43
- const result = await iconTransformer( { value: 'fas fa-star', library: 'fa-solid' }, { key: 'svg' } );
78
+ const result = await resolveSavedIcon( 'fas fa-star', 'fa-solid' );
44
79
 
45
80
  // Assert.
46
- expect( global.fetch ).toHaveBeenCalledWith( 'https://example.com/assets/lib/font-awesome/json/solid.json', {
81
+ expect( global.fetch ).toHaveBeenCalledWith( 'https://example.com/assets/lib/font-awesome-7/json/solid.json', {
47
82
  signal: undefined,
48
83
  } );
49
84
  expect( result ).toEqual( {
@@ -51,6 +86,45 @@ describe( 'iconTransformer', () => {
51
86
  url: null,
52
87
  } );
53
88
  expect( ( result as { html: string } ).html ).toContain( 'fill="currentColor"' );
89
+ expect( ( result as { html: string } ).html ).toContain( `viewBox="0 0 ${ STAR_WIDTH } ${ STAR_HEIGHT }"` );
90
+ expect( ( result as { html: string } ).html ).toContain( 'aria-hidden="true"' );
91
+ expect( ( result as { html: string } ).html ).toContain( 'overflow: visible' );
92
+ } );
93
+
94
+ it( 'resolves icons by alias name', async () => {
95
+ // Arrange.
96
+ mockFetch( {
97
+ icons: {
98
+ headphones: [ 448, 512, [ 'headphones-simple' ], 'f025', 'M0 0' ],
99
+ },
100
+ } );
101
+
102
+ // Act.
103
+ const result = await resolveSavedIcon( 'fas fa-headphones-simple', 'fa-solid' );
104
+
105
+ // Assert.
106
+ expect( result ).toEqual( {
107
+ html: expect.stringContaining( 'M0 0' ),
108
+ url: null,
109
+ } );
110
+ } );
111
+
112
+ it( 'renders multiple paths when the icon definition contains an array', async () => {
113
+ // Arrange.
114
+ mockFetch( {
115
+ icons: {
116
+ slash: [ 640, 640, [], 'f715', [ 'M0 0', 'M10 10' ] ],
117
+ },
118
+ } );
119
+
120
+ // Act.
121
+ const result = await resolveSavedIcon( 'fas fa-slash', 'fa-solid' );
122
+
123
+ // Assert.
124
+ const html = ( result as { html: string } ).html;
125
+ expect( html.match( /<path/g ) ).toHaveLength( 2 );
126
+ expect( html ).toContain( 'M0 0' );
127
+ expect( html ).toContain( 'M10 10' );
54
128
  } );
55
129
 
56
130
  it( 'returns null html when the icon cannot be resolved', async () => {
@@ -58,17 +132,68 @@ describe( 'iconTransformer', () => {
58
132
  mockFetch( { icons: {} } );
59
133
 
60
134
  // Act.
61
- const result = await iconTransformer( { value: 'fas fa-missing', library: 'fa-solid' }, { key: 'svg' } );
135
+ const result = await resolveSavedIcon( 'fas fa-missing', 'fa-solid' );
62
136
 
63
137
  // Assert.
64
138
  expect( result ).toEqual( { html: null, url: null } );
65
139
  } );
66
140
 
67
- it( 'returns null html when value or library is missing', async () => {
141
+ it( 'fetches json from the localized font awesome 7 base url', async () => {
142
+ // Arrange.
143
+ const jsonBaseUrl = 'https://cdn.example.com/fa7/json/';
144
+ window.elementorCommon = {
145
+ config: {
146
+ fontAwesome: {
147
+ v7: {
148
+ jsonFiles: [ 'solid', 'regular', 'brands' ],
149
+ jsonBaseUrl,
150
+ },
151
+ },
152
+ },
153
+ };
154
+ mockFetch( {
155
+ icons: {
156
+ star: [ STAR_WIDTH, STAR_HEIGHT, [], 'f005', STAR_PATH ],
157
+ },
158
+ } );
159
+
68
160
  // Act.
69
- const result = await iconTransformer( { value: 'fas fa-star' }, { key: 'svg' } );
161
+ await resolveSavedIcon( 'fas fa-star', 'fa-solid' );
70
162
 
71
163
  // Assert.
164
+ expect( global.fetch ).toHaveBeenCalledWith( `${ jsonBaseUrl }solid.json`, {
165
+ signal: undefined,
166
+ } );
167
+ } );
168
+
169
+ it( 'does not fetch json when library is not an allowed fa7 file', async () => {
170
+ // Arrange.
171
+ mockFetch( { icons: {} } );
172
+
173
+ // Act.
174
+ const result = await resolveSavedIcon( 'fas fa-star', 'fa-../../wp-config' );
175
+
176
+ // Assert.
177
+ expect( global.fetch ).not.toHaveBeenCalled();
72
178
  expect( result ).toEqual( { html: null, url: null } );
73
179
  } );
180
+
181
+ it( 'returns null html when value or library is missing', async () => {
182
+ // Act.
183
+ const resolve = createPropsResolver( {
184
+ transformers: settingsTransformersRegistry,
185
+ schema: { svg: iconPropType() },
186
+ } );
187
+ const result = await resolve( {
188
+ props: {
189
+ svg: iconPropTypeUtil.create( {
190
+ value: stringPropTypeUtil.create( 'fas fa-star' ),
191
+ library: stringPropTypeUtil.create( '' ),
192
+ } ),
193
+ },
194
+ } );
195
+
196
+ // Assert.
197
+ expect( result.svg ).toEqual( { html: null, url: null } );
198
+ } );
74
199
  } );
@@ -7,11 +7,18 @@ type IconValue = {
7
7
  library?: unknown;
8
8
  };
9
9
 
10
- type FontAwesomeIconJson = [ number, number, unknown, unknown, string ];
10
+ type FontAwesomeIconJson = [ number, number, unknown[], unknown, string | string[] ];
11
+
12
+ type FontAwesome7EditorConfig = {
13
+ jsonFiles: string[];
14
+ jsonBaseUrl: string;
15
+ };
11
16
 
12
17
  const FONT_AWESOME_JSON = {
13
18
  width: 0,
14
19
  height: 1,
20
+ aliases: 2,
21
+ unicode: 3,
15
22
  path: 4,
16
23
  } as const;
17
24
 
@@ -40,7 +47,12 @@ export const iconTransformer = createTransformer( async ( value: IconValue, { si
40
47
  }
41
48
 
42
49
  const svgText = buildFontAwesomeSvg( iconData );
43
- const html = processSvgContent( svgText );
50
+
51
+ if ( ! svgText ) {
52
+ return { html: null, url: null };
53
+ }
54
+
55
+ const html = processIconSvgContent( svgText );
44
56
 
45
57
  return { html, url: null };
46
58
  } );
@@ -52,17 +64,32 @@ function getFontAwesomeIconName( iconValue: string ): string | null {
52
64
  }
53
65
 
54
66
  function getFontAwesomeJsonFileName( library: string ): string | null {
55
- if ( ! library.startsWith( 'fa-' ) ) {
67
+ const fileName = library.replace( /^fa-/, '' );
68
+ const config = getFontAwesome7EditorConfig();
69
+
70
+ if ( ! config?.jsonFiles.includes( fileName ) ) {
56
71
  return null;
57
72
  }
58
73
 
59
- return library.replace( /^fa-/, '' );
74
+ return fileName;
60
75
  }
61
76
 
62
- function getAssetsBaseUrl(): string | null {
63
- const assetsUrl = window.elementorCommon?.config?.urls?.assets;
77
+ function getFontAwesome7EditorConfig(): FontAwesome7EditorConfig | null {
78
+ const config = window.elementorCommon?.config?.fontAwesome?.v7;
79
+
80
+ if (
81
+ ! config ||
82
+ ! Array.isArray( config.jsonFiles ) ||
83
+ typeof config.jsonBaseUrl !== 'string' ||
84
+ config.jsonBaseUrl === ''
85
+ ) {
86
+ return null;
87
+ }
64
88
 
65
- return typeof assetsUrl === 'string' && assetsUrl !== '' ? assetsUrl : null;
89
+ return {
90
+ jsonFiles: config.jsonFiles,
91
+ jsonBaseUrl: config.jsonBaseUrl,
92
+ };
66
93
  }
67
94
 
68
95
  async function fetchFontAwesomeIcons(
@@ -88,33 +115,115 @@ async function loadFontAwesomeIcons(
88
115
  jsonFileName: string,
89
116
  signal?: AbortSignal
90
117
  ): Promise< Record< string, FontAwesomeIconJson > | null > {
91
- const assetsUrl = getAssetsBaseUrl();
118
+ const config = getFontAwesome7EditorConfig();
92
119
 
93
- if ( ! assetsUrl ) {
120
+ if ( ! config?.jsonFiles.includes( jsonFileName ) ) {
94
121
  return null;
95
122
  }
96
123
 
97
124
  try {
98
- const response = await fetch( `${ assetsUrl }lib/font-awesome/json/${ jsonFileName }.json`, { signal } );
125
+ const response = await fetch( `${ config.jsonBaseUrl }${ jsonFileName }.json`, {
126
+ signal,
127
+ } );
99
128
 
100
129
  if ( ! response.ok ) {
101
130
  return null;
102
131
  }
103
132
 
104
133
  const data = ( await response.json() ) as { icons?: Record< string, FontAwesomeIconJson > };
134
+ const icons = data.icons;
105
135
 
106
- return data.icons ?? null;
136
+ if ( ! icons || typeof icons !== 'object' ) {
137
+ return null;
138
+ }
139
+
140
+ return indexFontAwesomeIcons( icons );
107
141
  } catch {
108
142
  return null;
109
143
  }
110
144
  }
111
145
 
112
- function buildFontAwesomeSvg( iconData: FontAwesomeIconJson ): string {
146
+ function indexFontAwesomeIcons( icons: Record< string, FontAwesomeIconJson > ): Record< string, FontAwesomeIconJson > {
147
+ const index: Record< string, FontAwesomeIconJson > = {};
148
+
149
+ for ( const [ name, iconData ] of Object.entries( icons ) ) {
150
+ if ( ! isValidIconTuple( iconData ) ) {
151
+ continue;
152
+ }
153
+
154
+ index[ name ] = iconData;
155
+
156
+ for ( const alias of iconData[ FONT_AWESOME_JSON.aliases ] ) {
157
+ if ( typeof alias === 'string' && alias !== '' && ! index[ alias ] ) {
158
+ index[ alias ] = iconData;
159
+ }
160
+ }
161
+ }
162
+
163
+ return index;
164
+ }
165
+
166
+ function isValidIconTuple( iconData: unknown ): iconData is FontAwesomeIconJson {
167
+ return (
168
+ Array.isArray( iconData ) &&
169
+ iconData.length >= 5 &&
170
+ typeof iconData[ FONT_AWESOME_JSON.width ] === 'number' &&
171
+ typeof iconData[ FONT_AWESOME_JSON.height ] === 'number' &&
172
+ Array.isArray( iconData[ FONT_AWESOME_JSON.aliases ] )
173
+ );
174
+ }
175
+
176
+ function buildFontAwesomeSvg( iconData: FontAwesomeIconJson ): string | null {
113
177
  const width = iconData[ FONT_AWESOME_JSON.width ];
114
178
  const height = iconData[ FONT_AWESOME_JSON.height ];
115
- const path = iconData[ FONT_AWESOME_JSON.path ];
179
+ const paths = normalizePaths( iconData[ FONT_AWESOME_JSON.path ] );
180
+
181
+ if ( paths.length === 0 ) {
182
+ return null;
183
+ }
184
+
185
+ const pathMarkup = paths.map( ( path ) => `<path d="${ escapeSvgPath( path ) }"></path>` ).join( '' );
186
+
187
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${ width } ${ height }">${ pathMarkup }</svg>`;
188
+ }
189
+
190
+ function normalizePaths( pathData: string | string[] ): string[] {
191
+ if ( typeof pathData === 'string' && pathData !== '' ) {
192
+ return [ pathData ];
193
+ }
194
+
195
+ if ( ! Array.isArray( pathData ) ) {
196
+ return [];
197
+ }
198
+
199
+ return pathData.filter( ( path ): path is string => typeof path === 'string' && path !== '' );
200
+ }
201
+
202
+ function escapeSvgPath( path: string ): string {
203
+ return path.replace( /&/g, '&amp;' ).replace( /"/g, '&quot;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' );
204
+ }
205
+
206
+ function processIconSvgContent( svgText: string ): string | null {
207
+ const html = processSvgContent( svgText );
208
+
209
+ if ( ! html ) {
210
+ return null;
211
+ }
212
+
213
+ const parser = new DOMParser();
214
+ const doc = parser.parseFromString( html, 'image/svg+xml' );
215
+ const svgElement = doc.querySelector( 'svg' );
216
+
217
+ if ( ! svgElement ) {
218
+ return null;
219
+ }
220
+
221
+ svgElement.setAttribute( 'aria-hidden', 'true' );
222
+ svgElement.style.setProperty( 'width', '100%' );
223
+ svgElement.style.setProperty( 'height', '100%' );
224
+ svgElement.style.setProperty( 'overflow', 'visible' );
116
225
 
117
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${ width } ${ height }"><path d="${ path }"></path></svg>`;
226
+ return svgElement.outerHTML;
118
227
  }
119
228
 
120
229
  export function resetFontAwesomeIconsCache() {