@elementor/editor-canvas 4.3.0-1058 → 4.3.0-1060

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
@@ -2107,6 +2107,102 @@ var timeRangeTransformer = createTransformer((value) => {
2107
2107
  };
2108
2108
  });
2109
2109
 
2110
+ // src/transformers/shared/process-svg-content.ts
2111
+ var import_dompurify2 = __toESM(require("dompurify"));
2112
+ var SVG_INLINE_STYLES = "width: 100%; height: 100%; overflow: unset;";
2113
+ function processSvgContent(svgText) {
2114
+ const sanitized = import_dompurify2.default.sanitize(svgText, {
2115
+ USE_PROFILES: { svg: true, svgFilters: true }
2116
+ });
2117
+ const parser = new DOMParser();
2118
+ const doc = parser.parseFromString(sanitized, "image/svg+xml");
2119
+ const svgElement = doc.querySelector("svg");
2120
+ if (!svgElement) {
2121
+ return null;
2122
+ }
2123
+ svgElement.setAttribute("fill", "currentColor");
2124
+ const existingStyle = svgElement.getAttribute("style") ?? "";
2125
+ const trimmed = existingStyle.trim();
2126
+ const merged = trimmed ? `${trimmed.replace(/;$/, "")}; ${SVG_INLINE_STYLES}` : SVG_INLINE_STYLES;
2127
+ svgElement.setAttribute("style", merged);
2128
+ return svgElement.outerHTML;
2129
+ }
2130
+
2131
+ // src/transformers/shared/icon-transformer.ts
2132
+ var FONT_AWESOME_JSON = {
2133
+ width: 0,
2134
+ height: 1,
2135
+ path: 4
2136
+ };
2137
+ var fontAwesomeJsonCache = /* @__PURE__ */ new Map();
2138
+ var iconTransformer = createTransformer(async (value, { signal }) => {
2139
+ const iconValue = typeof value.value === "string" ? value.value : null;
2140
+ const library = typeof value.library === "string" ? value.library : null;
2141
+ if (!iconValue || !library) {
2142
+ return { html: null, url: null };
2143
+ }
2144
+ const iconName = getFontAwesomeIconName(iconValue);
2145
+ const jsonFileName = getFontAwesomeJsonFileName(library);
2146
+ if (!iconName || !jsonFileName) {
2147
+ return { html: null, url: null };
2148
+ }
2149
+ const icons = await fetchFontAwesomeIcons(jsonFileName, signal);
2150
+ const iconData = icons?.[iconName];
2151
+ if (!iconData) {
2152
+ return { html: null, url: null };
2153
+ }
2154
+ const svgText = buildFontAwesomeSvg(iconData);
2155
+ const html = processSvgContent(svgText);
2156
+ return { html, url: null };
2157
+ });
2158
+ function getFontAwesomeIconName(iconValue) {
2159
+ const match = iconValue.match(/^fa\S*\s+fa-(.+)$/);
2160
+ return match?.[1] ?? null;
2161
+ }
2162
+ function getFontAwesomeJsonFileName(library) {
2163
+ if (!library.startsWith("fa-")) {
2164
+ return null;
2165
+ }
2166
+ return library.replace(/^fa-/, "");
2167
+ }
2168
+ function getAssetsBaseUrl() {
2169
+ const assetsUrl = window.elementorCommon?.config?.urls?.assets;
2170
+ return typeof assetsUrl === "string" && assetsUrl !== "" ? assetsUrl : null;
2171
+ }
2172
+ async function fetchFontAwesomeIcons(jsonFileName, signal) {
2173
+ const cached = fontAwesomeJsonCache.get(jsonFileName);
2174
+ if (cached) {
2175
+ return cached;
2176
+ }
2177
+ const icons = await loadFontAwesomeIcons(jsonFileName, signal);
2178
+ if (icons) {
2179
+ fontAwesomeJsonCache.set(jsonFileName, icons);
2180
+ }
2181
+ return icons;
2182
+ }
2183
+ async function loadFontAwesomeIcons(jsonFileName, signal) {
2184
+ const assetsUrl = getAssetsBaseUrl();
2185
+ if (!assetsUrl) {
2186
+ return null;
2187
+ }
2188
+ try {
2189
+ const response = await fetch(`${assetsUrl}lib/font-awesome/json/${jsonFileName}.json`, { signal });
2190
+ if (!response.ok) {
2191
+ return null;
2192
+ }
2193
+ const data = await response.json();
2194
+ return data.icons ?? null;
2195
+ } catch {
2196
+ return null;
2197
+ }
2198
+ }
2199
+ function buildFontAwesomeSvg(iconData) {
2200
+ const width = iconData[FONT_AWESOME_JSON.width];
2201
+ 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>`;
2204
+ }
2205
+
2110
2206
  // src/transformers/shared/image-src-transformer.ts
2111
2207
  var imageSrcTransformer = createTransformer((value) => ({
2112
2208
  id: value.id ?? null,
@@ -2148,26 +2244,7 @@ var plainTransformer = createTransformer((value) => {
2148
2244
  });
2149
2245
 
2150
2246
  // src/transformers/shared/svg-src-transformer.ts
2151
- var import_dompurify2 = __toESM(require("dompurify"));
2152
2247
  var import_wp_media2 = require("@elementor/wp-media");
2153
- var SVG_INLINE_STYLES = "width: 100%; height: 100%; overflow: unset;";
2154
- function processSvgContent(svgText) {
2155
- const sanitized = import_dompurify2.default.sanitize(svgText, {
2156
- USE_PROFILES: { svg: true, svgFilters: true }
2157
- });
2158
- const parser = new DOMParser();
2159
- const doc = parser.parseFromString(sanitized, "image/svg+xml");
2160
- const svgElement = doc.querySelector("svg");
2161
- if (!svgElement) {
2162
- return null;
2163
- }
2164
- svgElement.setAttribute("fill", "currentColor");
2165
- const existingStyle = svgElement.getAttribute("style") ?? "";
2166
- const trimmed = existingStyle.trim();
2167
- const merged = trimmed ? `${trimmed.replace(/;$/, "")}; ${SVG_INLINE_STYLES}` : SVG_INLINE_STYLES;
2168
- svgElement.setAttribute("style", merged);
2169
- return svgElement.outerHTML;
2170
- }
2171
2248
  async function fetchSvgContent(url, signal) {
2172
2249
  try {
2173
2250
  const response = await fetch(url, { signal });
@@ -2223,7 +2300,7 @@ var videoSrcTransformer = createTransformer(async (value) => {
2223
2300
 
2224
2301
  // src/init-settings-transformers.ts
2225
2302
  function initSettingsTransformers() {
2226
- settingsTransformersRegistry.register("classes", createClassesTransformer()).register("link", linkTransformer).register("query", queryTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).register("svg-src", svgSrcTransformer).register("video-src", videoSrcTransformer).register("attributes", attributesTransformer).register("date-time", dateTimeTransformer).register("html-v2", htmlV2Transformer).register("html-v3", htmlV3Transformer).register("escaped-html", escapedHtmlTransformer).register("date-range", dateRangeTransformer).register("time-range", timeRangeTransformer).registerFallback(plainTransformer);
2303
+ settingsTransformersRegistry.register("classes", createClassesTransformer()).register("link", linkTransformer).register("query", queryTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).register("svg-src", svgSrcTransformer).register("icon", iconTransformer).register("video-src", videoSrcTransformer).register("attributes", attributesTransformer).register("date-time", dateTimeTransformer).register("html-v2", htmlV2Transformer).register("html-v3", htmlV3Transformer).register("escaped-html", escapedHtmlTransformer).register("date-range", dateRangeTransformer).register("time-range", timeRangeTransformer).registerFallback(plainTransformer);
2227
2304
  }
2228
2305
 
2229
2306
  // src/transformers/styles/background-color-overlay-transformer.ts
package/dist/index.mjs CHANGED
@@ -2059,6 +2059,102 @@ var timeRangeTransformer = createTransformer((value) => {
2059
2059
  };
2060
2060
  });
2061
2061
 
2062
+ // src/transformers/shared/process-svg-content.ts
2063
+ import DOMPurify2 from "dompurify";
2064
+ var SVG_INLINE_STYLES = "width: 100%; height: 100%; overflow: unset;";
2065
+ function processSvgContent(svgText) {
2066
+ const sanitized = DOMPurify2.sanitize(svgText, {
2067
+ USE_PROFILES: { svg: true, svgFilters: true }
2068
+ });
2069
+ const parser = new DOMParser();
2070
+ const doc = parser.parseFromString(sanitized, "image/svg+xml");
2071
+ const svgElement = doc.querySelector("svg");
2072
+ if (!svgElement) {
2073
+ return null;
2074
+ }
2075
+ svgElement.setAttribute("fill", "currentColor");
2076
+ const existingStyle = svgElement.getAttribute("style") ?? "";
2077
+ const trimmed = existingStyle.trim();
2078
+ const merged = trimmed ? `${trimmed.replace(/;$/, "")}; ${SVG_INLINE_STYLES}` : SVG_INLINE_STYLES;
2079
+ svgElement.setAttribute("style", merged);
2080
+ return svgElement.outerHTML;
2081
+ }
2082
+
2083
+ // src/transformers/shared/icon-transformer.ts
2084
+ var FONT_AWESOME_JSON = {
2085
+ width: 0,
2086
+ height: 1,
2087
+ path: 4
2088
+ };
2089
+ var fontAwesomeJsonCache = /* @__PURE__ */ new Map();
2090
+ var iconTransformer = createTransformer(async (value, { signal }) => {
2091
+ const iconValue = typeof value.value === "string" ? value.value : null;
2092
+ const library = typeof value.library === "string" ? value.library : null;
2093
+ if (!iconValue || !library) {
2094
+ return { html: null, url: null };
2095
+ }
2096
+ const iconName = getFontAwesomeIconName(iconValue);
2097
+ const jsonFileName = getFontAwesomeJsonFileName(library);
2098
+ if (!iconName || !jsonFileName) {
2099
+ return { html: null, url: null };
2100
+ }
2101
+ const icons = await fetchFontAwesomeIcons(jsonFileName, signal);
2102
+ const iconData = icons?.[iconName];
2103
+ if (!iconData) {
2104
+ return { html: null, url: null };
2105
+ }
2106
+ const svgText = buildFontAwesomeSvg(iconData);
2107
+ const html = processSvgContent(svgText);
2108
+ return { html, url: null };
2109
+ });
2110
+ function getFontAwesomeIconName(iconValue) {
2111
+ const match = iconValue.match(/^fa\S*\s+fa-(.+)$/);
2112
+ return match?.[1] ?? null;
2113
+ }
2114
+ function getFontAwesomeJsonFileName(library) {
2115
+ if (!library.startsWith("fa-")) {
2116
+ return null;
2117
+ }
2118
+ return library.replace(/^fa-/, "");
2119
+ }
2120
+ function getAssetsBaseUrl() {
2121
+ const assetsUrl = window.elementorCommon?.config?.urls?.assets;
2122
+ return typeof assetsUrl === "string" && assetsUrl !== "" ? assetsUrl : null;
2123
+ }
2124
+ async function fetchFontAwesomeIcons(jsonFileName, signal) {
2125
+ const cached = fontAwesomeJsonCache.get(jsonFileName);
2126
+ if (cached) {
2127
+ return cached;
2128
+ }
2129
+ const icons = await loadFontAwesomeIcons(jsonFileName, signal);
2130
+ if (icons) {
2131
+ fontAwesomeJsonCache.set(jsonFileName, icons);
2132
+ }
2133
+ return icons;
2134
+ }
2135
+ async function loadFontAwesomeIcons(jsonFileName, signal) {
2136
+ const assetsUrl = getAssetsBaseUrl();
2137
+ if (!assetsUrl) {
2138
+ return null;
2139
+ }
2140
+ try {
2141
+ const response = await fetch(`${assetsUrl}lib/font-awesome/json/${jsonFileName}.json`, { signal });
2142
+ if (!response.ok) {
2143
+ return null;
2144
+ }
2145
+ const data = await response.json();
2146
+ return data.icons ?? null;
2147
+ } catch {
2148
+ return null;
2149
+ }
2150
+ }
2151
+ function buildFontAwesomeSvg(iconData) {
2152
+ const width = iconData[FONT_AWESOME_JSON.width];
2153
+ 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>`;
2156
+ }
2157
+
2062
2158
  // src/transformers/shared/image-src-transformer.ts
2063
2159
  var imageSrcTransformer = createTransformer((value) => ({
2064
2160
  id: value.id ?? null,
@@ -2100,26 +2196,7 @@ var plainTransformer = createTransformer((value) => {
2100
2196
  });
2101
2197
 
2102
2198
  // src/transformers/shared/svg-src-transformer.ts
2103
- import DOMPurify2 from "dompurify";
2104
2199
  import { getMediaAttachment as getMediaAttachment2 } from "@elementor/wp-media";
2105
- var SVG_INLINE_STYLES = "width: 100%; height: 100%; overflow: unset;";
2106
- function processSvgContent(svgText) {
2107
- const sanitized = DOMPurify2.sanitize(svgText, {
2108
- USE_PROFILES: { svg: true, svgFilters: true }
2109
- });
2110
- const parser = new DOMParser();
2111
- const doc = parser.parseFromString(sanitized, "image/svg+xml");
2112
- const svgElement = doc.querySelector("svg");
2113
- if (!svgElement) {
2114
- return null;
2115
- }
2116
- svgElement.setAttribute("fill", "currentColor");
2117
- const existingStyle = svgElement.getAttribute("style") ?? "";
2118
- const trimmed = existingStyle.trim();
2119
- const merged = trimmed ? `${trimmed.replace(/;$/, "")}; ${SVG_INLINE_STYLES}` : SVG_INLINE_STYLES;
2120
- svgElement.setAttribute("style", merged);
2121
- return svgElement.outerHTML;
2122
- }
2123
2200
  async function fetchSvgContent(url, signal) {
2124
2201
  try {
2125
2202
  const response = await fetch(url, { signal });
@@ -2175,7 +2252,7 @@ var videoSrcTransformer = createTransformer(async (value) => {
2175
2252
 
2176
2253
  // src/init-settings-transformers.ts
2177
2254
  function initSettingsTransformers() {
2178
- settingsTransformersRegistry.register("classes", createClassesTransformer()).register("link", linkTransformer).register("query", queryTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).register("svg-src", svgSrcTransformer).register("video-src", videoSrcTransformer).register("attributes", attributesTransformer).register("date-time", dateTimeTransformer).register("html-v2", htmlV2Transformer).register("html-v3", htmlV3Transformer).register("escaped-html", escapedHtmlTransformer).register("date-range", dateRangeTransformer).register("time-range", timeRangeTransformer).registerFallback(plainTransformer);
2255
+ settingsTransformersRegistry.register("classes", createClassesTransformer()).register("link", linkTransformer).register("query", queryTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).register("svg-src", svgSrcTransformer).register("icon", iconTransformer).register("video-src", videoSrcTransformer).register("attributes", attributesTransformer).register("date-time", dateTimeTransformer).register("html-v2", htmlV2Transformer).register("html-v3", htmlV3Transformer).register("escaped-html", escapedHtmlTransformer).register("date-range", dateRangeTransformer).register("time-range", timeRangeTransformer).registerFallback(plainTransformer);
2179
2256
  }
2180
2257
 
2181
2258
  // src/transformers/styles/background-color-overlay-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.3.0-1058",
4
+ "version": "4.3.0-1060",
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.3.0-1058",
41
- "@elementor/editor-controls": "4.3.0-1058",
42
- "@elementor/editor-documents": "4.3.0-1058",
43
- "@elementor/editor-elements": "4.3.0-1058",
44
- "@elementor/editor-interactions": "4.3.0-1058",
45
- "@elementor/editor-mcp": "4.3.0-1058",
46
- "@elementor/editor-notifications": "4.3.0-1058",
47
- "@elementor/editor-props": "4.3.0-1058",
48
- "@elementor/editor-responsive": "4.3.0-1058",
49
- "@elementor/editor-styles": "4.3.0-1058",
50
- "@elementor/editor-styles-repository": "4.3.0-1058",
51
- "@elementor/editor-ui": "4.3.0-1058",
52
- "@elementor/editor-v1-adapters": "4.3.0-1058",
53
- "@elementor/events": "4.3.0-1058",
54
- "@elementor/http-client": "4.3.0-1058",
55
- "@elementor/schema": "4.3.0-1058",
56
- "@elementor/twing": "4.3.0-1058",
40
+ "@elementor/editor": "4.3.0-1060",
41
+ "@elementor/editor-controls": "4.3.0-1060",
42
+ "@elementor/editor-documents": "4.3.0-1060",
43
+ "@elementor/editor-elements": "4.3.0-1060",
44
+ "@elementor/editor-interactions": "4.3.0-1060",
45
+ "@elementor/editor-mcp": "4.3.0-1060",
46
+ "@elementor/editor-notifications": "4.3.0-1060",
47
+ "@elementor/editor-props": "4.3.0-1060",
48
+ "@elementor/editor-responsive": "4.3.0-1060",
49
+ "@elementor/editor-styles": "4.3.0-1060",
50
+ "@elementor/editor-styles-repository": "4.3.0-1060",
51
+ "@elementor/editor-ui": "4.3.0-1060",
52
+ "@elementor/editor-v1-adapters": "4.3.0-1060",
53
+ "@elementor/events": "4.3.0-1060",
54
+ "@elementor/http-client": "4.3.0-1060",
55
+ "@elementor/schema": "4.3.0-1060",
56
+ "@elementor/twing": "4.3.0-1060",
57
57
  "@elementor/ui": "1.37.5",
58
- "@elementor/utils": "4.3.0-1058",
59
- "@elementor/wp-media": "4.3.0-1058",
58
+ "@elementor/utils": "4.3.0-1060",
59
+ "@elementor/wp-media": "4.3.0-1060",
60
60
  "@floating-ui/react": "^0.27.5",
61
61
  "@wordpress/i18n": "^5.13.0",
62
62
  "dompurify": "^3.2.6"
@@ -9,6 +9,7 @@ import { htmlV3Transformer } from './transformers/settings/html-v3-transformer';
9
9
  import { linkTransformer } from './transformers/settings/link-transformer';
10
10
  import { queryTransformer } from './transformers/settings/query-transformer';
11
11
  import { timeRangeTransformer } from './transformers/settings/time-range-transformer';
12
+ import { iconTransformer } from './transformers/shared/icon-transformer';
12
13
  import { imageSrcTransformer } from './transformers/shared/image-src-transformer';
13
14
  import { imageTransformer } from './transformers/shared/image-transformer';
14
15
  import { plainTransformer } from './transformers/shared/plain-transformer';
@@ -23,6 +24,7 @@ export function initSettingsTransformers() {
23
24
  .register( 'image', imageTransformer )
24
25
  .register( 'image-src', imageSrcTransformer )
25
26
  .register( 'svg-src', svgSrcTransformer )
27
+ .register( 'icon', iconTransformer )
26
28
  .register( 'video-src', videoSrcTransformer )
27
29
  .register( 'attributes', attributesTransformer )
28
30
  .register( 'date-time', dateTimeTransformer )
@@ -0,0 +1,74 @@
1
+ import { iconTransformer, resetFontAwesomeIconsCache } from '../icon-transformer';
2
+
3
+ const STAR_PATH = 'M0 0h100v100H0z';
4
+ const STAR_WIDTH = 512;
5
+ const STAR_HEIGHT = 512;
6
+
7
+ const mockFetch = ( body: unknown, ok = true ) => {
8
+ global.fetch = jest.fn().mockResolvedValue( {
9
+ ok,
10
+ json: () => Promise.resolve( body ),
11
+ } );
12
+ };
13
+
14
+ describe( 'iconTransformer', () => {
15
+ const originalElementorCommon = window.elementorCommon;
16
+
17
+ beforeEach( () => {
18
+ jest.clearAllMocks();
19
+ resetFontAwesomeIconsCache();
20
+ window.elementorCommon = {
21
+ config: {
22
+ urls: {
23
+ assets: 'https://example.com/assets/',
24
+ },
25
+ },
26
+ };
27
+ } );
28
+
29
+ afterEach( () => {
30
+ window.elementorCommon = originalElementorCommon;
31
+ jest.restoreAllMocks();
32
+ } );
33
+
34
+ it( 'returns processed inline svg for a font awesome icon', async () => {
35
+ // Arrange.
36
+ mockFetch( {
37
+ icons: {
38
+ star: [ STAR_WIDTH, STAR_HEIGHT, [], 'f005', STAR_PATH ],
39
+ },
40
+ } );
41
+
42
+ // Act.
43
+ const result = await iconTransformer( { value: 'fas fa-star', library: 'fa-solid' }, { key: 'svg' } );
44
+
45
+ // Assert.
46
+ expect( global.fetch ).toHaveBeenCalledWith( 'https://example.com/assets/lib/font-awesome/json/solid.json', {
47
+ signal: undefined,
48
+ } );
49
+ expect( result ).toEqual( {
50
+ html: expect.stringContaining( STAR_PATH ),
51
+ url: null,
52
+ } );
53
+ expect( ( result as { html: string } ).html ).toContain( 'fill="currentColor"' );
54
+ } );
55
+
56
+ it( 'returns null html when the icon cannot be resolved', async () => {
57
+ // Arrange.
58
+ mockFetch( { icons: {} } );
59
+
60
+ // Act.
61
+ const result = await iconTransformer( { value: 'fas fa-missing', library: 'fa-solid' }, { key: 'svg' } );
62
+
63
+ // Assert.
64
+ expect( result ).toEqual( { html: null, url: null } );
65
+ } );
66
+
67
+ it( 'returns null html when value or library is missing', async () => {
68
+ // Act.
69
+ const result = await iconTransformer( { value: 'fas fa-star' }, { key: 'svg' } );
70
+
71
+ // Assert.
72
+ expect( result ).toEqual( { html: null, url: null } );
73
+ } );
74
+ } );
@@ -0,0 +1,122 @@
1
+ import { createTransformer } from '../create-transformer';
2
+ import type { TransformerOptions } from '../types';
3
+ import { processSvgContent } from './process-svg-content';
4
+
5
+ type IconValue = {
6
+ value?: unknown;
7
+ library?: unknown;
8
+ };
9
+
10
+ type FontAwesomeIconJson = [ number, number, unknown, unknown, string ];
11
+
12
+ const FONT_AWESOME_JSON = {
13
+ width: 0,
14
+ height: 1,
15
+ path: 4,
16
+ } as const;
17
+
18
+ const fontAwesomeJsonCache = new Map< string, Record< string, FontAwesomeIconJson > >();
19
+
20
+ export const iconTransformer = createTransformer( async ( value: IconValue, { signal }: TransformerOptions ) => {
21
+ const iconValue = typeof value.value === 'string' ? value.value : null;
22
+ const library = typeof value.library === 'string' ? value.library : null;
23
+
24
+ if ( ! iconValue || ! library ) {
25
+ return { html: null, url: null };
26
+ }
27
+
28
+ const iconName = getFontAwesomeIconName( iconValue );
29
+ const jsonFileName = getFontAwesomeJsonFileName( library );
30
+
31
+ if ( ! iconName || ! jsonFileName ) {
32
+ return { html: null, url: null };
33
+ }
34
+
35
+ const icons = await fetchFontAwesomeIcons( jsonFileName, signal );
36
+ const iconData = icons?.[ iconName ];
37
+
38
+ if ( ! iconData ) {
39
+ return { html: null, url: null };
40
+ }
41
+
42
+ const svgText = buildFontAwesomeSvg( iconData );
43
+ const html = processSvgContent( svgText );
44
+
45
+ return { html, url: null };
46
+ } );
47
+
48
+ function getFontAwesomeIconName( iconValue: string ): string | null {
49
+ const match = iconValue.match( /^fa\S*\s+fa-(.+)$/ );
50
+
51
+ return match?.[ 1 ] ?? null;
52
+ }
53
+
54
+ function getFontAwesomeJsonFileName( library: string ): string | null {
55
+ if ( ! library.startsWith( 'fa-' ) ) {
56
+ return null;
57
+ }
58
+
59
+ return library.replace( /^fa-/, '' );
60
+ }
61
+
62
+ function getAssetsBaseUrl(): string | null {
63
+ const assetsUrl = window.elementorCommon?.config?.urls?.assets;
64
+
65
+ return typeof assetsUrl === 'string' && assetsUrl !== '' ? assetsUrl : null;
66
+ }
67
+
68
+ async function fetchFontAwesomeIcons(
69
+ jsonFileName: string,
70
+ signal?: AbortSignal
71
+ ): Promise< Record< string, FontAwesomeIconJson > | null > {
72
+ const cached = fontAwesomeJsonCache.get( jsonFileName );
73
+
74
+ if ( cached ) {
75
+ return cached;
76
+ }
77
+
78
+ const icons = await loadFontAwesomeIcons( jsonFileName, signal );
79
+
80
+ if ( icons ) {
81
+ fontAwesomeJsonCache.set( jsonFileName, icons );
82
+ }
83
+
84
+ return icons;
85
+ }
86
+
87
+ async function loadFontAwesomeIcons(
88
+ jsonFileName: string,
89
+ signal?: AbortSignal
90
+ ): Promise< Record< string, FontAwesomeIconJson > | null > {
91
+ const assetsUrl = getAssetsBaseUrl();
92
+
93
+ if ( ! assetsUrl ) {
94
+ return null;
95
+ }
96
+
97
+ try {
98
+ const response = await fetch( `${ assetsUrl }lib/font-awesome/json/${ jsonFileName }.json`, { signal } );
99
+
100
+ if ( ! response.ok ) {
101
+ return null;
102
+ }
103
+
104
+ const data = ( await response.json() ) as { icons?: Record< string, FontAwesomeIconJson > };
105
+
106
+ return data.icons ?? null;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ function buildFontAwesomeSvg( iconData: FontAwesomeIconJson ): string {
113
+ const width = iconData[ FONT_AWESOME_JSON.width ];
114
+ const height = iconData[ FONT_AWESOME_JSON.height ];
115
+ const path = iconData[ FONT_AWESOME_JSON.path ];
116
+
117
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${ width } ${ height }"><path d="${ path }"></path></svg>`;
118
+ }
119
+
120
+ export function resetFontAwesomeIconsCache() {
121
+ fontAwesomeJsonCache.clear();
122
+ }
@@ -0,0 +1,26 @@
1
+ import DOMPurify from 'dompurify';
2
+
3
+ const SVG_INLINE_STYLES = 'width: 100%; height: 100%; overflow: unset;';
4
+
5
+ export function processSvgContent( svgText: string ): string | null {
6
+ const sanitized = DOMPurify.sanitize( svgText, {
7
+ USE_PROFILES: { svg: true, svgFilters: true },
8
+ } );
9
+
10
+ const parser = new DOMParser();
11
+ const doc = parser.parseFromString( sanitized, 'image/svg+xml' );
12
+ const svgElement = doc.querySelector( 'svg' );
13
+
14
+ if ( ! svgElement ) {
15
+ return null;
16
+ }
17
+
18
+ svgElement.setAttribute( 'fill', 'currentColor' );
19
+
20
+ const existingStyle = svgElement.getAttribute( 'style' ) ?? '';
21
+ const trimmed = existingStyle.trim();
22
+ const merged = trimmed ? `${ trimmed.replace( /;$/, '' ) }; ${ SVG_INLINE_STYLES }` : SVG_INLINE_STYLES;
23
+ svgElement.setAttribute( 'style', merged );
24
+
25
+ return svgElement.outerHTML;
26
+ }
@@ -1,39 +1,14 @@
1
- import DOMPurify from 'dompurify';
2
1
  import { getMediaAttachment } from '@elementor/wp-media';
3
2
 
4
3
  import { createTransformer } from '../create-transformer';
5
4
  import type { TransformerOptions } from '../types';
5
+ import { processSvgContent } from './process-svg-content';
6
6
 
7
7
  type SvgSrc = {
8
8
  id?: unknown;
9
9
  url?: unknown;
10
10
  };
11
11
 
12
- const SVG_INLINE_STYLES = 'width: 100%; height: 100%; overflow: unset;';
13
-
14
- function processSvgContent( svgText: string ): string | null {
15
- const sanitized = DOMPurify.sanitize( svgText, {
16
- USE_PROFILES: { svg: true, svgFilters: true },
17
- } );
18
-
19
- const parser = new DOMParser();
20
- const doc = parser.parseFromString( sanitized, 'image/svg+xml' );
21
- const svgElement = doc.querySelector( 'svg' );
22
-
23
- if ( ! svgElement ) {
24
- return null;
25
- }
26
-
27
- svgElement.setAttribute( 'fill', 'currentColor' );
28
-
29
- const existingStyle = svgElement.getAttribute( 'style' ) ?? '';
30
- const trimmed = existingStyle.trim();
31
- const merged = trimmed ? `${ trimmed.replace( /;$/, '' ) }; ${ SVG_INLINE_STYLES }` : SVG_INLINE_STYLES;
32
- svgElement.setAttribute( 'style', merged );
33
-
34
- return svgElement.outerHTML;
35
- }
36
-
37
12
  async function fetchSvgContent( url: string, signal?: AbortSignal ): Promise< string | null > {
38
13
  try {
39
14
  const response = await fetch( url, { signal } );