@elementor/editor-canvas 4.3.3 → 4.3.5
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.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +310 -114
- package/dist/index.mjs +268 -69
- package/package.json +21 -21
- package/src/legacy/__tests__/build-editor-attributes.test.ts +41 -0
- package/src/legacy/__tests__/create-pending-element.test.ts +128 -0
- package/src/legacy/__tests__/create-templated-element-type.test.ts +50 -14
- package/src/legacy/create-nested-templated-element-type.ts +4 -2
- package/src/legacy/create-pending-element.ts +9 -5
- package/src/legacy/create-templated-element-type.ts +2 -1
- package/src/mcp/canvas-mcp.ts +5 -0
- package/src/mcp/icon-library-experiment.ts +5 -0
- package/src/mcp/tools/configure-element/prompt.ts +47 -1
- package/src/mcp/tools/find-icons/__tests__/tool.test.ts +143 -0
- package/src/mcp/tools/find-icons/prompt.ts +43 -0
- package/src/mcp/tools/find-icons/tool.ts +153 -0
- package/src/transformers/shared/__tests__/icon-transformer.test.ts +39 -0
- package/src/transformers/shared/icon-transformer.ts +56 -12
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
2
|
+
import { type HttpResponse, httpService } from '@elementor/http-client';
|
|
3
|
+
import { z } from '@elementor/schema';
|
|
4
|
+
|
|
5
|
+
import { getMcpErrorMessage } from '../../utils/get-mcp-error-message';
|
|
6
|
+
import { findIconsToolPrompt } from './prompt';
|
|
7
|
+
|
|
8
|
+
const MCP_PROXY_URL = 'elementor/v1/mcp-proxy';
|
|
9
|
+
|
|
10
|
+
const MAX_QUERIES = 10;
|
|
11
|
+
|
|
12
|
+
const MAX_PER_PAGE = 25;
|
|
13
|
+
|
|
14
|
+
type IconMatch = {
|
|
15
|
+
value: string;
|
|
16
|
+
library: string;
|
|
17
|
+
name: string;
|
|
18
|
+
label: string;
|
|
19
|
+
matched_on: 'name' | 'alias' | 'term' | 'category' | 'library';
|
|
20
|
+
license: 'free' | 'pro';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type IconQueryResult = {
|
|
24
|
+
query: string;
|
|
25
|
+
matches: IconMatch[];
|
|
26
|
+
total: number;
|
|
27
|
+
page: number;
|
|
28
|
+
per_page: number;
|
|
29
|
+
truncated: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// NOTE: shape defined by Find_Icons_Ability::execute() in modules/mcp/abilities/find-icons-ability.php
|
|
33
|
+
type FindIconsResponse = {
|
|
34
|
+
results: IconQueryResult[];
|
|
35
|
+
libraries?: string[];
|
|
36
|
+
custom_libraries?: Record< string, string >;
|
|
37
|
+
categories?: string[];
|
|
38
|
+
truncated_custom_libraries?: string[];
|
|
39
|
+
font_awesome_version: string | null;
|
|
40
|
+
llm_instructions?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const initFindIconsTool = ( reg: MCPRegistryEntry ) => {
|
|
44
|
+
const { addTool } = reg;
|
|
45
|
+
|
|
46
|
+
addTool( {
|
|
47
|
+
name: 'find-icons',
|
|
48
|
+
description: findIconsToolPrompt.prompt(),
|
|
49
|
+
schema: {
|
|
50
|
+
queries: z
|
|
51
|
+
.array( z.string() )
|
|
52
|
+
.max( MAX_QUERIES )
|
|
53
|
+
.optional()
|
|
54
|
+
.describe(
|
|
55
|
+
'One plain-language phrase per icon you need, for example ["add to cart", "free shipping"]. Batch every icon a page needs into a single call.'
|
|
56
|
+
),
|
|
57
|
+
library: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe(
|
|
61
|
+
'Restrict results to one library key reported in "libraries", for example "fa-solid". Omit to search every library installed on the site.'
|
|
62
|
+
),
|
|
63
|
+
category: z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe(
|
|
67
|
+
'Restrict results to one Font Awesome category reported in "categories", for example "shopping". May be used without a query to browse that category.'
|
|
68
|
+
),
|
|
69
|
+
page: z.number().int().min( 1 ).optional(),
|
|
70
|
+
perPage: z.number().int().min( 1 ).max( MAX_PER_PAGE ).optional(),
|
|
71
|
+
},
|
|
72
|
+
outputSchema: {
|
|
73
|
+
results: z
|
|
74
|
+
.array(
|
|
75
|
+
z.object( {
|
|
76
|
+
query: z.string(),
|
|
77
|
+
matches: z.array(
|
|
78
|
+
z.object( {
|
|
79
|
+
value: z.string(),
|
|
80
|
+
library: z.string(),
|
|
81
|
+
name: z.string(),
|
|
82
|
+
label: z.string(),
|
|
83
|
+
matched_on: z.enum( [ 'name', 'alias', 'term', 'category', 'library' ] ),
|
|
84
|
+
license: z.enum( [ 'free', 'pro' ] ),
|
|
85
|
+
} )
|
|
86
|
+
),
|
|
87
|
+
total: z.number(),
|
|
88
|
+
page: z.number(),
|
|
89
|
+
per_page: z.number(),
|
|
90
|
+
truncated: z.boolean(),
|
|
91
|
+
} )
|
|
92
|
+
)
|
|
93
|
+
.describe(
|
|
94
|
+
'One entry per query, each with ranked "matches". Every match carries the "value" and "library" pair to write on the e-svg icon prop, plus "matched_on" and "license".'
|
|
95
|
+
),
|
|
96
|
+
libraries: z
|
|
97
|
+
.array( z.string() )
|
|
98
|
+
.optional()
|
|
99
|
+
.describe(
|
|
100
|
+
'Icon library keys installed on this site. Only sent when nothing matched or when browsing.'
|
|
101
|
+
),
|
|
102
|
+
customLibraries: z
|
|
103
|
+
.record( z.string(), z.string() )
|
|
104
|
+
.optional()
|
|
105
|
+
.describe(
|
|
106
|
+
'Custom (uploaded) icon libraries keyed by library. Only sent when nothing matched or when browsing.'
|
|
107
|
+
),
|
|
108
|
+
categories: z
|
|
109
|
+
.array( z.string() )
|
|
110
|
+
.optional()
|
|
111
|
+
.describe(
|
|
112
|
+
'Icon categories for the "category" filter. Only sent when nothing matched or when browsing.'
|
|
113
|
+
),
|
|
114
|
+
truncatedCustomLibraries: z
|
|
115
|
+
.array( z.string() )
|
|
116
|
+
.optional()
|
|
117
|
+
.describe( 'Custom libraries with more than 250 icons; only their first 250 icons are searchable.' ),
|
|
118
|
+
fontAwesomeVersion: z.string().nullable().describe( 'Font Awesome version the site renders icons from.' ),
|
|
119
|
+
llmInstructions: z
|
|
120
|
+
.string()
|
|
121
|
+
.optional()
|
|
122
|
+
.describe( 'Present only when nothing matched, explaining what to do next.' ),
|
|
123
|
+
},
|
|
124
|
+
handler: async ( { queries, library, category, page, perPage } ) => {
|
|
125
|
+
try {
|
|
126
|
+
const { data } = await httpService().post< HttpResponse< FindIconsResponse > >( MCP_PROXY_URL, {
|
|
127
|
+
tool: 'find-icons',
|
|
128
|
+
input: {
|
|
129
|
+
...( queries ? { queries } : {} ),
|
|
130
|
+
...( library ? { library } : {} ),
|
|
131
|
+
...( category ? { category } : {} ),
|
|
132
|
+
...( page ? { page } : {} ),
|
|
133
|
+
...( perPage ? { per_page: perPage } : {} ),
|
|
134
|
+
},
|
|
135
|
+
} );
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
results: data.data.results,
|
|
139
|
+
...( data.data.libraries ? { libraries: data.data.libraries } : {} ),
|
|
140
|
+
...( data.data.custom_libraries ? { customLibraries: data.data.custom_libraries } : {} ),
|
|
141
|
+
...( data.data.categories ? { categories: data.data.categories } : {} ),
|
|
142
|
+
...( data.data.truncated_custom_libraries
|
|
143
|
+
? { truncatedCustomLibraries: data.data.truncated_custom_libraries }
|
|
144
|
+
: {} ),
|
|
145
|
+
fontAwesomeVersion: data.data.font_awesome_version,
|
|
146
|
+
...( data.data.llm_instructions ? { llmInstructions: data.data.llm_instructions } : {} ),
|
|
147
|
+
};
|
|
148
|
+
} catch ( error ) {
|
|
149
|
+
throw new Error( getMcpErrorMessage( error, 'find-icons' ) );
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
} );
|
|
153
|
+
};
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
jest.mock( '@elementor/http-client', () => ( {
|
|
2
|
+
httpService: () => ( {
|
|
3
|
+
get: jest.fn().mockRejectedValue( new Error( 'no custom svg' ) ),
|
|
4
|
+
} ),
|
|
5
|
+
} ) );
|
|
6
|
+
|
|
1
7
|
import { resetFontAwesome7IconsCache } from '@elementor/editor-controls';
|
|
2
8
|
import { iconPropTypeUtil, stringPropTypeUtil } from '@elementor/editor-props';
|
|
3
9
|
|
|
@@ -41,6 +47,7 @@ async function resolveSavedIcon( iconClass: string, library: string ) {
|
|
|
41
47
|
|
|
42
48
|
describe( 'iconTransformer', () => {
|
|
43
49
|
const originalElementorCommon = window.elementorCommon;
|
|
50
|
+
const originalElementor = window.elementor;
|
|
44
51
|
|
|
45
52
|
beforeEach( () => {
|
|
46
53
|
jest.clearAllMocks();
|
|
@@ -63,6 +70,7 @@ describe( 'iconTransformer', () => {
|
|
|
63
70
|
|
|
64
71
|
afterEach( () => {
|
|
65
72
|
window.elementorCommon = originalElementorCommon;
|
|
73
|
+
window.elementor = originalElementor;
|
|
66
74
|
jest.restoreAllMocks();
|
|
67
75
|
} );
|
|
68
76
|
|
|
@@ -178,6 +186,37 @@ describe( 'iconTransformer', () => {
|
|
|
178
186
|
expect( result ).toEqual( { html: null, url: null } );
|
|
179
187
|
} );
|
|
180
188
|
|
|
189
|
+
it( 'falls back to the default svg when a custom library has been deleted', async () => {
|
|
190
|
+
// Arrange.
|
|
191
|
+
window.elementor = {
|
|
192
|
+
config: {
|
|
193
|
+
icons: {
|
|
194
|
+
libraries: [ { name: 'fa-solid', prefix: 'fa-', native: true } ],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
} as typeof window.elementor;
|
|
198
|
+
const defaultSvg =
|
|
199
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path d="M24.9999 4.31543"></path></svg>';
|
|
200
|
+
global.fetch = jest.fn().mockResolvedValue( {
|
|
201
|
+
ok: true,
|
|
202
|
+
headers: new Headers( { 'content-type': 'image/svg+xml' } ),
|
|
203
|
+
text: () => Promise.resolve( defaultSvg ),
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
// Act.
|
|
207
|
+
const result = await resolveSavedIcon( 'missing-set missing-set-ghost', 'missing-set' );
|
|
208
|
+
|
|
209
|
+
// Assert.
|
|
210
|
+
expect( global.fetch ).toHaveBeenCalledWith(
|
|
211
|
+
'https://example.com/assets/images/default-svg.svg',
|
|
212
|
+
expect.anything()
|
|
213
|
+
);
|
|
214
|
+
expect( result ).toEqual( {
|
|
215
|
+
html: expect.stringContaining( 'M24.9999 4.31543' ),
|
|
216
|
+
url: 'https://example.com/assets/images/default-svg.svg',
|
|
217
|
+
} );
|
|
218
|
+
} );
|
|
219
|
+
|
|
181
220
|
it( 'returns null html when value or library is missing', async () => {
|
|
182
221
|
// Act.
|
|
183
222
|
const resolve = createPropsResolver( {
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type FontAwesome7IconDefinition,
|
|
3
3
|
getFontAwesome7IconName,
|
|
4
|
+
isDeletedCustomIconLibrary,
|
|
5
|
+
resolveCustomIconSvg,
|
|
4
6
|
resolveFontAwesome7Icon,
|
|
5
7
|
} from '@elementor/editor-controls';
|
|
6
8
|
|
|
7
9
|
import { createTransformer } from '../create-transformer';
|
|
8
10
|
import type { TransformerOptions } from '../types';
|
|
9
11
|
import { processSvgContent } from './process-svg-content';
|
|
12
|
+
import { svgSrcTransformer } from './svg-src-transformer';
|
|
10
13
|
|
|
11
14
|
const EMPTY_ICON_RESULT = { html: null, url: null };
|
|
12
15
|
const ICON_SVG_SIZE = '100%';
|
|
13
16
|
const ICON_SVG_OVERFLOW = 'visible';
|
|
17
|
+
const DEFAULT_SVG_RELATIVE_PATH = 'images/default-svg.svg';
|
|
14
18
|
|
|
15
19
|
type IconValue = {
|
|
16
20
|
value?: unknown;
|
|
@@ -25,30 +29,70 @@ export const iconTransformer = createTransformer( async ( value: IconValue, { si
|
|
|
25
29
|
return EMPTY_ICON_RESULT;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
const
|
|
32
|
+
const fontAwesomeHtml = await resolveFontAwesomeSvg( library, iconValue, signal );
|
|
29
33
|
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
34
|
+
if ( fontAwesomeHtml ) {
|
|
35
|
+
return { html: fontAwesomeHtml, url: null };
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
if ( isDeletedCustomIconLibrary( library, iconValue ) ) {
|
|
39
|
+
return resolveDefaultSvg( signal );
|
|
40
|
+
}
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
const customHtml = await resolveCustomSvg( library, iconValue, signal );
|
|
43
|
+
|
|
44
|
+
if ( customHtml ) {
|
|
45
|
+
return { html: customHtml, url: null };
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
return EMPTY_ICON_RESULT;
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
async function resolveDefaultSvg( signal?: AbortSignal ) {
|
|
52
|
+
const assetsUrl = window.elementorCommon?.config?.urls?.assets;
|
|
41
53
|
|
|
42
|
-
if (
|
|
54
|
+
if ( typeof assetsUrl !== 'string' || assetsUrl === '' ) {
|
|
43
55
|
return EMPTY_ICON_RESULT;
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
|
|
58
|
+
return svgSrcTransformer(
|
|
59
|
+
{
|
|
60
|
+
id: null,
|
|
61
|
+
url: `${ assetsUrl }${ DEFAULT_SVG_RELATIVE_PATH }`,
|
|
62
|
+
},
|
|
63
|
+
{ key: 'svg', signal }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
47
66
|
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
async function resolveCustomSvg( library: string, iconValue: string, signal?: AbortSignal ): Promise< string | null > {
|
|
68
|
+
const markup = await resolveCustomIconSvg( library, iconValue, signal );
|
|
69
|
+
|
|
70
|
+
return markup ? processIconSvgContent( markup ) : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveFontAwesomeSvg(
|
|
74
|
+
library: string,
|
|
75
|
+
iconValue: string,
|
|
76
|
+
signal?: AbortSignal
|
|
77
|
+
): Promise< string | null > {
|
|
78
|
+
const iconName = getFontAwesome7IconName( iconValue );
|
|
79
|
+
|
|
80
|
+
if ( ! iconName ) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const iconData = await resolveFontAwesome7Icon( library, iconName, signal );
|
|
85
|
+
|
|
86
|
+
if ( ! iconData ) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const svgText = buildPathSvg( iconData );
|
|
91
|
+
|
|
92
|
+
return svgText ? processIconSvgContent( svgText ) : null;
|
|
93
|
+
}
|
|
50
94
|
|
|
51
|
-
function
|
|
95
|
+
function buildPathSvg( iconData: FontAwesome7IconDefinition ): string | null {
|
|
52
96
|
if ( iconData.paths.length === 0 ) {
|
|
53
97
|
return null;
|
|
54
98
|
}
|