@elementor/editor-canvas 4.2.0-875 → 4.2.0-876
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 +70 -0
- package/dist/index.mjs +70 -0
- package/package.json +18 -18
- package/src/composition-builder/__tests__/composition-builder.test.ts +187 -1
- package/src/composition-builder/composition-builder.ts +6 -0
- package/src/composition-builder/utils/__tests__/required-children-enforcer.test.ts +79 -0
- package/src/composition-builder/utils/__tests__/required-default-child-tags.test.ts +46 -0
- package/src/composition-builder/utils/required-children-enforcer.ts +60 -0
- package/src/composition-builder/utils/required-default-child-tags.ts +31 -0
- package/src/mcp/resources/widgets-schema-resource.ts +6 -0
- package/src/mcp/tools/build-composition/prompt.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -109,6 +109,27 @@ var import_editor_mcp = require("@elementor/editor-mcp");
|
|
|
109
109
|
var import_editor_props = require("@elementor/editor-props");
|
|
110
110
|
var import_editor_styles = require("@elementor/editor-styles");
|
|
111
111
|
|
|
112
|
+
// src/composition-builder/utils/required-default-child-tags.ts
|
|
113
|
+
function resolveDefaultChildTemplateTagName(template) {
|
|
114
|
+
const elementType = template.elType;
|
|
115
|
+
if (elementType === "widget") {
|
|
116
|
+
return typeof template.widgetType === "string" ? template.widgetType : "";
|
|
117
|
+
}
|
|
118
|
+
return typeof elementType === "string" ? elementType : "";
|
|
119
|
+
}
|
|
120
|
+
function getRequiredDefaultChildTemplates(elementConfig) {
|
|
121
|
+
const defaultChildren = elementConfig?.default_children;
|
|
122
|
+
if (!Array.isArray(defaultChildren)) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
return defaultChildren.filter(
|
|
126
|
+
(child) => !!child?.meta?.required
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
function getRequiredDefaultChildTagNames(elementConfig) {
|
|
130
|
+
return getRequiredDefaultChildTemplates(elementConfig).map(resolveDefaultChildTemplateTagName).filter((tag) => Boolean(tag));
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
// src/mcp/utils/element-data-util.ts
|
|
113
134
|
var import_editor_elements = require("@elementor/editor-elements");
|
|
114
135
|
function hasV3Controls(controls) {
|
|
@@ -325,6 +346,10 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
|
|
|
325
346
|
...allowedParents.length ? { allowed_parents: allowedParents } : {}
|
|
326
347
|
};
|
|
327
348
|
}
|
|
349
|
+
const requiredDirectChildTags = getRequiredDefaultChildTagNames(widgetData);
|
|
350
|
+
if (requiredDirectChildTags.length) {
|
|
351
|
+
llmGuidance.required_direct_children = requiredDirectChildTags;
|
|
352
|
+
}
|
|
328
353
|
return {
|
|
329
354
|
contents: [
|
|
330
355
|
{
|
|
@@ -4091,6 +4116,46 @@ var validateInput = {
|
|
|
4091
4116
|
}
|
|
4092
4117
|
};
|
|
4093
4118
|
|
|
4119
|
+
// src/composition-builder/utils/required-children-enforcer.ts
|
|
4120
|
+
var REQUIRED_CHILD_SCHEMA_HINT = "Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.";
|
|
4121
|
+
var RequiredChildrenEnforcer = class {
|
|
4122
|
+
elementType;
|
|
4123
|
+
requiredTemplates;
|
|
4124
|
+
constructor(elementType, widgetsCache) {
|
|
4125
|
+
this.elementType = elementType;
|
|
4126
|
+
this.requiredTemplates = getRequiredDefaultChildTemplates(widgetsCache[elementType]);
|
|
4127
|
+
}
|
|
4128
|
+
enforce(xml) {
|
|
4129
|
+
if (this.requiredTemplates.length === 0) {
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
4132
|
+
const errors = [];
|
|
4133
|
+
for (const rootNode of Array.from(xml.children)) {
|
|
4134
|
+
this.collectMissingRequiredErrors(rootNode, errors);
|
|
4135
|
+
}
|
|
4136
|
+
if (errors.length) {
|
|
4137
|
+
throw new Error(`${errors.join("\n")}
|
|
4138
|
+
${REQUIRED_CHILD_SCHEMA_HINT}`);
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
collectMissingRequiredErrors(node, errors) {
|
|
4142
|
+
if (node.tagName === this.elementType) {
|
|
4143
|
+
const existingChildTags = new Set(Array.from(node.children).map((child) => child.tagName));
|
|
4144
|
+
const missingTags = this.requiredTemplates.map(resolveDefaultChildTemplateTagName).filter((tag) => tag && !existingChildTags.has(tag));
|
|
4145
|
+
if (missingTags.length) {
|
|
4146
|
+
const configurationId = node.getAttribute("configuration-id");
|
|
4147
|
+
const location2 = configurationId ? `<${node.tagName} configuration-id="${configurationId}">` : `<${node.tagName}>`;
|
|
4148
|
+
errors.push(
|
|
4149
|
+
`${location2} Missing required direct child element tag(s): ${missingTags.join(", ")}.`
|
|
4150
|
+
);
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
for (const childNode of Array.from(node.children)) {
|
|
4154
|
+
this.collectMissingRequiredErrors(childNode, errors);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
};
|
|
4158
|
+
|
|
4094
4159
|
// src/composition-builder/composition-builder.ts
|
|
4095
4160
|
var CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE = "createElement did not return an element container with a model.";
|
|
4096
4161
|
var CompositionBuilder = class _CompositionBuilder {
|
|
@@ -4294,6 +4359,10 @@ var CompositionBuilder = class _CompositionBuilder {
|
|
|
4294
4359
|
throw new Error(`Unknown widget type: ${node.tagName}`);
|
|
4295
4360
|
}
|
|
4296
4361
|
});
|
|
4362
|
+
Object.keys(widgetsCache).forEach((elementType) => {
|
|
4363
|
+
const requiredChildrenEnforcer = new RequiredChildrenEnforcer(elementType, widgetsCache);
|
|
4364
|
+
requiredChildrenEnforcer.enforce(this.xml);
|
|
4365
|
+
});
|
|
4297
4366
|
const childTypeErrors = [];
|
|
4298
4367
|
for (const rootChild of Array.from(this.xml.children)) {
|
|
4299
4368
|
childTypeErrors.push(...this.validateChildTypes(rootChild, widgetsCache));
|
|
@@ -4372,6 +4441,7 @@ This tool support v4 elements only
|
|
|
4372
4441
|
## NESTED ELEMENTS
|
|
4373
4442
|
Some elements have internal tree structures (nesting). When using these elements, you MUST build the FULL tree in XML.
|
|
4374
4443
|
- Check \`llm_guidance.nesting\` in widget schemas for structure requirements
|
|
4444
|
+
- \`llm_guidance.required_direct_children\` lists element types that must appear as direct child tags in XML (from widget defaults)
|
|
4375
4445
|
- \`allowed_child_types\` lists which element types can be nested inside
|
|
4376
4446
|
- \`allowed_parents\` lists which element types this element can be placed inside
|
|
4377
4447
|
|
package/dist/index.mjs
CHANGED
|
@@ -52,6 +52,27 @@ import {
|
|
|
52
52
|
} from "@elementor/editor-props";
|
|
53
53
|
import { getStylesSchema } from "@elementor/editor-styles";
|
|
54
54
|
|
|
55
|
+
// src/composition-builder/utils/required-default-child-tags.ts
|
|
56
|
+
function resolveDefaultChildTemplateTagName(template) {
|
|
57
|
+
const elementType = template.elType;
|
|
58
|
+
if (elementType === "widget") {
|
|
59
|
+
return typeof template.widgetType === "string" ? template.widgetType : "";
|
|
60
|
+
}
|
|
61
|
+
return typeof elementType === "string" ? elementType : "";
|
|
62
|
+
}
|
|
63
|
+
function getRequiredDefaultChildTemplates(elementConfig) {
|
|
64
|
+
const defaultChildren = elementConfig?.default_children;
|
|
65
|
+
if (!Array.isArray(defaultChildren)) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return defaultChildren.filter(
|
|
69
|
+
(child) => !!child?.meta?.required
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
function getRequiredDefaultChildTagNames(elementConfig) {
|
|
73
|
+
return getRequiredDefaultChildTemplates(elementConfig).map(resolveDefaultChildTemplateTagName).filter((tag) => Boolean(tag));
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
// src/mcp/utils/element-data-util.ts
|
|
56
77
|
import { getWidgetsCache } from "@elementor/editor-elements";
|
|
57
78
|
function hasV3Controls(controls) {
|
|
@@ -268,6 +289,10 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
|
|
|
268
289
|
...allowedParents.length ? { allowed_parents: allowedParents } : {}
|
|
269
290
|
};
|
|
270
291
|
}
|
|
292
|
+
const requiredDirectChildTags = getRequiredDefaultChildTagNames(widgetData);
|
|
293
|
+
if (requiredDirectChildTags.length) {
|
|
294
|
+
llmGuidance.required_direct_children = requiredDirectChildTags;
|
|
295
|
+
}
|
|
271
296
|
return {
|
|
272
297
|
contents: [
|
|
273
298
|
{
|
|
@@ -4086,6 +4111,46 @@ var validateInput = {
|
|
|
4086
4111
|
}
|
|
4087
4112
|
};
|
|
4088
4113
|
|
|
4114
|
+
// src/composition-builder/utils/required-children-enforcer.ts
|
|
4115
|
+
var REQUIRED_CHILD_SCHEMA_HINT = "Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.";
|
|
4116
|
+
var RequiredChildrenEnforcer = class {
|
|
4117
|
+
elementType;
|
|
4118
|
+
requiredTemplates;
|
|
4119
|
+
constructor(elementType, widgetsCache) {
|
|
4120
|
+
this.elementType = elementType;
|
|
4121
|
+
this.requiredTemplates = getRequiredDefaultChildTemplates(widgetsCache[elementType]);
|
|
4122
|
+
}
|
|
4123
|
+
enforce(xml) {
|
|
4124
|
+
if (this.requiredTemplates.length === 0) {
|
|
4125
|
+
return;
|
|
4126
|
+
}
|
|
4127
|
+
const errors = [];
|
|
4128
|
+
for (const rootNode of Array.from(xml.children)) {
|
|
4129
|
+
this.collectMissingRequiredErrors(rootNode, errors);
|
|
4130
|
+
}
|
|
4131
|
+
if (errors.length) {
|
|
4132
|
+
throw new Error(`${errors.join("\n")}
|
|
4133
|
+
${REQUIRED_CHILD_SCHEMA_HINT}`);
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
collectMissingRequiredErrors(node, errors) {
|
|
4137
|
+
if (node.tagName === this.elementType) {
|
|
4138
|
+
const existingChildTags = new Set(Array.from(node.children).map((child) => child.tagName));
|
|
4139
|
+
const missingTags = this.requiredTemplates.map(resolveDefaultChildTemplateTagName).filter((tag) => tag && !existingChildTags.has(tag));
|
|
4140
|
+
if (missingTags.length) {
|
|
4141
|
+
const configurationId = node.getAttribute("configuration-id");
|
|
4142
|
+
const location2 = configurationId ? `<${node.tagName} configuration-id="${configurationId}">` : `<${node.tagName}>`;
|
|
4143
|
+
errors.push(
|
|
4144
|
+
`${location2} Missing required direct child element tag(s): ${missingTags.join(", ")}.`
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
for (const childNode of Array.from(node.children)) {
|
|
4149
|
+
this.collectMissingRequiredErrors(childNode, errors);
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
};
|
|
4153
|
+
|
|
4089
4154
|
// src/composition-builder/composition-builder.ts
|
|
4090
4155
|
var CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE = "createElement did not return an element container with a model.";
|
|
4091
4156
|
var CompositionBuilder = class _CompositionBuilder {
|
|
@@ -4289,6 +4354,10 @@ var CompositionBuilder = class _CompositionBuilder {
|
|
|
4289
4354
|
throw new Error(`Unknown widget type: ${node.tagName}`);
|
|
4290
4355
|
}
|
|
4291
4356
|
});
|
|
4357
|
+
Object.keys(widgetsCache).forEach((elementType) => {
|
|
4358
|
+
const requiredChildrenEnforcer = new RequiredChildrenEnforcer(elementType, widgetsCache);
|
|
4359
|
+
requiredChildrenEnforcer.enforce(this.xml);
|
|
4360
|
+
});
|
|
4292
4361
|
const childTypeErrors = [];
|
|
4293
4362
|
for (const rootChild of Array.from(this.xml.children)) {
|
|
4294
4363
|
childTypeErrors.push(...this.validateChildTypes(rootChild, widgetsCache));
|
|
@@ -4367,6 +4436,7 @@ This tool support v4 elements only
|
|
|
4367
4436
|
## NESTED ELEMENTS
|
|
4368
4437
|
Some elements have internal tree structures (nesting). When using these elements, you MUST build the FULL tree in XML.
|
|
4369
4438
|
- Check \`llm_guidance.nesting\` in widget schemas for structure requirements
|
|
4439
|
+
- \`llm_guidance.required_direct_children\` lists element types that must appear as direct child tags in XML (from widget defaults)
|
|
4370
4440
|
- \`allowed_child_types\` lists which element types can be nested inside
|
|
4371
4441
|
- \`allowed_parents\` lists which element types this element can be placed inside
|
|
4372
4442
|
|
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.2.0-
|
|
4
|
+
"version": "4.2.0-876",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -37,25 +37,25 @@
|
|
|
37
37
|
"react-dom": "^18.3.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@elementor/editor": "4.2.0-
|
|
40
|
+
"@elementor/editor": "4.2.0-876",
|
|
41
41
|
"dompurify": "^3.2.6",
|
|
42
|
-
"@elementor/editor-controls": "4.2.0-
|
|
43
|
-
"@elementor/editor-documents": "4.2.0-
|
|
44
|
-
"@elementor/editor-elements": "4.2.0-
|
|
45
|
-
"@elementor/editor-interactions": "4.2.0-
|
|
46
|
-
"@elementor/editor-mcp": "4.2.0-
|
|
47
|
-
"@elementor/editor-notifications": "4.2.0-
|
|
48
|
-
"@elementor/editor-props": "4.2.0-
|
|
49
|
-
"@elementor/editor-responsive": "4.2.0-
|
|
50
|
-
"@elementor/editor-styles": "4.2.0-
|
|
51
|
-
"@elementor/editor-styles-repository": "4.2.0-
|
|
52
|
-
"@elementor/editor-ui": "4.2.0-
|
|
53
|
-
"@elementor/editor-v1-adapters": "4.2.0-
|
|
54
|
-
"@elementor/schema": "4.2.0-
|
|
55
|
-
"@elementor/twing": "4.2.0-
|
|
42
|
+
"@elementor/editor-controls": "4.2.0-876",
|
|
43
|
+
"@elementor/editor-documents": "4.2.0-876",
|
|
44
|
+
"@elementor/editor-elements": "4.2.0-876",
|
|
45
|
+
"@elementor/editor-interactions": "4.2.0-876",
|
|
46
|
+
"@elementor/editor-mcp": "4.2.0-876",
|
|
47
|
+
"@elementor/editor-notifications": "4.2.0-876",
|
|
48
|
+
"@elementor/editor-props": "4.2.0-876",
|
|
49
|
+
"@elementor/editor-responsive": "4.2.0-876",
|
|
50
|
+
"@elementor/editor-styles": "4.2.0-876",
|
|
51
|
+
"@elementor/editor-styles-repository": "4.2.0-876",
|
|
52
|
+
"@elementor/editor-ui": "4.2.0-876",
|
|
53
|
+
"@elementor/editor-v1-adapters": "4.2.0-876",
|
|
54
|
+
"@elementor/schema": "4.2.0-876",
|
|
55
|
+
"@elementor/twing": "4.2.0-876",
|
|
56
56
|
"@elementor/ui": "1.37.5",
|
|
57
|
-
"@elementor/utils": "4.2.0-
|
|
58
|
-
"@elementor/wp-media": "4.2.0-
|
|
57
|
+
"@elementor/utils": "4.2.0-876",
|
|
58
|
+
"@elementor/wp-media": "4.2.0-876",
|
|
59
59
|
"@floating-ui/react": "^0.27.5",
|
|
60
60
|
"@wordpress/i18n": "^5.13.0"
|
|
61
61
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type V1Element } from '@elementor/editor-elements';
|
|
1
|
+
import { type CreateElementParams, type V1Element, type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
2
|
|
|
3
3
|
import { CompositionBuilder } from '../composition-builder';
|
|
4
4
|
|
|
@@ -188,3 +188,189 @@ describe( 'CompositionBuilder.build applyProperties after create', () => {
|
|
|
188
188
|
expect( doUpdateElementProperty ).not.toHaveBeenCalled();
|
|
189
189
|
} );
|
|
190
190
|
} );
|
|
191
|
+
|
|
192
|
+
describe( 'CompositionBuilder.build required children', () => {
|
|
193
|
+
it( 'rejects build when required direct children are absent from XML', async () => {
|
|
194
|
+
// Arrange
|
|
195
|
+
let elementIdSequence = 0;
|
|
196
|
+
const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
|
|
197
|
+
const createElementMock = jest.fn().mockReturnValue( createdElement );
|
|
198
|
+
const formWidgetsCache = {
|
|
199
|
+
'e-form': {
|
|
200
|
+
title: 'Form',
|
|
201
|
+
controls: {},
|
|
202
|
+
elType: 'widget',
|
|
203
|
+
default_children: [
|
|
204
|
+
{
|
|
205
|
+
elType: 'e-form-success-message',
|
|
206
|
+
meta: { required: true },
|
|
207
|
+
elements: [],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
elType: 'e-form-error-message',
|
|
211
|
+
meta: { required: true },
|
|
212
|
+
elements: [],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
elType: 'widget',
|
|
216
|
+
widgetType: 'e-form-input',
|
|
217
|
+
elements: [],
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
'e-form-error-message': {
|
|
222
|
+
title: 'Error',
|
|
223
|
+
controls: {},
|
|
224
|
+
elType: 'e-form-error-message',
|
|
225
|
+
},
|
|
226
|
+
'e-form-input': { title: 'Input', controls: {}, elType: 'widget' },
|
|
227
|
+
'e-form-success-message': {
|
|
228
|
+
title: 'Success',
|
|
229
|
+
controls: {},
|
|
230
|
+
elType: 'e-form-success-message',
|
|
231
|
+
},
|
|
232
|
+
} as Record< string, V1ElementConfig >;
|
|
233
|
+
const builder = CompositionBuilder.fromXMLString(
|
|
234
|
+
'<e-form configuration-id="form-1"><e-form-input /></e-form>',
|
|
235
|
+
{
|
|
236
|
+
createElement: createElementMock,
|
|
237
|
+
deleteElement: jest.fn(),
|
|
238
|
+
getContainer: jest.fn(),
|
|
239
|
+
generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
|
|
240
|
+
getWidgetsCache: jest.fn().mockReturnValue( formWidgetsCache ),
|
|
241
|
+
doUpdateElementProperty: jest.fn(),
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Act & Assert
|
|
246
|
+
await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow(
|
|
247
|
+
/Missing required direct child element tag\(s\): e-form-success-message, e-form-error-message/
|
|
248
|
+
);
|
|
249
|
+
expect( createElementMock ).not.toHaveBeenCalled();
|
|
250
|
+
} );
|
|
251
|
+
|
|
252
|
+
it( 'rejects build when only some required direct children exist', async () => {
|
|
253
|
+
// Arrange
|
|
254
|
+
let elementIdSequence = 0;
|
|
255
|
+
const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
|
|
256
|
+
const createElementMock = jest.fn().mockReturnValue( createdElement );
|
|
257
|
+
const formWidgetsCache = {
|
|
258
|
+
'e-form': {
|
|
259
|
+
title: 'Form',
|
|
260
|
+
controls: {},
|
|
261
|
+
elType: 'widget',
|
|
262
|
+
default_children: [
|
|
263
|
+
{
|
|
264
|
+
elType: 'e-form-success-message',
|
|
265
|
+
meta: { required: true },
|
|
266
|
+
elements: [],
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
elType: 'e-form-error-message',
|
|
270
|
+
meta: { required: true },
|
|
271
|
+
elements: [],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
elType: 'widget',
|
|
275
|
+
widgetType: 'e-form-input',
|
|
276
|
+
elements: [],
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
'e-form-error-message': {
|
|
281
|
+
title: 'Error',
|
|
282
|
+
controls: {},
|
|
283
|
+
elType: 'e-form-error-message',
|
|
284
|
+
},
|
|
285
|
+
'e-form-input': { title: 'Input', controls: {}, elType: 'widget' },
|
|
286
|
+
'e-form-success-message': {
|
|
287
|
+
title: 'Success',
|
|
288
|
+
controls: {},
|
|
289
|
+
elType: 'e-form-success-message',
|
|
290
|
+
},
|
|
291
|
+
} as Record< string, V1ElementConfig >;
|
|
292
|
+
const builder = CompositionBuilder.fromXMLString(
|
|
293
|
+
'<e-form configuration-id="form-1"><e-form-success-message /><e-form-input /></e-form>',
|
|
294
|
+
{
|
|
295
|
+
createElement: createElementMock,
|
|
296
|
+
deleteElement: jest.fn(),
|
|
297
|
+
getContainer: jest.fn(),
|
|
298
|
+
generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
|
|
299
|
+
getWidgetsCache: jest.fn().mockReturnValue( formWidgetsCache ),
|
|
300
|
+
doUpdateElementProperty: jest.fn(),
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Act & Assert
|
|
305
|
+
await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow(
|
|
306
|
+
/Missing required direct child element tag\(s\): e-form-error-message/
|
|
307
|
+
);
|
|
308
|
+
expect( createElementMock ).not.toHaveBeenCalled();
|
|
309
|
+
} );
|
|
310
|
+
|
|
311
|
+
it( 'creates elements when XML includes all required direct children', async () => {
|
|
312
|
+
// Arrange
|
|
313
|
+
let elementIdSequence = 0;
|
|
314
|
+
const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
|
|
315
|
+
const createElementMock = jest.fn().mockReturnValue( createdElement );
|
|
316
|
+
const formWidgetsCache = {
|
|
317
|
+
'e-form': {
|
|
318
|
+
title: 'Form',
|
|
319
|
+
controls: {},
|
|
320
|
+
elType: 'widget',
|
|
321
|
+
default_children: [
|
|
322
|
+
{
|
|
323
|
+
elType: 'e-form-success-message',
|
|
324
|
+
meta: { required: true },
|
|
325
|
+
elements: [],
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
elType: 'e-form-error-message',
|
|
329
|
+
meta: { required: true },
|
|
330
|
+
elements: [],
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
elType: 'widget',
|
|
334
|
+
widgetType: 'e-form-input',
|
|
335
|
+
elements: [],
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
'e-form-error-message': {
|
|
340
|
+
title: 'Error',
|
|
341
|
+
controls: {},
|
|
342
|
+
elType: 'e-form-error-message',
|
|
343
|
+
},
|
|
344
|
+
'e-form-input': { title: 'Input', controls: {}, elType: 'widget' },
|
|
345
|
+
'e-form-success-message': {
|
|
346
|
+
title: 'Success',
|
|
347
|
+
controls: {},
|
|
348
|
+
elType: 'e-form-success-message',
|
|
349
|
+
},
|
|
350
|
+
} as Record< string, V1ElementConfig >;
|
|
351
|
+
const builder = CompositionBuilder.fromXMLString(
|
|
352
|
+
'<e-form configuration-id="form-1">' +
|
|
353
|
+
'<e-form-success-message /><e-form-error-message /><e-form-input />' +
|
|
354
|
+
'</e-form>',
|
|
355
|
+
{
|
|
356
|
+
createElement: createElementMock,
|
|
357
|
+
deleteElement: jest.fn(),
|
|
358
|
+
getContainer: jest.fn(),
|
|
359
|
+
generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
|
|
360
|
+
getWidgetsCache: jest.fn().mockReturnValue( formWidgetsCache ),
|
|
361
|
+
doUpdateElementProperty: jest.fn(),
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// Act
|
|
366
|
+
await builder.build( createMockRootContainer() );
|
|
367
|
+
|
|
368
|
+
// Assert
|
|
369
|
+
const createArgs = createElementMock.mock.calls[ 0 ]?.[ 0 ] as CreateElementParams;
|
|
370
|
+
const childElements = ( createArgs.model?.elements || [] ) as Array< { elType?: string; widgetType?: string } >;
|
|
371
|
+
|
|
372
|
+
expect( childElements.filter( ( child ) => child.elType === 'e-form-success-message' ).length ).toBe( 1 );
|
|
373
|
+
expect( childElements.filter( ( child ) => child.elType === 'e-form-error-message' ).length ).toBe( 1 );
|
|
374
|
+
expect( childElements.some( ( child ) => child.widgetType === 'e-form-input' ) ).toBe( true );
|
|
375
|
+
} );
|
|
376
|
+
} );
|
|
@@ -13,6 +13,7 @@ import { type z } from '@elementor/schema';
|
|
|
13
13
|
|
|
14
14
|
import { doUpdateElementProperty } from '../mcp/utils/do-update-element-property';
|
|
15
15
|
import { validateInput } from '../mcp/utils/validate-input';
|
|
16
|
+
import { RequiredChildrenEnforcer } from './utils/required-children-enforcer';
|
|
16
17
|
|
|
17
18
|
type AnyValue = z.infer< z.ZodTypeAny >;
|
|
18
19
|
type AnyConfig = Record< string, Record< string, AnyValue > >;
|
|
@@ -280,6 +281,11 @@ export class CompositionBuilder {
|
|
|
280
281
|
}
|
|
281
282
|
} );
|
|
282
283
|
|
|
284
|
+
Object.keys( widgetsCache ).forEach( ( elementType ) => {
|
|
285
|
+
const requiredChildrenEnforcer = new RequiredChildrenEnforcer( elementType, widgetsCache );
|
|
286
|
+
requiredChildrenEnforcer.enforce( this.xml );
|
|
287
|
+
} );
|
|
288
|
+
|
|
283
289
|
const childTypeErrors: string[] = [];
|
|
284
290
|
for ( const rootChild of Array.from( this.xml.children ) ) {
|
|
285
291
|
childTypeErrors.push( ...this.validateChildTypes( rootChild, widgetsCache ) );
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
import { RequiredChildrenEnforcer } from '../required-children-enforcer';
|
|
4
|
+
|
|
5
|
+
describe( 'RequiredChildrenEnforcer', () => {
|
|
6
|
+
const FULL_FORM_DIRECT_CHILD_COUNT = 3;
|
|
7
|
+
|
|
8
|
+
const createWidgetsCache = (): Record< string, V1ElementConfig > => ( {
|
|
9
|
+
'e-form': {
|
|
10
|
+
title: 'Form',
|
|
11
|
+
controls: {},
|
|
12
|
+
elType: 'widget',
|
|
13
|
+
default_children: [
|
|
14
|
+
{
|
|
15
|
+
elType: 'e-form-success-message',
|
|
16
|
+
meta: { required: true },
|
|
17
|
+
elements: [],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
elType: 'e-form-error-message',
|
|
21
|
+
meta: { required: true },
|
|
22
|
+
elements: [],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
elType: 'widget',
|
|
26
|
+
widgetType: 'e-form-input',
|
|
27
|
+
elements: [],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
} as V1ElementConfig,
|
|
31
|
+
'e-form-input': { title: 'Input', controls: {}, elType: 'widget' } as V1ElementConfig,
|
|
32
|
+
'e-form-success-message': {
|
|
33
|
+
title: 'Success',
|
|
34
|
+
controls: {},
|
|
35
|
+
elType: 'e-form-success-message',
|
|
36
|
+
} as V1ElementConfig,
|
|
37
|
+
'e-form-error-message': {
|
|
38
|
+
title: 'Error',
|
|
39
|
+
controls: {},
|
|
40
|
+
elType: 'e-form-error-message',
|
|
41
|
+
} as V1ElementConfig,
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
it( 'throws when required direct children are missing', () => {
|
|
45
|
+
// Arrange
|
|
46
|
+
const xml = new DOMParser().parseFromString( '<e-form><e-form-input /></e-form>', 'application/xml' );
|
|
47
|
+
const enforcer = new RequiredChildrenEnforcer( 'e-form', createWidgetsCache() );
|
|
48
|
+
|
|
49
|
+
// Act & Assert
|
|
50
|
+
expect( () => enforcer.enforce( xml ) ).toThrow(
|
|
51
|
+
/Missing required direct child element tag\(s\): e-form-success-message, e-form-error-message/
|
|
52
|
+
);
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
it( 'throws when only some required direct children exist', () => {
|
|
56
|
+
// Arrange
|
|
57
|
+
const xml = new DOMParser().parseFromString( '<e-form><e-form-success-message /></e-form>', 'application/xml' );
|
|
58
|
+
const enforcer = new RequiredChildrenEnforcer( 'e-form', createWidgetsCache() );
|
|
59
|
+
|
|
60
|
+
// Act & Assert
|
|
61
|
+
expect( () => enforcer.enforce( xml ) ).toThrow(
|
|
62
|
+
/Missing required direct child element tag\(s\): e-form-error-message/
|
|
63
|
+
);
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
it( 'does not throw when all required direct children exist', () => {
|
|
67
|
+
// Arrange
|
|
68
|
+
const xmlStr = '<e-form><e-form-success-message /><e-form-error-message /><e-form-input /></e-form>';
|
|
69
|
+
const xml = new DOMParser().parseFromString( xmlStr, 'application/xml' );
|
|
70
|
+
const enforcer = new RequiredChildrenEnforcer( 'e-form', createWidgetsCache() );
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
expect( () => enforcer.enforce( xml ) ).not.toThrow();
|
|
74
|
+
|
|
75
|
+
// Assert
|
|
76
|
+
const form = xml.querySelector( 'e-form' );
|
|
77
|
+
expect( form?.children.length ).toBe( FULL_FORM_DIRECT_CHILD_COUNT );
|
|
78
|
+
} );
|
|
79
|
+
} );
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
import { getRequiredDefaultChildTagNames, resolveDefaultChildTemplateTagName } from '../required-default-child-tags';
|
|
4
|
+
|
|
5
|
+
describe( 'required-default-child-tags', () => {
|
|
6
|
+
it( 'returns XML tag names for default children marked meta.required', () => {
|
|
7
|
+
// Arrange
|
|
8
|
+
const config = {
|
|
9
|
+
title: 'Form',
|
|
10
|
+
controls: {},
|
|
11
|
+
default_children: [
|
|
12
|
+
{
|
|
13
|
+
elType: 'e-form-success-message',
|
|
14
|
+
meta: { required: true },
|
|
15
|
+
elements: [],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
elType: 'widget',
|
|
19
|
+
widgetType: 'e-form-input',
|
|
20
|
+
meta: { required: true },
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
elType: 'e-form-label',
|
|
24
|
+
elements: [],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
} as unknown as V1ElementConfig;
|
|
28
|
+
|
|
29
|
+
// Act
|
|
30
|
+
const tags = getRequiredDefaultChildTagNames( config );
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect( tags ).toEqual( [ 'e-form-success-message', 'e-form-input' ] );
|
|
34
|
+
} );
|
|
35
|
+
|
|
36
|
+
it( 'resolveDefaultChildTemplateTagName uses widgetType when elType is widget', () => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const template = { elType: 'widget', widgetType: 'e-form-submit-button' };
|
|
39
|
+
|
|
40
|
+
// Act
|
|
41
|
+
const tag = resolveDefaultChildTemplateTagName( template );
|
|
42
|
+
|
|
43
|
+
// Assert
|
|
44
|
+
expect( tag ).toBe( 'e-form-submit-button' );
|
|
45
|
+
} );
|
|
46
|
+
} );
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type DefaultChildTemplate,
|
|
5
|
+
getRequiredDefaultChildTemplates,
|
|
6
|
+
resolveDefaultChildTemplateTagName,
|
|
7
|
+
} from './required-default-child-tags';
|
|
8
|
+
|
|
9
|
+
const REQUIRED_CHILD_SCHEMA_HINT =
|
|
10
|
+
'Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.';
|
|
11
|
+
|
|
12
|
+
export class RequiredChildrenEnforcer {
|
|
13
|
+
private readonly elementType: string;
|
|
14
|
+
private readonly requiredTemplates: DefaultChildTemplate[];
|
|
15
|
+
|
|
16
|
+
constructor( elementType: string, widgetsCache: Record< string, V1ElementConfig > ) {
|
|
17
|
+
this.elementType = elementType;
|
|
18
|
+
this.requiredTemplates = getRequiredDefaultChildTemplates( widgetsCache[ elementType ] );
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
enforce( xml: Document ) {
|
|
22
|
+
if ( this.requiredTemplates.length === 0 ) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const errors: string[] = [];
|
|
27
|
+
|
|
28
|
+
for ( const rootNode of Array.from( xml.children ) ) {
|
|
29
|
+
this.collectMissingRequiredErrors( rootNode, errors );
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if ( errors.length ) {
|
|
33
|
+
throw new Error( `${ errors.join( '\n' ) }\n${ REQUIRED_CHILD_SCHEMA_HINT }` );
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private collectMissingRequiredErrors( node: Element, errors: string[] ) {
|
|
38
|
+
if ( node.tagName === this.elementType ) {
|
|
39
|
+
const existingChildTags = new Set( Array.from( node.children ).map( ( child ) => child.tagName ) );
|
|
40
|
+
const missingTags = this.requiredTemplates
|
|
41
|
+
.map( resolveDefaultChildTemplateTagName )
|
|
42
|
+
.filter( ( tag ) => tag && ! existingChildTags.has( tag ) ) as string[];
|
|
43
|
+
|
|
44
|
+
if ( missingTags.length ) {
|
|
45
|
+
const configurationId = node.getAttribute( 'configuration-id' );
|
|
46
|
+
const location = configurationId
|
|
47
|
+
? `<${ node.tagName } configuration-id="${ configurationId }">`
|
|
48
|
+
: `<${ node.tagName }>`;
|
|
49
|
+
|
|
50
|
+
errors.push(
|
|
51
|
+
`${ location } Missing required direct child element tag(s): ${ missingTags.join( ', ' ) }.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for ( const childNode of Array.from( node.children ) ) {
|
|
57
|
+
this.collectMissingRequiredErrors( childNode, errors );
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
export type DefaultChildTemplate = Record< string, unknown >;
|
|
4
|
+
|
|
5
|
+
export function resolveDefaultChildTemplateTagName( template: DefaultChildTemplate ): string {
|
|
6
|
+
const elementType = template.elType;
|
|
7
|
+
|
|
8
|
+
if ( elementType === 'widget' ) {
|
|
9
|
+
return typeof template.widgetType === 'string' ? template.widgetType : '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return typeof elementType === 'string' ? elementType : '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getRequiredDefaultChildTemplates( elementConfig: V1ElementConfig | undefined ): DefaultChildTemplate[] {
|
|
16
|
+
const defaultChildren = elementConfig?.default_children;
|
|
17
|
+
|
|
18
|
+
if ( ! Array.isArray( defaultChildren ) ) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return defaultChildren.filter(
|
|
23
|
+
( child ): child is DefaultChildTemplate => !! ( child as { meta?: { required?: boolean } } )?.meta?.required
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getRequiredDefaultChildTagNames( elementConfig: V1ElementConfig | undefined ): string[] {
|
|
28
|
+
return getRequiredDefaultChildTemplates( elementConfig )
|
|
29
|
+
.map( resolveDefaultChildTemplateTagName )
|
|
30
|
+
.filter( ( tag ) => Boolean( tag ) );
|
|
31
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '@elementor/editor-props';
|
|
12
12
|
import { getStylesSchema } from '@elementor/editor-styles';
|
|
13
13
|
|
|
14
|
+
import { getRequiredDefaultChildTagNames } from '../../composition-builder/utils/required-default-child-tags';
|
|
14
15
|
import { hasV3Controls, isWidgetAvailableForLLM } from '../utils/element-data-util';
|
|
15
16
|
|
|
16
17
|
const V3_LAYOUT_CONTROL_TYPES = new Set( [ 'section', 'tab', 'tabs' ] );
|
|
@@ -222,6 +223,11 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
|
|
|
222
223
|
};
|
|
223
224
|
}
|
|
224
225
|
|
|
226
|
+
const requiredDirectChildTags = getRequiredDefaultChildTagNames( widgetData );
|
|
227
|
+
if ( requiredDirectChildTags.length ) {
|
|
228
|
+
llmGuidance.required_direct_children = requiredDirectChildTags;
|
|
229
|
+
}
|
|
230
|
+
|
|
225
231
|
return {
|
|
226
232
|
contents: [
|
|
227
233
|
{
|
|
@@ -30,6 +30,7 @@ This tool support v4 elements only
|
|
|
30
30
|
## NESTED ELEMENTS
|
|
31
31
|
Some elements have internal tree structures (nesting). When using these elements, you MUST build the FULL tree in XML.
|
|
32
32
|
- Check \`llm_guidance.nesting\` in widget schemas for structure requirements
|
|
33
|
+
- \`llm_guidance.required_direct_children\` lists element types that must appear as direct child tags in XML (from widget defaults)
|
|
33
34
|
- \`allowed_child_types\` lists which element types can be nested inside
|
|
34
35
|
- \`allowed_parents\` lists which element types this element can be placed inside
|
|
35
36
|
|