@elementor/editor-canvas 4.1.0-737 → 4.1.0-738
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
|
@@ -1453,14 +1453,74 @@ var plainTransformer = createTransformer((value) => {
|
|
|
1453
1453
|
return value;
|
|
1454
1454
|
});
|
|
1455
1455
|
|
|
1456
|
-
// src/transformers/shared/
|
|
1456
|
+
// src/transformers/shared/svg-src-transformer.ts
|
|
1457
|
+
var import_dompurify = __toESM(require("dompurify"));
|
|
1457
1458
|
var import_wp_media2 = require("@elementor/wp-media");
|
|
1459
|
+
var SVG_INLINE_STYLES = "width: 100%; height: 100%; overflow: unset;";
|
|
1460
|
+
function processSvgContent(svgText) {
|
|
1461
|
+
const sanitized = import_dompurify.default.sanitize(svgText, {
|
|
1462
|
+
USE_PROFILES: { svg: true, svgFilters: true }
|
|
1463
|
+
});
|
|
1464
|
+
const parser = new DOMParser();
|
|
1465
|
+
const doc = parser.parseFromString(sanitized, "image/svg+xml");
|
|
1466
|
+
const svgElement = doc.querySelector("svg");
|
|
1467
|
+
if (!svgElement) {
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
svgElement.setAttribute("fill", "currentColor");
|
|
1471
|
+
const existingStyle = svgElement.getAttribute("style") ?? "";
|
|
1472
|
+
const trimmed = existingStyle.trim();
|
|
1473
|
+
const merged = trimmed ? `${trimmed.replace(/;$/, "")}; ${SVG_INLINE_STYLES}` : SVG_INLINE_STYLES;
|
|
1474
|
+
svgElement.setAttribute("style", merged);
|
|
1475
|
+
return svgElement.outerHTML;
|
|
1476
|
+
}
|
|
1477
|
+
async function fetchSvgContent(url, signal) {
|
|
1478
|
+
try {
|
|
1479
|
+
const response = await fetch(url, { signal });
|
|
1480
|
+
if (!response.ok) {
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1484
|
+
const isSvg = contentType.includes("svg") || contentType.includes("xml") || url.endsWith(".svg");
|
|
1485
|
+
if (!isSvg) {
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
return await response.text();
|
|
1489
|
+
} catch {
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function resolveSvgSrcId(id) {
|
|
1494
|
+
if (typeof id !== "number" || id <= 0) {
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
return id;
|
|
1498
|
+
}
|
|
1499
|
+
var svgSrcTransformer = createTransformer(async (value, { signal }) => {
|
|
1500
|
+
const id = resolveSvgSrcId(value.id);
|
|
1501
|
+
const urlFromValue = typeof value.url === "string" ? value.url : null;
|
|
1502
|
+
let url = urlFromValue;
|
|
1503
|
+
if (id && !urlFromValue) {
|
|
1504
|
+
const attachment = await (0, import_wp_media2.getMediaAttachment)({ id });
|
|
1505
|
+
url = attachment?.url ?? null;
|
|
1506
|
+
}
|
|
1507
|
+
const resolvedUrl = typeof url === "string" ? url : null;
|
|
1508
|
+
if (!resolvedUrl) {
|
|
1509
|
+
return { html: null, url: null };
|
|
1510
|
+
}
|
|
1511
|
+
const svgText = await fetchSvgContent(resolvedUrl, signal);
|
|
1512
|
+
const html = svgText ? processSvgContent(svgText) : null;
|
|
1513
|
+
return { html, url: resolvedUrl };
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
// src/transformers/shared/video-src-transformer.ts
|
|
1517
|
+
var import_wp_media3 = require("@elementor/wp-media");
|
|
1458
1518
|
var videoSrcTransformer = createTransformer(async (value) => {
|
|
1459
1519
|
const { id, url } = value;
|
|
1460
1520
|
if (!id) {
|
|
1461
1521
|
return { id: null, url };
|
|
1462
1522
|
}
|
|
1463
|
-
const attachment = await (0,
|
|
1523
|
+
const attachment = await (0, import_wp_media3.getMediaAttachment)({ id });
|
|
1464
1524
|
return {
|
|
1465
1525
|
id,
|
|
1466
1526
|
url: attachment?.url ?? url
|
|
@@ -1469,7 +1529,7 @@ var videoSrcTransformer = createTransformer(async (value) => {
|
|
|
1469
1529
|
|
|
1470
1530
|
// src/init-settings-transformers.ts
|
|
1471
1531
|
function initSettingsTransformers() {
|
|
1472
|
-
settingsTransformersRegistry.register("classes", createClassesTransformer()).register("link", linkTransformer).register("query", queryTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).register("video-src", videoSrcTransformer).register("attributes", attributesTransformer).register("date-time", dateTimeTransformer).register("html-v2", htmlV2Transformer).register("html-v3", htmlV3Transformer).registerFallback(plainTransformer);
|
|
1532
|
+
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).registerFallback(plainTransformer);
|
|
1473
1533
|
}
|
|
1474
1534
|
|
|
1475
1535
|
// src/transformers/styles/background-color-overlay-transformer.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1419,14 +1419,74 @@ var plainTransformer = createTransformer((value) => {
|
|
|
1419
1419
|
return value;
|
|
1420
1420
|
});
|
|
1421
1421
|
|
|
1422
|
-
// src/transformers/shared/
|
|
1422
|
+
// src/transformers/shared/svg-src-transformer.ts
|
|
1423
|
+
import DOMPurify from "dompurify";
|
|
1423
1424
|
import { getMediaAttachment as getMediaAttachment2 } from "@elementor/wp-media";
|
|
1425
|
+
var SVG_INLINE_STYLES = "width: 100%; height: 100%; overflow: unset;";
|
|
1426
|
+
function processSvgContent(svgText) {
|
|
1427
|
+
const sanitized = DOMPurify.sanitize(svgText, {
|
|
1428
|
+
USE_PROFILES: { svg: true, svgFilters: true }
|
|
1429
|
+
});
|
|
1430
|
+
const parser = new DOMParser();
|
|
1431
|
+
const doc = parser.parseFromString(sanitized, "image/svg+xml");
|
|
1432
|
+
const svgElement = doc.querySelector("svg");
|
|
1433
|
+
if (!svgElement) {
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
svgElement.setAttribute("fill", "currentColor");
|
|
1437
|
+
const existingStyle = svgElement.getAttribute("style") ?? "";
|
|
1438
|
+
const trimmed = existingStyle.trim();
|
|
1439
|
+
const merged = trimmed ? `${trimmed.replace(/;$/, "")}; ${SVG_INLINE_STYLES}` : SVG_INLINE_STYLES;
|
|
1440
|
+
svgElement.setAttribute("style", merged);
|
|
1441
|
+
return svgElement.outerHTML;
|
|
1442
|
+
}
|
|
1443
|
+
async function fetchSvgContent(url, signal) {
|
|
1444
|
+
try {
|
|
1445
|
+
const response = await fetch(url, { signal });
|
|
1446
|
+
if (!response.ok) {
|
|
1447
|
+
return null;
|
|
1448
|
+
}
|
|
1449
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1450
|
+
const isSvg = contentType.includes("svg") || contentType.includes("xml") || url.endsWith(".svg");
|
|
1451
|
+
if (!isSvg) {
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
return await response.text();
|
|
1455
|
+
} catch {
|
|
1456
|
+
return null;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
function resolveSvgSrcId(id) {
|
|
1460
|
+
if (typeof id !== "number" || id <= 0) {
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
return id;
|
|
1464
|
+
}
|
|
1465
|
+
var svgSrcTransformer = createTransformer(async (value, { signal }) => {
|
|
1466
|
+
const id = resolveSvgSrcId(value.id);
|
|
1467
|
+
const urlFromValue = typeof value.url === "string" ? value.url : null;
|
|
1468
|
+
let url = urlFromValue;
|
|
1469
|
+
if (id && !urlFromValue) {
|
|
1470
|
+
const attachment = await getMediaAttachment2({ id });
|
|
1471
|
+
url = attachment?.url ?? null;
|
|
1472
|
+
}
|
|
1473
|
+
const resolvedUrl = typeof url === "string" ? url : null;
|
|
1474
|
+
if (!resolvedUrl) {
|
|
1475
|
+
return { html: null, url: null };
|
|
1476
|
+
}
|
|
1477
|
+
const svgText = await fetchSvgContent(resolvedUrl, signal);
|
|
1478
|
+
const html = svgText ? processSvgContent(svgText) : null;
|
|
1479
|
+
return { html, url: resolvedUrl };
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
// src/transformers/shared/video-src-transformer.ts
|
|
1483
|
+
import { getMediaAttachment as getMediaAttachment3 } from "@elementor/wp-media";
|
|
1424
1484
|
var videoSrcTransformer = createTransformer(async (value) => {
|
|
1425
1485
|
const { id, url } = value;
|
|
1426
1486
|
if (!id) {
|
|
1427
1487
|
return { id: null, url };
|
|
1428
1488
|
}
|
|
1429
|
-
const attachment = await
|
|
1489
|
+
const attachment = await getMediaAttachment3({ id });
|
|
1430
1490
|
return {
|
|
1431
1491
|
id,
|
|
1432
1492
|
url: attachment?.url ?? url
|
|
@@ -1435,7 +1495,7 @@ var videoSrcTransformer = createTransformer(async (value) => {
|
|
|
1435
1495
|
|
|
1436
1496
|
// src/init-settings-transformers.ts
|
|
1437
1497
|
function initSettingsTransformers() {
|
|
1438
|
-
settingsTransformersRegistry.register("classes", createClassesTransformer()).register("link", linkTransformer).register("query", queryTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).register("video-src", videoSrcTransformer).register("attributes", attributesTransformer).register("date-time", dateTimeTransformer).register("html-v2", htmlV2Transformer).register("html-v3", htmlV3Transformer).registerFallback(plainTransformer);
|
|
1498
|
+
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).registerFallback(plainTransformer);
|
|
1439
1499
|
}
|
|
1440
1500
|
|
|
1441
1501
|
// 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.1.0-
|
|
4
|
+
"version": "4.1.0-738",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -37,24 +37,25 @@
|
|
|
37
37
|
"react-dom": "^18.3.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@elementor/editor": "4.1.0-
|
|
41
|
-
"
|
|
42
|
-
"@elementor/editor-
|
|
43
|
-
"@elementor/editor-
|
|
44
|
-
"@elementor/editor-
|
|
45
|
-
"@elementor/editor-
|
|
46
|
-
"@elementor/editor-
|
|
47
|
-
"@elementor/editor-
|
|
48
|
-
"@elementor/editor-
|
|
49
|
-
"@elementor/editor-
|
|
50
|
-
"@elementor/editor-styles
|
|
51
|
-
"@elementor/editor-
|
|
52
|
-
"@elementor/editor-
|
|
53
|
-
"@elementor/
|
|
54
|
-
"@elementor/
|
|
40
|
+
"@elementor/editor": "4.1.0-738",
|
|
41
|
+
"dompurify": "^3.2.6",
|
|
42
|
+
"@elementor/editor-controls": "4.1.0-738",
|
|
43
|
+
"@elementor/editor-documents": "4.1.0-738",
|
|
44
|
+
"@elementor/editor-elements": "4.1.0-738",
|
|
45
|
+
"@elementor/editor-interactions": "4.1.0-738",
|
|
46
|
+
"@elementor/editor-mcp": "4.1.0-738",
|
|
47
|
+
"@elementor/editor-notifications": "4.1.0-738",
|
|
48
|
+
"@elementor/editor-props": "4.1.0-738",
|
|
49
|
+
"@elementor/editor-responsive": "4.1.0-738",
|
|
50
|
+
"@elementor/editor-styles": "4.1.0-738",
|
|
51
|
+
"@elementor/editor-styles-repository": "4.1.0-738",
|
|
52
|
+
"@elementor/editor-ui": "4.1.0-738",
|
|
53
|
+
"@elementor/editor-v1-adapters": "4.1.0-738",
|
|
54
|
+
"@elementor/schema": "4.1.0-738",
|
|
55
|
+
"@elementor/twing": "4.1.0-738",
|
|
55
56
|
"@elementor/ui": "1.36.17",
|
|
56
|
-
"@elementor/utils": "4.1.0-
|
|
57
|
-
"@elementor/wp-media": "4.1.0-
|
|
57
|
+
"@elementor/utils": "4.1.0-738",
|
|
58
|
+
"@elementor/wp-media": "4.1.0-738",
|
|
58
59
|
"@floating-ui/react": "^0.27.5",
|
|
59
60
|
"@wordpress/i18n": "^5.13.0"
|
|
60
61
|
},
|
|
@@ -9,6 +9,7 @@ import { queryTransformer } from './transformers/settings/query-transformer';
|
|
|
9
9
|
import { imageSrcTransformer } from './transformers/shared/image-src-transformer';
|
|
10
10
|
import { imageTransformer } from './transformers/shared/image-transformer';
|
|
11
11
|
import { plainTransformer } from './transformers/shared/plain-transformer';
|
|
12
|
+
import { svgSrcTransformer } from './transformers/shared/svg-src-transformer';
|
|
12
13
|
import { videoSrcTransformer } from './transformers/shared/video-src-transformer';
|
|
13
14
|
|
|
14
15
|
export function initSettingsTransformers() {
|
|
@@ -18,6 +19,7 @@ export function initSettingsTransformers() {
|
|
|
18
19
|
.register( 'query', queryTransformer )
|
|
19
20
|
.register( 'image', imageTransformer )
|
|
20
21
|
.register( 'image-src', imageSrcTransformer )
|
|
22
|
+
.register( 'svg-src', svgSrcTransformer )
|
|
21
23
|
.register( 'video-src', videoSrcTransformer )
|
|
22
24
|
.register( 'attributes', attributesTransformer )
|
|
23
25
|
.register( 'date-time', dateTimeTransformer )
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { getMediaAttachment } from '@elementor/wp-media';
|
|
2
|
+
|
|
3
|
+
import { svgSrcTransformer } from '../svg-src-transformer';
|
|
4
|
+
|
|
5
|
+
jest.mock( '@elementor/wp-media' );
|
|
6
|
+
|
|
7
|
+
const mockedGetMediaAttachment = jest.mocked( getMediaAttachment );
|
|
8
|
+
|
|
9
|
+
const SAMPLE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><path d="M0 0h100v100H0z"/></svg>';
|
|
10
|
+
|
|
11
|
+
const mockFetch = ( body: string, contentType = 'image/svg+xml', ok = true ) => {
|
|
12
|
+
global.fetch = jest.fn().mockResolvedValue( {
|
|
13
|
+
ok,
|
|
14
|
+
headers: new Headers( { 'content-type': contentType } ),
|
|
15
|
+
text: () => Promise.resolve( body ),
|
|
16
|
+
} );
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe( 'svgSrcTransformer', () => {
|
|
20
|
+
beforeEach( () => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
afterEach( () => {
|
|
25
|
+
jest.restoreAllMocks();
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'fetches SVG and returns processed html with currentColor fill', async () => {
|
|
29
|
+
// Arrange.
|
|
30
|
+
mockFetch( SAMPLE_SVG );
|
|
31
|
+
|
|
32
|
+
// Act.
|
|
33
|
+
const result = await svgSrcTransformer( { id: null, url: 'https://example.com/icon.svg' }, { key: 'svg' } );
|
|
34
|
+
|
|
35
|
+
// Assert.
|
|
36
|
+
expect( result ).toEqual( {
|
|
37
|
+
html: expect.stringContaining( 'fill="currentColor"' ),
|
|
38
|
+
url: 'https://example.com/icon.svg',
|
|
39
|
+
} );
|
|
40
|
+
} );
|
|
41
|
+
|
|
42
|
+
it( 'adds inline styles to the svg element', async () => {
|
|
43
|
+
// Arrange.
|
|
44
|
+
mockFetch( SAMPLE_SVG );
|
|
45
|
+
|
|
46
|
+
// Act.
|
|
47
|
+
const result = ( await svgSrcTransformer(
|
|
48
|
+
{ id: null, url: 'https://example.com/icon.svg' },
|
|
49
|
+
{ key: 'svg' }
|
|
50
|
+
) ) as { html: string; url: string };
|
|
51
|
+
|
|
52
|
+
// Assert.
|
|
53
|
+
expect( result.html ).toContain( 'width: 100%' );
|
|
54
|
+
expect( result.html ).toContain( 'height: 100%' );
|
|
55
|
+
expect( result.html ).toContain( 'overflow: unset' );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
it( 'returns null html when url is null', async () => {
|
|
59
|
+
// Arrange & Act.
|
|
60
|
+
const result = await svgSrcTransformer( { id: null, url: null }, { key: 'svg' } );
|
|
61
|
+
|
|
62
|
+
// Assert.
|
|
63
|
+
expect( result ).toEqual( { html: null, url: null } );
|
|
64
|
+
expect( mockedGetMediaAttachment ).not.toHaveBeenCalled();
|
|
65
|
+
} );
|
|
66
|
+
|
|
67
|
+
it( 'resolves url from attachment id then fetches SVG', async () => {
|
|
68
|
+
// Arrange.
|
|
69
|
+
mockedGetMediaAttachment.mockResolvedValue( {
|
|
70
|
+
url: 'https://example.com/resolved.svg',
|
|
71
|
+
} as never );
|
|
72
|
+
mockFetch( SAMPLE_SVG );
|
|
73
|
+
|
|
74
|
+
// Act.
|
|
75
|
+
const result = await svgSrcTransformer( { id: 42, url: null }, { key: 'svg' } );
|
|
76
|
+
|
|
77
|
+
// Assert.
|
|
78
|
+
expect( mockedGetMediaAttachment ).toHaveBeenCalledWith( { id: 42 } );
|
|
79
|
+
expect( result ).toEqual( {
|
|
80
|
+
html: expect.stringContaining( 'fill="currentColor"' ),
|
|
81
|
+
url: 'https://example.com/resolved.svg',
|
|
82
|
+
} );
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
it( 'falls back to provided url when attachment lookup fails', async () => {
|
|
86
|
+
// Arrange.
|
|
87
|
+
mockedGetMediaAttachment.mockResolvedValue( null as never );
|
|
88
|
+
mockFetch( SAMPLE_SVG );
|
|
89
|
+
|
|
90
|
+
// Act.
|
|
91
|
+
const result = ( await svgSrcTransformer(
|
|
92
|
+
{ id: 99, url: 'https://example.com/fallback.svg' },
|
|
93
|
+
{ key: 'svg' }
|
|
94
|
+
) ) as { html: string; url: string };
|
|
95
|
+
|
|
96
|
+
// Assert.
|
|
97
|
+
expect( result.url ).toBe( 'https://example.com/fallback.svg' );
|
|
98
|
+
expect( result.html ).toContain( 'fill="currentColor"' );
|
|
99
|
+
} );
|
|
100
|
+
|
|
101
|
+
it( 'returns null html when id is set but attachment and url are missing', async () => {
|
|
102
|
+
// Arrange.
|
|
103
|
+
mockedGetMediaAttachment.mockResolvedValue( null as never );
|
|
104
|
+
|
|
105
|
+
// Act.
|
|
106
|
+
const result = await svgSrcTransformer( { id: 7, url: null }, { key: 'svg' } );
|
|
107
|
+
|
|
108
|
+
// Assert.
|
|
109
|
+
expect( result ).toEqual( { html: null, url: null } );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( 'returns null html when fetch fails', async () => {
|
|
113
|
+
// Arrange.
|
|
114
|
+
mockFetch( '', 'image/svg+xml', false );
|
|
115
|
+
|
|
116
|
+
// Act.
|
|
117
|
+
const result = await svgSrcTransformer( { id: null, url: 'https://example.com/missing.svg' }, { key: 'svg' } );
|
|
118
|
+
|
|
119
|
+
// Assert.
|
|
120
|
+
expect( result ).toEqual( {
|
|
121
|
+
html: null,
|
|
122
|
+
url: 'https://example.com/missing.svg',
|
|
123
|
+
} );
|
|
124
|
+
} );
|
|
125
|
+
|
|
126
|
+
it( 'returns null html when content is not SVG', async () => {
|
|
127
|
+
// Arrange.
|
|
128
|
+
mockFetch( '<html><body>Not SVG</body></html>', 'text/html' );
|
|
129
|
+
|
|
130
|
+
// Act.
|
|
131
|
+
const result = await svgSrcTransformer( { id: null, url: 'https://example.com/page.html' }, { key: 'svg' } );
|
|
132
|
+
|
|
133
|
+
// Assert.
|
|
134
|
+
expect( result ).toEqual( {
|
|
135
|
+
html: null,
|
|
136
|
+
url: 'https://example.com/page.html',
|
|
137
|
+
} );
|
|
138
|
+
} );
|
|
139
|
+
|
|
140
|
+
it( 'merges with existing style attribute', async () => {
|
|
141
|
+
// Arrange.
|
|
142
|
+
const svgWithStyle = '<svg xmlns="http://www.w3.org/2000/svg" style="display: block"><path d="M0 0"/></svg>';
|
|
143
|
+
mockFetch( svgWithStyle );
|
|
144
|
+
|
|
145
|
+
// Act.
|
|
146
|
+
const result = ( await svgSrcTransformer(
|
|
147
|
+
{ id: null, url: 'https://example.com/styled.svg' },
|
|
148
|
+
{ key: 'svg' }
|
|
149
|
+
) ) as { html: string; url: string };
|
|
150
|
+
|
|
151
|
+
// Assert.
|
|
152
|
+
expect( result.html ).toContain( 'display: block' );
|
|
153
|
+
expect( result.html ).toContain( 'width: 100%' );
|
|
154
|
+
} );
|
|
155
|
+
|
|
156
|
+
it( 'returns null html when response has no svg element', async () => {
|
|
157
|
+
// Arrange.
|
|
158
|
+
mockFetch( '<div>Not an SVG</div>' );
|
|
159
|
+
|
|
160
|
+
// Act.
|
|
161
|
+
const result = await svgSrcTransformer( { id: null, url: 'https://example.com/not-svg.svg' }, { key: 'svg' } );
|
|
162
|
+
|
|
163
|
+
// Assert.
|
|
164
|
+
expect( result ).toEqual( {
|
|
165
|
+
html: null,
|
|
166
|
+
url: 'https://example.com/not-svg.svg',
|
|
167
|
+
} );
|
|
168
|
+
} );
|
|
169
|
+
|
|
170
|
+
it( 'passes abort signal to fetch', async () => {
|
|
171
|
+
// Arrange.
|
|
172
|
+
mockFetch( SAMPLE_SVG );
|
|
173
|
+
const controller = new AbortController();
|
|
174
|
+
|
|
175
|
+
// Act.
|
|
176
|
+
await svgSrcTransformer(
|
|
177
|
+
{ id: null, url: 'https://example.com/icon.svg' },
|
|
178
|
+
{ key: 'svg', signal: controller.signal }
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Assert.
|
|
182
|
+
expect( global.fetch ).toHaveBeenCalledWith( 'https://example.com/icon.svg', { signal: controller.signal } );
|
|
183
|
+
} );
|
|
184
|
+
} );
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
import { getMediaAttachment } from '@elementor/wp-media';
|
|
3
|
+
|
|
4
|
+
import { createTransformer } from '../create-transformer';
|
|
5
|
+
import type { TransformerOptions } from '../types';
|
|
6
|
+
|
|
7
|
+
type SvgSrc = {
|
|
8
|
+
id?: unknown;
|
|
9
|
+
url?: unknown;
|
|
10
|
+
};
|
|
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
|
+
async function fetchSvgContent( url: string, signal?: AbortSignal ): Promise< string | null > {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch( url, { signal } );
|
|
40
|
+
|
|
41
|
+
if ( ! response.ok ) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const contentType = response.headers.get( 'content-type' ) ?? '';
|
|
46
|
+
const isSvg = contentType.includes( 'svg' ) || contentType.includes( 'xml' ) || url.endsWith( '.svg' );
|
|
47
|
+
|
|
48
|
+
if ( ! isSvg ) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return await response.text();
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveSvgSrcId( id: unknown ): number | null {
|
|
59
|
+
if ( typeof id !== 'number' || id <= 0 ) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return id;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const svgSrcTransformer = createTransformer( async ( value: SvgSrc, { signal }: TransformerOptions ) => {
|
|
67
|
+
const id = resolveSvgSrcId( value.id );
|
|
68
|
+
const urlFromValue = typeof value.url === 'string' ? value.url : null;
|
|
69
|
+
|
|
70
|
+
let url: string | null | undefined = urlFromValue;
|
|
71
|
+
|
|
72
|
+
if ( id && ! urlFromValue ) {
|
|
73
|
+
const attachment = await getMediaAttachment( { id } );
|
|
74
|
+
url = attachment?.url ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const resolvedUrl = typeof url === 'string' ? url : null;
|
|
78
|
+
|
|
79
|
+
if ( ! resolvedUrl ) {
|
|
80
|
+
return { html: null, url: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const svgText = await fetchSvgContent( resolvedUrl, signal );
|
|
84
|
+
const html = svgText ? processSvgContent( svgText ) : null;
|
|
85
|
+
|
|
86
|
+
return { html, url: resolvedUrl };
|
|
87
|
+
} );
|