@elementor/editor-canvas 4.3.0-999 → 4.3.0-beta2
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 +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +535 -393
- package/dist/index.mjs +516 -370
- package/package.json +20 -20
- package/src/__tests__/flex-transformer.test.ts +11 -11
- package/src/__tests__/prop-types.ts +11 -0
- package/src/init-settings-transformers.ts +4 -0
- package/src/init-style-transformers.ts +2 -2
- package/src/init.tsx +5 -4
- package/src/legacy/__tests__/create-templated-element-type.test.ts +50 -0
- package/src/legacy/__tests__/list-type.test.ts +89 -0
- package/src/legacy/create-nested-templated-element-type.ts +3 -1
- package/src/legacy/create-templated-element-type.ts +5 -2
- package/src/legacy/list-type.ts +63 -0
- package/src/legacy/replacements/inline-editing/__tests__/inline-editing-eligibility.test.ts +7 -7
- package/src/legacy/replacements/inline-editing/inline-editing-elements.tsx +22 -9
- package/src/legacy/replacements/inline-editing/inline-editing-eligibility.ts +12 -3
- package/src/legacy/tabs-model-extensions.ts +2 -5
- package/src/legacy/twig-rendering-utils.ts +11 -2
- package/src/legacy/types.ts +4 -0
- package/src/mcp/canvas-mcp.ts +2 -4
- package/src/mcp/mcp-description.ts +18 -30
- package/src/mcp/resources/__tests__/available-widgets-resource.test.ts +22 -13
- package/src/mcp/resources/__tests__/dynamic-tags-resource.test.ts +22 -30
- package/src/mcp/resources/__tests__/widgets-schema-resource.test.ts +11 -6
- package/src/mcp/resources/available-widgets-resource.ts +13 -10
- package/src/mcp/resources/dynamic-tags-resource.ts +6 -15
- package/src/mcp/resources/widgets-schema-resource.ts +8 -5
- package/src/mcp/tools/configure-element/__tests__/tool.test.ts +130 -0
- package/src/mcp/tools/configure-element/prompt.ts +3 -6
- package/src/mcp/tools/configure-element/schema.ts +6 -0
- package/src/mcp/tools/configure-element/tool.ts +11 -1
- package/src/mcp/tools/get-page-structure/tool.ts +73 -0
- package/src/mcp/utils/__tests__/do-update-element-property.test.ts +27 -1
- package/src/mcp/utils/do-update-element-property.ts +18 -6
- package/src/mcp/utils/get-mcp-error-message.ts +16 -0
- package/src/renderers/__tests__/compute-html-tag.test.ts +55 -0
- package/src/renderers/__tests__/create-dom-renderer.test.ts +60 -0
- package/src/renderers/__tests__/fixtures/html-tag-computer-cases.json +87 -0
- package/src/renderers/compute-html-tag.ts +73 -0
- package/src/renderers/create-dom-renderer.ts +13 -22
- package/src/transformers/settings/escaped-html-transformer.ts +6 -0
- package/src/transformers/shared/__tests__/icon-transformer.test.ts +199 -0
- package/src/transformers/shared/icon-transformer.ts +231 -0
- package/src/transformers/shared/process-svg-content.ts +26 -0
- package/src/transformers/shared/svg-src-transformer.ts +1 -26
- package/src/transformers/styles/flex-transformer.ts +11 -32
- package/src/utils/__tests__/sanitize-escaped-html.test.ts +105 -0
- package/src/utils/sanitize-escaped-html.ts +32 -0
- package/src/mcp/tools/build-composition/tool.ts +0 -133
- package/src/mcp/tools/get-element-config/tool.ts +0 -114
|
@@ -6,7 +6,7 @@ import { type PropValue, Schema } from '@elementor/editor-props';
|
|
|
6
6
|
import { DYNAMIC_TAGS_URI } from '../../resources/dynamic-tags-resource';
|
|
7
7
|
import { WIDGET_SCHEMA_URI } from '../../resources/widgets-schema-resource';
|
|
8
8
|
import { convertCssToAtomic } from '../../utils/convert-css-to-atomic';
|
|
9
|
-
import { doUpdateElementProperty } from '../../utils/do-update-element-property';
|
|
9
|
+
import { doUpdateElementProperty, UnsupportedPropertyError } from '../../utils/do-update-element-property';
|
|
10
10
|
import { resolveCanonicalPropKeys } from '../../utils/resolve-canonical-prop-name';
|
|
11
11
|
import { CONFIGURE_ELEMENT_GUIDE_URI, generatePrompt } from './prompt';
|
|
12
12
|
import { inputSchema as schema, outputSchema } from './schema';
|
|
@@ -61,6 +61,7 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
61
61
|
}
|
|
62
62
|
const propertiesToUpdate = resolveCanonicalPropKeys( elementType, propertiesToChange );
|
|
63
63
|
const toUpdate = Object.entries( propertiesToUpdate );
|
|
64
|
+
const skippedProps: string[] = [];
|
|
64
65
|
for ( const [ propertyName, propertyValue ] of toUpdate as [ string, PropValue ][] ) {
|
|
65
66
|
if ( ! Schema.isPropKeyConfigurable( propertyName ) ) {
|
|
66
67
|
throw new Error( `Not allowed to update ${ propertyName }` );
|
|
@@ -73,6 +74,10 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
73
74
|
propertyValue,
|
|
74
75
|
} );
|
|
75
76
|
} catch ( error ) {
|
|
77
|
+
if ( error instanceof UnsupportedPropertyError ) {
|
|
78
|
+
skippedProps.push( error.propertyName );
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
76
81
|
const errorMessage = createUpdateErrorMessage( {
|
|
77
82
|
propertyName,
|
|
78
83
|
elementId,
|
|
@@ -86,6 +91,11 @@ export const initConfigureElementTool = ( reg: MCPRegistryEntry ) => {
|
|
|
86
91
|
await applyStyleFromCss( { elementId, elementType, style } );
|
|
87
92
|
return {
|
|
88
93
|
success: true,
|
|
94
|
+
warnings: skippedProps.length
|
|
95
|
+
? `Skipped unsupported props (not in the "${ elementType }" schema; other changes were applied): ${ skippedProps.join(
|
|
96
|
+
', '
|
|
97
|
+
) }.`
|
|
98
|
+
: undefined,
|
|
89
99
|
};
|
|
90
100
|
},
|
|
91
101
|
} );
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getCurrentDocument } from '@elementor/editor-documents';
|
|
2
|
+
import { type MCPRegistryEntry } from '@elementor/editor-mcp';
|
|
3
|
+
import { type HttpResponse, httpService } from '@elementor/http-client';
|
|
4
|
+
import { z } from '@elementor/schema';
|
|
5
|
+
|
|
6
|
+
import { getMcpErrorMessage } from '../../utils/get-mcp-error-message';
|
|
7
|
+
|
|
8
|
+
const MCP_PROXY_URL = 'elementor/v1/mcp-proxy';
|
|
9
|
+
|
|
10
|
+
type GetPageStructureResponse = {
|
|
11
|
+
elements: Array< Record< string, unknown > >;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const initGetPageStructureTool = ( reg: MCPRegistryEntry ) => {
|
|
15
|
+
const { addTool } = reg;
|
|
16
|
+
|
|
17
|
+
addTool( {
|
|
18
|
+
name: 'get-page-structure',
|
|
19
|
+
description:
|
|
20
|
+
'Returns a lean Elementor element tree skeleton (id, elType, widgetType, title, nested elements) for a post or page. ' +
|
|
21
|
+
'If no postId is provided, uses the currently open document. Optionally scope to a subtree with elementId. ' +
|
|
22
|
+
"Set includeContent=true (requires elementId) to also return each node's settings and styles.",
|
|
23
|
+
schema: {
|
|
24
|
+
postId: z
|
|
25
|
+
.number()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe(
|
|
28
|
+
'WordPress post ID of the Elementor document. If omitted, uses the currently open document.'
|
|
29
|
+
),
|
|
30
|
+
elementId: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe( 'If provided, returns only the subtree rooted at that element id.' ),
|
|
34
|
+
includeContent: z
|
|
35
|
+
.boolean()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe(
|
|
38
|
+
"If true, includes each node's settings and styles (same shape build-composition accepts as input). Requires elementId."
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
outputSchema: {
|
|
42
|
+
elements: z
|
|
43
|
+
.array( z.any() )
|
|
44
|
+
.describe(
|
|
45
|
+
'Skeleton of Elementor elements (id, elType, widgetType, title, nested elements). When includeContent is true, each node also includes settings and styles.'
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
handler: async ( { postId, elementId, includeContent } ) => {
|
|
49
|
+
const resolvedPostId = postId ?? getCurrentDocument()?.id;
|
|
50
|
+
|
|
51
|
+
if ( ! resolvedPostId ) {
|
|
52
|
+
throw new Error( 'No post ID provided and no active document found.' );
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const { data } = await httpService().post< HttpResponse< GetPageStructureResponse > >( MCP_PROXY_URL, {
|
|
57
|
+
tool: 'get-page-structure',
|
|
58
|
+
input: {
|
|
59
|
+
post_id: resolvedPostId,
|
|
60
|
+
...( elementId ? { element_id: elementId } : {} ),
|
|
61
|
+
...( includeContent ? { include_content: true } : {} ),
|
|
62
|
+
},
|
|
63
|
+
} );
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
elements: data.data.elements,
|
|
67
|
+
};
|
|
68
|
+
} catch ( error ) {
|
|
69
|
+
throw new Error( getMcpErrorMessage( error, 'get-page-structure' ) );
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
} );
|
|
73
|
+
};
|
|
@@ -8,7 +8,7 @@ import { Schema } from '@elementor/editor-props';
|
|
|
8
8
|
import { getStylesSchema, getVariantByMeta } from '@elementor/editor-styles';
|
|
9
9
|
import { __privateRunCommandSync } from '@elementor/editor-v1-adapters';
|
|
10
10
|
|
|
11
|
-
import { doUpdateElementProperty } from '../do-update-element-property';
|
|
11
|
+
import { doUpdateElementProperty, UnsupportedPropertyError } from '../do-update-element-property';
|
|
12
12
|
|
|
13
13
|
jest.mock( '@elementor/editor-elements', () => ( {
|
|
14
14
|
createElementStyle: jest.fn(),
|
|
@@ -108,6 +108,32 @@ describe( 'doUpdateElementProperty', () => {
|
|
|
108
108
|
expect( __privateRunCommandSync ).not.toHaveBeenCalled();
|
|
109
109
|
} );
|
|
110
110
|
|
|
111
|
+
it( 'throws UnsupportedPropertyError when the property is not in the element schema', () => {
|
|
112
|
+
// Arrange
|
|
113
|
+
const propertyName = 'link';
|
|
114
|
+
|
|
115
|
+
// Act
|
|
116
|
+
let thrown: unknown;
|
|
117
|
+
try {
|
|
118
|
+
doUpdateElementProperty( {
|
|
119
|
+
elementId: ELEMENT_ID,
|
|
120
|
+
elementType: ELEMENT_TYPE,
|
|
121
|
+
propertyName,
|
|
122
|
+
propertyValue: { $$type: 'link', value: {} },
|
|
123
|
+
} );
|
|
124
|
+
} catch ( error ) {
|
|
125
|
+
thrown = error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Assert
|
|
129
|
+
expect( thrown ).toBeInstanceOf( UnsupportedPropertyError );
|
|
130
|
+
expect( ( thrown as UnsupportedPropertyError ).propertyName ).toBe( propertyName );
|
|
131
|
+
expect( ( thrown as UnsupportedPropertyError ).elementType ).toBe( ELEMENT_TYPE );
|
|
132
|
+
expect( ( thrown as Error ).message ).toContain( 'does not exist on element type' );
|
|
133
|
+
expect( Schema.validatePropValue ).not.toHaveBeenCalled();
|
|
134
|
+
expect( updateElementSettings ).not.toHaveBeenCalled();
|
|
135
|
+
} );
|
|
136
|
+
|
|
111
137
|
it( 'updates settings when Schema.validatePropValue reports valid PropValue', () => {
|
|
112
138
|
// Arrange
|
|
113
139
|
const propertyValue = 'resolved-title';
|
|
@@ -21,6 +21,23 @@ const LOCAL_STYLE_META = {
|
|
|
21
21
|
breakpoint: 'desktop',
|
|
22
22
|
state: null,
|
|
23
23
|
} as const;
|
|
24
|
+
|
|
25
|
+
export class UnsupportedPropertyError extends Error {
|
|
26
|
+
public readonly elementType: string;
|
|
27
|
+
public readonly propertyName: string;
|
|
28
|
+
|
|
29
|
+
constructor( elementType: string, propertyName: string, availableProperties: string[] ) {
|
|
30
|
+
super(
|
|
31
|
+
`Property "${ propertyName }" does not exist on element type "${ elementType }". Available properties are: ${ availableProperties.join(
|
|
32
|
+
', '
|
|
33
|
+
) }`
|
|
34
|
+
);
|
|
35
|
+
this.name = 'UnsupportedPropertyError';
|
|
36
|
+
this.elementType = elementType;
|
|
37
|
+
this.propertyName = propertyName;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
type CustomCssWriteMode = 'replace' | 'merge-with-stored';
|
|
25
42
|
type OwnParams = {
|
|
26
43
|
elementId: string;
|
|
@@ -151,12 +168,7 @@ export const doUpdateElementProperty = ( params: OwnParams ) => {
|
|
|
151
168
|
throw new Error( `No prop schema found for element type: ${ elementType }` );
|
|
152
169
|
}
|
|
153
170
|
if ( ! elementPropSchema[ propertyName ] ) {
|
|
154
|
-
|
|
155
|
-
throw new Error(
|
|
156
|
-
`Property "${ propertyName }" does not exist on element type "${ elementType }". Available properties are: ${ propertyNames.join(
|
|
157
|
-
', '
|
|
158
|
-
) }`
|
|
159
|
-
);
|
|
171
|
+
throw new UnsupportedPropertyError( elementType, propertyName, Object.keys( elementPropSchema ) );
|
|
160
172
|
}
|
|
161
173
|
const propKey = elementPropSchema[ propertyName ].key;
|
|
162
174
|
const value = resolvePropValue( propertyValue, propKey );
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AxiosError } from '@elementor/http-client';
|
|
2
|
+
|
|
3
|
+
export function getMcpErrorMessage( error: unknown, toolName: string ): string {
|
|
4
|
+
if ( error instanceof AxiosError ) {
|
|
5
|
+
const data = error.response?.data as { message?: string; code?: string } | undefined;
|
|
6
|
+
if ( data?.message ) {
|
|
7
|
+
return data.code ? `${ data.code }: ${ data.message }` : data.message;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if ( error instanceof Error ) {
|
|
12
|
+
return error.message;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `${ toolName } failed with an unknown error.`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
import { computeHtmlTag } from '../compute-html-tag';
|
|
5
|
+
|
|
6
|
+
type HtmlTagComputerCase = {
|
|
7
|
+
settings: Record< string, unknown >;
|
|
8
|
+
default: string;
|
|
9
|
+
followLink: boolean;
|
|
10
|
+
expected: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const contractCases: HtmlTagComputerCase[] = JSON.parse(
|
|
14
|
+
readFileSync( join( __dirname, 'fixtures/html-tag-computer-cases.json' ), 'utf8' )
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
describe( 'computeHtmlTag', () => {
|
|
18
|
+
it.each( contractCases )(
|
|
19
|
+
'contract case: default=$default followLink=$followLink expected=$expected',
|
|
20
|
+
( { settings, default: defaultTag, followLink, expected } ) => {
|
|
21
|
+
expect( computeHtmlTag( settings, defaultTag, { followLink } ) ).toBe( expected );
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
it( 'uses link tag when href is present', () => {
|
|
26
|
+
expect(
|
|
27
|
+
computeHtmlTag(
|
|
28
|
+
{
|
|
29
|
+
tag: 'div',
|
|
30
|
+
link: {
|
|
31
|
+
href: 'https://example.com',
|
|
32
|
+
tag: 'a',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
'div'
|
|
36
|
+
)
|
|
37
|
+
).toBe( 'a' );
|
|
38
|
+
} );
|
|
39
|
+
|
|
40
|
+
it( 'ignores link when followLink is false', () => {
|
|
41
|
+
expect(
|
|
42
|
+
computeHtmlTag(
|
|
43
|
+
{
|
|
44
|
+
tag: 'h4',
|
|
45
|
+
link: {
|
|
46
|
+
href: 'https://example.com',
|
|
47
|
+
tag: 'a',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
'h2',
|
|
51
|
+
{ followLink: false }
|
|
52
|
+
)
|
|
53
|
+
).toBe( 'h4' );
|
|
54
|
+
} );
|
|
55
|
+
} );
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
/* eslint-disable testing-library/render-result-naming-convention */
|
|
2
2
|
import { createDomRenderer } from '../create-dom-renderer';
|
|
3
3
|
|
|
4
|
+
const TEST_ALLOWED_HTML_WRAPPER_TAGS = [ 'a', 'div', 'form', 'span' ];
|
|
5
|
+
|
|
4
6
|
describe( 'createDomRenderer', () => {
|
|
7
|
+
beforeEach( () => {
|
|
8
|
+
window.elementorCommon = {
|
|
9
|
+
config: {
|
|
10
|
+
allowedHTMLWrapperTags: TEST_ALLOWED_HTML_WRAPPER_TAGS,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
} );
|
|
14
|
+
|
|
15
|
+
afterEach( () => {
|
|
16
|
+
delete window.elementorCommon;
|
|
17
|
+
} );
|
|
18
|
+
|
|
5
19
|
it.each( [
|
|
6
20
|
{
|
|
7
21
|
title: 'basic string',
|
|
@@ -15,12 +29,24 @@ describe( 'createDomRenderer', () => {
|
|
|
15
29
|
context: { tag: 'a' },
|
|
16
30
|
expected: '<a></a>',
|
|
17
31
|
},
|
|
32
|
+
{
|
|
33
|
+
title: 'allowed form html tag',
|
|
34
|
+
template: `<{{ tag | e( 'html_tag' ) }}></{{ tag | e( 'html_tag' ) }}>`,
|
|
35
|
+
context: { tag: 'form' },
|
|
36
|
+
expected: '<form></form>',
|
|
37
|
+
},
|
|
18
38
|
{
|
|
19
39
|
title: 'disallowed html tags',
|
|
20
40
|
template: `<{{ tag | e( 'html_tag' ) }}></{{ tag | e( 'html_tag' ) }}>`,
|
|
21
41
|
context: { tag: 'script' },
|
|
22
42
|
expected: '<div></div>',
|
|
23
43
|
},
|
|
44
|
+
{
|
|
45
|
+
title: 'allowed html tag with uppercase casing',
|
|
46
|
+
template: `<{{ tag | e( 'html_tag' ) }}></{{ tag | e( 'html_tag' ) }}>`,
|
|
47
|
+
context: { tag: 'DIV' },
|
|
48
|
+
expected: '<DIV></DIV>',
|
|
49
|
+
},
|
|
24
50
|
{
|
|
25
51
|
title: 'allowed url (http)',
|
|
26
52
|
template: `{{ url | e( 'full_url' ) }}`,
|
|
@@ -63,4 +89,38 @@ describe( 'createDomRenderer', () => {
|
|
|
63
89
|
// Assert.
|
|
64
90
|
expect( result ).toBe( expected );
|
|
65
91
|
} );
|
|
92
|
+
|
|
93
|
+
it( 'should validate a tag added via localized config', async () => {
|
|
94
|
+
// Arrange.
|
|
95
|
+
window.elementorCommon = {
|
|
96
|
+
config: {
|
|
97
|
+
allowedHTMLWrapperTags: [ ...TEST_ALLOWED_HTML_WRAPPER_TAGS, 'custom-tag' ],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const domRenderer = createDomRenderer();
|
|
101
|
+
const template = `<{{ tag | e( 'html_tag' ) }}></{{ tag | e( 'html_tag' ) }}>`;
|
|
102
|
+
|
|
103
|
+
domRenderer.register( 'test-template', template );
|
|
104
|
+
|
|
105
|
+
// Act.
|
|
106
|
+
const result = await domRenderer.render( 'test-template', { tag: 'custom-tag' } );
|
|
107
|
+
|
|
108
|
+
// Assert.
|
|
109
|
+
expect( result ).toBe( '<custom-tag></custom-tag>' );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( 'should fail closed to div when the localized config is missing, even for an otherwise-safe tag', async () => {
|
|
113
|
+
// Arrange.
|
|
114
|
+
delete window.elementorCommon;
|
|
115
|
+
const domRenderer = createDomRenderer();
|
|
116
|
+
const template = `<{{ tag | e( 'html_tag' ) }}></{{ tag | e( 'html_tag' ) }}>`;
|
|
117
|
+
|
|
118
|
+
domRenderer.register( 'test-template', template );
|
|
119
|
+
|
|
120
|
+
// Act.
|
|
121
|
+
const result = await domRenderer.render( 'test-template', { tag: 'a' } );
|
|
122
|
+
|
|
123
|
+
// Assert.
|
|
124
|
+
expect( result ).toBe( '<div></div>' );
|
|
125
|
+
} );
|
|
66
126
|
} );
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"settings": {},
|
|
4
|
+
"default": "div",
|
|
5
|
+
"followLink": true,
|
|
6
|
+
"expected": "div"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"settings": { "tag": "header" },
|
|
10
|
+
"default": "div",
|
|
11
|
+
"followLink": true,
|
|
12
|
+
"expected": "header"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"settings": { "tag": "" },
|
|
16
|
+
"default": "div",
|
|
17
|
+
"followLink": true,
|
|
18
|
+
"expected": "div"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"settings": {
|
|
22
|
+
"link": {
|
|
23
|
+
"href": "https://example.com",
|
|
24
|
+
"tag": "a"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"default": "div",
|
|
28
|
+
"followLink": true,
|
|
29
|
+
"expected": "a"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"settings": {
|
|
33
|
+
"link": {
|
|
34
|
+
"tag": "a"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"default": "button",
|
|
38
|
+
"followLink": true,
|
|
39
|
+
"expected": "button"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"settings": {
|
|
43
|
+
"link": {
|
|
44
|
+
"tag": "a",
|
|
45
|
+
"attributes": "href=\"https://example.com\""
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"default": "div",
|
|
49
|
+
"followLink": true,
|
|
50
|
+
"expected": "a"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"settings": {
|
|
54
|
+
"tag": "div",
|
|
55
|
+
"link": {
|
|
56
|
+
"href": "https://example.com",
|
|
57
|
+
"tag": "a"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"default": "div",
|
|
61
|
+
"followLink": true,
|
|
62
|
+
"expected": "a"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"settings": {
|
|
66
|
+
"tag": "div",
|
|
67
|
+
"link": {
|
|
68
|
+
"href": "https://example.com",
|
|
69
|
+
"tag": "a"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"default": "div",
|
|
73
|
+
"followLink": false,
|
|
74
|
+
"expected": "div"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"settings": {
|
|
78
|
+
"tag": {
|
|
79
|
+
"$$type": "string",
|
|
80
|
+
"value": "h3"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"default": "h2",
|
|
84
|
+
"followLink": true,
|
|
85
|
+
"expected": "h3"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type ComputeHtmlTagOptions = {
|
|
2
|
+
followLink?: boolean;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_LINK_TAG = 'a';
|
|
6
|
+
|
|
7
|
+
type LinkSettings = Record< string, unknown >;
|
|
8
|
+
|
|
9
|
+
export function computeHtmlTag(
|
|
10
|
+
settings: Record< string, unknown >,
|
|
11
|
+
defaultTag: string,
|
|
12
|
+
options: ComputeHtmlTagOptions = {}
|
|
13
|
+
): string {
|
|
14
|
+
const followLink = options.followLink ?? true;
|
|
15
|
+
|
|
16
|
+
if ( followLink && settingsHaveActiveLink( settings ) ) {
|
|
17
|
+
const link = settings.link;
|
|
18
|
+
|
|
19
|
+
return extractLinkHtmlTag( isRecord( link ) ? link : {} );
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const settingsTag = extractHtmlTagValue( settings.tag );
|
|
23
|
+
|
|
24
|
+
if ( null !== settingsTag && '' !== settingsTag ) {
|
|
25
|
+
return settingsTag;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return defaultTag;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function settingsHaveActiveLink( settings: Record< string, unknown > ): boolean {
|
|
32
|
+
const link = settings.link;
|
|
33
|
+
|
|
34
|
+
if ( ! isRecord( link ) ) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const href = extractHtmlTagValue( link.href );
|
|
39
|
+
|
|
40
|
+
if ( null !== href && '' !== href ) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const attributes = link.attributes;
|
|
45
|
+
|
|
46
|
+
return typeof attributes === 'string' && '' !== attributes;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function extractLinkHtmlTag( link: LinkSettings ): string {
|
|
50
|
+
const tag = extractHtmlTagValue( link.tag );
|
|
51
|
+
|
|
52
|
+
if ( null !== tag && '' !== tag ) {
|
|
53
|
+
return tag;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return DEFAULT_LINK_TAG;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractHtmlTagValue( value: unknown ): string | null {
|
|
60
|
+
if ( isRecord( value ) && typeof value.value === 'string' ) {
|
|
61
|
+
return value.value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if ( typeof value === 'string' ) {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isRecord( value: unknown ): value is Record< string, unknown > {
|
|
72
|
+
return typeof value === 'object' && null !== value && ! Array.isArray( value );
|
|
73
|
+
}
|
|
@@ -18,29 +18,20 @@ export function createDomRenderer(): DomRenderer {
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* PHP (`Utils::get_allowed_html_wrapper_tags()`) is the single source of truth for this
|
|
23
|
+
* list; it's localized via `elementorCommon.config`. If it's ever missing, fail closed
|
|
24
|
+
* (no tags allowed) rather than fall back to a second hardcoded copy that could drift.
|
|
25
|
+
*/
|
|
26
|
+
function getAllowedHtmlWrapperTags(): readonly string[] {
|
|
27
|
+
return window.elementorCommon?.config?.allowedHTMLWrapperTags ?? [];
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
function escapeHtmlTag( value: string ) {
|
|
22
|
-
const allowedTags =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'button',
|
|
27
|
-
'div',
|
|
28
|
-
'footer',
|
|
29
|
-
'h1',
|
|
30
|
-
'h2',
|
|
31
|
-
'h3',
|
|
32
|
-
'h4',
|
|
33
|
-
'h5',
|
|
34
|
-
'h6',
|
|
35
|
-
'header',
|
|
36
|
-
'main',
|
|
37
|
-
'nav',
|
|
38
|
-
'p',
|
|
39
|
-
'section',
|
|
40
|
-
'span',
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
return allowedTags.includes( value ) ? value : 'div';
|
|
31
|
+
const allowedTags = getAllowedHtmlWrapperTags();
|
|
32
|
+
const normalizedTag = value?.toLowerCase?.() ?? '';
|
|
33
|
+
|
|
34
|
+
return allowedTags.includes( normalizedTag ) ? value : 'div';
|
|
44
35
|
}
|
|
45
36
|
|
|
46
37
|
function escapeURL( value: string ) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { sanitizeEscapedHtml } from '../../utils/sanitize-escaped-html';
|
|
2
|
+
import { createTransformer } from '../create-transformer';
|
|
3
|
+
|
|
4
|
+
export const escapedHtmlTransformer = createTransformer( ( value: string | null ) => {
|
|
5
|
+
return sanitizeEscapedHtml( value );
|
|
6
|
+
} );
|