@elementor/editor-canvas 4.2.0-881 → 4.2.0-883
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 +63 -0
- package/dist/index.mjs +63 -0
- package/package.json +18 -18
- package/src/composition-builder/__tests__/composition-builder.test.ts +118 -1
- package/src/composition-builder/composition-builder.ts +10 -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 +35 -0
- package/src/composition-builder/utils/required-children-enforcer.ts +56 -0
- package/src/composition-builder/utils/required-default-child-tags.ts +22 -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
|
@@ -111,6 +111,18 @@ var import_editor_mcp = require("@elementor/editor-mcp");
|
|
|
111
111
|
var import_editor_props = require("@elementor/editor-props");
|
|
112
112
|
var import_editor_styles = require("@elementor/editor-styles");
|
|
113
113
|
|
|
114
|
+
// src/composition-builder/utils/required-default-child-tags.ts
|
|
115
|
+
function getRequiredDefaultChildTemplates(elementConfig) {
|
|
116
|
+
const defaultChildren = elementConfig?.default_children;
|
|
117
|
+
if (!Array.isArray(defaultChildren)) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
return defaultChildren.filter((child) => child?.meta?.required ?? false);
|
|
121
|
+
}
|
|
122
|
+
function getRequiredDefaultChildTypes(elementConfig) {
|
|
123
|
+
return getRequiredDefaultChildTemplates(elementConfig).map((child) => child.widgetType ?? child.elType ?? "").filter((type) => Boolean(type));
|
|
124
|
+
}
|
|
125
|
+
|
|
114
126
|
// src/mcp/utils/element-data-util.ts
|
|
115
127
|
var import_editor_elements = require("@elementor/editor-elements");
|
|
116
128
|
function hasV3Controls(controls) {
|
|
@@ -327,6 +339,10 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
|
|
|
327
339
|
...allowedParents.length ? { allowed_parents: allowedParents } : {}
|
|
328
340
|
};
|
|
329
341
|
}
|
|
342
|
+
const requiredDirectChildTags = getRequiredDefaultChildTypes(widgetData);
|
|
343
|
+
if (requiredDirectChildTags.length) {
|
|
344
|
+
llmGuidance.required_direct_children = requiredDirectChildTags;
|
|
345
|
+
}
|
|
330
346
|
return {
|
|
331
347
|
contents: [
|
|
332
348
|
{
|
|
@@ -4114,6 +4130,46 @@ var validateInput = {
|
|
|
4114
4130
|
}
|
|
4115
4131
|
};
|
|
4116
4132
|
|
|
4133
|
+
// src/composition-builder/utils/required-children-enforcer.ts
|
|
4134
|
+
var REQUIRED_CHILD_SCHEMA_HINT = "Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.";
|
|
4135
|
+
var RequiredChildrenEnforcer = class {
|
|
4136
|
+
elementType;
|
|
4137
|
+
requiredTemplates;
|
|
4138
|
+
constructor(elementType, widgetsCache) {
|
|
4139
|
+
this.elementType = elementType;
|
|
4140
|
+
this.requiredTemplates = getRequiredDefaultChildTemplates(widgetsCache[elementType]);
|
|
4141
|
+
}
|
|
4142
|
+
enforce(xml) {
|
|
4143
|
+
if (this.requiredTemplates.length === 0) {
|
|
4144
|
+
return;
|
|
4145
|
+
}
|
|
4146
|
+
const errors = [];
|
|
4147
|
+
for (const rootNode of Array.from(xml.children)) {
|
|
4148
|
+
this.collectMissingRequiredErrors(rootNode, errors);
|
|
4149
|
+
}
|
|
4150
|
+
if (errors.length) {
|
|
4151
|
+
throw new Error(`${errors.join("\n")}
|
|
4152
|
+
${REQUIRED_CHILD_SCHEMA_HINT}`);
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
collectMissingRequiredErrors(node, errors) {
|
|
4156
|
+
if (node.tagName === this.elementType) {
|
|
4157
|
+
const existingChildTags = new Set(Array.from(node.children).map((child) => child.tagName));
|
|
4158
|
+
const missingTags = this.requiredTemplates.map((child) => child.widgetType ?? child.elType ?? "").filter((type) => type && !existingChildTags.has(type));
|
|
4159
|
+
if (missingTags.length) {
|
|
4160
|
+
const configurationId = node.getAttribute("configuration-id");
|
|
4161
|
+
const location2 = configurationId ? `<${node.tagName} configuration-id="${configurationId}">` : `<${node.tagName}>`;
|
|
4162
|
+
errors.push(
|
|
4163
|
+
`${location2} Missing required direct child element tag(s): ${missingTags.join(", ")}.`
|
|
4164
|
+
);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
for (const childNode of Array.from(node.children)) {
|
|
4168
|
+
this.collectMissingRequiredErrors(childNode, errors);
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
};
|
|
4172
|
+
|
|
4117
4173
|
// src/composition-builder/composition-builder.ts
|
|
4118
4174
|
var CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE = "createElement did not return an element container with a model.";
|
|
4119
4175
|
var CompositionBuilder = class _CompositionBuilder {
|
|
@@ -4317,6 +4373,12 @@ var CompositionBuilder = class _CompositionBuilder {
|
|
|
4317
4373
|
throw new Error(`Unknown widget type: ${node.tagName}`);
|
|
4318
4374
|
}
|
|
4319
4375
|
});
|
|
4376
|
+
const typesWithRequiredChildren = Object.keys(widgetsCache).filter(
|
|
4377
|
+
(elementType) => getRequiredDefaultChildTemplates(widgetsCache[elementType]).length > 0
|
|
4378
|
+
);
|
|
4379
|
+
typesWithRequiredChildren.forEach((elementType) => {
|
|
4380
|
+
new RequiredChildrenEnforcer(elementType, widgetsCache).enforce(this.xml);
|
|
4381
|
+
});
|
|
4320
4382
|
const childTypeErrors = [];
|
|
4321
4383
|
for (const rootChild of Array.from(this.xml.children)) {
|
|
4322
4384
|
childTypeErrors.push(...this.validateChildTypes(rootChild, widgetsCache));
|
|
@@ -4395,6 +4457,7 @@ This tool support v4 elements only
|
|
|
4395
4457
|
## NESTED ELEMENTS
|
|
4396
4458
|
Some elements have internal tree structures (nesting). When using these elements, you MUST build the FULL tree in XML.
|
|
4397
4459
|
- Check \`llm_guidance.nesting\` in widget schemas for structure requirements
|
|
4460
|
+
- \`llm_guidance.required_direct_children\` lists element types that must appear as direct child tags in XML (from widget defaults)
|
|
4398
4461
|
- \`allowed_child_types\` lists which element types can be nested inside
|
|
4399
4462
|
- \`allowed_parents\` lists which element types this element can be placed inside
|
|
4400
4463
|
|
package/dist/index.mjs
CHANGED
|
@@ -52,6 +52,18 @@ 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 getRequiredDefaultChildTemplates(elementConfig) {
|
|
57
|
+
const defaultChildren = elementConfig?.default_children;
|
|
58
|
+
if (!Array.isArray(defaultChildren)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return defaultChildren.filter((child) => child?.meta?.required ?? false);
|
|
62
|
+
}
|
|
63
|
+
function getRequiredDefaultChildTypes(elementConfig) {
|
|
64
|
+
return getRequiredDefaultChildTemplates(elementConfig).map((child) => child.widgetType ?? child.elType ?? "").filter((type) => Boolean(type));
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
// src/mcp/utils/element-data-util.ts
|
|
56
68
|
import { getWidgetsCache } from "@elementor/editor-elements";
|
|
57
69
|
function hasV3Controls(controls) {
|
|
@@ -268,6 +280,10 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
|
|
|
268
280
|
...allowedParents.length ? { allowed_parents: allowedParents } : {}
|
|
269
281
|
};
|
|
270
282
|
}
|
|
283
|
+
const requiredDirectChildTags = getRequiredDefaultChildTypes(widgetData);
|
|
284
|
+
if (requiredDirectChildTags.length) {
|
|
285
|
+
llmGuidance.required_direct_children = requiredDirectChildTags;
|
|
286
|
+
}
|
|
271
287
|
return {
|
|
272
288
|
contents: [
|
|
273
289
|
{
|
|
@@ -4107,6 +4123,46 @@ var validateInput = {
|
|
|
4107
4123
|
}
|
|
4108
4124
|
};
|
|
4109
4125
|
|
|
4126
|
+
// src/composition-builder/utils/required-children-enforcer.ts
|
|
4127
|
+
var REQUIRED_CHILD_SCHEMA_HINT = "Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.";
|
|
4128
|
+
var RequiredChildrenEnforcer = class {
|
|
4129
|
+
elementType;
|
|
4130
|
+
requiredTemplates;
|
|
4131
|
+
constructor(elementType, widgetsCache) {
|
|
4132
|
+
this.elementType = elementType;
|
|
4133
|
+
this.requiredTemplates = getRequiredDefaultChildTemplates(widgetsCache[elementType]);
|
|
4134
|
+
}
|
|
4135
|
+
enforce(xml) {
|
|
4136
|
+
if (this.requiredTemplates.length === 0) {
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
4139
|
+
const errors = [];
|
|
4140
|
+
for (const rootNode of Array.from(xml.children)) {
|
|
4141
|
+
this.collectMissingRequiredErrors(rootNode, errors);
|
|
4142
|
+
}
|
|
4143
|
+
if (errors.length) {
|
|
4144
|
+
throw new Error(`${errors.join("\n")}
|
|
4145
|
+
${REQUIRED_CHILD_SCHEMA_HINT}`);
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
collectMissingRequiredErrors(node, errors) {
|
|
4149
|
+
if (node.tagName === this.elementType) {
|
|
4150
|
+
const existingChildTags = new Set(Array.from(node.children).map((child) => child.tagName));
|
|
4151
|
+
const missingTags = this.requiredTemplates.map((child) => child.widgetType ?? child.elType ?? "").filter((type) => type && !existingChildTags.has(type));
|
|
4152
|
+
if (missingTags.length) {
|
|
4153
|
+
const configurationId = node.getAttribute("configuration-id");
|
|
4154
|
+
const location2 = configurationId ? `<${node.tagName} configuration-id="${configurationId}">` : `<${node.tagName}>`;
|
|
4155
|
+
errors.push(
|
|
4156
|
+
`${location2} Missing required direct child element tag(s): ${missingTags.join(", ")}.`
|
|
4157
|
+
);
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
for (const childNode of Array.from(node.children)) {
|
|
4161
|
+
this.collectMissingRequiredErrors(childNode, errors);
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
};
|
|
4165
|
+
|
|
4110
4166
|
// src/composition-builder/composition-builder.ts
|
|
4111
4167
|
var CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE = "createElement did not return an element container with a model.";
|
|
4112
4168
|
var CompositionBuilder = class _CompositionBuilder {
|
|
@@ -4310,6 +4366,12 @@ var CompositionBuilder = class _CompositionBuilder {
|
|
|
4310
4366
|
throw new Error(`Unknown widget type: ${node.tagName}`);
|
|
4311
4367
|
}
|
|
4312
4368
|
});
|
|
4369
|
+
const typesWithRequiredChildren = Object.keys(widgetsCache).filter(
|
|
4370
|
+
(elementType) => getRequiredDefaultChildTemplates(widgetsCache[elementType]).length > 0
|
|
4371
|
+
);
|
|
4372
|
+
typesWithRequiredChildren.forEach((elementType) => {
|
|
4373
|
+
new RequiredChildrenEnforcer(elementType, widgetsCache).enforce(this.xml);
|
|
4374
|
+
});
|
|
4313
4375
|
const childTypeErrors = [];
|
|
4314
4376
|
for (const rootChild of Array.from(this.xml.children)) {
|
|
4315
4377
|
childTypeErrors.push(...this.validateChildTypes(rootChild, widgetsCache));
|
|
@@ -4388,6 +4450,7 @@ This tool support v4 elements only
|
|
|
4388
4450
|
## NESTED ELEMENTS
|
|
4389
4451
|
Some elements have internal tree structures (nesting). When using these elements, you MUST build the FULL tree in XML.
|
|
4390
4452
|
- Check \`llm_guidance.nesting\` in widget schemas for structure requirements
|
|
4453
|
+
- \`llm_guidance.required_direct_children\` lists element types that must appear as direct child tags in XML (from widget defaults)
|
|
4391
4454
|
- \`allowed_child_types\` lists which element types can be nested inside
|
|
4392
4455
|
- \`allowed_parents\` lists which element types this element can be placed inside
|
|
4393
4456
|
|
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-883",
|
|
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-883",
|
|
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-883",
|
|
43
|
+
"@elementor/editor-documents": "4.2.0-883",
|
|
44
|
+
"@elementor/editor-elements": "4.2.0-883",
|
|
45
|
+
"@elementor/editor-interactions": "4.2.0-883",
|
|
46
|
+
"@elementor/editor-mcp": "4.2.0-883",
|
|
47
|
+
"@elementor/editor-notifications": "4.2.0-883",
|
|
48
|
+
"@elementor/editor-props": "4.2.0-883",
|
|
49
|
+
"@elementor/editor-responsive": "4.2.0-883",
|
|
50
|
+
"@elementor/editor-styles": "4.2.0-883",
|
|
51
|
+
"@elementor/editor-styles-repository": "4.2.0-883",
|
|
52
|
+
"@elementor/editor-ui": "4.2.0-883",
|
|
53
|
+
"@elementor/editor-v1-adapters": "4.2.0-883",
|
|
54
|
+
"@elementor/schema": "4.2.0-883",
|
|
55
|
+
"@elementor/twing": "4.2.0-883",
|
|
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-883",
|
|
58
|
+
"@elementor/wp-media": "4.2.0-883",
|
|
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
|
|
|
@@ -23,6 +23,42 @@ const createMinimalWidgetsCache = () =>
|
|
|
23
23
|
},
|
|
24
24
|
} ) as Record< string, { elType: string } >;
|
|
25
25
|
|
|
26
|
+
const FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN = {
|
|
27
|
+
'e-form': {
|
|
28
|
+
title: 'Form',
|
|
29
|
+
controls: {},
|
|
30
|
+
elType: 'widget',
|
|
31
|
+
default_children: [
|
|
32
|
+
{
|
|
33
|
+
elType: 'e-form-success-message',
|
|
34
|
+
meta: { required: true },
|
|
35
|
+
elements: [],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
elType: 'e-form-error-message',
|
|
39
|
+
meta: { required: true },
|
|
40
|
+
elements: [],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
elType: 'widget',
|
|
44
|
+
widgetType: 'e-form-input',
|
|
45
|
+
elements: [],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
'e-form-error-message': {
|
|
50
|
+
title: 'Error',
|
|
51
|
+
controls: {},
|
|
52
|
+
elType: 'e-form-error-message',
|
|
53
|
+
},
|
|
54
|
+
'e-form-input': { title: 'Input', controls: {}, elType: 'widget' },
|
|
55
|
+
'e-form-success-message': {
|
|
56
|
+
title: 'Success',
|
|
57
|
+
controls: {},
|
|
58
|
+
elType: 'e-form-success-message',
|
|
59
|
+
},
|
|
60
|
+
} as const satisfies Record< string, V1ElementConfig >;
|
|
61
|
+
|
|
26
62
|
const createMockRootContainer = (): V1Element =>
|
|
27
63
|
( {
|
|
28
64
|
id: 'root',
|
|
@@ -188,3 +224,84 @@ describe( 'CompositionBuilder.build applyProperties after create', () => {
|
|
|
188
224
|
expect( doUpdateElementProperty ).not.toHaveBeenCalled();
|
|
189
225
|
} );
|
|
190
226
|
} );
|
|
227
|
+
|
|
228
|
+
describe( 'CompositionBuilder.build required children', () => {
|
|
229
|
+
it( 'rejects build when required direct children are absent from XML', async () => {
|
|
230
|
+
// Arrange
|
|
231
|
+
let elementIdSequence = 0;
|
|
232
|
+
const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
|
|
233
|
+
const createElementMock = jest.fn().mockReturnValue( createdElement );
|
|
234
|
+
const builder = CompositionBuilder.fromXMLString(
|
|
235
|
+
'<e-form configuration-id="form-1"><e-form-input /></e-form>',
|
|
236
|
+
{
|
|
237
|
+
createElement: createElementMock,
|
|
238
|
+
deleteElement: jest.fn(),
|
|
239
|
+
getContainer: jest.fn(),
|
|
240
|
+
generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
|
|
241
|
+
getWidgetsCache: jest.fn().mockReturnValue( FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN ),
|
|
242
|
+
doUpdateElementProperty: jest.fn(),
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Act & Assert
|
|
247
|
+
await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow(
|
|
248
|
+
/Missing required direct child element tag\(s\): e-form-success-message, e-form-error-message/
|
|
249
|
+
);
|
|
250
|
+
expect( createElementMock ).not.toHaveBeenCalled();
|
|
251
|
+
} );
|
|
252
|
+
|
|
253
|
+
it( 'rejects build when only some required direct children exist', async () => {
|
|
254
|
+
// Arrange
|
|
255
|
+
let elementIdSequence = 0;
|
|
256
|
+
const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
|
|
257
|
+
const createElementMock = jest.fn().mockReturnValue( createdElement );
|
|
258
|
+
const builder = CompositionBuilder.fromXMLString(
|
|
259
|
+
'<e-form configuration-id="form-1"><e-form-success-message /><e-form-input /></e-form>',
|
|
260
|
+
{
|
|
261
|
+
createElement: createElementMock,
|
|
262
|
+
deleteElement: jest.fn(),
|
|
263
|
+
getContainer: jest.fn(),
|
|
264
|
+
generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
|
|
265
|
+
getWidgetsCache: jest.fn().mockReturnValue( FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN ),
|
|
266
|
+
doUpdateElementProperty: jest.fn(),
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Act & Assert
|
|
271
|
+
await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow(
|
|
272
|
+
/Missing required direct child element tag\(s\): e-form-error-message/
|
|
273
|
+
);
|
|
274
|
+
expect( createElementMock ).not.toHaveBeenCalled();
|
|
275
|
+
} );
|
|
276
|
+
|
|
277
|
+
it( 'creates elements when XML includes all required direct children', async () => {
|
|
278
|
+
// Arrange
|
|
279
|
+
let elementIdSequence = 0;
|
|
280
|
+
const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
|
|
281
|
+
const createElementMock = jest.fn().mockReturnValue( createdElement );
|
|
282
|
+
const builder = CompositionBuilder.fromXMLString(
|
|
283
|
+
'<e-form configuration-id="form-1">' +
|
|
284
|
+
'<e-form-success-message /><e-form-error-message /><e-form-input />' +
|
|
285
|
+
'</e-form>',
|
|
286
|
+
{
|
|
287
|
+
createElement: createElementMock,
|
|
288
|
+
deleteElement: jest.fn(),
|
|
289
|
+
getContainer: jest.fn(),
|
|
290
|
+
generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
|
|
291
|
+
getWidgetsCache: jest.fn().mockReturnValue( FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN ),
|
|
292
|
+
doUpdateElementProperty: jest.fn(),
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Act
|
|
297
|
+
await builder.build( createMockRootContainer() );
|
|
298
|
+
|
|
299
|
+
// Assert
|
|
300
|
+
const createArgs = createElementMock.mock.calls[ 0 ]?.[ 0 ] as CreateElementParams;
|
|
301
|
+
const childElements = ( createArgs.model?.elements || [] ) as Array< { elType?: string; widgetType?: string } >;
|
|
302
|
+
|
|
303
|
+
expect( childElements.filter( ( child ) => child.elType === 'e-form-success-message' ).length ).toBe( 1 );
|
|
304
|
+
expect( childElements.filter( ( child ) => child.elType === 'e-form-error-message' ).length ).toBe( 1 );
|
|
305
|
+
expect( childElements.some( ( child ) => child.widgetType === 'e-form-input' ) ).toBe( true );
|
|
306
|
+
} );
|
|
307
|
+
} );
|
|
@@ -13,6 +13,8 @@ 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';
|
|
17
|
+
import { getRequiredDefaultChildTemplates } from './utils/required-default-child-tags';
|
|
16
18
|
|
|
17
19
|
type AnyValue = z.infer< z.ZodTypeAny >;
|
|
18
20
|
type AnyConfig = Record< string, Record< string, AnyValue > >;
|
|
@@ -280,6 +282,14 @@ export class CompositionBuilder {
|
|
|
280
282
|
}
|
|
281
283
|
} );
|
|
282
284
|
|
|
285
|
+
const typesWithRequiredChildren = Object.keys( widgetsCache ).filter(
|
|
286
|
+
( elementType ) => getRequiredDefaultChildTemplates( widgetsCache[ elementType ] ).length > 0
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
typesWithRequiredChildren.forEach( ( elementType ) => {
|
|
290
|
+
new RequiredChildrenEnforcer( elementType, widgetsCache ).enforce( this.xml );
|
|
291
|
+
} );
|
|
292
|
+
|
|
283
293
|
const childTypeErrors: string[] = [];
|
|
284
294
|
for ( const rootChild of Array.from( this.xml.children ) ) {
|
|
285
295
|
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,35 @@
|
|
|
1
|
+
import { type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
import { getRequiredDefaultChildTypes } 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 = getRequiredDefaultChildTypes( config );
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect( tags ).toEqual( [ 'e-form-success-message', 'e-form-input' ] );
|
|
34
|
+
} );
|
|
35
|
+
} );
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
import { type ChildTemplate, getRequiredDefaultChildTemplates } from './required-default-child-tags';
|
|
4
|
+
|
|
5
|
+
const REQUIRED_CHILD_SCHEMA_HINT =
|
|
6
|
+
'Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.';
|
|
7
|
+
|
|
8
|
+
export class RequiredChildrenEnforcer {
|
|
9
|
+
private readonly elementType: string;
|
|
10
|
+
private readonly requiredTemplates: ChildTemplate[];
|
|
11
|
+
|
|
12
|
+
constructor( elementType: string, widgetsCache: Record< string, V1ElementConfig > ) {
|
|
13
|
+
this.elementType = elementType;
|
|
14
|
+
this.requiredTemplates = getRequiredDefaultChildTemplates( widgetsCache[ elementType ] );
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
enforce( xml: Document ) {
|
|
18
|
+
if ( this.requiredTemplates.length === 0 ) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const errors: string[] = [];
|
|
23
|
+
|
|
24
|
+
for ( const rootNode of Array.from( xml.children ) ) {
|
|
25
|
+
this.collectMissingRequiredErrors( rootNode, errors );
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if ( errors.length ) {
|
|
29
|
+
throw new Error( `${ errors.join( '\n' ) }\n${ REQUIRED_CHILD_SCHEMA_HINT }` );
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private collectMissingRequiredErrors( node: Element, errors: string[] ) {
|
|
34
|
+
if ( node.tagName === this.elementType ) {
|
|
35
|
+
const existingChildTags = new Set( Array.from( node.children ).map( ( child ) => child.tagName ) );
|
|
36
|
+
const missingTags = this.requiredTemplates
|
|
37
|
+
.map( ( child ) => child.widgetType ?? child.elType ?? '' )
|
|
38
|
+
.filter( ( type ) => type && ! existingChildTags.has( type ) ) as string[];
|
|
39
|
+
|
|
40
|
+
if ( missingTags.length ) {
|
|
41
|
+
const configurationId = node.getAttribute( 'configuration-id' );
|
|
42
|
+
const location = configurationId
|
|
43
|
+
? `<${ node.tagName } configuration-id="${ configurationId }">`
|
|
44
|
+
: `<${ node.tagName }>`;
|
|
45
|
+
|
|
46
|
+
errors.push(
|
|
47
|
+
`${ location } Missing required direct child element tag(s): ${ missingTags.join( ', ' ) }.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for ( const childNode of Array.from( node.children ) ) {
|
|
53
|
+
this.collectMissingRequiredErrors( childNode, errors );
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
export type ChildTemplate = {
|
|
4
|
+
widgetType?: string;
|
|
5
|
+
elType?: string;
|
|
6
|
+
meta?: { required?: boolean };
|
|
7
|
+
};
|
|
8
|
+
export function getRequiredDefaultChildTemplates( elementConfig: V1ElementConfig | undefined ): ChildTemplate[] {
|
|
9
|
+
const defaultChildren = elementConfig?.default_children as ChildTemplate[];
|
|
10
|
+
|
|
11
|
+
if ( ! Array.isArray( defaultChildren ) ) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return defaultChildren.filter( ( child ) => child?.meta?.required ?? false );
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getRequiredDefaultChildTypes( elementConfig: V1ElementConfig | undefined ): string[] {
|
|
19
|
+
return getRequiredDefaultChildTemplates( elementConfig )
|
|
20
|
+
.map( ( child ) => child.widgetType ?? child.elType ?? '' )
|
|
21
|
+
.filter( ( type ) => Boolean( type ) );
|
|
22
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '@elementor/editor-props';
|
|
12
12
|
import { getStylesSchema } from '@elementor/editor-styles';
|
|
13
13
|
|
|
14
|
+
import { getRequiredDefaultChildTypes } 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 = getRequiredDefaultChildTypes( 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
|
|